@zlink-systems/framework 0.28.0 → 0.29.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.
Files changed (87) 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/backend/node/node-raw-mesh-backend.js +1 -3
  12. package/dist/runtime/execution/serial-execution-queue.d.ts +12 -3
  13. package/dist/runtime/execution/serial-execution-queue.js +116 -28
  14. package/dist/runtime/foundation/operation-registry.d.ts +3 -2
  15. package/dist/runtime/foundation/operation-registry.js +5 -1
  16. package/dist/runtime/foundation/service-authority-payload-codec.d.ts +0 -2
  17. package/dist/runtime/foundation/service-authority-payload-codec.js +0 -43
  18. package/dist/runtime/foundation/service-stateful-registry.d.ts +4 -3
  19. package/dist/runtime/foundation/service-stateful-registry.js +5 -2
  20. package/dist/runtime/foundation/service-stateful-runtime.d.ts +1 -8
  21. package/dist/runtime/foundation/service-stateful-runtime.js +78 -126
  22. package/dist/runtime/foundation/service-stateful-wire-codec.d.ts +0 -11
  23. package/dist/runtime/foundation/service-stateful-wire-codec.js +1 -23
  24. package/dist/runtime/foundation/service-topology-registry.d.ts +5 -0
  25. package/dist/runtime/foundation/service-topology-registry.js +16 -17
  26. package/dist/runtime/host/instance-activation-authority.d.ts +0 -1
  27. package/dist/runtime/host/instance-activation-authority.js +46 -97
  28. package/dist/runtime/host/spot-address-transport.js +1 -1
  29. package/dist/runtime/host/stateful-authority-route-runtime.d.ts +1 -0
  30. package/dist/runtime/host/stateful-authority-route-runtime.js +17 -9
  31. package/dist/runtime/locations/domain-store-contract.d.ts +1 -0
  32. package/dist/runtime/locations/in-memory-authority-store.d.ts +2 -0
  33. package/dist/runtime/locations/in-memory-authority-store.js +22 -8
  34. package/dist/runtime/locations/in-memory-location-store.d.ts +1 -0
  35. package/dist/runtime/locations/in-memory-location-store.js +3 -0
  36. package/dist/runtime/locations/internal-store-contracts.d.ts +1 -1
  37. package/dist/runtime/locations/location-store-repository.d.ts +1 -0
  38. package/dist/runtime/locations/location-store-repository.js +92 -49
  39. package/dist/runtime/spots/spot-serial-executor.js +4 -1
  40. package/dist/runtime/spots/spot-serial-turn-executor.d.ts +3 -0
  41. package/dist/runtime/spots/spot-serial-turn-executor.js +5 -0
  42. package/dist/runtime/streams/index.d.ts +0 -1
  43. package/dist/runtime/streams/index.js +4 -3
  44. package/dist/runtime/streams/protocol.js +0 -9
  45. package/dist/runtime/streams/session-context.d.ts +0 -4
  46. package/dist/runtime/streams/session-context.js +0 -26
  47. package/dist/runtime/streams/session-serial-executor.d.ts +0 -1
  48. package/dist/runtime/streams/session-serial-executor.js +0 -3
  49. package/dist/runtime/streams/stream-message-utils.d.ts +0 -1
  50. package/dist/runtime/streams/stream-message-utils.js +0 -20
  51. package/dist/runtime/streams/stream-session-runtime.d.ts +6 -3
  52. package/dist/runtime/streams/stream-session-runtime.js +40 -85
  53. package/package.json +3 -3
  54. package/src/contracts/Configuration/Builders.ts +2 -0
  55. package/src/contracts/Configuration/InternalDefaults.ts +4 -0
  56. package/src/contracts/Configuration/RegistrationBuilders.ts +13 -0
  57. package/src/contracts/Configuration/RegistrationNormalizers.ts +25 -0
  58. package/src/contracts/Configuration/RegistrationTypes.ts +3 -0
  59. package/src/runtime/actors/actor-mailbox.ts +7 -2
  60. package/src/runtime/backend/node/node-raw-mesh-backend.ts +1 -7
  61. package/src/runtime/execution/serial-execution-queue.ts +117 -29
  62. package/src/runtime/foundation/operation-registry.ts +16 -9
  63. package/src/runtime/foundation/service-authority-payload-codec.ts +0 -46
  64. package/src/runtime/foundation/service-stateful-registry.ts +10 -5
  65. package/src/runtime/foundation/service-stateful-runtime.ts +103 -181
  66. package/src/runtime/foundation/service-stateful-wire-codec.ts +0 -25
  67. package/src/runtime/foundation/service-topology-registry.ts +30 -20
  68. package/src/runtime/host/instance-activation-authority.ts +58 -125
  69. package/src/runtime/host/spot-address-transport.ts +1 -1
  70. package/src/runtime/host/stateful-authority-route-runtime.ts +22 -9
  71. package/src/runtime/locations/domain-store-contract.ts +5 -0
  72. package/src/runtime/locations/in-memory-authority-store.ts +42 -8
  73. package/src/runtime/locations/in-memory-location-store.ts +8 -0
  74. package/src/runtime/locations/internal-store-contracts.ts +1 -0
  75. package/src/runtime/locations/location-store-repository.ts +123 -64
  76. package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +1 -1
  77. package/src/runtime/spots/spot-serial-executor.ts +4 -1
  78. package/src/runtime/spots/spot-serial-turn-executor.ts +10 -0
  79. package/src/runtime/streams/index.ts +3 -1
  80. package/src/runtime/streams/protocol.ts +0 -13
  81. package/src/runtime/streams/session-context.ts +0 -32
  82. package/src/runtime/streams/session-serial-executor.ts +0 -8
  83. package/src/runtime/streams/stream-message-utils.ts +1 -27
  84. package/src/runtime/streams/stream-session-runtime.ts +54 -97
  85. package/dist/runtime/streams/session-requests.d.ts +0 -25
  86. package/dist/runtime/streams/session-requests.js +0 -101
  87. package/src/runtime/streams/session-requests.ts +0 -110
@@ -16,7 +16,7 @@ import type {
16
16
  ZLinkAuthorityStore,
17
17
  ZLinkObjectCreationStore
18
18
  } from '../locations/internal-store-contracts';
19
- import { ZLinkFrameworkException } from '../../contracts';
19
+ import { ZLinkFrameworkErrorKind, ZLinkFrameworkException } from '../../contracts';
20
20
  import {
21
21
  decodeServiceInstanceAuthorityPayload,
22
22
  decodeServiceReadySpotAuthority,
@@ -46,7 +46,6 @@ import {
46
46
 
47
47
  const CREATION_REQUEST_RETENTION_MS = 24 * 60 * 60 * 1_000;
48
48
  const MAX_CREATION_REQUEST_BYTES = 1024 * 1024;
49
- const ACTIVATION_JOIN_POLL_INTERVAL_MS = 10;
50
49
 
51
50
  interface PendingReservation {
52
51
  readonly reservationId: string;
@@ -115,87 +114,81 @@ export class ZLinkInstanceActivationAuthority implements ServiceAsyncInstanceAct
115
114
  }
116
115
  let reserved: Awaited<ReturnType<ZLinkObjectCreationStore['reserve']>>;
117
116
  try {
118
- for (;;) {
119
- reserved = await this.options.store.reserve(
120
- {
121
- key: { kind: 'instance_spot', globalId: target.targetSpotId },
122
- intent: {
123
- stableType: target.stableType,
124
- requestContentReference: stored.reference.value,
125
- requestSha256: requestHash,
126
- requestEncodedSize: BigInt(requestBytes.byteLength)
127
- },
128
- target: {
129
- meshName: this.options.meshName,
130
- nodeRid: target.targetNodeRid,
131
- nodeLifecycleGeneration: target.targetNodeGeneration,
132
- owner
133
- },
134
- creatingPayload: encodeServiceInstanceAuthorityPayload({
135
- state: 'coldActivating',
117
+ reserved = await this.options.store.reserve(
118
+ {
119
+ key: { kind: 'instance_spot', globalId: target.targetSpotId },
120
+ intent: {
121
+ stableType: target.stableType,
122
+ requestContentReference: stored.reference.value,
123
+ requestSha256: requestHash,
124
+ requestEncodedSize: BigInt(requestBytes.byteLength)
125
+ },
126
+ target: {
127
+ meshName: this.options.meshName,
128
+ nodeRid: target.targetNodeRid,
129
+ nodeLifecycleGeneration: target.targetNodeGeneration,
130
+ owner
131
+ },
132
+ creatingPayload: encodeServiceInstanceAuthorityPayload({
133
+ state: 'coldActivating',
134
+ stableType: target.stableType,
135
+ spotId: target.targetSpotId,
136
+ ownerId: owner.ownerId,
137
+ ownerLeaseGeneration: owner.leaseGeneration,
138
+ ownerMeshName: this.options.meshName,
139
+ ownerNodeRid: target.targetNodeRid,
140
+ ownerNodeGeneration: target.targetNodeGeneration
141
+ }),
142
+ capacity: {
143
+ actors: 0,
144
+ spots: 1,
145
+ spotType: {
146
+ objectKind: 'instance_spot',
136
147
  stableType: target.stableType,
137
- spotId: target.targetSpotId,
138
- ownerId: owner.ownerId,
139
- ownerLeaseGeneration: owner.leaseGeneration,
140
- ownerMeshName: this.options.meshName,
141
- ownerNodeRid: target.targetNodeRid,
142
- ownerNodeGeneration: target.targetNodeGeneration
143
- }),
144
- capacity: {
145
- actors: 0,
146
- spots: 1,
147
- spotType: {
148
- objectKind: 'instance_spot',
149
- stableType: target.stableType,
150
- count: 1
151
- }
148
+ count: 1
152
149
  }
153
- },
154
- undefined,
155
- activation.deadlineUnixMs
156
- );
157
- if (reserved.kind !== 'alreadyExists') break;
158
- this.options.metrics?.recordInstanceSpotClaimConflict(
159
- this.options.meshName,
160
- target.stableType,
161
- 'already_exists'
162
- );
163
- await this.deleteOrphan(stored.reference);
164
- return readyExisting(reserved.current, target);
165
- }
150
+ }
151
+ },
152
+ undefined,
153
+ activation.deadlineUnixMs
154
+ );
166
155
  } catch (error) {
167
156
  await this.deleteOrphan(stored.reference);
168
157
  throw error;
169
158
  }
170
- if (
171
- reserved.kind === 'conflict' &&
172
- reserved.current.kind === 'snapshot' &&
173
- reserved.current.allocation.objectKind === 'instance_spot' &&
174
- reserved.current.allocation.stableType === target.stableType &&
175
- reserved.current.allocation.state === 'reserved'
176
- ) {
177
- this.options.metrics?.recordInstanceSpotClaimConflict(
178
- this.options.meshName,
179
- target.stableType,
180
- 'activation_in_progress'
181
- );
182
- await this.deleteOrphan(stored.reference);
183
- return await this.awaitConcurrentActivation(target, activation.deadlineUnixMs);
184
- }
185
159
  if (reserved.kind !== 'reserved') {
160
+ const conflictReason =
161
+ reserved.kind === 'alreadyExists'
162
+ ? 'already_exists'
163
+ : reserved.kind === 'conflict' &&
164
+ reserved.current.kind === 'snapshot' &&
165
+ reserved.current.allocation.objectKind === 'instance_spot' &&
166
+ reserved.current.allocation.stableType === target.stableType &&
167
+ reserved.current.allocation.state === 'reserved'
168
+ ? 'activation_in_progress'
169
+ : reserved.kind;
186
170
  this.options.metrics?.recordInstanceSpotClaimConflict(
187
171
  this.options.meshName,
188
172
  target.stableType,
189
- reserved.kind
173
+ conflictReason
190
174
  );
191
175
  await this.deleteOrphan(stored.reference);
176
+ if (reserved.kind === 'typeMismatch') {
177
+ throw createInternalFrameworkException(
178
+ ZLinkFrameworkInternalErrorKind.SpotTypeMismatch,
179
+ `Instance activation reservation failed: ${reserved.kind}.`
180
+ );
181
+ }
192
182
  if (reserved.kind === 'placementCapacityExhausted') {
193
183
  throw createInternalFrameworkException(
194
184
  ZLinkFrameworkInternalErrorKind.PlacementCapacityExhausted,
195
185
  `Instance Spot placement capacity is exhausted for '${String(target.targetSpotId)}'.`
196
186
  );
197
187
  }
198
- throw new Error(`Instance activation reservation failed: ${reserved.kind}.`);
188
+ throw new ZLinkFrameworkException(
189
+ ZLinkFrameworkErrorKind.Unavailable,
190
+ `Instance activation reservation failed: ${reserved.kind}.`
191
+ );
199
192
  }
200
193
  const reservation: ServiceInstanceActivationReservation = {
201
194
  attempt: reserved.creating.objectGeneration,
@@ -455,34 +448,6 @@ export class ZLinkInstanceActivationAuthority implements ServiceAsyncInstanceAct
455
448
  }
456
449
  }
457
450
 
458
- private async awaitConcurrentActivation(
459
- target: ServiceInstanceActivationTarget,
460
- deadlineUnixMs: bigint
461
- ): Promise<Extract<ServiceInstanceAuthorityRead, { readonly kind: 'ready' }>> {
462
- const key = authorityKey(target.targetSpotId);
463
- const deadlineMs = performance.now() + Number(deadlineUnixMs - BigInt(Date.now()));
464
- while (performance.now() <= deadlineMs) {
465
- const current = await this.options.store.readAuthority(key);
466
- if (current.kind === 'snapshot') {
467
- if (
468
- current.allocation.objectKind !== 'instance_spot' ||
469
- current.allocation.stableType !== target.stableType
470
- ) {
471
- throw new Error('Concurrent Instance activation resolved to a different Spot type.');
472
- }
473
- if (current.allocation.state === 'active') {
474
- return readyExisting(current, target);
475
- }
476
- } else {
477
- throw new Error('Concurrent Instance activation ended before the Ready barrier.');
478
- }
479
- await waitForActivationJoin(deadlineMs);
480
- }
481
- throw new Error(
482
- `Concurrent Instance activation for '${target.targetSpotId}' did not reach Ready before deadline.`
483
- );
484
- }
485
-
486
451
  private requirePending(reservation: ServiceInstanceActivationReservation): PendingReservation {
487
452
  const pending = this.pending.get(reservation.token);
488
453
  if (pending === undefined || pending.creating.objectGeneration !== reservation.attempt) {
@@ -539,38 +504,6 @@ function readyRead(
539
504
  return { kind: 'ready', route: routeFromSnapshot(snapshot), authority: snapshot };
540
505
  }
541
506
 
542
- function readyExisting(
543
- snapshot: ZLinkAuthoritySnapshot,
544
- target: ServiceInstanceActivationTarget
545
- ): Extract<ServiceInstanceAuthorityRead, { readonly kind: 'ready' }> {
546
- const decoded =
547
- decodeServiceReadySpotAuthority(snapshot.payload) ??
548
- decodeServiceClosingSpotAuthority(snapshot.payload);
549
- if (
550
- decoded?.kind !== 'instance_spot' ||
551
- decoded.stableType !== target.stableType ||
552
- decoded.spotId !== target.targetSpotId ||
553
- snapshot.allocation.objectKind !== 'instance_spot' ||
554
- snapshot.allocation.state !== 'active' ||
555
- decoded.ownerId !== snapshot.ownerId ||
556
- decoded.ownerLeaseGeneration !== snapshot.ownerLeaseGeneration ||
557
- decoded.ownerMeshName !== snapshot.allocation.descriptor.meshName ||
558
- decoded.ownerNodeGeneration !== snapshot.allocation.descriptorLifecycleGeneration ||
559
- !routingIdsEqual(decoded.ownerNodeRid, snapshot.allocation.descriptor.rid)
560
- ) {
561
- throw new Error('Existing Instance authority is not a matching Ready allocation.');
562
- }
563
- return { kind: 'ready', route: routeFromSnapshot(snapshot), authority: snapshot };
564
- }
565
-
566
- async function waitForActivationJoin(deadlineMs: number): Promise<void> {
567
- const remaining = deadlineMs - performance.now();
568
- if (remaining <= 0) return;
569
- await new Promise<void>((resolve) => {
570
- setTimeout(resolve, Math.min(ACTIVATION_JOIN_POLL_INTERVAL_MS, remaining));
571
- });
572
- }
573
-
574
507
  function requireCommitIdentity(
575
508
  target: ServiceInstanceActivationTarget,
576
509
  reservation: ServiceInstanceActivationReservation,
@@ -576,7 +576,7 @@ export class ZLinkHostSpotAddressTransport implements ZLinkSpotAddressTransport
576
576
  if (stableType === undefined) {
577
577
  if (distinctTypes.length === 0) return { kind: 'unsupported' };
578
578
  throw createInternalFrameworkException(
579
- ZLinkFrameworkInternalErrorKind.InvalidConfiguration,
579
+ ZLinkFrameworkInternalErrorKind.InvalidOperation,
580
580
  'Instance Spot type is required when multiple types are registered.'
581
581
  );
582
582
  }
@@ -1,3 +1,4 @@
1
+ import { encodeAuthorityKey } from '../locations/authority-key-codec';
1
2
  import { createHash } from 'node:crypto';
2
3
  import type {
3
4
  ZLinkAuthorityKey,
@@ -175,12 +176,25 @@ export class ZLinkStatefulAuthorityRouteRuntime {
175
176
 
176
177
  const status = sink.status();
177
178
  for (const pending of snapshot.pending) {
179
+ if (pending.targetMeshName !== meshName) continue;
178
180
  if (
179
- pending.targetMeshName === meshName &&
180
181
  routingIdsEqual(status.routingId, pending.targetNodeRid) &&
181
182
  status.lifecycleGeneration === pending.targetNodeGeneration
182
183
  ) {
183
184
  await this.recoverPendingInstanceActivation(sink, pending, signal);
185
+ } else {
186
+ const release = this.options.creationStore?.releaseEndedReservation;
187
+ if (release === undefined)
188
+ throw new Error('Reservation lifecycle recovery requires a Location repository.');
189
+ if (
190
+ await release.call(
191
+ this.options.creationStore,
192
+ encodeAuthorityKey('instance_spot', pending.spotId),
193
+ pending.pending.storeVersion,
194
+ signal
195
+ )
196
+ )
197
+ await this.clearRecoveryRoot(pending.pending.requestReference, signal);
184
198
  }
185
199
  }
186
200
 
@@ -386,14 +400,7 @@ export class ZLinkStatefulAuthorityRouteRuntime {
386
400
  signal
387
401
  );
388
402
  if (aborted.kind === 'aborted' || aborted.kind === 'alreadyAborted') {
389
- try {
390
- await this.options.relocationStore?.delete(
391
- relocationBlobReference(recovery.pending.requestReference)
392
- );
393
- } catch {
394
- // The authority no longer publishes this root. Retention owns a
395
- // failed best-effort orphan cleanup.
396
- }
403
+ await this.clearRecoveryRoot(recovery.pending.requestReference, signal);
397
404
  }
398
405
  return;
399
406
  }
@@ -401,6 +408,12 @@ export class ZLinkStatefulAuthorityRouteRuntime {
401
408
  return;
402
409
  }
403
410
 
411
+ private async clearRecoveryRoot(reference: string, signal?: AbortSignal): Promise<void> {
412
+ if (this.options.relocationStore === undefined)
413
+ throw new Error('Instance activation recovery requires a Relocation Store.');
414
+ await this.options.relocationStore.delete(relocationBlobReference(reference), signal);
415
+ }
416
+
404
417
  private async readRecoveryEnvelope(
405
418
  recovery: ServiceActivationRecoveryState,
406
419
  signal?: AbortSignal
@@ -140,6 +140,11 @@ export interface ZLinkDomainLocationStore {
140
140
  request: ZLinkObjectCreationCompleteRequest,
141
141
  signal?: AbortSignal
142
142
  ): Promise<ZLinkObjectCreationCompleteResult>;
143
+ releaseEndedReservation?(
144
+ key: ZLinkAuthorityKey,
145
+ expectedStoreVersion: string,
146
+ signal?: AbortSignal
147
+ ): Promise<boolean>;
143
148
  abort(
144
149
  request: ZLinkObjectAbortRequest,
145
150
  signal?: AbortSignal,
@@ -276,6 +276,42 @@ export class ZLinkInMemoryAuthorityStore {
276
276
  return BigInt(removed);
277
277
  }
278
278
 
279
+ async releaseEndedReservation(
280
+ key: ZLinkAuthorityKey,
281
+ expectedStoreVersion: string,
282
+ signal?: AbortSignal
283
+ ): Promise<boolean> {
284
+ signal?.throwIfAborted();
285
+ return this.tryReleaseEndedReservation(key, expectedStoreVersion);
286
+ }
287
+
288
+ private tryReleaseEndedReservation(
289
+ key: ZLinkAuthorityKey,
290
+ expectedStoreVersion: string
291
+ ): boolean {
292
+ const current = this.rows.get(key.value);
293
+ if (
294
+ current === undefined ||
295
+ current.snapshot.storeVersion.value !== expectedStoreVersion ||
296
+ current.snapshot.allocation.state !== 'reserved' ||
297
+ current.creation === undefined ||
298
+ this.validation.isTargetLive(
299
+ current.snapshot.allocation.descriptor,
300
+ current.snapshot.allocation.descriptorLifecycleGeneration,
301
+ {
302
+ ownerId: current.snapshot.ownerId,
303
+ leaseGeneration: current.snapshot.ownerLeaseGeneration
304
+ }
305
+ )
306
+ )
307
+ return false;
308
+ this.adjustCapacity(this.pendingCapacity, current.snapshot.allocation, -1);
309
+ this.rows.delete(key.value);
310
+ this.creationTerminals.set(current.creation.reservationId, 'aborted');
311
+ this.scanRevision++;
312
+ return true;
313
+ }
314
+
279
315
  async reserve(
280
316
  request: ZLinkObjectReserveRequest,
281
317
  signal?: AbortSignal
@@ -297,15 +333,13 @@ export class ZLinkInMemoryAuthorityStore {
297
333
  if (current.snapshot.allocation.state === 'active') {
298
334
  return { kind: 'alreadyExists', current: this.snapshot(current.snapshot) };
299
335
  }
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) {
336
+ if (
337
+ !this.tryReleaseEndedReservation(
338
+ encodeAuthorityKey(request.key.kind, request.key.globalId),
339
+ current.snapshot.storeVersion.value
340
+ )
341
+ )
303
342
  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++;
309
343
  }
310
344
  const target = creationTarget(request);
311
345
  if (
@@ -201,6 +201,14 @@ export class ZLinkInMemoryLocationStore
201
201
  return this.authority.readCreationTerminal(operation, signal);
202
202
  }
203
203
 
204
+ async releaseEndedReservation(
205
+ key: ZLinkAuthorityKey,
206
+ expectedStoreVersion: string,
207
+ signal?: AbortSignal
208
+ ): Promise<boolean> {
209
+ return this.authority.releaseEndedReservation(key, expectedStoreVersion, signal);
210
+ }
211
+
204
212
  async abort(
205
213
  request: ZLinkObjectAbortRequest,
206
214
  signal?: AbortSignal
@@ -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,77 @@ 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
+ const rowKey = authorityKey(key.value);
1134
+ const current = await this.provider.read(rowKey, signal);
1135
+ if (current.kind === 'missing' || current.value.version.value !== expectedStoreVersion)
1136
+ return false;
1137
+ const record = decodeAuthorityRecord(current.value.bytes);
1138
+ const snapshot = record.snapshot;
1139
+ if (
1140
+ snapshot.allocation.state !== 'reserved' ||
1141
+ snapshot.pendingCreation === undefined ||
1142
+ record.aggregate !== undefined ||
1143
+ hasRelocationAuthority(snapshot.payload)
1144
+ )
1145
+ return false;
1146
+ const leaseKey = ownerKey(snapshot.ownerId);
1147
+ const descriptorKey = meshKey(
1148
+ snapshot.allocation.descriptor.meshName,
1149
+ snapshot.allocation.descriptor.rid
1150
+ );
1151
+ const capacityRowKey = capacityKey(
1152
+ snapshot.allocation.descriptor.meshName,
1153
+ String(snapshot.allocation.descriptor.rid)
1154
+ );
1155
+ const [lease, descriptor, capacity] = await Promise.all([
1156
+ this.provider.read(leaseKey, signal),
1157
+ this.provider.read(descriptorKey, signal),
1158
+ this.provider.read(capacityRowKey, signal)
1159
+ ]);
1160
+ if (lease.kind === 'found' && lease.value.expiresAt === undefined)
1161
+ throw new Error('Location Store owner lease record is invalid.');
1162
+ if (
1163
+ sameLiveOwner(lease, snapshot) &&
1164
+ descriptor.kind === 'found' &&
1165
+ reviveMeshDescriptor(
1166
+ decodeCanonicalDescriptorRecord<ZLinkMeshNodeDescriptor>(descriptor.value.bytes).descriptor
1167
+ ).lifecycleGeneration === snapshot.allocation.descriptorLifecycleGeneration
1168
+ )
1169
+ return false;
1170
+ const mutations: ZLinkStoreWriteRequest['mutations'][number][] = [
1171
+ { kind: 'delete', key: rowKey }
1172
+ ];
1173
+ if (capacity.kind === 'found') {
1174
+ const stored = decodeJson<CapacityRecord>(capacity.value.bytes);
1175
+ mutations.push({
1176
+ kind: 'put',
1177
+ key: capacityRowKey,
1178
+ bytes: encodeJson({
1179
+ active: stored.active,
1180
+ pending: subtractCapacity(stored.pending, snapshot.allocation.capacity)
1181
+ } satisfies CapacityRecord)
1182
+ });
1183
+ }
1184
+ const result = await this.provider.write(
1185
+ {
1186
+ conditions: [
1187
+ { kind: 'version', key: rowKey, expected: current.value.version },
1188
+ conditionFor(leaseKey, lease),
1189
+ conditionFor(descriptorKey, descriptor),
1190
+ conditionFor(capacityRowKey, capacity)
1191
+ ],
1192
+ mutations
1193
+ },
1194
+ signal
1195
+ );
1196
+ return result.kind === 'applied';
1197
+ }
1198
+
1102
1199
  override async reserve(
1103
1200
  request: ZLinkObjectReserveRequest,
1104
1201
  signal?: AbortSignal,
@@ -1130,62 +1227,13 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1130
1227
  return { kind: 'alreadyExists', current: snapshot };
1131
1228
  }
1132
1229
  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.
1138
- 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)) {
1230
+ !(await this.releaseEndedReservation(
1231
+ encodedAuthorityKey,
1232
+ current.value.version.value,
1233
+ signal
1234
+ ))
1235
+ )
1159
1236
  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
1188
- );
1189
1237
  continue;
1190
1238
  }
1191
1239
  const [descriptorRead, leaseRead] = await Promise.all([
@@ -4037,15 +4085,10 @@ function sameCreationTarget(
4037
4085
  );
4038
4086
  }
4039
4087
 
4040
- const CANONICAL_AUTHORITY_MAGIC = Buffer.from('ZLAU');
4088
+ const relocationAuthorityCodec = new ServiceRelocationAuthorityPayloadCodec();
4041
4089
 
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
- );
4090
+ function hasRelocationAuthority(payload: Uint8Array): boolean {
4091
+ return relocationAuthorityCodec.read(payload) !== undefined;
4049
4092
  }
4050
4093
 
4051
4094
  function sameLiveOwner(lease: ZLinkStoreReadResult, snapshot: StoredAuthoritySnapshot): boolean {
@@ -4441,3 +4484,19 @@ function throwIfOperationExpired(signal?: AbortSignal, deadlineUnixMs?: bigint):
4441
4484
  throw new DOMException('Operation deadline exceeded.', 'TimeoutError');
4442
4485
  }
4443
4486
  }
4487
+
4488
+ // Store SPI §6: preserve caller failures and classify provider failures once.
4489
+ function locationStoreProviderFailure(error: unknown, signal?: AbortSignal): unknown {
4490
+ if (
4491
+ error instanceof ZLinkFrameworkException ||
4492
+ error instanceof TypeError ||
4493
+ error instanceof RangeError ||
4494
+ signal?.aborted === true
4495
+ )
4496
+ return error;
4497
+ return new ZLinkFrameworkException(
4498
+ ZLinkFrameworkErrorKind.Unavailable,
4499
+ 'Location Store provider is unavailable.',
4500
+ error
4501
+ );
4502
+ }
@@ -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; }
@@ -134,7 +134,10 @@ export class ZLinkSpotSerialExecutor {
134
134
  this.throwIfClosing();
135
135
  let executor = this.actorExecutors.get(actorId);
136
136
  if (executor === undefined) {
137
- executor = new ZLinkActorSerialExecutor(actorId, this.sourceSpotId, this.actorOptions);
137
+ executor =
138
+ this.executionMode === ZLinkUserSpotExecutionMode.SpotWide
139
+ ? this.spotSerial.createActorExecutor(actorId, this.sourceSpotId, this.actorOptions)
140
+ : new ZLinkActorSerialExecutor(actorId, this.sourceSpotId, this.actorOptions);
138
141
  this.actorExecutors.set(actorId, executor);
139
142
  }
140
143
  return executor;