@instantdb/core 0.22.139 → 0.22.140

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 (40) hide show
  1. package/dist/commonjs/Connection.d.ts +2 -1
  2. package/dist/commonjs/Connection.d.ts.map +1 -1
  3. package/dist/commonjs/Connection.js +6 -2
  4. package/dist/commonjs/Connection.js.map +1 -1
  5. package/dist/commonjs/Reactor.d.ts +12 -0
  6. package/dist/commonjs/Reactor.d.ts.map +1 -1
  7. package/dist/commonjs/Reactor.js +63 -18
  8. package/dist/commonjs/Reactor.js.map +1 -1
  9. package/dist/commonjs/Stream.d.ts +112 -0
  10. package/dist/commonjs/Stream.d.ts.map +1 -0
  11. package/dist/commonjs/Stream.js +708 -0
  12. package/dist/commonjs/Stream.js.map +1 -0
  13. package/dist/commonjs/index.d.ts +41 -2
  14. package/dist/commonjs/index.d.ts.map +1 -1
  15. package/dist/commonjs/index.js +41 -1
  16. package/dist/commonjs/index.js.map +1 -1
  17. package/dist/esm/Connection.d.ts +2 -1
  18. package/dist/esm/Connection.d.ts.map +1 -1
  19. package/dist/esm/Connection.js +6 -2
  20. package/dist/esm/Connection.js.map +1 -1
  21. package/dist/esm/Reactor.d.ts +12 -0
  22. package/dist/esm/Reactor.d.ts.map +1 -1
  23. package/dist/esm/Reactor.js +53 -9
  24. package/dist/esm/Reactor.js.map +1 -1
  25. package/dist/esm/Stream.d.ts +112 -0
  26. package/dist/esm/Stream.d.ts.map +1 -0
  27. package/dist/esm/Stream.js +701 -0
  28. package/dist/esm/Stream.js.map +1 -0
  29. package/dist/esm/index.d.ts +41 -2
  30. package/dist/esm/index.d.ts.map +1 -1
  31. package/dist/esm/index.js +42 -1
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/standalone/index.js +2203 -1604
  34. package/dist/standalone/index.umd.cjs +3 -3
  35. package/package.json +2 -2
  36. package/src/Connection.ts +6 -2
  37. package/src/Reactor.js +56 -10
  38. package/src/Stream.ts +1016 -0
  39. package/src/index.ts +71 -1
  40. package/tsconfig.json +2 -1
package/src/Stream.ts ADDED
@@ -0,0 +1,1016 @@
1
+ import uuid from './utils/id.ts';
2
+ import { Logger } from './utils/log.ts';
3
+ import { STATUS } from './Reactor.js';
4
+ import { InstantError } from './InstantError.ts';
5
+
6
+ export type WritableStreamCtor = {
7
+ new <W = any>(
8
+ underlyingSink?: UnderlyingSink<W>,
9
+ strategy?: QueuingStrategy<W>,
10
+ ): WritableStream<W>;
11
+ };
12
+
13
+ export type ReadableStreamCtor = {
14
+ new <R = any>(
15
+ underlyingSource?: UnderlyingDefaultSource<R>,
16
+ strategy?: QueuingStrategy<R>,
17
+ ): ReadableStream<R>;
18
+ };
19
+
20
+ export interface InstantWritableStream<T> extends WritableStream<T> {
21
+ streamId: () => Promise<string>;
22
+ }
23
+
24
+ type WriteStreamStartResult =
25
+ | { type: 'ok'; streamId: string; offset: number }
26
+ | { type: 'disconnect' }
27
+ | { type: 'error'; error: InstantError };
28
+
29
+ type WriteStreamCbs = {
30
+ onDisconnect: () => void;
31
+ onConnectionReconnect: () => void;
32
+ onFlush: (args: { offset: number; done: boolean }) => void;
33
+ onAppendFailed: () => void;
34
+ };
35
+
36
+ function createWriteStream({
37
+ WStream,
38
+ opts,
39
+ startStream,
40
+ appendStream,
41
+ registerStream,
42
+ }: {
43
+ WStream: WritableStreamCtor;
44
+ opts: { clientId: string };
45
+ startStream: (opts: {
46
+ clientId: string;
47
+ reconnectToken: string;
48
+ }) => Promise<WriteStreamStartResult>;
49
+ appendStream: (opts: {
50
+ streamId: string;
51
+ chunks: string[];
52
+ isDone?: boolean;
53
+ offset: number;
54
+ abortReason?: string;
55
+ }) => void;
56
+ registerStream: (streamId: string, cbs: WriteStreamCbs) => void;
57
+ }): {
58
+ stream: InstantWritableStream<string>;
59
+ closed: () => boolean;
60
+ addCloseCb: (cb: () => void) => void;
61
+ } {
62
+ const clientId = opts.clientId;
63
+ let streamId_: string | null = null;
64
+ let controller_: WritableStreamDefaultController | null = null;
65
+ const reconnectToken = uuid();
66
+ let isDone: boolean = false;
67
+ let closed: boolean = false;
68
+ const closeCbs: (() => void)[] = [];
69
+ const streamIdCbs: ((streamId: string) => void)[] = [];
70
+ let disconnected: boolean = false;
71
+ // Chunks that we haven't been notified are flushed to disk
72
+ let bufferOffset = 0;
73
+ let bufferByteSize = 0;
74
+ const buffer: { chunk: string; byteLen: number }[] = [];
75
+ const encoder = new TextEncoder();
76
+
77
+ function markClosed() {
78
+ closed = true;
79
+ for (const cb of closeCbs) {
80
+ cb();
81
+ }
82
+ }
83
+
84
+ function addCloseCb(cb: () => void) {
85
+ closeCbs.push(cb);
86
+ return () => {
87
+ const i = closeCbs.indexOf(cb);
88
+ if (i !== -1) {
89
+ closeCbs.splice(i, 1);
90
+ }
91
+ };
92
+ }
93
+
94
+ function addStreamIdCb(cb: (streamId: string) => void) {
95
+ streamIdCbs.push(cb);
96
+ return () => {
97
+ const i = streamIdCbs.indexOf(cb);
98
+ if (i !== -1) {
99
+ streamIdCbs.splice(i, 1);
100
+ }
101
+ };
102
+ }
103
+
104
+ function setStreamId(streamId: string) {
105
+ streamId_ = streamId;
106
+ for (const cb of streamIdCbs) {
107
+ cb(streamId_);
108
+ }
109
+ }
110
+
111
+ function onDisconnect() {
112
+ disconnected = true;
113
+ }
114
+
115
+ // Clears data from our buffer after it has been flushed to a file
116
+ function discardFlushed(offset: number) {
117
+ let chunkOffset = bufferOffset;
118
+ let segmentsToDrop = 0;
119
+ let droppedSegmentsByteLen = 0;
120
+
121
+ for (const { byteLen } of buffer) {
122
+ const nextChunkOffset = chunkOffset + byteLen;
123
+ if (nextChunkOffset > offset) {
124
+ break;
125
+ }
126
+ chunkOffset = nextChunkOffset;
127
+ segmentsToDrop++;
128
+ droppedSegmentsByteLen += byteLen;
129
+ }
130
+
131
+ if (segmentsToDrop > 0) {
132
+ bufferOffset += droppedSegmentsByteLen;
133
+ bufferByteSize -= droppedSegmentsByteLen;
134
+ buffer.splice(0, segmentsToDrop);
135
+ }
136
+ }
137
+
138
+ async function onConnectionReconnect() {
139
+ const result = await startStream({
140
+ clientId,
141
+ reconnectToken,
142
+ });
143
+ switch (result.type) {
144
+ case 'ok': {
145
+ const { streamId, offset } = result;
146
+ streamId_ = streamId;
147
+ discardFlushed(offset);
148
+ if (buffer.length) {
149
+ appendStream({
150
+ streamId: streamId,
151
+ chunks: buffer.map((b) => b.chunk),
152
+ offset: bufferOffset,
153
+ });
154
+ }
155
+ disconnected = false;
156
+ break;
157
+ }
158
+ case 'disconnect': {
159
+ onDisconnect();
160
+ break;
161
+ }
162
+ case 'error': {
163
+ if (controller_) {
164
+ controller_.error(result.error);
165
+ markClosed();
166
+ }
167
+ break;
168
+ }
169
+ }
170
+ }
171
+
172
+ // When the append fails, we'll just try to reconnect and start again
173
+ function onAppendFailed() {
174
+ onDisconnect();
175
+ onConnectionReconnect();
176
+ }
177
+
178
+ function onFlush({ offset, done }: { offset: number; done: boolean }) {
179
+ discardFlushed(offset);
180
+ if (done) {
181
+ isDone = true;
182
+ }
183
+ }
184
+
185
+ function error(
186
+ controller: WritableStreamDefaultController,
187
+ error: InstantError,
188
+ ) {
189
+ markClosed();
190
+ controller.error(error);
191
+ }
192
+
193
+ function ensureSetup(controller): string | null {
194
+ if (isDone) {
195
+ error(controller, new InstantError('Stream has been closed.'));
196
+ }
197
+ if (!streamId_) {
198
+ error(controller, new InstantError('Stream has not been initialized.'));
199
+ }
200
+ return streamId_;
201
+ }
202
+
203
+ async function start(controller: WritableStreamDefaultController) {
204
+ controller_ = controller;
205
+ let tryAgain = true;
206
+ let attempts = 0;
207
+
208
+ while (tryAgain) {
209
+ // rate-limit after the first few failed connects
210
+ let nextAttempt = Date.now() + Math.min(15000, 500 * (attempts - 1));
211
+ tryAgain = false;
212
+ const result = await startStream({
213
+ clientId: opts.clientId,
214
+ reconnectToken,
215
+ });
216
+
217
+ switch (result.type) {
218
+ case 'ok': {
219
+ const { streamId, offset } = result;
220
+ if (offset !== 0) {
221
+ const e = new InstantError('Write stream is corrupted');
222
+ error(controller, e);
223
+ return;
224
+ }
225
+ setStreamId(streamId);
226
+ registerStream(streamId, {
227
+ onDisconnect,
228
+ onFlush,
229
+ onConnectionReconnect,
230
+ onAppendFailed,
231
+ });
232
+ disconnected = false;
233
+ return;
234
+ }
235
+ case 'disconnect': {
236
+ tryAgain = true;
237
+ onDisconnect();
238
+ attempts++;
239
+ await new Promise((resolve) => {
240
+ // Try again immediately for the first two attempts, then back off
241
+ setTimeout(resolve, nextAttempt - Date.now());
242
+ });
243
+ break;
244
+ }
245
+ case 'error': {
246
+ error(controller, result.error);
247
+ return;
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ class WStreamEnhanced
254
+ extends WStream<string>
255
+ implements InstantWritableStream<string>
256
+ {
257
+ constructor(
258
+ sink?: UnderlyingSink<string>,
259
+ strategy?: QueuingStrategy<string>,
260
+ ) {
261
+ super(sink, strategy);
262
+ }
263
+
264
+ public async streamId(): Promise<string> {
265
+ if (streamId_) {
266
+ return streamId_;
267
+ }
268
+ return new Promise((resolve, reject) => {
269
+ const cleanupFns: (() => void)[] = [];
270
+ const cleanup = () => {
271
+ for (const f of cleanupFns) {
272
+ f();
273
+ }
274
+ };
275
+ const resolveCb = (streamId: string) => {
276
+ resolve(streamId);
277
+ cleanup();
278
+ };
279
+ const rejectCb = () => {
280
+ reject(new InstantError('Stream is closed.'));
281
+ cleanup();
282
+ };
283
+
284
+ cleanupFns.push(addStreamIdCb(resolveCb));
285
+ cleanupFns.push(addCloseCb(rejectCb));
286
+ });
287
+ }
288
+ }
289
+
290
+ const stream = new WStreamEnhanced({
291
+ // TODO(dww): accept a storage so that write streams can survive across
292
+ // browser restarts
293
+ async start(controller) {
294
+ try {
295
+ await start(controller);
296
+ } catch (e) {
297
+ error(controller, e);
298
+ }
299
+ },
300
+ write(chunk, controller) {
301
+ const streamId = ensureSetup(controller);
302
+ if (streamId) {
303
+ const byteLen = encoder.encode(chunk).length;
304
+ buffer.push({ chunk, byteLen });
305
+ const offset = bufferOffset + bufferByteSize;
306
+ bufferByteSize += byteLen;
307
+ if (!disconnected) {
308
+ appendStream({ streamId, chunks: [chunk], offset });
309
+ }
310
+ }
311
+ },
312
+ close() {
313
+ if (streamId_) {
314
+ appendStream({
315
+ streamId: streamId_,
316
+ chunks: [],
317
+ offset: bufferOffset + bufferByteSize,
318
+ isDone: true,
319
+ });
320
+ }
321
+ markClosed();
322
+ },
323
+ abort(reason) {
324
+ if (streamId_) {
325
+ appendStream({
326
+ streamId: streamId_,
327
+ chunks: [],
328
+ offset: bufferOffset + bufferByteSize,
329
+ isDone: true,
330
+ abortReason: reason,
331
+ });
332
+ }
333
+ markClosed();
334
+ },
335
+ });
336
+ return {
337
+ stream,
338
+ addCloseCb,
339
+ closed() {
340
+ return closed;
341
+ },
342
+ };
343
+ }
344
+
345
+ type ReadStreamUpdate =
346
+ | {
347
+ type: 'append';
348
+ offset: number;
349
+ files?: { url: string; size: number }[];
350
+ content?: string;
351
+ }
352
+ | { type: 'error'; error: InstantError }
353
+ | { type: 'reconnect' };
354
+
355
+ class StreamIterator<T> {
356
+ private items: T[] = [];
357
+ private resolvers: ((next: {
358
+ value: T | undefined;
359
+ done: boolean;
360
+ }) => void)[] = [];
361
+ private isClosed = false;
362
+
363
+ constructor() {}
364
+
365
+ push(item: T) {
366
+ if (this.isClosed) return;
367
+
368
+ const resolve = this.resolvers.shift();
369
+ if (resolve) {
370
+ resolve({ value: item, done: false });
371
+ } else {
372
+ this.items.push(item);
373
+ }
374
+ }
375
+
376
+ close() {
377
+ this.isClosed = true;
378
+ while (this.resolvers.length > 0) {
379
+ const resolve = this.resolvers.shift();
380
+ resolve!({ value: undefined, done: true });
381
+ }
382
+ }
383
+
384
+ async *[Symbol.asyncIterator]() {
385
+ while (true) {
386
+ if (this.items.length > 0) {
387
+ yield this.items.shift()!;
388
+ } else if (this.isClosed) {
389
+ return;
390
+ } else {
391
+ const { value, done } = await new Promise<{
392
+ value: T | undefined;
393
+ done: boolean;
394
+ }>((resolve) => {
395
+ this.resolvers.push(resolve);
396
+ });
397
+ if (done || !value) {
398
+ return;
399
+ }
400
+ yield value;
401
+ }
402
+ }
403
+ }
404
+ }
405
+
406
+ function createReadStream({
407
+ RStream,
408
+ opts,
409
+ startStream,
410
+ cancelStream,
411
+ }: {
412
+ RStream: ReadableStreamCtor;
413
+ opts: {
414
+ clientId?: string | null | undefined;
415
+ streamId?: string | null | undefined;
416
+ byteOffset?: number | null | undefined;
417
+ };
418
+ startStream: (opts: {
419
+ eventId: string;
420
+ clientId?: string | null | undefined;
421
+ streamId?: string | null | undefined;
422
+ offset?: number;
423
+ }) => StreamIterator<ReadStreamUpdate>;
424
+ cancelStream: (opts: { eventId: string }) => void;
425
+ }): {
426
+ stream: ReadableStream<string>;
427
+ closed: () => boolean;
428
+ addCloseCb: (cb: () => void) => void;
429
+ } {
430
+ let seenOffset = opts.byteOffset || 0;
431
+ let canceled = false;
432
+ const decoder = new TextDecoder('utf-8');
433
+ const encoder = new TextEncoder();
434
+ let eventId: string | null;
435
+ let closed = false;
436
+ const closeCbs: (() => void)[] = [];
437
+
438
+ function markClosed() {
439
+ closed = true;
440
+ for (const cb of closeCbs) {
441
+ cb();
442
+ }
443
+ }
444
+
445
+ function addCloseCb(cb: () => void) {
446
+ closeCbs.push(cb);
447
+ return () => {
448
+ const i = closeCbs.indexOf(cb);
449
+ if (i !== -1) {
450
+ closeCbs.splice(i, 1);
451
+ }
452
+ };
453
+ }
454
+
455
+ function error(
456
+ controller: ReadableStreamDefaultController<string>,
457
+ e: InstantError,
458
+ ) {
459
+ controller.error(e);
460
+ markClosed();
461
+ }
462
+
463
+ let fetchFailures = 0;
464
+ async function runStartStream(
465
+ opts: {
466
+ clientId?: string | null | undefined;
467
+ streamId?: string | null | undefined;
468
+ offset?: number;
469
+ },
470
+ controller: ReadableStreamDefaultController<string>,
471
+ ): Promise<{ retry: boolean } | undefined> {
472
+ eventId = uuid();
473
+ const streamOpts = { ...(opts || {}), eventId };
474
+ for await (const item of startStream(streamOpts)) {
475
+ if (canceled) {
476
+ return;
477
+ }
478
+
479
+ if (item.type === 'reconnect') {
480
+ return { retry: true };
481
+ }
482
+
483
+ if (item.type === 'error') {
484
+ error(controller, item.error);
485
+ return;
486
+ }
487
+
488
+ if (item.offset > seenOffset) {
489
+ error(controller, new InstantError('Stream is corrupted.'));
490
+ canceled = true;
491
+ return;
492
+ }
493
+
494
+ let discardLen = seenOffset - item.offset;
495
+
496
+ if (item.files && item.files.length) {
497
+ const fetchAbort = new AbortController();
498
+ let nextFetch = fetch(item.files[0].url, {
499
+ signal: fetchAbort.signal,
500
+ });
501
+ for (let i = 0; i < item.files.length; i++) {
502
+ const nextFile = item.files[i + 1];
503
+ const thisFetch = nextFetch;
504
+ const res = await thisFetch;
505
+ if (nextFile) {
506
+ nextFetch = fetch(nextFile.url, { signal: fetchAbort.signal });
507
+ }
508
+
509
+ if (!res.ok) {
510
+ fetchFailures++;
511
+ if (fetchFailures > 10) {
512
+ error(controller, new InstantError('Unable to process stream.'));
513
+ return;
514
+ }
515
+ return { retry: true };
516
+ }
517
+
518
+ if (res.body) {
519
+ for await (const bodyChunk of res.body) {
520
+ if (canceled) {
521
+ fetchAbort.abort();
522
+ return;
523
+ }
524
+ let chunk = bodyChunk;
525
+ if (discardLen > 0) {
526
+ chunk = bodyChunk.subarray(discardLen);
527
+ discardLen -= bodyChunk.length - chunk.length;
528
+ }
529
+ if (!chunk.length) {
530
+ continue;
531
+ }
532
+ seenOffset += chunk.length;
533
+ const s = decoder.decode(chunk);
534
+
535
+ controller.enqueue(s);
536
+ }
537
+ } else {
538
+ // RN doesn't support request.body
539
+ const bodyChunk = await res.arrayBuffer();
540
+ let chunk: ArrayBuffer | Uint8Array<ArrayBuffer> = bodyChunk;
541
+ if (canceled) {
542
+ fetchAbort.abort();
543
+ return;
544
+ }
545
+ if (discardLen > 0) {
546
+ chunk = new Uint8Array(bodyChunk).subarray(discardLen);
547
+ discardLen -= bodyChunk.byteLength - chunk.length;
548
+ }
549
+ if (!chunk.byteLength) {
550
+ continue;
551
+ }
552
+ seenOffset += chunk.byteLength;
553
+ const s = decoder.decode(chunk);
554
+ controller.enqueue(s);
555
+ }
556
+ }
557
+ }
558
+ fetchFailures = 0;
559
+ if (item.content) {
560
+ let content = item.content;
561
+ let encoded = encoder.encode(item.content);
562
+ if (discardLen > 0) {
563
+ const remaining = encoded.subarray(discardLen);
564
+ discardLen -= encoded.length - remaining.length;
565
+ if (!remaining.length) {
566
+ continue;
567
+ }
568
+ encoded = remaining;
569
+ content = decoder.decode(remaining);
570
+ }
571
+ seenOffset += encoded.length;
572
+ controller.enqueue(content);
573
+ }
574
+ }
575
+ }
576
+
577
+ async function start(controller: ReadableStreamDefaultController<string>) {
578
+ let retry = true;
579
+ let attempts = 0;
580
+ while (retry) {
581
+ retry = false;
582
+ let nextAttempt = Date.now() + Math.min(15000, 500 * (attempts - 1));
583
+ const res = await runStartStream(
584
+ { ...opts, offset: seenOffset },
585
+ controller,
586
+ );
587
+
588
+ if (res?.retry) {
589
+ retry = true;
590
+ attempts++;
591
+ if (nextAttempt < Date.now() - 300000) {
592
+ // reset attempts if we last tried 5 minutes ago
593
+ attempts = 0;
594
+ }
595
+ await new Promise((resolve) => {
596
+ setTimeout(resolve, nextAttempt - Date.now());
597
+ });
598
+ }
599
+ }
600
+ if (!canceled && !closed) {
601
+ controller.close();
602
+ markClosed();
603
+ }
604
+ }
605
+ const stream = new RStream<string>({
606
+ start(controller) {
607
+ start(controller);
608
+ },
609
+ cancel(_reason) {
610
+ canceled = true;
611
+ if (eventId) {
612
+ cancelStream({ eventId });
613
+ }
614
+ markClosed();
615
+ },
616
+ });
617
+
618
+ return {
619
+ stream,
620
+ addCloseCb,
621
+ closed() {
622
+ return closed;
623
+ },
624
+ };
625
+ }
626
+
627
+ type StartStreamMsg = {
628
+ op: 'start-stream';
629
+ 'client-id': string;
630
+ 'reconnect-token': string;
631
+ };
632
+
633
+ type AppendStreamMsg = {
634
+ op: 'append-stream';
635
+ 'stream-id': string;
636
+ chunks: string[];
637
+ offset: number;
638
+ done: boolean;
639
+ 'abort-reason'?: string;
640
+ };
641
+
642
+ type SubscribeStreamMsg = {
643
+ op: 'subscribe-stream';
644
+ 'stream-id'?: string;
645
+ 'client-id'?: string;
646
+ offset?: number;
647
+ };
648
+
649
+ type UnsubscribeStreamMsg = {
650
+ op: 'unsubscribe-stream';
651
+ 'subscribe-event-id': string;
652
+ };
653
+
654
+ type SendMsg =
655
+ | StartStreamMsg
656
+ | AppendStreamMsg
657
+ | SubscribeStreamMsg
658
+ | UnsubscribeStreamMsg;
659
+
660
+ type TrySend = (eventId: string, msg: SendMsg) => void;
661
+
662
+ type StartStreamOkMsg = {
663
+ op: 'start-stream-ok';
664
+ 'client-event-id': string;
665
+ 'stream-id': string;
666
+ offset: number;
667
+ };
668
+
669
+ type AppendStreamFailedMsg = {
670
+ op: 'append-failed';
671
+ 'stream-id': string;
672
+ };
673
+
674
+ type StreamFlushedMsg = {
675
+ op: 'stream-flushed';
676
+ 'stream-id': string;
677
+ offset: number;
678
+ done: boolean;
679
+ };
680
+
681
+ // Msg sent to reader when we receive new data
682
+ type StreamAppendMsg = {
683
+ op: 'stream-append';
684
+ 'stream-id': string;
685
+ 'client-id': string | null;
686
+ 'client-event-id': string;
687
+ files?: { url: string; size: number }[];
688
+ done?: boolean;
689
+ 'abort-reason'?: string;
690
+ offset: number;
691
+ error?: string;
692
+ retry: boolean;
693
+ content?: string;
694
+ };
695
+
696
+ type HandleRecieveErrorMsg = {
697
+ 'client-event-id': string;
698
+ 'original-event': SendMsg;
699
+ message?: string;
700
+ hint?: Record<string, any>;
701
+ type?: string;
702
+ };
703
+
704
+ export class InstantStream {
705
+ private trySend: TrySend;
706
+ private WStream: WritableStreamCtor;
707
+ private RStream: ReadableStreamCtor;
708
+ private writeStreams: Record<string, WriteStreamCbs> = {};
709
+ private startWriteStreamCbs: Record<
710
+ string,
711
+ (data: WriteStreamStartResult) => void
712
+ > = {};
713
+
714
+ private readStreamIterators: Record<
715
+ string,
716
+ StreamIterator<ReadStreamUpdate>
717
+ > = {};
718
+ private log: Logger;
719
+ private activeStreams: Set<ReadableStream | WritableStream> = new Set();
720
+
721
+ constructor({
722
+ WStream,
723
+ RStream,
724
+ trySend,
725
+ log,
726
+ }: {
727
+ WStream: WritableStreamCtor;
728
+ RStream: ReadableStreamCtor;
729
+ trySend: TrySend;
730
+ log: Logger;
731
+ }) {
732
+ this.WStream = WStream;
733
+ this.RStream = RStream;
734
+ this.trySend = trySend;
735
+ this.log = log;
736
+ }
737
+
738
+ public createWriteStream(opts: {
739
+ clientId: string;
740
+ }): InstantWritableStream<string> {
741
+ const { stream, addCloseCb } = createWriteStream({
742
+ WStream: this.WStream,
743
+ startStream: this.startWriteStream.bind(this),
744
+ appendStream: this.appendStream.bind(this),
745
+ registerStream: this.registerWriteStream.bind(this),
746
+ opts,
747
+ });
748
+ this.activeStreams.add(stream);
749
+ addCloseCb(() => {
750
+ this.activeStreams.delete(stream);
751
+ });
752
+ return stream;
753
+ }
754
+
755
+ public createReadStream(opts: {
756
+ clientId?: string | null | undefined;
757
+ streamId?: string | null | undefined;
758
+ byteOffset?: number | null | undefined;
759
+ }) {
760
+ const { stream, addCloseCb } = createReadStream({
761
+ RStream: this.RStream,
762
+ opts,
763
+ startStream: this.startReadStream.bind(this),
764
+ cancelStream: this.cancelReadStream.bind(this),
765
+ });
766
+ this.activeStreams.add(stream);
767
+ addCloseCb(() => {
768
+ this.activeStreams.delete(stream);
769
+ });
770
+ return stream;
771
+ }
772
+
773
+ private startWriteStream(opts: {
774
+ clientId: string;
775
+ reconnectToken: string;
776
+ }): Promise<WriteStreamStartResult> {
777
+ const eventId = uuid();
778
+ let resolve: ((data: WriteStreamStartResult) => void) | null = null;
779
+ const promise: Promise<WriteStreamStartResult> = new Promise((r) => {
780
+ resolve = r;
781
+ });
782
+ this.startWriteStreamCbs[eventId] = resolve!;
783
+ const msg: StartStreamMsg = {
784
+ op: 'start-stream',
785
+ 'client-id': opts.clientId,
786
+ 'reconnect-token': opts.reconnectToken,
787
+ };
788
+
789
+ this.trySend(eventId, msg);
790
+
791
+ return promise;
792
+ }
793
+
794
+ private registerWriteStream(streamId: string, cbs: WriteStreamCbs) {
795
+ this.writeStreams[streamId] = cbs;
796
+ }
797
+
798
+ private appendStream({
799
+ streamId,
800
+ chunks,
801
+ isDone,
802
+ offset,
803
+ abortReason,
804
+ }: {
805
+ streamId: string;
806
+ chunks: string[];
807
+ isDone?: boolean;
808
+ // XXX: Handle offset on the server
809
+ offset: number;
810
+ abortReason?: string;
811
+ }) {
812
+ const msg: AppendStreamMsg = {
813
+ op: 'append-stream',
814
+ 'stream-id': streamId,
815
+ chunks,
816
+ offset,
817
+ done: !!isDone,
818
+ };
819
+
820
+ if (abortReason) {
821
+ msg['abort-reason'] = abortReason;
822
+ }
823
+
824
+ this.trySend(uuid(), msg);
825
+ }
826
+
827
+ onAppendFailed(msg: AppendStreamFailedMsg) {
828
+ const cbs = this.writeStreams[msg['stream-id']];
829
+ if (cbs) {
830
+ cbs.onAppendFailed();
831
+ }
832
+ }
833
+
834
+ onStartStreamOk(msg: StartStreamOkMsg) {
835
+ const cb = this.startWriteStreamCbs[msg['client-event-id']];
836
+ if (!cb) {
837
+ this.log.info('No stream for start-stream-ok', msg);
838
+ return;
839
+ }
840
+ cb({ type: 'ok', streamId: msg['stream-id'], offset: msg.offset });
841
+ delete this.startWriteStreamCbs[msg['client-event-id']];
842
+ }
843
+
844
+ onStreamFlushed(msg: StreamFlushedMsg) {
845
+ const streamId = msg['stream-id'];
846
+ const cbs = this.writeStreams[streamId];
847
+ if (!cbs) {
848
+ this.log.info('No stream cbs for stream-flushed', msg);
849
+ return;
850
+ }
851
+ cbs.onFlush({ offset: msg.offset, done: msg.done });
852
+ if (msg.done) {
853
+ delete this.writeStreams[streamId];
854
+ }
855
+ }
856
+
857
+ private startReadStream({
858
+ eventId,
859
+ clientId,
860
+ streamId,
861
+ offset,
862
+ }: {
863
+ eventId: string;
864
+ clientId?: string;
865
+ streamId?: string;
866
+ offset?: number;
867
+ }): StreamIterator<ReadStreamUpdate> {
868
+ const msg: SubscribeStreamMsg = { op: 'subscribe-stream' };
869
+
870
+ if (!streamId && !clientId) {
871
+ throw new Error(
872
+ 'Must provide one of streamId or clientId to subscribe to the stream.',
873
+ );
874
+ }
875
+
876
+ if (streamId) {
877
+ msg['stream-id'] = streamId;
878
+ }
879
+
880
+ if (clientId) {
881
+ msg['client-id'] = clientId;
882
+ }
883
+
884
+ if (offset) {
885
+ msg['offset'] = offset;
886
+ }
887
+
888
+ const iterator = new StreamIterator<ReadStreamUpdate>();
889
+
890
+ this.readStreamIterators[eventId] = iterator;
891
+
892
+ this.trySend(eventId, msg);
893
+
894
+ return iterator;
895
+ }
896
+
897
+ private cancelReadStream({ eventId }: { eventId: string }) {
898
+ const msg: UnsubscribeStreamMsg = {
899
+ op: 'unsubscribe-stream',
900
+ 'subscribe-event-id': eventId,
901
+ };
902
+ this.trySend(uuid(), msg);
903
+ delete this.readStreamIterators[eventId];
904
+ }
905
+
906
+ onStreamAppend(msg: StreamAppendMsg) {
907
+ const eventId = msg['client-event-id'];
908
+ const iterator = this.readStreamIterators[eventId];
909
+
910
+ if (!iterator) {
911
+ this.log.info('No iterator for read stream', msg);
912
+ return;
913
+ }
914
+
915
+ if (msg.error) {
916
+ if (msg.retry) {
917
+ iterator.push({ type: 'reconnect' });
918
+ } else {
919
+ iterator.push({
920
+ type: 'error',
921
+ error: new InstantError(msg.error),
922
+ });
923
+ }
924
+ iterator.close();
925
+ delete this.readStreamIterators[eventId];
926
+ return;
927
+ }
928
+
929
+ if (msg.files?.length || msg.content) {
930
+ iterator.push({
931
+ type: 'append',
932
+ offset: msg.offset,
933
+ files: msg.files,
934
+ content: msg.content,
935
+ });
936
+ }
937
+
938
+ if (msg.done) {
939
+ iterator.close();
940
+ delete this.readStreamIterators[eventId];
941
+ }
942
+ }
943
+
944
+ onConnectionStatusChange(status) {
945
+ // Tell the writers to retry:
946
+ for (const cb of Object.values(this.startWriteStreamCbs)) {
947
+ cb({ type: 'disconnect' });
948
+ }
949
+ this.startWriteStreamCbs = {};
950
+
951
+ if (status !== STATUS.AUTHENTICATED) {
952
+ // Notify the writers that they've been disconnected
953
+ for (const { onDisconnect } of Object.values(this.writeStreams)) {
954
+ onDisconnect();
955
+ }
956
+ } else {
957
+ // Notify the writers that they need to reconnect
958
+ for (const { onConnectionReconnect } of Object.values(
959
+ this.writeStreams,
960
+ )) {
961
+ onConnectionReconnect();
962
+ }
963
+
964
+ // Notify the readers that they need to reconnect
965
+ for (const iterator of Object.values(this.readStreamIterators)) {
966
+ iterator.push({ type: 'reconnect' });
967
+ iterator.close();
968
+ }
969
+ this.readStreamIterators = {};
970
+ }
971
+ }
972
+
973
+ onRecieveError(msg: HandleRecieveErrorMsg) {
974
+ const ev = msg['original-event'];
975
+ switch (ev.op) {
976
+ case 'append-stream': {
977
+ const streamId = ev['stream-id'];
978
+ const cbs = this.writeStreams[streamId];
979
+ cbs?.onAppendFailed();
980
+ break;
981
+ }
982
+ case 'start-stream': {
983
+ const eventId = msg['client-event-id'];
984
+ const cb = this.startWriteStreamCbs[eventId];
985
+ if (cb) {
986
+ cb({
987
+ type: 'error',
988
+ error: new InstantError(msg.message || 'Unknown error', msg.hint),
989
+ });
990
+ delete this.startWriteStreamCbs[eventId];
991
+ }
992
+ break;
993
+ }
994
+ case 'subscribe-stream': {
995
+ const eventId = msg['client-event-id'];
996
+ const iterator = this.readStreamIterators[eventId];
997
+ if (iterator) {
998
+ iterator.push({
999
+ type: 'error',
1000
+ error: new InstantError(msg.message || 'Unknown error', msg.hint),
1001
+ });
1002
+ iterator.close();
1003
+ delete this.readStreamIterators[eventId];
1004
+ }
1005
+ break;
1006
+ }
1007
+ case 'unsubscribe-stream': {
1008
+ break;
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ hasActiveStreams() {
1014
+ return this.activeStreams.size > 0;
1015
+ }
1016
+ }