@zlink-systems/framework 0.24.0 → 0.26.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/Channels/IZLinkFanoutClient.d.ts +0 -6
- package/dist/contracts/RouteMesh/RuntimeTopology.d.ts +8 -0
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/runtime/activation-admission.d.ts +29 -0
- package/dist/runtime/activation-admission.js +61 -0
- package/dist/runtime/actors/actor-context.js +6 -6
- package/dist/runtime/actors/actor-handoff.d.ts +9 -3
- package/dist/runtime/actors/actor-handoff.js +28 -17
- package/dist/runtime/actors/actor-local-native-join.js +2 -18
- package/dist/runtime/actors/actor-remote-joiner.d.ts +2 -2
- package/dist/runtime/actors/actor-remote-joiner.js +6 -8
- package/dist/runtime/actors/actor-runtime-contracts.d.ts +4 -2
- package/dist/runtime/actors/actor-source-transfer.d.ts +5 -5
- package/dist/runtime/actors/deferred-join-completion.d.ts +14 -0
- package/dist/runtime/actors/deferred-join-completion.js +20 -0
- package/dist/runtime/actors/index.d.ts +1 -1
- package/dist/runtime/actors/index.js +15 -5
- package/dist/runtime/admission.d.ts +5 -1
- package/dist/runtime/admission.js +4 -0
- package/dist/runtime/backend/contracts/index.d.ts +2 -10
- package/dist/runtime/backend/mesh-actor-session-node-adapter.js +2 -9
- package/dist/runtime/backend/node/node-backend-adapter-factory.d.ts +1 -2
- package/dist/runtime/backend/node/node-backend-adapter-factory.js +4 -18
- package/dist/runtime/backend/node/node-backend-adapter-support.d.ts +6 -3
- package/dist/runtime/backend/node/node-backend-adapter-support.js +12 -28
- package/dist/runtime/backend/node/node-event-loop-poller.d.ts +1 -1
- package/dist/runtime/backend/node/node-event-loop-poller.js +8 -2
- package/dist/runtime/backend/node/node-raw-binding-port.d.ts +1 -1
- package/dist/runtime/backend/node/node-raw-binding-port.js +11 -3
- package/dist/runtime/backend/node/node-raw-mesh-backend.d.ts +12 -9
- package/dist/runtime/backend/node/node-raw-mesh-backend.js +61 -29
- package/dist/runtime/backend/node/node-socket-backend-adapter.js +1 -1
- package/dist/runtime/backend/raw-binding-port.d.ts +12 -0
- package/dist/runtime/channels/channel-clients.d.ts +0 -1
- package/dist/runtime/channels/channel-clients.js +0 -8
- package/dist/runtime/channels/channel-outbound-operations.js +2 -10
- package/dist/runtime/channels/channel-runtime-manager.d.ts +2 -5
- package/dist/runtime/channels/channel-runtime-manager.js +1 -12
- package/dist/runtime/channels/channel-socket-registry.d.ts +17 -4
- package/dist/runtime/channels/channel-socket-registry.js +49 -62
- package/dist/runtime/channels/channel-transports.d.ts +0 -3
- package/dist/runtime/channels/fanout-location-runtime.js +3 -9
- package/dist/runtime/execution/index.d.ts +18 -8
- package/dist/runtime/execution/index.js +55 -29
- package/dist/runtime/execution/serial-execution-queue.d.ts +5 -6
- package/dist/runtime/execution/serial-execution-queue.js +49 -15
- package/dist/runtime/foundation/listener-records.d.ts +11 -0
- package/dist/runtime/foundation/listener-records.js +20 -0
- package/dist/runtime/foundation/raw-service-mesh-runtime.d.ts +10 -21
- package/dist/runtime/foundation/raw-service-mesh-runtime.js +66 -392
- package/dist/runtime/foundation/service-authority-payload-codec.d.ts +1 -0
- package/dist/runtime/foundation/service-authority-payload-codec.js +8 -0
- package/dist/runtime/foundation/service-maintenance-runtime.js +23 -2
- package/dist/runtime/foundation/service-relocation-coordinator.d.ts +9 -0
- package/dist/runtime/foundation/service-relocation-coordinator.js +14 -1
- package/dist/runtime/foundation/service-relocation-object-owner.d.ts +8 -0
- package/dist/runtime/foundation/service-relocation-object-owner.js +16 -4
- package/dist/runtime/foundation/service-session-binding-ingress-port.d.ts +6 -13
- package/dist/runtime/foundation/service-stateful-runtime.d.ts +10 -7
- package/dist/runtime/foundation/service-stateful-runtime.js +100 -60
- package/dist/runtime/foundation/service-stateful-wire-codec.d.ts +9 -4
- package/dist/runtime/foundation/service-stateful-wire-codec.js +153 -90
- package/dist/runtime/foundation/service-topology-registry.d.ts +1 -2
- package/dist/runtime/foundation/service-topology-registry.js +6 -30
- package/dist/runtime/host/actor-transfer-runtime.d.ts +17 -15
- package/dist/runtime/host/actor-transfer-runtime.js +73 -86
- package/dist/runtime/host/index.d.ts +6 -2
- package/dist/runtime/host/index.js +63 -37
- package/dist/runtime/host/remote-bound-session-relay.js +4 -3
- package/dist/runtime/host/route-mesh-runtime.d.ts +2 -0
- package/dist/runtime/host/route-mesh-runtime.js +7 -5
- package/dist/runtime/host/service-relocation-host-runtime.d.ts +50 -8
- package/dist/runtime/host/service-relocation-host-runtime.js +343 -270
- package/dist/runtime/host/spot-runtime-options-factory.d.ts +7 -4
- package/dist/runtime/host/spot-runtime-options-factory.js +4 -8
- package/dist/runtime/host/stateful-authority-route-runtime.js +6 -2
- package/dist/runtime/host/user-spot-creation-coordinator.d.ts +11 -3
- package/dist/runtime/host/user-spot-creation-coordinator.js +85 -87
- package/dist/runtime/locations/in-memory-provider-location-store.js +8 -6
- package/dist/runtime/locations/lifecycle.d.ts +1 -1
- package/dist/runtime/locations/lifecycle.js +2 -2
- package/dist/runtime/locations/resolvers.js +7 -2
- package/dist/runtime/locations/spot-location-claims.d.ts +2 -2
- package/dist/runtime/locations/spot-location-claims.js +3 -27
- package/dist/runtime/protocol/service_wire_codec.generated.d.ts +1 -0
- package/dist/runtime/protocol/service_wire_codec.generated.js +5 -1
- package/dist/runtime/protocol/service_wire_pilot_codec.generated.d.ts +1 -0
- package/dist/runtime/protocol/service_wire_pilot_codec.generated.js +3 -2
- package/dist/runtime/spots/formal-remote-actor-admission-registry.d.ts +3 -3
- package/dist/runtime/spots/formal-remote-actor-admission-registry.js +4 -4
- package/dist/runtime/spots/formal-remote-actor-transfer-registry.d.ts +3 -3
- package/dist/runtime/spots/index.d.ts +25 -19
- package/dist/runtime/spots/index.js +207 -205
- package/dist/runtime/spots/spot-activation-registry.d.ts +4 -2
- package/dist/runtime/spots/spot-activation-registry.js +34 -31
- package/dist/runtime/spots/spot-activation-state.d.ts +2 -11
- package/dist/runtime/spots/spot-activation-state.js +4 -17
- package/dist/runtime/spots/spot-activation.d.ts +8 -6
- package/dist/runtime/spots/spot-activation.js +54 -49
- package/dist/runtime/spots/spot-actor-admission-coordinator.d.ts +1 -1
- package/dist/runtime/spots/spot-actor-admission-coordinator.js +1 -1
- package/dist/runtime/spots/spot-actor-join-dispatch.d.ts +2 -2
- package/dist/runtime/spots/spot-actor-join-dispatch.js +3 -5
- package/dist/runtime/spots/spot-actor-lifecycle-drain.js +2 -2
- package/dist/runtime/spots/spot-actor-membership.d.ts +4 -0
- package/dist/runtime/spots/spot-actor-membership.js +26 -12
- package/dist/runtime/spots/spot-entry-activation.d.ts +1 -2
- package/dist/runtime/spots/spot-entry-activation.js +0 -1
- package/dist/runtime/spots/spot-manager-public.js +1 -1
- package/dist/runtime/spots/spot-native-actor-join-admission.d.ts +1 -1
- package/dist/runtime/spots/spot-native-actor-join-admission.js +2 -2
- package/dist/runtime/spots/spot-node-runtime-manager.d.ts +5 -5
- package/dist/runtime/spots/spot-node-runtime-manager.js +7 -11
- package/dist/runtime/spots/spot-outbound.js +1 -29
- package/dist/runtime/spots/spot-routed-actor-admission.d.ts +1 -0
- package/dist/runtime/spots/spot-routed-actor-admission.js +10 -0
- package/dist/runtime/spots/spot-routed-frame-dispatch.d.ts +2 -6
- package/dist/runtime/spots/spot-routed-frame-dispatch.js +2 -70
- package/dist/runtime/spots/spot-runtime-ports.d.ts +3 -5
- package/dist/runtime/spots/spot-serial-executor.d.ts +5 -0
- package/dist/runtime/spots/spot-serial-executor.js +8 -1
- package/dist/runtime/spots/spot-serial-turn-executor.d.ts +3 -0
- package/dist/runtime/spots/spot-serial-turn-executor.js +8 -4
- package/dist/runtime/streams/actor-session-binding-registry.d.ts +13 -15
- package/dist/runtime/streams/actor-session-binding-registry.js +19 -171
- package/dist/runtime/streams/actor-session-binding-runtime-owner.d.ts +3 -4
- package/dist/runtime/streams/bound-session-service.js +1 -18
- package/dist/runtime/streams/index.d.ts +2 -0
- package/dist/runtime/streams/index.js +19 -19
- package/dist/runtime/streams/session-serial-executor.js +2 -2
- package/package.json +3 -3
- package/src/contracts/Channels/IZLinkFanoutClient.ts +0 -7
- package/src/contracts/RouteMesh/RuntimeTopology.ts +10 -0
- package/src/internal.ts +1 -0
- package/src/runtime/activation-admission.ts +78 -0
- package/src/runtime/actors/actor-context.ts +6 -6
- package/src/runtime/actors/actor-handoff.ts +31 -23
- package/src/runtime/actors/actor-local-native-join.ts +3 -42
- package/src/runtime/actors/actor-remote-joiner.ts +6 -25
- package/src/runtime/actors/actor-runtime-contracts.ts +4 -10
- package/src/runtime/actors/actor-source-transfer.ts +5 -18
- package/src/runtime/actors/deferred-join-completion.ts +40 -0
- package/src/runtime/actors/index.ts +22 -9
- package/src/runtime/admission.ts +5 -1
- package/src/runtime/backend/contracts/index.ts +2 -13
- package/src/runtime/backend/mesh-actor-session-node-adapter.ts +2 -10
- package/src/runtime/backend/node/node-backend-adapter-factory.ts +5 -25
- package/src/runtime/backend/node/node-backend-adapter-support.ts +9 -29
- package/src/runtime/backend/node/node-event-loop-poller.ts +14 -2
- package/src/runtime/backend/node/node-raw-binding-port.ts +17 -3
- package/src/runtime/backend/node/node-raw-mesh-backend.ts +65 -32
- package/src/runtime/backend/node/node-socket-backend-adapter.ts +2 -2
- package/src/runtime/backend/raw-binding-port.ts +13 -0
- package/src/runtime/channels/channel-clients.ts +0 -11
- package/src/runtime/channels/channel-outbound-operations.ts +3 -11
- package/src/runtime/channels/channel-runtime-manager.ts +4 -15
- package/src/runtime/channels/channel-socket-registry.ts +69 -71
- package/src/runtime/channels/channel-transports.ts +0 -3
- package/src/runtime/channels/fanout-location-runtime.ts +4 -16
- package/src/runtime/execution/index.ts +68 -30
- package/src/runtime/execution/serial-execution-queue.ts +52 -20
- package/src/runtime/foundation/listener-records.ts +21 -0
- package/src/runtime/foundation/raw-service-mesh-runtime.ts +66 -466
- package/src/runtime/foundation/service-authority-payload-codec.ts +10 -0
- package/src/runtime/foundation/service-maintenance-runtime.ts +30 -9
- package/src/runtime/foundation/service-relocation-coordinator.ts +13 -0
- package/src/runtime/foundation/service-relocation-object-owner.ts +18 -5
- package/src/runtime/foundation/service-session-binding-ingress-port.ts +6 -14
- package/src/runtime/foundation/service-stateful-runtime.ts +151 -89
- package/src/runtime/foundation/service-stateful-wire-codec.ts +203 -121
- package/src/runtime/foundation/service-topology-registry.ts +8 -48
- package/src/runtime/host/actor-transfer-runtime.ts +94 -166
- package/src/runtime/host/index.ts +119 -54
- package/src/runtime/host/remote-bound-session-relay.ts +4 -3
- package/src/runtime/host/route-mesh-runtime.ts +9 -5
- package/src/runtime/host/service-relocation-host-runtime.ts +495 -376
- package/src/runtime/host/spot-runtime-options-factory.ts +24 -13
- package/src/runtime/host/stateful-authority-route-runtime.ts +8 -4
- package/src/runtime/host/user-spot-creation-coordinator.ts +147 -142
- package/src/runtime/locations/in-memory-provider-location-store.ts +11 -7
- package/src/runtime/locations/lifecycle.ts +3 -2
- package/src/runtime/locations/resolvers.ts +19 -13
- package/src/runtime/locations/spot-location-claims.ts +5 -34
- package/src/runtime/protocol/service_wire_codec.generated.ts +9 -2
- package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +4 -4
- package/src/runtime/spots/formal-remote-actor-admission-registry.ts +6 -6
- package/src/runtime/spots/formal-remote-actor-transfer-registry.ts +3 -3
- package/src/runtime/spots/index.ts +341 -295
- package/src/runtime/spots/spot-activation-registry.ts +41 -31
- package/src/runtime/spots/spot-activation-state.ts +4 -16
- package/src/runtime/spots/spot-activation.ts +102 -80
- package/src/runtime/spots/spot-actor-admission-coordinator.ts +2 -2
- package/src/runtime/spots/spot-actor-join-dispatch.ts +6 -7
- package/src/runtime/spots/spot-actor-lifecycle-drain.ts +20 -18
- package/src/runtime/spots/spot-actor-membership.ts +81 -17
- package/src/runtime/spots/spot-entry-activation.ts +0 -3
- package/src/runtime/spots/spot-manager-public.ts +8 -4
- package/src/runtime/spots/spot-native-actor-join-admission.ts +2 -2
- package/src/runtime/spots/spot-node-runtime-manager.ts +12 -18
- package/src/runtime/spots/spot-outbound.ts +4 -47
- package/src/runtime/spots/spot-routed-actor-admission.ts +20 -0
- package/src/runtime/spots/spot-routed-frame-dispatch.ts +6 -76
- package/src/runtime/spots/spot-runtime-ports.ts +6 -17
- package/src/runtime/spots/spot-serial-executor.ts +9 -1
- package/src/runtime/spots/spot-serial-turn-executor.ts +10 -4
- package/src/runtime/streams/actor-session-binding-registry.ts +20 -308
- package/src/runtime/streams/actor-session-binding-runtime-owner.ts +2 -12
- package/src/runtime/streams/bound-session-service.ts +2 -36
- package/src/runtime/streams/index.ts +29 -27
- package/src/runtime/streams/session-serial-executor.ts +2 -2
- package/dist/runtime/actors/deferred-join-accepted-journal.d.ts +0 -47
- package/dist/runtime/actors/deferred-join-accepted-journal.js +0 -992
- package/src/runtime/actors/deferred-join-accepted-journal.ts +0 -1346
|
@@ -1,1346 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import type {
|
|
3
|
-
ActorRef,
|
|
4
|
-
ZLinkActor,
|
|
5
|
-
ZLinkActorJoinCompletion,
|
|
6
|
-
ZLinkActorJoinOperationId,
|
|
7
|
-
ZLinkBlobReference,
|
|
8
|
-
ZLinkMessageSerializer,
|
|
9
|
-
ZLinkRelocationStore
|
|
10
|
-
} from '../../contracts';
|
|
11
|
-
import type { ZLinkAuthoritySnapshot } from '../locations/internal-location-contracts';
|
|
12
|
-
import type { ZLinkAuthorityStore } from '../locations/internal-store-contracts';
|
|
13
|
-
import {
|
|
14
|
-
ZLinkEncodedPayload,
|
|
15
|
-
ZLinkFrameworkErrorKind,
|
|
16
|
-
ZLinkFrameworkException,
|
|
17
|
-
ZLinkMessage
|
|
18
|
-
} from '../../contracts';
|
|
19
|
-
import { encodeAuthorityKey } from '../locations/authority-key-codec';
|
|
20
|
-
import {
|
|
21
|
-
crc32c,
|
|
22
|
-
ServiceRelocationAuthorityPayloadCodec,
|
|
23
|
-
replaceServiceRelocationAuthorityApplicationPayload,
|
|
24
|
-
serviceRelocationAuthorityApplicationPayload,
|
|
25
|
-
serviceRelocationAuthoritySlotIdentity,
|
|
26
|
-
type ServiceRelocationPublication
|
|
27
|
-
} from '../foundation/service-relocation-runtime';
|
|
28
|
-
import { putNewRelocationBlob } from '../locations/relocation-blob';
|
|
29
|
-
import { decodeRoutingId, encodeRoutingIdStorageHex, routingIdWireHex } from '../routing-id';
|
|
30
|
-
import {
|
|
31
|
-
decodeRelocatingActorAuthorityIdentity,
|
|
32
|
-
encodeActorAuthorityIdentity
|
|
33
|
-
} from './actor-authority-publication';
|
|
34
|
-
import { replaceActorRelocationAuthorityApplicationPayload } from './actor-authority-payload-codec';
|
|
35
|
-
import { ZLinkBufferMessage as RuntimeMessage } from '../backend/runtime-message';
|
|
36
|
-
import { wrapFrameworkPayloadMessage } from '../messaging/payload-codec';
|
|
37
|
-
import {
|
|
38
|
-
decodeRelocationDataChunkV1,
|
|
39
|
-
decodeRelocationManifestV1,
|
|
40
|
-
encodeRelocationDataChunkV1,
|
|
41
|
-
encodeRelocationManifestV1
|
|
42
|
-
} from '../protocol/service_wire_pilot_codec.generated';
|
|
43
|
-
|
|
44
|
-
const RETENTION_MS = 24 * 60 * 60 * 1_000;
|
|
45
|
-
const ROOT_VERSION = 2;
|
|
46
|
-
const ROOT_MAGIC = Buffer.from('ZLJR');
|
|
47
|
-
const JOURNAL_INVENTORY_DOMAIN = Buffer.from('zlink-node-deferred-join-authority-v1\0', 'utf8');
|
|
48
|
-
|
|
49
|
-
export type ZLinkDeferredJoinDeliveryCursor = 'prepared' | 'committed' | 'delivered';
|
|
50
|
-
|
|
51
|
-
export interface ZLinkDeferredJoinAcceptedRoot {
|
|
52
|
-
readonly authority: ZLinkAuthoritySnapshot;
|
|
53
|
-
readonly reference: ZLinkBlobReference;
|
|
54
|
-
readonly checksumCrc32c: number;
|
|
55
|
-
readonly operationId: ZLinkActorJoinOperationId;
|
|
56
|
-
readonly actor: ActorRef;
|
|
57
|
-
readonly rawReply: Buffer;
|
|
58
|
-
readonly replyContentType?: string;
|
|
59
|
-
readonly cursor: ZLinkDeferredJoinDeliveryCursor;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface ZLinkDeferredJoinRootIdentity {
|
|
63
|
-
readonly authorityKey: string;
|
|
64
|
-
readonly objectKind: 'actor' | 'user_spot' | 'instance_spot';
|
|
65
|
-
readonly objectGeneration: bigint;
|
|
66
|
-
readonly aggregateId: string;
|
|
67
|
-
readonly aggregateGeneration: bigint;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Identifies whether an authority slot points at a deferred Join journal root. */
|
|
71
|
-
export async function isDeferredJoinAcceptedRootPublication(
|
|
72
|
-
relocation: ZLinkRelocationStore,
|
|
73
|
-
reference: string,
|
|
74
|
-
checksumCrc32c: number,
|
|
75
|
-
expected: ZLinkDeferredJoinRootIdentity,
|
|
76
|
-
signal?: AbortSignal
|
|
77
|
-
): Promise<boolean> {
|
|
78
|
-
if (expected.objectKind !== 'actor' || expected.aggregateGeneration === 0n) return false;
|
|
79
|
-
const read = await relocation.read({ value: reference } as ZLinkBlobReference, signal);
|
|
80
|
-
if (read.kind !== 'found' || crc32c(read.bytes) !== checksumCrc32c) return false;
|
|
81
|
-
if (hasJournalRootMagic(read.bytes)) {
|
|
82
|
-
try {
|
|
83
|
-
const root = decodeRoot(read.bytes);
|
|
84
|
-
return (
|
|
85
|
-
expected.aggregateId === operationAggregateId(root.operationId) &&
|
|
86
|
-
expected.aggregateGeneration === root.actor.objectGeneration &&
|
|
87
|
-
expected.authorityKey === encodeAuthorityKey('actor', root.actor.actorId).value &&
|
|
88
|
-
expected.objectGeneration === root.actor.objectGeneration
|
|
89
|
-
);
|
|
90
|
-
} catch {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const root = await readCanonicalDeferredJoinRoot(relocation, read.bytes, signal);
|
|
95
|
-
return (
|
|
96
|
-
root !== undefined &&
|
|
97
|
-
root.identity.aggregateId === expected.aggregateId &&
|
|
98
|
-
root.identity.aggregateGeneration === expected.aggregateGeneration &&
|
|
99
|
-
root.identity.authorityKey === expected.authorityKey &&
|
|
100
|
-
root.identity.objectKind === expected.objectKind &&
|
|
101
|
-
root.identity.objectGeneration === expected.objectGeneration
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
interface DeferredJoinAuthorityPublication {
|
|
106
|
-
readonly applicationPayload: Buffer;
|
|
107
|
-
readonly reference: ZLinkBlobReference;
|
|
108
|
-
readonly checksumCrc32c: number;
|
|
109
|
-
readonly aggregateId: string;
|
|
110
|
-
readonly aggregateGeneration: bigint;
|
|
111
|
-
readonly targetOwnerId: string;
|
|
112
|
-
readonly targetOwnerLeaseGeneration: bigint;
|
|
113
|
-
readonly canonical?: boolean;
|
|
114
|
-
readonly canonicalPublication?: ServiceRelocationPublication;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Keeps a cross-node Accepted Join completion under the Actor authority.
|
|
119
|
-
* Every cursor transition writes and verifies a new immutable root before the
|
|
120
|
-
* authority CAS. A failed callback leaves the committed root available for the
|
|
121
|
-
* next target Actor mailbox retry.
|
|
122
|
-
*/
|
|
123
|
-
export class ZLinkDeferredJoinAcceptedJournal {
|
|
124
|
-
constructor(
|
|
125
|
-
private readonly authority: ZLinkAuthorityStore,
|
|
126
|
-
private readonly relocation: ZLinkRelocationStore,
|
|
127
|
-
private readonly messageSerializers?: ReadonlyMap<string, ZLinkMessageSerializer>
|
|
128
|
-
) {}
|
|
129
|
-
|
|
130
|
-
async prepare(
|
|
131
|
-
actorId: string,
|
|
132
|
-
operationId: ZLinkActorJoinOperationId,
|
|
133
|
-
actor: ActorRef,
|
|
134
|
-
rawReply: Uint8Array,
|
|
135
|
-
replyContentType?: string,
|
|
136
|
-
signal?: AbortSignal,
|
|
137
|
-
canonicalInventoryDigest?: string
|
|
138
|
-
): Promise<ZLinkDeferredJoinAcceptedRoot> {
|
|
139
|
-
validateIdentity(actorId, operationId, actor);
|
|
140
|
-
if (replyContentType !== undefined && replyContentType.length === 0) {
|
|
141
|
-
throw new TypeError('Deferred Join reply content type must be non-empty.');
|
|
142
|
-
}
|
|
143
|
-
signal?.throwIfAborted();
|
|
144
|
-
const key = encodeAuthorityKey('actor', actorId);
|
|
145
|
-
const read = await this.authority.readAuthority(key, signal);
|
|
146
|
-
if (read.kind !== 'snapshot') {
|
|
147
|
-
throw new Error(`Actor '${actorId}' has no authority for durable Join completion.`);
|
|
148
|
-
}
|
|
149
|
-
requireAuthorityActor(read.payload, actor);
|
|
150
|
-
const existing = await this.readPublished(read, signal);
|
|
151
|
-
if (existing !== undefined) {
|
|
152
|
-
if (isSameOperation(existing, operationId, actor)) {
|
|
153
|
-
return existing;
|
|
154
|
-
}
|
|
155
|
-
if (existing.cursor !== 'delivered') {
|
|
156
|
-
requireSameOperation(existing, operationId, actor);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const rootValue = {
|
|
161
|
-
operationId,
|
|
162
|
-
actor,
|
|
163
|
-
rawReply: Buffer.from(rawReply),
|
|
164
|
-
...(replyContentType === undefined ? {} : { replyContentType }),
|
|
165
|
-
cursor: 'prepared'
|
|
166
|
-
} as const;
|
|
167
|
-
const initialPublication = decodeAuthorityPublication(read.payload);
|
|
168
|
-
const initialCanonicalIdentity = serviceRelocationAuthoritySlotIdentity(read.payload);
|
|
169
|
-
const canonicalPublication =
|
|
170
|
-
initialPublication?.canonical === true ? initialPublication : undefined;
|
|
171
|
-
const root =
|
|
172
|
-
canonicalPublication !== undefined &&
|
|
173
|
-
(canonicalInventoryDigest !== undefined ||
|
|
174
|
-
!canonicalPublication.reference.value.startsWith('zlink-direct:'))
|
|
175
|
-
? await this.storeCanonicalRoot(
|
|
176
|
-
rootValue,
|
|
177
|
-
read,
|
|
178
|
-
canonicalPublication,
|
|
179
|
-
signal,
|
|
180
|
-
canonicalInventoryDigest
|
|
181
|
-
)
|
|
182
|
-
: await this.storeRoot(rootValue, signal);
|
|
183
|
-
if (initialCanonicalIdentity !== undefined && canonicalInventoryDigest !== undefined) {
|
|
184
|
-
return {
|
|
185
|
-
...root,
|
|
186
|
-
authority: read
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
let expected: ZLinkAuthoritySnapshot = read;
|
|
190
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
191
|
-
const currentPublication = decodeAuthorityPublication(expected.payload);
|
|
192
|
-
const canonicalIdentity = serviceRelocationAuthoritySlotIdentity(expected.payload);
|
|
193
|
-
const publication: DeferredJoinAuthorityPublication = {
|
|
194
|
-
applicationPayload: Buffer.from(
|
|
195
|
-
currentPublication?.applicationPayload ??
|
|
196
|
-
serviceRelocationAuthorityApplicationPayload(expected.payload)
|
|
197
|
-
),
|
|
198
|
-
reference: root.reference,
|
|
199
|
-
checksumCrc32c: root.checksumCrc32c,
|
|
200
|
-
aggregateId:
|
|
201
|
-
canonicalIdentity !== undefined
|
|
202
|
-
? canonicalIdentity.aggregateId
|
|
203
|
-
: operationAggregateId(operationId),
|
|
204
|
-
aggregateGeneration:
|
|
205
|
-
canonicalIdentity === undefined
|
|
206
|
-
? actor.objectGeneration
|
|
207
|
-
: requireCanonicalAggregateGeneration(canonicalIdentity.aggregateGeneration),
|
|
208
|
-
targetOwnerId: expected.ownerId,
|
|
209
|
-
targetOwnerLeaseGeneration: expected.ownerLeaseGeneration,
|
|
210
|
-
...(currentPublication?.canonicalPublication === undefined
|
|
211
|
-
? {}
|
|
212
|
-
: { canonicalPublication: currentPublication.canonicalPublication })
|
|
213
|
-
};
|
|
214
|
-
const result = await this.authority.compareExchangeAuthority(
|
|
215
|
-
key,
|
|
216
|
-
expected.storeVersion,
|
|
217
|
-
{
|
|
218
|
-
kind: 'put',
|
|
219
|
-
generationTransition: 'preserve',
|
|
220
|
-
payload:
|
|
221
|
-
currentPublication === undefined && canonicalIdentity !== undefined
|
|
222
|
-
? new ServiceRelocationAuthorityPayloadCodec().publish(
|
|
223
|
-
expected.payload,
|
|
224
|
-
toServiceRelocationPublication(publication)
|
|
225
|
-
)
|
|
226
|
-
: replaceDeferredJoinAuthorityPublication(
|
|
227
|
-
expected.payload,
|
|
228
|
-
encodeAuthorityPublication(publication)
|
|
229
|
-
)
|
|
230
|
-
},
|
|
231
|
-
signal
|
|
232
|
-
);
|
|
233
|
-
if (result.kind === 'stored') {
|
|
234
|
-
if (existing !== undefined) {
|
|
235
|
-
await this.deleteBestEffort(existing.reference);
|
|
236
|
-
}
|
|
237
|
-
return {
|
|
238
|
-
...root,
|
|
239
|
-
authority: storedSnapshot(result)
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
if (result.kind !== 'conflict' || result.current.kind !== 'snapshot') break;
|
|
243
|
-
const current = await this.readPublished(result.current, signal);
|
|
244
|
-
if (current !== undefined) {
|
|
245
|
-
await this.deleteBestEffort(root.reference);
|
|
246
|
-
requireSameOperation(current, operationId, actor);
|
|
247
|
-
return current;
|
|
248
|
-
}
|
|
249
|
-
requireAuthorityActor(result.current.payload, actor);
|
|
250
|
-
expected = result.current;
|
|
251
|
-
}
|
|
252
|
-
await this.deleteBestEffort(root.reference);
|
|
253
|
-
throw new Error(`Actor '${actorId}' authority rejected Join completion publication.`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async recover(
|
|
257
|
-
actorId: string,
|
|
258
|
-
signal?: AbortSignal
|
|
259
|
-
): Promise<ZLinkDeferredJoinAcceptedRoot | undefined> {
|
|
260
|
-
const read = await this.authority.readAuthority(encodeAuthorityKey('actor', actorId), signal);
|
|
261
|
-
return read.kind === 'snapshot' ? await this.readPublished(read, signal) : undefined;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
markCommitted(
|
|
265
|
-
root: ZLinkDeferredJoinAcceptedRoot,
|
|
266
|
-
actor?: ActorRef,
|
|
267
|
-
signal?: AbortSignal
|
|
268
|
-
): Promise<ZLinkDeferredJoinAcceptedRoot> {
|
|
269
|
-
if (actor !== undefined) requireSameActor(root, actor);
|
|
270
|
-
return this.moveCursor(root, 'committed', actor, signal);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
markDelivered(
|
|
274
|
-
root: ZLinkDeferredJoinAcceptedRoot,
|
|
275
|
-
signal?: AbortSignal
|
|
276
|
-
): Promise<ZLinkDeferredJoinAcceptedRoot> {
|
|
277
|
-
return this.moveCursor(root, 'delivered', undefined, signal);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
async discardPrepared(root: ZLinkDeferredJoinAcceptedRoot, signal?: AbortSignal): Promise<void> {
|
|
281
|
-
const key = encodeAuthorityKey('actor', root.actor.actorId);
|
|
282
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
283
|
-
const read = await this.authority.readAuthority(key, signal);
|
|
284
|
-
if (read.kind !== 'snapshot') return;
|
|
285
|
-
const current = await this.readPublished(read, signal);
|
|
286
|
-
if (current === undefined) return;
|
|
287
|
-
requireSameOperation(current, root.operationId, root.actor);
|
|
288
|
-
if (current.cursor !== 'prepared') return;
|
|
289
|
-
const publication = decodeAuthorityPublication(read.payload);
|
|
290
|
-
if (publication === undefined) return;
|
|
291
|
-
const result = await this.authority.compareExchangeAuthority(
|
|
292
|
-
key,
|
|
293
|
-
read.storeVersion,
|
|
294
|
-
{
|
|
295
|
-
kind: 'put',
|
|
296
|
-
generationTransition: 'preserve',
|
|
297
|
-
payload: replaceDeferredJoinAuthorityPublication(
|
|
298
|
-
read.payload,
|
|
299
|
-
publication.applicationPayload
|
|
300
|
-
)
|
|
301
|
-
},
|
|
302
|
-
signal
|
|
303
|
-
);
|
|
304
|
-
if (result.kind === 'stored') {
|
|
305
|
-
await this.deleteBestEffort(current.reference);
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
throw new Error(
|
|
310
|
-
`Actor '${root.actor.actorId}' deferred Join preparation could not be discarded.`
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async deliver(
|
|
315
|
-
root: ZLinkDeferredJoinAcceptedRoot,
|
|
316
|
-
actor: ZLinkActor,
|
|
317
|
-
actorRef: ActorRef,
|
|
318
|
-
submitMailbox: <T>(operation: () => Promise<T>) => Promise<T>,
|
|
319
|
-
signal?: AbortSignal
|
|
320
|
-
): Promise<ZLinkDeferredJoinAcceptedRoot> {
|
|
321
|
-
requireSameActor(root, actorRef);
|
|
322
|
-
return await submitMailbox(async () => {
|
|
323
|
-
const current = await this.recover(actorRef.actorId, signal);
|
|
324
|
-
if (current === undefined) {
|
|
325
|
-
if (root.cursor === 'delivered') return root;
|
|
326
|
-
throw new Error(`Actor '${actorRef.actorId}' lost its deferred Join completion root.`);
|
|
327
|
-
}
|
|
328
|
-
requireSameOperation(current, root.operationId, actorRef);
|
|
329
|
-
if (current.cursor === 'delivered') {
|
|
330
|
-
await this.releaseDelivered(current, signal);
|
|
331
|
-
return current;
|
|
332
|
-
}
|
|
333
|
-
if (current.cursor !== 'committed') {
|
|
334
|
-
throw new Error(`Actor '${actorRef.actorId}' Join completion is not committed.`);
|
|
335
|
-
}
|
|
336
|
-
const completion: ZLinkActorJoinCompletion = {
|
|
337
|
-
status: 'accepted',
|
|
338
|
-
operationId: current.operationId,
|
|
339
|
-
actor: current.actor,
|
|
340
|
-
reply:
|
|
341
|
-
current.rawReply.byteLength === 0
|
|
342
|
-
? undefined
|
|
343
|
-
: current.replyContentType === undefined
|
|
344
|
-
? ZLinkMessage.fromEncoded(ZLinkEncodedPayload.from(current.rawReply))
|
|
345
|
-
: wrapFrameworkPayloadMessage(
|
|
346
|
-
RuntimeMessage.from(current.rawReply),
|
|
347
|
-
this.messageSerializers,
|
|
348
|
-
current.replyContentType,
|
|
349
|
-
undefined,
|
|
350
|
-
'reply'
|
|
351
|
-
)
|
|
352
|
-
};
|
|
353
|
-
await actor.onJoinCompleted?.(completion);
|
|
354
|
-
let delivered = current;
|
|
355
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
356
|
-
delivered = await this.markDelivered(delivered, signal);
|
|
357
|
-
if (delivered.cursor === 'delivered') {
|
|
358
|
-
await this.releaseDelivered(delivered, signal);
|
|
359
|
-
return delivered;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
throw new Error(
|
|
363
|
-
`Actor '${actorRef.actorId}' Join completion callback ran but its Delivered cursor could not be stored.`
|
|
364
|
-
);
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
private async releaseDelivered(
|
|
369
|
-
root: ZLinkDeferredJoinAcceptedRoot,
|
|
370
|
-
signal?: AbortSignal
|
|
371
|
-
): Promise<void> {
|
|
372
|
-
const key = encodeAuthorityKey('actor', root.actor.actorId);
|
|
373
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
374
|
-
const read = await this.authority.readAuthority(key, signal);
|
|
375
|
-
if (read.kind !== 'snapshot') return;
|
|
376
|
-
const current = await this.readPublished(read, signal);
|
|
377
|
-
if (current === undefined) return;
|
|
378
|
-
requireSameOperation(current, root.operationId, root.actor);
|
|
379
|
-
if (current.cursor !== 'delivered') {
|
|
380
|
-
throw new Error(`Actor '${root.actor.actorId}' Join completion root is not Delivered.`);
|
|
381
|
-
}
|
|
382
|
-
const publication = decodeAuthorityPublication(read.payload);
|
|
383
|
-
if (publication === undefined) return;
|
|
384
|
-
const result = await this.authority.compareExchangeAuthority(
|
|
385
|
-
key,
|
|
386
|
-
read.storeVersion,
|
|
387
|
-
{
|
|
388
|
-
kind: 'put',
|
|
389
|
-
generationTransition: 'preserve',
|
|
390
|
-
payload: replaceDeferredJoinAuthorityPublication(
|
|
391
|
-
read.payload,
|
|
392
|
-
publication.applicationPayload
|
|
393
|
-
)
|
|
394
|
-
},
|
|
395
|
-
signal
|
|
396
|
-
);
|
|
397
|
-
if (result.kind === 'stored') {
|
|
398
|
-
await this.deleteBestEffort(current.reference);
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
throw new Error(
|
|
403
|
-
`Actor '${root.actor.actorId}' Delivered Join completion could not be released.`
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
private async moveCursor(
|
|
408
|
-
root: ZLinkDeferredJoinAcceptedRoot,
|
|
409
|
-
next: ZLinkDeferredJoinDeliveryCursor,
|
|
410
|
-
actor: ActorRef | undefined,
|
|
411
|
-
signal?: AbortSignal
|
|
412
|
-
): Promise<ZLinkDeferredJoinAcceptedRoot> {
|
|
413
|
-
const currentIndex = cursorIndex(root.cursor);
|
|
414
|
-
const nextIndex = cursorIndex(next);
|
|
415
|
-
if (nextIndex < currentIndex || nextIndex > currentIndex + 1) {
|
|
416
|
-
throw new Error('Deferred Join completion cursor transition is invalid.');
|
|
417
|
-
}
|
|
418
|
-
if (next === root.cursor) return root;
|
|
419
|
-
|
|
420
|
-
const key = encodeAuthorityKey('actor', root.actor.actorId);
|
|
421
|
-
const read = await this.authority.readAuthority(key, signal);
|
|
422
|
-
if (read.kind !== 'snapshot') {
|
|
423
|
-
throw new Error(
|
|
424
|
-
`Actor '${root.actor.actorId}' authority disappeared during Join completion.`
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
let current = await this.readPublished(read, signal);
|
|
428
|
-
if (current === undefined) {
|
|
429
|
-
const retainedRoot = await this.relocation.read(root.reference, signal);
|
|
430
|
-
if (retainedRoot.kind === 'found' && crc32c(retainedRoot.bytes) === root.checksumCrc32c) {
|
|
431
|
-
const retained = hasJournalRootMagic(retainedRoot.bytes)
|
|
432
|
-
? decodeRoot(retainedRoot.bytes)
|
|
433
|
-
: (await readCanonicalDeferredJoinRoot(this.relocation, retainedRoot.bytes, signal))
|
|
434
|
-
?.completion;
|
|
435
|
-
if (
|
|
436
|
-
retained?.cursor === 'delivered' &&
|
|
437
|
-
isSameOperation({ ...root, ...retained }, root.operationId, root.actor)
|
|
438
|
-
) {
|
|
439
|
-
current = {
|
|
440
|
-
authority: read,
|
|
441
|
-
reference: root.reference,
|
|
442
|
-
checksumCrc32c: root.checksumCrc32c,
|
|
443
|
-
...retained
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
if (
|
|
448
|
-
current === undefined &&
|
|
449
|
-
next === 'committed' &&
|
|
450
|
-
actor !== undefined &&
|
|
451
|
-
authorityMatchesActor(read.payload, actor)
|
|
452
|
-
) {
|
|
453
|
-
throw new ZLinkFrameworkException(
|
|
454
|
-
ZLinkFrameworkErrorKind.Unavailable,
|
|
455
|
-
`Actor '${root.actor.actorId}' Join completion delivery is indeterminate: ` +
|
|
456
|
-
'the authority no longer references the operation and no matching retained root exists.'
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
if (current === undefined) {
|
|
460
|
-
throw new Error(
|
|
461
|
-
`Actor '${root.actor.actorId}' no longer references its Join completion root.`
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
requireSameOperation(current, root.operationId, root.actor);
|
|
466
|
-
if (cursorIndex(current.cursor) >= nextIndex) return current;
|
|
467
|
-
|
|
468
|
-
const replacement = await this.storeRoot(
|
|
469
|
-
{
|
|
470
|
-
operationId: current.operationId,
|
|
471
|
-
actor: actor ?? current.actor,
|
|
472
|
-
rawReply: current.rawReply,
|
|
473
|
-
...(current.replyContentType === undefined
|
|
474
|
-
? {}
|
|
475
|
-
: { replyContentType: current.replyContentType }),
|
|
476
|
-
cursor: next
|
|
477
|
-
},
|
|
478
|
-
signal
|
|
479
|
-
);
|
|
480
|
-
const publication = decodeAuthorityPublication(read.payload);
|
|
481
|
-
if (publication === undefined) {
|
|
482
|
-
await this.deleteBestEffort(replacement.reference);
|
|
483
|
-
throw new Error('Actor authority lost its deferred Join publication.');
|
|
484
|
-
}
|
|
485
|
-
const identity = decodeRelocatingActorAuthorityIdentity(
|
|
486
|
-
publication.applicationPayload,
|
|
487
|
-
read.objectGeneration
|
|
488
|
-
);
|
|
489
|
-
if (identity === undefined) {
|
|
490
|
-
await this.deleteBestEffort(replacement.reference);
|
|
491
|
-
throw new Error('Actor authority identity is invalid.');
|
|
492
|
-
}
|
|
493
|
-
const result = await this.authority.compareExchangeAuthority(
|
|
494
|
-
key,
|
|
495
|
-
read.storeVersion,
|
|
496
|
-
{
|
|
497
|
-
kind: 'put',
|
|
498
|
-
generationTransition: 'preserve',
|
|
499
|
-
payload: replaceDeferredJoinAuthorityPublication(
|
|
500
|
-
read.payload,
|
|
501
|
-
encodeAuthorityPublication({
|
|
502
|
-
...publication,
|
|
503
|
-
applicationPayload:
|
|
504
|
-
actor === undefined
|
|
505
|
-
? publication.applicationPayload
|
|
506
|
-
: replaceActorRelocationAuthorityApplicationPayload(
|
|
507
|
-
publication.applicationPayload,
|
|
508
|
-
encodeActorAuthorityIdentity({ ...identity, actor })
|
|
509
|
-
),
|
|
510
|
-
reference: replacement.reference,
|
|
511
|
-
checksumCrc32c: replacement.checksumCrc32c
|
|
512
|
-
})
|
|
513
|
-
)
|
|
514
|
-
},
|
|
515
|
-
signal
|
|
516
|
-
);
|
|
517
|
-
if (result.kind !== 'stored') {
|
|
518
|
-
await this.deleteBestEffort(replacement.reference);
|
|
519
|
-
const recovered = await this.recover(root.actor.actorId, signal);
|
|
520
|
-
if (recovered === undefined) {
|
|
521
|
-
throw new Error('Deferred Join cursor CAS conflicted without a recoverable root.');
|
|
522
|
-
}
|
|
523
|
-
requireSameOperation(recovered, root.operationId, root.actor);
|
|
524
|
-
return recovered;
|
|
525
|
-
}
|
|
526
|
-
await this.deleteBestEffort(current.reference);
|
|
527
|
-
return {
|
|
528
|
-
...replacement,
|
|
529
|
-
authority: storedSnapshot(result)
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
private async readPublished(
|
|
534
|
-
authority: ZLinkAuthoritySnapshot,
|
|
535
|
-
signal?: AbortSignal
|
|
536
|
-
): Promise<ZLinkDeferredJoinAcceptedRoot | undefined> {
|
|
537
|
-
const publication = decodeAuthorityPublication(authority.payload);
|
|
538
|
-
if (publication === undefined) return undefined;
|
|
539
|
-
// Preparing has no relocation root, while Captured still names the
|
|
540
|
-
// source-memory direct-transfer payload. Neither pre-target-publication
|
|
541
|
-
// phase can contain a Relocation Store backed deferred Join root.
|
|
542
|
-
const canonicalPhase = publication.canonicalPublication?.canonicalPhase;
|
|
543
|
-
if (canonicalPhase === 1 || canonicalPhase === 2) return undefined;
|
|
544
|
-
const read = await this.relocation.read(publication.reference, signal);
|
|
545
|
-
if (
|
|
546
|
-
read.kind !== 'found' &&
|
|
547
|
-
publication.canonical === true &&
|
|
548
|
-
publication.reference.value.startsWith('zlink-direct:')
|
|
549
|
-
) {
|
|
550
|
-
return undefined;
|
|
551
|
-
}
|
|
552
|
-
if (read.kind !== 'found' || crc32c(read.bytes) !== publication.checksumCrc32c) {
|
|
553
|
-
throw new Error('Published deferred Join completion root is missing or corrupt.');
|
|
554
|
-
}
|
|
555
|
-
if (publication.canonical === true && !hasJournalRootMagic(read.bytes)) {
|
|
556
|
-
const root = await readCanonicalDeferredJoinRoot(this.relocation, read.bytes, signal);
|
|
557
|
-
if (root === undefined) {
|
|
558
|
-
// A canonical relocation root and the post-commit Join journal share
|
|
559
|
-
// the embedded slot. A root without a completion remains Host-owned.
|
|
560
|
-
return undefined;
|
|
561
|
-
}
|
|
562
|
-
if (
|
|
563
|
-
root.identity.aggregateId !== publication.aggregateId ||
|
|
564
|
-
root.identity.aggregateGeneration !== publication.aggregateGeneration ||
|
|
565
|
-
root.identity.objectGeneration !== authority.objectGeneration
|
|
566
|
-
) {
|
|
567
|
-
throw new Error(
|
|
568
|
-
'Published deferred Join completion root identity does not match its authority slot.'
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
return {
|
|
572
|
-
authority,
|
|
573
|
-
reference: publication.reference,
|
|
574
|
-
checksumCrc32c: publication.checksumCrc32c,
|
|
575
|
-
...root.completion
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
return {
|
|
579
|
-
authority,
|
|
580
|
-
reference: publication.reference,
|
|
581
|
-
checksumCrc32c: publication.checksumCrc32c,
|
|
582
|
-
...decodeRoot(read.bytes)
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
private async storeRoot(
|
|
587
|
-
value: Omit<ZLinkDeferredJoinAcceptedRoot, 'authority' | 'reference' | 'checksumCrc32c'>,
|
|
588
|
-
signal?: AbortSignal
|
|
589
|
-
): Promise<Omit<ZLinkDeferredJoinAcceptedRoot, 'authority'>> {
|
|
590
|
-
const payload = encodeRoot(value);
|
|
591
|
-
const checksumCrc32c = crc32c(payload);
|
|
592
|
-
const stored = await putNewRelocationBlob(this.relocation, payload, RETENTION_MS, signal);
|
|
593
|
-
if (stored.expiresAt.getTime() <= stored.storeNow.getTime()) {
|
|
594
|
-
await this.deleteBestEffort(stored.reference);
|
|
595
|
-
throw new Error('Relocation Store returned an invalid deferred Join root receipt.');
|
|
596
|
-
}
|
|
597
|
-
const read = await this.relocation.read(stored.reference, signal);
|
|
598
|
-
if (read.kind !== 'found' || !Buffer.from(read.bytes).equals(payload)) {
|
|
599
|
-
await this.deleteBestEffort(stored.reference);
|
|
600
|
-
throw new Error('Relocation Store failed deferred Join root verification.');
|
|
601
|
-
}
|
|
602
|
-
return {
|
|
603
|
-
...value,
|
|
604
|
-
rawReply: Buffer.from(value.rawReply),
|
|
605
|
-
reference: stored.reference,
|
|
606
|
-
checksumCrc32c
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
private async storeCanonicalRoot(
|
|
611
|
-
value: Omit<ZLinkDeferredJoinAcceptedRoot, 'authority' | 'reference' | 'checksumCrc32c'>,
|
|
612
|
-
authority: ZLinkAuthoritySnapshot,
|
|
613
|
-
publication: DeferredJoinAuthorityPublication,
|
|
614
|
-
signal?: AbortSignal,
|
|
615
|
-
canonicalInventoryDigest?: string
|
|
616
|
-
): Promise<Omit<ZLinkDeferredJoinAcceptedRoot, 'authority'>> {
|
|
617
|
-
let inventory: Buffer;
|
|
618
|
-
if (canonicalInventoryDigest !== undefined) {
|
|
619
|
-
if (!/^[0-9a-f]{64}$/u.test(canonicalInventoryDigest)) {
|
|
620
|
-
throw new Error('Canonical relocation inventory digest is invalid.');
|
|
621
|
-
}
|
|
622
|
-
inventory = Buffer.from(canonicalInventoryDigest, 'hex');
|
|
623
|
-
} else {
|
|
624
|
-
const current = await this.relocation.read(publication.reference, signal);
|
|
625
|
-
if (current.kind !== 'found' || crc32c(current.bytes) !== publication.checksumCrc32c) {
|
|
626
|
-
throw new Error('Canonical relocation manifest is missing or corrupt.');
|
|
627
|
-
}
|
|
628
|
-
inventory = decodeCanonicalTreeManifest(current.bytes).inventoryDigest;
|
|
629
|
-
}
|
|
630
|
-
const logicalRoot = encodeCanonicalDeferredJoinRoot(value, authority, publication, inventory);
|
|
631
|
-
const stored = await storeCanonicalTree(this.relocation, logicalRoot, inventory, signal);
|
|
632
|
-
return {
|
|
633
|
-
...value,
|
|
634
|
-
rawReply: Buffer.from(value.rawReply),
|
|
635
|
-
reference: stored.reference,
|
|
636
|
-
checksumCrc32c: stored.checksumCrc32c
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
private async deleteBestEffort(reference: ZLinkBlobReference): Promise<void> {
|
|
641
|
-
try {
|
|
642
|
-
await this.relocation.delete(reference);
|
|
643
|
-
} catch {
|
|
644
|
-
// Fixed retention remains the orphan cleanup boundary.
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function requireAuthorityActor(payload: Uint8Array, actor: ActorRef): void {
|
|
650
|
-
if (!authorityMatchesActor(payload, actor)) {
|
|
651
|
-
throw new Error(`Actor '${actor.actorId}' authority fence does not match its ActorRef.`);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function authorityMatchesActor(payload: Uint8Array, actor: ActorRef): boolean {
|
|
656
|
-
const publication = decodeAuthorityPublication(payload);
|
|
657
|
-
const applications =
|
|
658
|
-
publication === undefined
|
|
659
|
-
? [serviceRelocationAuthorityApplicationPayload(payload)]
|
|
660
|
-
: [publication.applicationPayload, serviceRelocationAuthorityApplicationPayload(payload)];
|
|
661
|
-
return applications.some((application) => {
|
|
662
|
-
const identity = decodeRelocatingActorAuthorityIdentity(application, actor.objectGeneration);
|
|
663
|
-
return (
|
|
664
|
-
identity !== undefined &&
|
|
665
|
-
identity.actor.actorId === actor.actorId &&
|
|
666
|
-
identity.actor.objectGeneration === actor.objectGeneration &&
|
|
667
|
-
routingIdWireHex(identity.actor.nodeRid) === routingIdWireHex(actor.nodeRid)
|
|
668
|
-
);
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function encodeRoot(
|
|
673
|
-
value: Omit<ZLinkDeferredJoinAcceptedRoot, 'authority' | 'reference' | 'checksumCrc32c'>
|
|
674
|
-
): Buffer {
|
|
675
|
-
const nodeRidHex = routingIdWireHex(value.actor.nodeRid);
|
|
676
|
-
const encoded = Buffer.concat([
|
|
677
|
-
ROOT_MAGIC,
|
|
678
|
-
Buffer.from(
|
|
679
|
-
JSON.stringify({
|
|
680
|
-
version: ROOT_VERSION,
|
|
681
|
-
operationHigh: value.operationId.high.toString(),
|
|
682
|
-
operationLow: value.operationId.low.toString(),
|
|
683
|
-
actorId: value.actor.actorId,
|
|
684
|
-
actorGeneration: value.actor.objectGeneration.toString(),
|
|
685
|
-
actorMeshName: value.actor.meshName,
|
|
686
|
-
actorNodeRid: String(value.actor.nodeRid),
|
|
687
|
-
actorNodeRidHex: nodeRidHex,
|
|
688
|
-
rawReply: Buffer.from(value.rawReply).toString('base64'),
|
|
689
|
-
...(value.replyContentType === undefined
|
|
690
|
-
? {}
|
|
691
|
-
: { replyContentType: value.replyContentType }),
|
|
692
|
-
cursor: value.cursor
|
|
693
|
-
}),
|
|
694
|
-
'utf8'
|
|
695
|
-
)
|
|
696
|
-
]);
|
|
697
|
-
return encoded;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function decodeRoot(
|
|
701
|
-
payload: Uint8Array
|
|
702
|
-
): Omit<ZLinkDeferredJoinAcceptedRoot, 'authority' | 'reference' | 'checksumCrc32c'> {
|
|
703
|
-
if (payload.byteLength === 0) {
|
|
704
|
-
throw new Error('Deferred Join completion root is empty.');
|
|
705
|
-
}
|
|
706
|
-
const bytes = Buffer.from(payload);
|
|
707
|
-
const encoded = hasJournalRootMagic(bytes) ? bytes.subarray(ROOT_MAGIC.byteLength) : bytes;
|
|
708
|
-
const value = JSON.parse(encoded.toString('utf8')) as Record<string, unknown>;
|
|
709
|
-
if (
|
|
710
|
-
value.version !== ROOT_VERSION ||
|
|
711
|
-
typeof value.operationHigh !== 'string' ||
|
|
712
|
-
typeof value.operationLow !== 'string' ||
|
|
713
|
-
typeof value.actorId !== 'string' ||
|
|
714
|
-
typeof value.actorGeneration !== 'string' ||
|
|
715
|
-
typeof value.actorMeshName !== 'string' ||
|
|
716
|
-
typeof value.actorNodeRid !== 'string' ||
|
|
717
|
-
typeof value.rawReply !== 'string' ||
|
|
718
|
-
(value.replyContentType !== undefined &&
|
|
719
|
-
(typeof value.replyContentType !== 'string' || value.replyContentType.length === 0)) ||
|
|
720
|
-
!isCursor(value.cursor)
|
|
721
|
-
) {
|
|
722
|
-
throw new Error('Deferred Join completion root is invalid.');
|
|
723
|
-
}
|
|
724
|
-
const rawReply = Buffer.from(value.rawReply, 'base64');
|
|
725
|
-
if (rawReply.toString('base64') !== value.rawReply) {
|
|
726
|
-
throw new Error('Deferred Join completion reply is not canonical base64.');
|
|
727
|
-
}
|
|
728
|
-
const actorGeneration = BigInt(value.actorGeneration);
|
|
729
|
-
const operationId = {
|
|
730
|
-
high: BigInt(value.operationHigh),
|
|
731
|
-
low: BigInt(value.operationLow)
|
|
732
|
-
};
|
|
733
|
-
const actor: ActorRef = {
|
|
734
|
-
actorId: value.actorId,
|
|
735
|
-
objectGeneration: actorGeneration,
|
|
736
|
-
meshName: value.actorMeshName,
|
|
737
|
-
nodeRid: decodeRoutingId(
|
|
738
|
-
value.actorNodeRid,
|
|
739
|
-
typeof value.actorNodeRidHex === 'string' ? value.actorNodeRidHex : undefined
|
|
740
|
-
)
|
|
741
|
-
};
|
|
742
|
-
validateIdentity(value.actorId, operationId, actor);
|
|
743
|
-
return {
|
|
744
|
-
operationId,
|
|
745
|
-
actor,
|
|
746
|
-
rawReply,
|
|
747
|
-
...(typeof value.replyContentType === 'string'
|
|
748
|
-
? { replyContentType: value.replyContentType }
|
|
749
|
-
: {}),
|
|
750
|
-
cursor: value.cursor
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function encodeAuthorityPublication(value: DeferredJoinAuthorityPublication): Buffer {
|
|
755
|
-
return Buffer.from(
|
|
756
|
-
new ServiceRelocationAuthorityPayloadCodec().publish(
|
|
757
|
-
value.applicationPayload,
|
|
758
|
-
toServiceRelocationPublication(value)
|
|
759
|
-
)
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function toServiceRelocationPublication(
|
|
764
|
-
value: DeferredJoinAuthorityPublication
|
|
765
|
-
): ServiceRelocationPublication {
|
|
766
|
-
return {
|
|
767
|
-
...value.canonicalPublication,
|
|
768
|
-
reference: value.reference.value,
|
|
769
|
-
checksumCrc32c: value.checksumCrc32c,
|
|
770
|
-
aggregateId: value.aggregateId,
|
|
771
|
-
aggregateGeneration: value.aggregateGeneration,
|
|
772
|
-
inventoryDigest: journalInventoryDigest(value.applicationPayload),
|
|
773
|
-
targetOwnerId: value.targetOwnerId,
|
|
774
|
-
targetOwnerLeaseGeneration: value.targetOwnerLeaseGeneration
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function decodeAuthorityPublication(
|
|
779
|
-
payload: Uint8Array
|
|
780
|
-
): DeferredJoinAuthorityPublication | undefined {
|
|
781
|
-
const outerApplication = serviceRelocationAuthorityApplicationPayload(payload);
|
|
782
|
-
const candidates = Buffer.from(outerApplication).equals(Buffer.from(payload))
|
|
783
|
-
? [Buffer.from(payload)]
|
|
784
|
-
: [Buffer.from(outerApplication), Buffer.from(payload)];
|
|
785
|
-
for (const candidate of candidates) {
|
|
786
|
-
const publication = decodeDirectAuthorityPublication(candidate);
|
|
787
|
-
if (publication !== undefined) return publication;
|
|
788
|
-
}
|
|
789
|
-
return undefined;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function replaceDeferredJoinAuthorityPublication(
|
|
793
|
-
payload: Uint8Array,
|
|
794
|
-
replacement: Uint8Array
|
|
795
|
-
): Buffer {
|
|
796
|
-
const codec = new ServiceRelocationAuthorityPayloadCodec();
|
|
797
|
-
const currentPublication = codec.read(payload);
|
|
798
|
-
const replacementPublication = codec.read(replacement);
|
|
799
|
-
if (currentPublication?.canonical === true || replacementPublication?.canonical === true) {
|
|
800
|
-
// Canonical authority has exactly one embedded relocation slot. A journal
|
|
801
|
-
// cursor replaces that slot atomically; it must never be wrapped or nested
|
|
802
|
-
// under an older ZLAR/application layer.
|
|
803
|
-
return Buffer.from(replacement);
|
|
804
|
-
}
|
|
805
|
-
const outerApplication = serviceRelocationAuthorityApplicationPayload(payload);
|
|
806
|
-
if (
|
|
807
|
-
!Buffer.from(outerApplication).equals(Buffer.from(payload)) &&
|
|
808
|
-
decodeDirectAuthorityPublication(outerApplication) !== undefined
|
|
809
|
-
) {
|
|
810
|
-
return replaceServiceRelocationAuthorityApplicationPayload(payload, replacement);
|
|
811
|
-
}
|
|
812
|
-
return decodeDirectAuthorityPublication(payload) === undefined
|
|
813
|
-
? replaceServiceRelocationAuthorityApplicationPayload(payload, replacement)
|
|
814
|
-
: Buffer.from(replacement);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function decodeDirectAuthorityPublication(
|
|
818
|
-
payload: Uint8Array
|
|
819
|
-
): DeferredJoinAuthorityPublication | undefined {
|
|
820
|
-
const publication = new ServiceRelocationAuthorityPayloadCodec().read(payload);
|
|
821
|
-
if (publication === undefined) return undefined;
|
|
822
|
-
const applicationPayload = serviceRelocationAuthorityApplicationPayload(payload);
|
|
823
|
-
return publication.canonical === true ||
|
|
824
|
-
publication.inventoryDigest === journalInventoryDigest(applicationPayload)
|
|
825
|
-
? deferredJoinPublication(applicationPayload, publication)
|
|
826
|
-
: undefined;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function deferredJoinPublication(
|
|
830
|
-
applicationPayload: Buffer,
|
|
831
|
-
publication: ServiceRelocationPublication
|
|
832
|
-
): DeferredJoinAuthorityPublication {
|
|
833
|
-
return {
|
|
834
|
-
applicationPayload,
|
|
835
|
-
reference: { value: publication.reference } as ZLinkBlobReference,
|
|
836
|
-
checksumCrc32c: publication.checksumCrc32c,
|
|
837
|
-
aggregateId: publication.aggregateId,
|
|
838
|
-
aggregateGeneration: publication.aggregateGeneration,
|
|
839
|
-
targetOwnerId: publication.targetOwnerId,
|
|
840
|
-
targetOwnerLeaseGeneration: publication.targetOwnerLeaseGeneration,
|
|
841
|
-
canonical: publication.canonical,
|
|
842
|
-
...(publication.canonical === true ? { canonicalPublication: publication } : {})
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
function hasJournalRootMagic(payload: Uint8Array): boolean {
|
|
847
|
-
const bytes = Buffer.from(payload);
|
|
848
|
-
return (
|
|
849
|
-
bytes.byteLength >= ROOT_MAGIC.byteLength &&
|
|
850
|
-
bytes.subarray(0, ROOT_MAGIC.byteLength).equals(ROOT_MAGIC)
|
|
851
|
-
);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function journalInventoryDigest(applicationPayload: Uint8Array): string {
|
|
855
|
-
return createHash('sha256')
|
|
856
|
-
.update(JOURNAL_INVENTORY_DOMAIN)
|
|
857
|
-
.update(applicationPayload)
|
|
858
|
-
.digest('hex');
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function operationAggregateId(operationId: ZLinkActorJoinOperationId): string {
|
|
862
|
-
const encoded = [operationId.high, operationId.low]
|
|
863
|
-
.map((value) => BigInt.asUintN(64, value).toString(16).padStart(16, '0'))
|
|
864
|
-
.join('');
|
|
865
|
-
return (
|
|
866
|
-
`${encoded.slice(0, 8)}-${encoded.slice(8, 12)}-${encoded.slice(12, 16)}` +
|
|
867
|
-
`-${encoded.slice(16, 20)}-${encoded.slice(20)}`
|
|
868
|
-
);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
function requireCanonicalAggregateGeneration(value: bigint): bigint {
|
|
872
|
-
if (value === 0n) {
|
|
873
|
-
throw new ZLinkFrameworkException(
|
|
874
|
-
ZLinkFrameworkErrorKind.DataLost,
|
|
875
|
-
'Canonical relocation slot has no aggregate generation.'
|
|
876
|
-
);
|
|
877
|
-
}
|
|
878
|
-
return value;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
interface CanonicalTreeManifest {
|
|
882
|
-
readonly inventoryDigest: Buffer;
|
|
883
|
-
readonly logicalLength: bigint;
|
|
884
|
-
readonly logicalChecksumCrc32c: number;
|
|
885
|
-
readonly chunks: readonly {
|
|
886
|
-
readonly order: number;
|
|
887
|
-
readonly reference: ZLinkBlobReference;
|
|
888
|
-
readonly length: bigint;
|
|
889
|
-
readonly checksumCrc32c: number;
|
|
890
|
-
}[];
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function decodeCanonicalTreeManifest(payload: Uint8Array): CanonicalTreeManifest {
|
|
894
|
-
const generated = decodeRelocationManifestV1(payload);
|
|
895
|
-
const chunks = generated.chunks.map((chunk, index) => {
|
|
896
|
-
if (chunk.order !== index) {
|
|
897
|
-
throw new Error('Canonical relocation chunk identity is invalid.');
|
|
898
|
-
}
|
|
899
|
-
return {
|
|
900
|
-
order: chunk.order,
|
|
901
|
-
reference: { value: chunk.reference } as ZLinkBlobReference,
|
|
902
|
-
length: chunk.length,
|
|
903
|
-
checksumCrc32c: chunk.checksumCrc32c
|
|
904
|
-
};
|
|
905
|
-
});
|
|
906
|
-
if (chunks.length === 0) {
|
|
907
|
-
throw new Error('Canonical relocation chunk count is invalid.');
|
|
908
|
-
}
|
|
909
|
-
if (chunks.reduce((total, value) => total + value.length, 0n) !== generated.totalLength) {
|
|
910
|
-
throw new Error('Canonical relocation manifest length is invalid.');
|
|
911
|
-
}
|
|
912
|
-
return {
|
|
913
|
-
inventoryDigest: Buffer.from(generated.inventoryDigestSha256),
|
|
914
|
-
logicalLength: generated.totalLength,
|
|
915
|
-
logicalChecksumCrc32c: generated.totalChecksumCrc32c,
|
|
916
|
-
chunks
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
async function storeCanonicalTree(
|
|
921
|
-
store: ZLinkRelocationStore,
|
|
922
|
-
logicalRoot: Uint8Array,
|
|
923
|
-
inventoryDigest: Uint8Array,
|
|
924
|
-
signal?: AbortSignal
|
|
925
|
-
): Promise<{ readonly reference: ZLinkBlobReference; readonly checksumCrc32c: number }> {
|
|
926
|
-
const logical = Buffer.from(logicalRoot);
|
|
927
|
-
const logicalChecksumCrc32c = crc32c(logical);
|
|
928
|
-
const chunk = Buffer.from(
|
|
929
|
-
encodeRelocationDataChunkV1({
|
|
930
|
-
order: 0,
|
|
931
|
-
data: logical
|
|
932
|
-
})
|
|
933
|
-
);
|
|
934
|
-
const storedChunk = await putAndVerifyCanonicalBlob(store, chunk, signal);
|
|
935
|
-
const manifest = Buffer.from(
|
|
936
|
-
encodeRelocationManifestV1({
|
|
937
|
-
logicalFormatVersion: 1,
|
|
938
|
-
totalLength: BigInt(logical.byteLength),
|
|
939
|
-
totalChecksumCrc32c: logicalChecksumCrc32c,
|
|
940
|
-
inventoryDigestSha256: Buffer.from(inventoryDigest),
|
|
941
|
-
chunks: [
|
|
942
|
-
{
|
|
943
|
-
order: 0,
|
|
944
|
-
reference: storedChunk.reference.value,
|
|
945
|
-
length: BigInt(logical.byteLength),
|
|
946
|
-
checksumCrc32c: logicalChecksumCrc32c
|
|
947
|
-
}
|
|
948
|
-
]
|
|
949
|
-
})
|
|
950
|
-
);
|
|
951
|
-
const storedManifest = await putAndVerifyCanonicalBlob(store, manifest, signal);
|
|
952
|
-
return {
|
|
953
|
-
reference: storedManifest.reference,
|
|
954
|
-
checksumCrc32c: crc32c(manifest)
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
async function putAndVerifyCanonicalBlob(
|
|
959
|
-
store: ZLinkRelocationStore,
|
|
960
|
-
payload: Uint8Array,
|
|
961
|
-
signal?: AbortSignal
|
|
962
|
-
): Promise<{ readonly reference: ZLinkBlobReference }> {
|
|
963
|
-
const stored = await putNewRelocationBlob(store, payload, RETENTION_MS, signal);
|
|
964
|
-
const read = await store.read(stored.reference, signal);
|
|
965
|
-
if (read.kind !== 'found' || !Buffer.from(read.bytes).equals(Buffer.from(payload))) {
|
|
966
|
-
await store.delete(stored.reference).catch(() => undefined);
|
|
967
|
-
throw new Error('Relocation Store failed canonical tree verification.');
|
|
968
|
-
}
|
|
969
|
-
return { reference: stored.reference };
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
async function readCanonicalDeferredJoinRoot(
|
|
973
|
-
store: ZLinkRelocationStore,
|
|
974
|
-
manifestPayload: Uint8Array,
|
|
975
|
-
signal?: AbortSignal
|
|
976
|
-
): Promise<
|
|
977
|
-
| {
|
|
978
|
-
readonly identity: ZLinkDeferredJoinRootIdentity;
|
|
979
|
-
readonly completion: Omit<
|
|
980
|
-
ZLinkDeferredJoinAcceptedRoot,
|
|
981
|
-
'authority' | 'reference' | 'checksumCrc32c'
|
|
982
|
-
>;
|
|
983
|
-
}
|
|
984
|
-
| undefined
|
|
985
|
-
> {
|
|
986
|
-
let manifest: CanonicalTreeManifest;
|
|
987
|
-
try {
|
|
988
|
-
manifest = decodeCanonicalTreeManifest(manifestPayload);
|
|
989
|
-
} catch {
|
|
990
|
-
return undefined;
|
|
991
|
-
}
|
|
992
|
-
const parts: Buffer[] = [];
|
|
993
|
-
for (const expected of manifest.chunks) {
|
|
994
|
-
const read = await store.read(expected.reference, signal);
|
|
995
|
-
if (read.kind !== 'found') throw new Error('Canonical relocation chunk is missing.');
|
|
996
|
-
const chunk = decodeRelocationDataChunkV1(read.bytes);
|
|
997
|
-
if (chunk.order !== expected.order || BigInt(chunk.data.byteLength) !== expected.length) {
|
|
998
|
-
throw new Error('Canonical relocation chunk identity is invalid.');
|
|
999
|
-
}
|
|
1000
|
-
const data = Buffer.from(chunk.data);
|
|
1001
|
-
if (crc32c(data) !== expected.checksumCrc32c) {
|
|
1002
|
-
throw new Error('Canonical relocation chunk data checksum is invalid.');
|
|
1003
|
-
}
|
|
1004
|
-
parts.push(data);
|
|
1005
|
-
}
|
|
1006
|
-
const logical = Buffer.concat(parts);
|
|
1007
|
-
if (
|
|
1008
|
-
BigInt(logical.byteLength) !== manifest.logicalLength ||
|
|
1009
|
-
crc32c(logical) !== manifest.logicalChecksumCrc32c
|
|
1010
|
-
) {
|
|
1011
|
-
throw new Error('Canonical relocation logical root checksum is invalid.');
|
|
1012
|
-
}
|
|
1013
|
-
return decodeCanonicalDeferredJoinRoot(logical);
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
function encodeCanonicalDeferredJoinRoot(
|
|
1017
|
-
value: Omit<ZLinkDeferredJoinAcceptedRoot, 'authority' | 'reference' | 'checksumCrc32c'>,
|
|
1018
|
-
authority: ZLinkAuthoritySnapshot,
|
|
1019
|
-
publication: DeferredJoinAuthorityPublication,
|
|
1020
|
-
inventoryDigest: Uint8Array
|
|
1021
|
-
): Buffer {
|
|
1022
|
-
const key = encodeAuthorityKey('actor', value.actor.actorId).value;
|
|
1023
|
-
const completion = encodeDotNetDeferredJoinCompletion(value);
|
|
1024
|
-
return Buffer.concat([
|
|
1025
|
-
u32le(0x5a4c5231),
|
|
1026
|
-
u16le(2),
|
|
1027
|
-
dotNetGuidBytes(publication.aggregateId),
|
|
1028
|
-
u64le(publication.aggregateGeneration),
|
|
1029
|
-
bytes32le(inventoryDigest),
|
|
1030
|
-
u32le(1),
|
|
1031
|
-
text16le(key),
|
|
1032
|
-
Buffer.of(1),
|
|
1033
|
-
u64le(authority.objectGeneration),
|
|
1034
|
-
u64le(authority.authorityOwnerGeneration),
|
|
1035
|
-
bytes32le(Buffer.alloc(0)),
|
|
1036
|
-
u32le(0),
|
|
1037
|
-
u32le(0),
|
|
1038
|
-
bytes32le(Buffer.alloc(0)),
|
|
1039
|
-
bytes32le(completion)
|
|
1040
|
-
]);
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
function decodeCanonicalDeferredJoinRoot(payload: Uint8Array):
|
|
1044
|
-
| {
|
|
1045
|
-
readonly identity: ZLinkDeferredJoinRootIdentity;
|
|
1046
|
-
readonly completion: Omit<
|
|
1047
|
-
ZLinkDeferredJoinAcceptedRoot,
|
|
1048
|
-
'authority' | 'reference' | 'checksumCrc32c'
|
|
1049
|
-
>;
|
|
1050
|
-
}
|
|
1051
|
-
| undefined {
|
|
1052
|
-
const reader = new LittleEndianReader(payload);
|
|
1053
|
-
if (reader.u32() !== 0x5a4c5231 || reader.u16() !== 2) return undefined;
|
|
1054
|
-
const aggregateId = canonicalUuidFromDotNetBytes(reader.take(16));
|
|
1055
|
-
const aggregateGeneration = reader.u64();
|
|
1056
|
-
reader.bytes32();
|
|
1057
|
-
const count = reader.u32();
|
|
1058
|
-
let completion: ReturnType<typeof decodeDotNetDeferredJoinCompletion> | undefined;
|
|
1059
|
-
let completionIdentity:
|
|
1060
|
-
| Pick<ZLinkDeferredJoinRootIdentity, 'authorityKey' | 'objectKind' | 'objectGeneration'>
|
|
1061
|
-
| undefined;
|
|
1062
|
-
for (let index = 0; index < count; index++) {
|
|
1063
|
-
const authorityKey = reader.text16();
|
|
1064
|
-
const objectKind = reader.u8();
|
|
1065
|
-
const objectGeneration = reader.u64();
|
|
1066
|
-
reader.u64();
|
|
1067
|
-
reader.bytes32();
|
|
1068
|
-
const jobs = reader.u32();
|
|
1069
|
-
for (let job = 0; job < jobs; job++) {
|
|
1070
|
-
reader.u64();
|
|
1071
|
-
reader.bytes32();
|
|
1072
|
-
}
|
|
1073
|
-
const timers = reader.u32();
|
|
1074
|
-
for (let timer = 0; timer < timers; timer++) {
|
|
1075
|
-
reader.text16();
|
|
1076
|
-
reader.i64();
|
|
1077
|
-
reader.i64();
|
|
1078
|
-
reader.bytes32();
|
|
1079
|
-
}
|
|
1080
|
-
reader.bytes32();
|
|
1081
|
-
const encodedCompletion = reader.bytes32();
|
|
1082
|
-
if (encodedCompletion.byteLength !== 0) {
|
|
1083
|
-
if (completion !== undefined)
|
|
1084
|
-
throw new Error('Canonical relocation root has duplicate completions.');
|
|
1085
|
-
completion = decodeDotNetDeferredJoinCompletion(encodedCompletion);
|
|
1086
|
-
if (objectKind !== 1) return undefined;
|
|
1087
|
-
completionIdentity = {
|
|
1088
|
-
authorityKey,
|
|
1089
|
-
objectKind: 'actor',
|
|
1090
|
-
objectGeneration
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
if (!reader.done) throw new Error('Canonical relocation root has trailing bytes.');
|
|
1095
|
-
if (completion === undefined || completionIdentity === undefined) return undefined;
|
|
1096
|
-
if (
|
|
1097
|
-
completion.actor.objectGeneration !== completionIdentity.objectGeneration ||
|
|
1098
|
-
encodeAuthorityKey('actor', completion.actor.actorId).value !== completionIdentity.authorityKey
|
|
1099
|
-
) {
|
|
1100
|
-
return undefined;
|
|
1101
|
-
}
|
|
1102
|
-
return {
|
|
1103
|
-
identity: {
|
|
1104
|
-
...completionIdentity,
|
|
1105
|
-
aggregateId,
|
|
1106
|
-
aggregateGeneration
|
|
1107
|
-
},
|
|
1108
|
-
completion
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
function encodeDotNetDeferredJoinCompletion(
|
|
1113
|
-
value: Omit<ZLinkDeferredJoinAcceptedRoot, 'authority' | 'reference' | 'checksumCrc32c'>
|
|
1114
|
-
): Buffer {
|
|
1115
|
-
const routingId = Buffer.from(encodeRoutingIdStorageHex(value.actor.nodeRid), 'hex');
|
|
1116
|
-
return Buffer.concat([
|
|
1117
|
-
u32le(0x5a4c4a43),
|
|
1118
|
-
Buffer.of(2),
|
|
1119
|
-
text16le(value.actor.actorId),
|
|
1120
|
-
u64le(value.actor.objectGeneration),
|
|
1121
|
-
u64le(value.operationId.high),
|
|
1122
|
-
u64le(value.operationId.low),
|
|
1123
|
-
text16le(value.actor.meshName),
|
|
1124
|
-
Buffer.of(routingId.byteLength),
|
|
1125
|
-
routingId,
|
|
1126
|
-
u64le(value.actor.objectGeneration),
|
|
1127
|
-
Buffer.of(value.replyContentType === undefined ? 0 : 1),
|
|
1128
|
-
...(value.replyContentType === undefined ? [] : [text16le(value.replyContentType)]),
|
|
1129
|
-
bytes32le(value.rawReply),
|
|
1130
|
-
Buffer.of(cursorIndex(value.cursor) + 1)
|
|
1131
|
-
]);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
function decodeDotNetDeferredJoinCompletion(
|
|
1135
|
-
payload: Uint8Array
|
|
1136
|
-
): Omit<ZLinkDeferredJoinAcceptedRoot, 'authority' | 'reference' | 'checksumCrc32c'> {
|
|
1137
|
-
const reader = new LittleEndianReader(payload);
|
|
1138
|
-
if (reader.u32() !== 0x5a4c4a43 || reader.u8() !== 2) {
|
|
1139
|
-
throw new Error('Canonical deferred Join completion header is invalid.');
|
|
1140
|
-
}
|
|
1141
|
-
const actorId = reader.text16();
|
|
1142
|
-
const objectGeneration = reader.u64();
|
|
1143
|
-
const operationId = { high: reader.u64(), low: reader.u64() };
|
|
1144
|
-
const meshName = reader.text16();
|
|
1145
|
-
const nodeBytes = reader.take(reader.u8());
|
|
1146
|
-
const actorGeneration = reader.u64();
|
|
1147
|
-
const contentTypePresence = reader.u8();
|
|
1148
|
-
if (contentTypePresence > 1) {
|
|
1149
|
-
throw new Error('Canonical deferred Join completion content type is invalid.');
|
|
1150
|
-
}
|
|
1151
|
-
const replyContentType = contentTypePresence === 1 ? reader.text16() : undefined;
|
|
1152
|
-
const rawReply = reader.bytes32();
|
|
1153
|
-
const cursorValue = reader.u8();
|
|
1154
|
-
if (!reader.done || actorGeneration !== objectGeneration || cursorValue < 1 || cursorValue > 3) {
|
|
1155
|
-
throw new Error('Canonical deferred Join completion is invalid.');
|
|
1156
|
-
}
|
|
1157
|
-
const actor: ActorRef = {
|
|
1158
|
-
actorId,
|
|
1159
|
-
objectGeneration,
|
|
1160
|
-
meshName,
|
|
1161
|
-
nodeRid: decodeRoutingId('', nodeBytes.toString('hex'))
|
|
1162
|
-
};
|
|
1163
|
-
validateIdentity(actorId, operationId, actor);
|
|
1164
|
-
return {
|
|
1165
|
-
operationId,
|
|
1166
|
-
actor,
|
|
1167
|
-
rawReply,
|
|
1168
|
-
...(replyContentType === undefined ? {} : { replyContentType }),
|
|
1169
|
-
cursor: (['prepared', 'committed', 'delivered'] as const)[cursorValue - 1]!
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
class LittleEndianReader {
|
|
1174
|
-
private offset = 0;
|
|
1175
|
-
private readonly bytes: Buffer;
|
|
1176
|
-
constructor(payload: Uint8Array) {
|
|
1177
|
-
this.bytes = Buffer.from(payload);
|
|
1178
|
-
}
|
|
1179
|
-
get done(): boolean {
|
|
1180
|
-
return this.offset === this.bytes.byteLength;
|
|
1181
|
-
}
|
|
1182
|
-
take(length: number): Buffer {
|
|
1183
|
-
if (length < 0 || this.offset + length > this.bytes.byteLength) {
|
|
1184
|
-
throw new Error('Canonical relocation root is truncated.');
|
|
1185
|
-
}
|
|
1186
|
-
const result = this.bytes.subarray(this.offset, this.offset + length);
|
|
1187
|
-
this.offset += length;
|
|
1188
|
-
return result;
|
|
1189
|
-
}
|
|
1190
|
-
u8(): number {
|
|
1191
|
-
return this.take(1)[0]!;
|
|
1192
|
-
}
|
|
1193
|
-
u16(): number {
|
|
1194
|
-
return this.take(2).readUInt16LE();
|
|
1195
|
-
}
|
|
1196
|
-
u32(): number {
|
|
1197
|
-
return this.take(4).readUInt32LE();
|
|
1198
|
-
}
|
|
1199
|
-
u64(): bigint {
|
|
1200
|
-
return this.take(8).readBigUInt64LE();
|
|
1201
|
-
}
|
|
1202
|
-
i64(): bigint {
|
|
1203
|
-
return this.take(8).readBigInt64LE();
|
|
1204
|
-
}
|
|
1205
|
-
text16(): string {
|
|
1206
|
-
const bytes = this.take(this.u16());
|
|
1207
|
-
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
1208
|
-
}
|
|
1209
|
-
bytes32(): Buffer {
|
|
1210
|
-
return this.take(this.u32());
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
function dotNetGuidBytes(value: string): Buffer {
|
|
1215
|
-
const bytes = Buffer.from(value.replaceAll('-', ''), 'hex');
|
|
1216
|
-
return Buffer.from([
|
|
1217
|
-
bytes[3]!,
|
|
1218
|
-
bytes[2]!,
|
|
1219
|
-
bytes[1]!,
|
|
1220
|
-
bytes[0]!,
|
|
1221
|
-
bytes[5]!,
|
|
1222
|
-
bytes[4]!,
|
|
1223
|
-
bytes[7]!,
|
|
1224
|
-
bytes[6]!,
|
|
1225
|
-
...bytes.subarray(8)
|
|
1226
|
-
]);
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
function canonicalUuidFromDotNetBytes(value: Uint8Array): string {
|
|
1230
|
-
const bytes = Buffer.from(value);
|
|
1231
|
-
if (bytes.byteLength !== 16) {
|
|
1232
|
-
throw new TypeError('Canonical relocation aggregate id is invalid.');
|
|
1233
|
-
}
|
|
1234
|
-
const canonical = Buffer.from([
|
|
1235
|
-
bytes[3]!,
|
|
1236
|
-
bytes[2]!,
|
|
1237
|
-
bytes[1]!,
|
|
1238
|
-
bytes[0]!,
|
|
1239
|
-
bytes[5]!,
|
|
1240
|
-
bytes[4]!,
|
|
1241
|
-
bytes[7]!,
|
|
1242
|
-
bytes[6]!,
|
|
1243
|
-
...bytes.subarray(8)
|
|
1244
|
-
]).toString('hex');
|
|
1245
|
-
return (
|
|
1246
|
-
`${canonical.slice(0, 8)}-${canonical.slice(8, 12)}-${canonical.slice(12, 16)}` +
|
|
1247
|
-
`-${canonical.slice(16, 20)}-${canonical.slice(20)}`
|
|
1248
|
-
);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
function text16le(value: string): Buffer {
|
|
1252
|
-
const bytes = Buffer.from(value, 'utf8');
|
|
1253
|
-
if (bytes.byteLength === 0 || bytes.byteLength > 0xffff) throw new Error('Text16 is invalid.');
|
|
1254
|
-
return Buffer.concat([u16le(bytes.byteLength), bytes]);
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
function bytes32le(value: Uint8Array): Buffer {
|
|
1258
|
-
return Buffer.concat([u32le(value.byteLength), Buffer.from(value)]);
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
function u16le(value: number): Buffer {
|
|
1262
|
-
const result = Buffer.alloc(2);
|
|
1263
|
-
result.writeUInt16LE(value);
|
|
1264
|
-
return result;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
function u32le(value: number): Buffer {
|
|
1268
|
-
const result = Buffer.alloc(4);
|
|
1269
|
-
result.writeUInt32LE(value);
|
|
1270
|
-
return result;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
function u64le(value: bigint): Buffer {
|
|
1274
|
-
const result = Buffer.alloc(8);
|
|
1275
|
-
result.writeBigUInt64LE(value);
|
|
1276
|
-
return result;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
function validateIdentity(
|
|
1280
|
-
actorId: string,
|
|
1281
|
-
operationId: ZLinkActorJoinOperationId,
|
|
1282
|
-
actor: ActorRef
|
|
1283
|
-
): void {
|
|
1284
|
-
if (
|
|
1285
|
-
actorId.length === 0 ||
|
|
1286
|
-
actor.actorId !== actorId ||
|
|
1287
|
-
actor.objectGeneration <= 0n ||
|
|
1288
|
-
(operationId.high === 0n && operationId.low === 0n)
|
|
1289
|
-
) {
|
|
1290
|
-
throw new Error('Deferred Join completion identity is invalid.');
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
function requireSameOperation(
|
|
1295
|
-
root: ZLinkDeferredJoinAcceptedRoot,
|
|
1296
|
-
operationId: ZLinkActorJoinOperationId,
|
|
1297
|
-
actor: ActorRef
|
|
1298
|
-
): void {
|
|
1299
|
-
if (!isSameOperation(root, operationId, actor)) {
|
|
1300
|
-
throw new Error(`Actor '${actor.actorId}' already has a different durable Join completion.`);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
function isSameOperation(
|
|
1305
|
-
root: ZLinkDeferredJoinAcceptedRoot,
|
|
1306
|
-
operationId: ZLinkActorJoinOperationId,
|
|
1307
|
-
actor: ActorRef
|
|
1308
|
-
): boolean {
|
|
1309
|
-
return (
|
|
1310
|
-
root.operationId.high === operationId.high &&
|
|
1311
|
-
root.operationId.low === operationId.low &&
|
|
1312
|
-
root.actor.actorId === actor.actorId &&
|
|
1313
|
-
root.actor.objectGeneration === actor.objectGeneration
|
|
1314
|
-
);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
function requireSameActor(root: ZLinkDeferredJoinAcceptedRoot, actor: ActorRef): void {
|
|
1318
|
-
if (
|
|
1319
|
-
root.actor.actorId !== actor.actorId ||
|
|
1320
|
-
root.actor.objectGeneration !== actor.objectGeneration
|
|
1321
|
-
) {
|
|
1322
|
-
throw new Error(
|
|
1323
|
-
'Deferred Join completion generation fence is stale ' +
|
|
1324
|
-
`(root ${root.actor.actorId}/${root.actor.objectGeneration}, ` +
|
|
1325
|
-
`actor ${actor.actorId}/${actor.objectGeneration}).`
|
|
1326
|
-
);
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
function cursorIndex(cursor: ZLinkDeferredJoinDeliveryCursor): number {
|
|
1331
|
-
return cursor === 'prepared' ? 1 : cursor === 'committed' ? 2 : 3;
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
function isCursor(value: unknown): value is ZLinkDeferredJoinDeliveryCursor {
|
|
1335
|
-
return value === 'prepared' || value === 'committed' || value === 'delivered';
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
function storedSnapshot(
|
|
1339
|
-
result: Extract<
|
|
1340
|
-
Awaited<ReturnType<ZLinkAuthorityStore['compareExchangeAuthority']>>,
|
|
1341
|
-
{ readonly kind: 'stored' }
|
|
1342
|
-
>
|
|
1343
|
-
): ZLinkAuthoritySnapshot {
|
|
1344
|
-
const { kind: _kind, ...snapshot } = result;
|
|
1345
|
-
return { kind: 'snapshot', ...snapshot };
|
|
1346
|
-
}
|