@zlink-systems/framework 0.28.0 → 0.29.1

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.
Files changed (112) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/contracts/Configuration/Builders.d.ts +2 -0
  3. package/dist/contracts/Configuration/InternalDefaults.d.ts +3 -0
  4. package/dist/contracts/Configuration/InternalDefaults.js +4 -1
  5. package/dist/contracts/Configuration/RegistrationBuilders.js +9 -0
  6. package/dist/contracts/Configuration/RegistrationNormalizers.d.ts +1 -0
  7. package/dist/contracts/Configuration/RegistrationNormalizers.js +16 -0
  8. package/dist/contracts/Configuration/RegistrationTypes.d.ts +3 -0
  9. package/dist/runtime/actors/actor-mailbox.d.ts +2 -2
  10. package/dist/runtime/actors/actor-mailbox.js +2 -2
  11. package/dist/runtime/actors/actor-runtime-contracts.d.ts +1 -0
  12. package/dist/runtime/actors/index.d.ts +2 -0
  13. package/dist/runtime/actors/index.js +21 -6
  14. package/dist/runtime/backend/node/node-raw-mesh-backend.js +1 -3
  15. package/dist/runtime/execution/serial-execution-queue.d.ts +12 -3
  16. package/dist/runtime/execution/serial-execution-queue.js +116 -28
  17. package/dist/runtime/foundation/operation-registry.d.ts +3 -2
  18. package/dist/runtime/foundation/operation-registry.js +5 -1
  19. package/dist/runtime/foundation/service-authority-payload-codec.d.ts +0 -2
  20. package/dist/runtime/foundation/service-authority-payload-codec.js +0 -43
  21. package/dist/runtime/foundation/service-stateful-registry.d.ts +4 -3
  22. package/dist/runtime/foundation/service-stateful-registry.js +5 -2
  23. package/dist/runtime/foundation/service-stateful-runtime.d.ts +1 -8
  24. package/dist/runtime/foundation/service-stateful-runtime.js +105 -145
  25. package/dist/runtime/foundation/service-stateful-wire-codec.d.ts +0 -11
  26. package/dist/runtime/foundation/service-stateful-wire-codec.js +1 -23
  27. package/dist/runtime/foundation/service-topology-registry.d.ts +5 -0
  28. package/dist/runtime/foundation/service-topology-registry.js +16 -17
  29. package/dist/runtime/handlers/handler-instance-scope.js +1 -1
  30. package/dist/runtime/host/actor-placement-coordinator.d.ts +2 -1
  31. package/dist/runtime/host/actor-placement-coordinator.js +2 -1
  32. package/dist/runtime/host/actor-runtime-options-factory.d.ts +2 -1
  33. package/dist/runtime/host/actor-runtime-options-factory.js +1 -0
  34. package/dist/runtime/host/index.d.ts +1 -1
  35. package/dist/runtime/host/index.js +3 -2
  36. package/dist/runtime/host/instance-activation-authority.d.ts +0 -1
  37. package/dist/runtime/host/instance-activation-authority.js +46 -97
  38. package/dist/runtime/host/spot-address-transport.js +1 -1
  39. package/dist/runtime/host/stateful-authority-route-runtime.d.ts +1 -0
  40. package/dist/runtime/host/stateful-authority-route-runtime.js +17 -9
  41. package/dist/runtime/locations/domain-store-contract.d.ts +3 -1
  42. package/dist/runtime/locations/in-memory-authority-store.d.ts +3 -0
  43. package/dist/runtime/locations/in-memory-authority-store.js +47 -12
  44. package/dist/runtime/locations/in-memory-location-store.d.ts +2 -1
  45. package/dist/runtime/locations/in-memory-location-store.js +15 -10
  46. package/dist/runtime/locations/internal-location-contracts.d.ts +5 -0
  47. package/dist/runtime/locations/internal-store-contracts.d.ts +1 -1
  48. package/dist/runtime/locations/location-store-repository.d.ts +2 -0
  49. package/dist/runtime/locations/location-store-repository.js +130 -54
  50. package/dist/runtime/spots/spot-entry-activation.d.ts +1 -0
  51. package/dist/runtime/spots/spot-entry-activation.js +3 -0
  52. package/dist/runtime/spots/spot-node-runtime-manager.d.ts +1 -0
  53. package/dist/runtime/spots/spot-node-runtime-manager.js +6 -0
  54. package/dist/runtime/spots/spot-serial-executor.d.ts +1 -0
  55. package/dist/runtime/spots/spot-serial-executor.js +14 -2
  56. package/dist/runtime/spots/spot-serial-turn-executor.d.ts +3 -0
  57. package/dist/runtime/spots/spot-serial-turn-executor.js +5 -0
  58. package/dist/runtime/streams/index.d.ts +0 -1
  59. package/dist/runtime/streams/index.js +4 -3
  60. package/dist/runtime/streams/protocol.js +0 -9
  61. package/dist/runtime/streams/session-context.d.ts +0 -4
  62. package/dist/runtime/streams/session-context.js +0 -26
  63. package/dist/runtime/streams/session-serial-executor.d.ts +0 -1
  64. package/dist/runtime/streams/session-serial-executor.js +0 -3
  65. package/dist/runtime/streams/stream-message-utils.d.ts +0 -1
  66. package/dist/runtime/streams/stream-message-utils.js +0 -20
  67. package/dist/runtime/streams/stream-session-runtime.d.ts +6 -3
  68. package/dist/runtime/streams/stream-session-runtime.js +40 -85
  69. package/package.json +3 -3
  70. package/src/contracts/Configuration/Builders.ts +2 -0
  71. package/src/contracts/Configuration/InternalDefaults.ts +4 -0
  72. package/src/contracts/Configuration/RegistrationBuilders.ts +13 -0
  73. package/src/contracts/Configuration/RegistrationNormalizers.ts +25 -0
  74. package/src/contracts/Configuration/RegistrationTypes.ts +3 -0
  75. package/src/runtime/actors/actor-mailbox.ts +7 -2
  76. package/src/runtime/actors/actor-runtime-contracts.ts +4 -0
  77. package/src/runtime/actors/index.ts +77 -42
  78. package/src/runtime/backend/node/node-raw-mesh-backend.ts +1 -7
  79. package/src/runtime/execution/serial-execution-queue.ts +117 -29
  80. package/src/runtime/foundation/operation-registry.ts +16 -9
  81. package/src/runtime/foundation/service-authority-payload-codec.ts +0 -46
  82. package/src/runtime/foundation/service-stateful-registry.ts +10 -5
  83. package/src/runtime/foundation/service-stateful-runtime.ts +152 -224
  84. package/src/runtime/foundation/service-stateful-wire-codec.ts +0 -25
  85. package/src/runtime/foundation/service-topology-registry.ts +30 -20
  86. package/src/runtime/handlers/handler-instance-scope.ts +2 -1
  87. package/src/runtime/host/actor-placement-coordinator.ts +8 -2
  88. package/src/runtime/host/actor-runtime-options-factory.ts +6 -0
  89. package/src/runtime/host/index.ts +6 -2
  90. package/src/runtime/host/instance-activation-authority.ts +58 -125
  91. package/src/runtime/host/spot-address-transport.ts +1 -1
  92. package/src/runtime/host/stateful-authority-route-runtime.ts +22 -9
  93. package/src/runtime/locations/domain-store-contract.ts +6 -1
  94. package/src/runtime/locations/in-memory-authority-store.ts +83 -12
  95. package/src/runtime/locations/in-memory-location-store.ts +22 -10
  96. package/src/runtime/locations/internal-location-contracts.ts +6 -0
  97. package/src/runtime/locations/internal-store-contracts.ts +1 -0
  98. package/src/runtime/locations/location-store-repository.ts +182 -69
  99. package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +1 -1
  100. package/src/runtime/spots/spot-entry-activation.ts +4 -0
  101. package/src/runtime/spots/spot-node-runtime-manager.ts +7 -0
  102. package/src/runtime/spots/spot-serial-executor.ts +14 -5
  103. package/src/runtime/spots/spot-serial-turn-executor.ts +10 -0
  104. package/src/runtime/streams/index.ts +3 -1
  105. package/src/runtime/streams/protocol.ts +0 -13
  106. package/src/runtime/streams/session-context.ts +0 -32
  107. package/src/runtime/streams/session-serial-executor.ts +0 -8
  108. package/src/runtime/streams/stream-message-utils.ts +1 -27
  109. package/src/runtime/streams/stream-session-runtime.ts +54 -97
  110. package/dist/runtime/streams/session-requests.d.ts +0 -25
  111. package/dist/runtime/streams/session-requests.js +0 -101
  112. package/src/runtime/streams/session-requests.ts +0 -110
@@ -7,6 +7,7 @@ import { UINT64_MAX } from '@zlink-systems/stream-wire';
7
7
  import { ZLINK_MAX_ROUTING_ID_BYTES } from '../../contracts/Common/CoreTypes';
8
8
  import { ZLINK_PROVIDER_MAX_PAGE_SIZE } from '../../contracts/Locations/Stores';
9
9
  import { randomUUID } from 'node:crypto';
10
+ import { ZLinkFrameworkException, ZLinkFrameworkErrorKind } from '../../contracts';
10
11
  import { SHA256_DIGEST_BYTES } from '../foundation/actor-join-recovery-codec';
11
12
  import type {
12
13
  ZLinkAggregateAbortResult,
@@ -41,13 +42,16 @@ import type {
41
42
  } from './internal-location-contracts';
42
43
  import { encodeAuthorityKey } from './authority-key-codec';
43
44
  import { creationTerminalPreimage } from './opaque-record-key';
45
+ import { ServiceRelocationAuthorityPayloadCodec } from '../foundation/service-relocation-runtime';
44
46
 
45
47
  const MAX_GENERATION = 0x7fff_ffff_ffff_ffffn;
46
48
 
47
49
  const CREATION_TERMINAL_RETENTION_MS = 5 * 60 * 1000;
48
50
  const MAX_U64 = UINT64_MAX;
51
+ const relocationAuthorityCodec = new ServiceRelocationAuthorityPayloadCodec();
49
52
 
50
53
  export interface ZLinkInMemoryAuthorityValidation {
54
+ isOwnerLive(owner: ZLinkLocationOwnerToken): boolean;
51
55
  isTargetLive(
52
56
  descriptor: ZLinkMeshNodeDescriptorKey,
53
57
  lifecycleGeneration: bigint,
@@ -276,6 +280,75 @@ export class ZLinkInMemoryAuthorityStore {
276
280
  return BigInt(removed);
277
281
  }
278
282
 
283
+ async releaseEndedReservation(
284
+ key: ZLinkAuthorityKey,
285
+ expectedStoreVersion: string,
286
+ signal?: AbortSignal
287
+ ): Promise<boolean> {
288
+ signal?.throwIfAborted();
289
+ return this.tryReleaseEndedReservation(key, expectedStoreVersion);
290
+ }
291
+
292
+ private tryReleaseEndedReservation(
293
+ key: ZLinkAuthorityKey,
294
+ expectedStoreVersion: string,
295
+ request?: ZLinkObjectReserveRequest
296
+ ): boolean {
297
+ const current = this.rows.get(key.value);
298
+ if (
299
+ current === undefined ||
300
+ current.snapshot.storeVersion.value !== expectedStoreVersion ||
301
+ (current.snapshot.allocation.state === 'active'
302
+ ? request?.key.kind !== 'actor'
303
+ : current.creation === undefined)
304
+ )
305
+ return false;
306
+ if (current.snapshot.allocation.state === 'active') {
307
+ for (const aggregate of this.aggregates.values()) {
308
+ if (
309
+ aggregate.state === 'prepared' &&
310
+ aggregate.request.participants.some(
311
+ (participant) => participant.authorityKey.value === key.value
312
+ )
313
+ )
314
+ return false;
315
+ }
316
+ if (relocationAuthorityCodec.read(current.snapshot.payload) !== undefined) return false;
317
+ if (
318
+ this.validation.isOwnerLive({
319
+ ownerId: current.snapshot.ownerId,
320
+ leaseGeneration: current.snapshot.ownerLeaseGeneration
321
+ })
322
+ )
323
+ return false;
324
+ if (request?.actorRelocationPolicy !== 'disabled')
325
+ throw new ZLinkFrameworkException(
326
+ ZLinkFrameworkErrorKind.Unavailable,
327
+ 'Actor owner lease is unavailable.'
328
+ );
329
+ } else if (
330
+ this.validation.isTargetLive(
331
+ current.snapshot.allocation.descriptor,
332
+ current.snapshot.allocation.descriptorLifecycleGeneration,
333
+ {
334
+ ownerId: current.snapshot.ownerId,
335
+ leaseGeneration: current.snapshot.ownerLeaseGeneration
336
+ }
337
+ )
338
+ )
339
+ return false;
340
+ this.adjustCapacity(
341
+ current.snapshot.allocation.state === 'active' ? this.activeCapacity : this.pendingCapacity,
342
+ current.snapshot.allocation,
343
+ -1
344
+ );
345
+ this.rows.delete(key.value);
346
+ if (current.snapshot.allocation.state === 'reserved' && current.creation !== undefined)
347
+ this.creationTerminals.set(current.creation.reservationId, 'aborted');
348
+ this.scanRevision++;
349
+ return true;
350
+ }
351
+
279
352
  async reserve(
280
353
  request: ZLinkObjectReserveRequest,
281
354
  signal?: AbortSignal
@@ -294,18 +367,16 @@ export class ZLinkInMemoryAuthorityStore {
294
367
  ) {
295
368
  return { kind: 'typeMismatch', current: this.snapshot(current.snapshot) };
296
369
  }
297
- if (current.snapshot.allocation.state === 'active') {
298
- return { kind: 'alreadyExists', current: this.snapshot(current.snapshot) };
299
- }
300
- // An unfinished creation whose owner lease ended is cancellable.
301
- // Reclaim it so the key does not stay blocked forever.
302
- if (this.isOwnerLive(current.snapshot) || current.creation === undefined) {
303
- return { kind: 'conflict', current: this.read(key) };
304
- }
305
- this.adjustCapacity(this.pendingCapacity, current.snapshot.allocation, -1);
306
- this.rows.delete(key);
307
- this.creationTerminals.set(current.creation.reservationId, 'aborted');
308
- this.scanRevision++;
370
+ if (
371
+ !this.tryReleaseEndedReservation(
372
+ encodeAuthorityKey(request.key.kind, request.key.globalId),
373
+ current.snapshot.storeVersion.value,
374
+ request
375
+ )
376
+ )
377
+ return current.snapshot.allocation.state === 'active'
378
+ ? { kind: 'alreadyExists', current: this.snapshot(current.snapshot) }
379
+ : { kind: 'conflict', current: this.read(key) };
309
380
  }
310
381
  const target = creationTarget(request);
311
382
  if (
@@ -99,11 +99,14 @@ export class ZLinkInMemoryLocationStore
99
99
  private readonly authority: ZLinkInMemoryAuthorityStore;
100
100
 
101
101
  constructor(private readonly now: () => Date = () => new Date()) {
102
+ const isOwnerLive = (owner: ZLinkLocationOwnerToken): boolean =>
103
+ this.liveOwnerLease(owner.ownerId, this.now())?.token.leaseGeneration ===
104
+ owner.leaseGeneration;
102
105
  this.authority = new ZLinkInMemoryAuthorityStore(
103
106
  {
107
+ isOwnerLive,
104
108
  isTargetLive: (key, lifecycleGeneration, owner) => {
105
109
  const descriptor = this.meshNodes.rows.get(meshNodeKey(key.meshName, key.rid));
106
- const lease = this.leases.get(owner.ownerId);
107
110
  return (
108
111
  descriptor !== undefined &&
109
112
  descriptor.lifecycleGeneration === lifecycleGeneration &&
@@ -111,9 +114,7 @@ export class ZLinkInMemoryLocationStore
111
114
  descriptor.leaseGeneration === owner.leaseGeneration &&
112
115
  descriptor.objectRole === ZLinkObjectRole.Server &&
113
116
  descriptor.state === ZLinkFrameworkRuntimeState.Serving &&
114
- lease !== undefined &&
115
- lease.token.leaseGeneration === owner.leaseGeneration &&
116
- lease.leaseExpiresAt.getTime() > this.now().getTime()
117
+ isOwnerLive(owner)
117
118
  );
118
119
  },
119
120
  placementCapacityAvailable: (key, requested, reserved, active) => {
@@ -201,6 +202,14 @@ export class ZLinkInMemoryLocationStore
201
202
  return this.authority.readCreationTerminal(operation, signal);
202
203
  }
203
204
 
205
+ async releaseEndedReservation(
206
+ key: ZLinkAuthorityKey,
207
+ expectedStoreVersion: string,
208
+ signal?: AbortSignal
209
+ ): Promise<boolean> {
210
+ return this.authority.releaseEndedReservation(key, expectedStoreVersion, signal);
211
+ }
212
+
204
213
  async abort(
205
214
  request: ZLinkObjectAbortRequest,
206
215
  signal?: AbortSignal
@@ -558,7 +567,8 @@ export class ZLinkInMemoryLocationStore
558
567
  const storeNow = this.now();
559
568
  return pageRows(
560
569
  this.fanoutPublishers,
561
- (row) => row.channelName === channelName && this.isOwnerLive(row.ownerId, storeNow),
570
+ (row) =>
571
+ row.channelName === channelName && this.liveOwnerLease(row.ownerId, storeNow) !== undefined,
562
572
  page
563
573
  );
564
574
  }
@@ -620,7 +630,7 @@ export class ZLinkInMemoryLocationStore
620
630
  if (
621
631
  intent === ZLinkLocationWriteIntent.NewClaim &&
622
632
  current !== undefined &&
623
- this.isOwnerLive(current.ownerId, updatedAt)
633
+ this.liveOwnerLease(current.ownerId, updatedAt) !== undefined
624
634
  ) {
625
635
  return rejectedConflict();
626
636
  }
@@ -698,7 +708,7 @@ export class ZLinkInMemoryLocationStore
698
708
  if (
699
709
  intent === ZLinkLocationWriteIntent.NewClaim &&
700
710
  current !== undefined &&
701
- this.isOwnerLive(current.ownerId, updatedAt)
711
+ this.liveOwnerLease(current.ownerId, updatedAt) !== undefined
702
712
  ) {
703
713
  return rejectedConflict();
704
714
  }
@@ -1147,7 +1157,7 @@ export class ZLinkInMemoryLocationStore
1147
1157
  if (
1148
1158
  intent === ZLinkLocationWriteIntent.NewClaim &&
1149
1159
  current !== undefined &&
1150
- this.isOwnerLive(ownerOf(current), updatedAt)
1160
+ this.liveOwnerLease(ownerOf(current), updatedAt) !== undefined
1151
1161
  ) {
1152
1162
  return rejectedConflict();
1153
1163
  }
@@ -1244,9 +1254,11 @@ export class ZLinkInMemoryLocationStore
1244
1254
  return removed;
1245
1255
  }
1246
1256
 
1247
- private isOwnerLive(ownerId: string, now: Date): boolean {
1257
+ private liveOwnerLease(ownerId: string, now: Date): InMemoryOwnerLease | undefined {
1248
1258
  const lease = this.leases.get(ownerId);
1249
- return lease !== undefined && lease.leaseExpiresAt.getTime() > now.getTime();
1259
+ return lease !== undefined && lease.leaseExpiresAt.getTime() > now.getTime()
1260
+ ? lease
1261
+ : undefined;
1250
1262
  }
1251
1263
 
1252
1264
  private bump(kind: ZLinkLocationKind, meshName: string | undefined): void {
@@ -1,4 +1,10 @@
1
1
  export * from '../../contracts/Locations';
2
+ import type { ZLinkObjectReserveRequest as ReserveRequest } from '../../contracts/Locations/Authority';
3
+ import type { ZLinkRelocationConfiguration } from '../../contracts/Configuration/RegistrationTypes';
4
+
5
+ export interface ZLinkObjectReserveRequest extends ReserveRequest {
6
+ readonly actorRelocationPolicy?: ZLinkRelocationConfiguration<unknown>['kind'];
7
+ }
2
8
  export type * from '../../contracts/Locations/Authority';
3
9
  export type {
4
10
  ZLinkActorLocation,
@@ -32,6 +32,7 @@ export type ZLinkObjectCreationStore = Pick<
32
32
  | 'commit'
33
33
  | 'completeCreation'
34
34
  | 'abort'
35
+ | 'releaseEndedReservation'
35
36
  | 'prepareAggregate'
36
37
  | 'commitAggregate'
37
38
  | 'abortAggregate'
@@ -1,3 +1,4 @@
1
+ import { ServiceRelocationAuthorityPayloadCodec } from '../foundation/service-relocation-runtime';
1
2
  import { UINT64_MAX } from '@zlink-systems/stream-wire';
2
3
  import { createHash, randomUUID } from 'node:crypto';
3
4
  import { zlinkRuntimeDefaultLocationOptions } from '../../contracts/Locations/Options';
@@ -17,6 +18,8 @@ import {
17
18
  type ZLinkStoreScanCursor,
18
19
  type ZLinkStoreVersion,
19
20
  type ZLinkStoreWriteRequest,
21
+ ZLinkFrameworkException,
22
+ ZLinkFrameworkErrorKind,
20
23
  ZLinkFrameworkRuntimeState,
21
24
  ZLinkObjectRole
22
25
  } from '../../contracts';
@@ -204,7 +207,30 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
204
207
  private readonly nowProvider: () => Date = () => new Date()
205
208
  ) {
206
209
  super(nowProvider);
207
- this.provider = provider;
210
+ // Classify only failures of the opaque SPI, before interpreting its records.
211
+ this.provider = {
212
+ async read(key, signal) {
213
+ try {
214
+ return await provider.read(key, signal);
215
+ } catch (error) {
216
+ throw locationStoreProviderFailure(error, signal);
217
+ }
218
+ },
219
+ async write(request, signal) {
220
+ try {
221
+ return await provider.write(request, signal);
222
+ } catch (error) {
223
+ throw locationStoreProviderFailure(error, signal);
224
+ }
225
+ },
226
+ async scan(request, signal) {
227
+ try {
228
+ return await provider.scan(request, signal);
229
+ } catch (error) {
230
+ throw locationStoreProviderFailure(error, signal);
231
+ }
232
+ }
233
+ };
208
234
  this.aggregateInventory = new ZLinkAggregateInventoryStore(this.provider);
209
235
  }
210
236
 
@@ -1099,6 +1125,118 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1099
1125
  }
1100
1126
  }
1101
1127
 
1128
+ override async releaseEndedReservation(
1129
+ key: ZLinkAuthorityKey,
1130
+ expectedStoreVersion: string,
1131
+ signal?: AbortSignal
1132
+ ): Promise<boolean> {
1133
+ return (
1134
+ (await this.tryReleaseEndedReservation(key, expectedStoreVersion, signal)) === 'reclaimed'
1135
+ );
1136
+ }
1137
+
1138
+ private async tryReleaseEndedReservation(
1139
+ key: ZLinkAuthorityKey,
1140
+ expectedStoreVersion: string,
1141
+ signal?: AbortSignal,
1142
+ request?: ZLinkObjectReserveRequest
1143
+ ): Promise<'notReclaimable' | 'conflict' | 'reclaimed'> {
1144
+ const rowKey = authorityKey(key.value);
1145
+ const current = await this.provider.read(rowKey, signal);
1146
+ if (current.kind === 'missing' || current.value.version.value !== expectedStoreVersion)
1147
+ return 'conflict';
1148
+ const record = decodeAuthorityRecord(current.value.bytes);
1149
+ const snapshot = record.snapshot;
1150
+ if (
1151
+ (snapshot.allocation.state === 'active'
1152
+ ? request?.key.kind !== 'actor'
1153
+ : snapshot.pendingCreation === undefined) ||
1154
+ record.aggregate !== undefined ||
1155
+ hasRelocationAuthority(snapshot.payload)
1156
+ )
1157
+ return 'notReclaimable';
1158
+ const leaseKey = ownerKey(snapshot.ownerId);
1159
+ const descriptorKey = meshKey(
1160
+ snapshot.allocation.descriptor.meshName,
1161
+ snapshot.allocation.descriptor.rid
1162
+ );
1163
+ const capacityRowKey = capacityKey(
1164
+ snapshot.allocation.descriptor.meshName,
1165
+ String(snapshot.allocation.descriptor.rid)
1166
+ );
1167
+ const [lease, descriptor, capacity] = await Promise.all([
1168
+ this.provider.read(leaseKey, signal),
1169
+ this.provider.read(descriptorKey, signal),
1170
+ this.provider.read(capacityRowKey, signal)
1171
+ ]);
1172
+ if (lease.kind === 'found' && lease.value.expiresAt === undefined)
1173
+ throw new Error('Location Store owner lease record is invalid.');
1174
+ const ownerLive = sameLiveOwner(lease, snapshot);
1175
+ if (snapshot.allocation.state === 'active') {
1176
+ if (ownerLive) return 'notReclaimable';
1177
+ if (request?.actorRelocationPolicy !== 'disabled')
1178
+ throw new ZLinkFrameworkException(
1179
+ ZLinkFrameworkErrorKind.Unavailable,
1180
+ 'Actor owner lease is unavailable.'
1181
+ );
1182
+ } else if (
1183
+ ownerLive &&
1184
+ descriptor.kind === 'found' &&
1185
+ reviveMeshDescriptor(
1186
+ decodeCanonicalDescriptorRecord<ZLinkMeshNodeDescriptor>(descriptor.value.bytes).descriptor
1187
+ ).lifecycleGeneration === snapshot.allocation.descriptorLifecycleGeneration
1188
+ )
1189
+ return 'notReclaimable';
1190
+ const mutations: ZLinkStoreWriteRequest['mutations'][number][] = [
1191
+ { kind: 'delete', key: rowKey }
1192
+ ];
1193
+ if (capacity.kind === 'found') {
1194
+ const stored = decodeJson<CapacityRecord>(capacity.value.bytes);
1195
+ try {
1196
+ mutations.push({
1197
+ kind: 'put',
1198
+ key: capacityRowKey,
1199
+ bytes: encodeJson({
1200
+ active:
1201
+ snapshot.allocation.state === 'active'
1202
+ ? subtractCapacity(stored.active, snapshot.allocation.capacity)
1203
+ : stored.active,
1204
+ pending:
1205
+ snapshot.allocation.state === 'reserved'
1206
+ ? subtractCapacity(stored.pending, snapshot.allocation.capacity)
1207
+ : stored.pending
1208
+ } satisfies CapacityRecord)
1209
+ });
1210
+ } catch (error) {
1211
+ if (
1212
+ !(error instanceof ZLinkFrameworkException) ||
1213
+ error.kind !== ZLinkFrameworkErrorKind.DataLost
1214
+ )
1215
+ throw error;
1216
+ const observed = await this.provider.read(rowKey, signal);
1217
+ if (
1218
+ observed.kind === 'missing' ||
1219
+ observed.value.version.value !== current.value.version.value
1220
+ )
1221
+ return 'conflict';
1222
+ throw error;
1223
+ }
1224
+ }
1225
+ const result = await this.provider.write(
1226
+ {
1227
+ conditions: [
1228
+ { kind: 'version', key: rowKey, expected: current.value.version },
1229
+ conditionFor(leaseKey, lease),
1230
+ conditionFor(descriptorKey, descriptor),
1231
+ conditionFor(capacityRowKey, capacity)
1232
+ ],
1233
+ mutations
1234
+ },
1235
+ signal
1236
+ );
1237
+ return result.kind === 'applied' ? 'reclaimed' : 'conflict';
1238
+ }
1239
+
1102
1240
  override async reserve(
1103
1241
  request: ZLinkObjectReserveRequest,
1104
1242
  signal?: AbortSignal,
@@ -1106,6 +1244,7 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1106
1244
  ): Promise<ZLinkObjectReserveResult> {
1107
1245
  const encodedAuthorityKey = encodeAuthorityKey(request.key.kind, request.key.globalId);
1108
1246
  const reservationId = randomUUID();
1247
+ let reclaimVersion: string | undefined;
1109
1248
  for (;;) {
1110
1249
  throwIfOperationExpired(signal, deadlineUnixMs);
1111
1250
  const rowKey = authorityKey(encodedAuthorityKey.value);
@@ -1126,68 +1265,23 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1126
1265
  ) {
1127
1266
  return { kind: 'typeMismatch', current: snapshot };
1128
1267
  }
1129
- if (snapshot.allocation.state === 'active') {
1130
- return { kind: 'alreadyExists', current: snapshot };
1131
- }
1132
- if (
1133
- record.aggregate !== undefined ||
1134
- isCanonicalAuthorityPayload(record.snapshot.payload)
1135
- ) {
1136
- // A relocation record carries its own recovery protocol. Reserve
1137
- // may reclaim only an unfinished plain creation.
1268
+ if (reclaimVersion !== undefined && snapshot.storeVersion.value !== reclaimVersion)
1138
1269
  return { kind: 'conflict', current: snapshot };
1139
- }
1140
- // The row is Reserved by another owner. An unfinished creation
1141
- // whose owner lease ended is cancellable, so reclaim it and retry
1142
- // instead of blocking the key forever.
1143
- const staleOwnerKey = ownerKey(snapshot.ownerId);
1144
- const staleCapacityKey = capacityKey(
1145
- snapshot.allocation.descriptor.meshName,
1146
- String(snapshot.allocation.descriptor.rid)
1147
- );
1148
- const [staleOwnerRead, staleCapacityRead] = await Promise.all([
1149
- this.provider.read(staleOwnerKey, signal),
1150
- this.provider.read(staleCapacityKey, signal)
1151
- ]);
1152
- if (staleOwnerRead.kind === 'found' && staleOwnerRead.value.expiresAt === undefined) {
1153
- // A lease is always written with a positive TTL, so a missing
1154
- // expiry is a corrupt record. Reclaiming deletes authority state,
1155
- // so refuse rather than read the absence as expiry.
1156
- throw new Error('Location Store owner lease record is invalid.');
1157
- }
1158
- if (sameLiveOwner(staleOwnerRead, record.snapshot)) {
1159
- return { kind: 'conflict', current: snapshot };
1160
- }
1161
- const staleCapacity =
1162
- staleCapacityRead.kind === 'missing'
1163
- ? emptyCapacityRecord()
1164
- : decodeJson<CapacityRecord>(staleCapacityRead.value.bytes);
1165
- await this.provider.write(
1166
- {
1167
- conditions: [
1168
- { kind: 'version', key: rowKey, expected: current.value.version },
1169
- versionCondition(staleOwnerKey, staleOwnerRead),
1170
- conditionFor(staleCapacityKey, staleCapacityRead)
1171
- ],
1172
- mutations: [
1173
- { kind: 'delete', key: rowKey },
1174
- {
1175
- kind: 'put',
1176
- key: staleCapacityKey,
1177
- bytes: encodeJson({
1178
- active: staleCapacity.active,
1179
- pending: subtractCapacity(
1180
- staleCapacity.pending,
1181
- record.snapshot.allocation.capacity
1182
- )
1183
- } satisfies CapacityRecord)
1184
- }
1185
- ]
1186
- },
1187
- signal
1270
+ const reclaim = await this.tryReleaseEndedReservation(
1271
+ encodedAuthorityKey,
1272
+ current.value.version.value,
1273
+ signal,
1274
+ request
1188
1275
  );
1276
+ if (reclaim === 'notReclaimable')
1277
+ return snapshot.allocation.state === 'active'
1278
+ ? { kind: 'alreadyExists', current: snapshot }
1279
+ : { kind: 'conflict', current: snapshot };
1280
+ reclaimVersion = reclaim === 'conflict' ? snapshot.storeVersion.value : undefined;
1189
1281
  continue;
1190
1282
  }
1283
+ if (reclaimVersion !== undefined)
1284
+ return { kind: 'conflict', current: { kind: 'missing', storeNow: current.storeNow } };
1191
1285
  const [descriptorRead, leaseRead] = await Promise.all([
1192
1286
  this.provider.read(descriptorKey, signal),
1193
1287
  this.provider.read(leaseKey, signal)
@@ -3992,13 +4086,21 @@ function adjustCapacity(
3992
4086
  if (delta.spotType !== undefined) {
3993
4087
  const key = capacityTypeKey(delta.spotType);
3994
4088
  const next = (spotTypes[key] ?? 0) + direction * delta.spotType.count;
3995
- if (next < 0) throw new Error('Provider capacity counter underflow.');
4089
+ if (next < 0)
4090
+ throw new ZLinkFrameworkException(
4091
+ ZLinkFrameworkErrorKind.DataLost,
4092
+ 'Provider capacity counter underflow.'
4093
+ );
3996
4094
  if (next === 0) delete spotTypes[key];
3997
4095
  else spotTypes[key] = next;
3998
4096
  }
3999
4097
  const actors = usage.actors + direction * delta.actors;
4000
4098
  const spots = usage.spots + direction * delta.spots;
4001
- if (actors < 0 || spots < 0) throw new Error('Provider capacity counter underflow.');
4099
+ if (actors < 0 || spots < 0)
4100
+ throw new ZLinkFrameworkException(
4101
+ ZLinkFrameworkErrorKind.DataLost,
4102
+ 'Provider capacity counter underflow.'
4103
+ );
4002
4104
  return { actors, spots, spotTypes };
4003
4105
  }
4004
4106
 
@@ -4037,15 +4139,10 @@ function sameCreationTarget(
4037
4139
  );
4038
4140
  }
4039
4141
 
4040
- const CANONICAL_AUTHORITY_MAGIC = Buffer.from('ZLAU');
4142
+ const relocationAuthorityCodec = new ServiceRelocationAuthorityPayloadCodec();
4041
4143
 
4042
- function isCanonicalAuthorityPayload(payload: Uint8Array): boolean {
4043
- return (
4044
- payload.byteLength >= CANONICAL_AUTHORITY_MAGIC.byteLength &&
4045
- Buffer.from(payload.buffer, payload.byteOffset, CANONICAL_AUTHORITY_MAGIC.byteLength).equals(
4046
- CANONICAL_AUTHORITY_MAGIC
4047
- )
4048
- );
4144
+ function hasRelocationAuthority(payload: Uint8Array): boolean {
4145
+ return relocationAuthorityCodec.read(payload) !== undefined;
4049
4146
  }
4050
4147
 
4051
4148
  function sameLiveOwner(lease: ZLinkStoreReadResult, snapshot: StoredAuthoritySnapshot): boolean {
@@ -4441,3 +4538,19 @@ function throwIfOperationExpired(signal?: AbortSignal, deadlineUnixMs?: bigint):
4441
4538
  throw new DOMException('Operation deadline exceeded.', 'TimeoutError');
4442
4539
  }
4443
4540
  }
4541
+
4542
+ // Store SPI §6: preserve caller failures and classify provider failures once.
4543
+ function locationStoreProviderFailure(error: unknown, signal?: AbortSignal): unknown {
4544
+ if (
4545
+ error instanceof ZLinkFrameworkException ||
4546
+ error instanceof TypeError ||
4547
+ error instanceof RangeError ||
4548
+ signal?.aborted === true
4549
+ )
4550
+ return error;
4551
+ return new ZLinkFrameworkException(
4552
+ ZLinkFrameworkErrorKind.Unavailable,
4553
+ 'Location Store provider is unavailable.',
4554
+ error
4555
+ );
4556
+ }
@@ -1,4 +1,4 @@
1
- // <auto-generated> DO NOT EDIT. schema-sha256: 77748ba8f585cb70ad337fa3203b5f9b9aa7a89e7f95f7fdc20159c30987be0f
1
+ // <auto-generated> DO NOT EDIT. schema-sha256: 818fcd2f767793be8b5f626e5ac688807e671e546af2b11786e6fbaa02010ba5
2
2
  export interface ServiceWireRouteFence { readonly id: string; readonly generation: bigint; readonly targetNodeRid: Uint8Array; readonly targetNodeGeneration: bigint; readonly expectedAuthorityOwnerGeneration: bigint; readonly expectedOwnerLeaseGeneration: bigint; }
3
3
  export interface ApplicationPayloadEnvelopeV1 { readonly packetName: string; readonly contentType: string; readonly payload: Uint8Array; }
4
4
  export interface ActorJoin28 { readonly correlation: bigint; readonly actor: ServiceWireRouteFence; readonly entry: boolean; readonly targetSpot: ServiceWireRouteFence; readonly payload?: ApplicationPayloadEnvelopeV1; }
@@ -378,6 +378,10 @@ export class ZLinkEntrySpotActivation {
378
378
  return this.spotSerialExecutor.executeActor(actorId, operation);
379
379
  }
380
380
 
381
+ terminateActor(actorId: string, terminal: () => Promise<void>): Promise<void> {
382
+ return this.spotSerialExecutor.terminateActor(actorId, terminal);
383
+ }
384
+
381
385
  /**
382
386
  * Entry Spot membership does not have an application admission callback.
383
387
  * The shared core round-trip therefore accepts a valid returning Actor and
@@ -783,6 +783,13 @@ export class ZLinkSpotNodeRuntimeManager {
783
783
  return activation?.notifyJoinActor(actor, signal) ?? Promise.resolve();
784
784
  }
785
785
 
786
+ terminateActorActivation(actor: ZLinkActor, terminal: () => Promise<void>): Promise<void> {
787
+ const activation = this.entryActivations.get(actor.context.meshName);
788
+ return activation === undefined
789
+ ? terminal()
790
+ : activation.terminateActor(actor.context.actorId, terminal);
791
+ }
792
+
786
793
  notifyPrimaryEntrySpotActorLeft(
787
794
  actor: ZLinkActor,
788
795
  signal?: AbortSignal,
@@ -51,10 +51,8 @@ export class ZLinkSpotSerialExecutor {
51
51
  operation: (serial: ZLinkSpotSerialTurnExecutor) => Promise<T> | T,
52
52
  workOptions?: ZLinkSerialWorkOptions
53
53
  ): Promise<T> {
54
- return this.actorExecutor(actorId).execute(
55
- () => operation(this.actorSerial(actorId)),
56
- workOptions
57
- );
54
+ const serial = this.actorSerial(actorId);
55
+ return this.actorExecutor(actorId).execute(() => operation(serial), workOptions);
58
56
  }
59
57
 
60
58
  /** @internal Restores a durable Actor FIFO prefix without starting it. */
@@ -102,6 +100,14 @@ export class ZLinkSpotSerialExecutor {
102
100
  return this.spotSerial.execute(operation, { ...workOptions, lane: 'lifecycle' });
103
101
  }
104
102
 
103
+ async terminateActor(actorId: string, terminal: () => Promise<void>): Promise<void> {
104
+ const executor = this.actorExecutor(actorId);
105
+ await executor.close();
106
+ await terminal();
107
+ if (this.actorExecutors.get(actorId) === executor) this.actorExecutors.delete(actorId);
108
+ this.actorSerials.delete(actorId);
109
+ }
110
+
105
111
  /**
106
112
  * Makes all child queues unreachable and completes them in one synchronous
107
113
  * ownership turn. Already accepted work remains drainable.
@@ -134,7 +140,10 @@ export class ZLinkSpotSerialExecutor {
134
140
  this.throwIfClosing();
135
141
  let executor = this.actorExecutors.get(actorId);
136
142
  if (executor === undefined) {
137
- executor = new ZLinkActorSerialExecutor(actorId, this.sourceSpotId, this.actorOptions);
143
+ executor =
144
+ this.executionMode === ZLinkUserSpotExecutionMode.SpotWide
145
+ ? this.spotSerial.createActorExecutor(actorId, this.sourceSpotId, this.actorOptions)
146
+ : new ZLinkActorSerialExecutor(actorId, this.sourceSpotId, this.actorOptions);
138
147
  this.actorExecutors.set(actorId, executor);
139
148
  }
140
149
  return executor;
@@ -19,6 +19,7 @@ import {
19
19
  type ZLinkSerialWorkRecord
20
20
  } from '../execution/serial-execution-queue';
21
21
  import type { ZLinkSerialLifecycleContext } from '../execution/serial-execution-queue';
22
+ import { ZLinkActorSerialExecutor } from '../actors/actor-mailbox';
22
23
 
23
24
  export type ZLinkSpotApplicationTurnContext = ZLinkExecutionBarrierClaim & {
24
25
  readonly replay?: (failure?: unknown) => Promise<unknown> | unknown;
@@ -53,6 +54,15 @@ export class ZLinkSpotSerialTurnExecutor {
53
54
  return this.depth > 0;
54
55
  }
55
56
 
57
+ /** Creates a passive Actor mailbox consumed by this Spot's scheduler. */
58
+ createActorExecutor(
59
+ actorId: string,
60
+ sourceSpotId: unknown,
61
+ options?: ZLinkSerialSchedulerOptions
62
+ ): ZLinkActorSerialExecutor {
63
+ return new ZLinkActorSerialExecutor(actorId, sourceSpotId, options, this.scheduler);
64
+ }
65
+
56
66
  get isCurrentTurn(): boolean {
57
67
  return this.depth > 0 && isCurrentZLinkSpotSerialTurn(this);
58
68
  }
@@ -87,7 +87,6 @@ import {
87
87
  type ZLinkStreamSessionRuntimeOptions as ZLinkStreamSessionRuntimeCoreOptions
88
88
  } from './stream-session-runtime';
89
89
  const DEFAULT_STREAM_SERVICE_SHUTDOWN_TIMEOUT_MS = 1000;
90
- export { ZLinkPendingSessionRequest } from './session-requests';
91
90
  export { ZLinkActorSessionLifecycleCoordinator } from './actor-session-lifecycle-coordinator';
92
91
  export { ZLinkActorSessionBindingRegistry } from './actor-session-binding-registry';
93
92
  export { zlinkStreamLz4CompressionCodec } from './stream-frame-factory';
@@ -305,6 +304,9 @@ export class ZLinkStreamRuntimeManager {
305
304
  const readablePoller = streamAdapter.createReadablePoller(socket);
306
305
  const runtime = new ZLinkStreamSessionNodeRuntimeCore({
307
306
  nodeName,
307
+ heartbeatIntervalMs: streamNode.heartbeatIntervalMs,
308
+ heartbeatTimeoutMs: streamNode.heartbeatTimeoutMs,
309
+ idleTimeoutMs: streamNode.idleTimeoutMs,
308
310
  socket,
309
311
  createPacket: () => streamAdapter.createStreamPacket(),
310
312
  readablePoller,