@zlink-systems/framework 0.19.0 → 0.21.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/contracts/Configuration/Builders.d.ts +0 -2
- package/dist/contracts/Configuration/Configs.d.ts +1 -0
- package/dist/contracts/Configuration/RegistrationTypes.d.ts +1 -2
- package/dist/contracts/Locations/Authority.d.ts +1 -7
- package/dist/runtime/actors/actor-authority-publication.js +25 -22
- package/dist/runtime/actors/actor-creation.js +30 -9
- package/dist/runtime/actors/actor-runtime-state.d.ts +3 -0
- package/dist/runtime/actors/index.d.ts +3 -0
- package/dist/runtime/actors/index.js +14 -3
- package/dist/runtime/admission.d.ts +2 -0
- package/dist/runtime/admission.js +11 -5
- package/dist/runtime/backend/contracts/index.d.ts +4 -1
- package/dist/runtime/backend/node/node-mesh-backend-adapter.d.ts +1 -0
- package/dist/runtime/backend/node/node-mesh-backend-adapter.js +1 -1
- package/dist/runtime/backend/node/node-raw-binding-port.d.ts +2 -1
- package/dist/runtime/backend/node/node-raw-binding-port.js +12 -5
- package/dist/runtime/backend/node/node-raw-mesh-backend.d.ts +1 -1
- package/dist/runtime/backend/node/node-socket-backend-adapter.js +6 -0
- package/dist/runtime/channels/channel-socket-options.js +7 -0
- package/dist/runtime/channels/channel-socket-registry.js +3 -0
- package/dist/runtime/foundation/service-stateful-runtime.d.ts +1 -1
- package/dist/runtime/foundation/service-stateful-runtime.js +1 -5
- package/dist/runtime/host/actor-placement-coordinator.d.ts +14 -5
- package/dist/runtime/host/actor-placement-coordinator.js +159 -47
- package/dist/runtime/host/index.d.ts +2 -1
- package/dist/runtime/host/index.js +77 -120
- package/dist/runtime/host/location-runtime-owner.js +4 -2
- package/dist/runtime/host/remote-actor-join-receiver.js +12 -3
- package/dist/runtime/locations/creation-operation-id.d.ts +4 -0
- package/dist/runtime/locations/creation-operation-id.js +12 -0
- package/dist/runtime/locations/in-memory-authority-store.js +8 -22
- package/dist/runtime/locations/location-store-repository.js +36 -59
- package/dist/runtime/locations/opaque-record-key.d.ts +5 -0
- package/dist/runtime/locations/opaque-record-key.js +16 -0
- package/dist/runtime/locations/runtime.d.ts +7 -0
- package/dist/runtime/locations/runtime.js +130 -42
- package/dist/runtime/protocol/service_wire_codec.generated.d.ts +1672 -0
- package/dist/runtime/protocol/service_wire_codec.generated.js +9169 -0
- package/dist/runtime/spots/spot-node-runtime-manager.js +2 -2
- package/package.json +2 -2
- package/src/contracts/Configuration/Builders.ts +0 -2
- package/src/contracts/Configuration/Configs.ts +1 -0
- package/src/contracts/Configuration/RegistrationTypes.ts +1 -2
- package/src/contracts/Locations/Authority.ts +1 -8
- package/src/runtime/actors/actor-authority-publication.ts +25 -22
- package/src/runtime/actors/actor-creation.ts +45 -11
- package/src/runtime/actors/actor-runtime-state.ts +4 -0
- package/src/runtime/actors/index.ts +32 -4
- package/src/runtime/admission.ts +8 -5
- package/src/runtime/backend/contracts/index.ts +4 -1
- package/src/runtime/backend/node/node-mesh-backend-adapter.ts +2 -1
- package/src/runtime/backend/node/node-raw-binding-port.ts +16 -5
- package/src/runtime/backend/node/node-raw-mesh-backend.ts +1 -1
- package/src/runtime/backend/node/node-socket-backend-adapter.ts +7 -0
- package/src/runtime/channels/channel-socket-options.ts +9 -0
- package/src/runtime/channels/channel-socket-registry.ts +4 -0
- package/src/runtime/foundation/service-stateful-runtime.ts +2 -6
- package/src/runtime/host/actor-placement-coordinator.ts +216 -77
- package/src/runtime/host/index.ts +95 -135
- package/src/runtime/host/location-runtime-owner.ts +4 -2
- package/src/runtime/host/remote-actor-join-receiver.ts +15 -3
- package/src/runtime/locations/creation-operation-id.ts +11 -0
- package/src/runtime/locations/in-memory-authority-store.ts +7 -29
- package/src/runtime/locations/location-store-repository.ts +71 -69
- package/src/runtime/locations/opaque-record-key.ts +26 -0
- package/src/runtime/locations/runtime.ts +154 -60
- package/src/runtime/protocol/service_wire_codec.generated.ts +8302 -0
- package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +1 -1
- package/src/runtime/spots/spot-node-runtime-manager.ts +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash, randomUUID
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import type {
|
|
3
3
|
ZLinkLocationStore,
|
|
4
4
|
ZLinkLocationPage,
|
|
@@ -74,6 +74,11 @@ import {
|
|
|
74
74
|
import { ZLinkLocationWriteStatus as WriteStatus } from '../../contracts/Locations';
|
|
75
75
|
import { ZLinkInMemoryLocationStore } from './in-memory-location-store';
|
|
76
76
|
import { storeKey } from './in-memory-provider-location-store';
|
|
77
|
+
import {
|
|
78
|
+
creationTerminalPreimage,
|
|
79
|
+
opaqueRecordPreimage,
|
|
80
|
+
routingIdHexSegment
|
|
81
|
+
} from './opaque-record-key';
|
|
77
82
|
import { decodeAuthorityKey, encodeAuthorityKey } from './authority-key-codec';
|
|
78
83
|
import { ZLinkAggregateInventoryStore } from './aggregate-inventory-store';
|
|
79
84
|
import type { RoutingId } from '../../contracts/Common/CoreTypes';
|
|
@@ -101,7 +106,6 @@ type StoredAuthoritySnapshot = Omit<
|
|
|
101
106
|
|
|
102
107
|
interface AuthorityRecord {
|
|
103
108
|
readonly snapshot: StoredAuthoritySnapshot;
|
|
104
|
-
readonly reservationId?: string;
|
|
105
109
|
readonly terminal?: 'committed' | 'rejected' | 'failed' | 'aborted';
|
|
106
110
|
readonly aggregate?: AggregateParticipantFenceRecord;
|
|
107
111
|
readonly visibleStoreVersion?: string;
|
|
@@ -1174,7 +1178,6 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1174
1178
|
capacity: cloneCapacity(request.capacity)
|
|
1175
1179
|
};
|
|
1176
1180
|
const record: AuthorityRecord = {
|
|
1177
|
-
reservationId,
|
|
1178
1181
|
snapshot: {
|
|
1179
1182
|
payload: Buffer.from(request.creatingPayload),
|
|
1180
1183
|
objectGeneration: generation,
|
|
@@ -1246,7 +1249,7 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1246
1249
|
const record = decodeAuthorityRecord(current.value.bytes);
|
|
1247
1250
|
if (
|
|
1248
1251
|
record.terminal === 'committed'
|
|
1249
|
-
&& record.
|
|
1252
|
+
&& record.snapshot.pendingCreation === undefined
|
|
1250
1253
|
&& record.snapshot.allocation.state === 'active'
|
|
1251
1254
|
) {
|
|
1252
1255
|
return {
|
|
@@ -1255,7 +1258,7 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1255
1258
|
};
|
|
1256
1259
|
}
|
|
1257
1260
|
if (
|
|
1258
|
-
record.reservationId !== request.reservationId
|
|
1261
|
+
record.snapshot.pendingCreation?.reservationId !== request.reservationId
|
|
1259
1262
|
|| current.value.version.value !== request.expectedStoreVersion
|
|
1260
1263
|
|| record.snapshot.allocation.state !== 'reserved'
|
|
1261
1264
|
|| !sameCreationTarget(record.snapshot, request.target)
|
|
@@ -1277,7 +1280,6 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1277
1280
|
? emptyCapacityRecord()
|
|
1278
1281
|
: decodeJson<CapacityRecord>(capacityRead.value.bytes);
|
|
1279
1282
|
const ready: AuthorityRecord = {
|
|
1280
|
-
reservationId: request.reservationId,
|
|
1281
1283
|
terminal: 'committed',
|
|
1282
1284
|
snapshot: {
|
|
1283
1285
|
...record.snapshot,
|
|
@@ -1328,7 +1330,7 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1328
1330
|
if (current.kind === 'missing') return { kind: 'stale' };
|
|
1329
1331
|
const record = decodeAuthorityRecord(current.value.bytes);
|
|
1330
1332
|
if (
|
|
1331
|
-
record.reservationId !== request.reservationId
|
|
1333
|
+
record.snapshot.pendingCreation?.reservationId !== request.reservationId
|
|
1332
1334
|
|| current.value.version.value !== request.expectedStoreVersion
|
|
1333
1335
|
|| record.snapshot.allocation.state !== 'reserved'
|
|
1334
1336
|
|| !sameCreationTarget(record.snapshot, request.target)
|
|
@@ -1404,15 +1406,18 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1404
1406
|
if (existingTerminal.kind === 'found') {
|
|
1405
1407
|
return {
|
|
1406
1408
|
kind: 'alreadyCompleted',
|
|
1407
|
-
terminal:
|
|
1408
|
-
|
|
1409
|
+
terminal: retainedCreationTerminal(
|
|
1410
|
+
terminal.operation,
|
|
1411
|
+
existingTerminal.value.bytes,
|
|
1412
|
+
existingTerminal.value.expiresAt,
|
|
1413
|
+
existingTerminal.value.storeNow
|
|
1409
1414
|
)
|
|
1410
1415
|
};
|
|
1411
1416
|
}
|
|
1412
1417
|
if (current.kind === 'missing') return { kind: 'stale' };
|
|
1413
1418
|
const record = decodeAuthorityRecord(current.value.bytes);
|
|
1414
1419
|
if (
|
|
1415
|
-
record.reservationId !== request.reservationId
|
|
1420
|
+
record.snapshot.pendingCreation?.reservationId !== request.reservationId
|
|
1416
1421
|
|| current.value.version.value !== request.expectedStoreVersion
|
|
1417
1422
|
|| record.snapshot.allocation.state !== 'reserved'
|
|
1418
1423
|
|| !sameCreationTarget(record.snapshot, request.target)
|
|
@@ -1450,7 +1455,6 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1450
1455
|
kind: 'put' as const,
|
|
1451
1456
|
key: rowKey,
|
|
1452
1457
|
bytes: encodeAuthorityRecord({
|
|
1453
|
-
reservationId: request.reservationId,
|
|
1454
1458
|
terminal: 'committed',
|
|
1455
1459
|
snapshot: {
|
|
1456
1460
|
...record.snapshot,
|
|
@@ -1493,7 +1497,7 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1493
1497
|
{
|
|
1494
1498
|
kind: 'put',
|
|
1495
1499
|
key: terminalRowKey,
|
|
1496
|
-
bytes:
|
|
1500
|
+
bytes: terminalAtStore.terminalEnvelope,
|
|
1497
1501
|
retentionMs
|
|
1498
1502
|
}
|
|
1499
1503
|
]
|
|
@@ -1513,7 +1517,6 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1513
1517
|
}
|
|
1514
1518
|
const readyRecord = decodeAuthorityRecord(
|
|
1515
1519
|
encodeAuthorityRecord({
|
|
1516
|
-
reservationId: request.reservationId,
|
|
1517
1520
|
terminal: 'committed',
|
|
1518
1521
|
snapshot: {
|
|
1519
1522
|
...record.snapshot,
|
|
@@ -1541,9 +1544,7 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1541
1544
|
? { kind: 'missing', storeNow: result.storeNow }
|
|
1542
1545
|
: {
|
|
1543
1546
|
kind: 'found',
|
|
1544
|
-
|
|
1545
|
-
decodeJson<ZLinkCreationTerminalRecord>(result.value.bytes)
|
|
1546
|
-
)
|
|
1547
|
+
terminalEnvelope: Buffer.from(result.value.bytes)
|
|
1547
1548
|
};
|
|
1548
1549
|
}
|
|
1549
1550
|
|
|
@@ -2152,10 +2153,11 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
2152
2153
|
if (result.kind === 'deleted') removed += 1n;
|
|
2153
2154
|
continue;
|
|
2154
2155
|
}
|
|
2155
|
-
|
|
2156
|
+
const reservationId = record.snapshot.pendingCreation?.reservationId;
|
|
2157
|
+
if (reservationId === undefined) continue;
|
|
2156
2158
|
const aborted = await this.abort({
|
|
2157
2159
|
key: identity,
|
|
2158
|
-
reservationId
|
|
2160
|
+
reservationId,
|
|
2159
2161
|
expectedStoreVersion: current.value.version.value,
|
|
2160
2162
|
target: {
|
|
2161
2163
|
meshName: record.snapshot.allocation.descriptor.meshName,
|
|
@@ -3141,13 +3143,13 @@ function sha256Hex(bytes: Uint8Array): string {
|
|
|
3141
3143
|
// literal NUL bytes -- the same scheme the opaque record store hashes with
|
|
3142
3144
|
// SHA-256 to derive the Redis key -- so a runtime in one language can read a
|
|
3143
3145
|
// record another language wrote. Not used for framework-internal-only rows
|
|
3144
|
-
// (aggregate/capacity/
|
|
3145
|
-
//
|
|
3146
|
+
// (aggregate/capacity/spot/actor/route), which stay on the legacy PREFIX
|
|
3147
|
+
// scheme since they aren't part of the public opaque contract.
|
|
3146
3148
|
function ownerKey(ownerId: string) {
|
|
3147
|
-
return storeKey(
|
|
3149
|
+
return storeKey(opaqueRecordPreimage('owner-lease', requireNoNul(ownerId, 'OwnerId')));
|
|
3148
3150
|
}
|
|
3149
3151
|
|
|
3150
|
-
const AUTHORITY_PREIMAGE_PREFIX = 'authority\0
|
|
3152
|
+
const AUTHORITY_PREIMAGE_PREFIX = `${opaqueRecordPreimage('authority')}\0`;
|
|
3151
3153
|
|
|
3152
3154
|
function authorityKey(value: string) {
|
|
3153
3155
|
return storeKey(authorityPreimage(value));
|
|
@@ -3158,7 +3160,11 @@ function authorityPreimage(value: string): string {
|
|
|
3158
3160
|
// §2.3: Entry|User|Instance Spot kinds share one segment ("spot") -- one
|
|
3159
3161
|
// Id has exactly one authority row regardless of spot kind.
|
|
3160
3162
|
const segment = decoded.kind === 'actor' ? 'actor' : 'spot';
|
|
3161
|
-
return
|
|
3163
|
+
return opaqueRecordPreimage(
|
|
3164
|
+
'authority',
|
|
3165
|
+
segment,
|
|
3166
|
+
requireNoNul(decoded.globalId, 'Authority Id')
|
|
3167
|
+
);
|
|
3162
3168
|
}
|
|
3163
3169
|
|
|
3164
3170
|
// Reverses authorityPreimage(): recovers the framework-internal zla1-encoded
|
|
@@ -3235,42 +3241,49 @@ function capacityKey(meshName: string, nodeRid: string) {
|
|
|
3235
3241
|
|
|
3236
3242
|
function creationTerminalKey(operation: ZLinkCreationOperationIdentity) {
|
|
3237
3243
|
validateCreationOperation(operation);
|
|
3238
|
-
return storeKey(
|
|
3239
|
-
String(operation.sourceNodeRid),
|
|
3240
|
-
operation.sourceNodeGeneration.toString(),
|
|
3241
|
-
operation.operationId.high.toString(16).padStart(16, '0'),
|
|
3242
|
-
operation.operationId.low.toString(16).padStart(16, '0')
|
|
3243
|
-
].map(encodeURIComponent).join(':')}`);
|
|
3244
|
+
return storeKey(creationTerminalPreimage(operation));
|
|
3244
3245
|
}
|
|
3245
3246
|
|
|
3246
3247
|
function meshPrefix(meshName: string): string {
|
|
3247
|
-
return
|
|
3248
|
+
return `${opaqueRecordPreimage('mesh-node', requireNoNul(meshName, 'MeshName'))}\0`;
|
|
3248
3249
|
}
|
|
3249
3250
|
|
|
3250
3251
|
function meshKey(meshName: string, nodeRid: RoutingId) {
|
|
3251
|
-
return storeKey(
|
|
3252
|
+
return storeKey(opaqueRecordPreimage(
|
|
3253
|
+
'mesh-node',
|
|
3254
|
+
requireNoNul(meshName, 'MeshName'),
|
|
3255
|
+
routingIdHexSegment(nodeRid)
|
|
3256
|
+
));
|
|
3252
3257
|
}
|
|
3253
3258
|
|
|
3254
3259
|
function clientServerPrefix(channelName: string): string {
|
|
3255
|
-
return
|
|
3260
|
+
return `${opaqueRecordPreimage(
|
|
3261
|
+
'client-server',
|
|
3262
|
+
requireNoNul(channelName, 'ChannelName')
|
|
3263
|
+
)}\0`;
|
|
3256
3264
|
}
|
|
3257
3265
|
|
|
3258
3266
|
function clientServerKey(channelName: string, serverRid: RoutingId) {
|
|
3259
|
-
return storeKey(
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
function routingIdHexSegment(rid: RoutingId): string {
|
|
3265
|
-
return encodeRoutingIdStorageHex(rid).toLowerCase();
|
|
3267
|
+
return storeKey(opaqueRecordPreimage(
|
|
3268
|
+
'client-server',
|
|
3269
|
+
requireNoNul(channelName, 'ChannelName'),
|
|
3270
|
+
routingIdHexSegment(serverRid)
|
|
3271
|
+
));
|
|
3266
3272
|
}
|
|
3267
3273
|
|
|
3268
3274
|
function fanoutPrefix(channelName: string): string {
|
|
3269
|
-
return
|
|
3275
|
+
return `${opaqueRecordPreimage(
|
|
3276
|
+
'fanout-publisher',
|
|
3277
|
+
requireNoNul(channelName, 'ChannelName')
|
|
3278
|
+
)}\0`;
|
|
3270
3279
|
}
|
|
3271
3280
|
|
|
3272
3281
|
function fanoutKey(channelName: string, publisherRid: RoutingId) {
|
|
3273
|
-
return storeKey(
|
|
3282
|
+
return storeKey(opaqueRecordPreimage(
|
|
3283
|
+
'fanout-publisher',
|
|
3284
|
+
requireNoNul(channelName, 'ChannelName'),
|
|
3285
|
+
routingIdHexSegment(publisherRid)
|
|
3286
|
+
));
|
|
3274
3287
|
}
|
|
3275
3288
|
|
|
3276
3289
|
function spotKey(meshName: string, spotId: string) {
|
|
@@ -3567,7 +3580,6 @@ function encodeAuthorityRecord(record: AuthorityRecord): Uint8Array {
|
|
|
3567
3580
|
// These fields gate Node-only creation/aggregate recovery. They are absent
|
|
3568
3581
|
// from normal authority values (including the golden vector), while the
|
|
3569
3582
|
// interoperable envelope above remains byte-canonical.
|
|
3570
|
-
if (record.reservationId !== undefined) envelope.reservationId = record.reservationId;
|
|
3571
3583
|
if (record.terminal !== undefined) envelope.terminal = record.terminal;
|
|
3572
3584
|
if (record.aggregate !== undefined) envelope.aggregate = canonicalize(record.aggregate);
|
|
3573
3585
|
if (record.visibleStoreVersion !== undefined) envelope.visibleStoreVersion = record.visibleStoreVersion;
|
|
@@ -3583,7 +3595,6 @@ function decodeAuthorityRecord(bytes: Uint8Array): AuthorityRecord {
|
|
|
3583
3595
|
const pending = value.pendingCreation as Record<string, unknown> | null;
|
|
3584
3596
|
const objectKind = runtimePlacementObjectKind(String(allocation.objectKind));
|
|
3585
3597
|
return {
|
|
3586
|
-
reservationId: typeof value.reservationId === 'string' ? value.reservationId : undefined,
|
|
3587
3598
|
terminal: value.terminal as AuthorityRecord['terminal'],
|
|
3588
3599
|
aggregate: value.aggregate === undefined
|
|
3589
3600
|
? undefined
|
|
@@ -3682,17 +3693,8 @@ function createTerminalRecord(
|
|
|
3682
3693
|
): ZLinkCreationTerminalRecord {
|
|
3683
3694
|
const publication = request.completion.terminal;
|
|
3684
3695
|
validateCreationOperation(publication.operation);
|
|
3685
|
-
if (
|
|
3686
|
-
|
|
3687
|
-
|| publication.terminalEnvelopeSha256.byteLength !== 32
|
|
3688
|
-
) {
|
|
3689
|
-
throw new RangeError(
|
|
3690
|
-
'Creation terminal envelope must not exceed 1 MiB and requires a SHA-256 digest.'
|
|
3691
|
-
);
|
|
3692
|
-
}
|
|
3693
|
-
const actualSha = createHash('sha256').update(publication.terminalEnvelope).digest();
|
|
3694
|
-
if (!timingSafeEqual(actualSha, Buffer.from(publication.terminalEnvelopeSha256))) {
|
|
3695
|
-
throw new TypeError('Creation terminal envelope SHA-256 does not match its bytes.');
|
|
3696
|
+
if (publication.terminalEnvelope.byteLength > MAX_CREATION_TERMINAL_BYTES) {
|
|
3697
|
+
throw new RangeError('Creation terminal envelope must not exceed 1 MiB.');
|
|
3696
3698
|
}
|
|
3697
3699
|
const deadlineMs = publication.operationDeadline.getTime();
|
|
3698
3700
|
const expiresAtMs = deadlineMs + CREATION_TERMINAL_RETENTION_MS;
|
|
@@ -3709,35 +3711,35 @@ function createTerminalRecord(
|
|
|
3709
3711
|
validatePayloadSize(request.completion.readyPayload, 'Actor ready payload');
|
|
3710
3712
|
}
|
|
3711
3713
|
return {
|
|
3712
|
-
state: request.completion.kind,
|
|
3713
3714
|
operation: {
|
|
3714
3715
|
sourceNodeRid: publication.operation.sourceNodeRid,
|
|
3715
3716
|
sourceNodeGeneration: publication.operation.sourceNodeGeneration,
|
|
3716
3717
|
operationId: { ...publication.operation.operationId }
|
|
3717
3718
|
},
|
|
3718
|
-
reservationId: requireText(request.reservationId, 'creation reservation ID'),
|
|
3719
|
-
objectKind: request.key.kind,
|
|
3720
3719
|
terminalEnvelope: Buffer.from(publication.terminalEnvelope),
|
|
3721
|
-
terminalEnvelopeSha256: Buffer.from(publication.terminalEnvelopeSha256),
|
|
3722
3720
|
expiresAt: new Date(expiresAtMs),
|
|
3723
3721
|
storeNow: new Date(now)
|
|
3724
3722
|
};
|
|
3725
3723
|
}
|
|
3726
3724
|
|
|
3727
|
-
function
|
|
3728
|
-
|
|
3725
|
+
function retainedCreationTerminal(
|
|
3726
|
+
operation: ZLinkCreationOperationIdentity,
|
|
3727
|
+
terminalEnvelope: Uint8Array,
|
|
3728
|
+
expiresAt: Date | undefined,
|
|
3729
|
+
storeNow: Date
|
|
3729
3730
|
): ZLinkCreationTerminalRecord {
|
|
3731
|
+
if (expiresAt === undefined) {
|
|
3732
|
+
throw new Error('Retained creation terminal is missing its expiration.');
|
|
3733
|
+
}
|
|
3730
3734
|
return {
|
|
3731
|
-
...record,
|
|
3732
3735
|
operation: {
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
operationId: { ...
|
|
3736
|
+
sourceNodeRid: operation.sourceNodeRid,
|
|
3737
|
+
sourceNodeGeneration: operation.sourceNodeGeneration,
|
|
3738
|
+
operationId: { ...operation.operationId }
|
|
3736
3739
|
},
|
|
3737
|
-
terminalEnvelope: Buffer.from(
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
storeNow: reviveDate(record.storeNow)
|
|
3740
|
+
terminalEnvelope: Buffer.from(terminalEnvelope),
|
|
3741
|
+
expiresAt: new Date(expiresAt),
|
|
3742
|
+
storeNow: new Date(storeNow)
|
|
3741
3743
|
};
|
|
3742
3744
|
}
|
|
3743
3745
|
|
|
@@ -3751,7 +3753,7 @@ function validateCreationOperation(operation: ZLinkCreationOperationIdentity): v
|
|
|
3751
3753
|
}
|
|
3752
3754
|
if (
|
|
3753
3755
|
operation.sourceNodeGeneration < 1n
|
|
3754
|
-
|| operation.sourceNodeGeneration >
|
|
3756
|
+
|| operation.sourceNodeGeneration > MAX_U64
|
|
3755
3757
|
|| operation.operationId.high < 0n
|
|
3756
3758
|
|| operation.operationId.high > MAX_U64
|
|
3757
3759
|
|| operation.operationId.low < 0n
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RoutingId } from '../../contracts/Common/CoreTypes';
|
|
2
|
+
import type { ZLinkCreationOperationIdentity } from './internal-location-contracts';
|
|
3
|
+
import { encodeRoutingIdStorageHex } from '../routing-id';
|
|
4
|
+
|
|
5
|
+
export function opaqueRecordPreimage(
|
|
6
|
+
record: string,
|
|
7
|
+
...segments: readonly string[]
|
|
8
|
+
): string {
|
|
9
|
+
return [record, ...segments].join('\0');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function creationTerminalPreimage(
|
|
13
|
+
operation: ZLinkCreationOperationIdentity
|
|
14
|
+
): string {
|
|
15
|
+
return opaqueRecordPreimage(
|
|
16
|
+
'creation-terminal',
|
|
17
|
+
routingIdHexSegment(operation.sourceNodeRid),
|
|
18
|
+
operation.sourceNodeGeneration.toString(),
|
|
19
|
+
operation.operationId.high.toString(16).padStart(16, '0')
|
|
20
|
+
+ operation.operationId.low.toString(16).padStart(16, '0')
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function routingIdHexSegment(routingId: RoutingId): string {
|
|
25
|
+
return encodeRoutingIdStorageHex(routingId).toLowerCase();
|
|
26
|
+
}
|
|
@@ -132,6 +132,13 @@ export class ZLinkOwnerCleanupError extends Error {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
export class ZLinkOwnerLeaseClaimError extends Error {
|
|
136
|
+
constructor(readonly kind: 'conflict' | 'generationExhausted') {
|
|
137
|
+
super(`Owner lease claim failed with '${kind}'.`);
|
|
138
|
+
this.name = 'ZLinkOwnerLeaseClaimError';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
135
142
|
export class ZLinkLocationRuntime implements ZLinkLocationRuntimeQuery {
|
|
136
143
|
readonly ownerId: string;
|
|
137
144
|
private readonly options: Required<ZLinkLocationOptionOverrides>;
|
|
@@ -163,9 +170,9 @@ export class ZLinkLocationRuntime implements ZLinkLocationRuntimeQuery {
|
|
|
163
170
|
lastError?: string;
|
|
164
171
|
|
|
165
172
|
get ownerLeaseUsable(): boolean {
|
|
166
|
-
return this.
|
|
167
|
-
&&
|
|
168
|
-
|
|
173
|
+
return this.ownerToken !== undefined
|
|
174
|
+
&& this.ownerLeaseDeadlineMs !== undefined
|
|
175
|
+
&& this.monotonicNowMs() < this.ownerLeaseDeadlineMs;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
constructor(runtimeOptions: ZLinkLocationRuntimeOptions) {
|
|
@@ -200,6 +207,11 @@ export class ZLinkLocationRuntime implements ZLinkLocationRuntimeQuery {
|
|
|
200
207
|
return this.ownerToken;
|
|
201
208
|
}
|
|
202
209
|
|
|
210
|
+
closeOwnerLeaseAdmission(error: unknown): void {
|
|
211
|
+
this.ownerLeaseDeadlineMs = undefined;
|
|
212
|
+
this.recordFailure(errorMessage(error), 'owner_lease_recovery');
|
|
213
|
+
}
|
|
214
|
+
|
|
203
215
|
async reclaimOwnerAuthorities(signal?: AbortSignal): Promise<void> {
|
|
204
216
|
const owner = this.ownerToken;
|
|
205
217
|
if (owner === undefined) {
|
|
@@ -303,48 +315,19 @@ export class ZLinkLocationRuntime implements ZLinkLocationRuntimeQuery {
|
|
|
303
315
|
this.started = true;
|
|
304
316
|
this.ownerCleanupComplete = false;
|
|
305
317
|
this.nodeRidValue = nodeRid;
|
|
306
|
-
const claimStartedAtMs = this.monotonicNowMs();
|
|
307
|
-
let claim: Awaited<ReturnType<ZLinkOwnerLeaseStore['claimOwnerLease']>>;
|
|
308
318
|
try {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
this.options.ownerLeaseRenewTimeoutMs,
|
|
316
|
-
signal
|
|
317
|
-
);
|
|
318
|
-
} catch (error) {
|
|
319
|
-
this.ownerLeaseHealthy = false;
|
|
320
|
-
this.ownerLeaseDeadlineMs = undefined;
|
|
321
|
-
if (signal?.aborted === true) throw error;
|
|
322
|
-
this.recordFailure(errorMessage(error), 'owner_lease_claim');
|
|
323
|
-
// A Location host can still serve transport work while the Store is
|
|
324
|
-
// unavailable. Keep the runtime in degraded mode so the heartbeat can
|
|
325
|
-
// claim a lease after recovery; no descriptor or stateful authority is
|
|
326
|
-
// published until that claim succeeds.
|
|
327
|
-
this.nextLeaseRenewAtMs = this.monotonicNowMs();
|
|
319
|
+
const claimed = await this.renewOwnerLeaseOnce(signal);
|
|
320
|
+
if (claimed) {
|
|
321
|
+
this.scheduleHeartbeat();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.nextLeaseRenewAtMs = this.monotonicNowMs() + this.options.ownerLeaseRenewIntervalMs;
|
|
328
325
|
this.scheduleHeartbeat();
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
const claimCompletedAtMs = this.monotonicNowMs();
|
|
332
|
-
if (claim.kind !== 'claimed') {
|
|
326
|
+
} catch (error) {
|
|
333
327
|
this.started = false;
|
|
334
328
|
this.ownerCleanupComplete = true;
|
|
335
|
-
throw
|
|
329
|
+
throw error;
|
|
336
330
|
}
|
|
337
|
-
this.ownerToken = claim.token;
|
|
338
|
-
this.ownerLeaseHealthy = true;
|
|
339
|
-
this.ownerLeaseRenewedAt = claim.storeNow;
|
|
340
|
-
this.setOwnerLeaseDeadline(
|
|
341
|
-
claimStartedAtMs,
|
|
342
|
-
claimCompletedAtMs,
|
|
343
|
-
claim.leaseExpiresAt,
|
|
344
|
-
claim.storeNow
|
|
345
|
-
);
|
|
346
|
-
this.nextLeaseRenewAtMs = this.monotonicNowMs() + this.options.ownerLeaseRenewIntervalMs;
|
|
347
|
-
this.scheduleHeartbeat();
|
|
348
331
|
}
|
|
349
332
|
|
|
350
333
|
async stop(signal?: AbortSignal): Promise<void> {
|
|
@@ -395,6 +378,7 @@ export class ZLinkLocationRuntime implements ZLinkLocationRuntimeQuery {
|
|
|
395
378
|
return await this.claimFreshOwnerLease(signal);
|
|
396
379
|
}
|
|
397
380
|
|
|
381
|
+
const wasOwnerLeaseUsable = this.ownerLeaseUsable;
|
|
398
382
|
try {
|
|
399
383
|
const renewStartedAtMs = this.monotonicNowMs();
|
|
400
384
|
const result = await withTimeout(
|
|
@@ -431,7 +415,9 @@ export class ZLinkLocationRuntime implements ZLinkLocationRuntimeQuery {
|
|
|
431
415
|
result.storeNow
|
|
432
416
|
);
|
|
433
417
|
this.lastError = undefined;
|
|
434
|
-
|
|
418
|
+
if (!wasOwnerLeaseUsable) {
|
|
419
|
+
for (const handler of this.ownerLeaseRenewedHandlers) handler(result);
|
|
420
|
+
}
|
|
435
421
|
return true;
|
|
436
422
|
} catch (error) {
|
|
437
423
|
if (signal?.aborted === true) throw error;
|
|
@@ -443,26 +429,39 @@ export class ZLinkLocationRuntime implements ZLinkLocationRuntimeQuery {
|
|
|
443
429
|
}
|
|
444
430
|
|
|
445
431
|
private async claimFreshOwnerLease(signal?: AbortSignal): Promise<boolean> {
|
|
432
|
+
const claimStartedAtMs = this.monotonicNowMs();
|
|
433
|
+
const timeoutDeadline = performance.now() + this.options.ownerLeaseRenewTimeoutMs;
|
|
434
|
+
let claim: Awaited<ReturnType<ZLinkOwnerLeaseStore['claimOwnerLease']>>;
|
|
435
|
+
try {
|
|
436
|
+
claim = await this.claimOwnerLeaseWithConfirmation(timeoutDeadline, signal);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (signal?.aborted === true) throw error;
|
|
439
|
+
if (error instanceof ZLinkOwnerLeaseClaimError) throw error;
|
|
440
|
+
this.recordOwnerLeaseRenewFailure();
|
|
441
|
+
this.recordFailure(errorMessage(error), 'owner_lease_claim');
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
const claimCompletedAtMs = this.monotonicNowMs();
|
|
445
|
+
if (claim.kind !== 'claimed') {
|
|
446
|
+
const error = new ZLinkOwnerLeaseClaimError(claim.kind);
|
|
447
|
+
this.recordFailure(error.message, 'owner_lease_claim');
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
446
450
|
try {
|
|
447
|
-
const claimStartedAtMs = this.monotonicNowMs();
|
|
448
|
-
const claim = await withTimeout(
|
|
449
|
-
(claimSignal) => this.stores.ownerLeaseStore.claimOwnerLease(
|
|
450
|
-
this.ownerId,
|
|
451
|
-
this.options.ownerLeaseTtlMs,
|
|
452
|
-
claimSignal
|
|
453
|
-
),
|
|
454
|
-
this.options.ownerLeaseRenewTimeoutMs,
|
|
455
|
-
signal
|
|
456
|
-
);
|
|
457
|
-
const claimCompletedAtMs = this.monotonicNowMs();
|
|
458
|
-
if (claim.kind !== 'claimed') {
|
|
459
|
-
this.recordFailure(`Owner lease claim failed with '${claim.kind}'.`, 'owner_lease_claim');
|
|
460
|
-
return false;
|
|
461
|
-
}
|
|
462
451
|
if (!this.started) {
|
|
463
452
|
// stop() may have completed while the Store claim was in flight. Do
|
|
464
453
|
// not install a token into a stopped runtime or leave an orphan lease.
|
|
465
|
-
|
|
454
|
+
try {
|
|
455
|
+
await withTimeout(
|
|
456
|
+
releaseSignal => this.stores.ownerLeaseStore.releaseOwnerLease(
|
|
457
|
+
claim.token,
|
|
458
|
+
releaseSignal
|
|
459
|
+
),
|
|
460
|
+
remainingTimeoutMs(timeoutDeadline)
|
|
461
|
+
);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
this.recordFailure(errorMessage(error), 'owner_lease_release');
|
|
464
|
+
}
|
|
466
465
|
return false;
|
|
467
466
|
}
|
|
468
467
|
this.lastOwnerToken = this.ownerToken;
|
|
@@ -487,12 +486,83 @@ export class ZLinkLocationRuntime implements ZLinkLocationRuntimeQuery {
|
|
|
487
486
|
return true;
|
|
488
487
|
} catch (error) {
|
|
489
488
|
if (signal?.aborted === true) throw error;
|
|
490
|
-
this.recordOwnerLeaseRenewFailure();
|
|
491
489
|
this.recordFailure(errorMessage(error), 'owner_lease_claim');
|
|
492
490
|
return false;
|
|
493
491
|
}
|
|
494
492
|
}
|
|
495
493
|
|
|
494
|
+
private async claimOwnerLeaseWithConfirmation(
|
|
495
|
+
timeoutDeadline: number,
|
|
496
|
+
signal?: AbortSignal
|
|
497
|
+
): Promise<Awaited<ReturnType<ZLinkOwnerLeaseStore['claimOwnerLease']>>> {
|
|
498
|
+
signal?.throwIfAborted();
|
|
499
|
+
let claim: Awaited<ReturnType<ZLinkOwnerLeaseStore['claimOwnerLease']>>;
|
|
500
|
+
try {
|
|
501
|
+
claim = await withTimeout(
|
|
502
|
+
(claimSignal) => this.stores.ownerLeaseStore.claimOwnerLease(
|
|
503
|
+
this.ownerId,
|
|
504
|
+
this.options.ownerLeaseTtlMs,
|
|
505
|
+
claimSignal
|
|
506
|
+
),
|
|
507
|
+
remainingTimeoutMs(timeoutDeadline),
|
|
508
|
+
signal
|
|
509
|
+
);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (error instanceof OwnerLeaseRenewTimeoutError) throw error;
|
|
512
|
+
const confirmed = await this.confirmOwnerLease(error, timeoutDeadline, signal);
|
|
513
|
+
if (confirmed !== undefined) return confirmed;
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
if (claim.kind !== 'conflict') return claim;
|
|
517
|
+
const conflict = new ZLinkOwnerLeaseClaimError(claim.kind);
|
|
518
|
+
return await this.confirmOwnerLease(conflict, timeoutDeadline, signal) ?? claim;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private async confirmOwnerLease(
|
|
522
|
+
originalError: unknown,
|
|
523
|
+
timeoutDeadline: number,
|
|
524
|
+
signal?: AbortSignal
|
|
525
|
+
): Promise<Extract<Awaited<ReturnType<ZLinkOwnerLeaseStore['claimOwnerLease']>>, { kind: 'claimed' }> | undefined> {
|
|
526
|
+
const confirmationTimeoutMs = remainingTimeoutMs(timeoutDeadline);
|
|
527
|
+
if (confirmationTimeoutMs <= 0) throw originalError;
|
|
528
|
+
let confirmed: Awaited<ReturnType<ZLinkOwnerLeaseStore['readOwnerLease']>>;
|
|
529
|
+
try {
|
|
530
|
+
confirmed = await withTimeout(
|
|
531
|
+
readSignal => this.stores.ownerLeaseStore.readOwnerLease(this.ownerId, readSignal),
|
|
532
|
+
confirmationTimeoutMs
|
|
533
|
+
);
|
|
534
|
+
} catch (confirmationError) {
|
|
535
|
+
this.recordFailure(
|
|
536
|
+
errorMessage(confirmationError),
|
|
537
|
+
'owner_lease_claim_confirmation'
|
|
538
|
+
);
|
|
539
|
+
throw originalError;
|
|
540
|
+
}
|
|
541
|
+
if (confirmed.kind !== 'found' || confirmed.token.ownerId !== this.ownerId) {
|
|
542
|
+
throw originalError;
|
|
543
|
+
}
|
|
544
|
+
if (signal?.aborted === true) {
|
|
545
|
+
try {
|
|
546
|
+
await withTimeout(
|
|
547
|
+
releaseSignal => this.stores.ownerLeaseStore.releaseOwnerLease(
|
|
548
|
+
confirmed.token,
|
|
549
|
+
releaseSignal
|
|
550
|
+
),
|
|
551
|
+
remainingTimeoutMs(timeoutDeadline)
|
|
552
|
+
);
|
|
553
|
+
} catch (releaseError) {
|
|
554
|
+
this.recordFailure(errorMessage(releaseError), 'owner_lease_release');
|
|
555
|
+
}
|
|
556
|
+
throw originalError;
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
kind: 'claimed',
|
|
560
|
+
token: confirmed.token,
|
|
561
|
+
leaseExpiresAt: confirmed.leaseExpiresAt,
|
|
562
|
+
storeNow: confirmed.storeNow
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
496
566
|
private setOwnerLeaseDeadline(
|
|
497
567
|
requestStartedAtMs: number,
|
|
498
568
|
requestCompletedAtMs: number,
|
|
@@ -1106,21 +1176,45 @@ async function withTimeout<T>(
|
|
|
1106
1176
|
? timeoutController.signal
|
|
1107
1177
|
: AbortSignal.any([signal, timeoutController.signal]);
|
|
1108
1178
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1179
|
+
let abort: (() => void) | undefined;
|
|
1109
1180
|
const deadline = new Promise<never>((_resolve, reject) => {
|
|
1110
1181
|
timeout = setTimeout(() => {
|
|
1111
|
-
const error = new
|
|
1182
|
+
const error = new OwnerLeaseRenewTimeoutError(timeoutMs);
|
|
1112
1183
|
timeoutController.abort(error);
|
|
1113
1184
|
reject(error);
|
|
1114
1185
|
}, timeoutMs);
|
|
1115
1186
|
timeout.unref();
|
|
1116
1187
|
});
|
|
1188
|
+
const cancellation = signal === undefined
|
|
1189
|
+
? undefined
|
|
1190
|
+
: new Promise<never>((_resolve, reject) => {
|
|
1191
|
+
abort = () => reject(signal.reason);
|
|
1192
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
1193
|
+
});
|
|
1117
1194
|
try {
|
|
1118
|
-
|
|
1195
|
+
signal?.throwIfAborted();
|
|
1196
|
+
return await Promise.race(
|
|
1197
|
+
cancellation === undefined
|
|
1198
|
+
? [action(operationSignal), deadline]
|
|
1199
|
+
: [action(operationSignal), deadline, cancellation]
|
|
1200
|
+
);
|
|
1119
1201
|
} finally {
|
|
1120
1202
|
if (timeout !== undefined) clearTimeout(timeout);
|
|
1203
|
+
if (abort !== undefined) signal?.removeEventListener('abort', abort);
|
|
1121
1204
|
}
|
|
1122
1205
|
}
|
|
1123
1206
|
|
|
1207
|
+
class OwnerLeaseRenewTimeoutError extends Error {
|
|
1208
|
+
constructor(timeoutMs: number) {
|
|
1209
|
+
super(`Owner lease renewal exceeded ${timeoutMs}ms.`);
|
|
1210
|
+
this.name = 'OwnerLeaseRenewTimeoutError';
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function remainingTimeoutMs(timeoutDeadline: number): number {
|
|
1215
|
+
return Math.max(0, timeoutDeadline - performance.now());
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1124
1218
|
function errorMessage(error: unknown): string {
|
|
1125
1219
|
return error instanceof Error ? error.message : String(error);
|
|
1126
1220
|
}
|