@zlink-systems/framework 0.10.0 → 0.11.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.
@@ -92,6 +92,7 @@ const MAX_CREATION_TERMINAL_BYTES = 1024 * 1024;
92
92
  const CREATION_TERMINAL_RETENTION_MS = 5 * 60 * 1000;
93
93
  const AGGREGATE_COMMIT_RETRY_WINDOW_MS = 5_000;
94
94
  const MAX_AGGREGATE_COMMIT_CONFLICT_RETRIES = 64;
95
+ const MAX_DESCRIPTOR_WRITE_RETRIES = 3;
95
96
 
96
97
  type StoredAuthoritySnapshot = Omit<
97
98
  ZLinkAuthoritySnapshot,
@@ -1603,67 +1604,15 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
1603
1604
  intent: ZLinkLocationWriteIntent,
1604
1605
  signal?: AbortSignal
1605
1606
  ): Promise<ZLinkLocationWriteResult> {
1606
- const leaseKey = ownerKey(descriptor.ownerId);
1607
- const rowKey = meshKey(descriptor.meshName, descriptor.rid);
1608
- const [lease, current] = await Promise.all([
1609
- this.provider.read(leaseKey, signal),
1610
- this.provider.read(rowKey, signal)
1611
- ]);
1612
- if (lease.kind === 'missing') return rejected(lease.storeNow);
1613
- if (liveOwnerLeaseGeneration(lease) !== descriptor.leaseGeneration) {
1614
- return rejected(lease.value.storeNow);
1615
- }
1616
-
1617
- let generation = 1n;
1618
- let rowCondition: ZLinkStoreCondition = { kind: 'missing', key: rowKey };
1619
- if (current.kind === 'found') {
1620
- const record = decodeCanonicalDescriptorRecord<ZLinkMeshNodeDescriptor>(current.value.bytes);
1621
- const stored = reviveMeshDescriptor(record.descriptor);
1622
- generation = descriptorStoreGeneration(record, current.value.version.value);
1623
- if (sameMeshDescriptor(stored, descriptor)) {
1624
- return { status: WriteStatus.Stored, generation, updatedAt: current.value.storeNow };
1625
- }
1626
- const currentLease = await this.provider.read(ownerKey(stored.ownerId), signal);
1627
- const currentLeaseGeneration = liveOwnerLeaseGeneration(currentLease);
1628
- const takeover = canTakeOverStoredLocation(
1629
- intent,
1630
- stored.ownerId,
1631
- stored.leaseGeneration,
1632
- descriptor.ownerId,
1633
- descriptor.leaseGeneration,
1634
- currentLeaseGeneration
1635
- );
1636
- const renew = intent === 2
1637
- && stored.ownerId === descriptor.ownerId
1638
- && stored.leaseGeneration === descriptor.leaseGeneration
1639
- && stored.lifecycleGeneration === descriptor.lifecycleGeneration
1640
- && descriptor.descriptorRevision > stored.descriptorRevision;
1641
- if (!takeover && !renew) {
1642
- return {
1643
- status: WriteStatus.IgnoredStale,
1644
- generation,
1645
- updatedAt: current.value.storeNow
1646
- };
1647
- }
1648
- if (takeover) generation += 1n;
1649
- rowCondition = { kind: 'version', key: rowKey, expected: current.value.version };
1650
- } else if (intent === 2) {
1651
- return rejected(lease.value.storeNow);
1652
- }
1653
-
1654
- const result = await this.provider.write({
1655
- conditions: [
1656
- { kind: 'version', key: leaseKey, expected: lease.value.version },
1657
- rowCondition
1658
- ],
1659
- mutations: [{
1660
- kind: 'put',
1661
- key: rowKey,
1662
- bytes: encodeCanonicalDescriptorRecord(generation, persistMeshDescriptor(descriptor))
1663
- }]
1664
- }, signal);
1665
- if (result.kind === 'conflict') return rejected(result.storeNow);
1666
- return { status: WriteStatus.Stored, generation, updatedAt: result.storeNow };
1607
+ return this.updateDescriptor(
1608
+ meshKey(descriptor.meshName, descriptor.rid),
1609
+ descriptor,
1610
+ intent,
1611
+ reviveMeshDescriptor,
1612
+ sameMeshDescriptor,
1613
+ canRenewMesh,
1614
+ signal
1615
+ );
1667
1616
  }
1668
1617
 
1669
1618
  override async removeMeshNode(
@@ -2253,12 +2202,15 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
2253
2202
  this.provider.read(rowKey, signal)
2254
2203
  ]);
2255
2204
  if (lease.kind === 'missing') return rejected(lease.storeNow);
2256
- if (liveOwnerLeaseGeneration(lease) !== descriptor.leaseGeneration) {
2205
+ if (!matchesLiveOwnerLease(
2206
+ lease,
2207
+ descriptor.ownerId,
2208
+ descriptor.leaseGeneration
2209
+ )) {
2257
2210
  return rejected(lease.value.storeNow);
2258
2211
  }
2259
2212
 
2260
2213
  let generation = 1n;
2261
- let rowCondition: ZLinkStoreCondition = { kind: 'missing', key: rowKey };
2262
2214
  if (current.kind === 'found') {
2263
2215
  const record = decodeCanonicalDescriptorRecord<T>(current.value.bytes);
2264
2216
  const stored = revive(record.descriptor);
@@ -2285,24 +2237,67 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
2285
2237
  };
2286
2238
  }
2287
2239
  if (takeover) generation += 1n;
2288
- rowCondition = { kind: 'version', key: rowKey, expected: current.value.version };
2289
2240
  } else if (intent === 2) {
2290
2241
  return rejected(lease.value.storeNow);
2291
2242
  }
2292
2243
 
2293
- const result = await this.provider.write({
2294
- conditions: [
2295
- { kind: 'version', key: leaseKey, expected: lease.value.version },
2296
- rowCondition
2297
- ],
2298
- mutations: [{
2299
- kind: 'put',
2300
- key: rowKey,
2301
- bytes: encodeCanonicalDescriptorRecord(generation, descriptor)
2302
- }]
2303
- }, signal);
2304
- if (result.kind === 'conflict') return rejected(result.storeNow);
2305
- return { status: WriteStatus.Stored, generation, updatedAt: result.storeNow };
2244
+ return this.writeDescriptorWithReconciliation(
2245
+ leaseKey,
2246
+ descriptor.ownerId,
2247
+ descriptor.leaseGeneration,
2248
+ lease,
2249
+ rowKey,
2250
+ current,
2251
+ encodeCanonicalDescriptorRecord(generation, descriptor),
2252
+ generation,
2253
+ signal
2254
+ );
2255
+ }
2256
+
2257
+ private async writeDescriptorWithReconciliation(
2258
+ leaseKey: ZLinkStoreKey,
2259
+ ownerId: string,
2260
+ leaseGeneration: bigint,
2261
+ liveLease: Extract<ZLinkStoreReadResult, { kind: 'found' }>,
2262
+ rowKey: ZLinkStoreKey,
2263
+ predecessor: ZLinkStoreReadResult,
2264
+ encodedDescriptor: Uint8Array,
2265
+ generation: bigint,
2266
+ signal?: AbortSignal
2267
+ ): Promise<ZLinkLocationWriteResult> {
2268
+ let currentLease = liveLease;
2269
+ let currentRow = predecessor;
2270
+
2271
+ for (let attempt = 0; attempt <= MAX_DESCRIPTOR_WRITE_RETRIES; attempt += 1) {
2272
+ const rowCondition: ZLinkStoreCondition = currentRow.kind === 'found'
2273
+ ? { kind: 'version', key: rowKey, expected: currentRow.value.version }
2274
+ : { kind: 'missing', key: rowKey };
2275
+ const result = await this.provider.write({
2276
+ conditions: [
2277
+ { kind: 'version', key: leaseKey, expected: currentLease.value.version },
2278
+ rowCondition
2279
+ ],
2280
+ mutations: [{ kind: 'put', key: rowKey, bytes: encodedDescriptor }]
2281
+ }, signal);
2282
+ if (result.kind === 'applied') {
2283
+ return { status: WriteStatus.Stored, generation, updatedAt: result.storeNow };
2284
+ }
2285
+ if (attempt === MAX_DESCRIPTOR_WRITE_RETRIES) return ignoredStale(result.storeNow);
2286
+
2287
+ const [refreshedLease, refreshedRow] = await Promise.all([
2288
+ this.provider.read(leaseKey, signal),
2289
+ this.provider.read(rowKey, signal)
2290
+ ]);
2291
+ if (refreshedLease.kind === 'missing'
2292
+ || !matchesLiveOwnerLease(refreshedLease, ownerId, leaseGeneration)
2293
+ || !sameDescriptorPredecessor(predecessor, refreshedRow)) {
2294
+ return ignoredStale(result.storeNow);
2295
+ }
2296
+
2297
+ currentLease = refreshedLease;
2298
+ currentRow = refreshedRow;
2299
+ }
2300
+ throw new Error('Descriptor write retry loop exhausted unexpectedly.');
2306
2301
  }
2307
2302
 
2308
2303
  private async removeDescriptor<T extends OwnedDescriptor>(
@@ -3888,9 +3883,36 @@ function sameLiveOwner(
3888
3883
  lease: ZLinkStoreReadResult,
3889
3884
  snapshot: StoredAuthoritySnapshot
3890
3885
  ): boolean {
3891
- if (liveOwnerLeaseGeneration(lease) !== snapshot.ownerLeaseGeneration) return false;
3892
- const owner = decodeOwnerRecord((lease as Extract<ZLinkStoreReadResult, { kind: 'found' }>).value.bytes);
3893
- return owner.ownerId === snapshot.ownerId;
3886
+ if (lease.kind === 'missing') return false;
3887
+ return matchesLiveOwnerLease(
3888
+ lease,
3889
+ snapshot.ownerId,
3890
+ snapshot.ownerLeaseGeneration
3891
+ );
3892
+ }
3893
+
3894
+ function matchesLiveOwnerLease(
3895
+ lease: Extract<ZLinkStoreReadResult, { kind: 'found' }>,
3896
+ ownerId: string,
3897
+ leaseGeneration: bigint
3898
+ ): boolean {
3899
+ if (lease.value.expiresAt === undefined
3900
+ || lease.value.expiresAt.getTime() <= lease.value.storeNow.getTime()) {
3901
+ return false;
3902
+ }
3903
+ const owner = decodeOwnerRecord(lease.value.bytes);
3904
+ return owner.ownerId === ownerId
3905
+ && BigInt(owner.leaseGeneration) === leaseGeneration;
3906
+ }
3907
+
3908
+ function sameDescriptorPredecessor(
3909
+ expected: ZLinkStoreReadResult,
3910
+ current: ZLinkStoreReadResult
3911
+ ): boolean {
3912
+ if (expected.kind === 'missing' || current.kind === 'missing') {
3913
+ return expected.kind === current.kind;
3914
+ }
3915
+ return Buffer.from(expected.value.bytes).equals(Buffer.from(current.value.bytes));
3894
3916
  }
3895
3917
 
3896
3918
  function liveOwnerLeaseGeneration(
@@ -4113,6 +4135,14 @@ function sameFanoutDescriptor(
4113
4135
  return descriptorFingerprint(left, ['updatedAt']) === descriptorFingerprint(right, ['updatedAt']);
4114
4136
  }
4115
4137
 
4138
+ function canRenewMesh(
4139
+ current: ZLinkMeshNodeDescriptor,
4140
+ next: ZLinkMeshNodeDescriptor
4141
+ ): boolean {
4142
+ return sameOwnerGeneration(current, next)
4143
+ && next.descriptorRevision > current.descriptorRevision;
4144
+ }
4145
+
4116
4146
  function canRenewClientServer(
4117
4147
  current: ZLinkClientServerServerDescriptor,
4118
4148
  next: ZLinkClientServerServerDescriptor
@@ -4201,6 +4231,14 @@ function rejected(updatedAt: Date): ZLinkLocationWriteResult {
4201
4231
  };
4202
4232
  }
4203
4233
 
4234
+ function ignoredStale(updatedAt: Date): ZLinkLocationWriteResult {
4235
+ return {
4236
+ status: WriteStatus.IgnoredStale,
4237
+ generation: 0n,
4238
+ updatedAt
4239
+ };
4240
+ }
4241
+
4204
4242
  function requireOwnerInput(ownerId: string, leaseTtlMs: number): void {
4205
4243
  if (Buffer.byteLength(ownerId, 'utf8') < 1 || ownerId.includes('\0')) {
4206
4244
  throw new TypeError('Owner ID must be non-empty UTF-8 without NUL.');