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
package/src/coValueCore.ts
CHANGED
|
@@ -250,7 +250,8 @@ export class CoValueCore {
|
|
|
250
250
|
newTransactions,
|
|
251
251
|
newSignature,
|
|
252
252
|
expectedNewHash,
|
|
253
|
-
newStreamingHash
|
|
253
|
+
newStreamingHash,
|
|
254
|
+
"immediate"
|
|
254
255
|
);
|
|
255
256
|
|
|
256
257
|
return true;
|
|
@@ -287,7 +288,8 @@ export class CoValueCore {
|
|
|
287
288
|
return false;
|
|
288
289
|
}
|
|
289
290
|
|
|
290
|
-
const nTxBefore =
|
|
291
|
+
const nTxBefore =
|
|
292
|
+
this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
|
|
291
293
|
|
|
292
294
|
// const beforeHash = performance.now();
|
|
293
295
|
const { expectedNewHash, newStreamingHash } =
|
|
@@ -298,7 +300,8 @@ export class CoValueCore {
|
|
|
298
300
|
// afterHash - beforeHash
|
|
299
301
|
// );
|
|
300
302
|
|
|
301
|
-
const nTxAfter =
|
|
303
|
+
const nTxAfter =
|
|
304
|
+
this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
|
|
302
305
|
|
|
303
306
|
if (nTxAfter !== nTxBefore) {
|
|
304
307
|
const newTransactionLengthBefore = newTransactions.length;
|
|
@@ -320,7 +323,7 @@ export class CoValueCore {
|
|
|
320
323
|
return false;
|
|
321
324
|
}
|
|
322
325
|
|
|
323
|
-
|
|
326
|
+
performance.mark("verifyStart" + this.id);
|
|
324
327
|
if (!verify(newSignature, expectedNewHash, signerID)) {
|
|
325
328
|
console.warn(
|
|
326
329
|
"Invalid signature in",
|
|
@@ -332,18 +335,20 @@ export class CoValueCore {
|
|
|
332
335
|
resolveDone();
|
|
333
336
|
return false;
|
|
334
337
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
338
|
+
performance.mark("verifyEnd" + this.id);
|
|
339
|
+
performance.measure(
|
|
340
|
+
"verify" + this.id,
|
|
341
|
+
"verifyStart" + this.id,
|
|
342
|
+
"verifyEnd" + this.id
|
|
343
|
+
);
|
|
340
344
|
|
|
341
345
|
this.doAddTransactions(
|
|
342
346
|
sessionID,
|
|
343
347
|
newTransactions,
|
|
344
348
|
newSignature,
|
|
345
349
|
expectedNewHash,
|
|
346
|
-
newStreamingHash
|
|
350
|
+
newStreamingHash,
|
|
351
|
+
"deferred"
|
|
347
352
|
);
|
|
348
353
|
|
|
349
354
|
resolveDone();
|
|
@@ -355,12 +360,15 @@ export class CoValueCore {
|
|
|
355
360
|
newTransactions: Transaction[],
|
|
356
361
|
newSignature: Signature,
|
|
357
362
|
expectedNewHash: Hash,
|
|
358
|
-
newStreamingHash: StreamingHash
|
|
363
|
+
newStreamingHash: StreamingHash,
|
|
364
|
+
notifyMode: "immediate" | "deferred"
|
|
359
365
|
) {
|
|
360
|
-
const transactions =
|
|
366
|
+
const transactions =
|
|
367
|
+
this.sessionLogs.get(sessionID)?.transactions ?? [];
|
|
361
368
|
transactions.push(...newTransactions);
|
|
362
369
|
|
|
363
|
-
const signatureAfter =
|
|
370
|
+
const signatureAfter =
|
|
371
|
+
this.sessionLogs.get(sessionID)?.signatureAfter ?? {};
|
|
364
372
|
|
|
365
373
|
const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce(
|
|
366
374
|
(max, idx) => (parseInt(idx) > max ? parseInt(idx) : max),
|
|
@@ -402,13 +410,33 @@ export class CoValueCore {
|
|
|
402
410
|
this._cachedNewContentSinceEmpty = undefined;
|
|
403
411
|
|
|
404
412
|
if (this.listeners.size > 0) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
listener
|
|
413
|
+
if (notifyMode === "immediate") {
|
|
414
|
+
const content = this.getCurrentContent();
|
|
415
|
+
for (const listener of this.listeners) {
|
|
416
|
+
listener(content);
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
if (!this.nextDeferredNotify) {
|
|
420
|
+
this.nextDeferredNotify = new Promise((resolve) => {
|
|
421
|
+
setTimeout(() => {
|
|
422
|
+
this.nextDeferredNotify = undefined;
|
|
423
|
+
this.deferredUpdates = 0;
|
|
424
|
+
const content = this.getCurrentContent();
|
|
425
|
+
for (const listener of this.listeners) {
|
|
426
|
+
listener(content);
|
|
427
|
+
}
|
|
428
|
+
resolve();
|
|
429
|
+
}, 0);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
this.deferredUpdates++;
|
|
408
433
|
}
|
|
409
434
|
}
|
|
410
435
|
}
|
|
411
436
|
|
|
437
|
+
deferredUpdates = 0;
|
|
438
|
+
nextDeferredNotify: Promise<void> | undefined;
|
|
439
|
+
|
|
412
440
|
subscribe(listener: (content?: RawCoValue) => void): () => void {
|
|
413
441
|
this.listeners.add(listener);
|
|
414
442
|
listener(this.getCurrentContent());
|
|
@@ -533,7 +561,9 @@ export class CoValueCore {
|
|
|
533
561
|
return success;
|
|
534
562
|
}
|
|
535
563
|
|
|
536
|
-
getCurrentContent(options?: {
|
|
564
|
+
getCurrentContent(options?: {
|
|
565
|
+
ignorePrivateTransactions: true;
|
|
566
|
+
}): RawCoValue {
|
|
537
567
|
if (!options?.ignorePrivateTransactions && this._cachedContent) {
|
|
538
568
|
return this._cachedContent;
|
|
539
569
|
}
|
|
@@ -781,7 +811,11 @@ export class CoValueCore {
|
|
|
781
811
|
|
|
782
812
|
let sessionsTodoAgain: Set<SessionID> | undefined | "first" = "first";
|
|
783
813
|
|
|
784
|
-
while (
|
|
814
|
+
while (
|
|
815
|
+
sessionsTodoAgain === "first" ||
|
|
816
|
+
sessionsTodoAgain?.size ||
|
|
817
|
+
0 > 0
|
|
818
|
+
) {
|
|
785
819
|
if (sessionsTodoAgain === "first") {
|
|
786
820
|
sessionsTodoAgain = undefined;
|
|
787
821
|
}
|
|
@@ -798,10 +832,12 @@ export class CoValueCore {
|
|
|
798
832
|
sentStateForSessionID
|
|
799
833
|
);
|
|
800
834
|
|
|
801
|
-
const firstNewTxIdx =
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
835
|
+
const firstNewTxIdx =
|
|
836
|
+
sentStateForSessionID ?? knownStateForSessionID ?? 0;
|
|
837
|
+
const afterLastNewTxIdx =
|
|
838
|
+
nextKnownSignatureIdx === undefined
|
|
839
|
+
? log.transactions.length
|
|
840
|
+
: nextKnownSignatureIdx + 1;
|
|
805
841
|
|
|
806
842
|
const nNewTx = Math.max(0, afterLastNewTxIdx - firstNewTxIdx);
|
|
807
843
|
|
|
@@ -818,11 +854,16 @@ export class CoValueCore {
|
|
|
818
854
|
}
|
|
819
855
|
|
|
820
856
|
const oldPieceSize = pieceSize;
|
|
821
|
-
for (
|
|
857
|
+
for (
|
|
858
|
+
let txIdx = firstNewTxIdx;
|
|
859
|
+
txIdx < afterLastNewTxIdx;
|
|
860
|
+
txIdx++
|
|
861
|
+
) {
|
|
822
862
|
const tx = log.transactions[txIdx]!;
|
|
823
|
-
pieceSize +=
|
|
824
|
-
|
|
825
|
-
|
|
863
|
+
pieceSize +=
|
|
864
|
+
tx.privacy === "private"
|
|
865
|
+
? tx.encryptedChanges.length
|
|
866
|
+
: tx.changes.length;
|
|
826
867
|
}
|
|
827
868
|
|
|
828
869
|
if (pieceSize >= MAX_RECOMMENDED_TX_SIZE) {
|
|
@@ -839,26 +880,33 @@ export class CoValueCore {
|
|
|
839
880
|
let sessionEntry = currentPiece.new[sessionID];
|
|
840
881
|
if (!sessionEntry) {
|
|
841
882
|
sessionEntry = {
|
|
842
|
-
after:
|
|
883
|
+
after:
|
|
884
|
+
sentStateForSessionID ??
|
|
885
|
+
knownStateForSessionID ??
|
|
886
|
+
0,
|
|
843
887
|
newTransactions: [],
|
|
844
888
|
lastSignature: "WILL_BE_REPLACED" as Signature,
|
|
845
889
|
};
|
|
846
890
|
currentPiece.new[sessionID] = sessionEntry;
|
|
847
891
|
}
|
|
848
892
|
|
|
849
|
-
for (
|
|
893
|
+
for (
|
|
894
|
+
let txIdx = firstNewTxIdx;
|
|
895
|
+
txIdx < afterLastNewTxIdx;
|
|
896
|
+
txIdx++
|
|
897
|
+
) {
|
|
850
898
|
const tx = log.transactions[txIdx]!;
|
|
851
899
|
sessionEntry.newTransactions.push(tx);
|
|
852
900
|
}
|
|
853
901
|
|
|
854
|
-
|
|
855
902
|
sessionEntry.lastSignature =
|
|
856
903
|
nextKnownSignatureIdx === undefined
|
|
857
904
|
? log.lastSignature!
|
|
858
905
|
: log.signatureAfter[nextKnownSignatureIdx]!;
|
|
859
906
|
|
|
860
907
|
sentState[sessionID] =
|
|
861
|
-
(sentStateForSessionID ?? knownStateForSessionID ?? 0) +
|
|
908
|
+
(sentStateForSessionID ?? knownStateForSessionID ?? 0) +
|
|
909
|
+
nNewTx;
|
|
862
910
|
}
|
|
863
911
|
}
|
|
864
912
|
|
|
@@ -894,32 +942,35 @@ export class CoValueCore {
|
|
|
894
942
|
.keys()
|
|
895
943
|
.filter((k): k is AccountID => k.startsWith("co_"))
|
|
896
944
|
: this.header.ruleset.type === "ownedByGroup"
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
945
|
+
? [
|
|
946
|
+
this.header.ruleset.group,
|
|
947
|
+
...new Set(
|
|
948
|
+
[...this.sessionLogs.keys()]
|
|
949
|
+
.map((sessionID) =>
|
|
950
|
+
accountOrAgentIDfromSessionID(
|
|
951
|
+
sessionID as SessionID
|
|
952
|
+
)
|
|
953
|
+
)
|
|
954
|
+
.filter(
|
|
955
|
+
(session): session is AccountID =>
|
|
956
|
+
isAccountID(session) && session !== this.id
|
|
957
|
+
)
|
|
958
|
+
),
|
|
959
|
+
]
|
|
960
|
+
: [];
|
|
913
961
|
}
|
|
914
962
|
}
|
|
915
963
|
|
|
916
964
|
function getNextKnownSignatureIdx(
|
|
917
965
|
log: SessionLog,
|
|
918
966
|
knownStateForSessionID?: number,
|
|
919
|
-
sentStateForSessionID?: number
|
|
967
|
+
sentStateForSessionID?: number
|
|
920
968
|
) {
|
|
921
969
|
return Object.keys(log.signatureAfter)
|
|
922
970
|
.map(Number)
|
|
923
971
|
.sort((a, b) => a - b)
|
|
924
|
-
.find(
|
|
972
|
+
.find(
|
|
973
|
+
(idx) =>
|
|
974
|
+
idx >= (sentStateForSessionID ?? knownStateForSessionID ?? -1)
|
|
975
|
+
);
|
|
925
976
|
}
|
package/src/crypto.ts
CHANGED
|
@@ -276,7 +276,7 @@ export class StreamingHash {
|
|
|
276
276
|
this.state = fromClone || blake3Instance.init().save();
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
update(value: JsonValue) {
|
|
279
|
+
update(value: JsonValue): Uint8Array {
|
|
280
280
|
const encoded = textEncoder.encode(stableStringify(value));
|
|
281
281
|
// const before = performance.now();
|
|
282
282
|
this.state = blake3incrementalUpdateSLOW_WITH_DEVTOOLS(
|
|
@@ -285,6 +285,7 @@ export class StreamingHash {
|
|
|
285
285
|
);
|
|
286
286
|
// const after = performance.now();
|
|
287
287
|
// console.log(`Hashing throughput in MB/s`, 1000 * (encoded.length / (after - before)) / (1024 * 1024));
|
|
288
|
+
return encoded;
|
|
288
289
|
}
|
|
289
290
|
|
|
290
291
|
digest(): Hash {
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
secretSeedLength,
|
|
21
21
|
shortHashLength,
|
|
22
22
|
cryptoReady,
|
|
23
|
+
StreamingHash
|
|
23
24
|
} from "./crypto.js";
|
|
24
25
|
import { connectedPeers } from "./streamUtils.js";
|
|
25
26
|
import { ControlledAgent, RawControlledAccount } from "./coValues/account.js";
|
|
@@ -56,6 +57,14 @@ import type * as Media from "./media.js";
|
|
|
56
57
|
|
|
57
58
|
type Value = JsonValue | AnyRawCoValue;
|
|
58
59
|
|
|
60
|
+
import {
|
|
61
|
+
LSMStorage,
|
|
62
|
+
FSErr,
|
|
63
|
+
BlockFilename,
|
|
64
|
+
WalFilename,
|
|
65
|
+
} from "./storage/index.js";
|
|
66
|
+
import { FileSystem } from "./storage/FileSystem.js";
|
|
67
|
+
|
|
59
68
|
/** @hidden */
|
|
60
69
|
export const cojsonInternals = {
|
|
61
70
|
agentSecretFromBytes,
|
|
@@ -78,6 +87,7 @@ export const cojsonInternals = {
|
|
|
78
87
|
isAccountID,
|
|
79
88
|
accountHeaderForInitialAgentSecret,
|
|
80
89
|
idforHeader,
|
|
90
|
+
StreamingHash
|
|
81
91
|
};
|
|
82
92
|
|
|
83
93
|
export {
|
|
@@ -113,7 +123,12 @@ export {
|
|
|
113
123
|
AgentSecret,
|
|
114
124
|
InviteSecret,
|
|
115
125
|
SyncMessage,
|
|
116
|
-
isRawCoID
|
|
126
|
+
isRawCoID,
|
|
127
|
+
FileSystem,
|
|
128
|
+
LSMStorage,
|
|
129
|
+
FSErr,
|
|
130
|
+
BlockFilename,
|
|
131
|
+
WalFilename,
|
|
117
132
|
};
|
|
118
133
|
|
|
119
134
|
export type { Value };
|
package/src/localNode.ts
CHANGED
|
@@ -534,7 +534,7 @@ export class LocalNode {
|
|
|
534
534
|
);
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
-
return
|
|
537
|
+
return (coValue.getCurrentContent() as RawAccount).currentAgentID();
|
|
538
538
|
}
|
|
539
539
|
|
|
540
540
|
async resolveAccountAgentAsync(
|
|
@@ -569,7 +569,7 @@ export class LocalNode {
|
|
|
569
569
|
);
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
-
return
|
|
572
|
+
return (coValue.getCurrentContent() as RawAccount).currentAgentID();
|
|
573
573
|
}
|
|
574
574
|
|
|
575
575
|
/**
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { CoValueChunk } from "./index.js";
|
|
3
|
+
import { RawCoID } from "../ids.js";
|
|
4
|
+
import { StreamingHash } from "../crypto.js";
|
|
5
|
+
|
|
6
|
+
export type BlockFilename =
|
|
7
|
+
`${string}-L${number}-H${number}.jsonl`;
|
|
8
|
+
|
|
9
|
+
export type BlockHeader = { id: RawCoID; start: number; length: number }[];
|
|
10
|
+
|
|
11
|
+
export type WalEntry = { id: RawCoID } & CoValueChunk;
|
|
12
|
+
|
|
13
|
+
export type WalFilename = `wal-${number}.jsonl`;
|
|
14
|
+
|
|
15
|
+
export type FSErr = {
|
|
16
|
+
type: "fileSystemError";
|
|
17
|
+
error: Error;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface FileSystem<WriteHandle, ReadHandle> {
|
|
21
|
+
createFile(filename: string): Effect.Effect<WriteHandle, FSErr>;
|
|
22
|
+
append(handle: WriteHandle, data: Uint8Array): Effect.Effect<void, FSErr>;
|
|
23
|
+
close(
|
|
24
|
+
handle: ReadHandle | WriteHandle,
|
|
25
|
+
): Effect.Effect<void, FSErr>;
|
|
26
|
+
closeAndRename(
|
|
27
|
+
handle: WriteHandle,
|
|
28
|
+
filename: BlockFilename
|
|
29
|
+
): Effect.Effect<void, FSErr>;
|
|
30
|
+
openToRead(
|
|
31
|
+
filename: string
|
|
32
|
+
): Effect.Effect<{ handle: ReadHandle; size: number; }, FSErr>;
|
|
33
|
+
read(
|
|
34
|
+
handle: ReadHandle,
|
|
35
|
+
offset: number,
|
|
36
|
+
length: number
|
|
37
|
+
): Effect.Effect<Uint8Array, FSErr>;
|
|
38
|
+
listFiles(): Effect.Effect<string[], FSErr>;
|
|
39
|
+
removeFile(filename: BlockFilename | WalFilename): Effect.Effect<void, FSErr>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const textEncoder = new TextEncoder();
|
|
43
|
+
export const textDecoder = new TextDecoder();
|
|
44
|
+
|
|
45
|
+
export function readChunk<RH, FS extends FileSystem<unknown, RH>>(
|
|
46
|
+
handle: RH,
|
|
47
|
+
header: { start: number; length: number; },
|
|
48
|
+
fs: FS
|
|
49
|
+
): Effect.Effect<CoValueChunk, FSErr> {
|
|
50
|
+
return Effect.gen(function* ($) {
|
|
51
|
+
const chunkBytes = yield* $(
|
|
52
|
+
fs.read(handle, header.start, header.length)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const chunk = JSON.parse(textDecoder.decode(chunkBytes));
|
|
56
|
+
return chunk;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function readHeader<RH, FS extends FileSystem<unknown, RH>>(
|
|
61
|
+
filename: string,
|
|
62
|
+
handle: RH,
|
|
63
|
+
size: number,
|
|
64
|
+
fs: FS
|
|
65
|
+
): Effect.Effect<BlockHeader, FSErr> {
|
|
66
|
+
return Effect.gen(function* ($) {
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
const headerLength = Number(filename.match(/-H(\d+)\.jsonl$/)![1]!);
|
|
70
|
+
|
|
71
|
+
const headerBytes = yield* $(
|
|
72
|
+
fs.read(handle, size - headerLength, headerLength)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const header = JSON.parse(textDecoder.decode(headerBytes));
|
|
76
|
+
return header;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function writeBlock<WH, RH, FS extends FileSystem<WH, RH>>(
|
|
81
|
+
chunks: Map<RawCoID, CoValueChunk>,
|
|
82
|
+
level: number,
|
|
83
|
+
fs: FS
|
|
84
|
+
): Effect.Effect<void, FSErr> {
|
|
85
|
+
if (chunks.size === 0) {
|
|
86
|
+
return Effect.die(new Error("No chunks to write"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return Effect.gen(function* ($) {
|
|
90
|
+
const blockHeader: BlockHeader = [];
|
|
91
|
+
|
|
92
|
+
let offset = 0;
|
|
93
|
+
|
|
94
|
+
const file = yield* $(
|
|
95
|
+
fs.createFile(
|
|
96
|
+
"wipBlock" +
|
|
97
|
+
Math.random().toString(36).substring(7) +
|
|
98
|
+
".tmp.jsonl"
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
const hash = new StreamingHash();
|
|
102
|
+
|
|
103
|
+
const chunksSortedById = Array.from(chunks).sort(([id1], [id2]) => id1.localeCompare(id2)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
for (const [id, chunk] of chunksSortedById) {
|
|
107
|
+
const encodedBytes = hash.update(chunk);
|
|
108
|
+
const encodedBytesWithNewline = new Uint8Array(
|
|
109
|
+
encodedBytes.length + 1
|
|
110
|
+
);
|
|
111
|
+
encodedBytesWithNewline.set(encodedBytes);
|
|
112
|
+
encodedBytesWithNewline[encodedBytes.length] = 10;
|
|
113
|
+
yield* $(fs.append(file, encodedBytesWithNewline));
|
|
114
|
+
const length = encodedBytesWithNewline.length;
|
|
115
|
+
blockHeader.push({ id, start: offset, length });
|
|
116
|
+
offset += length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const headerBytes = textEncoder.encode(JSON.stringify(blockHeader));
|
|
120
|
+
yield* $(fs.append(file, headerBytes));
|
|
121
|
+
|
|
122
|
+
console.log(
|
|
123
|
+
"full file",
|
|
124
|
+
yield* $(
|
|
125
|
+
fs.read(file as unknown as RH, 0, offset + headerBytes.length)
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const filename: BlockFilename = `${hash.digest()}-L${level}-H${headerBytes.length}.jsonl`;
|
|
130
|
+
console.log("renaming to" + filename);
|
|
131
|
+
yield* $(fs.closeAndRename(file, filename));
|
|
132
|
+
|
|
133
|
+
console.log("Wrote block", filename, blockHeader);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function writeToWal<WH, RH, FS extends FileSystem<WH, RH>>(
|
|
138
|
+
handle: WH,
|
|
139
|
+
fs: FS,
|
|
140
|
+
id: RawCoID,
|
|
141
|
+
chunk: CoValueChunk
|
|
142
|
+
): Effect.Effect<void, FSErr> {
|
|
143
|
+
return Effect.gen(function* ($) {
|
|
144
|
+
const walEntry: WalEntry = {
|
|
145
|
+
id,
|
|
146
|
+
...chunk,
|
|
147
|
+
};
|
|
148
|
+
const bytes = textEncoder.encode(JSON.stringify(walEntry) + "\n");
|
|
149
|
+
yield* $(fs.append(handle, bytes));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Either } from 'effect';
|
|
2
|
+
import { RawCoID, SessionID } from '../ids.js';
|
|
3
|
+
import { MAX_RECOMMENDED_TX_SIZE } from '../index.js';
|
|
4
|
+
import { CoValueKnownState, NewContentMessage } from "../sync.js";
|
|
5
|
+
import { CoValueChunk } from "./index.js";
|
|
6
|
+
|
|
7
|
+
export function contentSinceChunk(
|
|
8
|
+
id: RawCoID,
|
|
9
|
+
chunk: CoValueChunk,
|
|
10
|
+
known?: CoValueKnownState
|
|
11
|
+
): NewContentMessage[] {
|
|
12
|
+
const newContentPieces: NewContentMessage[] = [];
|
|
13
|
+
|
|
14
|
+
newContentPieces.push({
|
|
15
|
+
id: id,
|
|
16
|
+
action: "content",
|
|
17
|
+
header: known?.header ? undefined : chunk.header,
|
|
18
|
+
new: {},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
for (const [sessionID, sessionsEntry] of Object.entries(
|
|
22
|
+
chunk.sessionEntries
|
|
23
|
+
)) {
|
|
24
|
+
for (const entry of sessionsEntry) {
|
|
25
|
+
const knownStart = known?.sessions[sessionID as SessionID] || 0;
|
|
26
|
+
|
|
27
|
+
if (entry.after + entry.transactions.length <= knownStart) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const actuallyNewTransactions = entry.transactions.slice(
|
|
32
|
+
Math.max(0, knownStart - entry.after)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const newAfter = entry.after +
|
|
36
|
+
(actuallyNewTransactions.length - entry.transactions.length);
|
|
37
|
+
|
|
38
|
+
let newContentEntry = newContentPieces[0]?.new[sessionID as SessionID];
|
|
39
|
+
|
|
40
|
+
if (!newContentEntry) {
|
|
41
|
+
newContentEntry = {
|
|
42
|
+
after: newAfter,
|
|
43
|
+
lastSignature: entry.lastSignature,
|
|
44
|
+
newTransactions: actuallyNewTransactions,
|
|
45
|
+
};
|
|
46
|
+
newContentPieces[0]!.new[sessionID as SessionID] =
|
|
47
|
+
newContentEntry;
|
|
48
|
+
} else {
|
|
49
|
+
newContentEntry.newTransactions.push(
|
|
50
|
+
...actuallyNewTransactions
|
|
51
|
+
);
|
|
52
|
+
newContentEntry.lastSignature = entry.lastSignature;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return newContentPieces;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function chunkToKnownState(id: RawCoID, chunk: CoValueChunk) {
|
|
61
|
+
const ourKnown: CoValueKnownState = {
|
|
62
|
+
id,
|
|
63
|
+
header: !!chunk.header,
|
|
64
|
+
sessions: {},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
for (const [sessionID, sessionEntries] of Object.entries(
|
|
68
|
+
chunk.sessionEntries
|
|
69
|
+
)) {
|
|
70
|
+
for (const entry of sessionEntries) {
|
|
71
|
+
ourKnown.sessions[sessionID as SessionID] =
|
|
72
|
+
entry.after + entry.transactions.length;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return ourKnown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function mergeChunks(
|
|
79
|
+
chunkA: CoValueChunk,
|
|
80
|
+
chunkB: CoValueChunk
|
|
81
|
+
): Either.Either<"nonContigous", CoValueChunk> {
|
|
82
|
+
const header = chunkA.header || chunkB.header;
|
|
83
|
+
|
|
84
|
+
const newSessions = { ...chunkA.sessionEntries };
|
|
85
|
+
for (const sessionID in chunkB.sessionEntries) {
|
|
86
|
+
// figure out if we can merge the chunks
|
|
87
|
+
const sessionEntriesA = chunkA.sessionEntries[sessionID];
|
|
88
|
+
const sessionEntriesB = chunkB.sessionEntries[sessionID]!;
|
|
89
|
+
|
|
90
|
+
if (!sessionEntriesA) {
|
|
91
|
+
newSessions[sessionID] = sessionEntriesB;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const lastEntryOfA = sessionEntriesA[sessionEntriesA.length - 1]!;
|
|
96
|
+
const firstEntryOfB = sessionEntriesB[0]!;
|
|
97
|
+
|
|
98
|
+
if (lastEntryOfA.after + lastEntryOfA.transactions.length ===
|
|
99
|
+
firstEntryOfB.after) {
|
|
100
|
+
const newEntries = [];
|
|
101
|
+
let bytesSinceLastSignature = 0;
|
|
102
|
+
for (const entry of sessionEntriesA.concat(sessionEntriesB)) {
|
|
103
|
+
const entryByteLength = entry.transactions.reduce(
|
|
104
|
+
(sum, tx) => sum +
|
|
105
|
+
(tx.privacy === "private"
|
|
106
|
+
? tx.encryptedChanges.length
|
|
107
|
+
: tx.changes.length),
|
|
108
|
+
0
|
|
109
|
+
);
|
|
110
|
+
if (newEntries.length === 0 ||
|
|
111
|
+
bytesSinceLastSignature + entryByteLength >
|
|
112
|
+
MAX_RECOMMENDED_TX_SIZE) {
|
|
113
|
+
newEntries.push({
|
|
114
|
+
after: entry.after,
|
|
115
|
+
lastSignature: entry.lastSignature,
|
|
116
|
+
transactions: entry.transactions,
|
|
117
|
+
});
|
|
118
|
+
bytesSinceLastSignature = 0;
|
|
119
|
+
} else {
|
|
120
|
+
const lastNewEntry = newEntries[newEntries.length - 1]!;
|
|
121
|
+
lastNewEntry.transactions.push(...entry.transactions);
|
|
122
|
+
|
|
123
|
+
bytesSinceLastSignature += entry.transactions.length;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
return Either.right("nonContigous" as const);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return Either.left({ header, sessionEntries: newSessions });
|
|
132
|
+
}
|