@peerbit/shared-log 13.1.17 → 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.
- package/dist/benchmark/index.js +91 -29
- package/dist/benchmark/index.js.map +1 -1
- package/dist/benchmark/native-graph.js +120 -10
- package/dist/benchmark/native-graph.js.map +1 -1
- package/dist/benchmark/network-preset-e2e.d.ts +2 -0
- package/dist/benchmark/network-preset-e2e.d.ts.map +1 -0
- package/dist/benchmark/network-preset-e2e.js +497 -0
- package/dist/benchmark/network-preset-e2e.js.map +1 -0
- package/dist/benchmark/receive-prune.d.ts +2 -0
- package/dist/benchmark/receive-prune.d.ts.map +1 -0
- package/dist/benchmark/receive-prune.js +816 -0
- package/dist/benchmark/receive-prune.js.map +1 -0
- package/dist/benchmark/sync-catchup.d.ts +1 -2
- package/dist/benchmark/sync-catchup.d.ts.map +1 -1
- package/dist/benchmark/sync-catchup.js +175 -7
- package/dist/benchmark/sync-catchup.js.map +1 -1
- package/dist/src/checked-prune.d.ts +3 -0
- package/dist/src/checked-prune.d.ts.map +1 -1
- package/dist/src/checked-prune.js +24 -0
- package/dist/src/checked-prune.js.map +1 -1
- package/dist/src/exchange-heads.d.ts +238 -1
- package/dist/src/exchange-heads.d.ts.map +1 -1
- package/dist/src/exchange-heads.js +1512 -36
- package/dist/src/exchange-heads.js.map +1 -1
- package/dist/src/index.d.ts +276 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7358 -666
- package/dist/src/index.js.map +1 -1
- package/dist/src/pid.d.ts.map +1 -1
- package/dist/src/pid.js +25 -9
- package/dist/src/pid.js.map +1 -1
- package/dist/src/ranges.d.ts +11 -2
- package/dist/src/ranges.d.ts.map +1 -1
- package/dist/src/ranges.js +74 -30
- package/dist/src/ranges.js.map +1 -1
- package/dist/src/sync/index.d.ts +77 -2
- package/dist/src/sync/index.d.ts.map +1 -1
- package/dist/src/sync/rateless-iblt.d.ts +11 -5
- package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
- package/dist/src/sync/rateless-iblt.js +123 -60
- package/dist/src/sync/rateless-iblt.js.map +1 -1
- package/dist/src/sync/simple.d.ts +56 -4
- package/dist/src/sync/simple.d.ts.map +1 -1
- package/dist/src/sync/simple.js +495 -63
- package/dist/src/sync/simple.js.map +1 -1
- package/dist/src/utils.d.ts +2 -1
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +49 -2
- package/dist/src/utils.js.map +1 -1
- package/package.json +27 -17
- package/src/checked-prune.ts +27 -0
- package/src/exchange-heads.ts +2018 -41
- package/src/index.ts +13114 -3167
- package/src/pid.ts +24 -13
- package/src/ranges.ts +90 -40
- package/src/sync/index.ts +115 -2
- package/src/sync/rateless-iblt.ts +154 -75
- package/src/sync/simple.ts +590 -95
- package/src/utils.ts +73 -3
package/src/exchange-heads.ts
CHANGED
|
@@ -1,15 +1,371 @@
|
|
|
1
|
-
import { field, fixedArray, variant, vec } from "@dao-xyz/borsh";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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(
|
|
94
|
-
return true;
|
|
95
|
-
});
|
|
1137
|
+
visitedHeads.add(entry.hash);
|
|
96
1138
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
};
|