@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.
Files changed (92) hide show
  1. package/README.md +133 -4
  2. package/dist/adaptive-overlay-contracts.d.ts +149 -0
  3. package/dist/adaptive-overlay-contracts.d.ts.map +1 -0
  4. package/dist/adaptive-overlay-contracts.js +3 -0
  5. package/dist/adaptive-overlay-contracts.js.map +1 -0
  6. package/dist/adaptive-overlay-runtime.d.ts +67 -0
  7. package/dist/adaptive-overlay-runtime.d.ts.map +1 -0
  8. package/dist/adaptive-overlay-runtime.js +427 -0
  9. package/dist/adaptive-overlay-runtime.js.map +1 -0
  10. package/dist/adaptive-overlay-validation.d.ts +19 -0
  11. package/dist/adaptive-overlay-validation.d.ts.map +1 -0
  12. package/dist/adaptive-overlay-validation.js +326 -0
  13. package/dist/adaptive-overlay-validation.js.map +1 -0
  14. package/dist/adaptive-overlay.d.ts +4 -0
  15. package/dist/adaptive-overlay.d.ts.map +1 -0
  16. package/dist/adaptive-overlay.js +4 -0
  17. package/dist/adaptive-overlay.js.map +1 -0
  18. package/dist/authenticated-overlay-transport.d.ts +227 -0
  19. package/dist/authenticated-overlay-transport.d.ts.map +1 -0
  20. package/dist/authenticated-overlay-transport.js +702 -0
  21. package/dist/authenticated-overlay-transport.js.map +1 -0
  22. package/dist/authority-continuity-contracts.d.ts +249 -0
  23. package/dist/authority-continuity-contracts.d.ts.map +1 -0
  24. package/dist/authority-continuity-contracts.js +11 -0
  25. package/dist/authority-continuity-contracts.js.map +1 -0
  26. package/dist/authority-continuity.d.ts +43 -0
  27. package/dist/authority-continuity.d.ts.map +1 -0
  28. package/dist/authority-continuity.js +1338 -0
  29. package/dist/authority-continuity.js.map +1 -0
  30. package/dist/continuity.d.ts +3 -0
  31. package/dist/continuity.d.ts.map +1 -0
  32. package/dist/continuity.js +3 -0
  33. package/dist/continuity.js.map +1 -0
  34. package/dist/coordination-allocation-contracts.d.ts +6 -0
  35. package/dist/coordination-allocation-contracts.d.ts.map +1 -1
  36. package/dist/coordination-allocation-state.d.ts.map +1 -1
  37. package/dist/coordination-allocation-state.js +37 -7
  38. package/dist/coordination-allocation-state.js.map +1 -1
  39. package/dist/coordination-allocation.d.ts +1 -1
  40. package/dist/coordination-allocation.d.ts.map +1 -1
  41. package/dist/coordination-allocation.js +92 -11
  42. package/dist/coordination-allocation.js.map +1 -1
  43. package/dist/coordination-discovery-state.d.ts.map +1 -1
  44. package/dist/coordination-discovery-state.js +5 -3
  45. package/dist/coordination-discovery-state.js.map +1 -1
  46. package/dist/coordination-recovery.js +5 -1
  47. package/dist/coordination-recovery.js.map +1 -1
  48. package/dist/durability.d.ts +22 -0
  49. package/dist/durability.d.ts.map +1 -1
  50. package/dist/durability.js +22 -0
  51. package/dist/durability.js.map +1 -1
  52. package/dist/overlay-transport.d.ts +7 -0
  53. package/dist/overlay-transport.d.ts.map +1 -0
  54. package/dist/overlay-transport.js +7 -0
  55. package/dist/overlay-transport.js.map +1 -0
  56. package/dist/overlay.d.ts +9 -0
  57. package/dist/overlay.d.ts.map +1 -0
  58. package/dist/overlay.js +9 -0
  59. package/dist/overlay.js.map +1 -0
  60. package/dist/sparse-overlay-contracts.d.ts +91 -0
  61. package/dist/sparse-overlay-contracts.d.ts.map +1 -0
  62. package/dist/sparse-overlay-contracts.js +2 -0
  63. package/dist/sparse-overlay-contracts.js.map +1 -0
  64. package/dist/sparse-overlay-durability.d.ts +23 -0
  65. package/dist/sparse-overlay-durability.d.ts.map +1 -0
  66. package/dist/sparse-overlay-durability.js +97 -0
  67. package/dist/sparse-overlay-durability.js.map +1 -0
  68. package/dist/sparse-overlay-runtime-contracts.d.ts +158 -0
  69. package/dist/sparse-overlay-runtime-contracts.d.ts.map +1 -0
  70. package/dist/sparse-overlay-runtime-contracts.js +2 -0
  71. package/dist/sparse-overlay-runtime-contracts.js.map +1 -0
  72. package/dist/sparse-overlay-runtime.d.ts +48 -0
  73. package/dist/sparse-overlay-runtime.d.ts.map +1 -0
  74. package/dist/sparse-overlay-runtime.js +763 -0
  75. package/dist/sparse-overlay-runtime.js.map +1 -0
  76. package/dist/sparse-overlay-transport-contracts.d.ts +183 -0
  77. package/dist/sparse-overlay-transport-contracts.d.ts.map +1 -0
  78. package/dist/sparse-overlay-transport-contracts.js +2 -0
  79. package/dist/sparse-overlay-transport-contracts.js.map +1 -0
  80. package/dist/sparse-overlay-transport-durability.d.ts +19 -0
  81. package/dist/sparse-overlay-transport-durability.d.ts.map +1 -0
  82. package/dist/sparse-overlay-transport-durability.js +91 -0
  83. package/dist/sparse-overlay-transport-durability.js.map +1 -0
  84. package/dist/sparse-overlay-transport.d.ts +97 -0
  85. package/dist/sparse-overlay-transport.d.ts.map +1 -0
  86. package/dist/sparse-overlay-transport.js +888 -0
  87. package/dist/sparse-overlay-transport.js.map +1 -0
  88. package/dist/sparse-overlay.d.ts +74 -0
  89. package/dist/sparse-overlay.d.ts.map +1 -0
  90. package/dist/sparse-overlay.js +817 -0
  91. package/dist/sparse-overlay.js.map +1 -0
  92. package/package.json +20 -4
@@ -0,0 +1,817 @@
1
+ import { sha256Base64Url } from './sha256.js';
2
+ export const MESH_SPARSE_OVERLAY_SCHEMA_VERSION_V2 = 2;
3
+ export const MESH_SPARSE_OVERLAY_PROFILE_IDS_V2 = Object.freeze([
4
+ 'standard-500',
5
+ 'large-5000',
6
+ 'frontier-100000',
7
+ ]);
8
+ const MAXIMUM_UPDATE_LIFETIME_V2 = 86_400_000;
9
+ const DIGEST = /^sha256:[A-Za-z0-9_-]{43}$/u;
10
+ const UPDATE_ID = /^overlay-update:[A-Za-z0-9_-]{43}$/u;
11
+ const TOPIC = /^[a-z0-9](?:[a-z0-9._/-]{0,126}[a-z0-9])?$/u;
12
+ const utf8 = new TextEncoder();
13
+ const emptyDeliveries = Object.freeze([]);
14
+ const profileIds = new Set(MESH_SPARSE_OVERLAY_PROFILE_IDS_V2);
15
+ const profileKeys = [
16
+ 'activeNeighborCount',
17
+ 'maximumFanout',
18
+ 'maximumHops',
19
+ 'maximumInteractions',
20
+ 'maximumPeers',
21
+ 'maximumRecentUpdates',
22
+ 'profileDigest',
23
+ 'profileId',
24
+ 'reserveNeighborCount',
25
+ 'schemaVersion',
26
+ ];
27
+ const viewKeys = [
28
+ 'activeNeighborIndexes',
29
+ 'excludedNeighborIndexes',
30
+ 'peerId',
31
+ 'peerIndex',
32
+ 'profileDigest',
33
+ 'reserveNeighborIndexes',
34
+ 'revision',
35
+ 'schemaVersion',
36
+ 'topologySeed',
37
+ 'viewDigest',
38
+ ];
39
+ const stateKeys = [
40
+ 'acceptedRemoteUpdates',
41
+ 'lastLogicalTime',
42
+ 'maximumOutboundInteractions',
43
+ 'nextOriginSequence',
44
+ 'outboundInteractions',
45
+ 'profileDigest',
46
+ 'recentUpdates',
47
+ 'schemaVersion',
48
+ 'stateDigest',
49
+ 'view',
50
+ ];
51
+ const updateKeys = [
52
+ 'createdAtLogicalTime',
53
+ 'expiresAtLogicalTime',
54
+ 'maximumHops',
55
+ 'originPeerId',
56
+ 'originPeerIndex',
57
+ 'originSequence',
58
+ 'payloadDigest',
59
+ 'profileDigest',
60
+ 'schemaVersion',
61
+ 'topic',
62
+ 'updateDigest',
63
+ 'updateId',
64
+ ];
65
+ const deliveryKeys = [
66
+ 'deliveryDigest',
67
+ 'hop',
68
+ 'previousDeliveryDigest',
69
+ 'recipientPeerIndex',
70
+ 'schemaVersion',
71
+ 'senderPeerIndex',
72
+ 'senderViewDigest',
73
+ 'update',
74
+ ];
75
+ function profileBody(profileId) {
76
+ const maximumPeers = profileId === 'standard-500'
77
+ ? 500
78
+ : profileId === 'large-5000'
79
+ ? 5_000
80
+ : 100_000;
81
+ const maximumInteractions = profileId === 'standard-500'
82
+ ? 5_000
83
+ : profileId === 'large-5000'
84
+ ? 50_000
85
+ : 1_000_000;
86
+ const activeNeighborCount = Math.ceil(Math.log2(maximumPeers));
87
+ return {
88
+ schemaVersion: 2,
89
+ profileId,
90
+ maximumPeers: maximumPeers,
91
+ maximumInteractions: maximumInteractions,
92
+ activeNeighborCount,
93
+ reserveNeighborCount: activeNeighborCount,
94
+ maximumFanout: 2,
95
+ maximumHops: activeNeighborCount * 2,
96
+ maximumRecentUpdates: activeNeighborCount * 128,
97
+ };
98
+ }
99
+ /** Returns one of the closed, immutable overlay scale profiles. */
100
+ export function meshSparseOverlayProfileV2(profileId) {
101
+ requireProfileId(profileId);
102
+ const body = profileBody(profileId);
103
+ return freeze({
104
+ ...body,
105
+ profileDigest: digest(body),
106
+ });
107
+ }
108
+ export const MESH_SPARSE_OVERLAY_PROFILES_V2 = Object.freeze(MESH_SPARSE_OVERLAY_PROFILE_IDS_V2.map((profileId) => meshSparseOverlayProfileV2(profileId)));
109
+ export function validateMeshSparseOverlayProfileV2(input) {
110
+ const value = exactRecord(input, profileKeys, [], 'sparse overlay profile');
111
+ requireProfileId(value.profileId);
112
+ const rebuilt = meshSparseOverlayProfileV2(value.profileId);
113
+ if (!same(value, rebuilt))
114
+ fail('sparse overlay profile binding is invalid');
115
+ return rebuilt;
116
+ }
117
+ /** Derives one peer's O(log N) active and reserve view without a global graph. */
118
+ export function createMeshSparsePeerViewV2(input) {
119
+ exactRecord(input, ['peerIndex', 'profile', 'schemaVersion', 'topologySeed'], ['excludedNeighborIndexes', 'revision'], 'sparse peer view input');
120
+ if (input.schemaVersion !== 2)
121
+ fail('sparse peer view schema is invalid');
122
+ const profile = validateMeshSparseOverlayProfileV2(input.profile);
123
+ safeInteger(input.topologySeed, 'topologySeed', 0, 0xffff_ffff);
124
+ safeInteger(input.peerIndex, 'peerIndex', 0, profile.maximumPeers - 1);
125
+ const revision = input.revision ?? 0;
126
+ safeInteger(revision, 'revision', 0, Number.MAX_SAFE_INTEGER);
127
+ const excluded = normalizeIndexes(input.excludedNeighborIndexes ?? [], profile, input.peerIndex, profile.activeNeighborCount + profile.reserveNeighborCount, 'excludedNeighborIndexes');
128
+ return buildView(profile, input.topologySeed, input.peerIndex, revision, excluded);
129
+ }
130
+ export function validateMeshSparsePeerViewV2(profileInput, input) {
131
+ const profile = validateMeshSparseOverlayProfileV2(profileInput);
132
+ const value = exactRecord(input, viewKeys, [], 'sparse peer view');
133
+ if (value.schemaVersion !== 2)
134
+ fail('sparse peer view schema is invalid');
135
+ if (value.profileDigest !== profile.profileDigest)
136
+ fail('sparse peer view profile binding is invalid');
137
+ safeInteger(value.topologySeed, 'topologySeed', 0, 0xffff_ffff);
138
+ safeInteger(value.peerIndex, 'peerIndex', 0, profile.maximumPeers - 1);
139
+ safeInteger(value.revision, 'revision', 0, Number.MAX_SAFE_INTEGER);
140
+ if (value.peerId !== peerId(profile, value.peerIndex))
141
+ fail('sparse peer view peer identity is invalid');
142
+ const excluded = normalizeIndexes(value.excludedNeighborIndexes, profile, value.peerIndex, profile.activeNeighborCount + profile.reserveNeighborCount, 'excludedNeighborIndexes');
143
+ const active = normalizeIndexes(value.activeNeighborIndexes, profile, value.peerIndex, profile.activeNeighborCount, 'activeNeighborIndexes', false);
144
+ const reserve = normalizeIndexes(value.reserveNeighborIndexes, profile, value.peerIndex, profile.reserveNeighborCount, 'reserveNeighborIndexes', false);
145
+ if (active.length !== profile.activeNeighborCount ||
146
+ reserve.length !== profile.reserveNeighborCount)
147
+ fail('sparse peer view neighbor count is invalid');
148
+ const combined = new Set([...active, ...reserve]);
149
+ if (combined.size !== active.length + reserve.length)
150
+ fail('sparse peer view neighbors overlap');
151
+ if ([...combined].some((index) => excluded.includes(index)))
152
+ fail('sparse peer view contains an excluded neighbor');
153
+ const rebuilt = buildView(profile, value.topologySeed, value.peerIndex, value.revision, excluded);
154
+ if (!same(value, rebuilt))
155
+ fail('sparse peer view binding is invalid');
156
+ return rebuilt;
157
+ }
158
+ /** Promotes reserves and deterministically refills one local view. */
159
+ export function refreshMeshSparsePeerViewV2(input) {
160
+ exactRecord(input, ['excludedNeighborIndexes', 'profile', 'schemaVersion', 'view'], [], 'sparse peer view refresh input');
161
+ if (input.schemaVersion !== 2)
162
+ fail('sparse peer view refresh schema is invalid');
163
+ const profile = validateMeshSparseOverlayProfileV2(input.profile);
164
+ const view = validateMeshSparsePeerViewV2(profile, input.view);
165
+ if (view.revision === Number.MAX_SAFE_INTEGER)
166
+ fail('sparse peer view revision is exhausted');
167
+ const excluded = normalizeIndexes(input.excludedNeighborIndexes, profile, view.peerIndex, profile.activeNeighborCount + profile.reserveNeighborCount, 'excludedNeighborIndexes');
168
+ return buildView(profile, view.topologySeed, view.peerIndex, view.revision + 1, excluded);
169
+ }
170
+ /** Constructs a bounded local routing state and exact outbound quota. */
171
+ export function createMeshSparseRoutingStateV2(input) {
172
+ exactRecord(input, ['profile', 'schemaVersion', 'view'], ['logicalTime'], 'sparse routing state input');
173
+ if (input.schemaVersion !== 2)
174
+ fail('sparse routing state schema is invalid');
175
+ const profile = validateMeshSparseOverlayProfileV2(input.profile);
176
+ const view = validateMeshSparsePeerViewV2(profile, input.view);
177
+ const logicalTime = input.logicalTime ?? 0;
178
+ safeInteger(logicalTime, 'logicalTime', 0, Number.MAX_SAFE_INTEGER);
179
+ return buildState({
180
+ profile,
181
+ view,
182
+ recentUpdates: [],
183
+ nextOriginSequence: 1,
184
+ maximumOutboundInteractions: outboundQuota(profile, view.peerIndex),
185
+ outboundInteractions: 0,
186
+ acceptedRemoteUpdates: 0,
187
+ lastLogicalTime: logicalTime,
188
+ });
189
+ }
190
+ export function validateMeshSparseRoutingStateV2(profileInput, input) {
191
+ const profile = validateMeshSparseOverlayProfileV2(profileInput);
192
+ const value = exactRecord(input, stateKeys, [], 'sparse routing state');
193
+ if (value.schemaVersion !== 2)
194
+ fail('sparse routing state schema is invalid');
195
+ if (value.profileDigest !== profile.profileDigest)
196
+ fail('sparse routing state profile binding is invalid');
197
+ const view = validateMeshSparsePeerViewV2(profile, value.view);
198
+ safeInteger(value.nextOriginSequence, 'nextOriginSequence', 1, Number.MAX_SAFE_INTEGER);
199
+ safeInteger(value.maximumOutboundInteractions, 'maximumOutboundInteractions', 0, profile.maximumInteractions);
200
+ if (value.maximumOutboundInteractions !== outboundQuota(profile, view.peerIndex))
201
+ fail('sparse routing outbound quota is invalid');
202
+ safeInteger(value.outboundInteractions, 'outboundInteractions', 0, value.maximumOutboundInteractions);
203
+ safeInteger(value.acceptedRemoteUpdates, 'acceptedRemoteUpdates', 0, Number.MAX_SAFE_INTEGER);
204
+ safeInteger(value.lastLogicalTime, 'lastLogicalTime', 0, Number.MAX_SAFE_INTEGER);
205
+ const recentUpdates = normalizeRecentUpdates(value.recentUpdates, profile.maximumRecentUpdates);
206
+ if (recentUpdates.some((entry) => entry.expiresAtLogicalTime <= value.lastLogicalTime))
207
+ fail('sparse routing state retains an expired update');
208
+ const rebuilt = buildState({
209
+ profile,
210
+ view,
211
+ recentUpdates,
212
+ nextOriginSequence: value.nextOriginSequence,
213
+ maximumOutboundInteractions: value.maximumOutboundInteractions,
214
+ outboundInteractions: value.outboundInteractions,
215
+ acceptedRemoteUpdates: value.acceptedRemoteUpdates,
216
+ lastLogicalTime: value.lastLogicalTime,
217
+ });
218
+ if (!same(value, rebuilt))
219
+ fail('sparse routing state binding is invalid');
220
+ return rebuilt;
221
+ }
222
+ /** Refreshes the construction-bound view without resetting counters or dedupe. */
223
+ export function refreshMeshSparseRoutingStateV2(input) {
224
+ exactRecord(input, [
225
+ 'excludedNeighborIndexes',
226
+ 'logicalTime',
227
+ 'profile',
228
+ 'schemaVersion',
229
+ 'state',
230
+ ], [], 'sparse routing refresh input');
231
+ if (input.schemaVersion !== 2)
232
+ fail('sparse routing refresh schema is invalid');
233
+ const profile = validateMeshSparseOverlayProfileV2(input.profile);
234
+ const state = validateMeshSparseRoutingStateV2(profile, input.state);
235
+ logicalTime(input.logicalTime, state.lastLogicalTime);
236
+ const view = refreshMeshSparsePeerViewV2({
237
+ schemaVersion: 2,
238
+ profile,
239
+ view: state.view,
240
+ excludedNeighborIndexes: input.excludedNeighborIndexes,
241
+ });
242
+ return buildState({
243
+ profile,
244
+ view,
245
+ recentUpdates: pruneRecent(state.recentUpdates, input.logicalTime),
246
+ nextOriginSequence: state.nextOriginSequence,
247
+ maximumOutboundInteractions: state.maximumOutboundInteractions,
248
+ outboundInteractions: state.outboundInteractions,
249
+ acceptedRemoteUpdates: state.acceptedRemoteUpdates,
250
+ lastLogicalTime: input.logicalTime,
251
+ });
252
+ }
253
+ /** Creates one local update plus a bounded, budget-accounted first fanout. */
254
+ export function publishMeshSparseUpdateV2(input) {
255
+ exactRecord(input, [
256
+ 'lifetime',
257
+ 'logicalTime',
258
+ 'payloadDigest',
259
+ 'profile',
260
+ 'schemaVersion',
261
+ 'state',
262
+ 'topic',
263
+ ], ['fanout'], 'sparse publish input');
264
+ if (input.schemaVersion !== 2)
265
+ fail('sparse publish schema is invalid');
266
+ const profile = validateMeshSparseOverlayProfileV2(input.profile);
267
+ const state = validateMeshSparseRoutingStateV2(profile, input.state);
268
+ logicalTime(input.logicalTime, state.lastLogicalTime);
269
+ topic(input.topic);
270
+ assertDigest(input.payloadDigest, 'payloadDigest');
271
+ safeInteger(input.lifetime, 'lifetime', 1, MAXIMUM_UPDATE_LIFETIME_V2);
272
+ if (input.logicalTime > Number.MAX_SAFE_INTEGER - input.lifetime)
273
+ fail('sparse update expiry exceeds safe logical time');
274
+ const fanout = input.fanout ?? profile.maximumFanout;
275
+ safeInteger(fanout, 'fanout', 1, profile.maximumFanout);
276
+ if (state.nextOriginSequence === Number.MAX_SAFE_INTEGER)
277
+ fail('sparse update origin sequence is exhausted');
278
+ const update = buildUpdate({
279
+ profile,
280
+ originPeerIndex: state.view.peerIndex,
281
+ originSequence: state.nextOriginSequence,
282
+ topic: input.topic,
283
+ payloadDigest: input.payloadDigest,
284
+ createdAtLogicalTime: input.logicalTime,
285
+ expiresAtLogicalTime: input.logicalTime + input.lifetime,
286
+ });
287
+ const recent = pruneRecent(state.recentUpdates, input.logicalTime);
288
+ if (recent.length >= profile.maximumRecentUpdates)
289
+ fail('sparse recent update capacity is exhausted');
290
+ const withUpdate = insertRecent(recent, update);
291
+ const available = state.maximumOutboundInteractions - state.outboundInteractions;
292
+ const recipientIndexes = selectRecipients(state.view, update, 0, null, Math.min(fanout, available));
293
+ const deliveries = Object.freeze(recipientIndexes.map((recipientPeerIndex) => buildDelivery({
294
+ update,
295
+ senderView: state.view,
296
+ recipientPeerIndex,
297
+ hop: 1,
298
+ previousDeliveryDigest: null,
299
+ })));
300
+ const nextState = buildState({
301
+ profile,
302
+ view: state.view,
303
+ recentUpdates: withUpdate,
304
+ nextOriginSequence: state.nextOriginSequence + 1,
305
+ maximumOutboundInteractions: state.maximumOutboundInteractions,
306
+ outboundInteractions: state.outboundInteractions + deliveries.length,
307
+ acceptedRemoteUpdates: state.acceptedRemoteUpdates,
308
+ lastLogicalTime: input.logicalTime,
309
+ });
310
+ return freeze({
311
+ state: nextState,
312
+ update,
313
+ deliveries,
314
+ });
315
+ }
316
+ export function validateMeshSparseUpdateV2(profileInput, input) {
317
+ const profile = validateMeshSparseOverlayProfileV2(profileInput);
318
+ const value = exactRecord(input, updateKeys, [], 'sparse update');
319
+ if (value.schemaVersion !== 2)
320
+ fail('sparse update schema is invalid');
321
+ if (value.profileDigest !== profile.profileDigest)
322
+ fail('sparse update profile binding is invalid');
323
+ safeInteger(value.originPeerIndex, 'originPeerIndex', 0, profile.maximumPeers - 1);
324
+ if (value.originPeerId !== peerId(profile, value.originPeerIndex))
325
+ fail('sparse update origin identity is invalid');
326
+ safeInteger(value.originSequence, 'originSequence', 1, Number.MAX_SAFE_INTEGER);
327
+ topic(value.topic);
328
+ assertDigest(value.payloadDigest, 'payloadDigest');
329
+ safeInteger(value.createdAtLogicalTime, 'createdAtLogicalTime', 0, Number.MAX_SAFE_INTEGER);
330
+ safeInteger(value.expiresAtLogicalTime, 'expiresAtLogicalTime', 1, Number.MAX_SAFE_INTEGER);
331
+ if (value.expiresAtLogicalTime <= value.createdAtLogicalTime ||
332
+ value.expiresAtLogicalTime - value.createdAtLogicalTime >
333
+ MAXIMUM_UPDATE_LIFETIME_V2)
334
+ fail('sparse update lifetime is invalid');
335
+ if (value.maximumHops !== profile.maximumHops)
336
+ fail('sparse update hop binding is invalid');
337
+ if (typeof value.updateId !== 'string' || !UPDATE_ID.test(value.updateId))
338
+ fail('sparse update ID is invalid');
339
+ assertDigest(value.updateDigest, 'updateDigest');
340
+ const rebuilt = buildUpdate({
341
+ profile,
342
+ originPeerIndex: value.originPeerIndex,
343
+ originSequence: value.originSequence,
344
+ topic: value.topic,
345
+ payloadDigest: value.payloadDigest,
346
+ createdAtLogicalTime: value.createdAtLogicalTime,
347
+ expiresAtLogicalTime: value.expiresAtLogicalTime,
348
+ });
349
+ if (!same(value, rebuilt))
350
+ fail('sparse update binding is invalid');
351
+ return rebuilt;
352
+ }
353
+ export function validateMeshSparseDeliveryV2(profileInput, input) {
354
+ const profile = validateMeshSparseOverlayProfileV2(profileInput);
355
+ const value = exactRecord(input, deliveryKeys, [], 'sparse delivery');
356
+ if (value.schemaVersion !== 2)
357
+ fail('sparse delivery schema is invalid');
358
+ const update = validateMeshSparseUpdateV2(profile, value.update);
359
+ safeInteger(value.senderPeerIndex, 'senderPeerIndex', 0, profile.maximumPeers - 1);
360
+ safeInteger(value.recipientPeerIndex, 'recipientPeerIndex', 0, profile.maximumPeers - 1);
361
+ if (value.senderPeerIndex === value.recipientPeerIndex)
362
+ fail('sparse delivery cannot target its sender');
363
+ safeInteger(value.hop, 'hop', 1, profile.maximumHops + 1);
364
+ assertDigest(value.senderViewDigest, 'senderViewDigest');
365
+ if (value.previousDeliveryDigest !== null)
366
+ assertDigest(value.previousDeliveryDigest, 'previousDeliveryDigest');
367
+ if (value.hop === 1 && value.previousDeliveryDigest !== null)
368
+ fail('sparse first delivery cannot have a predecessor');
369
+ if (value.hop > 1 && value.previousDeliveryDigest === null)
370
+ fail('sparse forwarded delivery requires a predecessor');
371
+ assertDigest(value.deliveryDigest, 'deliveryDigest');
372
+ const rebuilt = buildDelivery({
373
+ update,
374
+ senderView: {
375
+ peerIndex: value.senderPeerIndex,
376
+ viewDigest: value.senderViewDigest,
377
+ },
378
+ recipientPeerIndex: value.recipientPeerIndex,
379
+ hop: value.hop,
380
+ previousDeliveryDigest: value.previousDeliveryDigest,
381
+ });
382
+ if (!same(value, rebuilt))
383
+ fail('sparse delivery binding is invalid');
384
+ return rebuilt;
385
+ }
386
+ /**
387
+ * Builds one bounded relay delivery for recovery or an explicitly connected
388
+ * next hop. The sender view remains peer-local and the predecessor preserves
389
+ * the original causal chain.
390
+ */
391
+ export function createMeshSparseRelayDeliveryV2(input) {
392
+ exactRecord(input, [
393
+ 'predecessor',
394
+ 'profile',
395
+ 'recipientPeerIndex',
396
+ 'schemaVersion',
397
+ 'senderView',
398
+ ], [], 'sparse relay input');
399
+ if (input.schemaVersion !== 2)
400
+ fail('sparse relay schema is invalid');
401
+ const profile = validateMeshSparseOverlayProfileV2(input.profile);
402
+ const senderView = validateMeshSparsePeerViewV2(profile, input.senderView);
403
+ const predecessor = validateMeshSparseDeliveryV2(profile, input.predecessor);
404
+ safeInteger(input.recipientPeerIndex, 'recipientPeerIndex', 0, profile.maximumPeers - 1);
405
+ if (input.recipientPeerIndex === senderView.peerIndex)
406
+ fail('sparse relay cannot target its sender');
407
+ if (!senderView.activeNeighborIndexes.includes(input.recipientPeerIndex) &&
408
+ !senderView.reserveNeighborIndexes.includes(input.recipientPeerIndex))
409
+ fail('sparse relay recipient is outside the local sparse view');
410
+ if (predecessor.hop >= predecessor.update.maximumHops)
411
+ fail('sparse relay hop limit is exhausted');
412
+ return buildDelivery({
413
+ update: predecessor.update,
414
+ senderView,
415
+ recipientPeerIndex: input.recipientPeerIndex,
416
+ hop: predecessor.hop + 1,
417
+ previousDeliveryDigest: predecessor.deliveryDigest,
418
+ });
419
+ }
420
+ /** Accepts one authenticated transport delivery and plans bounded forwarding. */
421
+ export function receiveMeshSparseDeliveryV2(input) {
422
+ exactRecord(input, ['delivery', 'logicalTime', 'profile', 'schemaVersion', 'state'], [], 'sparse receive input');
423
+ if (input.schemaVersion !== 2)
424
+ fail('sparse receive schema is invalid');
425
+ const profile = validateMeshSparseOverlayProfileV2(input.profile);
426
+ const state = validateMeshSparseRoutingStateV2(profile, input.state);
427
+ logicalTime(input.logicalTime, state.lastLogicalTime);
428
+ const rawUpdate = dataProperty(input.delivery, 'update');
429
+ const rawProfileDigest = dataProperty(rawUpdate, 'profileDigest');
430
+ if (rawProfileDigest !== profile.profileDigest)
431
+ return rejection(state, 'profile_mismatch');
432
+ const delivery = validateMeshSparseDeliveryV2(profile, input.delivery);
433
+ if (delivery.recipientPeerIndex !== state.view.peerIndex)
434
+ return rejection(state, 'recipient_mismatch');
435
+ if (input.logicalTime < delivery.update.createdAtLogicalTime)
436
+ fail('sparse delivery logical time precedes update creation');
437
+ if (input.logicalTime >= delivery.update.expiresAtLogicalTime)
438
+ return rejection(state, 'update_expired');
439
+ if (delivery.hop > delivery.update.maximumHops)
440
+ return rejection(state, 'hop_limit_exceeded');
441
+ const recent = pruneRecent(state.recentUpdates, input.logicalTime);
442
+ if (recent.some((entry) => entry.updateId === delivery.update.updateId)) {
443
+ const next = buildState({
444
+ profile,
445
+ view: state.view,
446
+ recentUpdates: recent,
447
+ nextOriginSequence: state.nextOriginSequence,
448
+ maximumOutboundInteractions: state.maximumOutboundInteractions,
449
+ outboundInteractions: state.outboundInteractions,
450
+ acceptedRemoteUpdates: state.acceptedRemoteUpdates,
451
+ lastLogicalTime: input.logicalTime,
452
+ });
453
+ return freeze({
454
+ accepted: true,
455
+ duplicate: true,
456
+ state: next,
457
+ deliveries: emptyDeliveries,
458
+ });
459
+ }
460
+ if (recent.length >= profile.maximumRecentUpdates)
461
+ return rejection(state, 'recent_update_capacity_exceeded');
462
+ const withUpdate = insertRecent(recent, delivery.update);
463
+ const available = state.maximumOutboundInteractions - state.outboundInteractions;
464
+ const forwardCount = delivery.hop < delivery.update.maximumHops
465
+ ? Math.min(profile.maximumFanout, available)
466
+ : 0;
467
+ const recipientIndexes = selectRecipients(state.view, delivery.update, delivery.hop, delivery.senderPeerIndex, forwardCount);
468
+ const deliveries = Object.freeze(recipientIndexes.map((recipientPeerIndex) => buildDelivery({
469
+ update: delivery.update,
470
+ senderView: state.view,
471
+ recipientPeerIndex,
472
+ hop: delivery.hop + 1,
473
+ previousDeliveryDigest: delivery.deliveryDigest,
474
+ })));
475
+ const next = buildState({
476
+ profile,
477
+ view: state.view,
478
+ recentUpdates: withUpdate,
479
+ nextOriginSequence: state.nextOriginSequence,
480
+ maximumOutboundInteractions: state.maximumOutboundInteractions,
481
+ outboundInteractions: state.outboundInteractions + deliveries.length,
482
+ acceptedRemoteUpdates: state.acceptedRemoteUpdates + 1,
483
+ lastLogicalTime: input.logicalTime,
484
+ });
485
+ return freeze({
486
+ accepted: true,
487
+ duplicate: false,
488
+ state: next,
489
+ deliveries,
490
+ });
491
+ }
492
+ function buildView(profile, topologySeed, peerIndex, revision, excludedNeighborIndexes) {
493
+ const excluded = new Set(excludedNeighborIndexes);
494
+ const selected = [];
495
+ const selectedSet = new Set();
496
+ const add = (candidate) => {
497
+ if (candidate !== peerIndex &&
498
+ !excluded.has(candidate) &&
499
+ !selectedSet.has(candidate)) {
500
+ selectedSet.add(candidate);
501
+ selected.push(candidate);
502
+ }
503
+ };
504
+ const successor = (peerIndex + 1) % profile.maximumPeers;
505
+ add(successor);
506
+ const needed = profile.activeNeighborCount + profile.reserveNeighborCount;
507
+ const modulus = profile.maximumPeers - 1;
508
+ const stride = coprimeMultiplier(modulus, mix32(topologySeed ^ peerIndex ^ 0x85eb_ca6b));
509
+ const offset = mix32(topologySeed + Math.imul(peerIndex + 1, 0x9e37_79b1)) % modulus;
510
+ const revisionWindow = ((revision % modulus) * (profile.activeNeighborCount - 1)) % modulus;
511
+ for (let slot = 0; selected.length < needed && slot < modulus; slot += 1) {
512
+ const position = (revisionWindow + slot) % modulus;
513
+ const jump = 1 + ((offset + Math.imul(position, stride)) % modulus);
514
+ add((peerIndex + jump) % profile.maximumPeers);
515
+ }
516
+ if (selected.length !== needed)
517
+ fail('sparse peer view cannot satisfy neighbor bounds');
518
+ const activeNeighborIndexes = Object.freeze(selected.slice(0, profile.activeNeighborCount));
519
+ const reserveNeighborIndexes = Object.freeze(selected.slice(profile.activeNeighborCount));
520
+ const body = {
521
+ schemaVersion: 2,
522
+ profileDigest: profile.profileDigest,
523
+ topologySeed,
524
+ peerIndex,
525
+ peerId: peerId(profile, peerIndex),
526
+ revision,
527
+ activeNeighborIndexes,
528
+ reserveNeighborIndexes,
529
+ excludedNeighborIndexes: Object.freeze([...excludedNeighborIndexes]),
530
+ };
531
+ return freeze({ ...body, viewDigest: digest(body) });
532
+ }
533
+ function buildState(input) {
534
+ const body = {
535
+ schemaVersion: 2,
536
+ profileDigest: input.profile.profileDigest,
537
+ view: input.view,
538
+ recentUpdates: Object.freeze([...input.recentUpdates]),
539
+ nextOriginSequence: input.nextOriginSequence,
540
+ maximumOutboundInteractions: input.maximumOutboundInteractions,
541
+ outboundInteractions: input.outboundInteractions,
542
+ acceptedRemoteUpdates: input.acceptedRemoteUpdates,
543
+ lastLogicalTime: input.lastLogicalTime,
544
+ };
545
+ return freeze({
546
+ ...body,
547
+ stateDigest: digest(body),
548
+ });
549
+ }
550
+ function buildUpdate(input) {
551
+ const identity = {
552
+ schemaVersion: 2,
553
+ profileDigest: input.profile.profileDigest,
554
+ originPeerIndex: input.originPeerIndex,
555
+ originPeerId: peerId(input.profile, input.originPeerIndex),
556
+ originSequence: input.originSequence,
557
+ topic: input.topic,
558
+ payloadDigest: input.payloadDigest,
559
+ createdAtLogicalTime: input.createdAtLogicalTime,
560
+ expiresAtLogicalTime: input.expiresAtLogicalTime,
561
+ maximumHops: input.profile.maximumHops,
562
+ };
563
+ const identityDigest = digest(identity);
564
+ const updateId = `overlay-update:${identityDigest.slice('sha256:'.length)}`;
565
+ const body = { ...identity, updateId };
566
+ return freeze({ ...body, updateDigest: digest(body) });
567
+ }
568
+ function buildDelivery(input) {
569
+ const body = {
570
+ schemaVersion: 2,
571
+ update: input.update,
572
+ senderPeerIndex: input.senderView.peerIndex,
573
+ recipientPeerIndex: input.recipientPeerIndex,
574
+ hop: input.hop,
575
+ senderViewDigest: input.senderView.viewDigest,
576
+ previousDeliveryDigest: input.previousDeliveryDigest,
577
+ };
578
+ return freeze({
579
+ ...body,
580
+ deliveryDigest: digest(body),
581
+ });
582
+ }
583
+ function selectRecipients(view, update, hop, excludedPeerIndex, count) {
584
+ if (count === 0)
585
+ return Object.freeze([]);
586
+ const candidates = view.activeNeighborIndexes.filter((index) => index !== excludedPeerIndex);
587
+ if (candidates.length === 0)
588
+ return Object.freeze([]);
589
+ const selected = [];
590
+ const successor = (view.peerIndex + 1) % profileMaximumPeers(update.profileDigest);
591
+ if (candidates.includes(successor))
592
+ selected.push(successor);
593
+ const offset = stringHash32(`${update.updateDigest}:${view.peerIndex}:${hop}:${view.revision}`) % candidates.length;
594
+ for (let slot = 0; selected.length < count && slot < candidates.length; slot += 1) {
595
+ const candidate = candidates[(offset + slot) % candidates.length];
596
+ if (!selected.includes(candidate))
597
+ selected.push(candidate);
598
+ }
599
+ return Object.freeze(selected.slice(0, count));
600
+ }
601
+ function profileMaximumPeers(profileDigest) {
602
+ const profile = MESH_SPARSE_OVERLAY_PROFILES_V2.find((candidate) => candidate.profileDigest === profileDigest);
603
+ if (!profile)
604
+ fail('sparse overlay profile digest is unknown');
605
+ return profile.maximumPeers;
606
+ }
607
+ function outboundQuota(profile, peerIndex) {
608
+ const base = Math.floor(profile.maximumInteractions / profile.maximumPeers);
609
+ const remainder = profile.maximumInteractions % profile.maximumPeers;
610
+ return base + (peerIndex < remainder ? 1 : 0);
611
+ }
612
+ function insertRecent(recent, update) {
613
+ return Object.freeze([
614
+ ...recent,
615
+ Object.freeze({
616
+ updateId: update.updateId,
617
+ expiresAtLogicalTime: update.expiresAtLogicalTime,
618
+ }),
619
+ ].sort(compareRecent));
620
+ }
621
+ function pruneRecent(recent, logicalTimeValue) {
622
+ return Object.freeze(recent.filter((entry) => entry.expiresAtLogicalTime > logicalTimeValue));
623
+ }
624
+ function normalizeRecentUpdates(input, maximum) {
625
+ const values = denseArray(input, maximum, 'recentUpdates');
626
+ const normalized = [];
627
+ const ids = new Set();
628
+ for (const entry of values) {
629
+ const value = exactRecord(entry, ['expiresAtLogicalTime', 'updateId'], [], 'recent update');
630
+ if (typeof value.updateId !== 'string' || !UPDATE_ID.test(value.updateId))
631
+ fail('recent update ID is invalid');
632
+ if (ids.has(value.updateId))
633
+ fail('recent update IDs must be unique');
634
+ ids.add(value.updateId);
635
+ safeInteger(value.expiresAtLogicalTime, 'recent update expiresAtLogicalTime', 1, Number.MAX_SAFE_INTEGER);
636
+ normalized.push(Object.freeze({
637
+ updateId: value.updateId,
638
+ expiresAtLogicalTime: value.expiresAtLogicalTime,
639
+ }));
640
+ }
641
+ const sorted = [...normalized].sort(compareRecent);
642
+ if (normalized.some((entry, index) => entry !== sorted[index]))
643
+ fail('recent updates must be canonically ordered');
644
+ return Object.freeze(normalized);
645
+ }
646
+ function compareRecent(left, right) {
647
+ return (left.expiresAtLogicalTime - right.expiresAtLogicalTime ||
648
+ compareAscii(left.updateId, right.updateId));
649
+ }
650
+ function normalizeIndexes(input, profile, peerIndex, maximum, label, requireSorted = true) {
651
+ const values = denseArray(input, maximum, label);
652
+ const normalized = [];
653
+ const seen = new Set();
654
+ for (const value of values) {
655
+ safeInteger(value, label, 0, profile.maximumPeers - 1);
656
+ if (value === peerIndex)
657
+ fail(`${label} cannot contain the local peer`);
658
+ if (seen.has(value))
659
+ fail(`${label} must contain unique indexes`);
660
+ seen.add(value);
661
+ normalized.push(value);
662
+ }
663
+ if (requireSorted) {
664
+ const sorted = [...normalized].sort((left, right) => left - right);
665
+ if (normalized.some((value, index) => value !== sorted[index]))
666
+ fail(`${label} must be sorted`);
667
+ }
668
+ return Object.freeze(normalized);
669
+ }
670
+ function rejection(state, code) {
671
+ return freeze({
672
+ accepted: false,
673
+ code,
674
+ state,
675
+ deliveries: emptyDeliveries,
676
+ });
677
+ }
678
+ function peerId(profile, peerIndex) {
679
+ return `peer-${String(peerIndex).padStart(String(profile.maximumPeers - 1).length, '0')}`;
680
+ }
681
+ function coprimeMultiplier(modulus, seed) {
682
+ let candidate = seed % modulus || 1;
683
+ while (greatestCommonDivisor(candidate, modulus) !== 1) {
684
+ candidate += 1;
685
+ if (candidate >= modulus)
686
+ candidate = 1;
687
+ }
688
+ return candidate;
689
+ }
690
+ function greatestCommonDivisor(left, right) {
691
+ let a = left;
692
+ let b = right;
693
+ while (b !== 0) {
694
+ const remainder = a % b;
695
+ a = b;
696
+ b = remainder;
697
+ }
698
+ return a;
699
+ }
700
+ function mix32(value) {
701
+ let output = value >>> 0;
702
+ output ^= output >>> 16;
703
+ output = Math.imul(output, 0x7feb_352d);
704
+ output ^= output >>> 15;
705
+ output = Math.imul(output, 0x846c_a68b);
706
+ output ^= output >>> 16;
707
+ return output >>> 0;
708
+ }
709
+ function stringHash32(value) {
710
+ let hash = 0x811c_9dc5;
711
+ for (let index = 0; index < value.length; index += 1) {
712
+ hash ^= value.charCodeAt(index);
713
+ hash = Math.imul(hash, 0x0100_0193);
714
+ }
715
+ return hash >>> 0;
716
+ }
717
+ function logicalTime(value, previous) {
718
+ safeInteger(value, 'logicalTime', previous, Number.MAX_SAFE_INTEGER);
719
+ }
720
+ function topic(value) {
721
+ if (typeof value !== 'string' || !TOPIC.test(value))
722
+ fail('sparse update topic is invalid');
723
+ }
724
+ function requireProfileId(value) {
725
+ if (typeof value !== 'string' || !profileIds.has(value))
726
+ fail('sparse overlay profile ID is invalid');
727
+ }
728
+ function assertDigest(value, label) {
729
+ if (typeof value !== 'string' || !DIGEST.test(value))
730
+ fail(`${label} is invalid`);
731
+ }
732
+ function safeInteger(value, label, minimum, maximum) {
733
+ if (!Number.isSafeInteger(value) ||
734
+ value < minimum ||
735
+ value > maximum)
736
+ fail(`${label} is outside its allowed range`);
737
+ }
738
+ function denseArray(value, maximum, label) {
739
+ if (!Array.isArray(value) || value.length > maximum)
740
+ fail(`${label} must be a bounded array`);
741
+ for (let index = 0; index < value.length; index += 1)
742
+ if (!Object.prototype.hasOwnProperty.call(value, index))
743
+ fail(`${label} cannot be sparse`);
744
+ return value;
745
+ }
746
+ function exactRecord(value, required, optional, label) {
747
+ if (!isRecord(value))
748
+ fail(`${label} must be a plain object`);
749
+ if (Object.getOwnPropertySymbols(value).length !== 0)
750
+ fail(`${label} cannot contain symbol keys`);
751
+ const allowed = new Set([...required, ...optional]);
752
+ const names = Object.getOwnPropertyNames(value);
753
+ if (required.some((key) => !names.includes(key)) ||
754
+ names.some((key) => !allowed.has(key)))
755
+ fail(`${label} has invalid fields`);
756
+ for (const name of names) {
757
+ const descriptor = Object.getOwnPropertyDescriptor(value, name);
758
+ if (!descriptor || !('value' in descriptor) || !descriptor.enumerable)
759
+ fail(`${label} must contain enumerable data properties only`);
760
+ }
761
+ return value;
762
+ }
763
+ function isRecord(value) {
764
+ return (value !== null &&
765
+ typeof value === 'object' &&
766
+ !Array.isArray(value) &&
767
+ (Object.getPrototypeOf(value) === Object.prototype ||
768
+ Object.getPrototypeOf(value) === null));
769
+ }
770
+ function dataProperty(value, key) {
771
+ if (!isRecord(value))
772
+ return undefined;
773
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
774
+ return descriptor && 'value' in descriptor ? descriptor.value : undefined;
775
+ }
776
+ function digest(value) {
777
+ return `sha256:${sha256Base64Url(utf8.encode(stableJson(value)))}`;
778
+ }
779
+ function same(left, right) {
780
+ return stableJson(left) === stableJson(right);
781
+ }
782
+ function stableJson(value) {
783
+ if (value === null)
784
+ return 'null';
785
+ if (typeof value === 'string')
786
+ return JSON.stringify(value);
787
+ if (typeof value === 'number') {
788
+ if (!Number.isSafeInteger(value))
789
+ fail('sparse overlay JSON number is invalid');
790
+ return String(value);
791
+ }
792
+ if (typeof value === 'boolean')
793
+ return value ? 'true' : 'false';
794
+ if (Array.isArray(value))
795
+ return `[${value.map(stableJson).join(',')}]`;
796
+ if (!isRecord(value))
797
+ fail('sparse overlay value is not canonical JSON data');
798
+ return `{${Object.keys(value)
799
+ .sort(compareAscii)
800
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
801
+ .join(',')}}`;
802
+ }
803
+ function freeze(value) {
804
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
805
+ for (const child of Object.values(value))
806
+ freeze(child);
807
+ Object.freeze(value);
808
+ }
809
+ return value;
810
+ }
811
+ function compareAscii(left, right) {
812
+ return left < right ? -1 : left > right ? 1 : 0;
813
+ }
814
+ function fail(message) {
815
+ throw new TypeError(message);
816
+ }
817
+ //# sourceMappingURL=sparse-overlay.js.map