cojson 0.7.0-alpha.29 → 0.7.0-alpha.36
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/.turbo/turbo-build.log +10 -4
- package/CHANGELOG.md +13 -0
- package/dist/coValueCore.js +40 -18
- package/dist/coValueCore.js.map +1 -1
- package/dist/crypto.js +1 -0
- package/dist/crypto.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/localNode.js +3 -3
- package/dist/localNode.js.map +1 -1
- package/dist/storage/FileSystem.js +61 -0
- package/dist/storage/FileSystem.js.map +1 -0
- package/dist/storage/chunksAndKnownStates.js +97 -0
- package/dist/storage/chunksAndKnownStates.js.map +1 -0
- package/dist/storage/index.js +265 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/sync.js +23 -23
- package/dist/sync.js.map +1 -1
- package/package.json +2 -1
- package/src/coValueCore.ts +99 -48
- package/src/crypto.ts +2 -1
- package/src/index.ts +16 -1
- package/src/localNode.ts +2 -2
- package/src/storage/FileSystem.ts +151 -0
- package/src/storage/chunksAndKnownStates.ts +132 -0
- package/src/storage/index.ts +475 -0
- package/src/sync.ts +23 -23
- package/src/tests/coList.test.ts +100 -0
- package/src/tests/coMap.test.ts +167 -0
- package/src/tests/{coValue.test.ts → coStream.test.ts} +1 -231
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReadableStream,
|
|
3
|
+
WritableStream,
|
|
4
|
+
ReadableStreamDefaultReader,
|
|
5
|
+
WritableStreamDefaultWriter,
|
|
6
|
+
} from "isomorphic-streams";
|
|
7
|
+
import { Effect, Either, SynchronizedRef } from "effect";
|
|
8
|
+
import { RawCoID } from "../ids.js";
|
|
9
|
+
import { CoValueHeader, Transaction } from "../coValueCore.js";
|
|
10
|
+
import { Signature } from "../crypto.js";
|
|
11
|
+
import {
|
|
12
|
+
CoValueKnownState,
|
|
13
|
+
NewContentMessage,
|
|
14
|
+
Peer,
|
|
15
|
+
SyncMessage,
|
|
16
|
+
} from "../sync.js";
|
|
17
|
+
import { CoID, RawCoValue } from "../index.js";
|
|
18
|
+
import { connectedPeers } from "../streamUtils.js";
|
|
19
|
+
import {
|
|
20
|
+
chunkToKnownState,
|
|
21
|
+
contentSinceChunk,
|
|
22
|
+
mergeChunks,
|
|
23
|
+
} from "./chunksAndKnownStates.js";
|
|
24
|
+
import {
|
|
25
|
+
BlockFilename,
|
|
26
|
+
FSErr,
|
|
27
|
+
FileSystem,
|
|
28
|
+
WalEntry,
|
|
29
|
+
WalFilename,
|
|
30
|
+
readChunk,
|
|
31
|
+
readHeader,
|
|
32
|
+
textDecoder,
|
|
33
|
+
writeBlock,
|
|
34
|
+
writeToWal,
|
|
35
|
+
} from "./FileSystem.js";
|
|
36
|
+
export { FSErr, BlockFilename, WalFilename } from "./FileSystem.js";
|
|
37
|
+
|
|
38
|
+
export type CoValueChunk = {
|
|
39
|
+
header?: CoValueHeader;
|
|
40
|
+
sessionEntries: {
|
|
41
|
+
[sessionID: string]: {
|
|
42
|
+
after: number;
|
|
43
|
+
lastSignature: Signature;
|
|
44
|
+
transactions: Transaction[];
|
|
45
|
+
}[];
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
|
|
50
|
+
fromLocalNode!: ReadableStreamDefaultReader<SyncMessage>;
|
|
51
|
+
toLocalNode: WritableStreamDefaultWriter<SyncMessage>;
|
|
52
|
+
fs: FS;
|
|
53
|
+
currentWal: SynchronizedRef.SynchronizedRef<WH | undefined>;
|
|
54
|
+
coValues: SynchronizedRef.SynchronizedRef<{
|
|
55
|
+
[id: RawCoID]: CoValueChunk | undefined;
|
|
56
|
+
}>;
|
|
57
|
+
fileCache: string[] | undefined;
|
|
58
|
+
headerCache = new Map<
|
|
59
|
+
BlockFilename,
|
|
60
|
+
{ [id: RawCoID]: { start: number; length: number } }
|
|
61
|
+
>();
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
fs: FS,
|
|
65
|
+
fromLocalNode: ReadableStream<SyncMessage>,
|
|
66
|
+
toLocalNode: WritableStream<SyncMessage>
|
|
67
|
+
) {
|
|
68
|
+
this.fs = fs;
|
|
69
|
+
this.fromLocalNode = fromLocalNode.getReader();
|
|
70
|
+
this.toLocalNode = toLocalNode.getWriter();
|
|
71
|
+
this.coValues = SynchronizedRef.unsafeMake({});
|
|
72
|
+
this.currentWal = SynchronizedRef.unsafeMake<WH | undefined>(undefined);
|
|
73
|
+
|
|
74
|
+
void Effect.runPromise(
|
|
75
|
+
Effect.gen(this, function* () {
|
|
76
|
+
let done = false;
|
|
77
|
+
while (!done) {
|
|
78
|
+
const result = yield* Effect.promise(() =>
|
|
79
|
+
this.fromLocalNode.read()
|
|
80
|
+
);
|
|
81
|
+
done = result.done;
|
|
82
|
+
|
|
83
|
+
if (result.value) {
|
|
84
|
+
if (result.value.action === "done") {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (result.value.action === "content") {
|
|
89
|
+
yield* this.handleNewContent(result.value);
|
|
90
|
+
} else {
|
|
91
|
+
yield* this.sendNewContent(
|
|
92
|
+
result.value.id,
|
|
93
|
+
result.value,
|
|
94
|
+
undefined
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return;
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
setTimeout(() => this.compact(), 20000);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
sendNewContent(
|
|
108
|
+
id: RawCoID,
|
|
109
|
+
known: CoValueKnownState | undefined,
|
|
110
|
+
asDependencyOf: RawCoID | undefined
|
|
111
|
+
): Effect.Effect<void, FSErr> {
|
|
112
|
+
return SynchronizedRef.updateEffect(this.coValues, (coValues) =>
|
|
113
|
+
this.sendNewContentInner(coValues, id, known, asDependencyOf)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private sendNewContentInner(
|
|
118
|
+
coValues: { [id: `co_z${string}`]: CoValueChunk | undefined },
|
|
119
|
+
id: RawCoID,
|
|
120
|
+
known: CoValueKnownState | undefined,
|
|
121
|
+
asDependencyOf: RawCoID | undefined
|
|
122
|
+
): Effect.Effect<
|
|
123
|
+
{ [id: `co_z${string}`]: CoValueChunk | undefined },
|
|
124
|
+
FSErr,
|
|
125
|
+
never
|
|
126
|
+
> {
|
|
127
|
+
return Effect.gen(this, function* () {
|
|
128
|
+
let coValue = coValues[id];
|
|
129
|
+
|
|
130
|
+
if (!coValue) {
|
|
131
|
+
coValue = yield* this.loadCoValue(id, this.fs);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!coValue) {
|
|
135
|
+
yield* Effect.promise(() =>
|
|
136
|
+
this.toLocalNode.write({
|
|
137
|
+
id: id,
|
|
138
|
+
action: "known",
|
|
139
|
+
header: false,
|
|
140
|
+
sessions: {},
|
|
141
|
+
asDependencyOf,
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return coValues;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
!known?.header &&
|
|
150
|
+
coValue.header?.ruleset.type === "ownedByGroup"
|
|
151
|
+
) {
|
|
152
|
+
coValues = yield* this.sendNewContentInner(
|
|
153
|
+
coValues,
|
|
154
|
+
coValue.header.ruleset.group,
|
|
155
|
+
undefined,
|
|
156
|
+
asDependencyOf || id
|
|
157
|
+
);
|
|
158
|
+
} else if (
|
|
159
|
+
!known?.header &&
|
|
160
|
+
coValue.header?.ruleset.type === "group"
|
|
161
|
+
) {
|
|
162
|
+
const dependedOnAccounts = new Set();
|
|
163
|
+
for (const session of Object.values(coValue.sessionEntries)) {
|
|
164
|
+
for (const entry of session) {
|
|
165
|
+
for (const tx of entry.transactions) {
|
|
166
|
+
if (tx.privacy === "trusting") {
|
|
167
|
+
const parsedChanges = JSON.parse(tx.changes);
|
|
168
|
+
for (const change of parsedChanges) {
|
|
169
|
+
if (
|
|
170
|
+
change.op === "set" &&
|
|
171
|
+
change.key.startsWith("co_")
|
|
172
|
+
) {
|
|
173
|
+
dependedOnAccounts.add(change.key);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const account of dependedOnAccounts) {
|
|
181
|
+
coValues = yield* this.sendNewContentInner(
|
|
182
|
+
coValues,
|
|
183
|
+
account as CoID<RawCoValue>,
|
|
184
|
+
undefined,
|
|
185
|
+
asDependencyOf || id
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const newContentMessages = contentSinceChunk(
|
|
191
|
+
id,
|
|
192
|
+
coValue,
|
|
193
|
+
known
|
|
194
|
+
).map((message) => ({ ...message, asDependencyOf }));
|
|
195
|
+
|
|
196
|
+
const ourKnown: CoValueKnownState = chunkToKnownState(id, coValue);
|
|
197
|
+
|
|
198
|
+
yield* Effect.promise(() =>
|
|
199
|
+
this.toLocalNode.write({
|
|
200
|
+
action: "known",
|
|
201
|
+
...ourKnown,
|
|
202
|
+
asDependencyOf,
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
for (const message of newContentMessages) {
|
|
207
|
+
if (Object.keys(message.new).length === 0) continue;
|
|
208
|
+
yield* Effect.promise(() => this.toLocalNode.write(message));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { ...coValues, [id]: coValue };
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
withWAL(
|
|
216
|
+
handler: (wal: WH) => Effect.Effect<void, FSErr>
|
|
217
|
+
): Effect.Effect<void, FSErr> {
|
|
218
|
+
return SynchronizedRef.updateEffect(this.currentWal, (wal) =>
|
|
219
|
+
Effect.gen(this, function* () {
|
|
220
|
+
let newWal = wal;
|
|
221
|
+
if (!newWal) {
|
|
222
|
+
newWal = yield* this.fs.createFile(
|
|
223
|
+
`wal-${new Date().toISOString()}-${Math.random()
|
|
224
|
+
.toString(36)
|
|
225
|
+
.slice(2)}.jsonl`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
yield* handler(newWal);
|
|
229
|
+
return newWal;
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
handleNewContent(
|
|
235
|
+
newContent: NewContentMessage
|
|
236
|
+
): Effect.Effect<void, FSErr> {
|
|
237
|
+
return SynchronizedRef.updateEffect(this.coValues, (coValues) =>
|
|
238
|
+
Effect.gen(this, function* () {
|
|
239
|
+
const coValue = coValues[newContent.id];
|
|
240
|
+
|
|
241
|
+
const newContentAsChunk: CoValueChunk = {
|
|
242
|
+
header: newContent.header,
|
|
243
|
+
sessionEntries: Object.fromEntries(
|
|
244
|
+
Object.entries(newContent.new).map(
|
|
245
|
+
([sessionID, newInSession]) => [
|
|
246
|
+
sessionID,
|
|
247
|
+
[
|
|
248
|
+
{
|
|
249
|
+
after: newInSession.after,
|
|
250
|
+
lastSignature:
|
|
251
|
+
newInSession.lastSignature,
|
|
252
|
+
transactions:
|
|
253
|
+
newInSession.newTransactions,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
]
|
|
257
|
+
)
|
|
258
|
+
),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
if (!coValue) {
|
|
262
|
+
if (newContent.header) {
|
|
263
|
+
console.log("Creating in WAL", newContent.id);
|
|
264
|
+
yield* this.withWAL((wal) =>
|
|
265
|
+
writeToWal(
|
|
266
|
+
wal,
|
|
267
|
+
this.fs,
|
|
268
|
+
newContent.id,
|
|
269
|
+
newContentAsChunk
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
...coValues,
|
|
275
|
+
[newContent.id]: newContentAsChunk,
|
|
276
|
+
};
|
|
277
|
+
} else {
|
|
278
|
+
// yield*
|
|
279
|
+
// Effect.promise(() =>
|
|
280
|
+
// this.toLocalNode.write({
|
|
281
|
+
// action: "known",
|
|
282
|
+
// id: newContent.id,
|
|
283
|
+
// header: false,
|
|
284
|
+
// sessions: {},
|
|
285
|
+
// isCorrection: true,
|
|
286
|
+
// })
|
|
287
|
+
// )
|
|
288
|
+
// );
|
|
289
|
+
console.warn(
|
|
290
|
+
"Incontiguous incoming update for " + newContent.id
|
|
291
|
+
);
|
|
292
|
+
return coValues;
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
const merged = mergeChunks(coValue, newContentAsChunk);
|
|
296
|
+
if (Either.isRight(merged)) {
|
|
297
|
+
yield* Effect.logWarning(
|
|
298
|
+
"Non-contigous new content for " + newContent.id
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// yield* Effect.promise(() =>
|
|
302
|
+
// this.toLocalNode.write({
|
|
303
|
+
// action: "known",
|
|
304
|
+
// ...chunkToKnownState(newContent.id, coValue),
|
|
305
|
+
// isCorrection: true,
|
|
306
|
+
// })
|
|
307
|
+
// );
|
|
308
|
+
|
|
309
|
+
return coValues;
|
|
310
|
+
} else {
|
|
311
|
+
console.log("Appending to WAL", newContent.id);
|
|
312
|
+
yield* this.withWAL((wal) =>
|
|
313
|
+
writeToWal(
|
|
314
|
+
wal,
|
|
315
|
+
this.fs,
|
|
316
|
+
newContent.id,
|
|
317
|
+
newContentAsChunk
|
|
318
|
+
)
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
return { ...coValues, [newContent.id]: merged.left };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
loadCoValue<WH, RH, FS extends FileSystem<WH, RH>>(
|
|
329
|
+
id: RawCoID,
|
|
330
|
+
fs: FS
|
|
331
|
+
): Effect.Effect<CoValueChunk | undefined, FSErr> {
|
|
332
|
+
// return _loadChunkFromWal(id, fs);
|
|
333
|
+
return Effect.gen(this, function* () {
|
|
334
|
+
const files = this.fileCache || (yield* fs.listFiles());
|
|
335
|
+
this.fileCache = files;
|
|
336
|
+
const blockFiles = files.filter((name) =>
|
|
337
|
+
name.startsWith("hash_")
|
|
338
|
+
) as BlockFilename[];
|
|
339
|
+
|
|
340
|
+
for (const blockFile of blockFiles) {
|
|
341
|
+
let cachedHeader:
|
|
342
|
+
| { [id: RawCoID]: { start: number; length: number } }
|
|
343
|
+
| undefined = this.headerCache.get(blockFile);
|
|
344
|
+
|
|
345
|
+
const { handle, size } = yield* fs.openToRead(blockFile);
|
|
346
|
+
|
|
347
|
+
if (!cachedHeader) {
|
|
348
|
+
cachedHeader = {};
|
|
349
|
+
const header = yield* readHeader(blockFile, handle, size, fs);
|
|
350
|
+
for (const entry of header) {
|
|
351
|
+
cachedHeader[entry.id] = {
|
|
352
|
+
start: entry.start,
|
|
353
|
+
length: entry.length,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.headerCache.set(blockFile, cachedHeader);
|
|
358
|
+
}
|
|
359
|
+
const headerEntry = cachedHeader[id];
|
|
360
|
+
|
|
361
|
+
let result;
|
|
362
|
+
if (headerEntry) {
|
|
363
|
+
result = yield* readChunk(handle, headerEntry, fs);
|
|
364
|
+
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
yield* fs.close(handle);
|
|
368
|
+
|
|
369
|
+
return result
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return undefined;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async compact() {
|
|
377
|
+
await Effect.runPromise(
|
|
378
|
+
Effect.gen(this, function* () {
|
|
379
|
+
const fileNames = yield* this.fs.listFiles();
|
|
380
|
+
|
|
381
|
+
const walFiles = fileNames.filter((name) =>
|
|
382
|
+
name.startsWith("wal-")
|
|
383
|
+
) as WalFilename[];
|
|
384
|
+
walFiles.sort();
|
|
385
|
+
|
|
386
|
+
const coValues = new Map<RawCoID, CoValueChunk>();
|
|
387
|
+
|
|
388
|
+
console.log("Compacting WAL files", walFiles);
|
|
389
|
+
if (walFiles.length === 0) return;
|
|
390
|
+
|
|
391
|
+
yield* SynchronizedRef.updateEffect(this.currentWal, (wal) =>
|
|
392
|
+
Effect.gen(this, function* () {
|
|
393
|
+
if (wal) {
|
|
394
|
+
yield* this.fs.close(wal);
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
for (const fileName of walFiles) {
|
|
401
|
+
const { handle, size } =
|
|
402
|
+
yield* this.fs.openToRead(fileName);
|
|
403
|
+
if (size === 0) {
|
|
404
|
+
yield* this.fs.close(handle);
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
const bytes = yield* this.fs.read(handle, 0, size);
|
|
408
|
+
|
|
409
|
+
const decoded = textDecoder.decode(bytes);
|
|
410
|
+
const lines = decoded.split("\n");
|
|
411
|
+
|
|
412
|
+
for (const line of lines) {
|
|
413
|
+
if (line.length === 0) continue;
|
|
414
|
+
const chunk = JSON.parse(line) as WalEntry;
|
|
415
|
+
|
|
416
|
+
const existingChunk = coValues.get(chunk.id);
|
|
417
|
+
|
|
418
|
+
if (existingChunk) {
|
|
419
|
+
const merged = mergeChunks(existingChunk, chunk);
|
|
420
|
+
if (Either.isRight(merged)) {
|
|
421
|
+
console.warn(
|
|
422
|
+
"Non-contigous chunks in " +
|
|
423
|
+
chunk.id +
|
|
424
|
+
", " +
|
|
425
|
+
fileName,
|
|
426
|
+
existingChunk,
|
|
427
|
+
chunk
|
|
428
|
+
);
|
|
429
|
+
} else {
|
|
430
|
+
coValues.set(chunk.id, merged.left);
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
coValues.set(chunk.id, chunk);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
yield* this.fs.close(handle);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
yield* writeBlock(coValues, 0, this.fs);
|
|
441
|
+
for (const walFile of walFiles) {
|
|
442
|
+
yield* this.fs.removeFile(walFile);
|
|
443
|
+
}
|
|
444
|
+
this.fileCache = undefined;
|
|
445
|
+
})
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
setTimeout(() => this.compact(), 5000);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
static asPeer<WH, RH, FS extends FileSystem<WH, RH>>({
|
|
452
|
+
fs,
|
|
453
|
+
trace,
|
|
454
|
+
localNodeName = "local",
|
|
455
|
+
}: {
|
|
456
|
+
fs: FS;
|
|
457
|
+
trace?: boolean;
|
|
458
|
+
localNodeName?: string;
|
|
459
|
+
}): Peer {
|
|
460
|
+
const [localNodeAsPeer, storageAsPeer] = connectedPeers(
|
|
461
|
+
localNodeName,
|
|
462
|
+
"storage",
|
|
463
|
+
{
|
|
464
|
+
peer1role: "client",
|
|
465
|
+
peer2role: "server",
|
|
466
|
+
trace,
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
new LSMStorage(fs, localNodeAsPeer.incoming, localNodeAsPeer.outgoing);
|
|
471
|
+
|
|
472
|
+
// return { ...storageAsPeer, priority: 200 };
|
|
473
|
+
return storageAsPeer;
|
|
474
|
+
}
|
|
475
|
+
}
|
package/src/sync.ts
CHANGED
|
@@ -151,25 +151,25 @@ export class SyncManager {
|
|
|
151
151
|
throw new Error("Expected firstPeerState to be waiting " + id);
|
|
152
152
|
}
|
|
153
153
|
await new Promise<void>((resolve) => {
|
|
154
|
-
const timeout = setTimeout(() => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}, 1000);
|
|
154
|
+
// const timeout = setTimeout(() => {
|
|
155
|
+
// if (this.local.coValues[id]?.state === "loading") {
|
|
156
|
+
// console.warn(
|
|
157
|
+
// "Timeout waiting for peer to load",
|
|
158
|
+
// id,
|
|
159
|
+
// "from",
|
|
160
|
+
// peer.id,
|
|
161
|
+
// "and it hasn't loaded from other peers yet"
|
|
162
|
+
// );
|
|
163
|
+
// }
|
|
164
|
+
// resolve();
|
|
165
|
+
// }, 1000);
|
|
166
166
|
firstStateEntry.done
|
|
167
167
|
.then(() => {
|
|
168
|
-
clearTimeout(timeout);
|
|
168
|
+
// clearTimeout(timeout);
|
|
169
169
|
resolve();
|
|
170
170
|
})
|
|
171
171
|
.catch((e) => {
|
|
172
|
-
clearTimeout(timeout);
|
|
172
|
+
// clearTimeout(timeout);
|
|
173
173
|
console.error(
|
|
174
174
|
"Error waiting for peer to load",
|
|
175
175
|
id,
|
|
@@ -688,14 +688,14 @@ export class SyncManager {
|
|
|
688
688
|
return this.requestedSyncs[coValue.id]!.done;
|
|
689
689
|
} else {
|
|
690
690
|
const done = new Promise<void>((resolve) => {
|
|
691
|
-
|
|
691
|
+
queueMicrotask(async () => {
|
|
692
692
|
delete this.requestedSyncs[coValue.id];
|
|
693
693
|
// if (entry.nRequestsThisTick >= 2) {
|
|
694
694
|
// console.log("Syncing", coValue.id, "for", entry.nRequestsThisTick, "requests");
|
|
695
695
|
// }
|
|
696
696
|
await this.actuallySyncCoValue(coValue);
|
|
697
697
|
resolve();
|
|
698
|
-
}
|
|
698
|
+
});
|
|
699
699
|
});
|
|
700
700
|
const entry = {
|
|
701
701
|
done,
|
|
@@ -707,14 +707,14 @@ export class SyncManager {
|
|
|
707
707
|
}
|
|
708
708
|
|
|
709
709
|
async actuallySyncCoValue(coValue: CoValueCore) {
|
|
710
|
-
let blockingSince = performance.now();
|
|
710
|
+
// let blockingSince = performance.now();
|
|
711
711
|
for (const peer of this.peersInPriorityOrder()) {
|
|
712
|
-
if (performance.now() - blockingSince > 5) {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
712
|
+
// if (performance.now() - blockingSince > 5) {
|
|
713
|
+
// await new Promise<void>((resolve) => {
|
|
714
|
+
// setTimeout(resolve, 0);
|
|
715
|
+
// });
|
|
716
|
+
// blockingSince = performance.now();
|
|
717
|
+
// }
|
|
718
718
|
const optimisticKnownState = peer.optimisticKnownStates[coValue.id];
|
|
719
719
|
|
|
720
720
|
if (optimisticKnownState) {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { expect, test, beforeEach } from "vitest";
|
|
2
|
+
import { expectList, expectMap, expectStream } from "../coValue.js";
|
|
3
|
+
import { RawBinaryCoStream } from "../coValues/coStream.js";
|
|
4
|
+
import { createdNowUnique } from "../crypto.js";
|
|
5
|
+
import { MAX_RECOMMENDED_TX_SIZE, cojsonReady } from "../index.js";
|
|
6
|
+
import { LocalNode } from "../localNode.js";
|
|
7
|
+
import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
|
|
8
|
+
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
|
9
|
+
|
|
10
|
+
import { webcrypto } from "node:crypto";
|
|
11
|
+
if (!("crypto" in globalThis)) {
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
(globalThis as any).crypto = webcrypto;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
await cojsonReady;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("Empty CoList works", () => {
|
|
21
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
|
22
|
+
|
|
23
|
+
const coValue = node.createCoValue({
|
|
24
|
+
type: "colist",
|
|
25
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
26
|
+
meta: null,
|
|
27
|
+
...createdNowUnique(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const content = expectList(coValue.getCurrentContent());
|
|
31
|
+
|
|
32
|
+
expect(content.type).toEqual("colist");
|
|
33
|
+
expect(content.toJSON()).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("Can append, prepend, delete and replace items in CoList", () => {
|
|
37
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
|
38
|
+
|
|
39
|
+
const coValue = node.createCoValue({
|
|
40
|
+
type: "colist",
|
|
41
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
42
|
+
meta: null,
|
|
43
|
+
...createdNowUnique(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const content = expectList(coValue.getCurrentContent());
|
|
47
|
+
|
|
48
|
+
content.append("hello", 0, "trusting");
|
|
49
|
+
expect(content.toJSON()).toEqual(["hello"]);
|
|
50
|
+
content.append("world", 0, "trusting");
|
|
51
|
+
expect(content.toJSON()).toEqual(["hello", "world"]);
|
|
52
|
+
content.prepend("beautiful", 1, "trusting");
|
|
53
|
+
expect(content.toJSON()).toEqual(["hello", "beautiful", "world"]);
|
|
54
|
+
content.prepend("hooray", 3, "trusting");
|
|
55
|
+
expect(content.toJSON()).toEqual(["hello", "beautiful", "world", "hooray"]);
|
|
56
|
+
content.replace(2, "universe", "trusting");
|
|
57
|
+
expect(content.toJSON()).toEqual(["hello", "beautiful", "universe", "hooray"]);
|
|
58
|
+
content.delete(2, "trusting");
|
|
59
|
+
expect(content.toJSON()).toEqual(["hello", "beautiful", "hooray"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("Push is equivalent to append after last item", () => {
|
|
63
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
|
64
|
+
|
|
65
|
+
const coValue = node.createCoValue({
|
|
66
|
+
type: "colist",
|
|
67
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
68
|
+
meta: null,
|
|
69
|
+
...createdNowUnique(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const content = expectList(coValue.getCurrentContent());
|
|
73
|
+
|
|
74
|
+
expect(content.type).toEqual("colist");
|
|
75
|
+
|
|
76
|
+
content.append("hello", 0, "trusting");
|
|
77
|
+
expect(content.toJSON()).toEqual(["hello"]);
|
|
78
|
+
content.append("world", undefined, "trusting");
|
|
79
|
+
expect(content.toJSON()).toEqual(["hello", "world"]);
|
|
80
|
+
content.append("hooray", undefined, "trusting");
|
|
81
|
+
expect(content.toJSON()).toEqual(["hello", "world", "hooray"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("Can push into empty list", () => {
|
|
85
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
|
86
|
+
|
|
87
|
+
const coValue = node.createCoValue({
|
|
88
|
+
type: "colist",
|
|
89
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
90
|
+
meta: null,
|
|
91
|
+
...createdNowUnique(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const content = expectList(coValue.getCurrentContent());
|
|
95
|
+
|
|
96
|
+
expect(content.type).toEqual("colist");
|
|
97
|
+
|
|
98
|
+
content.append("hello", undefined, "trusting");
|
|
99
|
+
expect(content.toJSON()).toEqual(["hello"]);
|
|
100
|
+
});
|