04. Spot instance¶
This category covers the external entry points ZLinkSpotManager (ZLINK_SPOT_MANAGER)/
ZLinkRouteClient/ZLinkSpotPublisherClient (ZLINK_SPOT_PUBLISHER_CLIENT) provide, and the
entry points used inside Spot code via ZLinkSpotContext/ZLinkInstanceSpotContext. The exact
signatures are owned by the
Spot and Instance Spot exact interface
and the
STREAM, timer, and worker exact interface
(Korean-only).
ZLinkSpotManager.create¶
Always creates a new User Spot. The Framework issues a new global SpotId.
const created = await spotManager
.create("room")
.inMesh("play")
.request(new CreateRoom("ranked"))
.timeout(5_000)
.submit();
const spotId = created.spot.spotId;
Options. This call carries the following modifiers.
| Modifier | Default | Meaning |
|---|---|---|
.inMesh(meshName) |
Optional if exactly one Mesh has Object Client/Server role | The Mesh to create the Spot in. Omitting it with two or more candidates completes with InvalidOperation; none completes with NotConfigured; a nonexistent specified Mesh completes with NotFound |
.request(request) |
None (empty request) | The creation request passed to the Spot's onCreate(...) |
.timeout(timeoutMs) |
Default applied to the whole of resolve/factory/initialize | The upper bound until the entire creation reaches a terminal state |
.submit(signal?) |
terminal (pick one) | Waits until creation completes |
.yield(signal?) |
terminal (pick one) | Only valid inside a SpotWide handler |
Completion result. ZLinkSpotCreateResult.state is "created" (newly created). If the
Spot's onCreate(...) rejects it, it is "rejected" and reply carries the rejection message.
Setting the same option twice, or calling a terminal twice, completes with InvalidOperation;
not finishing within the deadline completes with DeadlineExceeded.
When to use. Use this when a new instance is always needed. Use getOrCreate to reuse an
existing one and only create when there is none.
ZLinkSpotManager.getOrCreate¶
Returns the Ready Spot with the specified SpotId if it exists, and creates a new one otherwise.
const existingOrCreated = await spotManager
.getOrCreate("lobby-eu", "lobby")
.inMesh("play")
.request(new CreateLobby("eu"))
.submit();
Options. The same as create — .inMesh(...), .request(...), .timeout(...), terminal
.submit(signal?) or .yield(signal?).
Completion result. If state is "existing", it returns the already-existing Spot as-is and
ignores request. "created" means a new one was made. If the same SpotId is currently
contended in a creating state, it waits for that result and joins it; if cleanup makes it
missing, it re-competes for a new reservation.
When to use. Use this when an idempotent "use if it exists, create if it doesn't" by SpotId
is needed. Use create if a new instance is always needed.
find / close (manager)¶
Queries an existing Spot, or closes the exact incarnation.
const spot = await spotManager.find("lobby-eu");
if (spot) {
const closed = await spotManager.close(spot);
}
Options. Neither call has modifiers — both only take the target identifier.
Completion result. find returns undefined if there is no Ready Spot. close returns
false if the incarnation does not exist, completes with InvalidOperation if the generation
differs, and Unavailable while a pre-commit seal is in progress. If a User Spot still has Actor
membership, it returns false and does not automatically leave/destroy the Actor.
When to use. Use this when you need to check current existence or explicitly terminate a
Spot. close does not close a different incarnation on behalf of a stale SpotRef.
sendToSpot¶
Sends a one-way message to a single global SpotId. The external client (ZLinkRouteClient) and
Spot code (ZLinkSpotOutbound) provide the same shape.
await routeClient
.sendToSpot("room-42", new PlayerJoinedRoom("player-1"))
.submit();
// activating a new Instance Spot on demand (cold activation) before sending
await routeClient
.sendToSpot("device-42", new DeviceCommand("reboot"))
.instanceSpot("device")
.submit();
Options. This call carries the following modifiers.
| Modifier | Default | Meaning |
|---|---|---|
.metadata(key, value) |
None | Key-value to pass to the handler |
.instanceSpot() |
None (resolves User Spot only) | Performs cold activation if missing. If an existing authority already exists, it uses the stored stable type regardless of the number of registered types |
.instanceSpot(instanceSpotType) |
— | If missing and several types are registered, the stable type must be specified |
.inMesh(meshName) |
Optional if exactly one Mesh has Object Client/Server role | The Mesh to first create a missing Instance Spot in. Using it without an instance marker completes with InvalidOperation |
.submit(signal?) |
Required terminal | Waits only until source-local admission |
Completion result. No SpotId and no Instance marker completes with NotFound. If
.instanceSpot(...) was used but the existing authority is a User Spot, or differs from the
specified type, it completes with TypeMismatch. Other completion kinds follow the same common
rules as the messaging-execution category.
When to use. Use this for Spot messaging where no reply is needed. Use requestToSpot if a
reply is needed.
requestToSpot¶
Sends and receives a typed request/reply to a single global SpotId.
const reply = await routeClient
.requestToSpot("room-42", new GetRoomState())
.timeout(3_000)
.submit<RoomState>();
Options. In addition to the same .instanceSpot(...)/.inMesh(...) as sendToSpot, this
adds the following.
| Modifier | Default | Meaning |
|---|---|---|
.timeout(timeoutMs) |
The MeshNode's request default timeout | The deadline covering resolve, cold activation, handler, and reply altogether |
.submit<TReply>(signal?) |
terminal (pick one) | Waits until the reply arrives |
.yield<TReply>(signal?) |
terminal (pick one) | Only valid inside a SpotWide User Spot/Instance Spot handler. Calling it elsewhere completes with invalidConfiguration |
Completion result. In addition to the same failure kinds as sendToSpot, if the factory or
initialize fails during cold activation, it completes as a typed failure — the Framework does not
retry internally.
When to use. Use this when the reply value is needed. Use sendToSpot if it is one-way.
publish (Spot Logical Multicast)¶
Publishes a typed event to subscribers by ChannelName and topic. ZLinkSpotPublisherClient
(external) and ZLinkSpotOutbound (inside Spot code) provide the same shape.
await spotPublisherClient
.publish("room.events", "room-42", new RoomStateChanged("started"))
.submit();
Options. This call has .metadata(...) and the required terminal .submit(signal?) — the
topic is a required argument.
Completion result. A normal completion means publish admission finished. It does not wait for
subscriber reception. Unlike classic fanout publish in the messaging-execution category, the
owner MeshNode is determined by ChannelName alone, and the caller does not pass a MeshName
separately.
When to use. Use this to notify observers of a Spot state change. If a direct reply from a
subscriber is needed, use requestToSpot instead of this entry.
addTimer (inside Spot code)¶
Registers a periodic timer belonging to a Spot. Called via
ZLinkSpotCommonContext.addTimer(...).
const timer = await context.addTimer(
"room-tick",
1_000,
RoomTickHandler,
{ overrunPolicy: ZLinkTimerOverrunPolicy.SkipLateTicks },
);
Options. The fields of ZLinkTimerOptions are as follows.
| Field | Default | Meaning |
|---|---|---|
overrunPolicy |
SkipLateTicks |
Whether to skip when a tick falls behind, catch up within a bound, or delay the next tick |
maxCatchUpTicks |
1 | The maximum ticks to catch up at once when CatchUpBounded |
stopOnUnhandledException |
false |
Whether to stop the timer on a handler exception |
Completion result. Returns a ZLinkTimer. Because the timer is a logical registration
belonging to this Spot, it is automatically carried over on relocation and the application does
not need to re-register it at the target. Cancel it with cancel(signal?) or dispose().
When to use. Use this when a Spot needs periodic work.
runCpuWorker / runIoWorker (inside Spot code)¶
Runs work on a separate worker without blocking the Spot's owner turn.
const result = await context
.runCpuWorker((signal) => computeExpensiveScore(signal))
.timeoutMs(2_000)
.submit();
Options. ZLinkWorkerCall<T> provides the following modifiers.
| Modifier | Default | Meaning |
|---|---|---|
.timeoutMs(durationMs) |
The worker option's default | The upper bound for the work to complete |
.submit(signal?) |
terminal (pick one) | Waits until completion |
.yield(signal?) |
terminal (pick one) | Only valid inside a SpotWide handler |
Completion result. Returns T, or completes with DeadlineExceeded on timeout. The worker
pool size (minThreads/maxThreads) and idle timeout are configured only before the host starts.
When to use. Use runCpuWorker for CPU-bound computation, and runIoWorker (returning
Promise<T>) for work that waits on I/O. Both exist to avoid blocking the owner turn's sequential
execution.
Handler registration (inside Spot code, decorator)¶
Marks a handler class that will process the packets/requests/subscriptions/member Actor messages
a Spot receives, using a decorator. NestJS provider discovery (zlinkDiscoverProviders(...))
finds handler classes inside the module and registers them.
@zlinkSpotRequestHandler({ spot: () => RoomSpot, packetName: "start-game" })
export class StartGameHandler implements ZLinkSpotRequestHandler<RoomSpot, StartGameReq, StartGameRes> {
handle(spot: RoomSpot, request: StartGameReq): Promise<StartGameRes> { ... }
}
Options. The interface implemented and the decorator used differ by what the handler processes.
| Target | Handler interface | Decorator |
|---|---|---|
| One-way packet in front of a User Spot | ZLinkSpotPacketHandler<TSpot, TMessage> |
@zlinkSpotPacketHandler({ spot, packetName? }) |
| Request in front of a User Spot | ZLinkSpotRequestHandler<TSpot, TRequest, TReply> |
@zlinkSpotRequestHandler({ spot, packetName }) (a @ZLinkSpotRequest(packetName?) method decorator in the raw builder) |
| A Logical Multicast subscription event | ZLinkSpotSubscriptionHandler<TSpot, TEvent> |
@zlinkSpotSubscriptionHandler({ spot, channelName, topic }) (raw is @ZLinkSpotSubscription(channelName, topic)) |
| A Spot's periodic timer | ZLinkSpotTimerHandler<TSpot> |
@zlinkSpotTimerHandler({ spot?, name?, periodMs?, options? }) |
| A one-way packet in front of a User Spot's member Actor | ZLinkSpotActorSendHandler<TSpot, TActor, TMessage> |
@zlinkSpotActorSendHandler({ spot, actor, packetName }) (raw is @ZLinkSpotActorSend(packetName?)) |
| A request in front of a User Spot's member Actor | ZLinkSpotActorRequestHandler<TSpot, TActor, TRequest, TReply> |
@zlinkSpotActorRequestHandler({ spot, actor, packetName }) |
| A one-way packet/request in front of an Entry Spot's member Actor | ZLinkEntrySpotActorSendHandler/ZLinkEntrySpotActorRequestHandler |
@zlinkEntrySpotActorSendHandler/@zlinkEntrySpotActorRequestHandler({ entrySpot, actor, packetName }) |
| An Entry Spot's own packet/subscription | The same shape as ZLinkSpotPacketHandler/ZLinkSpotSubscriptionHandler |
@zlinkEntrySpotPacketHandler/@zlinkEntrySpotSubscriptionHandler({ entrySpot, ... }) |
| A packet in front of an Instance Spot (raw builder) | ZLinkInstanceSpotHandlerRegistry.addPacket(handlerType) |
None (direct registration) |
Completion result. Registers synchronously with no return value. The spot/entrySpot
field is Type<T> | (() => Type<T>) — use a lazy resolver (() => RoomSpot) to avoid a circular
reference. A duplicate handler key under the same owner surfaces as a configuration error in
startup validation.
When to use. Attach exactly one decorator to every Spot handler class. See the registration entries in the topology-discovery category for Node/Channel handlers, and the stream-session category for STREAM session handlers.
outbound — sendToChannel / requestToChannel (inside Spot code)¶
Sends a one-way message by ChannelName, or exchanges a typed request/reply, from inside Spot
code. Provided by ZLinkSpotCommonContext.outbound, in the same shape as sendToChannel/
requestToChannel in the messaging-execution category.
const reply = await context.outbound
.requestToChannel("leaderboard.api", new GetLeaderboard())
.submit<Leaderboard>();
Options. Takes the same modifiers as sendToChannel/requestToChannel in the
messaging-execution category.
Completion result. Same as the completion kinds in the messaging-execution category.
When to use. Use this when a Spot must call a handler on a different ChannelName from inside
its own code, rather than an external client doing so. Use sendToSpot/requestToSpot to call
another Spot directly.
leaveActor / close / destroyActor (inside Spot code, termination/departure)¶
Removes a member Actor from this Spot, closes the Spot itself, or destroys an Actor from an Entry Spot.
await context.leaveActor(actor); // User Spot: only removes the member Actor
const closed = await context.close(); // User/Instance Spot: closes this Spot itself
await entryContext.destroyActor(actor); // Entry Spot: destroys the Actor entirely
Options. None of the three calls has modifiers — they only take the target
(leaveActor/destroyActor) and an optional signal.
Completion result. leaveActor (ZLinkSpotContext only) only releases member Actor
membership and does not destroy the Actor itself. close (ZLinkSpotContext/
ZLinkInstanceSpotContext) uses the same completion kinds as the manager's close(spotRef) (the
earlier entry in the spot-instance category), but targets this Spot itself. destroyActor
(ZLinkEntrySpotContext only) destroys the Actor entirely — unlike leaveActor, it removes the
Actor itself rather than releasing membership.
When to use. Use leaveActor to remove a member Actor from this Spot without moving it
elsewhere, close to terminate the Spot itself, and destroyActor to entirely remove an Actor
that is no longer needed at an Entry Spot.
relocationReady().defer() (inside Spot code)¶
In a SpotWide Spot that has chosen ApplicationSignaled coordination mode, defers the relocation
boundary to just before the next application turn.
Options. This call has no modifiers.
Completion result. No return value. Registers the relocation boundary after the current
handler ends. If it did not move, or aborted before commit, it receives a Continued completion
at the source; if it moved, it receives a Relocated completion at the target, via the optional
onRelocationReadyCompleted(...) (completing as a no-op if there is no callback).
FrameworkManaged mode, a PerActor Spot, an Entry/Instance Spot, outside a Spot turn, or a
duplicate call in the same turn all complete with InvalidOperation.
When to use. Use this when the application must precisely control the relocation moment down
to a specific turn boundary. This call is not needed under the default FrameworkManaged mode.
See the Spot and Instance Spot exact interface and the STREAM, timer, and worker exact interface (Korean-only) for the full rationale.