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,479 @@
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/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 type { 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(
350
+ blockFile,
351
+ handle,
352
+ size,
353
+ fs,
354
+ );
355
+ for (const entry of header) {
356
+ cachedHeader[entry.id] = {
357
+ start: entry.start,
358
+ length: entry.length,
359
+ };
360
+ }
361
+
362
+ this.headerCache.set(blockFile, cachedHeader);
363
+ }
364
+ const headerEntry = cachedHeader[id];
365
+
366
+ let result;
367
+ if (headerEntry) {
368
+ result = yield* readChunk(handle, headerEntry, fs);
369
+ }
370
+
371
+ yield* fs.close(handle);
372
+
373
+ return result;
374
+ }
375
+
376
+ return undefined;
377
+ });
378
+ }
379
+
380
+ async compact() {
381
+ await Effect.runPromise(
382
+ Effect.gen(this, function* () {
383
+ const fileNames = yield* this.fs.listFiles();
384
+
385
+ const walFiles = fileNames.filter((name) =>
386
+ name.startsWith("wal-"),
387
+ ) as WalFilename[];
388
+ walFiles.sort();
389
+
390
+ const coValues = new Map<RawCoID, CoValueChunk>();
391
+
392
+ console.log("Compacting WAL files", walFiles);
393
+ if (walFiles.length === 0) return;
394
+
395
+ yield* SynchronizedRef.updateEffect(this.currentWal, (wal) =>
396
+ Effect.gen(this, function* () {
397
+ if (wal) {
398
+ yield* this.fs.close(wal);
399
+ }
400
+ return undefined;
401
+ }),
402
+ );
403
+
404
+ for (const fileName of walFiles) {
405
+ const { handle, size } =
406
+ yield* this.fs.openToRead(fileName);
407
+ if (size === 0) {
408
+ yield* this.fs.close(handle);
409
+ continue;
410
+ }
411
+ const bytes = yield* this.fs.read(handle, 0, size);
412
+
413
+ const decoded = textDecoder.decode(bytes);
414
+ const lines = decoded.split("\n");
415
+
416
+ for (const line of lines) {
417
+ if (line.length === 0) continue;
418
+ const chunk = JSON.parse(line) as WalEntry;
419
+
420
+ const existingChunk = coValues.get(chunk.id);
421
+
422
+ if (existingChunk) {
423
+ const merged = mergeChunks(existingChunk, chunk);
424
+ if (Either.isRight(merged)) {
425
+ console.warn(
426
+ "Non-contigous chunks in " +
427
+ chunk.id +
428
+ ", " +
429
+ fileName,
430
+ existingChunk,
431
+ chunk,
432
+ );
433
+ } else {
434
+ coValues.set(chunk.id, merged.left);
435
+ }
436
+ } else {
437
+ coValues.set(chunk.id, chunk);
438
+ }
439
+ }
440
+
441
+ yield* this.fs.close(handle);
442
+ }
443
+
444
+ yield* writeBlock(coValues, 0, this.fs);
445
+ for (const walFile of walFiles) {
446
+ yield* this.fs.removeFile(walFile);
447
+ }
448
+ this.fileCache = undefined;
449
+ }),
450
+ );
451
+
452
+ setTimeout(() => this.compact(), 5000);
453
+ }
454
+
455
+ static asPeer<WH, RH, FS extends FileSystem<WH, RH>>({
456
+ fs,
457
+ trace,
458
+ localNodeName = "local",
459
+ }: {
460
+ fs: FS;
461
+ trace?: boolean;
462
+ localNodeName?: string;
463
+ }): Peer {
464
+ const [localNodeAsPeer, storageAsPeer] = connectedPeers(
465
+ localNodeName,
466
+ "storage",
467
+ {
468
+ peer1role: "client",
469
+ peer2role: "server",
470
+ trace,
471
+ },
472
+ );
473
+
474
+ new LSMStorage(fs, localNodeAsPeer.incoming, localNodeAsPeer.outgoing);
475
+
476
+ // return { ...storageAsPeer, priority: 200 };
477
+ return storageAsPeer;
478
+ }
479
+ }
@@ -16,7 +16,7 @@ export function connectedPeers(
16
16
  trace?: boolean;
17
17
  peer1role?: Peer["role"];
18
18
  peer2role?: Peer["role"];
19
- } = {}
19
+ } = {},
20
20
  ): [Peer, Peer] {
21
21
  const [inRx1, inTx1] = newStreamPair<SyncMessage>(peer1id + "_in");
22
22
  const [outRx1, outTx1] = newStreamPair<SyncMessage>(peer1id + "_out");
@@ -29,7 +29,7 @@ export function connectedPeers(
29
29
  new TransformStream({
30
30
  transform(
31
31
  chunk: SyncMessage,
32
- controller: { enqueue: (msg: SyncMessage) => void }
32
+ controller: { enqueue: (msg: SyncMessage) => void },
33
33
  ) {
34
34
  trace &&
35
35
  console.debug(
@@ -40,12 +40,12 @@ export function connectedPeers(
40
40
  k === "changes" || k === "encryptedChanges"
41
41
  ? v.slice(0, 20) + "..."
42
42
  : v,
43
- 2
44
- )
43
+ 2,
44
+ ),
45
45
  );
46
46
  controller.enqueue(chunk);
47
47
  },
48
- })
48
+ }),
49
49
  )
50
50
  .pipeTo(inTx1);
51
51
 
@@ -54,7 +54,7 @@ export function connectedPeers(
54
54
  new TransformStream({
55
55
  transform(
56
56
  chunk: SyncMessage,
57
- controller: { enqueue: (msg: SyncMessage) => void }
57
+ controller: { enqueue: (msg: SyncMessage) => void },
58
58
  ) {
59
59
  trace &&
60
60
  console.debug(
@@ -65,12 +65,12 @@ export function connectedPeers(
65
65
  k === "changes" || k === "encryptedChanges"
66
66
  ? v.slice(0, 20) + "..."
67
67
  : v,
68
- 2
69
- )
68
+ 2,
69
+ ),
70
70
  );
71
71
  controller.enqueue(chunk);
72
72
  },
73
- })
73
+ }),
74
74
  )
75
75
  .pipeTo(inTx2);
76
76
 
@@ -92,7 +92,7 @@ export function connectedPeers(
92
92
  }
93
93
 
94
94
  export function newStreamPair<T>(
95
- pairName?: string
95
+ pairName?: string,
96
96
  ): [ReadableStream<T>, WritableStream<T>] {
97
97
  let queueLength = 0;
98
98
  let readerClosed = false;
@@ -138,13 +138,13 @@ export function newStreamPair<T>(
138
138
  new TransformStream<any, any>({
139
139
  transform(
140
140
  chunk: SyncMessage,
141
- controller: { enqueue: (msg: SyncMessage) => void }
141
+ controller: { enqueue: (msg: SyncMessage) => void },
142
142
  ) {
143
143
  queueLength -= 1;
144
144
  maybeReportQueueLength();
145
145
  controller.enqueue(chunk);
146
146
  },
147
- })
147
+ }),
148
148
  ) as ReadableStream<T>;
149
149
 
150
150
  let lastWritePromise = Promise.resolve();