cojson 0.7.0-alpha.5 → 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.
Files changed (113) hide show
  1. package/.eslintrc.cjs +3 -2
  2. package/.prettierrc.js +9 -0
  3. package/.turbo/turbo-build.log +3 -30
  4. package/.turbo/turbo-lint.log +4 -0
  5. package/.turbo/turbo-test.log +1106 -0
  6. package/CHANGELOG.md +104 -0
  7. package/README.md +3 -1
  8. package/dist/base64url.test.js +25 -0
  9. package/dist/base64url.test.js.map +1 -0
  10. package/dist/coValueCore.js +60 -37
  11. package/dist/coValueCore.js.map +1 -1
  12. package/dist/coValues/account.js +16 -15
  13. package/dist/coValues/account.js.map +1 -1
  14. package/dist/coValues/coList.js +1 -1
  15. package/dist/coValues/coList.js.map +1 -1
  16. package/dist/coValues/coMap.js +17 -8
  17. package/dist/coValues/coMap.js.map +1 -1
  18. package/dist/coValues/group.js +13 -14
  19. package/dist/coValues/group.js.map +1 -1
  20. package/dist/coreToCoValue.js.map +1 -1
  21. package/dist/crypto/PureJSCrypto.js +89 -0
  22. package/dist/crypto/PureJSCrypto.js.map +1 -0
  23. package/dist/crypto/WasmCrypto.js +127 -0
  24. package/dist/crypto/WasmCrypto.js.map +1 -0
  25. package/dist/crypto/crypto.js +151 -0
  26. package/dist/crypto/crypto.js.map +1 -0
  27. package/dist/ids.js +4 -2
  28. package/dist/ids.js.map +1 -1
  29. package/dist/index.js +6 -8
  30. package/dist/index.js.map +1 -1
  31. package/dist/jsonStringify.js.map +1 -1
  32. package/dist/localNode.js +41 -38
  33. package/dist/localNode.js.map +1 -1
  34. package/dist/permissions.js +6 -6
  35. package/dist/permissions.js.map +1 -1
  36. package/dist/storage/FileSystem.js +61 -0
  37. package/dist/storage/FileSystem.js.map +1 -0
  38. package/dist/storage/chunksAndKnownStates.js +97 -0
  39. package/dist/storage/chunksAndKnownStates.js.map +1 -0
  40. package/dist/storage/index.js +265 -0
  41. package/dist/storage/index.js.map +1 -0
  42. package/dist/sync.js +29 -25
  43. package/dist/sync.js.map +1 -1
  44. package/dist/tests/account.test.js +58 -0
  45. package/dist/tests/account.test.js.map +1 -0
  46. package/dist/tests/coList.test.js +76 -0
  47. package/dist/tests/coList.test.js.map +1 -0
  48. package/dist/tests/coMap.test.js +136 -0
  49. package/dist/tests/coMap.test.js.map +1 -0
  50. package/dist/tests/coStream.test.js +172 -0
  51. package/dist/tests/coStream.test.js.map +1 -0
  52. package/dist/tests/coValueCore.test.js +114 -0
  53. package/dist/tests/coValueCore.test.js.map +1 -0
  54. package/dist/tests/crypto.test.js +118 -0
  55. package/dist/tests/crypto.test.js.map +1 -0
  56. package/dist/tests/cryptoImpl.test.js +113 -0
  57. package/dist/tests/cryptoImpl.test.js.map +1 -0
  58. package/dist/tests/group.test.js +34 -0
  59. package/dist/tests/group.test.js.map +1 -0
  60. package/dist/tests/permissions.test.js +1060 -0
  61. package/dist/tests/permissions.test.js.map +1 -0
  62. package/dist/tests/sync.test.js +816 -0
  63. package/dist/tests/sync.test.js.map +1 -0
  64. package/dist/tests/testUtils.js +12 -11
  65. package/dist/tests/testUtils.js.map +1 -1
  66. package/dist/typeUtils/accountOrAgentIDfromSessionID.js.map +1 -1
  67. package/dist/typeUtils/isAccountID.js.map +1 -1
  68. package/dist/typeUtils/isCoValue.js.map +1 -1
  69. package/package.json +14 -27
  70. package/src/base64url.test.ts +6 -5
  71. package/src/coValue.ts +1 -1
  72. package/src/coValueCore.ts +179 -126
  73. package/src/coValues/account.ts +30 -32
  74. package/src/coValues/coList.ts +11 -11
  75. package/src/coValues/coMap.ts +27 -17
  76. package/src/coValues/coStream.ts +17 -17
  77. package/src/coValues/group.ts +93 -109
  78. package/src/coreToCoValue.ts +5 -2
  79. package/src/crypto/PureJSCrypto.ts +200 -0
  80. package/src/crypto/WasmCrypto.ts +259 -0
  81. package/src/crypto/crypto.ts +336 -0
  82. package/src/ids.ts +8 -7
  83. package/src/index.ts +24 -24
  84. package/src/jsonStringify.ts +6 -4
  85. package/src/jsonValue.ts +2 -2
  86. package/src/localNode.ts +103 -109
  87. package/src/media.ts +3 -3
  88. package/src/permissions.ts +19 -21
  89. package/src/storage/FileSystem.ts +152 -0
  90. package/src/storage/chunksAndKnownStates.ts +139 -0
  91. package/src/storage/index.ts +479 -0
  92. package/src/streamUtils.ts +12 -12
  93. package/src/sync.ts +79 -63
  94. package/src/tests/account.test.ts +15 -15
  95. package/src/tests/coList.test.ts +94 -0
  96. package/src/tests/coMap.test.ts +162 -0
  97. package/src/tests/coStream.test.ts +246 -0
  98. package/src/tests/coValueCore.test.ts +36 -37
  99. package/src/tests/crypto.test.ts +66 -72
  100. package/src/tests/cryptoImpl.test.ts +183 -0
  101. package/src/tests/group.test.ts +16 -17
  102. package/src/tests/permissions.test.ts +269 -283
  103. package/src/tests/sync.test.ts +122 -123
  104. package/src/tests/testUtils.ts +24 -21
  105. package/src/typeUtils/accountOrAgentIDfromSessionID.ts +1 -2
  106. package/src/typeUtils/expectGroup.ts +1 -1
  107. package/src/typeUtils/isAccountID.ts +0 -1
  108. package/src/typeUtils/isCoValue.ts +1 -2
  109. package/tsconfig.json +0 -1
  110. package/dist/crypto.js +0 -254
  111. package/dist/crypto.js.map +0 -1
  112. package/src/crypto.ts +0 -484
  113. 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
+ }