@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.
@@ -45,6 +45,7 @@ export declare class ZLinkBackendResultError extends Error {
45
45
  }
46
46
  export declare function isZLinkBackendResultError(error: unknown): error is ZLinkBackendResultError;
47
47
  export declare function isBackendNotConnectedError(error: unknown): boolean;
48
+ export declare function isBackendRequestTimeoutError(error: unknown): boolean;
48
49
  export type ZLinkBackendMessageLike = Message | Buffer | Uint8Array | string;
49
50
  export interface ZLinkBackendSendSubmitBuilder {
50
51
  message(message: ZLinkBackendMessageLike): ZLinkBackendSendSubmitBuilder;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ZLinkBackendResultError = exports.ZLINK_BACKEND_RECV_DONT_WAIT = exports.ZLINK_BACKEND_SEND_NONE = exports.SubmitResult = exports.RequestResult = void 0;
4
4
  exports.isZLinkBackendResultError = isZLinkBackendResultError;
5
5
  exports.isBackendNotConnectedError = isBackendNotConnectedError;
6
+ exports.isBackendRequestTimeoutError = isBackendRequestTimeoutError;
6
7
  exports.RequestResult = Object.freeze({
7
8
  Ok: 0,
8
9
  TimedOut: 101,
@@ -64,3 +65,8 @@ function isBackendNotConnectedError(error) {
64
65
  const result = error.result;
65
66
  return result === exports.SubmitResult.NotConnected || result === exports.RequestResult.NotConnected;
66
67
  }
68
+ function isBackendRequestTimeoutError(error) {
69
+ return isZLinkBackendResultError(error)
70
+ && error.operation === 'request'
71
+ && error.result === exports.RequestResult.TimedOut;
72
+ }
@@ -15,6 +15,7 @@ const runtime_state_projections_1 = require("../foundation/runtime-state-project
15
15
  const service_wire_m6a_codec_1 = require("../foundation/service-wire-m6a-codec");
16
16
  const client_server_service_wire_1 = require("./client-server-service-wire");
17
17
  const fanout_service_wire_1 = require("./fanout-service-wire");
18
+ const runtime_values_1 = require("../backend/runtime-values");
18
19
  const MAX_LIFECYCLE_GENERATION = 0x7fffffffffffffffn;
19
20
  const CLIENT_SERVER_PROBE_INTERVAL_MS = 5_000;
20
21
  const CLIENT_SERVER_PEER_DEADLINE_MS = 15_000;
@@ -952,6 +953,7 @@ class ZLinkChannelSocketRegistry {
952
953
  return;
953
954
  const admissionAttempt = Symbol(connectionId);
954
955
  connection.admissionAttempt = admissionAttempt;
956
+ let retryAdmission = false;
955
957
  try {
956
958
  const admission = await requestClientServerAdmission(connection.dealer, channelName, 'default', 15_000);
957
959
  if (this.clientServerConnections.get(connectionId) !== connection
@@ -972,12 +974,21 @@ class ZLinkChannelSocketRegistry {
972
974
  || connection.admissionAttempt !== admissionAttempt)
973
975
  return;
974
976
  this.removeReadyConnection(connectionId);
977
+ if ((0, runtime_values_1.isBackendRequestTimeoutError)(error)) {
978
+ retryAdmission = true;
979
+ return;
980
+ }
975
981
  throw error;
976
982
  }
977
983
  finally {
978
984
  if (this.clientServerConnections.get(connectionId) === connection
985
+ && connection.physicalConnectionId === physicalConnectionId
979
986
  && connection.admissionAttempt === admissionAttempt) {
980
987
  connection.admissionAttempt = undefined;
988
+ if (retryAdmission) {
989
+ void this.admitConfiguredClientServerConnection(channelName, connectionId)
990
+ .catch(error => this.oneWayFailureSink?.(error));
991
+ }
981
992
  }
982
993
  }
983
994
  }
@@ -8,6 +8,7 @@ const runtime_message_1 = require("../backend/runtime-message");
8
8
  const client_server_service_wire_1 = require("./client-server-service-wire");
9
9
  const runtime_state_projections_1 = require("../foundation/runtime-state-projections");
10
10
  const state_lane_1 = require("../execution/state-lane");
11
+ const runtime_values_1 = require("../backend/runtime-values");
11
12
  /**
12
13
  * Owns the dedicated ClientServer discovery domain. It never converts these
13
14
  * descriptors into RouteMesh peer rows.
@@ -260,6 +261,7 @@ class ZLinkClientServerLocationRuntime {
260
261
  if (current === undefined) {
261
262
  return;
262
263
  }
264
+ let retryAdmission = false;
263
265
  try {
264
266
  const admission = await requestAdmission(current.dealer, current.target.descriptor, this.options.ownerLeaseRenewTimeoutMs);
265
267
  const admittedDescriptor = await this.lane.run(() => {
@@ -294,13 +296,23 @@ class ZLinkClientServerLocationRuntime {
294
296
  current.target.dealer = undefined;
295
297
  await this.sockets.closeClientServerConnection(connectionId);
296
298
  }
299
+ if ((0, runtime_values_1.isBackendRequestTimeoutError)(error)) {
300
+ retryAdmission = true;
301
+ return;
302
+ }
297
303
  throw error;
298
304
  }
299
305
  finally {
300
- await this.lane.run(() => {
301
- if (this.connections.get(connectionId) === current.target)
302
- current.target.handshakeInFlight = false;
306
+ const shouldRetry = await this.lane.run(() => {
307
+ if (this.connections.get(connectionId) !== current.target)
308
+ return false;
309
+ current.target.handshakeInFlight = false;
310
+ return retryAdmission;
303
311
  });
312
+ if (shouldRetry) {
313
+ void this.handleTransportReady(connectionId, _routingId, _endpoint)
314
+ .catch(error => this.locationRuntime.reportDiscoveryFailure(error));
315
+ }
304
316
  }
305
317
  }
306
318
  async removeSupersededConnections(admitted) {
@@ -56,6 +56,7 @@ export declare class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationS
56
56
  private removeOwnedAuthorities;
57
57
  private removeOwnerCleanupBatch;
58
58
  private updateDescriptor;
59
+ private writeDescriptorWithReconciliation;
59
60
  private removeDescriptor;
60
61
  private listDescriptors;
61
62
  private updateOwnedLocation;
@@ -23,6 +23,7 @@ const MAX_CREATION_TERMINAL_BYTES = 1024 * 1024;
23
23
  const CREATION_TERMINAL_RETENTION_MS = 5 * 60 * 1000;
24
24
  const AGGREGATE_COMMIT_RETRY_WINDOW_MS = 5_000;
25
25
  const MAX_AGGREGATE_COMMIT_CONFLICT_RETRIES = 64;
26
+ const MAX_DESCRIPTOR_WRITE_RETRIES = 3;
26
27
  const MAX_OWNER_CLEANUP_BATCH_ROWS = 2_047;
27
28
  /**
28
29
  * Maps framework domain records to the minimal provider SPI.
@@ -1225,62 +1226,7 @@ class ZLinkLocationStoreRepository extends in_memory_location_store_1.ZLinkInMem
1225
1226
  return result.kind === 'applied' ? 'released' : 'stale';
1226
1227
  }
1227
1228
  async updateMeshNode(descriptor, intent, signal) {
1228
- const leaseKey = ownerKey(descriptor.ownerId);
1229
- const rowKey = meshKey(descriptor.meshName, descriptor.rid);
1230
- const [lease, current] = await Promise.all([
1231
- this.provider.read(leaseKey, signal),
1232
- this.provider.read(rowKey, signal)
1233
- ]);
1234
- if (lease.kind === 'missing')
1235
- return rejected(lease.storeNow);
1236
- if (liveOwnerLeaseGeneration(lease) !== descriptor.leaseGeneration) {
1237
- return rejected(lease.value.storeNow);
1238
- }
1239
- let generation = 1n;
1240
- let rowCondition = { kind: 'missing', key: rowKey };
1241
- if (current.kind === 'found') {
1242
- const record = decodeCanonicalDescriptorRecord(current.value.bytes);
1243
- const stored = reviveMeshDescriptor(record.descriptor);
1244
- generation = descriptorStoreGeneration(record, current.value.version.value);
1245
- if (sameMeshDescriptor(stored, descriptor)) {
1246
- return { status: Locations_1.ZLinkLocationWriteStatus.Stored, generation, updatedAt: current.value.storeNow };
1247
- }
1248
- const currentLease = await this.provider.read(ownerKey(stored.ownerId), signal);
1249
- const currentLeaseGeneration = liveOwnerLeaseGeneration(currentLease);
1250
- const takeover = canTakeOverStoredLocation(intent, stored.ownerId, stored.leaseGeneration, descriptor.ownerId, descriptor.leaseGeneration, currentLeaseGeneration);
1251
- const renew = intent === 2
1252
- && stored.ownerId === descriptor.ownerId
1253
- && stored.leaseGeneration === descriptor.leaseGeneration
1254
- && stored.lifecycleGeneration === descriptor.lifecycleGeneration
1255
- && descriptor.descriptorRevision > stored.descriptorRevision;
1256
- if (!takeover && !renew) {
1257
- return {
1258
- status: Locations_1.ZLinkLocationWriteStatus.IgnoredStale,
1259
- generation,
1260
- updatedAt: current.value.storeNow
1261
- };
1262
- }
1263
- if (takeover)
1264
- generation += 1n;
1265
- rowCondition = { kind: 'version', key: rowKey, expected: current.value.version };
1266
- }
1267
- else if (intent === 2) {
1268
- return rejected(lease.value.storeNow);
1269
- }
1270
- const result = await this.provider.write({
1271
- conditions: [
1272
- { kind: 'version', key: leaseKey, expected: lease.value.version },
1273
- rowCondition
1274
- ],
1275
- mutations: [{
1276
- kind: 'put',
1277
- key: rowKey,
1278
- bytes: encodeCanonicalDescriptorRecord(generation, persistMeshDescriptor(descriptor))
1279
- }]
1280
- }, signal);
1281
- if (result.kind === 'conflict')
1282
- return rejected(result.storeNow);
1283
- return { status: Locations_1.ZLinkLocationWriteStatus.Stored, generation, updatedAt: result.storeNow };
1229
+ return this.updateDescriptor(meshKey(descriptor.meshName, descriptor.rid), descriptor, intent, reviveMeshDescriptor, sameMeshDescriptor, canRenewMesh, signal);
1284
1230
  }
1285
1231
  async removeMeshNode(key, owner, signal) {
1286
1232
  const rowKey = meshKey(key.meshName, key.rid);
@@ -1656,11 +1602,10 @@ class ZLinkLocationStoreRepository extends in_memory_location_store_1.ZLinkInMem
1656
1602
  ]);
1657
1603
  if (lease.kind === 'missing')
1658
1604
  return rejected(lease.storeNow);
1659
- if (liveOwnerLeaseGeneration(lease) !== descriptor.leaseGeneration) {
1605
+ if (!matchesLiveOwnerLease(lease, descriptor.ownerId, descriptor.leaseGeneration)) {
1660
1606
  return rejected(lease.value.storeNow);
1661
1607
  }
1662
1608
  let generation = 1n;
1663
- let rowCondition = { kind: 'missing', key: rowKey };
1664
1609
  if (current.kind === 'found') {
1665
1610
  const record = decodeCanonicalDescriptorRecord(current.value.bytes);
1666
1611
  const stored = revive(record.descriptor);
@@ -1681,25 +1626,44 @@ class ZLinkLocationStoreRepository extends in_memory_location_store_1.ZLinkInMem
1681
1626
  }
1682
1627
  if (takeover)
1683
1628
  generation += 1n;
1684
- rowCondition = { kind: 'version', key: rowKey, expected: current.value.version };
1685
1629
  }
1686
1630
  else if (intent === 2) {
1687
1631
  return rejected(lease.value.storeNow);
1688
1632
  }
1689
- const result = await this.provider.write({
1690
- conditions: [
1691
- { kind: 'version', key: leaseKey, expected: lease.value.version },
1692
- rowCondition
1693
- ],
1694
- mutations: [{
1695
- kind: 'put',
1696
- key: rowKey,
1697
- bytes: encodeCanonicalDescriptorRecord(generation, descriptor)
1698
- }]
1699
- }, signal);
1700
- if (result.kind === 'conflict')
1701
- return rejected(result.storeNow);
1702
- return { status: Locations_1.ZLinkLocationWriteStatus.Stored, generation, updatedAt: result.storeNow };
1633
+ return this.writeDescriptorWithReconciliation(leaseKey, descriptor.ownerId, descriptor.leaseGeneration, lease, rowKey, current, encodeCanonicalDescriptorRecord(generation, descriptor), generation, signal);
1634
+ }
1635
+ async writeDescriptorWithReconciliation(leaseKey, ownerId, leaseGeneration, liveLease, rowKey, predecessor, encodedDescriptor, generation, signal) {
1636
+ let currentLease = liveLease;
1637
+ let currentRow = predecessor;
1638
+ for (let attempt = 0; attempt <= MAX_DESCRIPTOR_WRITE_RETRIES; attempt += 1) {
1639
+ const rowCondition = currentRow.kind === 'found'
1640
+ ? { kind: 'version', key: rowKey, expected: currentRow.value.version }
1641
+ : { kind: 'missing', key: rowKey };
1642
+ const result = await this.provider.write({
1643
+ conditions: [
1644
+ { kind: 'version', key: leaseKey, expected: currentLease.value.version },
1645
+ rowCondition
1646
+ ],
1647
+ mutations: [{ kind: 'put', key: rowKey, bytes: encodedDescriptor }]
1648
+ }, signal);
1649
+ if (result.kind === 'applied') {
1650
+ return { status: Locations_1.ZLinkLocationWriteStatus.Stored, generation, updatedAt: result.storeNow };
1651
+ }
1652
+ if (attempt === MAX_DESCRIPTOR_WRITE_RETRIES)
1653
+ return ignoredStale(result.storeNow);
1654
+ const [refreshedLease, refreshedRow] = await Promise.all([
1655
+ this.provider.read(leaseKey, signal),
1656
+ this.provider.read(rowKey, signal)
1657
+ ]);
1658
+ if (refreshedLease.kind === 'missing'
1659
+ || !matchesLiveOwnerLease(refreshedLease, ownerId, leaseGeneration)
1660
+ || !sameDescriptorPredecessor(predecessor, refreshedRow)) {
1661
+ return ignoredStale(result.storeNow);
1662
+ }
1663
+ currentLease = refreshedLease;
1664
+ currentRow = refreshedRow;
1665
+ }
1666
+ throw new Error('Descriptor write retry loop exhausted unexpectedly.');
1703
1667
  }
1704
1668
  async removeDescriptor(rowKey, owner, revive, signal) {
1705
1669
  const current = await this.provider.read(rowKey, signal);
@@ -2933,10 +2897,24 @@ function sameCreationTarget(snapshot, target) {
2933
2897
  && snapshot.ownerLeaseGeneration === target.owner.leaseGeneration;
2934
2898
  }
2935
2899
  function sameLiveOwner(lease, snapshot) {
2936
- if (liveOwnerLeaseGeneration(lease) !== snapshot.ownerLeaseGeneration)
2900
+ if (lease.kind === 'missing')
2901
+ return false;
2902
+ return matchesLiveOwnerLease(lease, snapshot.ownerId, snapshot.ownerLeaseGeneration);
2903
+ }
2904
+ function matchesLiveOwnerLease(lease, ownerId, leaseGeneration) {
2905
+ if (lease.value.expiresAt === undefined
2906
+ || lease.value.expiresAt.getTime() <= lease.value.storeNow.getTime()) {
2937
2907
  return false;
2908
+ }
2938
2909
  const owner = decodeOwnerRecord(lease.value.bytes);
2939
- return owner.ownerId === snapshot.ownerId;
2910
+ return owner.ownerId === ownerId
2911
+ && BigInt(owner.leaseGeneration) === leaseGeneration;
2912
+ }
2913
+ function sameDescriptorPredecessor(expected, current) {
2914
+ if (expected.kind === 'missing' || current.kind === 'missing') {
2915
+ return expected.kind === current.kind;
2916
+ }
2917
+ return Buffer.from(expected.value.bytes).equals(Buffer.from(current.value.bytes));
2940
2918
  }
2941
2919
  function liveOwnerLeaseGeneration(lease) {
2942
2920
  if (lease.kind === 'missing'
@@ -3118,6 +3096,10 @@ function sameClientServerDescriptor(left, right) {
3118
3096
  function sameFanoutDescriptor(left, right) {
3119
3097
  return descriptorFingerprint(left, ['updatedAt']) === descriptorFingerprint(right, ['updatedAt']);
3120
3098
  }
3099
+ function canRenewMesh(current, next) {
3100
+ return sameOwnerGeneration(current, next)
3101
+ && next.descriptorRevision > current.descriptorRevision;
3102
+ }
3121
3103
  function canRenewClientServer(current, next) {
3122
3104
  return sameOwnerGeneration(current, next)
3123
3105
  && next.descriptorRevision > current.descriptorRevision
@@ -3188,6 +3170,13 @@ function rejected(updatedAt) {
3188
3170
  updatedAt
3189
3171
  };
3190
3172
  }
3173
+ function ignoredStale(updatedAt) {
3174
+ return {
3175
+ status: Locations_1.ZLinkLocationWriteStatus.IgnoredStale,
3176
+ generation: 0n,
3177
+ updatedAt
3178
+ };
3179
+ }
3191
3180
  function requireOwnerInput(ownerId, leaseTtlMs) {
3192
3181
  if (Buffer.byteLength(ownerId, 'utf8') < 1 || ownerId.includes('\0')) {
3193
3182
  throw new TypeError('Owner ID must be non-empty UTF-8 without NUL.');
package/package.json CHANGED
@@ -1,7 +1,13 @@
1
1
  {
2
2
  "name": "@zlink-systems/framework",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/zlink-systems/zlink.git",
8
+ "directory": "framework/languages/node/packages/framework"
9
+ },
10
+ "homepage": "https://zlink.systems/",
5
11
  "main": "dist/index.js",
6
12
  "types": "dist/index.d.ts",
7
13
  "exports": {
@@ -13,7 +19,7 @@
13
19
  "dependencies": {
14
20
  "@opentelemetry/api": "^1.9.1",
15
21
  "@opentelemetry/api-logs": "^0.221.0",
16
- "@zlink-systems/stream-wire": "0.10.0",
17
- "@zlink-systems/zlink": "0.17.3"
22
+ "@zlink-systems/stream-wire": "0.11.0",
23
+ "@zlink-systems/zlink": "0.17.6"
18
24
  }
19
25
  }
@@ -68,6 +68,12 @@ export function isBackendNotConnectedError(error: unknown): boolean {
68
68
  const result = error.result;
69
69
  return result === SubmitResult.NotConnected || result === RequestResult.NotConnected;
70
70
  }
71
+
72
+ export function isBackendRequestTimeoutError(error: unknown): boolean {
73
+ return isZLinkBackendResultError(error)
74
+ && error.operation === 'request'
75
+ && error.result === RequestResult.TimedOut;
76
+ }
71
77
  export type ZLinkBackendMessageLike = Message | Buffer | Uint8Array | string;
72
78
  export interface ZLinkBackendSendSubmitBuilder {
73
79
  message(message: ZLinkBackendMessageLike): ZLinkBackendSendSubmitBuilder;
@@ -53,6 +53,9 @@ import {
53
53
  } from './fanout-service-wire';
54
54
  import type { ZLinkChannelEnvelopeHeader } from './channel-envelope';
55
55
  import type { ApplicationJobQueue } from '../host/application-job-queue';
56
+ import {
57
+ isBackendRequestTimeoutError
58
+ } from '../backend/runtime-values';
56
59
 
57
60
  const MAX_LIFECYCLE_GENERATION = 0x7fff_ffff_ffff_ffffn;
58
61
  const CLIENT_SERVER_PROBE_INTERVAL_MS = 5_000;
@@ -1213,6 +1216,7 @@ export class ZLinkChannelSocketRegistry {
1213
1216
  if (connection.admissionAttempt !== undefined) return;
1214
1217
  const admissionAttempt = Symbol(connectionId);
1215
1218
  connection.admissionAttempt = admissionAttempt;
1219
+ let retryAdmission = false;
1216
1220
  try {
1217
1221
  const admission = await requestClientServerAdmission(
1218
1222
  connection.dealer,
@@ -1242,11 +1246,20 @@ export class ZLinkChannelSocketRegistry {
1242
1246
  || connection.physicalConnectionId !== physicalConnectionId
1243
1247
  || connection.admissionAttempt !== admissionAttempt) return;
1244
1248
  this.removeReadyConnection(connectionId);
1249
+ if (isBackendRequestTimeoutError(error)) {
1250
+ retryAdmission = true;
1251
+ return;
1252
+ }
1245
1253
  throw error;
1246
1254
  } finally {
1247
1255
  if (this.clientServerConnections.get(connectionId) === connection
1256
+ && connection.physicalConnectionId === physicalConnectionId
1248
1257
  && connection.admissionAttempt === admissionAttempt) {
1249
1258
  connection.admissionAttempt = undefined;
1259
+ if (retryAdmission) {
1260
+ void this.admitConfiguredClientServerConnection(channelName, connectionId)
1261
+ .catch(error => this.oneWayFailureSink?.(error));
1262
+ }
1250
1263
  }
1251
1264
  }
1252
1265
  }
@@ -24,6 +24,9 @@ import {
24
24
  } from './client-server-service-wire';
25
25
  import { discoveryAvailabilityForRuntimeState } from '../foundation/runtime-state-projections';
26
26
  import { ZLinkStateLane } from '../execution/state-lane';
27
+ import {
28
+ isBackendRequestTimeoutError
29
+ } from '../backend/runtime-values';
27
30
 
28
31
  interface ActiveClientServerTarget {
29
32
  descriptor: ZLinkClientServerServerDescriptor;
@@ -331,6 +334,7 @@ export class ZLinkClientServerLocationRuntime {
331
334
  if (current === undefined) {
332
335
  return;
333
336
  }
337
+ let retryAdmission = false;
334
338
  try {
335
339
  const admission = await requestAdmission(
336
340
  current.dealer,
@@ -370,11 +374,21 @@ export class ZLinkClientServerLocationRuntime {
370
374
  current.target.dealer = undefined;
371
375
  await this.sockets.closeClientServerConnection(connectionId);
372
376
  }
377
+ if (isBackendRequestTimeoutError(error)) {
378
+ retryAdmission = true;
379
+ return;
380
+ }
373
381
  throw error;
374
382
  } finally {
375
- await this.lane.run(() => {
376
- if (this.connections.get(connectionId) === current.target) current.target.handshakeInFlight = false;
383
+ const shouldRetry = await this.lane.run(() => {
384
+ if (this.connections.get(connectionId) !== current.target) return false;
385
+ current.target.handshakeInFlight = false;
386
+ return retryAdmission;
377
387
  });
388
+ if (shouldRetry) {
389
+ void this.handleTransportReady(connectionId, _routingId, _endpoint)
390
+ .catch(error => this.locationRuntime.reportDiscoveryFailure(error));
391
+ }
378
392
  }
379
393
  }
380
394