cojson 0.7.0-alpha.7 → 0.7.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/.eslintrc.cjs +3 -2
- package/.prettierrc.js +9 -0
- package/.turbo/turbo-build.log +3 -30
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +1106 -0
- package/CHANGELOG.md +98 -0
- package/README.md +3 -1
- package/dist/base64url.test.js +25 -0
- package/dist/base64url.test.js.map +1 -0
- package/dist/coValueCore.js +60 -37
- package/dist/coValueCore.js.map +1 -1
- package/dist/coValues/account.js +16 -15
- package/dist/coValues/account.js.map +1 -1
- package/dist/coValues/coList.js +1 -1
- package/dist/coValues/coList.js.map +1 -1
- package/dist/coValues/coMap.js +17 -8
- package/dist/coValues/coMap.js.map +1 -1
- package/dist/coValues/group.js +13 -14
- package/dist/coValues/group.js.map +1 -1
- package/dist/coreToCoValue.js.map +1 -1
- package/dist/crypto/PureJSCrypto.js +89 -0
- package/dist/crypto/PureJSCrypto.js.map +1 -0
- package/dist/crypto/WasmCrypto.js +127 -0
- package/dist/crypto/WasmCrypto.js.map +1 -0
- package/dist/crypto/crypto.js +151 -0
- package/dist/crypto/crypto.js.map +1 -0
- package/dist/ids.js +4 -2
- package/dist/ids.js.map +1 -1
- package/dist/index.js +6 -8
- package/dist/index.js.map +1 -1
- package/dist/jsonStringify.js.map +1 -1
- package/dist/localNode.js +41 -38
- package/dist/localNode.js.map +1 -1
- package/dist/permissions.js +6 -6
- package/dist/permissions.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 +29 -25
- package/dist/sync.js.map +1 -1
- package/dist/tests/account.test.js +58 -0
- package/dist/tests/account.test.js.map +1 -0
- package/dist/tests/coList.test.js +76 -0
- package/dist/tests/coList.test.js.map +1 -0
- package/dist/tests/coMap.test.js +136 -0
- package/dist/tests/coMap.test.js.map +1 -0
- package/dist/tests/coStream.test.js +172 -0
- package/dist/tests/coStream.test.js.map +1 -0
- package/dist/tests/coValueCore.test.js +114 -0
- package/dist/tests/coValueCore.test.js.map +1 -0
- package/dist/tests/crypto.test.js +118 -0
- package/dist/tests/crypto.test.js.map +1 -0
- package/dist/tests/cryptoImpl.test.js +113 -0
- package/dist/tests/cryptoImpl.test.js.map +1 -0
- package/dist/tests/group.test.js +34 -0
- package/dist/tests/group.test.js.map +1 -0
- package/dist/tests/permissions.test.js +1060 -0
- package/dist/tests/permissions.test.js.map +1 -0
- package/dist/tests/sync.test.js +816 -0
- package/dist/tests/sync.test.js.map +1 -0
- package/dist/tests/testUtils.js +12 -11
- package/dist/tests/testUtils.js.map +1 -1
- package/dist/typeUtils/accountOrAgentIDfromSessionID.js.map +1 -1
- package/dist/typeUtils/isAccountID.js.map +1 -1
- package/dist/typeUtils/isCoValue.js.map +1 -1
- package/package.json +14 -27
- package/src/base64url.test.ts +6 -5
- package/src/coValue.ts +1 -1
- package/src/coValueCore.ts +179 -126
- package/src/coValues/account.ts +30 -32
- package/src/coValues/coList.ts +11 -11
- package/src/coValues/coMap.ts +27 -17
- package/src/coValues/coStream.ts +17 -17
- package/src/coValues/group.ts +93 -109
- package/src/coreToCoValue.ts +5 -2
- package/src/crypto/PureJSCrypto.ts +200 -0
- package/src/crypto/WasmCrypto.ts +259 -0
- package/src/crypto/crypto.ts +336 -0
- package/src/ids.ts +8 -7
- package/src/index.ts +24 -24
- package/src/jsonStringify.ts +6 -4
- package/src/jsonValue.ts +2 -2
- package/src/localNode.ts +103 -109
- package/src/media.ts +3 -3
- package/src/permissions.ts +19 -21
- package/src/storage/FileSystem.ts +152 -0
- package/src/storage/chunksAndKnownStates.ts +139 -0
- package/src/storage/index.ts +479 -0
- package/src/streamUtils.ts +12 -12
- package/src/sync.ts +79 -63
- package/src/tests/account.test.ts +15 -15
- package/src/tests/coList.test.ts +94 -0
- package/src/tests/coMap.test.ts +162 -0
- package/src/tests/coStream.test.ts +246 -0
- package/src/tests/coValueCore.test.ts +36 -37
- package/src/tests/crypto.test.ts +66 -72
- package/src/tests/cryptoImpl.test.ts +183 -0
- package/src/tests/group.test.ts +16 -17
- package/src/tests/permissions.test.ts +269 -283
- package/src/tests/sync.test.ts +122 -123
- package/src/tests/testUtils.ts +24 -21
- package/src/typeUtils/accountOrAgentIDfromSessionID.ts +1 -2
- package/src/typeUtils/expectGroup.ts +1 -1
- package/src/typeUtils/isAccountID.ts +0 -1
- package/src/typeUtils/isCoValue.ts +1 -2
- package/tsconfig.json +0 -1
- package/dist/crypto.js +0 -254
- package/dist/crypto.js.map +0 -1
- package/src/crypto.ts +0 -484
- package/src/tests/coValue.test.ts +0 -497
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { CoValueChunk } from "./index.js";
|
|
3
|
+
import { RawCoID } from "../ids.js";
|
|
4
|
+
import { CryptoProvider, StreamingHash } from "../crypto/crypto.js";
|
|
5
|
+
|
|
6
|
+
export type BlockFilename = `${string}-L${number}-H${number}.jsonl`;
|
|
7
|
+
|
|
8
|
+
export type BlockHeader = { id: RawCoID; start: number; length: number }[];
|
|
9
|
+
|
|
10
|
+
export type WalEntry = { id: RawCoID } & CoValueChunk;
|
|
11
|
+
|
|
12
|
+
export type WalFilename = `wal-${number}.jsonl`;
|
|
13
|
+
|
|
14
|
+
export type FSErr = {
|
|
15
|
+
type: "fileSystemError";
|
|
16
|
+
error: Error;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface FileSystem<WriteHandle, ReadHandle> {
|
|
20
|
+
crypto: CryptoProvider;
|
|
21
|
+
createFile(filename: string): Effect.Effect<WriteHandle, FSErr>;
|
|
22
|
+
append(handle: WriteHandle, data: Uint8Array): Effect.Effect<void, FSErr>;
|
|
23
|
+
close(handle: ReadHandle | WriteHandle): Effect.Effect<void, FSErr>;
|
|
24
|
+
closeAndRename(
|
|
25
|
+
handle: WriteHandle,
|
|
26
|
+
filename: BlockFilename,
|
|
27
|
+
): Effect.Effect<void, FSErr>;
|
|
28
|
+
openToRead(
|
|
29
|
+
filename: string,
|
|
30
|
+
): Effect.Effect<{ handle: ReadHandle; size: number }, FSErr>;
|
|
31
|
+
read(
|
|
32
|
+
handle: ReadHandle,
|
|
33
|
+
offset: number,
|
|
34
|
+
length: number,
|
|
35
|
+
): Effect.Effect<Uint8Array, FSErr>;
|
|
36
|
+
listFiles(): Effect.Effect<string[], FSErr>;
|
|
37
|
+
removeFile(
|
|
38
|
+
filename: BlockFilename | WalFilename,
|
|
39
|
+
): 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
|
+
const headerLength = Number(filename.match(/-H(\d+)\.jsonl$/)![1]!);
|
|
68
|
+
|
|
69
|
+
const headerBytes = yield* $(
|
|
70
|
+
fs.read(handle, size - headerLength, headerLength),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const header = JSON.parse(textDecoder.decode(headerBytes));
|
|
74
|
+
return header;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function writeBlock<WH, RH, FS extends FileSystem<WH, RH>>(
|
|
79
|
+
chunks: Map<RawCoID, CoValueChunk>,
|
|
80
|
+
level: number,
|
|
81
|
+
fs: FS,
|
|
82
|
+
): Effect.Effect<void, FSErr> {
|
|
83
|
+
if (chunks.size === 0) {
|
|
84
|
+
return Effect.die(new Error("No chunks to write"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return Effect.gen(function* ($) {
|
|
88
|
+
const blockHeader: BlockHeader = [];
|
|
89
|
+
|
|
90
|
+
let offset = 0;
|
|
91
|
+
|
|
92
|
+
const file = yield* $(
|
|
93
|
+
fs.createFile(
|
|
94
|
+
"wipBlock" +
|
|
95
|
+
Math.random().toString(36).substring(7) +
|
|
96
|
+
".tmp.jsonl",
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
const hash = new StreamingHash(fs.crypto);
|
|
100
|
+
|
|
101
|
+
const chunksSortedById = Array.from(chunks).sort(([id1], [id2]) =>
|
|
102
|
+
id1.localeCompare(id2),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
for (const [id, chunk] of chunksSortedById) {
|
|
106
|
+
const encodedBytes = hash.update(chunk);
|
|
107
|
+
const encodedBytesWithNewline = new Uint8Array(
|
|
108
|
+
encodedBytes.length + 1,
|
|
109
|
+
);
|
|
110
|
+
encodedBytesWithNewline.set(encodedBytes);
|
|
111
|
+
encodedBytesWithNewline[encodedBytes.length] = 10;
|
|
112
|
+
yield* $(fs.append(file, encodedBytesWithNewline));
|
|
113
|
+
const length = encodedBytesWithNewline.length;
|
|
114
|
+
blockHeader.push({ id, start: offset, length });
|
|
115
|
+
offset += length;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const headerBytes = textEncoder.encode(JSON.stringify(blockHeader));
|
|
119
|
+
yield* $(fs.append(file, headerBytes));
|
|
120
|
+
|
|
121
|
+
console.log(
|
|
122
|
+
"full file",
|
|
123
|
+
yield* $(
|
|
124
|
+
fs.read(file as unknown as RH, 0, offset + headerBytes.length),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const filename: BlockFilename = `${hash.digest()}-L${level}-H${
|
|
129
|
+
headerBytes.length
|
|
130
|
+
}.jsonl`;
|
|
131
|
+
console.log("renaming to" + filename);
|
|
132
|
+
yield* $(fs.closeAndRename(file, filename));
|
|
133
|
+
|
|
134
|
+
console.log("Wrote block", filename, blockHeader);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function writeToWal<WH, RH, FS extends FileSystem<WH, RH>>(
|
|
139
|
+
handle: WH,
|
|
140
|
+
fs: FS,
|
|
141
|
+
id: RawCoID,
|
|
142
|
+
chunk: CoValueChunk,
|
|
143
|
+
): Effect.Effect<void, FSErr> {
|
|
144
|
+
return Effect.gen(function* ($) {
|
|
145
|
+
const walEntry: WalEntry = {
|
|
146
|
+
id,
|
|
147
|
+
...chunk,
|
|
148
|
+
};
|
|
149
|
+
const bytes = textEncoder.encode(JSON.stringify(walEntry) + "\n");
|
|
150
|
+
yield* $(fs.append(handle, bytes));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
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 =
|
|
36
|
+
entry.after +
|
|
37
|
+
(actuallyNewTransactions.length - entry.transactions.length);
|
|
38
|
+
|
|
39
|
+
let newContentEntry =
|
|
40
|
+
newContentPieces[0]?.new[sessionID as SessionID];
|
|
41
|
+
|
|
42
|
+
if (!newContentEntry) {
|
|
43
|
+
newContentEntry = {
|
|
44
|
+
after: newAfter,
|
|
45
|
+
lastSignature: entry.lastSignature,
|
|
46
|
+
newTransactions: actuallyNewTransactions,
|
|
47
|
+
};
|
|
48
|
+
newContentPieces[0]!.new[sessionID as SessionID] =
|
|
49
|
+
newContentEntry;
|
|
50
|
+
} else {
|
|
51
|
+
newContentEntry.newTransactions.push(
|
|
52
|
+
...actuallyNewTransactions,
|
|
53
|
+
);
|
|
54
|
+
newContentEntry.lastSignature = entry.lastSignature;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return newContentPieces;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function chunkToKnownState(id: RawCoID, chunk: CoValueChunk) {
|
|
63
|
+
const ourKnown: CoValueKnownState = {
|
|
64
|
+
id,
|
|
65
|
+
header: !!chunk.header,
|
|
66
|
+
sessions: {},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
for (const [sessionID, sessionEntries] of Object.entries(
|
|
70
|
+
chunk.sessionEntries,
|
|
71
|
+
)) {
|
|
72
|
+
for (const entry of sessionEntries) {
|
|
73
|
+
ourKnown.sessions[sessionID as SessionID] =
|
|
74
|
+
entry.after + entry.transactions.length;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return ourKnown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function mergeChunks(
|
|
81
|
+
chunkA: CoValueChunk,
|
|
82
|
+
chunkB: CoValueChunk,
|
|
83
|
+
): Either.Either<"nonContigous", CoValueChunk> {
|
|
84
|
+
const header = chunkA.header || chunkB.header;
|
|
85
|
+
|
|
86
|
+
const newSessions = { ...chunkA.sessionEntries };
|
|
87
|
+
for (const sessionID in chunkB.sessionEntries) {
|
|
88
|
+
// figure out if we can merge the chunks
|
|
89
|
+
const sessionEntriesA = chunkA.sessionEntries[sessionID];
|
|
90
|
+
const sessionEntriesB = chunkB.sessionEntries[sessionID]!;
|
|
91
|
+
|
|
92
|
+
if (!sessionEntriesA) {
|
|
93
|
+
newSessions[sessionID] = sessionEntriesB;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lastEntryOfA = sessionEntriesA[sessionEntriesA.length - 1]!;
|
|
98
|
+
const firstEntryOfB = sessionEntriesB[0]!;
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
lastEntryOfA.after + lastEntryOfA.transactions.length ===
|
|
102
|
+
firstEntryOfB.after
|
|
103
|
+
) {
|
|
104
|
+
const newEntries = [];
|
|
105
|
+
let bytesSinceLastSignature = 0;
|
|
106
|
+
for (const entry of sessionEntriesA.concat(sessionEntriesB)) {
|
|
107
|
+
const entryByteLength = entry.transactions.reduce(
|
|
108
|
+
(sum, tx) =>
|
|
109
|
+
sum +
|
|
110
|
+
(tx.privacy === "private"
|
|
111
|
+
? tx.encryptedChanges.length
|
|
112
|
+
: tx.changes.length),
|
|
113
|
+
0,
|
|
114
|
+
);
|
|
115
|
+
if (
|
|
116
|
+
newEntries.length === 0 ||
|
|
117
|
+
bytesSinceLastSignature + entryByteLength >
|
|
118
|
+
MAX_RECOMMENDED_TX_SIZE
|
|
119
|
+
) {
|
|
120
|
+
newEntries.push({
|
|
121
|
+
after: entry.after,
|
|
122
|
+
lastSignature: entry.lastSignature,
|
|
123
|
+
transactions: entry.transactions,
|
|
124
|
+
});
|
|
125
|
+
bytesSinceLastSignature = 0;
|
|
126
|
+
} else {
|
|
127
|
+
const lastNewEntry = newEntries[newEntries.length - 1]!;
|
|
128
|
+
lastNewEntry.transactions.push(...entry.transactions);
|
|
129
|
+
|
|
130
|
+
bytesSinceLastSignature += entry.transactions.length;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
return Either.right("nonContigous" as const);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return Either.left({ header, sessionEntries: newSessions });
|
|
139
|
+
}
|