@loro-dev/streams-crdt 0.2.0 → 0.3.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/dist/flock.d.ts +1 -1
- package/dist/flock.js +4 -3
- package/dist/flock.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/loro.d.ts +1 -1
- package/dist/loro.js +1 -1
- package/dist/{stream-id-kM9JM1F7.js → stream-id-BS_IJr-Y.js} +174 -396
- package/dist/stream-id-BS_IJr-Y.js.map +1 -0
- package/dist/{stream-id-BxGVitUa.d.ts → stream-id-CSottEoY.d.ts} +13 -1
- package/package.json +4 -1
- package/dist/stream-id-kM9JM1F7.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StreamsClient } from "@loro-dev/streams-client";
|
|
1
2
|
//#region src/checkpoint.ts
|
|
2
3
|
function composeBeforeRemoteCursorSaveHooks(...hooks) {
|
|
3
4
|
const active = hooks.filter((hook) => hook != null);
|
|
@@ -181,7 +182,7 @@ const DEFAULT_RECONNECT_CONFIG = {
|
|
|
181
182
|
15e3
|
|
182
183
|
],
|
|
183
184
|
jitterFraction: .2,
|
|
184
|
-
maxAttempts:
|
|
185
|
+
maxAttempts: Infinity,
|
|
185
186
|
connectTimeoutMs: 1e4,
|
|
186
187
|
pollTimeoutMs: 3e4
|
|
187
188
|
};
|
|
@@ -234,79 +235,6 @@ function decodeItems(payload) {
|
|
|
234
235
|
return out;
|
|
235
236
|
}
|
|
236
237
|
//#endregion
|
|
237
|
-
//#region src/multipart.ts
|
|
238
|
-
/**
|
|
239
|
-
* Parse an RFC 2046 multipart/mixed body into its constituent parts.
|
|
240
|
-
*
|
|
241
|
-
* The boundary must NOT include the leading "--".
|
|
242
|
-
*/
|
|
243
|
-
function decodeMultipartMixed(boundary, data) {
|
|
244
|
-
const delimiter = new TextEncoder().encode(`--${boundary}`);
|
|
245
|
-
const closeDelimiter = new TextEncoder().encode(`--${boundary}--`);
|
|
246
|
-
const crlfCrlf = new TextEncoder().encode("\r\n\r\n");
|
|
247
|
-
const parts = [];
|
|
248
|
-
let pos = 0;
|
|
249
|
-
const firstDelimPos = indexOf(data, delimiter, pos);
|
|
250
|
-
if (firstDelimPos < 0) return parts;
|
|
251
|
-
pos = firstDelimPos + delimiter.byteLength;
|
|
252
|
-
if (pos < data.byteLength && data[pos] === 13) pos++;
|
|
253
|
-
if (pos < data.byteLength && data[pos] === 10) pos++;
|
|
254
|
-
while (pos < data.byteLength) {
|
|
255
|
-
if (startsWith(data.subarray(pos), closeDelimiter.subarray(delimiter.byteLength))) break;
|
|
256
|
-
const headerEnd = indexOf(data, crlfCrlf, pos);
|
|
257
|
-
if (headerEnd < 0) break;
|
|
258
|
-
const headerBytes = data.subarray(pos, headerEnd);
|
|
259
|
-
const headerText = new TextDecoder().decode(headerBytes);
|
|
260
|
-
let contentType = "application/octet-stream";
|
|
261
|
-
for (const line of headerText.split("\r\n")) {
|
|
262
|
-
const colonIdx = line.indexOf(":");
|
|
263
|
-
if (colonIdx > 0) {
|
|
264
|
-
if (line.slice(0, colonIdx).trim().toLowerCase() === "content-type") contentType = line.slice(colonIdx + 1).trim();
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
const bodyStart = headerEnd + crlfCrlf.byteLength;
|
|
268
|
-
const nextDelimPos = indexOf(data, delimiter, bodyStart);
|
|
269
|
-
if (nextDelimPos < 0) {
|
|
270
|
-
parts.push({
|
|
271
|
-
contentType,
|
|
272
|
-
body: data.subarray(bodyStart)
|
|
273
|
-
});
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
let bodyEnd = nextDelimPos;
|
|
277
|
-
if (bodyEnd >= 2 && data[bodyEnd - 2] === 13 && data[bodyEnd - 1] === 10) bodyEnd -= 2;
|
|
278
|
-
parts.push({
|
|
279
|
-
contentType,
|
|
280
|
-
body: data.subarray(bodyStart, bodyEnd)
|
|
281
|
-
});
|
|
282
|
-
pos = nextDelimPos + delimiter.byteLength;
|
|
283
|
-
if (pos < data.byteLength && data[pos] === 45 && pos + 1 < data.byteLength && data[pos + 1] === 45) break;
|
|
284
|
-
if (pos < data.byteLength && data[pos] === 13) pos++;
|
|
285
|
-
if (pos < data.byteLength && data[pos] === 10) pos++;
|
|
286
|
-
}
|
|
287
|
-
return parts;
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Extract the boundary parameter from a Content-Type header value.
|
|
291
|
-
* Returns null if not found.
|
|
292
|
-
*/
|
|
293
|
-
function extractBoundary(contentType) {
|
|
294
|
-
const match = /boundary=("?)([^";,\s]+)\1/i.exec(contentType);
|
|
295
|
-
return match ? match[2] : null;
|
|
296
|
-
}
|
|
297
|
-
function indexOf(haystack, needle, from) {
|
|
298
|
-
outer: for (let i = from; i <= haystack.byteLength - needle.byteLength; i++) {
|
|
299
|
-
for (let j = 0; j < needle.byteLength; j++) if (haystack[i + j] !== needle[j]) continue outer;
|
|
300
|
-
return i;
|
|
301
|
-
}
|
|
302
|
-
return -1;
|
|
303
|
-
}
|
|
304
|
-
function startsWith(data, prefix) {
|
|
305
|
-
if (data.byteLength < prefix.byteLength) return false;
|
|
306
|
-
for (let i = 0; i < prefix.byteLength; i++) if (data[i] !== prefix[i]) return false;
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
//#endregion
|
|
310
238
|
//#region src/transport-internal.ts
|
|
311
239
|
function createProducerId() {
|
|
312
240
|
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
@@ -335,30 +263,9 @@ var ProtocolError = class extends Error {
|
|
|
335
263
|
this.status = status;
|
|
336
264
|
}
|
|
337
265
|
};
|
|
338
|
-
function getHeader(headers, lowerCaseName, titleCaseName) {
|
|
339
|
-
return headers.get(lowerCaseName) ?? headers.get(titleCaseName);
|
|
340
|
-
}
|
|
341
|
-
function visibleHeaderNames(headers) {
|
|
342
|
-
const names = [];
|
|
343
|
-
headers.forEach((_value, name) => {
|
|
344
|
-
names.push(name);
|
|
345
|
-
});
|
|
346
|
-
if (names.length === 0) return "(none)";
|
|
347
|
-
names.sort();
|
|
348
|
-
return names.join(", ");
|
|
349
|
-
}
|
|
350
|
-
function missingRequiredHeaderError(context, requiredHeader, headers) {
|
|
351
|
-
return new ProtocolError(`${context} response missing ${requiredHeader}. If this is cross-origin, expose it via Access-Control-Expose-Headers (${requiredHeader.toLowerCase()}). Visible headers: ${visibleHeaderNames(headers)}`);
|
|
352
|
-
}
|
|
353
266
|
function isObject(value) {
|
|
354
267
|
return typeof value === "object" && value != null;
|
|
355
268
|
}
|
|
356
|
-
function parseHeaderU64(value) {
|
|
357
|
-
if (value == null) return null;
|
|
358
|
-
const parsed = Number.parseInt(value, 10);
|
|
359
|
-
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
|
360
|
-
return parsed;
|
|
361
|
-
}
|
|
362
269
|
function decodeBase64Bytes(encoded) {
|
|
363
270
|
const normalized = encoded.replace(/[\r\n]/g, "");
|
|
364
271
|
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(normalized, "base64"));
|
|
@@ -425,34 +332,15 @@ function isSseUnsupportedErrorText(message) {
|
|
|
425
332
|
const mentionsCompatibility = normalized.includes("unsupported") || normalized.includes("not support") || normalized.includes("incompatible") || normalized.includes("content type");
|
|
426
333
|
return mentionsSse && mentionsCompatibility;
|
|
427
334
|
}
|
|
428
|
-
function
|
|
429
|
-
if (
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
436
|
-
}
|
|
437
|
-
function sleep(ms) {
|
|
438
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
439
|
-
}
|
|
440
|
-
function mergeAbortSignals(...signals) {
|
|
441
|
-
const active = signals.filter((signal) => signal != null);
|
|
442
|
-
if (active.length === 0) return;
|
|
443
|
-
if (active.length === 1) return active[0];
|
|
444
|
-
if (typeof AbortSignal.any === "function") return AbortSignal.any(active);
|
|
445
|
-
const controller = new AbortController();
|
|
446
|
-
for (const signal of active) {
|
|
447
|
-
if (signal.aborted) {
|
|
448
|
-
controller.abort(signal.reason);
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
signal.addEventListener("abort", () => {
|
|
452
|
-
controller.abort(signal.reason);
|
|
335
|
+
function sleep(ms, signal) {
|
|
336
|
+
if (signal?.aborted) return Promise.resolve();
|
|
337
|
+
return new Promise((resolve) => {
|
|
338
|
+
const timer = setTimeout(resolve, ms);
|
|
339
|
+
signal?.addEventListener("abort", () => {
|
|
340
|
+
clearTimeout(timer);
|
|
341
|
+
resolve();
|
|
453
342
|
}, { once: true });
|
|
454
|
-
}
|
|
455
|
-
return controller.signal;
|
|
343
|
+
});
|
|
456
344
|
}
|
|
457
345
|
function classifyFetchError(err) {
|
|
458
346
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -651,209 +539,143 @@ function resolveNextOffsetAfterConcurrentAdvance(requestOffset, currentOffset, r
|
|
|
651
539
|
var StreamsTransportClient = class {
|
|
652
540
|
streamUrl;
|
|
653
541
|
adapter;
|
|
654
|
-
|
|
655
|
-
fetchImpl;
|
|
542
|
+
client;
|
|
656
543
|
reconnectConfig;
|
|
657
|
-
authRefreshInFlight;
|
|
658
544
|
constructor(options) {
|
|
659
545
|
this.streamUrl = options.streamUrl;
|
|
660
546
|
this.adapter = options.adapter;
|
|
661
|
-
this.
|
|
662
|
-
|
|
547
|
+
this.client = new StreamsClient({
|
|
548
|
+
url: options.streamUrl,
|
|
549
|
+
auth: options.authProvider,
|
|
550
|
+
fetch: options.fetchImpl,
|
|
551
|
+
retry: { maxAttempts: 0 },
|
|
552
|
+
timeout: {
|
|
553
|
+
connectTimeoutMs: options.reconnectConfig.connectTimeoutMs,
|
|
554
|
+
pollTimeoutMs: options.reconnectConfig.pollTimeoutMs
|
|
555
|
+
}
|
|
556
|
+
});
|
|
663
557
|
this.reconnectConfig = options.reconnectConfig;
|
|
664
558
|
}
|
|
665
559
|
async createStreamRequest() {
|
|
666
560
|
const adapterHeaders = this.adapter.getCreateStreamHeaders ? await this.adapter.getCreateStreamHeaders() : void 0;
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
"Content-Type": BINARY_CONTENT_TYPE,
|
|
671
|
-
...adapterHeaders
|
|
672
|
-
}
|
|
673
|
-
}, this.reconnectConfig.connectTimeoutMs, "connect");
|
|
674
|
-
if (response.status === 200 || response.status === 201) return response.status === 201;
|
|
675
|
-
throw new HttpError(`create stream failed with status ${response.status}`, response.status);
|
|
561
|
+
const result = await this.client.create({ headers: adapterHeaders });
|
|
562
|
+
if (result.ok) return result.result.created;
|
|
563
|
+
throw this.toTransportFailure(result.result);
|
|
676
564
|
}
|
|
677
565
|
async deleteStreamRequest() {
|
|
678
|
-
const
|
|
679
|
-
if (
|
|
680
|
-
|
|
681
|
-
throw new HttpError(`delete stream failed with status ${response.status}`, response.status);
|
|
566
|
+
const result = await this.client.delete();
|
|
567
|
+
if (result.ok) return result.result.deleted;
|
|
568
|
+
throw this.toTransportFailure(result.result);
|
|
682
569
|
}
|
|
683
570
|
async readFromOffsetSseItems(offset, cursor, onControl, onOpen, signal) {
|
|
684
|
-
|
|
685
|
-
offset,
|
|
686
|
-
live: "sse"
|
|
687
|
-
});
|
|
688
|
-
return this.readItemsSse(withCursorQuery(`${this.streamUrl}?${query.toString()}`, cursor), offset, onControl, onOpen, signal);
|
|
571
|
+
return this.readItemsSse(offset, cursor, onControl, onOpen, signal);
|
|
689
572
|
}
|
|
690
573
|
async readFromOffset(offset, cursor, live, signal) {
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const timeoutType = live === "long-poll" ? "poll" : "connect";
|
|
696
|
-
const response = await this.fetchAuthorized(`${this.streamUrl}?${query.toString()}`, {
|
|
697
|
-
method: "GET",
|
|
574
|
+
const result = await this.client.readOnce({
|
|
575
|
+
offset,
|
|
576
|
+
cursor,
|
|
577
|
+
live,
|
|
698
578
|
signal
|
|
699
|
-
}
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
const streamCursor = getHeader(response.headers, "stream-cursor", "Stream-Cursor") ?? void 0;
|
|
706
|
-
const body = new Uint8Array(await response.arrayBuffer());
|
|
579
|
+
});
|
|
580
|
+
if (!result.ok) {
|
|
581
|
+
if (result.result.code === "gone") return { kind: "gone" };
|
|
582
|
+
throw this.toTransportFailure(result.result);
|
|
583
|
+
}
|
|
584
|
+
const payload = result.result.payload?.body ?? new Uint8Array();
|
|
707
585
|
return {
|
|
708
586
|
kind: "ok",
|
|
709
587
|
value: {
|
|
710
|
-
nextOffset,
|
|
711
|
-
upToDate,
|
|
712
|
-
updates:
|
|
713
|
-
streamCursor
|
|
588
|
+
nextOffset: result.result.nextOffset,
|
|
589
|
+
upToDate: result.result.upToDate,
|
|
590
|
+
updates: payload.byteLength === 0 ? [] : decodeItems(payload),
|
|
591
|
+
streamCursor: result.result.cursor
|
|
714
592
|
}
|
|
715
593
|
};
|
|
716
594
|
}
|
|
717
595
|
async bootstrap() {
|
|
718
|
-
const
|
|
719
|
-
if (!
|
|
720
|
-
const nextOffset = getHeader(response.headers, "stream-next-offset", "Stream-Next-Offset");
|
|
721
|
-
if (nextOffset == null) throw missingRequiredHeaderError("bootstrap", "Stream-Next-Offset", response.headers);
|
|
722
|
-
const upToDate = getHeader(response.headers, "stream-up-to-date", "Stream-Up-To-Date") === "true";
|
|
723
|
-
const streamCursor = getHeader(response.headers, "stream-cursor", "Stream-Cursor") ?? void 0;
|
|
724
|
-
const body = new Uint8Array(await response.arrayBuffer());
|
|
725
|
-
const boundary = extractBoundary(response.headers.get("Content-Type") ?? "");
|
|
726
|
-
if (boundary != null && body.byteLength > 0) {
|
|
727
|
-
const parts = decodeMultipartMixed(boundary, body);
|
|
728
|
-
return {
|
|
729
|
-
nextOffset,
|
|
730
|
-
upToDate,
|
|
731
|
-
snapshot: parts.length > 0 ? parts[0].body : new Uint8Array(),
|
|
732
|
-
updates: parts.slice(1).flatMap((part) => decodeMultipartPartItems(part.body)),
|
|
733
|
-
streamCursor
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
if (body.byteLength > 0) throw new ProtocolError("bootstrap returned a non-multipart body");
|
|
596
|
+
const result = await this.client.bootstrap();
|
|
597
|
+
if (!result.ok) throw this.toTransportFailure(result.result);
|
|
737
598
|
return {
|
|
738
|
-
nextOffset,
|
|
739
|
-
upToDate,
|
|
740
|
-
snapshot: new Uint8Array(),
|
|
741
|
-
updates:
|
|
742
|
-
streamCursor
|
|
599
|
+
nextOffset: result.result.nextOffset,
|
|
600
|
+
upToDate: result.result.upToDate,
|
|
601
|
+
snapshot: result.result.snapshot?.body ?? new Uint8Array(),
|
|
602
|
+
updates: result.result.updates.flatMap((part) => decodeMultipartPartItems(part.body)),
|
|
603
|
+
streamCursor: result.result.cursor
|
|
743
604
|
};
|
|
744
605
|
}
|
|
745
606
|
async readSnapshotOffsetValue() {
|
|
746
|
-
const
|
|
747
|
-
if (
|
|
748
|
-
|
|
749
|
-
|
|
607
|
+
const result = await this.client.head();
|
|
608
|
+
if (!result.ok) {
|
|
609
|
+
if (result.result.code === "not_found") return null;
|
|
610
|
+
throw this.toTransportFailure(result.result);
|
|
611
|
+
}
|
|
612
|
+
const snapshotOffset = result.result.snapshotOffset;
|
|
750
613
|
return snapshotOffset == null || snapshotOffset.length === 0 ? null : snapshotOffset;
|
|
751
614
|
}
|
|
752
615
|
async putSnapshotAt(snapshotOffset, snapshot) {
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
616
|
+
const result = await this.client.putSnapshot({
|
|
617
|
+
offset: snapshotOffset,
|
|
618
|
+
part: {
|
|
619
|
+
contentType: "application/octet-stream",
|
|
620
|
+
body: snapshot
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
if (!result.ok) throw this.toTransportFailure(result.result);
|
|
760
624
|
}
|
|
761
625
|
async appendUpdates(updates, producerSession) {
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
"Producer-Id": producerSession.producerId,
|
|
767
|
-
"Producer-Epoch": producerSession.producerEpoch.toString(),
|
|
768
|
-
"Producer-Seq": producerSession.nextProducerSeq.toString()
|
|
626
|
+
const result = await this.client.append({
|
|
627
|
+
part: {
|
|
628
|
+
contentType: BINARY_CONTENT_TYPE,
|
|
629
|
+
body: encodeItems(updates)
|
|
769
630
|
},
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
return {
|
|
776
|
-
kind: "ok",
|
|
777
|
-
ackEpoch: parseHeaderU64(getHeader(response.headers, "producer-epoch", "Producer-Epoch")) ?? producerSession.producerEpoch,
|
|
778
|
-
ackSeq: parseHeaderU64(getHeader(response.headers, "producer-seq", "Producer-Seq")) ?? producerSession.nextProducerSeq,
|
|
779
|
-
nextOffset
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
if (response.status === 403) {
|
|
783
|
-
const serverEpoch = parseHeaderU64(getHeader(response.headers, "producer-epoch", "Producer-Epoch"));
|
|
784
|
-
if (serverEpoch != null) return {
|
|
785
|
-
kind: "stale-epoch",
|
|
786
|
-
serverEpoch
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
if (response.status === 409) {
|
|
790
|
-
const expectedSeq = parseHeaderU64(getHeader(response.headers, "producer-expected-seq", "Producer-Expected-Seq"));
|
|
791
|
-
if (expectedSeq != null) return {
|
|
792
|
-
kind: "seq-gap",
|
|
793
|
-
expectedSeq
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
throw new HttpError(`append failed with status ${response.status}`, response.status);
|
|
797
|
-
}
|
|
798
|
-
async resolveToken(context) {
|
|
799
|
-
if (this.authProvider == null) return;
|
|
800
|
-
return await this.authProvider(context);
|
|
801
|
-
}
|
|
802
|
-
async refreshTokenAfterAuthFailure(status, previousToken) {
|
|
803
|
-
if (this.authProvider == null) return;
|
|
804
|
-
if (this.authRefreshInFlight == null) this.authRefreshInFlight = this.resolveToken({
|
|
805
|
-
reason: "unauthorized",
|
|
806
|
-
status,
|
|
807
|
-
previousToken
|
|
808
|
-
}).finally(() => {
|
|
809
|
-
this.authRefreshInFlight = void 0;
|
|
631
|
+
producer: {
|
|
632
|
+
producerId: producerSession.producerId,
|
|
633
|
+
epoch: producerSession.producerEpoch,
|
|
634
|
+
seq: producerSession.nextProducerSeq
|
|
635
|
+
}
|
|
810
636
|
});
|
|
811
|
-
return
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
if (controller.signal.aborted && controller.signal.reason instanceof TimeoutError) throw controller.signal.reason;
|
|
850
|
-
throw error;
|
|
851
|
-
} finally {
|
|
852
|
-
clearTimeout(timer);
|
|
637
|
+
if (result.ok) return {
|
|
638
|
+
kind: "ok",
|
|
639
|
+
ackEpoch: result.result.producer?.epoch ?? producerSession.producerEpoch,
|
|
640
|
+
ackSeq: result.result.producer?.seq ?? producerSession.nextProducerSeq,
|
|
641
|
+
nextOffset: result.result.nextOffset
|
|
642
|
+
};
|
|
643
|
+
const error = result.result;
|
|
644
|
+
if (error.code === "conflict" && error.detail?.kind === "producer_seq_gap") return {
|
|
645
|
+
kind: "seq-gap",
|
|
646
|
+
expectedSeq: error.detail.expectedSeq
|
|
647
|
+
};
|
|
648
|
+
if (error.code === "forbidden" && error.detail?.kind === "stale_producer_epoch") return {
|
|
649
|
+
kind: "stale-epoch",
|
|
650
|
+
serverEpoch: error.detail.serverEpoch
|
|
651
|
+
};
|
|
652
|
+
throw this.toTransportFailure(error);
|
|
653
|
+
}
|
|
654
|
+
toTransportFailure(error) {
|
|
655
|
+
switch (error.code) {
|
|
656
|
+
case "bad_request":
|
|
657
|
+
case "unauthorized":
|
|
658
|
+
case "forbidden":
|
|
659
|
+
case "not_found":
|
|
660
|
+
case "conflict":
|
|
661
|
+
case "gone":
|
|
662
|
+
case "rate_limited": return new HttpError(error.message, error.status);
|
|
663
|
+
case "timeout": return new TimeoutError(error.phase, error.phase === "poll" ? this.reconnectConfig.pollTimeoutMs : this.reconnectConfig.connectTimeoutMs);
|
|
664
|
+
case "network_error": return error.cause instanceof TypeError ? error.cause : new TypeError(error.message);
|
|
665
|
+
case "protocol_error": return new ProtocolError(error.message, error.status);
|
|
666
|
+
case "unknown":
|
|
667
|
+
if (typeof error.status === "number") return new HttpError(error.message, error.status);
|
|
668
|
+
if (error.cause instanceof TypeError) return error.cause;
|
|
669
|
+
if (error.cause instanceof Error) return error.cause;
|
|
670
|
+
return new Error(error.message);
|
|
671
|
+
default: {
|
|
672
|
+
const exhaustive = error;
|
|
673
|
+
return new Error(String(exhaustive));
|
|
674
|
+
}
|
|
853
675
|
}
|
|
854
676
|
}
|
|
855
|
-
async readItemsSse(
|
|
856
|
-
const opened = await this.openItemsSseSession(
|
|
677
|
+
async readItemsSse(initialOffset, cursor, onControl, onOpen, signal) {
|
|
678
|
+
const opened = await this.openItemsSseSession(initialOffset, cursor, onOpen, signal);
|
|
857
679
|
if (opened.kind !== "ok") return opened;
|
|
858
680
|
const result = await opened.session.continue(onControl);
|
|
859
681
|
if (result.paused) await opened.session.cancel();
|
|
@@ -863,41 +685,23 @@ var StreamsTransportClient = class {
|
|
|
863
685
|
latestCursor: result.latestCursor
|
|
864
686
|
};
|
|
865
687
|
}
|
|
866
|
-
async openItemsSseSession(
|
|
867
|
-
const
|
|
868
|
-
|
|
688
|
+
async openItemsSseSession(initialOffset, cursor, onOpen, signal) {
|
|
689
|
+
const result = await this.client.openSseSession({
|
|
690
|
+
offset: initialOffset,
|
|
691
|
+
cursor,
|
|
869
692
|
signal
|
|
870
|
-
}
|
|
871
|
-
if (
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
throw new HttpError(errorText.length > 0 ? `sse read failed with status 400: ${errorText}` : "sse read failed with status 400", response.status);
|
|
693
|
+
});
|
|
694
|
+
if (!result.ok) {
|
|
695
|
+
if (result.result.code === "gone") return { kind: "gone" };
|
|
696
|
+
if (result.result.code === "not_found") return { kind: "not-found" };
|
|
697
|
+
if (result.result.code === "bad_request" && isSseUnsupportedErrorText(result.result.message)) return { kind: "unsupported" };
|
|
698
|
+
throw this.toTransportFailure(result.result);
|
|
877
699
|
}
|
|
878
|
-
|
|
879
|
-
if (!(response.headers.get("Content-Type") ?? "").toLowerCase().includes("text/event-stream")) throw new ProtocolError("sse read response must use text/event-stream");
|
|
880
|
-
const sseDataEncoding = response.headers.get("stream-sse-data-encoding");
|
|
700
|
+
const session = result.result;
|
|
881
701
|
onOpen?.();
|
|
882
|
-
if (response.body == null) return {
|
|
883
|
-
kind: "ok",
|
|
884
|
-
session: {
|
|
885
|
-
continue: async () => ({
|
|
886
|
-
kind: "ok",
|
|
887
|
-
latestOffset: initialOffset,
|
|
888
|
-
paused: false
|
|
889
|
-
}),
|
|
890
|
-
cancel: async () => {}
|
|
891
|
-
}
|
|
892
|
-
};
|
|
893
|
-
const reader = response.body.getReader();
|
|
894
|
-
const decoder = new TextDecoder();
|
|
895
|
-
let pendingText = "";
|
|
896
|
-
let currentEvent = "";
|
|
897
|
-
let dataLines = [];
|
|
898
702
|
let pendingDataEvents = [];
|
|
899
|
-
let latestOffset = initialOffset;
|
|
900
|
-
let latestCursor;
|
|
703
|
+
let latestOffset = session.nextOffset ?? initialOffset;
|
|
704
|
+
let latestCursor = session.cursor;
|
|
901
705
|
let exhausted = false;
|
|
902
706
|
return {
|
|
903
707
|
kind: "ok",
|
|
@@ -909,96 +713,48 @@ var StreamsTransportClient = class {
|
|
|
909
713
|
latestCursor,
|
|
910
714
|
paused: false
|
|
911
715
|
};
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
currentEvent = "";
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
const eventType = currentEvent.length === 0 ? "message" : currentEvent;
|
|
920
|
-
const data = dataLines.join("\n");
|
|
921
|
-
currentEvent = "";
|
|
922
|
-
dataLines = [];
|
|
923
|
-
if (eventType === "data") {
|
|
924
|
-
pendingDataEvents.push(data);
|
|
925
|
-
return;
|
|
716
|
+
for await (const event of session.events) {
|
|
717
|
+
if (event.event === "data") {
|
|
718
|
+
pendingDataEvents.push(event.data);
|
|
719
|
+
continue;
|
|
926
720
|
}
|
|
927
|
-
if (
|
|
928
|
-
const control = parseSseControlData(data);
|
|
721
|
+
if (event.event !== "control") continue;
|
|
722
|
+
const control = parseSseControlData(event.data);
|
|
929
723
|
const updates = [];
|
|
930
|
-
for (const eventData of pendingDataEvents)
|
|
724
|
+
for (const eventData of pendingDataEvents) updates.push(...decodeSseItemPayloads(eventData, session.dataEncoding));
|
|
931
725
|
pendingDataEvents = [];
|
|
932
726
|
const handlerResult = await nextOnControl(updates, control.nextOffset, control.upToDate, control.streamCursor);
|
|
933
727
|
latestOffset = control.nextOffset;
|
|
934
728
|
if (control.streamCursor != null) latestCursor = control.streamCursor;
|
|
935
729
|
if (handlerResult === "stop") {
|
|
936
|
-
stopRequested = true;
|
|
937
730
|
exhausted = true;
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
731
|
+
await session.cancel();
|
|
732
|
+
return {
|
|
733
|
+
kind: "ok",
|
|
734
|
+
latestOffset,
|
|
735
|
+
latestCursor,
|
|
736
|
+
paused: false
|
|
737
|
+
};
|
|
942
738
|
}
|
|
943
|
-
if (handlerResult === "pause")
|
|
944
|
-
};
|
|
945
|
-
while (true) {
|
|
946
|
-
if (stopRequested || pauseRequested) return {
|
|
739
|
+
if (handlerResult === "pause") return {
|
|
947
740
|
kind: "ok",
|
|
948
741
|
latestOffset,
|
|
949
742
|
latestCursor,
|
|
950
|
-
paused:
|
|
951
|
-
};
|
|
952
|
-
const { done, value } = await reader.read();
|
|
953
|
-
if (value != null) {
|
|
954
|
-
pendingText += decoder.decode(value, { stream: true });
|
|
955
|
-
while (true) {
|
|
956
|
-
const newline = pendingText.indexOf("\n");
|
|
957
|
-
if (newline < 0) break;
|
|
958
|
-
const rawLine = pendingText.slice(0, newline);
|
|
959
|
-
pendingText = pendingText.slice(newline + 1);
|
|
960
|
-
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
961
|
-
if (line.length === 0) {
|
|
962
|
-
await processEvent();
|
|
963
|
-
if (stopRequested || pauseRequested) break;
|
|
964
|
-
continue;
|
|
965
|
-
}
|
|
966
|
-
if (line.startsWith(":")) continue;
|
|
967
|
-
if (line.startsWith("event:")) {
|
|
968
|
-
currentEvent = line.slice(6).trim();
|
|
969
|
-
continue;
|
|
970
|
-
}
|
|
971
|
-
if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
if (stopRequested || pauseRequested) return {
|
|
975
|
-
kind: "ok",
|
|
976
|
-
latestOffset,
|
|
977
|
-
latestCursor,
|
|
978
|
-
paused: pauseRequested
|
|
979
|
-
};
|
|
980
|
-
if (!done) continue;
|
|
981
|
-
exhausted = true;
|
|
982
|
-
pendingText += decoder.decode();
|
|
983
|
-
if (pendingText.length > 0) {
|
|
984
|
-
const line = pendingText.endsWith("\r") ? pendingText.slice(0, -1) : pendingText;
|
|
985
|
-
if (line.length > 0 && line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
986
|
-
}
|
|
987
|
-
await processEvent();
|
|
988
|
-
return {
|
|
989
|
-
kind: "ok",
|
|
990
|
-
latestOffset,
|
|
991
|
-
latestCursor,
|
|
992
|
-
paused: pauseRequested
|
|
743
|
+
paused: true
|
|
993
744
|
};
|
|
994
745
|
}
|
|
746
|
+
exhausted = true;
|
|
747
|
+
return {
|
|
748
|
+
kind: "ok",
|
|
749
|
+
latestOffset,
|
|
750
|
+
latestCursor,
|
|
751
|
+
paused: false
|
|
752
|
+
};
|
|
995
753
|
},
|
|
996
754
|
cancel: async () => {
|
|
997
755
|
if (exhausted) return;
|
|
998
756
|
exhausted = true;
|
|
999
|
-
|
|
1000
|
-
await reader.cancel();
|
|
1001
|
-
} catch {}
|
|
757
|
+
await session.cancel();
|
|
1002
758
|
}
|
|
1003
759
|
}
|
|
1004
760
|
};
|
|
@@ -1319,6 +1075,7 @@ function createJoinState(cursor, liveMode = "sse") {
|
|
|
1319
1075
|
pendingLocal: new Deque(),
|
|
1320
1076
|
flushingLocal: false,
|
|
1321
1077
|
retryAttempt: 0,
|
|
1078
|
+
retrySleepController: new AbortController(),
|
|
1322
1079
|
cursor,
|
|
1323
1080
|
liveMode,
|
|
1324
1081
|
snapshot: createTransportSnapshotState(),
|
|
@@ -1407,6 +1164,27 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1407
1164
|
if (state != null) await this.disposeJoinState(state);
|
|
1408
1165
|
}
|
|
1409
1166
|
/**
|
|
1167
|
+
* Resets the retry counter and immediately attempts to reconnect.
|
|
1168
|
+
*
|
|
1169
|
+
* Use this after device wake or network recovery to skip any pending
|
|
1170
|
+
* backoff delay. No-op when the room is already joined or no join is active.
|
|
1171
|
+
*
|
|
1172
|
+
* If the room reached `"disconnected"` status (only possible with a finite
|
|
1173
|
+
* `maxAttempts` config), this restarts the read loop.
|
|
1174
|
+
*/
|
|
1175
|
+
rejoin() {
|
|
1176
|
+
const state = this.joinState;
|
|
1177
|
+
if (state == null || state.closed || state.status === "joined") return;
|
|
1178
|
+
const wasDisconnected = state.status === "disconnected";
|
|
1179
|
+
state.retryAttempt = 0;
|
|
1180
|
+
state.retrySleepController.abort();
|
|
1181
|
+
state.retrySleepController = new AbortController();
|
|
1182
|
+
if (wasDisconnected) {
|
|
1183
|
+
this.setJoinStatus(state, "reconnecting");
|
|
1184
|
+
this.runReadLoop(state);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1410
1188
|
* Reads the latest remote snapshot offset, if the server exposes one.
|
|
1411
1189
|
*/
|
|
1412
1190
|
async getSnapshotOffset() {
|
|
@@ -1827,7 +1605,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1827
1605
|
}
|
|
1828
1606
|
const delay = computeRetryDelay(state.retryAttempt, this.reconnectConfig);
|
|
1829
1607
|
state.retryAttempt += 1;
|
|
1830
|
-
await sleep(delay);
|
|
1608
|
+
await sleep(delay, state.retrySleepController.signal);
|
|
1831
1609
|
}
|
|
1832
1610
|
}
|
|
1833
1611
|
/** Maximum encoded size (bytes) for a single batched append. */
|
|
@@ -1899,7 +1677,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1899
1677
|
this.setJoinStatus(state, "reconnecting");
|
|
1900
1678
|
const delay = computeRetryDelay(attempts, this.reconnectConfig);
|
|
1901
1679
|
attempts += 1;
|
|
1902
|
-
await sleep(delay);
|
|
1680
|
+
await sleep(delay, state.retrySleepController.signal);
|
|
1903
1681
|
}
|
|
1904
1682
|
}
|
|
1905
1683
|
} finally {
|
|
@@ -2001,4 +1779,4 @@ function createStreamUrl(input) {
|
|
|
2001
1779
|
//#endregion
|
|
2002
1780
|
export { InMemoryRemoteCursorStore as a, StreamsCrdt as i, isValidBucketId as n, IndexedDbRemoteCursorStore as o, isValidRillId as r, createInitialRemoteCursor as s, createStreamUrl as t };
|
|
2003
1781
|
|
|
2004
|
-
//# sourceMappingURL=stream-id-
|
|
1782
|
+
//# sourceMappingURL=stream-id-BS_IJr-Y.js.map
|