Node.js Spot And Instance Spot Public Interface¶
Interface table of contents · Spot Address And Messaging · Spot/Actor Membership
Session–Actor binding §8.2 owns relocation route updates for a bound Session.
This document fixes the TypeScript declarations related to Spot
that @zlink-systems/framework and @zlink-systems/nestjs export in
ZLink Framework.
The information the Location Store holds, fixing the current owner and lifecycle state of a Spot, is called authority. The process of preparing a new Instance Spot when authority is Missing and the caller specified Instance intent is called cold activation.
1. Global Identity And Lifecycle¶
SpotId is a string of UTF-8 encoded size 1..255 bytes, a logical ID
unique across the whole Location Store
transaction domain. Comparison is case-sensitive comparison, with no
Unicode normalization or case folding applied. A regular message only
takes SpotId and resolves current
authority. SpotRef is the
immutable location snapshot used to close an specified incarnation.
export declare enum ZLinkSpotKind {
Invalid = "invalid",
Entry = "entry",
User = "user",
Instance = "instance"
}
export declare enum ZLinkSpotCloseReason {
ExplicitClose = 0,
HostShutdown = 1,
RelocationOut = 2,
IdleEvicted = 3
}
export interface ZLinkSpotClosingContext {
readonly reason: ZLinkSpotCloseReason;
readonly deadline: Date;
}
export declare enum ZLinkSpotRelocationReadyOutcome {
Continued = 0,
Relocated = 1
}
export interface ZLinkSpotRelocationReadyCompletion {
readonly outcome: ZLinkSpotRelocationReadyOutcome;
}
export interface ZLinkSpotRelocationReadyCall {
defer(): void;
}
export interface ZLinkSpotAcceptRejectResponse {
readonly accepted: boolean;
readonly reply?: unknown;
}
export interface ZLinkSpotActorJoinResult extends ZLinkSpotAcceptRejectResponse {}
export interface ZLinkSpotCreateResponse extends ZLinkSpotAcceptRejectResponse {}
export interface ZLinkActorCreateResponse extends ZLinkSpotAcceptRejectResponse {}
export interface ZLinkSpotActorMembershipLifecycle<TActor extends ZLinkActor = ZLinkActor> {
onJoinedActor(actor: TActor): Promise<void>;
onLeaveActor(actor: TActor): Promise<void>;
onDisconnectActor?(actor: TActor): Promise<void>;
}
export interface ZLinkUserSpotActorLifecycle<TActor extends ZLinkActor = ZLinkActor>
extends ZLinkSpotActorMembershipLifecycle<TActor> {
onActorJoin(actorId: string, request: ZLinkMessage): Promise<ZLinkSpotActorJoinResult>;
}
export interface ZLinkSpot<TActor extends ZLinkActor = ZLinkActor>
extends ZLinkUserSpotActorLifecycle<TActor> {
readonly context: ZLinkSpotContext<TActor>;
configure?(): void;
onCreate?(request: ZLinkMessage): Promise<ZLinkSpotCreateResponse>;
onInitialize?(): Promise<void>;
onClosing?(
context: ZLinkSpotClosingContext,
cleanupSignal: AbortSignal): Promise<void>;
onRelocationReadyCompleted?(
completion: ZLinkSpotRelocationReadyCompletion): Promise<void>;
}
export interface ZLinkInstanceSpot {
readonly context: ZLinkInstanceSpotContext;
configure?(): void;
onInitialize?(): Promise<void>;
onClosing?(
context: ZLinkSpotClosingContext,
cleanupSignal: AbortSignal): Promise<void>;
}
export interface ZLinkSpotCommonContext<TSpot> {
readonly meshName: string;
readonly spotId: SpotId;
readonly objectGeneration: bigint;
readonly nodeRid: RoutingId;
readonly outbound: ZLinkSpotOutbound;
addTimer<THandler extends ZLinkSpotTimerHandler<TSpot>>(
name: string,
periodMs: number,
handlerType: Type<THandler>,
options?: ZLinkTimerOptions,
signal?: AbortSignal): Promise<ZLinkTimer>;
runCpuWorker<T>(work: (signal: AbortSignal) => T): ZLinkWorkerCall<T>;
runIoWorker<T>(work: (signal: AbortSignal) => Promise<T>): ZLinkWorkerCall<T>;
}
export interface ZLinkSpotContext<
TActor extends ZLinkActor = ZLinkActor,
TSpot extends ZLinkSpot<TActor> = ZLinkSpot<TActor>>
extends ZLinkSpotCommonContext<TSpot> {
readonly handlers: ZLinkSpotHandlerRegistry;
relocationReady(): ZLinkSpotRelocationReadyCall;
leaveActor(actor: TActor, signal?: AbortSignal): Promise<void>;
close(signal?: AbortSignal): Promise<boolean>;
}
export interface ZLinkInstanceSpotContext
extends ZLinkSpotCommonContext<ZLinkInstanceSpot> {
readonly handlers: ZLinkInstanceSpotHandlerRegistry;
close(signal?: AbortSignal): Promise<boolean>;
}
The canonical declaration of SpotId and SpotRef is owned by
Foundation Types And Configuration.
This document doesn't redeclare that type — it only fixes where it's
used in the Spot lifecycle.
ZLinkSpotCloseReason's numeric values are ExplicitClose=0,
HostShutdown=1, RelocationOut=2, IdleEvicted=3. IdleEvicted is an
Instance-Spot-only reason and isn't delivered to Entry Spot or User
Spot. The idle judgment condition and the reactivation rule after
cleanup are owned by
Spot Model §6.2.
deadline is the closing operation's absolute UTC instant. The
framework doesn't abort cleanupSignal before the callback invocation,
and aborts it when the deadline ends. Only Entry/User/
Instance Spot
receive this callback — a per-Actor closing callback isn't provided.
Host Shutdown runs the callback while Actor membership and the local
instance are valid, and cleans up scope and authority after fulfillment.
A standalone Actor relocation doesn't close the Entry Spot, so it
doesn't call this callback.
relocationReady().defer() is only valid on a Spot turn that
registered SpotWide and ApplicationSignaled together. The framework
delivers Continued from the source if it didn't move or aborted
before relay-ready was accepted, and Relocated from the target if it moved, to the
optional onRelocationReadyCompleted(...). If there's no callback, it
completes as a no-op. Held application messages and timers aren't run
before the callback completes.
A duplicate defer() on the default FrameworkManaged, on PerActor,
on Entry/Instance Spot, outside the Spot turn, or in the same turn fails
with InvalidOperation before any queue mutation. A different
Framework operation in the same turn after defer() is the same error.
Since the callback can run again during recovery, the implemented
callback must be retry-safe.
2. Handler And Outbound¶
export interface ZLinkSpotHandlerRegistry extends ZLinkActorHandlerRegistry {
addHandler<THandler>(handlerType: Type<THandler>, packetName?: string): this;
addPacket<THandler>(handlerType: Type<THandler>): this;
addSubscribe<THandler>(handlerType: Type<THandler>, channelName: string, topic: string): this;
}
export interface ZLinkInstanceSpotHandlerRegistry {
addPacket<THandler>(handlerType: Type<THandler>): this;
}
export interface ZLinkSpotOutbound {
sendToSpot(spotId: SpotId, message: unknown): ZLinkSpotSendCall;
requestToSpot(spotId: SpotId, request: unknown): ZLinkSpotRequestCall;
publish(channelName: string, topic: string, event: unknown): ZLinkPublishCall;
sendToChannel(channelName: string, message: unknown): ZLinkSendCall;
requestToChannel(channelName: string, request: unknown): ZLinkChannelRequestCall;
}
export interface ZLinkSpotSendCall {
metadata(key: string, value: string): this;
metadata(metadata: ZLinkMessageMetadata): this;
instanceSpot(): this;
instanceSpot(instanceSpotType: string): this;
inMesh(meshName: string): this;
submit(signal?: AbortSignal): Promise<void>;
}
export interface ZLinkSpotRequestCall {
metadata(key: string, value: string): this;
metadata(metadata: ZLinkMessageMetadata): this;
instanceSpot(): this;
instanceSpot(instanceSpotType: string): this;
inMesh(meshName: string): this;
timeout(timeoutMs: number): this;
submit<TReply>(signal?: AbortSignal): Promise<TReply>;
yield<TReply>(signal?: AbortSignal): Promise<TReply>;
}
export interface ZLinkSpotPacketHandler<TSpot, TMessage> {
handle(spot: TSpot, message: TMessage, context: ZLinkMessageContext): Promise<void>;
}
export declare function ZLinkSpotActorRequest(packetName?: string): MethodDecorator;
export declare function ZLinkSpotActorSend(packetName?: string): MethodDecorator;
export interface ZLinkSpotActorSendHandler<TSpot, TActor extends ZLinkActor, TMessage> {
handle(spot: TSpot, actor: TActor, context: ZLinkMessageContext, message: TMessage): Promise<void>;
}
export interface ZLinkSpotActorRequestHandler<TSpot, TActor extends ZLinkActor, TRequest, TReply> {
handle(spot: TSpot, actor: TActor, context: ZLinkMessageContext, request: TRequest): Promise<TReply>;
}
The Node runtime creates each Spot packet/request/subscription/timer handler class once per Spot activation and reuses it. An Actor send/ request handler class is created once per Actor activation and reused. Different Actors don't share a handler instance or activation-scoped provider. Nest provider's singleton/request/transient configuration doesn't change this lifetime, and a handler lifetime option isn't added.
A same-node Join keeps the Actor handler. A cross-node Join and relocation clean up the source handler and re-create it in the target activation. State that must be recovered isn't put in a handler field — it's owned by the Spot or Actor.
3. Manager And Single-Use Create Call¶
export interface ZLinkSpotCreateResult {
readonly spot: SpotRef;
readonly state: ZLinkSpotCreateState;
readonly reply?: unknown;
}
export declare enum ZLinkSpotCreateState {
Existing = "existing",
Created = "created",
Rejected = "rejected"
}
export interface ZLinkSpotManager {
create(spotType: string): ZLinkSpotCreateCall;
getOrCreate(
spotId: SpotId,
spotType: string): ZLinkSpotGetOrCreateCall;
find(spotId: SpotId, signal?: AbortSignal): Promise<SpotRef | undefined>;
close(spot: SpotRef, signal?: AbortSignal): Promise<boolean>;
}
export interface ZLinkSpotCreateCall {
inMesh(meshName: string): this;
request(request: unknown): this;
timeout(timeoutMs: number): this;
submit(signal?: AbortSignal): Promise<ZLinkSpotCreateResult>;
yield(signal?: AbortSignal): Promise<ZLinkSpotCreateResult>;
}
export interface ZLinkSpotGetOrCreateCall {
inMesh(meshName: string): this;
request(request: unknown): this;
timeout(timeoutMs: number): this;
submit(signal?: AbortSignal): Promise<ZLinkSpotCreateResult>;
yield(signal?: AbortSignal): Promise<ZLinkSpotCreateResult>;
}
Entry/User/Instance SpotId is a global string key of UTF-8 encoded size 1..255 bytes. Stable type is UTF-8 1..255 bytes, compared as a case-sensitive value comparison with no normalization. Object generation is a positive signed-63-bit value. MeshName and NodeRid are the route snapshot at query time, and aren't included in the identity key.
The Create and GetOrCreate calls are single-use. Setting the same
option twice, or calling terminal submit(...) twice, is
InvalidOperation. User Spot's create has the framework issue a new
global Spot ID. getOrCreate returns a ready Spot of the same User
kind/stable type as
existing. If Creating, it waits for the authority change; once Ready,
existing; if it becomes Missing through cleanup, it competes for a
new reservation. If kind or type differs, TypeMismatch; if the
terminal state isn't reached within the
deadline, DeadlineExceeded.
close(spotRef) only closes the specified incarnation. If the generation
differs, InvalidOperation; while moving, Unavailable. The framework
doesn't find the current ref again and close a different incarnation.
Manager create/get-or-create is not provided for Instance Spot.
Instance Spot Cold Activation And The First Message¶
sendToSpot and requestToSpot take the global SpotId and return ZLinkSpotSendCall
or ZLinkSpotRequestCall. Marker overloads are instanceSpot() and
instanceSpot(instanceSpotType: string); the Mesh input is inMesh(meshName: string).
Send submit(signal?: AbortSignal) returns Promise<void>; request
submit<TReply>(signal?: AbortSignal) and yield<TReply>(signal?: AbortSignal) return
Promise<TReply>. Exact declarations appear in §2.
Spot address messaging §4 owns type/Mesh selection, the creation sequence, and first-message preservation for cold activation. Completion boundaries follow Spot address messaging §5.
A User Spot's close(spotRef) returns false if active Actor
membership remains. The
framework doesn't automatically leave/destroy the Actor. A one-way Spot
message only waits until local outbound admission, and doesn't
hidden-retry an operation that failed after target queue admission to a
new owner.
An Instance Spot factory only creates an actor-free lifecycle. An
Actor handler, Actor membership, and Logical Multicast subscription
can't be registered — it only processes direct packets and timers. An
Instance Spot closes when its handler or timer calls its own context's
close(...). A regular message doesn't create a new intent or directly
start a factory for a missing RID.
Object lifecycle §3 owns the scope of stored creation intent resumption and the distinction
from steady Ready owner failure.
In the following example, spotClient is a ZLinkSpotOutbound, and
cartId is the global SpotId to call. Since Instance intent is
specified, the stable type and initial Mesh needed for cold activation
are only used if the Spot doesn't exist.
const reply = await spotClient
.requestToSpot(cartId, request)
.instanceSpot("shopping-cart") // if Missing, requests creation of an Instance Spot of this stable type.
.inMesh("commerce") // only restricts the Mesh selection scope for Missing cold activation.
.timeout(5_000)
.submit<CartReply>(); // waits for the reply the creation or the existing owner's handler returned.
In User/Instance Spot relocation, the framework includes, in the
relocation payload, the logical timer registration created by
addTimer(...), the last completed tick sequence, the next scheduled
time, and pending ticks not yet run. The target restores the logical
timer registration, so the application doesn't re-register the timer.
Only the currently running timer handler finishes on the source, and
the restored tick isn't run before target Ready.
The public trace categories are spot-instance, actor-relocation.
The meaning and verification criteria are owned by
Spot Address And Messaging
and Spot/Actor Membership.
yield(...) declared in this document is only valid on the shared turn
of a SpotWide User Spot or Instance Spot. Called on an Entry Spot or
PerActor User Spot, it completes with invalidConfiguration, without
submitting the operation or returning the turn. submit(...) is the
common Async semantics that keeps the current turn.