@zlink-systems/framework 0.29.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 (42) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/runtime/actors/actor-runtime-contracts.d.ts +1 -0
  3. package/dist/runtime/actors/index.d.ts +2 -0
  4. package/dist/runtime/actors/index.js +21 -6
  5. package/dist/runtime/foundation/service-stateful-runtime.js +27 -19
  6. package/dist/runtime/handlers/handler-instance-scope.js +1 -1
  7. package/dist/runtime/host/actor-placement-coordinator.d.ts +2 -1
  8. package/dist/runtime/host/actor-placement-coordinator.js +2 -1
  9. package/dist/runtime/host/actor-runtime-options-factory.d.ts +2 -1
  10. package/dist/runtime/host/actor-runtime-options-factory.js +1 -0
  11. package/dist/runtime/host/index.d.ts +1 -1
  12. package/dist/runtime/host/index.js +3 -2
  13. package/dist/runtime/locations/domain-store-contract.d.ts +2 -1
  14. package/dist/runtime/locations/in-memory-authority-store.d.ts +1 -0
  15. package/dist/runtime/locations/in-memory-authority-store.js +32 -11
  16. package/dist/runtime/locations/in-memory-location-store.d.ts +1 -1
  17. package/dist/runtime/locations/in-memory-location-store.js +12 -10
  18. package/dist/runtime/locations/internal-location-contracts.d.ts +5 -0
  19. package/dist/runtime/locations/location-store-repository.d.ts +1 -0
  20. package/dist/runtime/locations/location-store-repository.js +54 -21
  21. package/dist/runtime/spots/spot-entry-activation.d.ts +1 -0
  22. package/dist/runtime/spots/spot-entry-activation.js +3 -0
  23. package/dist/runtime/spots/spot-node-runtime-manager.d.ts +1 -0
  24. package/dist/runtime/spots/spot-node-runtime-manager.js +6 -0
  25. package/dist/runtime/spots/spot-serial-executor.d.ts +1 -0
  26. package/dist/runtime/spots/spot-serial-executor.js +10 -1
  27. package/package.json +2 -2
  28. package/src/runtime/actors/actor-runtime-contracts.ts +4 -0
  29. package/src/runtime/actors/index.ts +77 -42
  30. package/src/runtime/foundation/service-stateful-runtime.ts +49 -43
  31. package/src/runtime/handlers/handler-instance-scope.ts +2 -1
  32. package/src/runtime/host/actor-placement-coordinator.ts +8 -2
  33. package/src/runtime/host/actor-runtime-options-factory.ts +6 -0
  34. package/src/runtime/host/index.ts +6 -2
  35. package/src/runtime/locations/domain-store-contract.ts +1 -1
  36. package/src/runtime/locations/in-memory-authority-store.ts +47 -10
  37. package/src/runtime/locations/in-memory-location-store.ts +14 -10
  38. package/src/runtime/locations/internal-location-contracts.ts +6 -0
  39. package/src/runtime/locations/location-store-repository.ts +82 -28
  40. package/src/runtime/spots/spot-entry-activation.ts +4 -0
  41. package/src/runtime/spots/spot-node-runtime-manager.ts +7 -0
  42. package/src/runtime/spots/spot-serial-executor.ts +10 -4
@@ -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) => {
@@ -566,7 +567,8 @@ export class ZLinkInMemoryLocationStore
566
567
  const storeNow = this.now();
567
568
  return pageRows(
568
569
  this.fanoutPublishers,
569
- (row) => row.channelName === channelName && this.isOwnerLive(row.ownerId, storeNow),
570
+ (row) =>
571
+ row.channelName === channelName && this.liveOwnerLease(row.ownerId, storeNow) !== undefined,
570
572
  page
571
573
  );
572
574
  }
@@ -628,7 +630,7 @@ export class ZLinkInMemoryLocationStore
628
630
  if (
629
631
  intent === ZLinkLocationWriteIntent.NewClaim &&
630
632
  current !== undefined &&
631
- this.isOwnerLive(current.ownerId, updatedAt)
633
+ this.liveOwnerLease(current.ownerId, updatedAt) !== undefined
632
634
  ) {
633
635
  return rejectedConflict();
634
636
  }
@@ -706,7 +708,7 @@ export class ZLinkInMemoryLocationStore
706
708
  if (
707
709
  intent === ZLinkLocationWriteIntent.NewClaim &&
708
710
  current !== undefined &&
709
- this.isOwnerLive(current.ownerId, updatedAt)
711
+ this.liveOwnerLease(current.ownerId, updatedAt) !== undefined
710
712
  ) {
711
713
  return rejectedConflict();
712
714
  }
@@ -1155,7 +1157,7 @@ export class ZLinkInMemoryLocationStore
1155
1157
  if (
1156
1158
  intent === ZLinkLocationWriteIntent.NewClaim &&
1157
1159
  current !== undefined &&
1158
- this.isOwnerLive(ownerOf(current), updatedAt)
1160
+ this.liveOwnerLease(ownerOf(current), updatedAt) !== undefined
1159
1161
  ) {
1160
1162
  return rejectedConflict();
1161
1163
  }
@@ -1252,9 +1254,11 @@ export class ZLinkInMemoryLocationStore
1252
1254
  return removed;
1253
1255
  }
1254
1256
 
1255
- private isOwnerLive(ownerId: string, now: Date): boolean {
1257
+ private liveOwnerLease(ownerId: string, now: Date): InMemoryOwnerLease | undefined {
1256
1258
  const lease = this.leases.get(ownerId);
1257
- return lease !== undefined && lease.leaseExpiresAt.getTime() > now.getTime();
1259
+ return lease !== undefined && lease.leaseExpiresAt.getTime() > now.getTime()
1260
+ ? lease
1261
+ : undefined;
1258
1262
  }
1259
1263
 
1260
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,
@@ -1130,19 +1130,31 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1130
1130
  expectedStoreVersion: string,
1131
1131
  signal?: AbortSignal
1132
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'> {
1133
1144
  const rowKey = authorityKey(key.value);
1134
1145
  const current = await this.provider.read(rowKey, signal);
1135
1146
  if (current.kind === 'missing' || current.value.version.value !== expectedStoreVersion)
1136
- return false;
1147
+ return 'conflict';
1137
1148
  const record = decodeAuthorityRecord(current.value.bytes);
1138
1149
  const snapshot = record.snapshot;
1139
1150
  if (
1140
- snapshot.allocation.state !== 'reserved' ||
1141
- snapshot.pendingCreation === undefined ||
1151
+ (snapshot.allocation.state === 'active'
1152
+ ? request?.key.kind !== 'actor'
1153
+ : snapshot.pendingCreation === undefined) ||
1142
1154
  record.aggregate !== undefined ||
1143
1155
  hasRelocationAuthority(snapshot.payload)
1144
1156
  )
1145
- return false;
1157
+ return 'notReclaimable';
1146
1158
  const leaseKey = ownerKey(snapshot.ownerId);
1147
1159
  const descriptorKey = meshKey(
1148
1160
  snapshot.allocation.descriptor.meshName,
@@ -1159,27 +1171,56 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1159
1171
  ]);
1160
1172
  if (lease.kind === 'found' && lease.value.expiresAt === undefined)
1161
1173
  throw new Error('Location Store owner lease record is invalid.');
1162
- if (
1163
- sameLiveOwner(lease, snapshot) &&
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 &&
1164
1184
  descriptor.kind === 'found' &&
1165
1185
  reviveMeshDescriptor(
1166
1186
  decodeCanonicalDescriptorRecord<ZLinkMeshNodeDescriptor>(descriptor.value.bytes).descriptor
1167
1187
  ).lifecycleGeneration === snapshot.allocation.descriptorLifecycleGeneration
1168
1188
  )
1169
- return false;
1189
+ return 'notReclaimable';
1170
1190
  const mutations: ZLinkStoreWriteRequest['mutations'][number][] = [
1171
1191
  { kind: 'delete', key: rowKey }
1172
1192
  ];
1173
1193
  if (capacity.kind === 'found') {
1174
1194
  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
- });
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
+ }
1183
1224
  }
1184
1225
  const result = await this.provider.write(
1185
1226
  {
@@ -1193,7 +1234,7 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1193
1234
  },
1194
1235
  signal
1195
1236
  );
1196
- return result.kind === 'applied';
1237
+ return result.kind === 'applied' ? 'reclaimed' : 'conflict';
1197
1238
  }
1198
1239
 
1199
1240
  override async reserve(
@@ -1203,6 +1244,7 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1203
1244
  ): Promise<ZLinkObjectReserveResult> {
1204
1245
  const encodedAuthorityKey = encodeAuthorityKey(request.key.kind, request.key.globalId);
1205
1246
  const reservationId = randomUUID();
1247
+ let reclaimVersion: string | undefined;
1206
1248
  for (;;) {
1207
1249
  throwIfOperationExpired(signal, deadlineUnixMs);
1208
1250
  const rowKey = authorityKey(encodedAuthorityKey.value);
@@ -1223,19 +1265,23 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1223
1265
  ) {
1224
1266
  return { kind: 'typeMismatch', current: snapshot };
1225
1267
  }
1226
- if (snapshot.allocation.state === 'active') {
1227
- return { kind: 'alreadyExists', current: snapshot };
1228
- }
1229
- if (
1230
- !(await this.releaseEndedReservation(
1231
- encodedAuthorityKey,
1232
- current.value.version.value,
1233
- signal
1234
- ))
1235
- )
1268
+ if (reclaimVersion !== undefined && snapshot.storeVersion.value !== reclaimVersion)
1236
1269
  return { kind: 'conflict', current: snapshot };
1270
+ const reclaim = await this.tryReleaseEndedReservation(
1271
+ encodedAuthorityKey,
1272
+ current.value.version.value,
1273
+ signal,
1274
+ request
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;
1237
1281
  continue;
1238
1282
  }
1283
+ if (reclaimVersion !== undefined)
1284
+ return { kind: 'conflict', current: { kind: 'missing', storeNow: current.storeNow } };
1239
1285
  const [descriptorRead, leaseRead] = await Promise.all([
1240
1286
  this.provider.read(descriptorKey, signal),
1241
1287
  this.provider.read(leaseKey, signal)
@@ -4040,13 +4086,21 @@ function adjustCapacity(
4040
4086
  if (delta.spotType !== undefined) {
4041
4087
  const key = capacityTypeKey(delta.spotType);
4042
4088
  const next = (spotTypes[key] ?? 0) + direction * delta.spotType.count;
4043
- 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
+ );
4044
4094
  if (next === 0) delete spotTypes[key];
4045
4095
  else spotTypes[key] = next;
4046
4096
  }
4047
4097
  const actors = usage.actors + direction * delta.actors;
4048
4098
  const spots = usage.spots + direction * delta.spots;
4049
- 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
+ );
4050
4104
  return { actors, spots, spotTypes };
4051
4105
  }
4052
4106
 
@@ -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.