@agentplat/mesh 0.3.0-beta.2 → 0.3.0-beta.3
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/README.md +133 -4
- package/dist/adaptive-overlay-contracts.d.ts +149 -0
- package/dist/adaptive-overlay-contracts.d.ts.map +1 -0
- package/dist/adaptive-overlay-contracts.js +3 -0
- package/dist/adaptive-overlay-contracts.js.map +1 -0
- package/dist/adaptive-overlay-runtime.d.ts +67 -0
- package/dist/adaptive-overlay-runtime.d.ts.map +1 -0
- package/dist/adaptive-overlay-runtime.js +427 -0
- package/dist/adaptive-overlay-runtime.js.map +1 -0
- package/dist/adaptive-overlay-validation.d.ts +19 -0
- package/dist/adaptive-overlay-validation.d.ts.map +1 -0
- package/dist/adaptive-overlay-validation.js +326 -0
- package/dist/adaptive-overlay-validation.js.map +1 -0
- package/dist/adaptive-overlay.d.ts +4 -0
- package/dist/adaptive-overlay.d.ts.map +1 -0
- package/dist/adaptive-overlay.js +4 -0
- package/dist/adaptive-overlay.js.map +1 -0
- package/dist/authenticated-overlay-transport.d.ts +227 -0
- package/dist/authenticated-overlay-transport.d.ts.map +1 -0
- package/dist/authenticated-overlay-transport.js +702 -0
- package/dist/authenticated-overlay-transport.js.map +1 -0
- package/dist/authority-continuity-contracts.d.ts +249 -0
- package/dist/authority-continuity-contracts.d.ts.map +1 -0
- package/dist/authority-continuity-contracts.js +11 -0
- package/dist/authority-continuity-contracts.js.map +1 -0
- package/dist/authority-continuity.d.ts +43 -0
- package/dist/authority-continuity.d.ts.map +1 -0
- package/dist/authority-continuity.js +1338 -0
- package/dist/authority-continuity.js.map +1 -0
- package/dist/continuity.d.ts +3 -0
- package/dist/continuity.d.ts.map +1 -0
- package/dist/continuity.js +3 -0
- package/dist/continuity.js.map +1 -0
- package/dist/coordination-allocation-contracts.d.ts +6 -0
- package/dist/coordination-allocation-contracts.d.ts.map +1 -1
- package/dist/coordination-allocation-state.d.ts.map +1 -1
- package/dist/coordination-allocation-state.js +37 -7
- package/dist/coordination-allocation-state.js.map +1 -1
- package/dist/coordination-allocation.d.ts +1 -1
- package/dist/coordination-allocation.d.ts.map +1 -1
- package/dist/coordination-allocation.js +92 -11
- package/dist/coordination-allocation.js.map +1 -1
- package/dist/coordination-discovery-state.d.ts.map +1 -1
- package/dist/coordination-discovery-state.js +5 -3
- package/dist/coordination-discovery-state.js.map +1 -1
- package/dist/coordination-recovery.js +5 -1
- package/dist/coordination-recovery.js.map +1 -1
- package/dist/durability.d.ts +22 -0
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +22 -0
- package/dist/durability.js.map +1 -1
- package/dist/overlay-transport.d.ts +7 -0
- package/dist/overlay-transport.d.ts.map +1 -0
- package/dist/overlay-transport.js +7 -0
- package/dist/overlay-transport.js.map +1 -0
- package/dist/overlay.d.ts +9 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +9 -0
- package/dist/overlay.js.map +1 -0
- package/dist/sparse-overlay-contracts.d.ts +91 -0
- package/dist/sparse-overlay-contracts.d.ts.map +1 -0
- package/dist/sparse-overlay-contracts.js +2 -0
- package/dist/sparse-overlay-contracts.js.map +1 -0
- package/dist/sparse-overlay-durability.d.ts +23 -0
- package/dist/sparse-overlay-durability.d.ts.map +1 -0
- package/dist/sparse-overlay-durability.js +97 -0
- package/dist/sparse-overlay-durability.js.map +1 -0
- package/dist/sparse-overlay-runtime-contracts.d.ts +158 -0
- package/dist/sparse-overlay-runtime-contracts.d.ts.map +1 -0
- package/dist/sparse-overlay-runtime-contracts.js +2 -0
- package/dist/sparse-overlay-runtime-contracts.js.map +1 -0
- package/dist/sparse-overlay-runtime.d.ts +48 -0
- package/dist/sparse-overlay-runtime.d.ts.map +1 -0
- package/dist/sparse-overlay-runtime.js +763 -0
- package/dist/sparse-overlay-runtime.js.map +1 -0
- package/dist/sparse-overlay-transport-contracts.d.ts +183 -0
- package/dist/sparse-overlay-transport-contracts.d.ts.map +1 -0
- package/dist/sparse-overlay-transport-contracts.js +2 -0
- package/dist/sparse-overlay-transport-contracts.js.map +1 -0
- package/dist/sparse-overlay-transport-durability.d.ts +19 -0
- package/dist/sparse-overlay-transport-durability.d.ts.map +1 -0
- package/dist/sparse-overlay-transport-durability.js +91 -0
- package/dist/sparse-overlay-transport-durability.js.map +1 -0
- package/dist/sparse-overlay-transport.d.ts +97 -0
- package/dist/sparse-overlay-transport.d.ts.map +1 -0
- package/dist/sparse-overlay-transport.js +888 -0
- package/dist/sparse-overlay-transport.js.map +1 -0
- package/dist/sparse-overlay.d.ts +74 -0
- package/dist/sparse-overlay.d.ts.map +1 -0
- package/dist/sparse-overlay.js +817 -0
- package/dist/sparse-overlay.js.map +1 -0
- package/package.json +20 -4
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
import { createMeshAdaptiveOverlayAppliedViewV1, validateMeshAdaptiveOverlayCertificateV1, validateMeshAdaptiveOverlayStateV1, } from "./adaptive-overlay-validation.js";
|
|
2
|
+
import { sha256Base64Url } from "./sha256.js";
|
|
3
|
+
import { createMeshSparsePeerViewV2, createMeshSparseRoutingStateV2, publishMeshSparseUpdateV2, receiveMeshSparseDeliveryV2, refreshMeshSparseRoutingStateV2, validateMeshSparseDeliveryV2, validateMeshSparseOverlayProfileV2, validateMeshSparseRoutingStateV2, validateMeshSparseUpdateV2, } from "./sparse-overlay.js";
|
|
4
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
5
|
+
maximumPendingDeliveries: 4_096,
|
|
6
|
+
maximumPendingUpdates: 1_024,
|
|
7
|
+
maximumPendingPerNeighbor: 256,
|
|
8
|
+
maximumPendingPerTopic: 256,
|
|
9
|
+
maximumDrainItems: 128,
|
|
10
|
+
maximumCommitAttempts: 8,
|
|
11
|
+
});
|
|
12
|
+
const DIGEST = /^sha256:[A-Za-z0-9_-]{43}$/u;
|
|
13
|
+
const meshSparsePeerPlaneRuntimeInvokersV1 = new WeakMap();
|
|
14
|
+
/** Durable peer-local execution path for sparse selection, deduplication and forwarding. */
|
|
15
|
+
export class MeshSparsePeerPlaneRuntimeV1 {
|
|
16
|
+
#options;
|
|
17
|
+
#limits;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
if (!options || typeof options !== "object")
|
|
20
|
+
fail("peer plane options are required");
|
|
21
|
+
identifier(options.overlayId, "overlayId");
|
|
22
|
+
const profile = validateMeshSparseOverlayProfileV2(options.profile);
|
|
23
|
+
integer(options.topologySeed, "topologySeed", 0, 0xffff_ffff);
|
|
24
|
+
integer(options.localPeerIndex, "localPeerIndex", 0, profile.maximumPeers - 1);
|
|
25
|
+
digest(options.membershipDigest, "membershipDigest");
|
|
26
|
+
if (!options.store ||
|
|
27
|
+
typeof options.store.load !== "function" ||
|
|
28
|
+
typeof options.store.compareAndSwap !== "function")
|
|
29
|
+
fail("durable peer plane store is required");
|
|
30
|
+
if (!options.membership || typeof options.membership.resolve !== "function")
|
|
31
|
+
fail("membership projection port is required");
|
|
32
|
+
if (!options.deliveryQueue ||
|
|
33
|
+
typeof options.deliveryQueue.pending !== "function" ||
|
|
34
|
+
typeof options.deliveryQueue.enqueue !== "function")
|
|
35
|
+
fail("durable delivery queue is required");
|
|
36
|
+
if (!options.updateAdmission ||
|
|
37
|
+
typeof options.updateAdmission.pending !== "function" ||
|
|
38
|
+
typeof options.updateAdmission.admit !== "function")
|
|
39
|
+
fail("durable update admission port is required");
|
|
40
|
+
if (options.membershipCatchUp &&
|
|
41
|
+
typeof options.membershipCatchUp.catchUp !== "function")
|
|
42
|
+
fail("membership catch-up port is invalid");
|
|
43
|
+
if (options.adaptive && typeof options.adaptive.apply !== "function")
|
|
44
|
+
fail("adaptive overlay application port is invalid");
|
|
45
|
+
this.#limits = limits(options.limits);
|
|
46
|
+
const storeLoad = options.store.load.bind(options.store);
|
|
47
|
+
const storeCompareAndSwap = options.store.compareAndSwap.bind(options.store);
|
|
48
|
+
const membershipResolve = options.membership.resolve.bind(options.membership);
|
|
49
|
+
const deliveryPending = options.deliveryQueue.pending.bind(options.deliveryQueue);
|
|
50
|
+
const deliveryEnqueue = options.deliveryQueue.enqueue.bind(options.deliveryQueue);
|
|
51
|
+
const updatePending = options.updateAdmission.pending.bind(options.updateAdmission);
|
|
52
|
+
const updateAdmit = options.updateAdmission.admit.bind(options.updateAdmission);
|
|
53
|
+
const membershipCatchUp = options.membershipCatchUp?.catchUp.bind(options.membershipCatchUp);
|
|
54
|
+
const adaptiveApply = options.adaptive?.apply.bind(options.adaptive);
|
|
55
|
+
this.#options = Object.freeze({
|
|
56
|
+
overlayId: options.overlayId,
|
|
57
|
+
profile,
|
|
58
|
+
topologySeed: options.topologySeed,
|
|
59
|
+
localPeerIndex: options.localPeerIndex,
|
|
60
|
+
membershipDigest: options.membershipDigest,
|
|
61
|
+
store: Object.freeze({
|
|
62
|
+
load: storeLoad,
|
|
63
|
+
compareAndSwap: storeCompareAndSwap,
|
|
64
|
+
}),
|
|
65
|
+
membership: Object.freeze({ resolve: membershipResolve }),
|
|
66
|
+
deliveryQueue: Object.freeze({
|
|
67
|
+
pending: deliveryPending,
|
|
68
|
+
enqueue: deliveryEnqueue,
|
|
69
|
+
}),
|
|
70
|
+
updateAdmission: Object.freeze({
|
|
71
|
+
pending: updatePending,
|
|
72
|
+
admit: updateAdmit,
|
|
73
|
+
}),
|
|
74
|
+
...(membershipCatchUp
|
|
75
|
+
? {
|
|
76
|
+
membershipCatchUp: Object.freeze({ catchUp: membershipCatchUp }),
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
...(adaptiveApply
|
|
80
|
+
? { adaptive: Object.freeze({ apply: adaptiveApply }) }
|
|
81
|
+
: {}),
|
|
82
|
+
limits: this.#limits,
|
|
83
|
+
});
|
|
84
|
+
const invokers = Object.freeze({
|
|
85
|
+
restore: (logicalTime = 0) => this.#restore(logicalTime),
|
|
86
|
+
publish: (input) => this.#publish(input),
|
|
87
|
+
receive: (input) => this.#receive(input),
|
|
88
|
+
drain: (input) => this.#drain(input),
|
|
89
|
+
topologyFreshness: (logicalTime) => this.#topologyFreshness(logicalTime),
|
|
90
|
+
catchUpMembership: (logicalTime) => this.#catchUpMembership(logicalTime),
|
|
91
|
+
applyAdaptation: (input) => this.#applyAdaptation(input),
|
|
92
|
+
});
|
|
93
|
+
meshSparsePeerPlaneRuntimeInvokersV1.set(this, invokers);
|
|
94
|
+
Object.defineProperties(this, {
|
|
95
|
+
restore: immutableMethod(invokers.restore),
|
|
96
|
+
publish: immutableMethod(invokers.publish),
|
|
97
|
+
receive: immutableMethod(invokers.receive),
|
|
98
|
+
drain: immutableMethod(invokers.drain),
|
|
99
|
+
topologyFreshness: immutableMethod(invokers.topologyFreshness),
|
|
100
|
+
catchUpMembership: immutableMethod(invokers.catchUpMembership),
|
|
101
|
+
applyAdaptation: immutableMethod(invokers.applyAdaptation),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async #restore(logicalTime = 0) {
|
|
105
|
+
integer(logicalTime, "logicalTime", 0, Number.MAX_SAFE_INTEGER);
|
|
106
|
+
const existing = await this.#options.store.load(this.#options.overlayId);
|
|
107
|
+
if (existing)
|
|
108
|
+
return validateState(existing, this.#options);
|
|
109
|
+
const view = createMeshSparsePeerViewV2({
|
|
110
|
+
schemaVersion: 2,
|
|
111
|
+
profile: this.#options.profile,
|
|
112
|
+
topologySeed: this.#options.topologySeed,
|
|
113
|
+
peerIndex: this.#options.localPeerIndex,
|
|
114
|
+
});
|
|
115
|
+
const routing = createMeshSparseRoutingStateV2({
|
|
116
|
+
schemaVersion: 2,
|
|
117
|
+
profile: this.#options.profile,
|
|
118
|
+
view,
|
|
119
|
+
logicalTime,
|
|
120
|
+
});
|
|
121
|
+
const initial = buildState({
|
|
122
|
+
overlayId: this.#options.overlayId,
|
|
123
|
+
revision: 0,
|
|
124
|
+
membershipDigest: this.#options.membershipDigest,
|
|
125
|
+
profile: this.#options.profile,
|
|
126
|
+
routing,
|
|
127
|
+
pendingDeliveries: [],
|
|
128
|
+
pendingUpdates: [],
|
|
129
|
+
lastLogicalTime: logicalTime,
|
|
130
|
+
});
|
|
131
|
+
if (await this.#options.store.compareAndSwap({
|
|
132
|
+
overlayId: initial.overlayId,
|
|
133
|
+
expectedRevision: null,
|
|
134
|
+
expectedStateDigest: null,
|
|
135
|
+
next: initial,
|
|
136
|
+
}))
|
|
137
|
+
return initial;
|
|
138
|
+
const raced = await this.#options.store.load(this.#options.overlayId);
|
|
139
|
+
if (!raced)
|
|
140
|
+
fail("peer plane initialization conflict");
|
|
141
|
+
return validateState(raced, this.#options);
|
|
142
|
+
}
|
|
143
|
+
async #publish(input) {
|
|
144
|
+
const captured = Object.freeze({
|
|
145
|
+
topic: input.topic,
|
|
146
|
+
payloadDigest: input.payloadDigest,
|
|
147
|
+
logicalTime: input.logicalTime,
|
|
148
|
+
lifetime: input.lifetime,
|
|
149
|
+
fanout: input.fanout,
|
|
150
|
+
});
|
|
151
|
+
let published;
|
|
152
|
+
await this.#mutate(captured.logicalTime, (current) => {
|
|
153
|
+
const result = publishMeshSparseUpdateV2({
|
|
154
|
+
schemaVersion: 2,
|
|
155
|
+
profile: current.profile,
|
|
156
|
+
state: current.routing,
|
|
157
|
+
topic: captured.topic,
|
|
158
|
+
payloadDigest: captured.payloadDigest,
|
|
159
|
+
logicalTime: captured.logicalTime,
|
|
160
|
+
lifetime: captured.lifetime,
|
|
161
|
+
...(captured.fanout === undefined ? {} : { fanout: captured.fanout }),
|
|
162
|
+
});
|
|
163
|
+
capacity(current.pendingDeliveries.length + result.deliveries.length, this.#limits.maximumPendingDeliveries, "pending delivery");
|
|
164
|
+
published = result.update;
|
|
165
|
+
return nextState(current, {
|
|
166
|
+
routing: result.state,
|
|
167
|
+
pendingDeliveries: [
|
|
168
|
+
...current.pendingDeliveries,
|
|
169
|
+
...result.deliveries.map((delivery) => Object.freeze({
|
|
170
|
+
delivery,
|
|
171
|
+
queuedAtLogicalTime: captured.logicalTime,
|
|
172
|
+
})),
|
|
173
|
+
],
|
|
174
|
+
lastLogicalTime: captured.logicalTime,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
const drain = await this.#drain({ logicalTime: captured.logicalTime });
|
|
178
|
+
return Object.freeze({
|
|
179
|
+
update: published,
|
|
180
|
+
state: await this.#restore(),
|
|
181
|
+
drain,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async #receive(input) {
|
|
185
|
+
const delivery = validateMeshSparseDeliveryV2(this.#options.profile, input.delivery);
|
|
186
|
+
const logicalTime = input.logicalTime;
|
|
187
|
+
integer(logicalTime, "logicalTime", 0, Number.MAX_SAFE_INTEGER);
|
|
188
|
+
let accepted = false;
|
|
189
|
+
let duplicate = false;
|
|
190
|
+
let reasonCode = null;
|
|
191
|
+
const state = await this.#mutate(logicalTime, (current) => {
|
|
192
|
+
const result = receiveMeshSparseDeliveryV2({
|
|
193
|
+
schemaVersion: 2,
|
|
194
|
+
profile: current.profile,
|
|
195
|
+
state: current.routing,
|
|
196
|
+
delivery,
|
|
197
|
+
logicalTime,
|
|
198
|
+
});
|
|
199
|
+
if (!result.accepted) {
|
|
200
|
+
reasonCode = result.code;
|
|
201
|
+
return current;
|
|
202
|
+
}
|
|
203
|
+
const addUpdate = result.duplicate ? 0 : 1;
|
|
204
|
+
if (current.pendingDeliveries.length + result.deliveries.length >
|
|
205
|
+
this.#limits.maximumPendingDeliveries ||
|
|
206
|
+
current.pendingUpdates.length + addUpdate >
|
|
207
|
+
this.#limits.maximumPendingUpdates) {
|
|
208
|
+
reasonCode = "peer_plane_backpressure";
|
|
209
|
+
return current;
|
|
210
|
+
}
|
|
211
|
+
accepted = true;
|
|
212
|
+
duplicate = result.duplicate;
|
|
213
|
+
return nextState(current, {
|
|
214
|
+
routing: result.state,
|
|
215
|
+
pendingDeliveries: [
|
|
216
|
+
...current.pendingDeliveries,
|
|
217
|
+
...result.deliveries.map((delivery) => Object.freeze({ delivery, queuedAtLogicalTime: logicalTime })),
|
|
218
|
+
],
|
|
219
|
+
pendingUpdates: result.duplicate
|
|
220
|
+
? current.pendingUpdates
|
|
221
|
+
: [
|
|
222
|
+
...current.pendingUpdates,
|
|
223
|
+
Object.freeze({
|
|
224
|
+
update: delivery.update,
|
|
225
|
+
admittedAtLogicalTime: logicalTime,
|
|
226
|
+
}),
|
|
227
|
+
],
|
|
228
|
+
lastLogicalTime: logicalTime,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
const drain = accepted
|
|
232
|
+
? await this.#drain({ logicalTime })
|
|
233
|
+
: emptyDrain(state);
|
|
234
|
+
return Object.freeze({
|
|
235
|
+
accepted,
|
|
236
|
+
duplicate,
|
|
237
|
+
reasonCode,
|
|
238
|
+
state: accepted ? await this.#restore() : state,
|
|
239
|
+
drain,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
async #drain(input) {
|
|
243
|
+
integer(input.logicalTime, "logicalTime", 0, Number.MAX_SAFE_INTEGER);
|
|
244
|
+
const maximumItems = input.maximumItems ?? this.#limits.maximumDrainItems;
|
|
245
|
+
integer(maximumItems, "maximumItems", 1, this.#limits.maximumDrainItems);
|
|
246
|
+
let attempted = 0;
|
|
247
|
+
let admitted = 0;
|
|
248
|
+
let backpressured = 0;
|
|
249
|
+
let rejected = 0;
|
|
250
|
+
const blockedUpdates = new Set();
|
|
251
|
+
const blockedDeliveries = new Set();
|
|
252
|
+
while (attempted < maximumItems) {
|
|
253
|
+
const state = await this.#restore();
|
|
254
|
+
const pendingUpdate = state.pendingUpdates.find(({ update }) => !blockedUpdates.has(update.updateId));
|
|
255
|
+
if (pendingUpdate) {
|
|
256
|
+
attempted += 1;
|
|
257
|
+
if (pendingUpdate.update.expiresAtLogicalTime <= input.logicalTime) {
|
|
258
|
+
rejected += 1;
|
|
259
|
+
await this.#removePending("update", pendingUpdate.update.updateId, input.logicalTime);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
let outcome;
|
|
263
|
+
try {
|
|
264
|
+
const count = await this.#options.updateAdmission.pending(pendingUpdate.update.topic);
|
|
265
|
+
nonNegative(count, "update admission pending");
|
|
266
|
+
if (count >= this.#limits.maximumPendingPerTopic) {
|
|
267
|
+
backpressured += 1;
|
|
268
|
+
blockedUpdates.add(pendingUpdate.update.updateId);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
outcome = admission(await this.#options.updateAdmission.admit({
|
|
272
|
+
overlayId: state.overlayId,
|
|
273
|
+
update: pendingUpdate.update,
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
backpressured += 1;
|
|
278
|
+
blockedUpdates.add(pendingUpdate.update.updateId);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (outcome.status === "backpressured") {
|
|
282
|
+
backpressured += 1;
|
|
283
|
+
blockedUpdates.add(pendingUpdate.update.updateId);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (outcome.status === "rejected")
|
|
287
|
+
rejected += 1;
|
|
288
|
+
else
|
|
289
|
+
admitted += 1;
|
|
290
|
+
await this.#removePending("update", pendingUpdate.update.updateId, input.logicalTime);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const pendingDelivery = state.pendingDeliveries.find(({ delivery }) => !blockedDeliveries.has(delivery.deliveryDigest));
|
|
294
|
+
if (!pendingDelivery)
|
|
295
|
+
break;
|
|
296
|
+
attempted += 1;
|
|
297
|
+
if (pendingDelivery.delivery.update.expiresAtLogicalTime <=
|
|
298
|
+
input.logicalTime) {
|
|
299
|
+
rejected += 1;
|
|
300
|
+
await this.#removePending("delivery", pendingDelivery.delivery.deliveryDigest, input.logicalTime);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
let recipient;
|
|
304
|
+
try {
|
|
305
|
+
const slice = await this.#membershipSlice(state, [pendingDelivery.delivery.recipientPeerIndex], input.logicalTime);
|
|
306
|
+
if (slice.observedAtLogicalTime > input.logicalTime ||
|
|
307
|
+
slice.validUntilLogicalTime <= input.logicalTime) {
|
|
308
|
+
backpressured += 1;
|
|
309
|
+
blockedDeliveries.add(pendingDelivery.delivery.deliveryDigest);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
recipient = slice.peers[0];
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
backpressured += 1;
|
|
316
|
+
blockedDeliveries.add(pendingDelivery.delivery.deliveryDigest);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (!recipient || recipient.availability !== "active") {
|
|
320
|
+
backpressured += 1;
|
|
321
|
+
blockedDeliveries.add(pendingDelivery.delivery.deliveryDigest);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
let outcome;
|
|
325
|
+
try {
|
|
326
|
+
const count = await this.#options.deliveryQueue.pending(recipient.peerId);
|
|
327
|
+
nonNegative(count, "delivery queue pending");
|
|
328
|
+
if (count >= this.#limits.maximumPendingPerNeighbor) {
|
|
329
|
+
backpressured += 1;
|
|
330
|
+
blockedDeliveries.add(pendingDelivery.delivery.deliveryDigest);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
outcome = admission(await this.#options.deliveryQueue.enqueue({
|
|
334
|
+
overlayId: state.overlayId,
|
|
335
|
+
recipient,
|
|
336
|
+
delivery: pendingDelivery.delivery,
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
backpressured += 1;
|
|
341
|
+
blockedDeliveries.add(pendingDelivery.delivery.deliveryDigest);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (outcome.status === "backpressured") {
|
|
345
|
+
backpressured += 1;
|
|
346
|
+
blockedDeliveries.add(pendingDelivery.delivery.deliveryDigest);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (outcome.status === "rejected")
|
|
350
|
+
rejected += 1;
|
|
351
|
+
else
|
|
352
|
+
admitted += 1;
|
|
353
|
+
await this.#removePending("delivery", pendingDelivery.delivery.deliveryDigest, input.logicalTime);
|
|
354
|
+
}
|
|
355
|
+
const remainingState = await this.#restore();
|
|
356
|
+
return Object.freeze({
|
|
357
|
+
attempted,
|
|
358
|
+
admitted,
|
|
359
|
+
backpressured,
|
|
360
|
+
rejected,
|
|
361
|
+
remaining: remainingState.pendingDeliveries.length +
|
|
362
|
+
remainingState.pendingUpdates.length,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
async #topologyFreshness(logicalTime) {
|
|
366
|
+
try {
|
|
367
|
+
const state = await this.#restore();
|
|
368
|
+
const slice = await this.#membershipSlice(state, state.routing.view.activeNeighborIndexes, logicalTime);
|
|
369
|
+
if (slice.observedAtLogicalTime > logicalTime ||
|
|
370
|
+
slice.validUntilLogicalTime <= logicalTime)
|
|
371
|
+
return "stale";
|
|
372
|
+
return slice.peers.length ===
|
|
373
|
+
state.routing.view.activeNeighborIndexes.length &&
|
|
374
|
+
slice.peers.every((peer) => peer.availability === "active")
|
|
375
|
+
? "fresh"
|
|
376
|
+
: "stale";
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return "unknown";
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async #catchUpMembership(logicalTime) {
|
|
383
|
+
integer(logicalTime, "logicalTime", 0, Number.MAX_SAFE_INTEGER);
|
|
384
|
+
if (!this.#options.membershipCatchUp)
|
|
385
|
+
return false;
|
|
386
|
+
const state = await this.#restore();
|
|
387
|
+
const requiredPeerIndexes = Object.freeze([
|
|
388
|
+
...new Set([
|
|
389
|
+
...state.routing.view.activeNeighborIndexes,
|
|
390
|
+
...state.routing.view.reserveNeighborIndexes,
|
|
391
|
+
]),
|
|
392
|
+
].sort((left, right) => left - right));
|
|
393
|
+
return this.#options.membershipCatchUp.catchUp({
|
|
394
|
+
overlayId: state.overlayId,
|
|
395
|
+
membershipDigest: state.membershipDigest,
|
|
396
|
+
requiredPeerIndexes,
|
|
397
|
+
logicalTime,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/** Applies adaptive-overlay authorization, then durably reconciles the exact local view. */
|
|
401
|
+
async #applyAdaptation(input) {
|
|
402
|
+
if (!this.#options.adaptive)
|
|
403
|
+
fail("adaptive overlay runtime is not configured");
|
|
404
|
+
const certificate = validateMeshAdaptiveOverlayCertificateV1(input.certificate);
|
|
405
|
+
const expectedRevision = input.expectedRevision;
|
|
406
|
+
const logicalTime = input.logicalTime;
|
|
407
|
+
integer(expectedRevision, "expectedRevision", 0, Number.MAX_SAFE_INTEGER);
|
|
408
|
+
integer(logicalTime, "logicalTime", 0, Number.MAX_SAFE_INTEGER);
|
|
409
|
+
const current = await this.#restore();
|
|
410
|
+
if (certificate.binding.membershipDigest !== current.membershipDigest ||
|
|
411
|
+
(certificate.binding.viewDigest === current.routing.view.viewDigest &&
|
|
412
|
+
certificate.binding.revision !== current.routing.view.revision))
|
|
413
|
+
fail("adaptation local binding is stale");
|
|
414
|
+
const adaptation = snapshotAdaptiveOverlayResult(await this.#options.adaptive.apply({
|
|
415
|
+
certificate,
|
|
416
|
+
profile: current.profile,
|
|
417
|
+
view: current.routing.view,
|
|
418
|
+
expectedRevision,
|
|
419
|
+
logicalTimeMs: logicalTime,
|
|
420
|
+
}));
|
|
421
|
+
if (adaptation.decision !== "applied" &&
|
|
422
|
+
adaptation.decision !== "duplicate")
|
|
423
|
+
return Object.freeze({ adaptation, state: current });
|
|
424
|
+
const applied = adaptation.applied ?? adaptation.state.applied;
|
|
425
|
+
if (!applied)
|
|
426
|
+
fail("adaptive application record is missing");
|
|
427
|
+
const proposal = adaptation.state.proposals.find((candidate) => candidate.proposalDigest === input.certificate.proposalDigest);
|
|
428
|
+
if (!proposal)
|
|
429
|
+
fail("adaptive proposal is missing after application");
|
|
430
|
+
const state = await this.#mutate(logicalTime, (latest) => {
|
|
431
|
+
if (latest.routing.view.viewDigest === applied.resultingViewDigest)
|
|
432
|
+
return latest;
|
|
433
|
+
if (latest.routing.view.viewDigest !== applied.binding.viewDigest)
|
|
434
|
+
fail("adaptive view cannot be reconciled with local routing state");
|
|
435
|
+
const routing = refreshMeshSparseRoutingStateV2({
|
|
436
|
+
schemaVersion: 2,
|
|
437
|
+
profile: latest.profile,
|
|
438
|
+
state: latest.routing,
|
|
439
|
+
excludedNeighborIndexes: proposal.excludedNeighborIndexes,
|
|
440
|
+
logicalTime,
|
|
441
|
+
});
|
|
442
|
+
if (routing.view.viewDigest !== applied.resultingViewDigest)
|
|
443
|
+
fail("adaptive resulting view digest conflicts with sparse routing");
|
|
444
|
+
if (routing.view.revision !== applied.resultingRevision)
|
|
445
|
+
fail("adaptive resulting revision conflicts with sparse routing");
|
|
446
|
+
return nextState(latest, { routing, lastLogicalTime: logicalTime });
|
|
447
|
+
});
|
|
448
|
+
return Object.freeze({ adaptation, state });
|
|
449
|
+
}
|
|
450
|
+
async #mutate(logicalTime, operation) {
|
|
451
|
+
integer(logicalTime, "logicalTime", 0, Number.MAX_SAFE_INTEGER);
|
|
452
|
+
for (let attempt = 0; attempt < this.#limits.maximumCommitAttempts; attempt += 1) {
|
|
453
|
+
const current = await this.#restore(logicalTime);
|
|
454
|
+
if (logicalTime < current.lastLogicalTime)
|
|
455
|
+
fail("peer plane logical time regressed");
|
|
456
|
+
if (current.revision === Number.MAX_SAFE_INTEGER)
|
|
457
|
+
fail("peer plane revision is exhausted");
|
|
458
|
+
const next = operation(current);
|
|
459
|
+
if (next === current)
|
|
460
|
+
return current;
|
|
461
|
+
if (await this.#options.store.compareAndSwap({
|
|
462
|
+
overlayId: current.overlayId,
|
|
463
|
+
expectedRevision: current.revision,
|
|
464
|
+
expectedStateDigest: current.stateDigest,
|
|
465
|
+
next,
|
|
466
|
+
}))
|
|
467
|
+
return next;
|
|
468
|
+
}
|
|
469
|
+
fail("peer plane compare-and-swap attempts exhausted");
|
|
470
|
+
}
|
|
471
|
+
async #removePending(kind, identity, logicalTime) {
|
|
472
|
+
await this.#mutate(logicalTime, (current) => {
|
|
473
|
+
const pendingDeliveries = kind === "delivery"
|
|
474
|
+
? current.pendingDeliveries.filter((entry) => entry.delivery.deliveryDigest !== identity)
|
|
475
|
+
: current.pendingDeliveries;
|
|
476
|
+
const pendingUpdates = kind === "update"
|
|
477
|
+
? current.pendingUpdates.filter((entry) => entry.update.updateId !== identity)
|
|
478
|
+
: current.pendingUpdates;
|
|
479
|
+
if (pendingDeliveries.length === current.pendingDeliveries.length &&
|
|
480
|
+
pendingUpdates.length === current.pendingUpdates.length)
|
|
481
|
+
return current;
|
|
482
|
+
return nextState(current, {
|
|
483
|
+
pendingDeliveries,
|
|
484
|
+
pendingUpdates,
|
|
485
|
+
lastLogicalTime: Math.max(current.lastLogicalTime, logicalTime),
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async #membershipSlice(state, peerIndexes, logicalTime) {
|
|
490
|
+
const requested = Object.freeze([...new Set(peerIndexes)].sort((left, right) => left - right));
|
|
491
|
+
const slice = await this.#options.membership.resolve({
|
|
492
|
+
overlayId: state.overlayId,
|
|
493
|
+
membershipDigest: state.membershipDigest,
|
|
494
|
+
peerIndexes: requested,
|
|
495
|
+
logicalTime,
|
|
496
|
+
});
|
|
497
|
+
return validateSlice(slice, state, requested);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/** Nominal check for the module-owned sparse peer plane runtime. */
|
|
501
|
+
export function isMeshSparsePeerPlaneRuntimeV1(value) {
|
|
502
|
+
return (typeof value === "object" &&
|
|
503
|
+
value !== null &&
|
|
504
|
+
meshSparsePeerPlaneRuntimeInvokersV1.has(value));
|
|
505
|
+
}
|
|
506
|
+
/** Invokes the construction-time publish implementation. */
|
|
507
|
+
export function invokeMeshSparsePeerPlanePublishV1(runtime, input) {
|
|
508
|
+
return requireMeshSparsePeerPlaneInvokers(runtime).publish(input);
|
|
509
|
+
}
|
|
510
|
+
/** Invokes the construction-time sparse admission implementation. */
|
|
511
|
+
export function invokeMeshSparsePeerPlaneReceiveV1(runtime, input) {
|
|
512
|
+
return requireMeshSparsePeerPlaneInvokers(runtime).receive(input);
|
|
513
|
+
}
|
|
514
|
+
function requireMeshSparsePeerPlaneInvokers(runtime) {
|
|
515
|
+
const invokers = meshSparsePeerPlaneRuntimeInvokersV1.get(runtime);
|
|
516
|
+
if (!invokers)
|
|
517
|
+
fail("peer plane runtime is not genuine");
|
|
518
|
+
return invokers;
|
|
519
|
+
}
|
|
520
|
+
function buildState(input) {
|
|
521
|
+
const body = { schemaVersion: 1, ...input };
|
|
522
|
+
return deepFreeze({ ...body, stateDigest: contentDigest(body) });
|
|
523
|
+
}
|
|
524
|
+
function nextState(state, changes) {
|
|
525
|
+
return buildState({
|
|
526
|
+
overlayId: state.overlayId,
|
|
527
|
+
revision: state.revision + 1,
|
|
528
|
+
membershipDigest: state.membershipDigest,
|
|
529
|
+
profile: state.profile,
|
|
530
|
+
routing: changes.routing ?? state.routing,
|
|
531
|
+
pendingDeliveries: changes.pendingDeliveries ?? state.pendingDeliveries,
|
|
532
|
+
pendingUpdates: changes.pendingUpdates ?? state.pendingUpdates,
|
|
533
|
+
lastLogicalTime: changes.lastLogicalTime ?? state.lastLogicalTime,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function validateState(input, options) {
|
|
537
|
+
if (!input ||
|
|
538
|
+
input.schemaVersion !== 1 ||
|
|
539
|
+
input.overlayId !== options.overlayId)
|
|
540
|
+
fail("peer plane state binding is invalid");
|
|
541
|
+
integer(input.revision, "state.revision", 0, Number.MAX_SAFE_INTEGER);
|
|
542
|
+
if (input.membershipDigest !== options.membershipDigest)
|
|
543
|
+
fail("peer plane membership binding is invalid");
|
|
544
|
+
const profile = validateMeshSparseOverlayProfileV2(input.profile);
|
|
545
|
+
if (profile.profileDigest !== options.profile.profileDigest)
|
|
546
|
+
fail("peer plane profile binding is invalid");
|
|
547
|
+
const routing = validateMeshSparseRoutingStateV2(profile, input.routing);
|
|
548
|
+
if (routing.view.peerIndex !== options.localPeerIndex ||
|
|
549
|
+
routing.view.topologySeed !== options.topologySeed)
|
|
550
|
+
fail("peer plane local view binding is invalid");
|
|
551
|
+
if (!Array.isArray(input.pendingDeliveries) ||
|
|
552
|
+
!Array.isArray(input.pendingUpdates))
|
|
553
|
+
fail("peer plane pending state is invalid");
|
|
554
|
+
const configuredLimits = limits(options.limits);
|
|
555
|
+
capacity(input.pendingDeliveries.length, configuredLimits.maximumPendingDeliveries, "pending delivery");
|
|
556
|
+
capacity(input.pendingUpdates.length, configuredLimits.maximumPendingUpdates, "pending update");
|
|
557
|
+
const deliveryIds = new Set();
|
|
558
|
+
const pendingDeliveries = input.pendingDeliveries.map((entry) => {
|
|
559
|
+
const delivery = validateMeshSparseDeliveryV2(profile, entry.delivery);
|
|
560
|
+
integer(entry.queuedAtLogicalTime, "pending delivery logical time", 0, Number.MAX_SAFE_INTEGER);
|
|
561
|
+
if (deliveryIds.has(delivery.deliveryDigest))
|
|
562
|
+
fail("pending delivery identity is duplicated");
|
|
563
|
+
deliveryIds.add(delivery.deliveryDigest);
|
|
564
|
+
return Object.freeze({
|
|
565
|
+
delivery,
|
|
566
|
+
queuedAtLogicalTime: entry.queuedAtLogicalTime,
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
const updateIds = new Set();
|
|
570
|
+
const pendingUpdates = input.pendingUpdates.map((entry) => {
|
|
571
|
+
const update = validateMeshSparseUpdateV2(profile, entry.update);
|
|
572
|
+
integer(entry.admittedAtLogicalTime, "pending update logical time", 0, Number.MAX_SAFE_INTEGER);
|
|
573
|
+
if (updateIds.has(update.updateId))
|
|
574
|
+
fail("pending update identity is duplicated");
|
|
575
|
+
updateIds.add(update.updateId);
|
|
576
|
+
return Object.freeze({
|
|
577
|
+
update,
|
|
578
|
+
admittedAtLogicalTime: entry.admittedAtLogicalTime,
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
integer(input.lastLogicalTime, "state.lastLogicalTime", 0, Number.MAX_SAFE_INTEGER);
|
|
582
|
+
if (input.lastLogicalTime < routing.lastLogicalTime)
|
|
583
|
+
fail("peer plane state time precedes routing state");
|
|
584
|
+
const rebuilt = buildState({
|
|
585
|
+
overlayId: input.overlayId,
|
|
586
|
+
revision: input.revision,
|
|
587
|
+
membershipDigest: input.membershipDigest,
|
|
588
|
+
profile,
|
|
589
|
+
routing,
|
|
590
|
+
pendingDeliveries,
|
|
591
|
+
pendingUpdates,
|
|
592
|
+
lastLogicalTime: input.lastLogicalTime,
|
|
593
|
+
});
|
|
594
|
+
if (rebuilt.stateDigest !== input.stateDigest)
|
|
595
|
+
fail("peer plane state digest is invalid");
|
|
596
|
+
return rebuilt;
|
|
597
|
+
}
|
|
598
|
+
function validateSlice(slice, state, requested) {
|
|
599
|
+
if (!slice ||
|
|
600
|
+
slice.schemaVersion !== 1 ||
|
|
601
|
+
slice.overlayId !== state.overlayId ||
|
|
602
|
+
slice.membershipDigest !== state.membershipDigest)
|
|
603
|
+
fail("membership slice binding is invalid");
|
|
604
|
+
integer(slice.observedAtLogicalTime, "membership observedAtLogicalTime", 0, Number.MAX_SAFE_INTEGER);
|
|
605
|
+
integer(slice.validUntilLogicalTime, "membership validUntilLogicalTime", 1, Number.MAX_SAFE_INTEGER);
|
|
606
|
+
if (slice.validUntilLogicalTime <= slice.observedAtLogicalTime)
|
|
607
|
+
fail("membership slice lifetime is invalid");
|
|
608
|
+
if (!Array.isArray(slice.peers) || slice.peers.length > requested.length)
|
|
609
|
+
fail("membership slice is not bounded by the request");
|
|
610
|
+
const allowed = new Set(requested);
|
|
611
|
+
const seen = new Set();
|
|
612
|
+
const peers = slice.peers.map((peer) => {
|
|
613
|
+
integer(peer.peerIndex, "membership peerIndex", 0, state.profile.maximumPeers - 1);
|
|
614
|
+
if (!allowed.has(peer.peerIndex) || seen.has(peer.peerIndex))
|
|
615
|
+
fail("membership slice contains an unrequested or duplicate peer");
|
|
616
|
+
seen.add(peer.peerIndex);
|
|
617
|
+
identifier(peer.peerId, "membership peerId");
|
|
618
|
+
if (peer.availability !== "active" &&
|
|
619
|
+
peer.availability !== "suspect" &&
|
|
620
|
+
peer.availability !== "departed")
|
|
621
|
+
fail("membership availability is invalid");
|
|
622
|
+
return Object.freeze({
|
|
623
|
+
peerIndex: peer.peerIndex,
|
|
624
|
+
peerId: peer.peerId,
|
|
625
|
+
availability: peer.availability,
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
return Object.freeze({
|
|
629
|
+
schemaVersion: 1,
|
|
630
|
+
overlayId: slice.overlayId,
|
|
631
|
+
membershipDigest: slice.membershipDigest,
|
|
632
|
+
observedAtLogicalTime: slice.observedAtLogicalTime,
|
|
633
|
+
validUntilLogicalTime: slice.validUntilLogicalTime,
|
|
634
|
+
peers: Object.freeze(peers),
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
function limits(input) {
|
|
638
|
+
const result = { ...DEFAULT_LIMITS, ...input };
|
|
639
|
+
for (const [key, value] of Object.entries(result))
|
|
640
|
+
integer(value, key, 1, 1_000_000);
|
|
641
|
+
return Object.freeze(result);
|
|
642
|
+
}
|
|
643
|
+
function snapshotAdaptiveOverlayResult(input) {
|
|
644
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
645
|
+
fail("adaptive overlay result is invalid");
|
|
646
|
+
const snapshot = structuredClone(input);
|
|
647
|
+
if (![
|
|
648
|
+
"observed",
|
|
649
|
+
"certified",
|
|
650
|
+
"applied",
|
|
651
|
+
"duplicate",
|
|
652
|
+
"stale",
|
|
653
|
+
"conflict",
|
|
654
|
+
"rejected",
|
|
655
|
+
].includes(snapshot.decision) ||
|
|
656
|
+
typeof snapshot.reasonCode !== "string" ||
|
|
657
|
+
!/^[a-z0-9][a-z0-9_.:-]{0,127}$/u.test(snapshot.reasonCode))
|
|
658
|
+
fail("adaptive overlay result disposition is invalid");
|
|
659
|
+
const state = validateMeshAdaptiveOverlayStateV1(snapshot.state);
|
|
660
|
+
const certificate = snapshot.certificate
|
|
661
|
+
? validateMeshAdaptiveOverlayCertificateV1(snapshot.certificate)
|
|
662
|
+
: undefined;
|
|
663
|
+
let applied = snapshot.applied;
|
|
664
|
+
if (applied) {
|
|
665
|
+
const { schemaVersion: _schemaVersion, applicationDigest, ...body } = applied;
|
|
666
|
+
const rebuilt = createMeshAdaptiveOverlayAppliedViewV1(body);
|
|
667
|
+
if (rebuilt.applicationDigest !== applicationDigest)
|
|
668
|
+
fail("adaptive overlay applied result digest is invalid");
|
|
669
|
+
applied = rebuilt;
|
|
670
|
+
}
|
|
671
|
+
if ((snapshot.decision === "applied" || snapshot.decision === "duplicate") &&
|
|
672
|
+
!(applied ?? state.applied))
|
|
673
|
+
fail("adaptive overlay applied result is missing");
|
|
674
|
+
if (certificate && certificate.policyDigest !== state.policyDigest)
|
|
675
|
+
fail("adaptive overlay result policy binding changed");
|
|
676
|
+
return deepFreeze({
|
|
677
|
+
decision: snapshot.decision,
|
|
678
|
+
reasonCode: snapshot.reasonCode,
|
|
679
|
+
state,
|
|
680
|
+
...(certificate ? { certificate } : {}),
|
|
681
|
+
...(applied ? { applied } : {}),
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
function admission(input) {
|
|
685
|
+
if (!input ||
|
|
686
|
+
(input.status !== "admitted" &&
|
|
687
|
+
input.status !== "duplicate" &&
|
|
688
|
+
input.status !== "backpressured" &&
|
|
689
|
+
input.status !== "rejected"))
|
|
690
|
+
fail("durable admission result is invalid");
|
|
691
|
+
return input;
|
|
692
|
+
}
|
|
693
|
+
function emptyDrain(state) {
|
|
694
|
+
return Object.freeze({
|
|
695
|
+
attempted: 0,
|
|
696
|
+
admitted: 0,
|
|
697
|
+
backpressured: 0,
|
|
698
|
+
rejected: 0,
|
|
699
|
+
remaining: state.pendingDeliveries.length + state.pendingUpdates.length,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function capacity(value, maximum, label) {
|
|
703
|
+
if (value > maximum)
|
|
704
|
+
fail(`${label} capacity exceeded`);
|
|
705
|
+
}
|
|
706
|
+
function contentDigest(value) {
|
|
707
|
+
return `sha256:${sha256Base64Url(new TextEncoder().encode(canonical(value)))}`;
|
|
708
|
+
}
|
|
709
|
+
function canonical(value) {
|
|
710
|
+
if (value === undefined)
|
|
711
|
+
fail("undefined is not canonical JSON");
|
|
712
|
+
if (value === null || typeof value !== "object") {
|
|
713
|
+
const serialized = JSON.stringify(value);
|
|
714
|
+
if (serialized === undefined)
|
|
715
|
+
fail("value is not canonical JSON");
|
|
716
|
+
return serialized;
|
|
717
|
+
}
|
|
718
|
+
if (Array.isArray(value))
|
|
719
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
720
|
+
const record = value;
|
|
721
|
+
return `{${Object.keys(record)
|
|
722
|
+
.sort()
|
|
723
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`)
|
|
724
|
+
.join(",")}}`;
|
|
725
|
+
}
|
|
726
|
+
function deepFreeze(value) {
|
|
727
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
728
|
+
Object.freeze(value);
|
|
729
|
+
for (const child of Object.values(value))
|
|
730
|
+
deepFreeze(child);
|
|
731
|
+
}
|
|
732
|
+
return value;
|
|
733
|
+
}
|
|
734
|
+
function identifier(value, label) {
|
|
735
|
+
if (typeof value !== "string" ||
|
|
736
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/u.test(value))
|
|
737
|
+
fail(`${label} is invalid`);
|
|
738
|
+
}
|
|
739
|
+
function digest(value, label) {
|
|
740
|
+
if (typeof value !== "string" || !DIGEST.test(value))
|
|
741
|
+
fail(`${label} is invalid`);
|
|
742
|
+
}
|
|
743
|
+
function integer(value, label, minimum, maximum) {
|
|
744
|
+
if (!Number.isSafeInteger(value) ||
|
|
745
|
+
value < minimum ||
|
|
746
|
+
value > maximum)
|
|
747
|
+
fail(`${label} is invalid`);
|
|
748
|
+
}
|
|
749
|
+
function nonNegative(value, label) {
|
|
750
|
+
integer(value, label, 0, Number.MAX_SAFE_INTEGER);
|
|
751
|
+
}
|
|
752
|
+
function immutableMethod(value) {
|
|
753
|
+
return {
|
|
754
|
+
value,
|
|
755
|
+
writable: false,
|
|
756
|
+
configurable: false,
|
|
757
|
+
enumerable: false,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function fail(message) {
|
|
761
|
+
throw new TypeError(message);
|
|
762
|
+
}
|
|
763
|
+
//# sourceMappingURL=sparse-overlay-runtime.js.map
|