@zlink-systems/framework 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/contracts/Dispatch/ZLinkDispatchOptions.d.ts +1 -0
- package/dist/contracts/Streams/IZLinkSession.d.ts +2 -1
- package/dist/runtime/actors/actor-context.js +4 -4
- package/dist/runtime/actors/actor-join-deferred-scope.d.ts +0 -1
- package/dist/runtime/actors/actor-join-deferred-scope.js +0 -18
- package/dist/runtime/actors/deferred-join-accepted-journal.js +2 -6
- package/dist/runtime/backend/contracts/index.d.ts +1 -0
- package/dist/runtime/backend/node/node-raw-mesh-backend.d.ts +3 -0
- package/dist/runtime/backend/node/node-raw-mesh-backend.js +21 -8
- package/dist/runtime/channels/channel-dispatch-pipeline.js +2 -2
- package/dist/runtime/channels/channel-transports.js +3 -9
- package/dist/runtime/channels/dispatch-error-reporter.d.ts +1 -0
- package/dist/runtime/channels/dispatch-error-reporter.js +6 -38
- package/dist/runtime/diagnostics/dispatch-error-details.d.ts +6 -0
- package/dist/runtime/diagnostics/dispatch-error-details.js +55 -0
- package/dist/runtime/diagnostics/flow-context.d.ts +2 -1
- package/dist/runtime/diagnostics/flow-context.js +2 -2
- package/dist/runtime/diagnostics/message-flow.js +11 -3
- package/dist/runtime/foundation/actor-join-recovery-codec.js +0 -4
- package/dist/runtime/foundation/raw-service-mesh-runtime.d.ts +4 -2
- package/dist/runtime/foundation/raw-service-mesh-runtime.js +34 -9
- package/dist/runtime/foundation/service-session-binding-ingress-port.d.ts +1 -0
- package/dist/runtime/foundation/service-stateful-runtime.d.ts +4 -0
- package/dist/runtime/foundation/service-stateful-runtime.js +46 -2
- package/dist/runtime/foundation/service-topology-registry.d.ts +1 -0
- package/dist/runtime/foundation/service-topology-registry.js +12 -4
- package/dist/runtime/host/index.js +1 -1
- package/dist/runtime/protocol/service_wire_pilot_codec.generated.js +20 -18
- package/dist/runtime/spots/index.js +1 -1
- package/dist/runtime/spots/spot-actor-packet-dispatch.d.ts +2 -1
- package/dist/runtime/spots/spot-actor-packet-dispatch.js +6 -1
- package/dist/runtime/spots/spot-actor-packet-drain.js +2 -0
- package/dist/runtime/spots/spot-node-runtime-manager.js +5 -1
- package/dist/runtime/spots/spot-route-packet-dispatch.js +1 -1
- package/dist/runtime/spots/spot-routed-bound-session-dispatch.js +1 -1
- package/dist/runtime/spots/spot-subscription-dispatch.js +1 -1
- package/dist/runtime/spots/spot-timer.js +1 -1
- package/dist/runtime/streams/actor-session-binding-registry.d.ts +21 -8
- package/dist/runtime/streams/actor-session-binding-registry.js +71 -42
- package/dist/runtime/streams/actor-session-binding-runtime-owner.d.ts +1 -0
- package/dist/runtime/streams/bound-actor-relay-sender.js +2 -1
- package/dist/runtime/streams/bound-session-response-target.js +8 -1
- package/dist/runtime/streams/bound-session-service.d.ts +1 -0
- package/dist/runtime/streams/bound-session-service.js +6 -3
- package/dist/runtime/streams/index.js +8 -1
- package/dist/runtime/streams/managed-stream.d.ts +3 -0
- package/dist/runtime/streams/managed-stream.js +20 -1
- package/dist/runtime/streams/protocol.d.ts +5 -1
- package/dist/runtime/streams/protocol.js +41 -4
- package/dist/runtime/streams/session-actor-coordinator.js +3 -4
- package/dist/runtime/streams/session-context.d.ts +9 -2
- package/dist/runtime/streams/session-context.js +20 -4
- package/dist/runtime/streams/session-local-actors.d.ts +3 -1
- package/dist/runtime/streams/session-local-actors.js +17 -2
- package/dist/runtime/streams/stream-frame-factory.d.ts +1 -1
- package/dist/runtime/streams/stream-frame-factory.js +3 -2
- package/dist/runtime/streams/stream-session-runtime.js +42 -12
- package/package.json +3 -3
- package/src/contracts/Dispatch/ZLinkDispatchOptions.ts +1 -0
- package/src/contracts/Streams/IZLinkSession.ts +2 -1
- package/src/runtime/actors/actor-context.ts +4 -4
- package/src/runtime/actors/actor-join-deferred-scope.ts +0 -32
- package/src/runtime/actors/deferred-join-accepted-journal.ts +2 -6
- package/src/runtime/backend/contracts/index.ts +4 -0
- package/src/runtime/backend/node/node-raw-mesh-backend.ts +39 -11
- package/src/runtime/channels/channel-dispatch-pipeline.ts +4 -2
- package/src/runtime/channels/channel-transports.ts +3 -9
- package/src/runtime/channels/dispatch-error-reporter.ts +7 -45
- package/src/runtime/diagnostics/dispatch-error-details.ts +62 -0
- package/src/runtime/diagnostics/flow-context.ts +6 -4
- package/src/runtime/diagnostics/message-flow.ts +12 -3
- package/src/runtime/foundation/actor-join-recovery-codec.ts +0 -4
- package/src/runtime/foundation/raw-service-mesh-runtime.ts +54 -13
- package/src/runtime/foundation/service-session-binding-ingress-port.ts +1 -0
- package/src/runtime/foundation/service-stateful-runtime.ts +64 -5
- package/src/runtime/foundation/service-topology-registry.ts +18 -5
- package/src/runtime/host/index.ts +1 -1
- package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +18 -17
- package/src/runtime/spots/index.ts +2 -1
- package/src/runtime/spots/spot-actor-packet-dispatch.ts +20 -3
- package/src/runtime/spots/spot-actor-packet-drain.ts +2 -0
- package/src/runtime/spots/spot-node-runtime-manager.ts +5 -1
- package/src/runtime/spots/spot-route-packet-dispatch.ts +2 -1
- package/src/runtime/spots/spot-routed-bound-session-dispatch.ts +2 -1
- package/src/runtime/spots/spot-subscription-dispatch.ts +2 -1
- package/src/runtime/spots/spot-timer.ts +6 -1
- package/src/runtime/streams/actor-session-binding-registry.ts +150 -63
- package/src/runtime/streams/actor-session-binding-runtime-owner.ts +1 -0
- package/src/runtime/streams/bound-actor-relay-sender.ts +10 -2
- package/src/runtime/streams/bound-session-response-target.ts +10 -1
- package/src/runtime/streams/bound-session-service.ts +13 -3
- package/src/runtime/streams/index.ts +8 -1
- package/src/runtime/streams/managed-stream.ts +34 -12
- package/src/runtime/streams/protocol.ts +44 -5
- package/src/runtime/streams/session-actor-coordinator.ts +12 -4
- package/src/runtime/streams/session-context.ts +31 -4
- package/src/runtime/streams/session-local-actors.ts +19 -3
- package/src/runtime/streams/stream-frame-factory.ts +4 -2
- package/src/runtime/streams/stream-session-runtime.ts +58 -13
|
@@ -138,6 +138,7 @@ export class ZLinkSpotActorPacketDrain {
|
|
|
138
138
|
actorId,
|
|
139
139
|
parts,
|
|
140
140
|
returnResponse: false,
|
|
141
|
+
sourceSessionRid,
|
|
141
142
|
fallbackActorRef: actorRef as unknown as ActorRef | undefined
|
|
142
143
|
});
|
|
143
144
|
}
|
|
@@ -200,6 +201,7 @@ export class ZLinkSpotActorPacketDrain {
|
|
|
200
201
|
actorId,
|
|
201
202
|
parts,
|
|
202
203
|
returnResponse: true,
|
|
204
|
+
sourceSessionRid: info.sourceSessionRid,
|
|
203
205
|
fallbackActorRef: actorRef as unknown as ActorRef | undefined
|
|
204
206
|
});
|
|
205
207
|
this.options.replyActorNoBind?.(
|
|
@@ -272,6 +272,9 @@ export class ZLinkSpotNodeRuntimeManager {
|
|
|
272
272
|
applicationJobReceiveFlowFailureSink: this.options.applicationJobReceiveFlowFailureSink,
|
|
273
273
|
peerAdmissionSealed: () => this.options.peerAdmissionSealed?.(spotNodeName) ?? false
|
|
274
274
|
});
|
|
275
|
+
if (this.options.dispatchErrors !== undefined) {
|
|
276
|
+
node.setDispatchErrorReporter?.(this.options.dispatchErrors, spotNodeName);
|
|
277
|
+
}
|
|
275
278
|
node.setMailboxRecordDroppedHandler?.((record) =>
|
|
276
279
|
this.options.dispatchErrors?.report({
|
|
277
280
|
surface: ZLinkDispatchErrorSurface.RouteMeshChannel,
|
|
@@ -1183,7 +1186,8 @@ export class ZLinkSpotNodeRuntimeManager {
|
|
|
1183
1186
|
)
|
|
1184
1187
|
);
|
|
1185
1188
|
try {
|
|
1186
|
-
publisher.publish(channelName, topic, parts, { flags });
|
|
1189
|
+
const processing = publisher.publish(channelName, topic, parts, { flags });
|
|
1190
|
+
void Promise.resolve(processing).catch(() => undefined);
|
|
1187
1191
|
return { status: ZLinkSubmitStatus.Submitted };
|
|
1188
1192
|
} catch (error) {
|
|
1189
1193
|
if (isZLinkBackendResultError(error) && error.operation === 'submit') {
|
|
@@ -156,7 +156,8 @@ export class ZLinkSpotRoutePacketDispatch {
|
|
|
156
156
|
createInboundFlow(
|
|
157
157
|
envelope.header.flowId,
|
|
158
158
|
envelope.header.flowOrigin,
|
|
159
|
-
this.options.dispatchErrors?.flow.flowCreationEnabled() ?? true
|
|
159
|
+
this.options.dispatchErrors?.flow.flowCreationEnabled() ?? true,
|
|
160
|
+
undefined
|
|
160
161
|
),
|
|
161
162
|
() =>
|
|
162
163
|
this.options.serial.execute(
|
|
@@ -57,7 +57,8 @@ export class ZLinkSpotRoutedBoundSessionDispatch {
|
|
|
57
57
|
createInboundFlow(
|
|
58
58
|
boundSessionSend.flowId ?? boundSessionSend.envelope?.header.flowId,
|
|
59
59
|
boundSessionSend.flowOrigin ?? boundSessionSend.envelope?.header.flowOrigin,
|
|
60
|
-
this.options.dispatchErrors?.flow.flowCreationEnabled() ?? true
|
|
60
|
+
this.options.dispatchErrors?.flow.flowCreationEnabled() ?? true,
|
|
61
|
+
undefined
|
|
61
62
|
),
|
|
62
63
|
() =>
|
|
63
64
|
this.options.routedBoundSessionReceiver?.(
|
|
@@ -187,7 +187,8 @@ export class ZLinkSpotSubscriptionDispatch {
|
|
|
187
187
|
const inboundFlow = createInboundFlow(
|
|
188
188
|
envelope.header.flowId,
|
|
189
189
|
envelope.header.flowOrigin,
|
|
190
|
-
this.options.dispatchErrors?.flow.flowCreationEnabled() ?? true
|
|
190
|
+
this.options.dispatchErrors?.flow.flowCreationEnabled() ?? true,
|
|
191
|
+
undefined
|
|
191
192
|
);
|
|
192
193
|
try {
|
|
193
194
|
await this.options.serial.execute(
|
|
@@ -152,7 +152,12 @@ export class ZLinkSpotTimerRegistry {
|
|
|
152
152
|
normalizeTimerOptions(options),
|
|
153
153
|
async (tick) => {
|
|
154
154
|
callbackStarted = false;
|
|
155
|
-
const timerFlow = createInboundFlow(
|
|
155
|
+
const timerFlow = createInboundFlow(
|
|
156
|
+
undefined,
|
|
157
|
+
'Timer',
|
|
158
|
+
this.flowCreationEnabled(),
|
|
159
|
+
undefined
|
|
160
|
+
);
|
|
156
161
|
const operation = () => {
|
|
157
162
|
const current = this.timers.get(name);
|
|
158
163
|
if (
|
|
@@ -18,7 +18,11 @@ export interface ZLinkActorSessionBindingActor {
|
|
|
18
18
|
|
|
19
19
|
export interface ZLinkActorSessionBindingContext<TActor extends ZLinkActorSessionBindingActor> {
|
|
20
20
|
readonly routingId?: unknown;
|
|
21
|
-
|
|
21
|
+
readonly actorSlotControls?: {
|
|
22
|
+
enqueueBound(actorSlot: number, actorId: string): Promise<void>;
|
|
23
|
+
enqueueUnbound(actorSlot: number): Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
bindLocal(actor: TActor, bindingToken: string, actorSlot?: number): void;
|
|
22
26
|
unbindLocal(actorId: string, bindingToken: string): void;
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -29,6 +33,7 @@ export interface ZLinkActorSessionRoute<
|
|
|
29
33
|
readonly context: TContext;
|
|
30
34
|
readonly actor: TActor;
|
|
31
35
|
readonly bindingToken: string;
|
|
36
|
+
readonly actorSlot?: number;
|
|
32
37
|
readonly sessionIdentity?: string;
|
|
33
38
|
readonly activeFrames: ZLinkActorSessionActiveFrames;
|
|
34
39
|
sealId?: string;
|
|
@@ -137,12 +142,18 @@ export interface ZLinkActorSessionAuthorityFence {
|
|
|
137
142
|
readonly actorType?: string;
|
|
138
143
|
}
|
|
139
144
|
|
|
145
|
+
export enum ZLinkActorSessionBindingTermination {
|
|
146
|
+
BindingEnd,
|
|
147
|
+
PhysicalDisconnect
|
|
148
|
+
}
|
|
149
|
+
|
|
140
150
|
export class ZLinkActorSessionBindingRegistry<
|
|
141
151
|
TContext extends ZLinkActorSessionBindingContext<TActor>,
|
|
142
152
|
TActor extends ZLinkActorSessionBindingActor
|
|
143
153
|
> {
|
|
144
154
|
private readonly lane = new ZLinkStateLane();
|
|
145
155
|
private readonly routes = new Map<string, ZLinkActorSessionRoute<TContext, TActor>>();
|
|
156
|
+
private readonly nextActorSlots = new WeakMap<TContext, number>();
|
|
146
157
|
private readonly sealWaiters = new Map<
|
|
147
158
|
string,
|
|
148
159
|
Set<{
|
|
@@ -164,7 +175,9 @@ export class ZLinkActorSessionBindingRegistry<
|
|
|
164
175
|
// (the sealing side) must observe that bound; an unbounded wait here
|
|
165
176
|
// turns a lost control or a lifecycle interlock into a silent stall
|
|
166
177
|
// (spec 48:205 — transport/deadline limits apply during relocation).
|
|
167
|
-
private readonly sealWaitTimeoutMs = 3_000
|
|
178
|
+
private readonly sealWaitTimeoutMs = 3_000,
|
|
179
|
+
private readonly errorSink?: () =>
|
|
180
|
+
{ reportRuntimeTaskException(taskName: string, error: unknown): void } | undefined
|
|
168
181
|
) {
|
|
169
182
|
if (!Number.isSafeInteger(terminalRelocationCapacity) || terminalRelocationCapacity <= 0) {
|
|
170
183
|
throw new RangeError('Session relocation terminal capacity must be a positive safe integer.');
|
|
@@ -182,83 +195,120 @@ export class ZLinkActorSessionBindingRegistry<
|
|
|
182
195
|
actor: TActor,
|
|
183
196
|
bindingToken: string,
|
|
184
197
|
authorityFence?: ZLinkActorSessionAuthorityFence,
|
|
185
|
-
sessionIdentity?: string
|
|
198
|
+
sessionIdentity?: string,
|
|
199
|
+
commitActor?: () => void
|
|
186
200
|
): Promise<void> {
|
|
187
201
|
await this.lane.run(() =>
|
|
188
|
-
this.
|
|
202
|
+
this.commitCore(
|
|
203
|
+
undefined,
|
|
204
|
+
context,
|
|
205
|
+
actor,
|
|
206
|
+
bindingToken,
|
|
207
|
+
authorityFence,
|
|
208
|
+
sessionIdentity,
|
|
209
|
+
commitActor
|
|
210
|
+
)
|
|
189
211
|
);
|
|
190
212
|
}
|
|
191
213
|
|
|
192
|
-
private
|
|
193
|
-
|
|
194
|
-
actor: TActor,
|
|
195
|
-
bindingToken: string,
|
|
196
|
-
authorityFence?: ZLinkActorSessionAuthorityFence,
|
|
197
|
-
sessionIdentity?: string
|
|
198
|
-
): void {
|
|
199
|
-
this.routes.set(actor.actorId, {
|
|
200
|
-
context,
|
|
201
|
-
actor,
|
|
202
|
-
bindingToken,
|
|
203
|
-
sessionIdentity: sessionIdentity ?? sessionIdentityFromContext(context),
|
|
204
|
-
activeFrames: { count: 0, requests: new Set() },
|
|
205
|
-
authorityFence
|
|
206
|
-
});
|
|
207
|
-
context.bindLocal(actor, bindingToken);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async replace(
|
|
211
|
-
previous: ZLinkActorSessionRoute<TContext, TActor>,
|
|
214
|
+
private async commitCore(
|
|
215
|
+
previous: ZLinkActorSessionRoute<TContext, TActor> | undefined,
|
|
212
216
|
context: TContext,
|
|
213
217
|
actor: TActor,
|
|
214
218
|
bindingToken: string,
|
|
215
219
|
authorityFence?: ZLinkActorSessionAuthorityFence,
|
|
216
|
-
sessionIdentity?: string
|
|
220
|
+
sessionIdentity?: string,
|
|
221
|
+
commitActor?: () => void
|
|
217
222
|
): Promise<void> {
|
|
218
|
-
await this.lane.run(() =>
|
|
219
|
-
this.replaceCore(previous, context, actor, bindingToken, authorityFence, sessionIdentity)
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private replaceCore(
|
|
224
|
-
previous: ZLinkActorSessionRoute<TContext, TActor>,
|
|
225
|
-
context: TContext,
|
|
226
|
-
actor: TActor,
|
|
227
|
-
bindingToken: string,
|
|
228
|
-
authorityFence?: ZLinkActorSessionAuthorityFence,
|
|
229
|
-
sessionIdentity?: string
|
|
230
|
-
): void {
|
|
231
223
|
const current = this.routes.get(actor.actorId);
|
|
232
224
|
if (current !== previous) {
|
|
233
225
|
throw createInternalFrameworkException(
|
|
234
226
|
ZLinkFrameworkInternalErrorKind.ActorSessionNotBound,
|
|
235
|
-
`Actor '${actor.actorId}' session binding changed before route
|
|
227
|
+
`Actor '${actor.actorId}' session binding changed before route commit.`,
|
|
236
228
|
true
|
|
237
229
|
);
|
|
238
230
|
}
|
|
239
231
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
232
|
+
const sameBinding =
|
|
233
|
+
previous !== undefined &&
|
|
234
|
+
previous.context === context &&
|
|
235
|
+
previous.bindingToken === bindingToken;
|
|
236
|
+
const actorSlot = sameBinding ? previous.actorSlot : this.issueActorSlot(context);
|
|
237
|
+
commitActor?.();
|
|
238
|
+
context.bindLocal(actor, bindingToken, actorSlot);
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
if (previous !== undefined && !sameBinding) {
|
|
242
|
+
this.routes.delete(actor.actorId);
|
|
244
243
|
previous.context.unbindLocal(actor.actorId, previous.bindingToken);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
244
|
+
if (previous.actorSlot !== undefined) {
|
|
245
|
+
let unbound: Promise<void> | undefined;
|
|
246
|
+
try {
|
|
247
|
+
unbound = previous.context.actorSlotControls?.enqueueUnbound(previous.actorSlot);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
unbound = Promise.reject(error);
|
|
250
|
+
}
|
|
251
|
+
void unbound?.catch((error) =>
|
|
252
|
+
this.errorSink?.()?.reportRuntimeTaskException('retired session actor unbound', error)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
249
255
|
}
|
|
256
|
+
|
|
257
|
+
if (!sameBinding && actorSlot !== undefined) {
|
|
258
|
+
await context.actorSlotControls!.enqueueBound(actorSlot, actor.actorId);
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
context.unbindLocal(actor.actorId, bindingToken);
|
|
262
|
+
throw error;
|
|
250
263
|
}
|
|
264
|
+
|
|
251
265
|
this.routes.set(actor.actorId, {
|
|
252
266
|
context,
|
|
253
267
|
actor,
|
|
254
268
|
bindingToken,
|
|
269
|
+
actorSlot,
|
|
255
270
|
sessionIdentity: sessionIdentity ?? sessionIdentityFromContext(context),
|
|
256
|
-
activeFrames: previous
|
|
257
|
-
sealId: previous
|
|
258
|
-
authorityFence: authorityFence ?? previous
|
|
271
|
+
activeFrames: previous?.activeFrames ?? { count: 0, requests: new Set() },
|
|
272
|
+
sealId: previous?.sealId,
|
|
273
|
+
authorityFence: authorityFence ?? previous?.authorityFence
|
|
259
274
|
});
|
|
260
275
|
}
|
|
261
276
|
|
|
277
|
+
private issueActorSlot(context: TContext): number | undefined {
|
|
278
|
+
if (context.actorSlotControls === undefined) return undefined;
|
|
279
|
+
const actorSlot = this.nextActorSlots.get(context) ?? 1;
|
|
280
|
+
if (actorSlot > 0xffff) {
|
|
281
|
+
throw createInternalFrameworkException(
|
|
282
|
+
ZLinkFrameworkInternalErrorKind.InvalidOperation,
|
|
283
|
+
`Stream session '${String(context.routingId)}' exhausted its Actor slots.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
this.nextActorSlots.set(context, actorSlot + 1);
|
|
287
|
+
return actorSlot;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async replace(
|
|
291
|
+
previous: ZLinkActorSessionRoute<TContext, TActor>,
|
|
292
|
+
context: TContext,
|
|
293
|
+
actor: TActor,
|
|
294
|
+
bindingToken: string,
|
|
295
|
+
authorityFence?: ZLinkActorSessionAuthorityFence,
|
|
296
|
+
sessionIdentity?: string,
|
|
297
|
+
commitActor?: () => void
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
await this.lane.run(() =>
|
|
300
|
+
this.commitCore(
|
|
301
|
+
previous,
|
|
302
|
+
context,
|
|
303
|
+
actor,
|
|
304
|
+
bindingToken,
|
|
305
|
+
authorityFence,
|
|
306
|
+
sessionIdentity,
|
|
307
|
+
commitActor
|
|
308
|
+
)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
262
312
|
async replaceAndReleaseSeal(
|
|
263
313
|
previous: ZLinkActorSessionRoute<TContext, TActor>,
|
|
264
314
|
context: TContext,
|
|
@@ -266,7 +316,8 @@ export class ZLinkActorSessionBindingRegistry<
|
|
|
266
316
|
bindingToken: string,
|
|
267
317
|
sealId: string,
|
|
268
318
|
authorityFence?: ZLinkActorSessionAuthorityFence,
|
|
269
|
-
sessionIdentity?: string
|
|
319
|
+
sessionIdentity?: string,
|
|
320
|
+
commitActor?: () => void
|
|
270
321
|
): Promise<void> {
|
|
271
322
|
await this.lane.run(() =>
|
|
272
323
|
this.replaceAndReleaseSealCore(
|
|
@@ -276,20 +327,22 @@ export class ZLinkActorSessionBindingRegistry<
|
|
|
276
327
|
bindingToken,
|
|
277
328
|
sealId,
|
|
278
329
|
authorityFence,
|
|
279
|
-
sessionIdentity
|
|
330
|
+
sessionIdentity,
|
|
331
|
+
commitActor
|
|
280
332
|
)
|
|
281
333
|
);
|
|
282
334
|
}
|
|
283
335
|
|
|
284
|
-
private replaceAndReleaseSealCore(
|
|
336
|
+
private async replaceAndReleaseSealCore(
|
|
285
337
|
previous: ZLinkActorSessionRoute<TContext, TActor>,
|
|
286
338
|
context: TContext,
|
|
287
339
|
actor: TActor,
|
|
288
340
|
bindingToken: string,
|
|
289
341
|
sealId: string,
|
|
290
342
|
authorityFence?: ZLinkActorSessionAuthorityFence,
|
|
291
|
-
sessionIdentity?: string
|
|
292
|
-
|
|
343
|
+
sessionIdentity?: string,
|
|
344
|
+
commitActor?: () => void
|
|
345
|
+
): Promise<void> {
|
|
293
346
|
if (previous.sealId !== sealId) {
|
|
294
347
|
throw createInternalFrameworkException(
|
|
295
348
|
ZLinkFrameworkInternalErrorKind.ActorLocationStale,
|
|
@@ -300,7 +353,15 @@ export class ZLinkActorSessionBindingRegistry<
|
|
|
300
353
|
// JavaScript cannot interleave another ingress turn between these two
|
|
301
354
|
// synchronous mutations. The replacement preserves the seal, then the
|
|
302
355
|
// exact release publishes the route to held ingress as one owner turn.
|
|
303
|
-
this.
|
|
356
|
+
await this.commitCore(
|
|
357
|
+
previous,
|
|
358
|
+
context,
|
|
359
|
+
actor,
|
|
360
|
+
bindingToken,
|
|
361
|
+
authorityFence,
|
|
362
|
+
sessionIdentity,
|
|
363
|
+
commitActor
|
|
364
|
+
);
|
|
304
365
|
if (!this.abortSealCore(actor.actorId, sealId)) {
|
|
305
366
|
throw createInternalFrameworkException(
|
|
306
367
|
ZLinkFrameworkInternalErrorKind.ActorLocationStale,
|
|
@@ -348,11 +409,21 @@ export class ZLinkActorSessionBindingRegistry<
|
|
|
348
409
|
return Object.freeze({ context: route.context });
|
|
349
410
|
}
|
|
350
411
|
|
|
351
|
-
async unbind(
|
|
352
|
-
|
|
412
|
+
async unbind(
|
|
413
|
+
actorId: string,
|
|
414
|
+
context: TContext,
|
|
415
|
+
bindingToken: string,
|
|
416
|
+
termination = ZLinkActorSessionBindingTermination.BindingEnd
|
|
417
|
+
): Promise<void> {
|
|
418
|
+
await this.lane.run(() => this.unbindCore(actorId, context, bindingToken, termination));
|
|
353
419
|
}
|
|
354
420
|
|
|
355
|
-
private unbindCore(
|
|
421
|
+
private async unbindCore(
|
|
422
|
+
actorId: string,
|
|
423
|
+
context: TContext,
|
|
424
|
+
bindingToken: string,
|
|
425
|
+
termination: ZLinkActorSessionBindingTermination
|
|
426
|
+
): Promise<void> {
|
|
356
427
|
const route = this.routes.get(actorId);
|
|
357
428
|
if (route === undefined || route.context !== context || route.bindingToken !== bindingToken) {
|
|
358
429
|
return;
|
|
@@ -372,28 +443,44 @@ export class ZLinkActorSessionBindingRegistry<
|
|
|
372
443
|
route,
|
|
373
444
|
new Error(`Actor '${actorId}' session binding was removed.`)
|
|
374
445
|
);
|
|
446
|
+
if (
|
|
447
|
+
termination === ZLinkActorSessionBindingTermination.BindingEnd &&
|
|
448
|
+
route.actorSlot !== undefined
|
|
449
|
+
) {
|
|
450
|
+
await context.actorSlotControls?.enqueueUnbound(route.actorSlot);
|
|
451
|
+
}
|
|
375
452
|
}
|
|
376
453
|
|
|
377
454
|
async unbindActor(actorId: string): Promise<void> {
|
|
378
455
|
await this.lane.run(() => this.unbindActorCore(actorId));
|
|
379
456
|
}
|
|
380
457
|
|
|
381
|
-
private unbindActorCore(actorId: string): void {
|
|
458
|
+
private async unbindActorCore(actorId: string): Promise<void> {
|
|
382
459
|
const route = this.routes.get(actorId);
|
|
383
460
|
if (route === undefined) {
|
|
384
461
|
return;
|
|
385
462
|
}
|
|
386
|
-
this.unbindCore(
|
|
463
|
+
await this.unbindCore(
|
|
464
|
+
actorId,
|
|
465
|
+
route.context,
|
|
466
|
+
route.bindingToken,
|
|
467
|
+
ZLinkActorSessionBindingTermination.BindingEnd
|
|
468
|
+
);
|
|
387
469
|
}
|
|
388
470
|
|
|
389
471
|
async cleanup(context: TContext): Promise<void> {
|
|
390
472
|
await this.lane.run(() => this.cleanupCore(context));
|
|
391
473
|
}
|
|
392
474
|
|
|
393
|
-
private cleanupCore(context: TContext): void {
|
|
475
|
+
private async cleanupCore(context: TContext): Promise<void> {
|
|
394
476
|
for (const route of [...this.routes.values()]) {
|
|
395
477
|
if (route.context === context) {
|
|
396
|
-
this.unbindCore(
|
|
478
|
+
await this.unbindCore(
|
|
479
|
+
route.actor.actorId,
|
|
480
|
+
context,
|
|
481
|
+
route.bindingToken,
|
|
482
|
+
ZLinkActorSessionBindingTermination.PhysicalDisconnect
|
|
483
|
+
);
|
|
397
484
|
}
|
|
398
485
|
}
|
|
399
486
|
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
} from './actor-session-binding-registry';
|
|
14
14
|
|
|
15
15
|
export interface ZLinkActorSessionBindingRuntimeOwner {
|
|
16
|
+
actorSlot(actorId: string, sessionRid: string): Promise<number | undefined>;
|
|
16
17
|
sealRelocation(
|
|
17
18
|
claim: ZLinkActorSessionRelocationClaim,
|
|
18
19
|
expected: ZLinkActorSessionRouteFence,
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
} from './protocol';
|
|
11
11
|
import { throwIfAborted } from '../abort';
|
|
12
12
|
import { captureZLinkExecutionTurn } from '../execution';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ZLinkActorSessionBindingRegistry,
|
|
15
|
+
ZLinkActorSessionBindingTermination
|
|
16
|
+
} from './actor-session-binding-registry';
|
|
14
17
|
import { ZLinkActorSessionLifecycleCoordinator } from './actor-session-lifecycle-coordinator';
|
|
15
18
|
import { ZLinkManagedStream } from './managed-stream';
|
|
16
19
|
import { DefaultZLinkSessionActor, DefaultZLinkSessionContext } from './session-context';
|
|
@@ -246,7 +249,12 @@ export class ZLinkBoundActorRelaySender {
|
|
|
246
249
|
// The transport is already closed, so remove its exact route before
|
|
247
250
|
// invoking application lifecycle code. A reconnect can then install
|
|
248
251
|
// a successor while this best-effort notification is in flight.
|
|
249
|
-
await this.routes.unbind(
|
|
252
|
+
await this.routes.unbind(
|
|
253
|
+
actor.actorId,
|
|
254
|
+
context,
|
|
255
|
+
actor.bindingToken,
|
|
256
|
+
ZLinkActorSessionBindingTermination.PhysicalDisconnect
|
|
257
|
+
);
|
|
250
258
|
return true;
|
|
251
259
|
});
|
|
252
260
|
if (!detached) return;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Message } from '../../contracts/Common/Message';
|
|
2
|
+
import { ZLinkFrameworkErrorKind, ZLinkFrameworkException } from '../../contracts';
|
|
3
|
+
import { channelErrorCodeName } from '../channels/channel-envelope';
|
|
4
|
+
import { ZLinkRouteDisconnectedError } from '../channels/route-disconnected-error';
|
|
2
5
|
import { ZLinkSubmitStatus, type ZLinkSubmitResult } from '../messaging/submission-result';
|
|
3
6
|
import { ZLinkStreamMessageKind } from './protocol';
|
|
4
7
|
import type { ZLinkStreamFrameMessageFactory } from './stream-frame-factory';
|
|
@@ -96,7 +99,13 @@ export function boundSessionErrorPayload(error: unknown): {
|
|
|
96
99
|
readonly message: string;
|
|
97
100
|
} {
|
|
98
101
|
return {
|
|
99
|
-
code:
|
|
102
|
+
code: channelErrorCodeName(
|
|
103
|
+
error instanceof ZLinkFrameworkException
|
|
104
|
+
? error.kind
|
|
105
|
+
: error instanceof ZLinkRouteDisconnectedError
|
|
106
|
+
? ZLinkFrameworkErrorKind.Unavailable
|
|
107
|
+
: ZLinkFrameworkErrorKind.InternalFailure
|
|
108
|
+
),
|
|
100
109
|
message: errorMessage(error)
|
|
101
110
|
};
|
|
102
111
|
}
|
|
@@ -87,7 +87,9 @@ export class ZLinkBoundSessionService {
|
|
|
87
87
|
metadata,
|
|
88
88
|
false,
|
|
89
89
|
undefined,
|
|
90
|
-
message
|
|
90
|
+
message,
|
|
91
|
+
undefined,
|
|
92
|
+
this.actorSlot(route)
|
|
91
93
|
);
|
|
92
94
|
try {
|
|
93
95
|
const result = await this.requireTransport().send(actorId, frame, {
|
|
@@ -120,7 +122,9 @@ export class ZLinkBoundSessionService {
|
|
|
120
122
|
metadata,
|
|
121
123
|
false,
|
|
122
124
|
undefined,
|
|
123
|
-
message
|
|
125
|
+
message,
|
|
126
|
+
undefined,
|
|
127
|
+
this.actorSlot(route)
|
|
124
128
|
);
|
|
125
129
|
try {
|
|
126
130
|
throwIfAborted(signal);
|
|
@@ -348,7 +352,9 @@ export class ZLinkBoundSessionService {
|
|
|
348
352
|
metadata,
|
|
349
353
|
compressPayload,
|
|
350
354
|
requestSeq,
|
|
351
|
-
payload
|
|
355
|
+
payload,
|
|
356
|
+
undefined,
|
|
357
|
+
this.actorSlot(route)
|
|
352
358
|
);
|
|
353
359
|
try {
|
|
354
360
|
if ((await this.routes.route(actorId))?.bindingToken !== route.bindingToken) {
|
|
@@ -448,6 +454,10 @@ export class ZLinkBoundSessionService {
|
|
|
448
454
|
}
|
|
449
455
|
return this.options.transport;
|
|
450
456
|
}
|
|
457
|
+
|
|
458
|
+
private actorSlot(route: ZLinkStreamActorSessionRoute): number | undefined {
|
|
459
|
+
return route.actorSlot;
|
|
460
|
+
}
|
|
451
461
|
}
|
|
452
462
|
|
|
453
463
|
function requireBoundSessionGeneration(actorRef: ActorRef): bigint {
|
|
@@ -264,6 +264,7 @@ export class ZLinkStreamRuntimeManager {
|
|
|
264
264
|
const service = createService.call(meshNode, socket.nativeInstance as never);
|
|
265
265
|
const bindingOwner = actorSessionBindingRuntimeOwner(this.options.bindingRuntime);
|
|
266
266
|
registerServiceSessionBindingIngressPort(service, {
|
|
267
|
+
actorSlot: (actorId, sessionRid) => bindingOwner.actorSlot(actorId, sessionRid),
|
|
267
268
|
retainOutbound: (claim, delivery) =>
|
|
268
269
|
bindingOwner.admitRelocationOutbound(claim, delivery),
|
|
269
270
|
clearOutbound: (actorId, error) => bindingOwner.clearRelocation(actorId, error)
|
|
@@ -419,7 +420,7 @@ export class ZLinkStreamBindingRuntime {
|
|
|
419
420
|
this.routes = new ZLinkActorSessionBindingRegistry<
|
|
420
421
|
DefaultZLinkSessionContext,
|
|
421
422
|
DefaultZLinkSessionActor
|
|
422
|
-
>(4096, 4096, runtimeOptions.sessionRelocationSealTimeoutMs ?? 3_000);
|
|
423
|
+
>(4096, 4096, runtimeOptions.sessionRelocationSealTimeoutMs ?? 3_000, runtimeOptions.errorSink);
|
|
423
424
|
this.compressionCodec = resolveStreamCompressionCodec(runtimeOptions.streamCompression);
|
|
424
425
|
this.frameMessages = new ZLinkStreamFrameMessageFactory(runtimeOptions);
|
|
425
426
|
this.boundSessions = new ZLinkBoundSessionService(
|
|
@@ -441,6 +442,12 @@ export class ZLinkStreamBindingRuntime {
|
|
|
441
442
|
actorSessionLifecycle
|
|
442
443
|
);
|
|
443
444
|
registerActorSessionBindingRuntimeOwner(this, {
|
|
445
|
+
actorSlot: async (actorId, sessionRid) => {
|
|
446
|
+
const route = await this.routes.route(actorId);
|
|
447
|
+
return route !== undefined && route.sessionIdentity === sessionRid
|
|
448
|
+
? route.actorSlot
|
|
449
|
+
: undefined;
|
|
450
|
+
},
|
|
444
451
|
sealRelocation: (claim, expected, signal) =>
|
|
445
452
|
this.routes.sealRelocation(claim, expected, signal),
|
|
446
453
|
relocationSnapshot: (actorId, sealId) => this.routes.relocationSnapshot(actorId, sealId),
|
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
type ZLinkMeshCompletionTable
|
|
34
34
|
} from '../backend/mesh-completion-table';
|
|
35
35
|
import {
|
|
36
|
+
encodeActorBoundFrame,
|
|
37
|
+
encodeActorUnboundFrame,
|
|
36
38
|
encodeSessionClosingFrame,
|
|
37
39
|
encodeStreamControlFrame,
|
|
38
40
|
ZLinkStreamCloseReasonCode
|
|
@@ -51,18 +53,17 @@ export interface ZLinkNativeSessionRoute {
|
|
|
51
53
|
readonly completions: ZLinkMeshCompletionTable;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
interface ZLinkManagedActorBinding {
|
|
57
|
+
readonly actor: ZLinkBackendActorRef;
|
|
58
|
+
readonly bindingGeneration?: bigint;
|
|
59
|
+
readonly route?: ZLinkNativeSessionRoute;
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
export class ZLinkManagedStream implements ZLinkStream {
|
|
55
63
|
private currentLocalAddr: string | undefined;
|
|
56
64
|
private currentRemoteAddr: string | undefined;
|
|
57
65
|
private transportClosed = false;
|
|
58
|
-
private readonly nativeActorBindings = new Map<
|
|
59
|
-
string,
|
|
60
|
-
{
|
|
61
|
-
readonly actor: ZLinkBackendActorRef;
|
|
62
|
-
readonly bindingGeneration: bigint;
|
|
63
|
-
readonly route?: ZLinkNativeSessionRoute;
|
|
64
|
-
}
|
|
65
|
-
>();
|
|
66
|
+
private readonly nativeActorBindings = new Map<string, ZLinkManagedActorBinding>();
|
|
66
67
|
|
|
67
68
|
constructor(
|
|
68
69
|
private readonly socket: ZLinkBackendStreamSocket,
|
|
@@ -195,12 +196,12 @@ export class ZLinkManagedStream implements ZLinkStream {
|
|
|
195
196
|
`Stream session '${this.sessionId}' is disconnected.`
|
|
196
197
|
);
|
|
197
198
|
}
|
|
199
|
+
const nativeActor = toNativeActorRef(actor);
|
|
198
200
|
const route = this.nativeRoute(actor.meshName);
|
|
199
201
|
if (route !== undefined) {
|
|
200
202
|
if (route.service.status().state === 1) {
|
|
201
203
|
route.service.start();
|
|
202
204
|
}
|
|
203
|
-
const nativeActor = toNativeActorRef(actor);
|
|
204
205
|
await this.ensureNativeActorRoute(route, actor, timeoutMs, signal);
|
|
205
206
|
if (this.isTransportClosed()) {
|
|
206
207
|
throw createInternalFrameworkException(
|
|
@@ -278,6 +279,7 @@ export class ZLinkManagedStream implements ZLinkStream {
|
|
|
278
279
|
timeoutMs,
|
|
279
280
|
signal
|
|
280
281
|
);
|
|
282
|
+
this.nativeActorBindings.set(actor.actorId, { actor: nativeActor });
|
|
281
283
|
}
|
|
282
284
|
|
|
283
285
|
private async ensureNativeActorRoute(
|
|
@@ -327,7 +329,7 @@ export class ZLinkManagedStream implements ZLinkStream {
|
|
|
327
329
|
const binding = this.nativeActorBindings.get(actorId);
|
|
328
330
|
if (binding?.route !== undefined) {
|
|
329
331
|
const route = binding.route;
|
|
330
|
-
if (!this.hasNativeBinding(route, actorId, binding.bindingGeneration)) {
|
|
332
|
+
if (!this.hasNativeBinding(route, actorId, binding.bindingGeneration!)) {
|
|
331
333
|
this.nativeActorBindings.delete(actorId);
|
|
332
334
|
return;
|
|
333
335
|
}
|
|
@@ -338,7 +340,7 @@ export class ZLinkManagedStream implements ZLinkStream {
|
|
|
338
340
|
route.service.unbindActor(
|
|
339
341
|
this.backendRoutingId(),
|
|
340
342
|
binding.actor as never,
|
|
341
|
-
binding.bindingGeneration
|
|
343
|
+
binding.bindingGeneration!,
|
|
342
344
|
timeoutMs
|
|
343
345
|
),
|
|
344
346
|
signal
|
|
@@ -354,7 +356,7 @@ export class ZLinkManagedStream implements ZLinkStream {
|
|
|
354
356
|
// tombstone. A transport teardown can therefore report an internal
|
|
355
357
|
// completion after the exact binding is already gone. Treat only that
|
|
356
358
|
// exact missing binding as stale cleanup; preserve other failures.
|
|
357
|
-
if (this.hasNativeBinding(route, actorId, binding.bindingGeneration)) {
|
|
359
|
+
if (this.hasNativeBinding(route, actorId, binding.bindingGeneration!)) {
|
|
358
360
|
throw error;
|
|
359
361
|
}
|
|
360
362
|
}
|
|
@@ -364,6 +366,26 @@ export class ZLinkManagedStream implements ZLinkStream {
|
|
|
364
366
|
return;
|
|
365
367
|
}
|
|
366
368
|
await this.socket.unbindActor(this.backendRoutingId(), actorId, timeoutMs, signal);
|
|
369
|
+
if (binding !== undefined && this.nativeActorBindings.get(actorId) === binding) {
|
|
370
|
+
this.nativeActorBindings.delete(actorId);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async enqueueActorBound(actorSlot: number, actorId: string): Promise<void> {
|
|
375
|
+
await this.submitActorBindingControl(encodeActorBoundFrame(actorSlot, actorId));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async enqueueActorUnbound(actorSlot: number): Promise<void> {
|
|
379
|
+
await this.submitActorBindingControl(encodeActorUnboundFrame(actorSlot));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private async submitActorBindingControl(frame: Uint8Array): Promise<void> {
|
|
383
|
+
const message = NativeMessage.from(frame);
|
|
384
|
+
try {
|
|
385
|
+
await this.socket.submit(this.backendRoutingId(), message);
|
|
386
|
+
} finally {
|
|
387
|
+
message.close();
|
|
388
|
+
}
|
|
367
389
|
}
|
|
368
390
|
|
|
369
391
|
async sendBoundActor(
|