@peerbit/shared-log 13.1.18 → 13.2.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.
Files changed (59) hide show
  1. package/dist/benchmark/index.js +91 -29
  2. package/dist/benchmark/index.js.map +1 -1
  3. package/dist/benchmark/native-graph.js +120 -10
  4. package/dist/benchmark/native-graph.js.map +1 -1
  5. package/dist/benchmark/network-preset-e2e.d.ts +2 -0
  6. package/dist/benchmark/network-preset-e2e.d.ts.map +1 -0
  7. package/dist/benchmark/network-preset-e2e.js +497 -0
  8. package/dist/benchmark/network-preset-e2e.js.map +1 -0
  9. package/dist/benchmark/receive-prune.d.ts +2 -0
  10. package/dist/benchmark/receive-prune.d.ts.map +1 -0
  11. package/dist/benchmark/receive-prune.js +816 -0
  12. package/dist/benchmark/receive-prune.js.map +1 -0
  13. package/dist/benchmark/sync-catchup.d.ts +1 -2
  14. package/dist/benchmark/sync-catchup.d.ts.map +1 -1
  15. package/dist/benchmark/sync-catchup.js +175 -7
  16. package/dist/benchmark/sync-catchup.js.map +1 -1
  17. package/dist/src/checked-prune.d.ts +3 -0
  18. package/dist/src/checked-prune.d.ts.map +1 -1
  19. package/dist/src/checked-prune.js +24 -0
  20. package/dist/src/checked-prune.js.map +1 -1
  21. package/dist/src/exchange-heads.d.ts +238 -1
  22. package/dist/src/exchange-heads.d.ts.map +1 -1
  23. package/dist/src/exchange-heads.js +1512 -36
  24. package/dist/src/exchange-heads.js.map +1 -1
  25. package/dist/src/index.d.ts +276 -5
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/index.js +7358 -666
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/pid.d.ts.map +1 -1
  30. package/dist/src/pid.js +25 -9
  31. package/dist/src/pid.js.map +1 -1
  32. package/dist/src/ranges.d.ts +11 -2
  33. package/dist/src/ranges.d.ts.map +1 -1
  34. package/dist/src/ranges.js +74 -30
  35. package/dist/src/ranges.js.map +1 -1
  36. package/dist/src/sync/index.d.ts +77 -2
  37. package/dist/src/sync/index.d.ts.map +1 -1
  38. package/dist/src/sync/rateless-iblt.d.ts +11 -5
  39. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  40. package/dist/src/sync/rateless-iblt.js +123 -60
  41. package/dist/src/sync/rateless-iblt.js.map +1 -1
  42. package/dist/src/sync/simple.d.ts +56 -4
  43. package/dist/src/sync/simple.d.ts.map +1 -1
  44. package/dist/src/sync/simple.js +495 -63
  45. package/dist/src/sync/simple.js.map +1 -1
  46. package/dist/src/utils.d.ts +2 -1
  47. package/dist/src/utils.d.ts.map +1 -1
  48. package/dist/src/utils.js +49 -2
  49. package/dist/src/utils.js.map +1 -1
  50. package/package.json +27 -17
  51. package/src/checked-prune.ts +27 -0
  52. package/src/exchange-heads.ts +2018 -41
  53. package/src/index.ts +13114 -3167
  54. package/src/pid.ts +24 -13
  55. package/src/ranges.ts +90 -40
  56. package/src/sync/index.ts +115 -2
  57. package/src/sync/rateless-iblt.ts +154 -75
  58. package/src/sync/simple.ts +590 -95
  59. package/src/utils.ts +73 -3
@@ -1,15 +1,371 @@
1
- import { field, fixedArray, variant, vec } from "@dao-xyz/borsh";
2
- import { Entry, EntryType, type ShallowEntry } from "@peerbit/log";
1
+ import { deserialize, field, fixedArray, variant, vec } from "@dao-xyz/borsh";
2
+ import { cidifyString } from "@peerbit/blocks-interface";
3
+ import {
4
+ LamportClock as Clock,
5
+ Entry,
6
+ EntryType,
7
+ Meta,
8
+ type PreparedAppendJoinFacts,
9
+ type PreparedNativeLogEntry,
10
+ type PreparedRawEntryV0Facts,
11
+ ShallowEntry,
12
+ ShallowMeta,
13
+ Timestamp,
14
+ calculateRawCidV1Batch,
15
+ prepareRawEntryV0Batch,
16
+ verifyEntryV0Ed25519StorageBatch,
17
+ } from "@peerbit/log";
3
18
  import { Log } from "@peerbit/log";
4
19
  import { logger as loggerFn } from "@peerbit/logger";
5
20
  import { TransportMessage } from "./message.js";
21
+ import type { SyncProfileFn } from "./sync/index.js";
22
+ import {
23
+ emitSyncProfileDuration,
24
+ emitSyncProfileEvent,
25
+ syncProfileStart,
26
+ } from "./sync/profile.js";
6
27
 
7
28
  const logger = loggerFn("peerbit:shared-log:exchange-heads");
8
29
  const warn = logger.newScope("warn");
9
30
 
31
+ type NativeBackboneAppendProfile = {
32
+ nativeBackboneRawReceiveInputCopyMs: number;
33
+ nativeBackboneRawReceivePrepareMs: number;
34
+ nativeBackboneRawReceiveDigestMs: number;
35
+ nativeBackboneRawReceiveCidStringMs: number;
36
+ nativeBackboneRawReceiveExpectedCidMs: number;
37
+ nativeBackboneRawReceiveStorageParseMs: number;
38
+ nativeBackboneRawReceiveMetaParseMs: number;
39
+ nativeBackboneRawReceivePayloadParseMs: number;
40
+ nativeBackboneRawReceiveSignatureParseMs: number;
41
+ nativeBackboneRawReceiveSignableMs: number;
42
+ nativeBackboneRawReceiveVerifyBatchMs: number;
43
+ nativeBackboneRawReceiveVerifyFallbackMs: number;
44
+ nativeBackboneRawReceivePrepareColumnsMs: number;
45
+ };
46
+
47
+ type RawReceiveNativeBackbone = {
48
+ prepareRawReceiveBatch(blocks: Uint8Array[]): PreparedRawEntryV0Facts[];
49
+ prepareRawReceiveExpectedColumnsBatch?(
50
+ blocks: Uint8Array[],
51
+ hashes: string[],
52
+ options?: { verifySignatures?: boolean },
53
+ ): PreparedRawEntryV0FactsColumns | undefined;
54
+ prepareRawReceiveColumnsBatch?(
55
+ blocks: Uint8Array[],
56
+ hashes?: string[],
57
+ options?: { verifySignatures?: boolean },
58
+ ): PreparedRawEntryV0FactsColumns | undefined;
59
+ verifyPreparedRawReceiveEntries?(
60
+ hashes: Iterable<string>,
61
+ ): boolean[] | undefined;
62
+ clearPreparedRawReceiveEntries?(hashes: Iterable<string>): number;
63
+ setAppendProfileEnabled?(enabled: boolean): void;
64
+ resetAppendProfile?(): void;
65
+ appendProfile?(): NativeBackboneAppendProfile;
66
+ };
67
+
68
+ const emitNativeBackboneRawPrepareProfile = (
69
+ profile: SyncProfileFn | undefined,
70
+ nativeProfile: NativeBackboneAppendProfile | undefined,
71
+ entries: number,
72
+ bytes: number,
73
+ ) => {
74
+ if (!profile || !nativeProfile) {
75
+ return;
76
+ }
77
+ const events: Array<[name: string, durationMs: number]> = [
78
+ [
79
+ "sharedLog.rawReceive.nativePrepare.inputCopy",
80
+ nativeProfile.nativeBackboneRawReceiveInputCopyMs,
81
+ ],
82
+ [
83
+ "sharedLog.rawReceive.nativePrepare.prepare",
84
+ nativeProfile.nativeBackboneRawReceivePrepareMs,
85
+ ],
86
+ [
87
+ "sharedLog.rawReceive.nativePrepare.digest",
88
+ nativeProfile.nativeBackboneRawReceiveDigestMs,
89
+ ],
90
+ [
91
+ "sharedLog.rawReceive.nativePrepare.cidString",
92
+ nativeProfile.nativeBackboneRawReceiveCidStringMs,
93
+ ],
94
+ [
95
+ "sharedLog.rawReceive.nativePrepare.expectedCid",
96
+ nativeProfile.nativeBackboneRawReceiveExpectedCidMs,
97
+ ],
98
+ [
99
+ "sharedLog.rawReceive.nativePrepare.storageParse",
100
+ nativeProfile.nativeBackboneRawReceiveStorageParseMs,
101
+ ],
102
+ [
103
+ "sharedLog.rawReceive.nativePrepare.metaParse",
104
+ nativeProfile.nativeBackboneRawReceiveMetaParseMs,
105
+ ],
106
+ [
107
+ "sharedLog.rawReceive.nativePrepare.payloadParse",
108
+ nativeProfile.nativeBackboneRawReceivePayloadParseMs,
109
+ ],
110
+ [
111
+ "sharedLog.rawReceive.nativePrepare.signatureParse",
112
+ nativeProfile.nativeBackboneRawReceiveSignatureParseMs,
113
+ ],
114
+ [
115
+ "sharedLog.rawReceive.nativePrepare.signable",
116
+ nativeProfile.nativeBackboneRawReceiveSignableMs,
117
+ ],
118
+ [
119
+ "sharedLog.rawReceive.nativePrepare.verifyBatch",
120
+ nativeProfile.nativeBackboneRawReceiveVerifyBatchMs,
121
+ ],
122
+ [
123
+ "sharedLog.rawReceive.nativePrepare.verifyFallback",
124
+ nativeProfile.nativeBackboneRawReceiveVerifyFallbackMs,
125
+ ],
126
+ [
127
+ "sharedLog.rawReceive.nativePrepare.columns",
128
+ nativeProfile.nativeBackboneRawReceivePrepareColumnsMs,
129
+ ],
130
+ ];
131
+ for (const [name, durationMs] of events) {
132
+ if (
133
+ durationMs > 0 ||
134
+ name === "sharedLog.rawReceive.nativePrepare.prepare"
135
+ ) {
136
+ emitSyncProfileEvent(profile, {
137
+ name,
138
+ component: "shared-log",
139
+ durationMs,
140
+ entries,
141
+ bytes,
142
+ messages: 1,
143
+ });
144
+ }
145
+ }
146
+ };
147
+
148
+ type PreparedRawEntryV0FactsColumns = [
149
+ cids: string[],
150
+ hashDigestBytes: Array<Uint8Array | undefined>,
151
+ byteLengths: Uint32Array,
152
+ clockIds: Uint8Array[],
153
+ wallTimes: BigUint64Array,
154
+ logicals: Uint32Array,
155
+ gids: string[],
156
+ nexts: string[][],
157
+ types: Uint8Array,
158
+ metaBytes: Uint8Array[],
159
+ metaDatas: Array<Uint8Array | undefined>,
160
+ payloadByteLengths: Uint32Array,
161
+ signatureVerified: Uint8Array,
162
+ requestedReplicas: Uint32Array,
163
+ hashNumbers: string[] | BigUint64Array,
164
+ ];
165
+
166
+ type PreparedRawEntryV0FactsSource =
167
+ | PreparedRawEntryV0Facts
168
+ | PreparedRawEntryV0FactsColumns;
169
+
170
+ const isPreparedRawEntryV0FactsColumns = (
171
+ facts: PreparedRawEntryV0FactsSource,
172
+ ): facts is PreparedRawEntryV0FactsColumns => Array.isArray(facts);
173
+
174
+ const preparedRawColumnValue = <T>(
175
+ values: ArrayLike<T>,
176
+ index: number,
177
+ label: string,
178
+ ): T => {
179
+ const value = values[index];
180
+ if (value === undefined) {
181
+ throw new Error(`Missing prepared raw receive ${label}`);
182
+ }
183
+ return value;
184
+ };
185
+
186
+ const preparedRawCid = (facts: PreparedRawEntryV0FactsSource, index: number) =>
187
+ isPreparedRawEntryV0FactsColumns(facts)
188
+ ? preparedRawColumnValue(facts[0], index, "cid")
189
+ : facts.cid;
190
+
191
+ const preparedRawHashDigestBytes = (
192
+ facts: PreparedRawEntryV0FactsSource,
193
+ index: number,
194
+ fallbackCid?: string,
195
+ ) => {
196
+ if (!isPreparedRawEntryV0FactsColumns(facts)) {
197
+ return facts.hashDigestBytes;
198
+ }
199
+ const digest = facts[1][index];
200
+ if (digest) {
201
+ return digest;
202
+ }
203
+ const cid = facts[0][index] ?? fallbackCid;
204
+ if (!cid) {
205
+ throw new Error("Missing prepared raw receive cid");
206
+ }
207
+ return cidifyString(cid).multihash.digest;
208
+ };
209
+
210
+ const preparedRawByteLength = (
211
+ facts: PreparedRawEntryV0FactsSource,
212
+ index: number,
213
+ ) =>
214
+ isPreparedRawEntryV0FactsColumns(facts)
215
+ ? preparedRawColumnValue(facts[2], index, "byte length")
216
+ : facts.byteLength;
217
+
218
+ const preparedRawClockId = (
219
+ facts: PreparedRawEntryV0FactsSource,
220
+ index: number,
221
+ ) =>
222
+ isPreparedRawEntryV0FactsColumns(facts)
223
+ ? preparedRawColumnValue(facts[3], index, "clock id")
224
+ : facts.clockId;
225
+
226
+ const preparedRawWallTime = (
227
+ facts: PreparedRawEntryV0FactsSource,
228
+ index: number,
229
+ ) =>
230
+ isPreparedRawEntryV0FactsColumns(facts)
231
+ ? preparedRawColumnValue(facts[4], index, "wall time")
232
+ : facts.wallTime;
233
+
234
+ const preparedRawLogical = (
235
+ facts: PreparedRawEntryV0FactsSource,
236
+ index: number,
237
+ ) =>
238
+ isPreparedRawEntryV0FactsColumns(facts)
239
+ ? preparedRawColumnValue(facts[5], index, "logical time")
240
+ : facts.logical;
241
+
242
+ const preparedRawGid = (facts: PreparedRawEntryV0FactsSource, index: number) =>
243
+ isPreparedRawEntryV0FactsColumns(facts)
244
+ ? preparedRawColumnValue(facts[6], index, "gid")
245
+ : facts.gid;
246
+
247
+ const preparedRawNext = (
248
+ facts: PreparedRawEntryV0FactsSource,
249
+ index: number,
250
+ ) =>
251
+ isPreparedRawEntryV0FactsColumns(facts)
252
+ ? preparedRawColumnValue(facts[7], index, "next hashes")
253
+ : facts.next;
254
+
255
+ const preparedRawType = (
256
+ facts: PreparedRawEntryV0FactsSource,
257
+ index: number,
258
+ ) =>
259
+ isPreparedRawEntryV0FactsColumns(facts)
260
+ ? preparedRawColumnValue(facts[8], index, "entry type")
261
+ : facts.type;
262
+
263
+ const preparedRawMetaBytes = (
264
+ facts: PreparedRawEntryV0FactsSource,
265
+ index: number,
266
+ ) =>
267
+ isPreparedRawEntryV0FactsColumns(facts)
268
+ ? preparedRawColumnValue(facts[9], index, "meta bytes")
269
+ : facts.metaBytes;
270
+
271
+ const preparedRawMetaData = (
272
+ facts: PreparedRawEntryV0FactsSource,
273
+ index: number,
274
+ ) =>
275
+ isPreparedRawEntryV0FactsColumns(facts) ? facts[10][index] : facts.metaData;
276
+
277
+ const preparedRawPayloadByteLength = (
278
+ facts: PreparedRawEntryV0FactsSource,
279
+ index: number,
280
+ ) =>
281
+ isPreparedRawEntryV0FactsColumns(facts)
282
+ ? preparedRawColumnValue(facts[11], index, "payload byte length")
283
+ : facts.payloadByteLength;
284
+
285
+ const preparedRawSignatureVerified = (
286
+ facts: PreparedRawEntryV0FactsSource,
287
+ index: number,
288
+ ) =>
289
+ isPreparedRawEntryV0FactsColumns(facts)
290
+ ? preparedRawColumnValue(facts[12], index, "signature flag") !== 0
291
+ : facts.signatureVerified;
292
+
293
+ const preparedRawRequestedReplicas = (
294
+ facts: PreparedRawEntryV0FactsSource,
295
+ index: number,
296
+ ) => {
297
+ const value = isPreparedRawEntryV0FactsColumns(facts)
298
+ ? facts[13]?.[index]
299
+ : facts.requestedReplicas;
300
+ return value && value > 0 ? value : undefined;
301
+ };
302
+
303
+ const preparedRawHashNumber = (
304
+ facts: PreparedRawEntryV0FactsSource,
305
+ index: number,
306
+ ) =>
307
+ isPreparedRawEntryV0FactsColumns(facts)
308
+ ? facts[14]?.[index]
309
+ : facts.hashNumber;
310
+
311
+ const preparedRawFactsCount = (facts: PreparedRawEntryV0FactsColumns) =>
312
+ facts[0].length || facts[2].length;
313
+
314
+ const preparedRawFactsHashes = (
315
+ facts: PreparedRawEntryV0FactsColumns | PreparedRawEntryV0Facts[],
316
+ ) =>
317
+ Array.isArray(facts[0])
318
+ ? [...(facts as PreparedRawEntryV0FactsColumns)[0]]
319
+ : (facts as PreparedRawEntryV0Facts[]).map((entry) => entry.cid);
320
+
321
+ const attachPreparedRawShallowFacts = (
322
+ shallow: ShallowEntry,
323
+ facts: PreparedRawEntryV0FactsSource,
324
+ index: number,
325
+ fallbackCid: string,
326
+ ): ShallowEntry => {
327
+ Object.defineProperties(shallow, {
328
+ getMetaBytes: {
329
+ value: () => preparedRawMetaBytes(facts, index),
330
+ configurable: true,
331
+ },
332
+ getHashDigestBytes: {
333
+ value: () => preparedRawHashDigestBytes(facts, index, fallbackCid),
334
+ configurable: true,
335
+ },
336
+ });
337
+ return shallow;
338
+ };
339
+
10
340
  // Stored in the reserved bytes so older peers ignore the hint.
11
341
  export const EXCHANGE_HEADS_REPAIR_HINT = 1;
12
342
 
343
+ /**
344
+ * Capability bit: the peer can receive `RawExchangeHeadsMessage` ([0, 7]).
345
+ * Advertised once per peer via {@link SyncCapabilitiesMessage} (and echoed by
346
+ * the per-request capability messages of the simple synchronizer), so live
347
+ * append gossip can pick the raw path without a per-request round trip.
348
+ */
349
+ export const SYNC_CAPABILITY_RAW_EXCHANGE_HEADS = 1;
350
+
351
+ /**
352
+ * One-shot capability advertisement, sent to a peer when it (or we) subscribe
353
+ * to the program topic and `sync.rawExchangeHeads` is enabled. Peers that do
354
+ * not know this message drop it as an unknown variant; peers that never
355
+ * advertise keep receiving the plain `ExchangeHeadsMessage` path.
356
+ */
357
+ @variant([0, 10])
358
+ export class SyncCapabilitiesMessage extends TransportMessage {
359
+ @field({ type: "u32" })
360
+ capabilities: number;
361
+
362
+ constructor(props?: { capabilities?: number }) {
363
+ super();
364
+ this.capabilities =
365
+ props?.capabilities ?? SYNC_CAPABILITY_RAW_EXCHANGE_HEADS;
366
+ }
367
+ }
368
+
13
369
  /**
14
370
  * This thing allows use to faster sync since we can provide
15
371
  * references that can be read concurrently to
@@ -29,6 +385,28 @@ export class EntryWithRefs<T> {
29
385
  }
30
386
  }
31
387
 
388
+ @variant(1)
389
+ export class RawEntryWithRefs {
390
+ @field({ type: "string" })
391
+ hash: string;
392
+
393
+ @field({ type: Uint8Array })
394
+ bytes: Uint8Array;
395
+
396
+ @field({ type: vec("string") })
397
+ gidRefrences: string[];
398
+
399
+ constructor(properties: {
400
+ hash: string;
401
+ bytes: Uint8Array;
402
+ gidRefrences: string[];
403
+ }) {
404
+ this.hash = properties.hash;
405
+ this.bytes = properties.bytes;
406
+ this.gidRefrences = properties.gidRefrences;
407
+ }
408
+ }
409
+
32
410
  @variant([0, 0])
33
411
  export class ExchangeHeadsMessage<T> extends TransportMessage {
34
412
  @field({ type: vec(EntryWithRefs) })
@@ -37,12 +415,660 @@ export class ExchangeHeadsMessage<T> extends TransportMessage {
37
415
  @field({ type: fixedArray("u8", 4) })
38
416
  reserved: Uint8Array = new Uint8Array(4);
39
417
 
40
- constructor(props: { heads: EntryWithRefs<T>[] }) {
418
+ preparedHashes?: string[];
419
+
420
+ constructor(props: { heads: EntryWithRefs<T>[]; preparedHashes?: string[] }) {
41
421
  super();
42
422
  this.heads = props.heads;
423
+ this.preparedHashes = props.preparedHashes;
424
+ }
425
+ }
426
+
427
+ @variant([0, 7])
428
+ export class RawExchangeHeadsMessage extends TransportMessage {
429
+ @field({ type: vec(RawEntryWithRefs) })
430
+ heads: RawEntryWithRefs[];
431
+
432
+ @field({ type: fixedArray("u8", 4) })
433
+ reserved: Uint8Array = new Uint8Array(4);
434
+
435
+ constructor(props: { heads: RawEntryWithRefs[]; reserved?: Uint8Array }) {
436
+ super();
437
+ this.heads = props.heads;
438
+ if (props.reserved) {
439
+ this.reserved = props.reserved;
440
+ }
441
+ }
442
+ }
443
+
444
+ /**
445
+ * The stash surface a {@link StashBackedRawExchangeHeadsMessage} consumes:
446
+ * entry block bytes kept in native (wasm) memory by the wire-level decoder,
447
+ * keyed by the enclosing DataMessage id.
448
+ */
449
+ export type RawExchangeHeadsStash = {
450
+ stashedBlocks(
451
+ id: Uint8Array,
452
+ indexes?: Uint32Array,
453
+ ): Uint8Array[] | undefined;
454
+ release(id: Uint8Array): boolean;
455
+ };
456
+
457
+ /**
458
+ * A head of a stash-backed raw exchange message: hash/refs/length come from
459
+ * stash metadata; `bytes` materializes lazily (and is counted) because the
460
+ * fused receive path never needs the block bytes in JS.
461
+ */
462
+ export class StashBackedRawEntryWithRefs {
463
+ private bytesValue?: Uint8Array;
464
+
465
+ constructor(
466
+ private readonly message: StashBackedRawExchangeHeadsMessage,
467
+ readonly stashIndex: number,
468
+ readonly hash: string,
469
+ readonly gidRefrences: string[],
470
+ readonly byteLength: number,
471
+ ) {}
472
+
473
+ get bytes(): Uint8Array {
474
+ return (this.bytesValue ??= this.message.materializeHeadBytes(
475
+ this.stashIndex,
476
+ this.hash,
477
+ ));
478
+ }
479
+ }
480
+
481
+ /**
482
+ * A raw exchange-heads message resolved from the native wire stash instead of
483
+ * a TS borsh decode: JS holds only head facts while the entry block bytes stay
484
+ * in wasm memory until `prepareStashedRawReceive*` consumes them there — or a
485
+ * fallback path materializes them per head.
486
+ */
487
+ export class StashBackedRawExchangeHeadsMessage extends RawExchangeHeadsMessage {
488
+ readonly messageId: Uint8Array;
489
+ /** Wasm-to-JS head byte materializations (0 on the fully fused path). */
490
+ bytesMaterializedCount = 0;
491
+ private released = false;
492
+ private readonly stash: RawExchangeHeadsStash;
493
+ private readonly resolveReleasedBlock?: (
494
+ hash: string,
495
+ ) => Uint8Array | undefined;
496
+
497
+ constructor(properties: {
498
+ messageId: Uint8Array;
499
+ hashes: string[];
500
+ gidRefrences: string[][];
501
+ byteLengths: Uint32Array;
502
+ reserved: Uint8Array;
503
+ stash: RawExchangeHeadsStash;
504
+ resolveReleasedBlock?: (hash: string) => Uint8Array | undefined;
505
+ }) {
506
+ const heads = new Array<RawEntryWithRefs>(properties.hashes.length);
507
+ super({ heads, reserved: properties.reserved });
508
+ this.messageId = properties.messageId;
509
+ this.stash = properties.stash;
510
+ this.resolveReleasedBlock = properties.resolveReleasedBlock;
511
+ for (let i = 0; i < properties.hashes.length; i++) {
512
+ heads[i] = new StashBackedRawEntryWithRefs(
513
+ this,
514
+ i,
515
+ properties.hashes[i]!,
516
+ properties.gidRefrences[i] ?? [],
517
+ properties.byteLengths[i]!,
518
+ ) as unknown as RawEntryWithRefs;
519
+ }
520
+ }
521
+
522
+ materializeHeadBytes(index: number, hash: string): Uint8Array {
523
+ if (!this.released) {
524
+ const bytes = this.stash.stashedBlocks(
525
+ this.messageId,
526
+ Uint32Array.of(index),
527
+ )?.[0];
528
+ if (bytes) {
529
+ this.bytesMaterializedCount++;
530
+ return bytes;
531
+ }
532
+ }
533
+ const bytes = this.resolveReleasedBlock?.(hash);
534
+ if (bytes) {
535
+ this.bytesMaterializedCount++;
536
+ return bytes;
537
+ }
538
+ throw new Error(
539
+ "Stashed raw exchange head bytes are no longer available: " + hash,
540
+ );
541
+ }
542
+
543
+ release(): boolean {
544
+ if (this.released) {
545
+ return false;
546
+ }
547
+ this.released = true;
548
+ return this.stash.release(this.messageId);
549
+ }
550
+ }
551
+
552
+ export const isStashBackedRawExchangeHeadsMessage = (
553
+ message: TransportMessage,
554
+ ): message is StashBackedRawExchangeHeadsMessage =>
555
+ message instanceof StashBackedRawExchangeHeadsMessage;
556
+
557
+ export const getRawExchangeHeadByteLength = (head: RawEntryWithRefs): number =>
558
+ head instanceof StashBackedRawEntryWithRefs
559
+ ? head.byteLength
560
+ : head.bytes.byteLength;
561
+
562
+ /**
563
+ * Stash indexes for a subset of a stash-backed message's heads (in subset
564
+ * order), or undefined when any head is not stash-backed.
565
+ */
566
+ export const getRawExchangeHeadStashIndexes = (
567
+ heads: RawEntryWithRefs[],
568
+ ): Uint32Array | undefined => {
569
+ const indexes = new Uint32Array(heads.length);
570
+ for (let i = 0; i < heads.length; i++) {
571
+ const head = heads[i]!;
572
+ if (!(head instanceof StashBackedRawEntryWithRefs)) {
573
+ return undefined;
574
+ }
575
+ indexes[i] = head.stashIndex;
576
+ }
577
+ return indexes;
578
+ };
579
+
580
+ export type RawReceiveHashSelection =
581
+ | Iterable<string>
582
+ | {
583
+ hashes?: Iterable<string>;
584
+ indexes?: Iterable<number>;
585
+ droppedIndexes?: Iterable<number>;
586
+ };
587
+
588
+ const isIterableRawReceiveHashSelection = (
589
+ selection: RawReceiveHashSelection,
590
+ ): selection is Iterable<string> =>
591
+ typeof (selection as Iterable<string>)[Symbol.iterator] === "function";
592
+
593
+ type RawExchangeEntryMaterializeOptions<T> = {
594
+ bytes: Uint8Array;
595
+ hash: string;
596
+ gidRefrences: string[];
597
+ size: number;
598
+ keychain: unknown;
599
+ encoding: Log<T>["encoding"];
600
+ };
601
+
602
+ const materializeRawExchangeEntry = <T>(
603
+ options: RawExchangeEntryMaterializeOptions<T>,
604
+ ): Entry<T> => {
605
+ const entry = deserialize(options.bytes, Entry) as Entry<T>;
606
+ (entry as unknown as { hash: string | undefined }).hash = undefined;
607
+ Entry.prepareMultihashBytes(entry, options.bytes, options.hash);
608
+ entry.hash = options.hash;
609
+ entry.size = options.size;
610
+ entry.init({
611
+ keychain: options.keychain as any,
612
+ encoding: options.encoding,
613
+ });
614
+ prepareRawExchangeHeadEntryFacts(entry, {
615
+ hash: options.hash,
616
+ bytes: options.bytes,
617
+ gidRefrences: options.gidRefrences,
618
+ });
619
+ return entry;
620
+ };
621
+
622
+ class PreparedRawExchangeEntry<T> extends Entry<T> {
623
+ hash!: string;
624
+ size: number;
625
+ createdLocally?: boolean;
626
+
627
+ private materialized?: Entry<T>;
628
+ private metaValue?: Meta;
629
+ private shallowHeadValue?: ShallowEntry;
630
+ private keychain?: unknown;
631
+ private encodingValue?: Log<T>["encoding"];
632
+ private bytesValue?: Uint8Array;
633
+
634
+ constructor(
635
+ private readonly bytesSource: () => Uint8Array,
636
+ private readonly facts: PreparedRawEntryV0FactsSource,
637
+ private readonly factsIndex = 0,
638
+ private readonly onJsEntryDecode?: () => void,
639
+ ) {
640
+ super();
641
+ this.size = preparedRawByteLength(facts, factsIndex);
642
+ }
643
+
644
+ /**
645
+ * Lazy: stash-backed heads keep the block bytes in wasm memory; they are
646
+ * copied out only when a consumer actually needs them (payload/signature
647
+ * materialization, storage bytes).
648
+ */
649
+ private get bytes(): Uint8Array {
650
+ return (this.bytesValue ??= this.bytesSource());
651
+ }
652
+
653
+ get meta(): Meta {
654
+ return (this.metaValue ??= new Meta({
655
+ gid: preparedRawGid(this.facts, this.factsIndex),
656
+ clock: new Clock({
657
+ id: preparedRawClockId(this.facts, this.factsIndex),
658
+ timestamp: new Timestamp({
659
+ wallTime: preparedRawWallTime(this.facts, this.factsIndex),
660
+ logical: preparedRawLogical(this.facts, this.factsIndex),
661
+ }),
662
+ }),
663
+ next: preparedRawNext(this.facts, this.factsIndex),
664
+ type: preparedRawType(this.facts, this.factsIndex) as EntryType,
665
+ data: preparedRawMetaData(this.facts, this.factsIndex),
666
+ }));
667
+ }
668
+
669
+ set meta(value: Meta) {
670
+ this.metaValue = value;
671
+ }
672
+
673
+ init(props: any): this {
674
+ this.keychain = props.keychain ?? props._keychain;
675
+ this.encodingValue = props.encoding ?? props._encoding;
676
+ this.materialized?.init(props);
677
+ return this;
678
+ }
679
+
680
+ private get encoding(): Log<T>["encoding"] {
681
+ if (!this.encodingValue) {
682
+ throw new Error("Not initialized");
683
+ }
684
+ return this.encodingValue;
685
+ }
686
+
687
+ get payload() {
688
+ return this.materialize().payload;
689
+ }
690
+
691
+ get signatures() {
692
+ return this.materialize().signatures;
693
+ }
694
+
695
+ get publicKeys() {
696
+ return this.materialize().publicKeys;
697
+ }
698
+
699
+ get __peerbitSignatureVerified() {
700
+ return preparedRawSignatureVerified(this.facts, this.factsIndex);
701
+ }
702
+
703
+ get __peerbitRequestedReplicas() {
704
+ return preparedRawRequestedReplicas(this.facts, this.factsIndex);
705
+ }
706
+
707
+ get __peerbitHashNumber() {
708
+ return preparedRawHashNumber(this.facts, this.factsIndex);
709
+ }
710
+
711
+ get __peerbitGid() {
712
+ return preparedRawGid(this.facts, this.factsIndex);
713
+ }
714
+
715
+ get __peerbitWallTime() {
716
+ return preparedRawWallTime(this.facts, this.factsIndex);
717
+ }
718
+
719
+ get __peerbitLogical() {
720
+ return preparedRawLogical(this.facts, this.factsIndex);
721
+ }
722
+
723
+ get __peerbitNext() {
724
+ return preparedRawNext(this.facts, this.factsIndex);
725
+ }
726
+
727
+ getMeta(): Meta {
728
+ return this.meta;
729
+ }
730
+
731
+ getMetaBytes(): Uint8Array | undefined {
732
+ return preparedRawMetaBytes(this.facts, this.factsIndex);
733
+ }
734
+
735
+ getHashDigestBytes(): Uint8Array {
736
+ return preparedRawHashDigestBytes(
737
+ this.facts,
738
+ this.factsIndex,
739
+ this.hash,
740
+ );
741
+ }
742
+
743
+ getClock(): Clock {
744
+ return this.meta.clock;
745
+ }
746
+
747
+ getNext(): string[] {
748
+ return this.meta.next;
749
+ }
750
+
751
+ getSignatures() {
752
+ return this.materialize().getSignatures();
753
+ }
754
+
755
+ getPayloadValue(): Promise<T> | T {
756
+ return this.materialize().getPayloadValue();
757
+ }
758
+
759
+ override getStorageBytes(): Uint8Array {
760
+ return this.bytes;
761
+ }
762
+
763
+ async verifySignatures(): Promise<boolean> {
764
+ if (preparedRawSignatureVerified(this.facts, this.factsIndex)) {
765
+ return true;
766
+ }
767
+ try {
768
+ const result = await verifyEntryV0Ed25519StorageBatch([this.bytes]);
769
+ if (result) {
770
+ return result.every(Boolean);
771
+ }
772
+ } catch {
773
+ // Fall back to full materialization for encrypted or non-Ed25519 entries.
774
+ }
775
+ return this.materialize().verifySignatures();
776
+ }
777
+
778
+ equals(other: Entry<T>): boolean {
779
+ return this.hash === other.hash || this.materialize().equals(other);
780
+ }
781
+
782
+ toSignable(): Entry<T> {
783
+ return this.materialize().toSignable();
784
+ }
785
+
786
+ toShallow(isHead: boolean): ShallowEntry {
787
+ if (isHead && this.shallowHeadValue) {
788
+ this.shallowHeadValue.head = true;
789
+ return this.shallowHeadValue;
790
+ }
791
+ const clock = this.getClock();
792
+ const shallow = attachPreparedRawShallowFacts(
793
+ new ShallowEntry({
794
+ hash: this.hash,
795
+ payloadSize: preparedRawPayloadByteLength(
796
+ this.facts,
797
+ this.factsIndex,
798
+ ),
799
+ head: isHead,
800
+ meta: new ShallowMeta({
801
+ gid: preparedRawGid(this.facts, this.factsIndex),
802
+ data: preparedRawMetaData(this.facts, this.factsIndex),
803
+ clock,
804
+ next: preparedRawNext(this.facts, this.factsIndex),
805
+ type: preparedRawType(this.facts, this.factsIndex) as EntryType,
806
+ }),
807
+ }),
808
+ this.facts,
809
+ this.factsIndex,
810
+ this.hash,
811
+ );
812
+ if (isHead) {
813
+ this.shallowHeadValue = shallow;
814
+ }
815
+ return shallow;
816
+ }
817
+
818
+ toPreparedAppendJoinFacts(): PreparedAppendJoinFacts {
819
+ const shallow = this.toShallow(true);
820
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
821
+ const self = this;
822
+ return {
823
+ hash: this.hash,
824
+ // Lazy: stash-backed heads keep entry bytes in wasm memory and the
825
+ // native prepared-join commit never reads them in JS.
826
+ get bytes() {
827
+ return self.bytes;
828
+ },
829
+ byteLength: this.size,
830
+ meta: shallow.meta,
831
+ getShallowEntry: (isHead = true) => this.toShallow(isHead),
832
+ materializeEntry: () => this,
833
+ };
834
+ }
835
+
836
+ private materialize(): Entry<T> {
837
+ if (this.materialized) {
838
+ return this.materialized;
839
+ }
840
+ this.onJsEntryDecode?.();
841
+ const entry = materializeRawExchangeEntry<T>({
842
+ hash: this.hash,
843
+ bytes: this.bytes,
844
+ size: this.size,
845
+ keychain: this.keychain,
846
+ encoding: this.encoding,
847
+ gidRefrences: [],
848
+ });
849
+ this.materialized = entry;
850
+ return entry;
851
+ }
852
+ }
853
+
854
+ class PreparedRawEntryWithRefs<T> {
855
+ readonly gidRefrences: string[];
856
+ private entryValue?: PreparedRawExchangeEntry<T>;
857
+ private shallowHeadValue?: ShallowEntry;
858
+ private keychain?: unknown;
859
+ private encodingValue?: Log<T>["encoding"];
860
+
861
+ constructor(
862
+ private readonly head: RawEntryWithRefs,
863
+ private readonly facts: PreparedRawEntryV0FactsSource,
864
+ private readonly factsIndex = 0,
865
+ private readonly onJsEntryDecode?: () => void,
866
+ ) {
867
+ this.gidRefrences = head.gidRefrences;
868
+ }
869
+
870
+ get entry(): Entry<T> {
871
+ if (!this.entryValue) {
872
+ const head = this.head;
873
+ const entry = new PreparedRawExchangeEntry<T>(
874
+ () => head.bytes,
875
+ this.facts,
876
+ this.factsIndex,
877
+ this.onJsEntryDecode,
878
+ );
879
+ // Lazy block registration: stash-backed heads keep the bytes in
880
+ // wasm memory until a consumer (block store put, payload
881
+ // materialization) actually pulls them.
882
+ Entry.prepareMultihashBytesLazy(
883
+ entry,
884
+ head.hash,
885
+ preparedRawByteLength(this.facts, this.factsIndex),
886
+ () => head.bytes,
887
+ );
888
+ entry.hash = head.hash;
889
+ entry.size = preparedRawByteLength(this.facts, this.factsIndex);
890
+ if (this.keychain || this.encodingValue) {
891
+ entry.init({
892
+ keychain: this.keychain as any,
893
+ encoding: this.encodingValue,
894
+ });
895
+ }
896
+ this.entryValue = entry;
897
+ }
898
+ return this.entryValue;
899
+ }
900
+
901
+ set entry(value: Entry<T>) {
902
+ if (value) {
903
+ this.entryValue = value as PreparedRawExchangeEntry<T>;
904
+ }
905
+ }
906
+
907
+ initEntry(props: any): void {
908
+ this.keychain = props.keychain ?? props._keychain;
909
+ this.encodingValue = props.encoding ?? props._encoding;
910
+ this.entryValue?.init(props);
911
+ }
912
+
913
+ get hash(): string {
914
+ return this.head.hash;
915
+ }
916
+
917
+ get preparedGid(): string {
918
+ return preparedRawGid(this.facts, this.factsIndex);
919
+ }
920
+
921
+ get preparedRequestedReplicas(): number | undefined {
922
+ return preparedRawRequestedReplicas(this.facts, this.factsIndex);
923
+ }
924
+
925
+ get preparedSignatureVerified(): boolean {
926
+ return preparedRawSignatureVerified(this.facts, this.factsIndex);
927
+ }
928
+
929
+ toShallow(isHead = true): ShallowEntry {
930
+ if (isHead && this.shallowHeadValue) {
931
+ this.shallowHeadValue.head = true;
932
+ return this.shallowHeadValue;
933
+ }
934
+ const clock = new Clock({
935
+ id: preparedRawClockId(this.facts, this.factsIndex),
936
+ timestamp: new Timestamp({
937
+ wallTime: preparedRawWallTime(this.facts, this.factsIndex),
938
+ logical: preparedRawLogical(this.facts, this.factsIndex),
939
+ }),
940
+ });
941
+ const shallow = attachPreparedRawShallowFacts(
942
+ new ShallowEntry({
943
+ hash: this.head.hash,
944
+ payloadSize: preparedRawPayloadByteLength(
945
+ this.facts,
946
+ this.factsIndex,
947
+ ),
948
+ head: isHead,
949
+ meta: new ShallowMeta({
950
+ gid: preparedRawGid(this.facts, this.factsIndex),
951
+ data: preparedRawMetaData(this.facts, this.factsIndex),
952
+ clock,
953
+ next: preparedRawNext(this.facts, this.factsIndex),
954
+ type: preparedRawType(this.facts, this.factsIndex) as EntryType,
955
+ }),
956
+ }),
957
+ this.facts,
958
+ this.factsIndex,
959
+ this.head.hash,
960
+ );
961
+ if (isHead) {
962
+ this.shallowHeadValue = shallow;
963
+ }
964
+ return shallow;
965
+ }
966
+
967
+ toPreparedAppendJoinFacts(): PreparedAppendJoinFacts {
968
+ const shallow = this.toShallow(true);
969
+ const head = this.head;
970
+ return {
971
+ hash: head.hash,
972
+ // Lazy: stash-backed heads keep entry bytes in wasm memory and the
973
+ // native prepared-join commit never reads them in JS.
974
+ get bytes() {
975
+ return head.bytes;
976
+ },
977
+ byteLength: preparedRawByteLength(this.facts, this.factsIndex),
978
+ meta: shallow.meta,
979
+ getShallowEntry: (isHead = true) => this.toShallow(isHead),
980
+ materializeEntry: () => this.entry,
981
+ };
43
982
  }
44
983
  }
45
984
 
985
+ export const isPreparedRawEntryWithRefs = (
986
+ head: EntryWithRefs<any>,
987
+ ): head is EntryWithRefs<any> & PreparedRawEntryWithRefs<any> =>
988
+ head instanceof PreparedRawEntryWithRefs;
989
+
990
+ export const getExchangeHeadHash = (head: EntryWithRefs<any>): string =>
991
+ isPreparedRawEntryWithRefs(head) ? head.hash : head.entry.hash;
992
+
993
+ export const initExchangeHeadEntry = (
994
+ head: EntryWithRefs<any>,
995
+ props: any,
996
+ ): void => {
997
+ if (isPreparedRawEntryWithRefs(head)) {
998
+ head.initEntry(props);
999
+ return;
1000
+ }
1001
+ head.entry.init(props);
1002
+ };
1003
+
1004
+ export const getPreparedRawExchangeHeadGid = (
1005
+ head: EntryWithRefs<any>,
1006
+ ): string | undefined =>
1007
+ isPreparedRawEntryWithRefs(head) ? head.preparedGid : undefined;
1008
+
1009
+ export const getPreparedRawExchangeHeadRequestedReplicas = (
1010
+ head: EntryWithRefs<any>,
1011
+ ): number | undefined =>
1012
+ isPreparedRawEntryWithRefs(head) ? head.preparedRequestedReplicas : undefined;
1013
+
1014
+ export const getPreparedRawExchangeHeadSignatureVerified = (
1015
+ head: EntryWithRefs<any>,
1016
+ ): boolean | undefined =>
1017
+ isPreparedRawEntryWithRefs(head) ? head.preparedSignatureVerified : undefined;
1018
+
1019
+ export const getPreparedRawExchangeHeadShallowEntry = (
1020
+ head: EntryWithRefs<any>,
1021
+ ): ShallowEntry | undefined =>
1022
+ isPreparedRawEntryWithRefs(head) ? head.toShallow(true) : undefined;
1023
+
1024
+ export const getPreparedRawExchangeHeadAppendFacts = (
1025
+ head: EntryWithRefs<any>,
1026
+ ): PreparedAppendJoinFacts | undefined =>
1027
+ isPreparedRawEntryWithRefs(head)
1028
+ ? head.toPreparedAppendJoinFacts()
1029
+ : getPreparedRawExchangeAppendFacts(head.entry);
1030
+
1031
+ export const getPreparedRawExchangeAppendFacts = (
1032
+ entry: Entry<any>,
1033
+ ): PreparedAppendJoinFacts | undefined =>
1034
+ entry instanceof PreparedRawExchangeEntry
1035
+ ? entry.toPreparedAppendJoinFacts()
1036
+ : undefined;
1037
+
1038
+ export const getPreparedRawExchangeRequestedReplicas = (
1039
+ entry: Entry<any>,
1040
+ ): number | undefined =>
1041
+ entry instanceof PreparedRawExchangeEntry
1042
+ ? entry.__peerbitRequestedReplicas
1043
+ : undefined;
1044
+
1045
+ export const getPreparedRawExchangeHashNumber = (
1046
+ entry: Entry<any>,
1047
+ ): string | bigint | undefined =>
1048
+ entry instanceof PreparedRawExchangeEntry
1049
+ ? entry.__peerbitHashNumber
1050
+ : undefined;
1051
+
1052
+ export const getPreparedRawExchangeGid = (
1053
+ entry: Entry<any>,
1054
+ ): string | undefined =>
1055
+ entry instanceof PreparedRawExchangeEntry ? entry.__peerbitGid : undefined;
1056
+
1057
+ export const getPreparedRawExchangeTimestamp = (
1058
+ entry: Entry<any>,
1059
+ ): { wallTime: bigint; logical: number } | undefined =>
1060
+ entry instanceof PreparedRawExchangeEntry
1061
+ ? {
1062
+ wallTime: entry.__peerbitWallTime,
1063
+ logical: entry.__peerbitLogical,
1064
+ }
1065
+ : undefined;
1066
+
1067
+ export const getPreparedRawExchangeNext = (
1068
+ entry: Entry<any>,
1069
+ ): string[] | undefined =>
1070
+ entry instanceof PreparedRawExchangeEntry ? entry.__peerbitNext : undefined;
1071
+
46
1072
  @variant([0, 3])
47
1073
  export class RequestIPrune extends TransportMessage {
48
1074
  // Hashes which I want to prune
@@ -68,6 +1094,8 @@ export class ResponseIPrune extends TransportMessage {
68
1094
  }
69
1095
 
70
1096
  const MAX_EXCHANGE_MESSAGE_SIZE = 1e5; // 100kb. Too large size might not be faster (even if we can do 5mb)
1097
+ export const MAX_RAW_EXCHANGE_MESSAGE_SIZE = 512 * 1024;
1098
+ export const EXCHANGE_HEADS_RESOLVE_BATCH_SIZE = 256;
71
1099
 
72
1100
  export const createExchangeHeadsMessages = async function* (
73
1101
  log: Log<any>,
@@ -76,42 +1104,120 @@ export const createExchangeHeadsMessages = async function* (
76
1104
  let size = 0;
77
1105
  let current: EntryWithRefs<any>[] = [];
78
1106
  const visitedHeads = new Set<string>();
79
- for (const fromHead of heads) {
80
- let entry = fromHead instanceof Entry ? fromHead : await log.get(fromHead);
81
- if (!entry) {
82
- continue; // missing this entry, could be deleted while iterating
83
- }
1107
+ const headArray = Array.isArray(heads) ? heads : [...heads];
1108
+ const canUseNativeReferenceGids = headArray.length === 1;
84
1109
 
85
- visitedHeads.add(entry.hash);
1110
+ for (
1111
+ let offset = 0;
1112
+ offset < headArray.length;
1113
+ offset += EXCHANGE_HEADS_RESOLVE_BATCH_SIZE
1114
+ ) {
1115
+ const headBatch = headArray.slice(
1116
+ offset,
1117
+ offset + EXCHANGE_HEADS_RESOLVE_BATCH_SIZE,
1118
+ );
1119
+ const nativeReferenceRowsByPosition =
1120
+ canUseNativeReferenceGids === false
1121
+ ? getNativeReferenceRowsByHeadInput(log, headBatch, visitedHeads)
1122
+ : undefined;
1123
+ const resolvedHeads = await resolveExchangeHeadEntries(
1124
+ log,
1125
+ headBatch,
1126
+ visitedHeads,
1127
+ );
1128
+ for (let i = 0; i < resolvedHeads.length; i++) {
1129
+ const entry = resolvedHeads[i];
1130
+ if (!entry) {
1131
+ continue; // missing this entry, could be deleted while iterating
1132
+ }
86
1133
 
87
- // TODO eventually we don't want to load all refs
88
- // since majority of the old leader would not be interested in these anymore
89
- const refs = (await allEntriesWithUniqueGids(log, entry)).filter((x) => {
90
- if (visitedHeads.has(x.hash)) {
91
- return false;
1134
+ if (visitedHeads.has(entry.hash)) {
1135
+ continue;
92
1136
  }
93
- visitedHeads.add(x.hash);
94
- return true;
95
- });
1137
+ visitedHeads.add(entry.hash);
96
1138
 
97
- if (refs.length > 1000) {
98
- warn("Large refs count: ", refs.length);
99
- }
100
- current.push(
101
- new EntryWithRefs({
102
- entry,
103
- gidRefrences: refs.map((x) => x.meta.gid),
104
- }),
105
- );
1139
+ const nativeGidReferences = canUseNativeReferenceGids
1140
+ ? log.entryIndex.getUniqueReferenceGids(entry.hash)
1141
+ : undefined;
1142
+ if (nativeGidReferences) {
1143
+ if (nativeGidReferences.length > 1000) {
1144
+ warn("Large refs count: ", nativeGidReferences.length);
1145
+ }
1146
+ current.push(
1147
+ new EntryWithRefs({
1148
+ entry,
1149
+ gidRefrences: nativeGidReferences,
1150
+ }),
1151
+ );
1152
+ size += entry.size;
1153
+ if (size > MAX_EXCHANGE_MESSAGE_SIZE) {
1154
+ size = 0;
1155
+ yield new ExchangeHeadsMessage({
1156
+ heads: current,
1157
+ });
1158
+ current = [];
1159
+ }
1160
+ continue;
1161
+ }
106
1162
 
107
- size += entry.size;
108
- if (size > MAX_EXCHANGE_MESSAGE_SIZE) {
109
- size = 0;
110
- yield new ExchangeHeadsMessage({
111
- heads: current,
1163
+ const nativeReferenceRows = nativeReferenceRowsByPosition?.[i];
1164
+ if (nativeReferenceRows) {
1165
+ const gidRefrences: string[] = [];
1166
+ for (const [hash, gid] of nativeReferenceRows) {
1167
+ if (visitedHeads.has(hash)) {
1168
+ continue;
1169
+ }
1170
+ visitedHeads.add(hash);
1171
+ gidRefrences.push(gid);
1172
+ }
1173
+ if (gidRefrences.length > 1000) {
1174
+ warn("Large refs count: ", gidRefrences.length);
1175
+ }
1176
+ current.push(
1177
+ new EntryWithRefs({
1178
+ entry,
1179
+ gidRefrences,
1180
+ }),
1181
+ );
1182
+ size += entry.size;
1183
+ if (size > MAX_EXCHANGE_MESSAGE_SIZE) {
1184
+ size = 0;
1185
+ yield new ExchangeHeadsMessage({
1186
+ heads: current,
1187
+ });
1188
+ current = [];
1189
+ }
1190
+ continue;
1191
+ }
1192
+
1193
+ // Fallback for logs without native reference rows.
1194
+ const refs = (await allEntriesWithUniqueGids(log, entry)).filter((x) => {
1195
+ if (visitedHeads.has(x.hash)) {
1196
+ return false;
1197
+ }
1198
+ visitedHeads.add(x.hash);
1199
+ return true;
112
1200
  });
113
- current = [];
114
- continue;
1201
+
1202
+ if (refs.length > 1000) {
1203
+ warn("Large refs count: ", refs.length);
1204
+ }
1205
+ current.push(
1206
+ new EntryWithRefs({
1207
+ entry,
1208
+ gidRefrences: refs.map((x) => x.meta.gid),
1209
+ }),
1210
+ );
1211
+
1212
+ size += entry.size;
1213
+ if (size > MAX_EXCHANGE_MESSAGE_SIZE) {
1214
+ size = 0;
1215
+ yield new ExchangeHeadsMessage({
1216
+ heads: current,
1217
+ });
1218
+ current = [];
1219
+ continue;
1220
+ }
115
1221
  }
116
1222
  }
117
1223
  if (current.length > 0) {
@@ -121,6 +1227,862 @@ export const createExchangeHeadsMessages = async function* (
121
1227
  }
122
1228
  };
123
1229
 
1230
+ export const createRawExchangeHeadsMessages = async function* (
1231
+ log: Log<any>,
1232
+ heads: string[] | Set<string>,
1233
+ profile?: SyncProfileFn,
1234
+ ): AsyncGenerator<RawExchangeHeadsMessage | ExchangeHeadsMessage<any>, void, void> {
1235
+ let size = 0;
1236
+ let current: RawEntryWithRefs[] = [];
1237
+ const visitedHeads = new Set<string>();
1238
+ const headArray = Array.isArray(heads) ? heads : [...heads];
1239
+ // This path materializes entry block bytes as JS values (log.blocks reads)
1240
+ // and TS-serializes the message; the fused wasm send path replaces it.
1241
+ // The event makes the remaining JS-side outbound block copies countable.
1242
+ const emitJsBlockBytes = (heads: RawEntryWithRefs[], bytes: number) => {
1243
+ if (profile) {
1244
+ emitSyncProfileEvent(profile, {
1245
+ name: "sharedLog.rawSend.jsBlockBytes",
1246
+ component: "shared-log",
1247
+ entries: heads.length,
1248
+ bytes,
1249
+ messages: 1,
1250
+ });
1251
+ }
1252
+ };
1253
+
1254
+ for (let offset = 0; offset < headArray.length; offset += EXCHANGE_HEADS_RESOLVE_BATCH_SIZE) {
1255
+ const headBatch = headArray.slice(
1256
+ offset,
1257
+ offset + EXCHANGE_HEADS_RESOLVE_BATCH_SIZE,
1258
+ );
1259
+ const nativeReferenceRowsByPosition = getNativeReferenceRowsByHeadInput(
1260
+ log,
1261
+ headBatch,
1262
+ visitedHeads,
1263
+ );
1264
+ if (!nativeReferenceRowsByPosition) {
1265
+ for await (const message of createExchangeHeadsMessages(log, headBatch)) {
1266
+ yield message;
1267
+ }
1268
+ continue;
1269
+ }
1270
+ const blockRows = await resolveExchangeHeadBlocks(
1271
+ log,
1272
+ headBatch,
1273
+ visitedHeads,
1274
+ );
1275
+ if (!blockRows) {
1276
+ for await (const message of createExchangeHeadsMessages(log, headBatch)) {
1277
+ yield message;
1278
+ }
1279
+ continue;
1280
+ }
1281
+
1282
+ for (let i = 0; i < blockRows.length; i++) {
1283
+ const block = blockRows[i];
1284
+ if (!block) {
1285
+ continue;
1286
+ }
1287
+ if (visitedHeads.has(block.hash)) {
1288
+ continue;
1289
+ }
1290
+ visitedHeads.add(block.hash);
1291
+
1292
+ const nativeReferenceRows = nativeReferenceRowsByPosition[i];
1293
+ if (!nativeReferenceRows) {
1294
+ continue;
1295
+ }
1296
+ const gidRefrences: string[] = [];
1297
+ for (const [hash, gid] of nativeReferenceRows) {
1298
+ if (visitedHeads.has(hash)) {
1299
+ continue;
1300
+ }
1301
+ visitedHeads.add(hash);
1302
+ gidRefrences.push(gid);
1303
+ }
1304
+ if (gidRefrences.length > 1000) {
1305
+ warn("Large refs count: ", gidRefrences.length);
1306
+ }
1307
+ current.push(
1308
+ new RawEntryWithRefs({
1309
+ hash: block.hash,
1310
+ bytes: block.bytes,
1311
+ gidRefrences,
1312
+ }),
1313
+ );
1314
+ size += block.bytes.byteLength;
1315
+ if (size > MAX_RAW_EXCHANGE_MESSAGE_SIZE) {
1316
+ emitJsBlockBytes(current, size);
1317
+ size = 0;
1318
+ yield new RawExchangeHeadsMessage({ heads: current });
1319
+ current = [];
1320
+ }
1321
+ }
1322
+ }
1323
+ if (current.length > 0) {
1324
+ emitJsBlockBytes(current, size);
1325
+ yield new RawExchangeHeadsMessage({ heads: current });
1326
+ }
1327
+ };
1328
+
1329
+ export type RawExchangeHeadSendPlan = {
1330
+ hashes: string[];
1331
+ gidRefrences: string[][];
1332
+ };
1333
+
1334
+ /**
1335
+ * The head/reference selection of {@link createRawExchangeHeadsMessages}
1336
+ * without resolving any block bytes: same visited-head deduplication, same
1337
+ * reference-gid collection, same batching of the native index lookups. Used
1338
+ * by the fused send path, where the block bytes stay in the native store and
1339
+ * the payload is serialized in wasm. Returns `undefined` when the log has no
1340
+ * native reference rows (callers fall back to the TS message path).
1341
+ */
1342
+ export const collectRawExchangeHeadSendPlan = (
1343
+ log: Log<any>,
1344
+ heads: string[] | Set<string>,
1345
+ ): RawExchangeHeadSendPlan | undefined => {
1346
+ const headArray = Array.isArray(heads) ? heads : [...heads];
1347
+ const visitedHeads = new Set<string>();
1348
+ const hashes: string[] = [];
1349
+ const gidRefrences: string[][] = [];
1350
+ for (
1351
+ let offset = 0;
1352
+ offset < headArray.length;
1353
+ offset += EXCHANGE_HEADS_RESOLVE_BATCH_SIZE
1354
+ ) {
1355
+ const headBatch = headArray.slice(
1356
+ offset,
1357
+ offset + EXCHANGE_HEADS_RESOLVE_BATCH_SIZE,
1358
+ );
1359
+ const nativeReferenceRowsByPosition = getNativeReferenceRowsByHeadInput(
1360
+ log,
1361
+ headBatch,
1362
+ visitedHeads,
1363
+ );
1364
+ if (!nativeReferenceRowsByPosition) {
1365
+ return undefined;
1366
+ }
1367
+ for (let i = 0; i < headBatch.length; i++) {
1368
+ const hash = headBatch[i]!;
1369
+ if (visitedHeads.has(hash)) {
1370
+ continue;
1371
+ }
1372
+ visitedHeads.add(hash);
1373
+ const nativeReferenceRows = nativeReferenceRowsByPosition[i];
1374
+ if (!nativeReferenceRows) {
1375
+ continue;
1376
+ }
1377
+ const refs: string[] = [];
1378
+ for (const [refHash, gid] of nativeReferenceRows) {
1379
+ if (visitedHeads.has(refHash)) {
1380
+ continue;
1381
+ }
1382
+ visitedHeads.add(refHash);
1383
+ refs.push(gid);
1384
+ }
1385
+ if (refs.length > 1000) {
1386
+ warn("Large refs count: ", refs.length);
1387
+ }
1388
+ hashes.push(hash);
1389
+ gidRefrences.push(refs);
1390
+ }
1391
+ }
1392
+ return { hashes, gidRefrences };
1393
+ };
1394
+
1395
+ export const materializeRawExchangeHeadsMessage = (
1396
+ message: RawExchangeHeadsMessage,
1397
+ log: Log<any>,
1398
+ ): ExchangeHeadsMessage<any> => {
1399
+ const materialized = new ExchangeHeadsMessage({
1400
+ heads: message.heads.map((head) => {
1401
+ const entry = deserialize(head.bytes, Entry) as Entry<any>;
1402
+ entry.hash = head.hash;
1403
+ entry.size = head.bytes.byteLength;
1404
+ entry.init({
1405
+ keychain: log.keychain,
1406
+ encoding: log.encoding,
1407
+ });
1408
+ return new EntryWithRefs({
1409
+ entry,
1410
+ gidRefrences: head.gidRefrences,
1411
+ });
1412
+ }),
1413
+ });
1414
+ materialized.reserved = message.reserved;
1415
+ return materialized;
1416
+ };
1417
+
1418
+ const prepareRawExchangeHeadEntryFacts = (
1419
+ entry: Entry<any>,
1420
+ head: RawEntryWithRefs,
1421
+ ) => {
1422
+ const meta = entry.meta;
1423
+ const payload = entry.payload;
1424
+ const payloadSize = payload.byteLength;
1425
+ const shallowEntry = new ShallowEntry({
1426
+ hash: head.hash,
1427
+ payloadSize,
1428
+ head: true,
1429
+ meta: new ShallowMeta({
1430
+ gid: meta.gid,
1431
+ data: meta.data,
1432
+ clock: meta.clock,
1433
+ next: meta.next,
1434
+ type: meta.type,
1435
+ }),
1436
+ });
1437
+ const nativeEntry: PreparedNativeLogEntry = {
1438
+ hash: head.hash,
1439
+ gid: meta.gid,
1440
+ next: meta.next,
1441
+ type: meta.type,
1442
+ head: true,
1443
+ payloadSize,
1444
+ data: meta.data,
1445
+ clock: {
1446
+ timestamp: {
1447
+ wallTime: meta.clock.timestamp.wallTime,
1448
+ logical: meta.clock.timestamp.logical,
1449
+ },
1450
+ },
1451
+ };
1452
+ Entry.prepareShallowEntry(entry, shallowEntry);
1453
+ Entry.prepareNativeLogEntry(entry, nativeEntry);
1454
+ };
1455
+
1456
+ export const materializeVerifiedRawExchangeHeadsMessage = async (
1457
+ message: RawExchangeHeadsMessage,
1458
+ log: Log<any>,
1459
+ profile?: SyncProfileFn,
1460
+ options?: {
1461
+ nativeBackbone?: RawReceiveNativeBackbone;
1462
+ verifyNativeBackboneSignaturesDuringPrepare?: boolean;
1463
+ deferNativeBackboneSignatureVerificationUntilSelection?: boolean;
1464
+ deferNativeBackboneSignatureVerificationUntilCommit?: boolean;
1465
+ prepareNativeBackboneExpectedColumnsAndSelection?: (properties: {
1466
+ /** Lazy so the fused (stash-backed) path never builds JS block arrays. */
1467
+ blocks: () => Uint8Array[];
1468
+ hashes: string[];
1469
+ verifySignatures: boolean;
1470
+ }) =>
1471
+ | { columns: PreparedRawEntryV0FactsColumns }
1472
+ | undefined
1473
+ | Promise<{ columns: PreparedRawEntryV0FactsColumns } | undefined>;
1474
+ /**
1475
+ * Stash-backed expected-columns prepare (blocks stay in wasm memory).
1476
+ * Tried before the blocks-based nativeBackbone variants; undefined
1477
+ * falls through to them.
1478
+ */
1479
+ prepareNativeBackboneExpectedColumns?: (properties: {
1480
+ hashes: string[];
1481
+ verifySignatures: boolean;
1482
+ }) => PreparedRawEntryV0FactsColumns | undefined;
1483
+ tryPreparedRawReceiveFastDrop?: (properties: {
1484
+ heads: RawEntryWithRefs[];
1485
+ hashes: string[];
1486
+ }) => boolean | Promise<boolean>;
1487
+ selectPreparedRawReceiveHashes?: (properties: {
1488
+ heads: RawEntryWithRefs[];
1489
+ hashes: string[];
1490
+ }) =>
1491
+ | RawReceiveHashSelection
1492
+ | undefined
1493
+ | Promise<RawReceiveHashSelection | undefined>;
1494
+ },
1495
+ ): Promise<ExchangeHeadsMessage<any> | undefined> => {
1496
+ const hashes = new Array<string>(message.heads.length);
1497
+ let rawBytes = 0;
1498
+ for (let i = 0; i < message.heads.length; i++) {
1499
+ const head = message.heads[i]!;
1500
+ hashes[i] = head.hash;
1501
+ rawBytes += getRawExchangeHeadByteLength(head);
1502
+ }
1503
+ // Built lazily: the fused (stash-backed) prepare paths keep the entry
1504
+ // block bytes in wasm memory and never need a JS blocks array.
1505
+ let blocksValue: Uint8Array[] | undefined;
1506
+ const blocks = () =>
1507
+ (blocksValue ??= message.heads.map((head) => head.bytes));
1508
+ const nativePrepareStartedAt = syncProfileStart(profile);
1509
+ let preparedFacts: PreparedRawEntryV0Facts[] | undefined;
1510
+ let preparedColumns: PreparedRawEntryV0FactsColumns | undefined;
1511
+ let nativePrepareSource: "backbone-columns" | "backbone" | "log" | undefined;
1512
+ let hashesVerifiedByNative = false;
1513
+ const requestedVerifyDuringPrepare =
1514
+ options?.verifyNativeBackboneSignaturesDuringPrepare === true;
1515
+ const canDeferPreparedSelectionVerification =
1516
+ requestedVerifyDuringPrepare &&
1517
+ options?.deferNativeBackboneSignatureVerificationUntilSelection === true &&
1518
+ !!options?.nativeBackbone?.verifyPreparedRawReceiveEntries &&
1519
+ (!!options?.tryPreparedRawReceiveFastDrop ||
1520
+ !!options?.selectPreparedRawReceiveHashes);
1521
+ const verifySignaturesInPrepare =
1522
+ requestedVerifyDuringPrepare && !canDeferPreparedSelectionVerification;
1523
+ if (options?.nativeBackbone) {
1524
+ const profileNativeBackbone =
1525
+ !!profile &&
1526
+ !!options.nativeBackbone.setAppendProfileEnabled &&
1527
+ !!options.nativeBackbone.resetAppendProfile &&
1528
+ !!options.nativeBackbone.appendProfile;
1529
+ if (profileNativeBackbone) {
1530
+ options.nativeBackbone.resetAppendProfile?.();
1531
+ options.nativeBackbone.setAppendProfileEnabled?.(true);
1532
+ }
1533
+ try {
1534
+ const preparedColumnsAndSelection =
1535
+ await options.prepareNativeBackboneExpectedColumnsAndSelection?.({
1536
+ blocks,
1537
+ hashes,
1538
+ verifySignatures: verifySignaturesInPrepare,
1539
+ });
1540
+ preparedColumns = preparedColumnsAndSelection?.columns;
1541
+ preparedColumns ??= options.prepareNativeBackboneExpectedColumns?.({
1542
+ hashes,
1543
+ verifySignatures: verifySignaturesInPrepare,
1544
+ });
1545
+ preparedColumns ??=
1546
+ options.nativeBackbone.prepareRawReceiveExpectedColumnsBatch?.(
1547
+ blocks(),
1548
+ hashes,
1549
+ { verifySignatures: verifySignaturesInPrepare },
1550
+ );
1551
+ hashesVerifiedByNative = !!preparedColumns;
1552
+ preparedColumns ??=
1553
+ options.nativeBackbone.prepareRawReceiveColumnsBatch?.(
1554
+ blocks(),
1555
+ hashes,
1556
+ {
1557
+ verifySignatures: verifySignaturesInPrepare,
1558
+ },
1559
+ );
1560
+ if (preparedColumns) {
1561
+ nativePrepareSource = "backbone-columns";
1562
+ } else {
1563
+ preparedFacts = options.nativeBackbone.prepareRawReceiveBatch(blocks());
1564
+ nativePrepareSource = "backbone";
1565
+ }
1566
+ } catch {
1567
+ preparedColumns = undefined;
1568
+ preparedFacts = undefined;
1569
+ } finally {
1570
+ if (profileNativeBackbone) {
1571
+ options.nativeBackbone.setAppendProfileEnabled?.(false);
1572
+ emitNativeBackboneRawPrepareProfile(
1573
+ profile,
1574
+ options.nativeBackbone.appendProfile?.(),
1575
+ message.heads.length,
1576
+ rawBytes,
1577
+ );
1578
+ }
1579
+ }
1580
+ }
1581
+ if (!preparedColumns && !preparedFacts) {
1582
+ preparedFacts = await prepareRawEntryV0Batch(blocks())
1583
+ .then((facts) => {
1584
+ nativePrepareSource = "log";
1585
+ return facts;
1586
+ })
1587
+ .catch(() => undefined);
1588
+ }
1589
+ if (preparedColumns || preparedFacts) {
1590
+ const clearPreparedRawHashes = (hashesToClear: Iterable<string>) => {
1591
+ if (nativePrepareSource === "backbone") {
1592
+ options?.nativeBackbone?.clearPreparedRawReceiveEntries?.(
1593
+ hashesToClear,
1594
+ );
1595
+ return;
1596
+ }
1597
+ if (nativePrepareSource === "backbone-columns") {
1598
+ options?.nativeBackbone?.clearPreparedRawReceiveEntries?.(
1599
+ hashesToClear,
1600
+ );
1601
+ }
1602
+ };
1603
+ const clearPreparedRaw = () => {
1604
+ clearPreparedRawHashes(
1605
+ nativePrepareSource === "backbone"
1606
+ ? preparedRawFactsHashes(preparedFacts!)
1607
+ : hashes,
1608
+ );
1609
+ };
1610
+ emitSyncProfileDuration(profile, nativePrepareStartedAt, {
1611
+ name: "sharedLog.rawReceive.prepareFacts",
1612
+ component: "shared-log",
1613
+ entries: message.heads.length,
1614
+ bytes: rawBytes,
1615
+ messages: 1,
1616
+ details: {
1617
+ native: true,
1618
+ source: nativePrepareSource,
1619
+ verifySignatures: verifySignaturesInPrepare,
1620
+ deferredVerifySignatures: canDeferPreparedSelectionVerification,
1621
+ deferredVerifySignaturesUntilCommit:
1622
+ canDeferPreparedSelectionVerification &&
1623
+ options?.deferNativeBackboneSignatureVerificationUntilCommit ===
1624
+ true,
1625
+ },
1626
+ });
1627
+ try {
1628
+ if (
1629
+ preparedColumns &&
1630
+ preparedRawFactsCount(preparedColumns) !== message.heads.length
1631
+ ) {
1632
+ throw new Error("Raw exchange head prepared column count mismatch");
1633
+ }
1634
+ if (preparedFacts && preparedFacts.length !== message.heads.length) {
1635
+ throw new Error("Raw exchange head prepared fact count mismatch");
1636
+ }
1637
+ if (!hashesVerifiedByNative) {
1638
+ const rowFacts = preparedFacts!;
1639
+ for (let i = 0; i < message.heads.length; i++) {
1640
+ const head = message.heads[i]!;
1641
+ const facts = preparedColumns ?? rowFacts[i]!;
1642
+ if (preparedRawCid(facts, i) !== head.hash) {
1643
+ throw new Error("Raw exchange head hash did not match bytes");
1644
+ }
1645
+ }
1646
+ }
1647
+ if (
1648
+ options?.tryPreparedRawReceiveFastDrop &&
1649
+ (await options.tryPreparedRawReceiveFastDrop({
1650
+ heads: message.heads,
1651
+ hashes,
1652
+ }))
1653
+ ) {
1654
+ return undefined;
1655
+ }
1656
+ let selectedHeads = message.heads;
1657
+ let selectedHashes = hashes;
1658
+ let selectedIndexes: number[] | undefined;
1659
+ const selectedHashSelection =
1660
+ await options?.selectPreparedRawReceiveHashes?.({
1661
+ heads: message.heads,
1662
+ hashes,
1663
+ });
1664
+ if (selectedHashSelection) {
1665
+ const selectedHashSelectionObject =
1666
+ !isIterableRawReceiveHashSelection(selectedHashSelection)
1667
+ ? selectedHashSelection
1668
+ : undefined;
1669
+ const selectedIndexIterable = selectedHashSelectionObject?.indexes;
1670
+ if (selectedIndexIterable) {
1671
+ const selectedFlags = new Uint8Array(hashes.length);
1672
+ selectedHeads = [];
1673
+ selectedHashes = [];
1674
+ selectedIndexes = [];
1675
+ for (const rawIndex of selectedIndexIterable) {
1676
+ if (
1677
+ !Number.isInteger(rawIndex) ||
1678
+ rawIndex < 0 ||
1679
+ rawIndex >= hashes.length
1680
+ ) {
1681
+ throw new Error("Selected unknown raw receive index");
1682
+ }
1683
+ const index = rawIndex;
1684
+ if (selectedFlags[index]) {
1685
+ throw new Error("Selected duplicate raw receive index");
1686
+ }
1687
+ selectedFlags[index] = 1;
1688
+ selectedHeads.push(message.heads[index]!);
1689
+ selectedHashes.push(hashes[index]!);
1690
+ selectedIndexes.push(index);
1691
+ }
1692
+ let droppedHashes: string[] | undefined;
1693
+ if (selectedHashSelectionObject.droppedIndexes) {
1694
+ const droppedFlags = new Uint8Array(hashes.length);
1695
+ droppedHashes = [];
1696
+ for (const rawIndex of selectedHashSelectionObject.droppedIndexes) {
1697
+ if (
1698
+ !Number.isInteger(rawIndex) ||
1699
+ rawIndex < 0 ||
1700
+ rawIndex >= hashes.length
1701
+ ) {
1702
+ throw new Error("Dropped unknown raw receive index");
1703
+ }
1704
+ const index = rawIndex;
1705
+ if (selectedFlags[index]) {
1706
+ throw new Error("Raw receive index selected and dropped");
1707
+ }
1708
+ if (droppedFlags[index]) {
1709
+ throw new Error("Dropped duplicate raw receive index");
1710
+ }
1711
+ droppedFlags[index] = 1;
1712
+ droppedHashes.push(hashes[index]!);
1713
+ }
1714
+ if (selectedHashes.length + droppedHashes.length !== hashes.length) {
1715
+ throw new Error("Raw receive selection did not cover every index");
1716
+ }
1717
+ }
1718
+ const expectedHashes = selectedHashSelectionObject.hashes
1719
+ ? Array.from(selectedHashSelectionObject.hashes)
1720
+ : undefined;
1721
+ if (
1722
+ expectedHashes &&
1723
+ (expectedHashes.length !== selectedHashes.length ||
1724
+ expectedHashes.some(
1725
+ (hash, index) => hash !== selectedHashes[index],
1726
+ ))
1727
+ ) {
1728
+ throw new Error("Selected raw receive hashes did not match indexes");
1729
+ }
1730
+ if (!droppedHashes) {
1731
+ droppedHashes = [];
1732
+ for (let i = 0; i < hashes.length; i++) {
1733
+ if (!selectedFlags[i]) {
1734
+ droppedHashes.push(hashes[i]!);
1735
+ }
1736
+ }
1737
+ }
1738
+ if (droppedHashes.length > 0) {
1739
+ clearPreparedRawHashes(droppedHashes);
1740
+ }
1741
+ if (selectedHashes.length === 0) {
1742
+ return undefined;
1743
+ }
1744
+ } else {
1745
+ const selectedHashIterable =
1746
+ isIterableRawReceiveHashSelection(selectedHashSelection)
1747
+ ? selectedHashSelection
1748
+ : selectedHashSelectionObject?.hashes;
1749
+ if (!selectedHashIterable) {
1750
+ throw new Error("Missing selected raw receive hashes");
1751
+ }
1752
+ const selectedHashSet = new Set(selectedHashIterable);
1753
+ const knownHashes = new Set(hashes);
1754
+ for (const hash of selectedHashSet) {
1755
+ if (!knownHashes.has(hash)) {
1756
+ throw new Error("Selected unknown raw receive hash");
1757
+ }
1758
+ }
1759
+ selectedHeads = [];
1760
+ selectedHashes = [];
1761
+ selectedIndexes = [];
1762
+ const droppedHashes: string[] = [];
1763
+ for (let i = 0; i < hashes.length; i++) {
1764
+ const hash = hashes[i]!;
1765
+ if (selectedHashSet.has(hash)) {
1766
+ selectedHeads.push(message.heads[i]!);
1767
+ selectedHashes.push(hash);
1768
+ selectedIndexes.push(i);
1769
+ } else {
1770
+ droppedHashes.push(hash);
1771
+ }
1772
+ }
1773
+ if (droppedHashes.length > 0) {
1774
+ clearPreparedRawHashes(droppedHashes);
1775
+ }
1776
+ if (selectedHashes.length === 0) {
1777
+ return undefined;
1778
+ }
1779
+ }
1780
+ }
1781
+ if (canDeferPreparedSelectionVerification) {
1782
+ if (
1783
+ options?.deferNativeBackboneSignatureVerificationUntilCommit === true
1784
+ ) {
1785
+ emitSyncProfileDuration(profile, syncProfileStart(profile), {
1786
+ name: "sharedLog.rawReceive.deferVerifySelected",
1787
+ component: "shared-log",
1788
+ entries: selectedHashes.length,
1789
+ count: message.heads.length - selectedHashes.length,
1790
+ messages: 1,
1791
+ });
1792
+ } else {
1793
+ const verifyStartedAt = syncProfileStart(profile);
1794
+ const verified =
1795
+ options?.nativeBackbone?.verifyPreparedRawReceiveEntries?.(
1796
+ selectedHashes,
1797
+ );
1798
+ if (
1799
+ !verified ||
1800
+ verified.length !== selectedHashes.length ||
1801
+ verified.some((ok) => !ok)
1802
+ ) {
1803
+ throw new Error("Raw exchange head signature verification failed");
1804
+ }
1805
+ if (preparedColumns) {
1806
+ for (
1807
+ let selectedIndex = 0;
1808
+ selectedIndex < selectedHashes.length;
1809
+ selectedIndex++
1810
+ ) {
1811
+ preparedColumns[12][
1812
+ selectedIndexes?.[selectedIndex] ?? selectedIndex
1813
+ ] = 1;
1814
+ }
1815
+ } else if (preparedFacts) {
1816
+ for (
1817
+ let selectedIndex = 0;
1818
+ selectedIndex < selectedHashes.length;
1819
+ selectedIndex++
1820
+ ) {
1821
+ preparedFacts[
1822
+ selectedIndexes?.[selectedIndex] ?? selectedIndex
1823
+ ]!.signatureVerified = true;
1824
+ }
1825
+ }
1826
+ emitSyncProfileDuration(profile, verifyStartedAt, {
1827
+ name: "sharedLog.rawReceive.verifySelected",
1828
+ component: "shared-log",
1829
+ entries: selectedHashes.length,
1830
+ count: message.heads.length - selectedHashes.length,
1831
+ messages: 1,
1832
+ });
1833
+ }
1834
+ }
1835
+ const headsToWrap = selectedHeads;
1836
+ const hashesToWrap = selectedHashes;
1837
+ const indexesToWrap = selectedIndexes;
1838
+ const wrapStartedAt = syncProfileStart(profile);
1839
+ // Lazy per-entry JS materialization (a change consumer reading
1840
+ // payload/signatures) is counted on the existing counter so the
1841
+ // fused no-consumer path stays assertable at zero.
1842
+ const onJsEntryDecode = profile
1843
+ ? () =>
1844
+ emitSyncProfileEvent(profile, {
1845
+ name: "sharedLog.rawReceive.jsEntryDecode",
1846
+ component: "shared-log",
1847
+ entries: 1,
1848
+ messages: 0,
1849
+ details: { lazy: true },
1850
+ })
1851
+ : undefined;
1852
+ let materializedHeads: EntryWithRefs<any>[];
1853
+ try {
1854
+ const rowFacts = preparedFacts!;
1855
+ materializedHeads = headsToWrap.map((head, selectedIndex) => {
1856
+ const factsIndex = indexesToWrap?.[selectedIndex] ?? selectedIndex;
1857
+ const facts = preparedColumns ?? rowFacts[factsIndex]!;
1858
+ const preparedHead = new PreparedRawEntryWithRefs(
1859
+ head,
1860
+ facts,
1861
+ factsIndex,
1862
+ onJsEntryDecode,
1863
+ );
1864
+ preparedHead.initEntry({
1865
+ keychain: log.keychain,
1866
+ encoding: log.encoding,
1867
+ });
1868
+ return preparedHead as EntryWithRefs<any>;
1869
+ });
1870
+ } catch (error) {
1871
+ clearPreparedRaw();
1872
+ throw error;
1873
+ }
1874
+ const materialized = new ExchangeHeadsMessage({
1875
+ heads: materializedHeads,
1876
+ preparedHashes: hashesToWrap,
1877
+ });
1878
+ materialized.reserved = message.reserved;
1879
+ emitSyncProfileDuration(profile, wrapStartedAt, {
1880
+ name: "sharedLog.rawReceive.wrapPrepared",
1881
+ component: "shared-log",
1882
+ entries: headsToWrap.length,
1883
+ count: message.heads.length - headsToWrap.length,
1884
+ messages: 1,
1885
+ });
1886
+ return materialized;
1887
+ } catch (error) {
1888
+ clearPreparedRaw();
1889
+ throw error;
1890
+ }
1891
+ }
1892
+ emitSyncProfileDuration(profile, nativePrepareStartedAt, {
1893
+ name: "sharedLog.rawReceive.prepareFacts",
1894
+ component: "shared-log",
1895
+ entries: message.heads.length,
1896
+ bytes: rawBytes,
1897
+ messages: 1,
1898
+ details: { native: false },
1899
+ });
1900
+ const hashStartedAt = syncProfileStart(profile);
1901
+ const calculatedHashes = await calculateRawCidV1Batch(blocks());
1902
+ emitSyncProfileDuration(profile, hashStartedAt, {
1903
+ name: "sharedLog.rawReceive.calculateHashes",
1904
+ component: "shared-log",
1905
+ entries: message.heads.length,
1906
+ messages: 1,
1907
+ });
1908
+ const deserializeStartedAt = syncProfileStart(profile);
1909
+ const materialized = new ExchangeHeadsMessage({
1910
+ heads: message.heads.map((head, index) => {
1911
+ if (calculatedHashes[index] !== head.hash) {
1912
+ throw new Error("Raw exchange head hash did not match bytes");
1913
+ }
1914
+ const entry = materializeRawExchangeEntry({
1915
+ hash: head.hash,
1916
+ bytes: head.bytes,
1917
+ size: head.bytes.byteLength,
1918
+ keychain: log.keychain,
1919
+ encoding: log.encoding,
1920
+ gidRefrences: head.gidRefrences,
1921
+ });
1922
+ return new EntryWithRefs({
1923
+ entry,
1924
+ gidRefrences: head.gidRefrences,
1925
+ });
1926
+ }),
1927
+ });
1928
+ materialized.reserved = message.reserved;
1929
+ emitSyncProfileDuration(profile, deserializeStartedAt, {
1930
+ name: "sharedLog.rawReceive.deserializeFallback",
1931
+ component: "shared-log",
1932
+ entries: message.heads.length,
1933
+ messages: 1,
1934
+ });
1935
+ return materialized;
1936
+ };
1937
+
1938
+ const getNativeReferenceRowsByHeadInput = (
1939
+ log: Log<any>,
1940
+ heads: Array<Entry<any> | string>,
1941
+ visitedHeads: Set<string>,
1942
+ ) => {
1943
+ const positions: number[] = [];
1944
+ const hashes: string[] = [];
1945
+ for (let i = 0; i < heads.length; i++) {
1946
+ const head = heads[i]!;
1947
+ const hash = head instanceof Entry ? head.hash : head;
1948
+ if (visitedHeads.has(hash)) {
1949
+ continue;
1950
+ }
1951
+ positions.push(i);
1952
+ hashes.push(hash);
1953
+ }
1954
+ if (hashes.length === 0) {
1955
+ return [];
1956
+ }
1957
+ const flatRows = log.entryIndex.getUniqueReferenceGidRowsFlatBatch(hashes);
1958
+ if (flatRows) {
1959
+ const byPosition: Array<Array<[string, string]> | undefined> = new Array(
1960
+ heads.length,
1961
+ );
1962
+ for (const position of positions) {
1963
+ byPosition[position] = [];
1964
+ }
1965
+ for (const [hashPosition, hash, gid] of flatRows) {
1966
+ const position = positions[hashPosition];
1967
+ if (position === undefined) {
1968
+ continue;
1969
+ }
1970
+ byPosition[position]!.push([hash, gid]);
1971
+ }
1972
+ return byPosition;
1973
+ }
1974
+ const rows = log.entryIndex.getUniqueReferenceGidRowsBatch(hashes);
1975
+ if (!rows) {
1976
+ return undefined;
1977
+ }
1978
+ const byPosition: Array<Array<[string, string]> | undefined> = new Array(
1979
+ heads.length,
1980
+ );
1981
+ for (let i = 0; i < rows.length; i++) {
1982
+ byPosition[positions[i]!] = rows[i];
1983
+ }
1984
+ return byPosition;
1985
+ };
1986
+
1987
+ type BlocksWithGetMany = {
1988
+ getMany?: (
1989
+ cids: string[],
1990
+ ) =>
1991
+ | Promise<Array<Uint8Array | undefined>>
1992
+ | Array<Uint8Array | undefined>;
1993
+ get: (cid: string) => Promise<Uint8Array | undefined> | Uint8Array | undefined;
1994
+ };
1995
+
1996
+ const resolveExchangeHeadBlocks = async (
1997
+ log: Log<any>,
1998
+ headArray: string[],
1999
+ visitedHeads?: Set<string>,
2000
+ ): Promise<Array<{ hash: string; bytes: Uint8Array } | undefined> | undefined> => {
2001
+ const resolved: Array<{ hash: string; bytes: Uint8Array } | undefined> =
2002
+ new Array(headArray.length);
2003
+ const hashes: string[] = [];
2004
+ const positionsByHash = new Map<string, number[]>();
2005
+ for (let i = 0; i < headArray.length; i++) {
2006
+ const hash = headArray[i]!;
2007
+ if (visitedHeads?.has(hash)) {
2008
+ continue;
2009
+ }
2010
+ const positions = positionsByHash.get(hash);
2011
+ if (positions) {
2012
+ positions.push(i);
2013
+ continue;
2014
+ }
2015
+ hashes.push(hash);
2016
+ positionsByHash.set(hash, [i]);
2017
+ }
2018
+ if (hashes.length === 0) {
2019
+ return resolved;
2020
+ }
2021
+
2022
+ const blocks = log.blocks as BlocksWithGetMany;
2023
+ const values =
2024
+ typeof blocks.getMany === "function"
2025
+ ? await blocks.getMany(hashes)
2026
+ : await Promise.all(hashes.map((hash) => blocks.get(hash)));
2027
+ for (let i = 0; i < values.length; i++) {
2028
+ const hash = hashes[i]!;
2029
+ const bytes = values[i];
2030
+ if (!bytes) {
2031
+ return undefined;
2032
+ }
2033
+ for (const position of positionsByHash.get(hash)!) {
2034
+ resolved[position] = { hash, bytes };
2035
+ }
2036
+ }
2037
+ return resolved;
2038
+ };
2039
+
2040
+ const resolveExchangeHeadEntries = async (
2041
+ log: Log<any>,
2042
+ headArray: Array<Entry<any> | string>,
2043
+ visitedHeads?: Set<string>,
2044
+ ): Promise<Array<Entry<any> | undefined>> => {
2045
+ const resolved: Array<Entry<any> | undefined> = new Array(headArray.length);
2046
+ const hashes: string[] = [];
2047
+ const positionsByHash = new Map<string, number[]>();
2048
+ for (let i = 0; i < headArray.length; i++) {
2049
+ const head = headArray[i]!;
2050
+ if (head instanceof Entry) {
2051
+ if (visitedHeads?.has(head.hash)) {
2052
+ continue;
2053
+ }
2054
+ resolved[i] = head;
2055
+ continue;
2056
+ }
2057
+ if (visitedHeads?.has(head)) {
2058
+ continue;
2059
+ }
2060
+ const positions = positionsByHash.get(head);
2061
+ if (positions) {
2062
+ positions.push(i);
2063
+ continue;
2064
+ }
2065
+ hashes.push(head);
2066
+ positionsByHash.set(head, [i]);
2067
+ }
2068
+ if (hashes.length === 0) {
2069
+ return resolved;
2070
+ }
2071
+ const entries =
2072
+ hashes.length === 1
2073
+ ? [await log.get(hashes[0]!)]
2074
+ : await log.entryIndex.getMany(hashes, {
2075
+ type: "full",
2076
+ ignoreMissing: true,
2077
+ });
2078
+ for (let i = 0; i < entries.length; i++) {
2079
+ for (const position of positionsByHash.get(hashes[i]!)!) {
2080
+ resolved[position] = entries[i];
2081
+ }
2082
+ }
2083
+ return resolved;
2084
+ };
2085
+
124
2086
  export const allEntriesWithUniqueGids = async (
125
2087
  log: Log<any>,
126
2088
  entry: Entry<any>,
@@ -150,12 +2112,27 @@ export const allEntriesWithUniqueGids = async (
150
2112
  curr = nexts;
151
2113
  }
152
2114
  }
153
- const value = [
154
- ...(await Promise.all(
155
- [...map.values()].map((x) =>
156
- x instanceof Entry ? x : log.entryIndex.get(x.hash),
157
- ),
158
- )),
159
- ].filter((x) => !!x) as Entry<any>[];
160
- return value;
2115
+ const values = [...map.values()];
2116
+ const resolved: Array<Entry<any> | undefined> = new Array(values.length);
2117
+ const unresolvedHashes: string[] = [];
2118
+ const unresolvedPositions: number[] = [];
2119
+ for (let i = 0; i < values.length; i++) {
2120
+ const value = values[i]!;
2121
+ if (value instanceof Entry) {
2122
+ resolved[i] = value;
2123
+ continue;
2124
+ }
2125
+ unresolvedHashes.push(value.hash);
2126
+ unresolvedPositions.push(i);
2127
+ }
2128
+ if (unresolvedHashes.length > 0) {
2129
+ const entries = await log.entryIndex.getMany(unresolvedHashes, {
2130
+ type: "full",
2131
+ ignoreMissing: true,
2132
+ });
2133
+ for (let i = 0; i < entries.length; i++) {
2134
+ resolved[unresolvedPositions[i]!] = entries[i];
2135
+ }
2136
+ }
2137
+ return resolved.filter((x) => !!x) as Entry<any>[];
161
2138
  };