@loro-dev/streams-crdt 0.7.1 → 0.8.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.
@@ -1,211 +1,4 @@
1
- import { StreamsClient } from "@loro-dev/streams-client";
2
- //#region src/checkpoint.ts
3
- function composeBeforeRemoteCursorSaveHooks(...hooks) {
4
- const active = hooks.filter((hook) => hook != null);
5
- if (active.length === 0) return;
6
- return async (context) => {
7
- for (const hook of active) await hook(context);
8
- };
9
- }
10
- async function persistRemoteCursor(input) {
11
- const next = {
12
- ...input.cursor,
13
- updatedAtMs: (input.now ?? Date.now)()
14
- };
15
- if (input.beforeRemoteCursorSave != null) await input.beforeRemoteCursorSave({
16
- cursor: next,
17
- source: input.source
18
- });
19
- await input.remoteCursorStore.save(next);
20
- return next;
21
- }
22
- function key(streamUrl) {
23
- return streamUrl;
24
- }
25
- function isJsonObject(value) {
26
- return typeof value === "object" && value != null && !Array.isArray(value);
27
- }
28
- function cloneJsonValue(value) {
29
- return structuredClone(value);
30
- }
31
- function cloneRemoteCursor(cursor) {
32
- return {
33
- ...cursor,
34
- serverLowerBoundVersion: cloneJsonValue(cursor.serverLowerBoundVersion)
35
- };
36
- }
37
- function fromStoredRemoteCursor(record) {
38
- if (!isJsonObject(record.serverLowerBoundVersion)) throw new Error("invalid remote cursor record: serverLowerBoundVersion is not a JSON object");
39
- return {
40
- streamUrl: record.streamUrl,
41
- nextOffset: record.nextOffset,
42
- serverLowerBoundVersion: cloneJsonValue(record.serverLowerBoundVersion),
43
- updatedAtMs: record.updatedAtMs
44
- };
45
- }
46
- function toStoredRemoteCursor(cursor) {
47
- return {
48
- streamUrl: cursor.streamUrl,
49
- nextOffset: cursor.nextOffset,
50
- serverLowerBoundVersion: cloneJsonValue(cursor.serverLowerBoundVersion),
51
- updatedAtMs: cursor.updatedAtMs
52
- };
53
- }
54
- /**
55
- * In-memory cursor store useful for tests or ephemeral runtimes.
56
- */
57
- var InMemoryRemoteCursorStore = class {
58
- data = /* @__PURE__ */ new Map();
59
- async load(streamUrl) {
60
- const value = this.data.get(key(streamUrl));
61
- return value == null ? null : cloneRemoteCursor(value);
62
- }
63
- async save(cursor) {
64
- this.data.set(key(cursor.streamUrl), cloneRemoteCursor(cursor));
65
- }
66
- async delete(streamUrl) {
67
- this.data.delete(key(streamUrl));
68
- }
69
- };
70
- const DEFAULT_DB_NAME = "streams-crdt";
71
- const DEFAULT_STORE_NAME = "remote-cursors";
72
- const DEFAULT_DB_VERSION = 1;
73
- /**
74
- * IndexedDB-backed cursor store for browser clients that need replay progress persistence.
75
- *
76
- * This store only persists `RemoteCursor` metadata. It does not persist CRDT
77
- * payloads. Use it only when the application also persists the corresponding
78
- * local document state and restores that state before calling `sync()` or
79
- * `join()` after a reload.
80
- */
81
- var IndexedDbRemoteCursorStore = class {
82
- dbName;
83
- storeName;
84
- dbVersion;
85
- indexedDb;
86
- db = null;
87
- initPromise = null;
88
- constructor(options) {
89
- this.dbName = options?.dbName ?? DEFAULT_DB_NAME;
90
- this.storeName = options?.storeName ?? DEFAULT_STORE_NAME;
91
- this.dbVersion = options?.dbVersion ?? DEFAULT_DB_VERSION;
92
- this.indexedDb = options?.indexedDb ?? globalThis.indexedDB;
93
- }
94
- async load(streamUrl) {
95
- const db = await this.ensureDb();
96
- const record = await new Promise((resolve, reject) => {
97
- const request = db.transaction([this.storeName], "readonly").objectStore(this.storeName).get(key(streamUrl));
98
- request.onsuccess = () => {
99
- resolve(request.result ?? null);
100
- };
101
- request.onerror = () => {
102
- reject(request.error ?? /* @__PURE__ */ new Error("indexeddb load failed"));
103
- };
104
- });
105
- return record == null ? null : fromStoredRemoteCursor(record);
106
- }
107
- async save(cursor) {
108
- const db = await this.ensureDb();
109
- const record = toStoredRemoteCursor(cursor);
110
- await new Promise((resolve, reject) => {
111
- const tx = db.transaction([this.storeName], "readwrite");
112
- const request = tx.objectStore(this.storeName).put(record, key(cursor.streamUrl));
113
- tx.oncomplete = () => resolve();
114
- tx.onerror = () => reject(tx.error ?? request.error ?? /* @__PURE__ */ new Error("indexeddb save failed"));
115
- tx.onabort = () => reject(tx.error ?? request.error ?? /* @__PURE__ */ new Error("indexeddb save transaction aborted"));
116
- request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("indexeddb save failed"));
117
- });
118
- }
119
- async delete(streamUrl) {
120
- const db = await this.ensureDb();
121
- await new Promise((resolve, reject) => {
122
- const tx = db.transaction([this.storeName], "readwrite");
123
- const request = tx.objectStore(this.storeName).delete(key(streamUrl));
124
- tx.oncomplete = () => resolve();
125
- tx.onerror = () => reject(tx.error ?? request.error ?? /* @__PURE__ */ new Error("indexeddb delete failed"));
126
- tx.onabort = () => reject(tx.error ?? request.error ?? /* @__PURE__ */ new Error("indexeddb delete transaction aborted"));
127
- request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("indexeddb delete failed"));
128
- });
129
- }
130
- async ensureDb() {
131
- if (this.db != null) return this.db;
132
- if (this.initPromise != null) {
133
- await this.initPromise;
134
- if (this.db == null) throw new Error("indexeddb is not available");
135
- return this.db;
136
- }
137
- if (this.indexedDb == null) throw new Error("indexeddb is not available in this environment");
138
- this.initPromise = new Promise((resolve, reject) => {
139
- const request = this.indexedDb.open(this.dbName, this.dbVersion);
140
- request.onupgradeneeded = () => {
141
- const db = request.result;
142
- if (!db.objectStoreNames.contains(this.storeName)) db.createObjectStore(this.storeName);
143
- };
144
- request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("indexeddb open failed"));
145
- request.onsuccess = () => {
146
- this.db = request.result;
147
- this.db.onversionchange = () => {
148
- this.db?.close();
149
- this.db = null;
150
- };
151
- resolve();
152
- };
153
- }).finally(() => {
154
- this.initPromise = null;
155
- });
156
- await this.initPromise;
157
- if (this.db == null) throw new Error("indexeddb open succeeded without a database handle");
158
- return this.db;
159
- }
160
- };
161
- /**
162
- * Creates a cursor value suitable for seeding a `RemoteCursorStore`.
163
- */
164
- function createInitialRemoteCursor(input) {
165
- return {
166
- streamUrl: input.streamUrl,
167
- nextOffset: input.nextOffset,
168
- serverLowerBoundVersion: cloneJsonValue(input.serverLowerBoundVersion),
169
- updatedAtMs: Date.now()
170
- };
171
- }
172
- //#endregion
173
- //#region src/reconnect.ts
174
- const DEFAULT_RECONNECT_CONFIG = {
175
- delays: [
176
- 0,
177
- 500,
178
- 1e3,
179
- 2e3,
180
- 4e3,
181
- 8e3,
182
- 15e3
183
- ],
184
- jitterFraction: .2,
185
- maxAttempts: Infinity,
186
- connectTimeoutMs: 1e4,
187
- pollTimeoutMs: 3e4,
188
- liveInactivityTimeoutMs: 45e3
189
- };
190
- /**
191
- * Apply jitter of +/- fraction to a base delay.
192
- * E.g. applyJitter(1000, 0.2) returns a value in [800, 1200].
193
- */
194
- function applyJitter(baseMs, fraction) {
195
- if (baseMs === 0) return 0;
196
- const jitter = baseMs * fraction * (2 * Math.random() - 1);
197
- return Math.max(0, Math.round(baseMs + jitter));
198
- }
199
- /**
200
- * Compute the retry delay for a given attempt number.
201
- * Uses the delays table from config, clamping at the last entry.
202
- */
203
- function computeRetryDelay(attempt, config = DEFAULT_RECONNECT_CONFIG) {
204
- const { delays, jitterFraction } = config;
205
- const base = delays[Math.min(attempt, delays.length - 1)];
206
- return applyJitter(base, jitterFraction);
207
- }
208
- //#endregion
1
+ import { StreamsClient, decodeSsePayload, readSseEvents } from "@loro-dev/streams-client";
209
2
  //#region src/deque.ts
210
3
  const DEFAULT_COMPACT_THRESHOLD = 64;
211
4
  const DEFAULT_MIN_CAPACITY = 16;
@@ -327,6 +120,42 @@ var Deque = class {
327
120
  }
328
121
  };
329
122
  //#endregion
123
+ //#region src/reconnect.ts
124
+ const DEFAULT_RECONNECT_CONFIG = {
125
+ delays: [
126
+ 0,
127
+ 500,
128
+ 1e3,
129
+ 2e3,
130
+ 4e3,
131
+ 8e3,
132
+ 15e3
133
+ ],
134
+ jitterFraction: .2,
135
+ maxAttempts: Infinity,
136
+ connectTimeoutMs: 1e4,
137
+ pollTimeoutMs: 3e4,
138
+ liveInactivityTimeoutMs: 45e3
139
+ };
140
+ /**
141
+ * Apply jitter of +/- fraction to a base delay.
142
+ * E.g. applyJitter(1000, 0.2) returns a value in [800, 1200].
143
+ */
144
+ function applyJitter(baseMs, fraction) {
145
+ if (baseMs === 0) return 0;
146
+ const jitter = baseMs * fraction * (2 * Math.random() - 1);
147
+ return Math.max(0, Math.round(baseMs + jitter));
148
+ }
149
+ /**
150
+ * Compute the retry delay for a given attempt number.
151
+ * Uses the delays table from config, clamping at the last entry.
152
+ */
153
+ function computeRetryDelay(attempt, config = DEFAULT_RECONNECT_CONFIG) {
154
+ const { delays, jitterFraction } = config;
155
+ const base = delays[Math.min(attempt, delays.length - 1)];
156
+ return applyJitter(base, jitterFraction);
157
+ }
158
+ //#endregion
330
159
  //#region src/items.ts
331
160
  const BINARY_CONTENT_TYPE = "application/octet-stream";
332
161
  function encodeItems(items) {
@@ -748,6 +577,23 @@ function sleep(ms, signal) {
748
577
  }, { once: true });
749
578
  });
750
579
  }
580
+ function mergeAbortSignals(...signals) {
581
+ const active = signals.filter((signal) => signal != null);
582
+ if (active.length === 0) return;
583
+ if (active.length === 1) return active[0];
584
+ if (typeof AbortSignal.any === "function") return AbortSignal.any(active);
585
+ const controller = new AbortController();
586
+ for (const signal of active) {
587
+ if (signal.aborted) {
588
+ controller.abort(signal.reason);
589
+ break;
590
+ }
591
+ signal.addEventListener("abort", () => {
592
+ controller.abort(signal.reason);
593
+ }, { once: true });
594
+ }
595
+ return controller.signal;
596
+ }
751
597
  function classifyFetchError(err) {
752
598
  const message = err instanceof Error ? err.message : String(err);
753
599
  const status = err instanceof HttpError ? err.status : void 0;
@@ -959,7 +805,10 @@ var StreamsTransportClient = class {
959
805
  streamUrl;
960
806
  adapter;
961
807
  client;
808
+ fetchImpl;
809
+ authProvider;
962
810
  reconnectConfig;
811
+ authRefreshInFlight;
963
812
  constructor(options) {
964
813
  this.streamUrl = options.streamUrl;
965
814
  this.adapter = options.adapter;
@@ -973,6 +822,8 @@ var StreamsTransportClient = class {
973
822
  pollTimeoutMs: options.reconnectConfig.pollTimeoutMs
974
823
  }
975
824
  });
825
+ this.fetchImpl = options.fetchImpl;
826
+ this.authProvider = options.authProvider;
976
827
  this.reconnectConfig = options.reconnectConfig;
977
828
  }
978
829
  async createStreamRequest() {
@@ -1031,171 +882,859 @@ var StreamsTransportClient = class {
1031
882
  const snapshotOffset = result.result.snapshotOffset;
1032
883
  return snapshotOffset == null || snapshotOffset.length === 0 ? null : snapshotOffset;
1033
884
  }
1034
- async putSnapshotAt(snapshotOffset, snapshot) {
1035
- const result = await this.client.putSnapshot({
1036
- offset: snapshotOffset,
1037
- part: {
1038
- contentType: "application/octet-stream",
1039
- body: snapshot
1040
- }
885
+ async putSnapshotAt(snapshotOffset, snapshot) {
886
+ const result = await this.client.putSnapshot({
887
+ offset: snapshotOffset,
888
+ part: {
889
+ contentType: "application/octet-stream",
890
+ body: snapshot
891
+ }
892
+ });
893
+ if (!result.ok) throw this.toTransportFailure(result.result);
894
+ }
895
+ async appendUpdates(updates, producerSession) {
896
+ const result = await this.client.append({
897
+ part: {
898
+ contentType: BINARY_CONTENT_TYPE,
899
+ body: encodeItems(updates)
900
+ },
901
+ producer: {
902
+ producerId: producerSession.producerId,
903
+ epoch: producerSession.producerEpoch,
904
+ seq: producerSession.nextProducerSeq
905
+ }
906
+ });
907
+ if (result.ok) return {
908
+ kind: "ok",
909
+ ackEpoch: result.result.producer?.epoch ?? producerSession.producerEpoch,
910
+ ackSeq: result.result.producer?.seq ?? producerSession.nextProducerSeq,
911
+ nextOffset: result.result.nextOffset
912
+ };
913
+ const error = result.result;
914
+ if (error.code === "conflict" && error.detail?.kind === "producer_seq_gap") return {
915
+ kind: "seq-gap",
916
+ expectedSeq: error.detail.expectedSeq
917
+ };
918
+ if (error.code === "forbidden" && error.detail?.kind === "stale_producer_epoch") return {
919
+ kind: "stale-epoch",
920
+ serverEpoch: error.detail.serverEpoch
921
+ };
922
+ throw this.toTransportFailure(error);
923
+ }
924
+ async appendItems(updates) {
925
+ const result = await this.client.append({ part: {
926
+ contentType: BINARY_CONTENT_TYPE,
927
+ body: encodeItems(updates)
928
+ } });
929
+ if (result.ok) return result.result.nextOffset;
930
+ throw this.toTransportFailure(result.result);
931
+ }
932
+ async appendEphemeralUpdate(update, signal) {
933
+ if (update.byteLength === 0) return;
934
+ const response = await this.fetchAuthorized(this.streamUrl, {
935
+ method: "POST",
936
+ headers: { "Content-Type": BINARY_CONTENT_TYPE },
937
+ body: update,
938
+ signal
939
+ }, this.reconnectConfig.connectTimeoutMs, "connect");
940
+ if (response.status === 200 || response.status === 204) return;
941
+ throw await this.responseToEphemeralFailure(response, "ephemeral append");
942
+ }
943
+ /**
944
+ * Opens one bootstrap-first ephemeral SSE session.
945
+ *
946
+ * Ephemeral channels intentionally do not expose offsets or control events:
947
+ * the first event must be `bootstrap`, and all following events must be live
948
+ * `data` updates.
949
+ */
950
+ async readEphemeralSse(onBootstrap, onData, signal) {
951
+ const requestUrl = this.ephemeralReadEndpoint();
952
+ const response = await this.fetchAuthorized(requestUrl, {
953
+ method: "GET",
954
+ headers: { Accept: "text/event-stream" },
955
+ signal
956
+ }, this.reconnectConfig.connectTimeoutMs, "connect");
957
+ if (response.status === 404) return { kind: "not-found" };
958
+ if (!response.ok) {
959
+ if (response.status === 400) {
960
+ const message = await response.text();
961
+ if (isSseUnsupportedErrorText(message)) return { kind: "unsupported" };
962
+ throw new HttpError(message.length > 0 ? `ephemeral live read failed: ${message}` : "ephemeral live read failed", response.status);
963
+ }
964
+ throw await this.responseToEphemeralFailure(response, "ephemeral live read");
965
+ }
966
+ if (response.body == null) throw new ProtocolError("ephemeral sse response missing body");
967
+ if (!(response.headers.get("Content-Type") ?? "").toLowerCase().includes("text/event-stream")) throw new ProtocolError("ephemeral sse response must use text/event-stream content type");
968
+ const dataEncoding = response.headers.get("Stream-SSE-Data-Encoding") ?? void 0;
969
+ const consume = async () => {
970
+ let sawBootstrap = false;
971
+ for await (const event of readSseEvents(response.body)) {
972
+ if (!sawBootstrap) {
973
+ if (event.event !== "bootstrap") throw new ProtocolError(`ephemeral sse must start with bootstrap, got '${event.event}'`);
974
+ await onBootstrap(decodeSsePayload(event.data, dataEncoding));
975
+ sawBootstrap = true;
976
+ continue;
977
+ }
978
+ if (event.event === "data") {
979
+ await onData(decodeSsePayload(event.data, dataEncoding));
980
+ continue;
981
+ }
982
+ throw new ProtocolError(`unexpected ephemeral sse event '${event.event}'`);
983
+ }
984
+ if (!sawBootstrap) throw new ProtocolError("ephemeral sse ended before bootstrap");
985
+ return { kind: "ok" };
986
+ };
987
+ let abortHandler;
988
+ const abortRace = signal != null && !signal.aborted ? new Promise((_, reject) => {
989
+ abortHandler = () => {
990
+ response.body?.cancel().catch(() => {});
991
+ reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
992
+ };
993
+ signal.addEventListener("abort", abortHandler, { once: true });
994
+ }) : null;
995
+ try {
996
+ if (signal?.aborted) {
997
+ await response.body.cancel().catch(() => {});
998
+ throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
999
+ }
1000
+ return abortRace == null ? await consume() : await Promise.race([consume(), abortRace]);
1001
+ } finally {
1002
+ if (abortHandler != null && signal != null) signal.removeEventListener("abort", abortHandler);
1003
+ }
1004
+ }
1005
+ async fetchAuthorized(input, init, timeoutMs, phase) {
1006
+ const firstToken = await this.resolveToken({ reason: "request" });
1007
+ const firstResponse = await this.fetchWithTimeout(input, {
1008
+ ...init,
1009
+ headers: this.attachAuthorization(init.headers, firstToken)
1010
+ }, timeoutMs, phase);
1011
+ if (this.authProvider == null || firstResponse.status !== 401 && firstResponse.status !== 403) return firstResponse;
1012
+ await firstResponse.body?.cancel().catch(() => {});
1013
+ const refreshedToken = await this.refreshTokenAfterFailure(firstResponse.status, firstToken);
1014
+ return this.fetchWithTimeout(input, {
1015
+ ...init,
1016
+ headers: this.attachAuthorization(init.headers, refreshedToken)
1017
+ }, timeoutMs, phase);
1018
+ }
1019
+ async fetchWithTimeout(input, init, timeoutMs, phase) {
1020
+ const controller = new AbortController();
1021
+ const signal = mergeAbortSignals(init.signal, controller.signal);
1022
+ const timer = setTimeout(() => {
1023
+ controller.abort(new TimeoutError(phase, timeoutMs));
1024
+ }, timeoutMs);
1025
+ try {
1026
+ return await this.fetchImpl(input, {
1027
+ ...init,
1028
+ signal
1029
+ });
1030
+ } catch (error) {
1031
+ if (controller.signal.aborted && controller.signal.reason instanceof TimeoutError) throw controller.signal.reason;
1032
+ throw error;
1033
+ } finally {
1034
+ clearTimeout(timer);
1035
+ }
1036
+ }
1037
+ async resolveToken(context) {
1038
+ return await this.authProvider?.(context);
1039
+ }
1040
+ async refreshTokenAfterFailure(status, previousToken) {
1041
+ if (this.authProvider == null) return;
1042
+ if (this.authRefreshInFlight == null) this.authRefreshInFlight = this.resolveToken({
1043
+ reason: "unauthorized",
1044
+ status,
1045
+ previousToken
1046
+ }).finally(() => {
1047
+ this.authRefreshInFlight = void 0;
1048
+ });
1049
+ return await this.authRefreshInFlight;
1050
+ }
1051
+ attachAuthorization(base, token) {
1052
+ const headers = new Headers(base);
1053
+ if (token == null || token.length === 0) {
1054
+ headers.delete("Authorization");
1055
+ return headers;
1056
+ }
1057
+ headers.set("Authorization", `Bearer ${token}`);
1058
+ return headers;
1059
+ }
1060
+ async responseToEphemeralFailure(response, context) {
1061
+ const message = await response.text().catch(() => "");
1062
+ return new HttpError(message.length > 0 ? `${context} failed: ${message}` : `${context} failed with status ${response.status}`, response.status);
1063
+ }
1064
+ ephemeralReadEndpoint() {
1065
+ const target = new URL(this.streamUrl);
1066
+ target.searchParams.set("live", "sse");
1067
+ return target.toString();
1068
+ }
1069
+ toTransportFailure(error) {
1070
+ switch (error.code) {
1071
+ case "bad_request":
1072
+ case "unauthorized":
1073
+ case "forbidden":
1074
+ case "not_found":
1075
+ case "conflict":
1076
+ case "gone":
1077
+ case "rate_limited": return new HttpError(error.message, error.status);
1078
+ case "timeout": return new TimeoutError(error.phase, error.phase === "poll" ? this.reconnectConfig.pollTimeoutMs : this.reconnectConfig.connectTimeoutMs);
1079
+ case "network_error": return error.cause instanceof TypeError ? error.cause : new TypeError(error.message);
1080
+ case "protocol_error": return new ProtocolError(error.message, error.status);
1081
+ case "unknown":
1082
+ if (typeof error.status === "number") return new HttpError(error.message, error.status);
1083
+ if (error.cause instanceof TypeError) return error.cause;
1084
+ if (error.cause instanceof Error) return error.cause;
1085
+ return new Error(error.message);
1086
+ default: {
1087
+ const exhaustive = error;
1088
+ return new Error(String(exhaustive));
1089
+ }
1090
+ }
1091
+ }
1092
+ async readItemsSse(initialOffset, cursor, onControl, onOpen, signal, onActivity) {
1093
+ const opened = await this.openItemsSseSession(initialOffset, cursor, onOpen, signal, onActivity);
1094
+ if (opened.kind !== "ok") return opened;
1095
+ let abortHandler;
1096
+ const abortRace = signal != null && !signal.aborted ? new Promise((_, reject) => {
1097
+ abortHandler = () => {
1098
+ opened.session.cancel();
1099
+ reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
1100
+ };
1101
+ signal.addEventListener("abort", abortHandler, { once: true });
1102
+ }) : null;
1103
+ try {
1104
+ if (signal?.aborted) {
1105
+ await opened.session.cancel();
1106
+ throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
1107
+ }
1108
+ const result = abortRace != null ? await Promise.race([opened.session.continue(onControl), abortRace]) : await opened.session.continue(onControl);
1109
+ if (result.paused) await opened.session.cancel();
1110
+ return {
1111
+ kind: "ok",
1112
+ latestOffset: result.latestOffset,
1113
+ latestCursor: result.latestCursor
1114
+ };
1115
+ } finally {
1116
+ if (abortHandler != null && signal != null) signal.removeEventListener("abort", abortHandler);
1117
+ }
1118
+ }
1119
+ async openItemsSseSession(initialOffset, cursor, onOpen, signal, onActivity) {
1120
+ const result = await this.client.openSseSession({
1121
+ offset: initialOffset,
1122
+ cursor,
1123
+ signal,
1124
+ onActivity
1125
+ });
1126
+ if (!result.ok) {
1127
+ if (result.result.code === "gone") return { kind: "gone" };
1128
+ if (result.result.code === "not_found") return { kind: "not-found" };
1129
+ if (result.result.code === "bad_request" && isSseUnsupportedErrorText(result.result.message)) return { kind: "unsupported" };
1130
+ throw this.toTransportFailure(result.result);
1131
+ }
1132
+ const session = result.result;
1133
+ onOpen?.();
1134
+ let pendingDataEvents = [];
1135
+ let latestOffset = session.nextOffset ?? initialOffset;
1136
+ let latestCursor = session.cursor;
1137
+ let exhausted = false;
1138
+ return {
1139
+ kind: "ok",
1140
+ session: {
1141
+ continue: async (nextOnControl) => {
1142
+ if (exhausted) return {
1143
+ kind: "ok",
1144
+ latestOffset,
1145
+ latestCursor,
1146
+ paused: false
1147
+ };
1148
+ for await (const event of session.events) {
1149
+ if (event.event === "data") {
1150
+ pendingDataEvents.push(event.data);
1151
+ continue;
1152
+ }
1153
+ if (event.event !== "control") continue;
1154
+ const control = parseSseControlData(event.data);
1155
+ const updates = [];
1156
+ for (const eventData of pendingDataEvents) updates.push(...decodeSseItemPayloads(eventData, session.dataEncoding));
1157
+ pendingDataEvents = [];
1158
+ const handlerResult = await nextOnControl(updates, control.nextOffset, control.upToDate, control.streamCursor);
1159
+ latestOffset = control.nextOffset;
1160
+ if (control.streamCursor != null) latestCursor = control.streamCursor;
1161
+ if (handlerResult === "stop") {
1162
+ exhausted = true;
1163
+ await session.cancel();
1164
+ return {
1165
+ kind: "ok",
1166
+ latestOffset,
1167
+ latestCursor,
1168
+ paused: false
1169
+ };
1170
+ }
1171
+ if (handlerResult === "pause") return {
1172
+ kind: "ok",
1173
+ latestOffset,
1174
+ latestCursor,
1175
+ paused: true
1176
+ };
1177
+ }
1178
+ exhausted = true;
1179
+ return {
1180
+ kind: "ok",
1181
+ latestOffset,
1182
+ latestCursor,
1183
+ paused: false
1184
+ };
1185
+ },
1186
+ cancel: async () => {
1187
+ if (exhausted) return;
1188
+ exhausted = true;
1189
+ await session.cancel();
1190
+ }
1191
+ }
1192
+ };
1193
+ }
1194
+ };
1195
+ //#endregion
1196
+ //#region src/ephemeral-runtime.ts
1197
+ function createNoopAdapter() {
1198
+ return {
1199
+ emptyVersion: () => ({}),
1200
+ mergeVersions: (base) => base,
1201
+ exportSnapshot: () => new Uint8Array(),
1202
+ applySnapshot: () => ({}),
1203
+ exportUpdates: () => void 0,
1204
+ applyRemoteUpdates: () => ({}),
1205
+ subscribeLocalUpdates: () => () => void 0
1206
+ };
1207
+ }
1208
+ function normalizeEphemeralUpdate(update) {
1209
+ if (update == null || update.byteLength === 0) return null;
1210
+ return update;
1211
+ }
1212
+ function toError(error) {
1213
+ return error instanceof Error ? error : new Error(String(error));
1214
+ }
1215
+ function closedSubscriptionError() {
1216
+ return /* @__PURE__ */ new Error("subscription closed");
1217
+ }
1218
+ /**
1219
+ * Realtime transport for one ephemeral CRDT stream.
1220
+ *
1221
+ * This is intentionally separate from `StreamsCrdt`: it bootstraps through
1222
+ * the first SSE event, then tails live deltas, and never stores a replay
1223
+ * offset, snapshot checkpoint, or remote cursor.
1224
+ */
1225
+ var EphemeralStreamCrdt = class {
1226
+ /** Full ephemeral stream URL, including `?ephemeral=` or `?awareness=`. */
1227
+ streamUrl;
1228
+ /** Store-specific adapter used to encode local state and apply remote bytes. */
1229
+ adaptor;
1230
+ /** Low-level HTTP/SSE transport. It is reused only for auth, fetch, and parsing helpers. */
1231
+ client;
1232
+ /** Retry policy shared by the read loop and local write queue. */
1233
+ reconnectConfig;
1234
+ /** Period for republishing full local ephemeral state, or `false` to disable it. */
1235
+ heartbeatMs;
1236
+ /** Enables diagnostic console errors for failed read/write loops. */
1237
+ debug;
1238
+ /** Current shared subscription state, if the stream is joined. */
1239
+ joinState;
1240
+ constructor(options) {
1241
+ this.streamUrl = options.streamUrl;
1242
+ this.adaptor = options.adaptor;
1243
+ this.reconnectConfig = {
1244
+ ...DEFAULT_RECONNECT_CONFIG,
1245
+ ...options.reconnectConfig
1246
+ };
1247
+ this.heartbeatMs = options.heartbeatMs ?? 15e3;
1248
+ this.debug = options.debug ?? false;
1249
+ this.client = new StreamsTransportClient({
1250
+ streamUrl: options.streamUrl,
1251
+ adapter: createNoopAdapter(),
1252
+ authProvider: normalizeAuthProvider(options.auth),
1253
+ fetchImpl: options.fetch ?? globalThis.fetch.bind(globalThis),
1254
+ reconnectConfig: this.reconnectConfig
1255
+ });
1256
+ }
1257
+ async join(params) {
1258
+ const existing = this.joinState;
1259
+ if (existing != null) try {
1260
+ await existing.started;
1261
+ return {
1262
+ ok: true,
1263
+ value: this.createSubscription(existing, params)
1264
+ };
1265
+ } catch (error) {
1266
+ return {
1267
+ ok: false,
1268
+ error: toTransportError(error)
1269
+ };
1270
+ }
1271
+ const state = this.createJoinState();
1272
+ state.started = this.startJoin(state);
1273
+ this.joinState = state;
1274
+ try {
1275
+ await state.started;
1276
+ return {
1277
+ ok: true,
1278
+ value: this.createSubscription(state, params)
1279
+ };
1280
+ } catch (error) {
1281
+ await this.disposeJoinState(state);
1282
+ return {
1283
+ ok: false,
1284
+ error: toTransportError(error)
1285
+ };
1286
+ }
1287
+ }
1288
+ rejoin() {
1289
+ const state = this.joinState;
1290
+ if (state == null || state.closed) return;
1291
+ state.retryAttempt = 0;
1292
+ state.retrySleepController.abort();
1293
+ state.retrySleepController = new AbortController();
1294
+ const previousController = state.requestController;
1295
+ state.requestController = new AbortController();
1296
+ previousController?.abort();
1297
+ if (state.readStatus === "disconnected" || state.readStatus === "error") {
1298
+ this.setReadStatus(state, "reconnecting");
1299
+ this.runReadLoop(state);
1300
+ }
1301
+ if (state.writeStatus === "error") {
1302
+ this.setWriteStatus(state, "reconnecting");
1303
+ this.flushPendingLocal(state);
1304
+ }
1305
+ }
1306
+ async close() {
1307
+ const state = this.joinState;
1308
+ if (state != null) await this.disposeJoinState(state);
1309
+ }
1310
+ createJoinState() {
1311
+ return {
1312
+ refCount: 0,
1313
+ closed: false,
1314
+ status: "connecting",
1315
+ readStatus: "connecting",
1316
+ writeStatus: "connecting",
1317
+ statusListeners: /* @__PURE__ */ new Set(),
1318
+ pendingLocal: new Deque(),
1319
+ syncWaiters: [],
1320
+ flushingLocal: false,
1321
+ retryAttempt: 0,
1322
+ retrySleepController: new AbortController(),
1323
+ firstOpenSettled: false,
1324
+ started: Promise.resolve()
1325
+ };
1326
+ }
1327
+ async startJoin(state) {
1328
+ state.requestController = new AbortController();
1329
+ const firstOpen = this.waitForFirstOpen(state);
1330
+ this.runReadLoop(state);
1331
+ await firstOpen;
1332
+ if (state.closed) return;
1333
+ state.unsubscribeLocal = this.adaptor.subscribeLocalUpdates((update) => {
1334
+ this.enqueueLocal(state, normalizeEphemeralUpdate(update));
1335
+ });
1336
+ this.setWriteStatus(state, "ok");
1337
+ const fullState = normalizeEphemeralUpdate(await this.adaptor.encodeAll());
1338
+ if (fullState != null) state.pendingLocal.pushBack(fullState);
1339
+ this.startHeartbeat(state);
1340
+ await this.flushPendingLocal(state);
1341
+ if (state.status === "error" || state.status === "disconnected") throw new Error(`ephemeral stream failed to join: ${state.status}`);
1342
+ }
1343
+ waitForFirstOpen(state) {
1344
+ return new Promise((resolve, reject) => {
1345
+ state.firstOpenResolve = resolve;
1346
+ state.firstOpenReject = reject;
1347
+ });
1348
+ }
1349
+ createSubscription(state, params) {
1350
+ state.refCount += 1;
1351
+ const statusListener = params?.onStatusChange;
1352
+ if (statusListener != null) {
1353
+ state.statusListeners.add(statusListener);
1354
+ statusListener(state.status);
1355
+ }
1356
+ let unsubscribed = false;
1357
+ return {
1358
+ unsubscribe: () => {
1359
+ if (unsubscribed) return;
1360
+ unsubscribed = true;
1361
+ if (statusListener != null) state.statusListeners.delete(statusListener);
1362
+ state.refCount = Math.max(0, state.refCount - 1);
1363
+ if (state.refCount === 0 && !state.closed) this.disposeJoinState(state);
1364
+ },
1365
+ get connected() {
1366
+ return state.status === "joined";
1367
+ },
1368
+ get status() {
1369
+ return state.status;
1370
+ },
1371
+ onStatusChange: (listener) => this.onStatusChange(state, listener),
1372
+ waitUntilSynced: async () => {
1373
+ if (state.closed) throw closedSubscriptionError();
1374
+ const remaining = state.pendingLocal.length;
1375
+ if (remaining === 0) return;
1376
+ return new Promise((resolve, reject) => {
1377
+ state.syncWaiters.push({
1378
+ remaining,
1379
+ resolve,
1380
+ reject
1381
+ });
1382
+ });
1383
+ }
1384
+ };
1385
+ }
1386
+ onStatusChange(state, listener) {
1387
+ state.statusListeners.add(listener);
1388
+ listener(state.status);
1389
+ return () => {
1390
+ state.statusListeners.delete(listener);
1391
+ };
1392
+ }
1393
+ async disposeJoinState(state) {
1394
+ if (state.closed) return;
1395
+ state.closed = true;
1396
+ state.unsubscribeLocal?.();
1397
+ if (state.heartbeatTimer != null) clearInterval(state.heartbeatTimer);
1398
+ state.requestController?.abort();
1399
+ state.retrySleepController.abort();
1400
+ this.rejectFirstOpen(state, closedSubscriptionError());
1401
+ this.rejectSyncWaiters(state, closedSubscriptionError());
1402
+ if (this.joinState === state) this.joinState = void 0;
1403
+ }
1404
+ resolveFirstOpen(state) {
1405
+ if (state.firstOpenSettled) return;
1406
+ state.firstOpenSettled = true;
1407
+ state.firstOpenResolve?.();
1408
+ }
1409
+ rejectFirstOpen(state, error) {
1410
+ if (state.firstOpenSettled) return;
1411
+ state.firstOpenSettled = true;
1412
+ state.firstOpenReject?.(error);
1413
+ }
1414
+ async runReadLoop(state) {
1415
+ while (!state.closed) {
1416
+ state.requestController ??= new AbortController();
1417
+ const capturedController = state.requestController;
1418
+ try {
1419
+ const result = await this.client.readEphemeralSse(async (bootstrap) => {
1420
+ if (bootstrap.byteLength > 0) await this.adaptor.applyRemoteUpdates([bootstrap]);
1421
+ this.setReadStatus(state, "ok");
1422
+ this.resolveFirstOpen(state);
1423
+ if (state.unsubscribeLocal != null) this.enqueueFullState(state);
1424
+ }, async (update) => {
1425
+ if (update.byteLength > 0) await this.adaptor.applyRemoteUpdates([update]);
1426
+ }, capturedController.signal);
1427
+ if (state.closed) return;
1428
+ if (result.kind === "ok") {
1429
+ state.retryAttempt = 0;
1430
+ this.setReadStatus(state, "reconnecting");
1431
+ await sleep(0);
1432
+ continue;
1433
+ }
1434
+ if (result.kind === "unsupported") throw new Error("ephemeral stream requires SSE support");
1435
+ if (result.kind === "not-found") throw new HttpError("ephemeral stream not found", 404);
1436
+ } catch (error) {
1437
+ if (state.closed) return;
1438
+ if (capturedController.signal.aborted) {
1439
+ state.requestController = new AbortController();
1440
+ this.setReadStatus(state, "reconnecting");
1441
+ continue;
1442
+ }
1443
+ const classified = classifyFetchError(error);
1444
+ if (!classified.retriable || isAuthOrProtocolError(error)) {
1445
+ this.logError("ephemeral live read failed", error, {
1446
+ streamUrl: this.streamUrl,
1447
+ code: classified.code
1448
+ });
1449
+ this.setReadStatus(state, "error");
1450
+ this.rejectFirstOpen(state, toError(error));
1451
+ return;
1452
+ }
1453
+ this.setReadStatus(state, "reconnecting");
1454
+ if (state.retryAttempt >= this.reconnectConfig.maxAttempts) {
1455
+ this.logError("ephemeral live reconnect exhausted", error, {
1456
+ streamUrl: this.streamUrl,
1457
+ attempts: state.retryAttempt
1458
+ });
1459
+ this.setReadStatus(state, "disconnected");
1460
+ this.rejectFirstOpen(state, toError(error));
1461
+ return;
1462
+ }
1463
+ const delay = computeRetryDelay(state.retryAttempt, this.reconnectConfig);
1464
+ state.retryAttempt += 1;
1465
+ await sleep(delay, state.retrySleepController.signal);
1466
+ }
1467
+ }
1468
+ }
1469
+ startHeartbeat(state) {
1470
+ if (this.heartbeatMs === false || this.heartbeatMs <= 0) return;
1471
+ state.heartbeatTimer = setInterval(() => {
1472
+ this.enqueueFullState(state);
1473
+ }, this.heartbeatMs);
1474
+ }
1475
+ async enqueueFullState(state) {
1476
+ try {
1477
+ const update = await this.adaptor.encodeAll();
1478
+ this.enqueueLocal(state, normalizeEphemeralUpdate(update));
1479
+ } catch (error) {
1480
+ this.logError("ephemeral encodeAll failed", error, { streamUrl: this.streamUrl });
1481
+ this.setWriteStatus(state, "error");
1482
+ }
1483
+ }
1484
+ enqueueLocal(state, update) {
1485
+ if (state.closed || update == null || update.byteLength === 0) return;
1486
+ state.pendingLocal.pushBack(update);
1487
+ this.flushPendingLocal(state);
1488
+ }
1489
+ async flushPendingLocal(state) {
1490
+ if (state.flushingLocal || state.closed) return;
1491
+ state.flushingLocal = true;
1492
+ try {
1493
+ while (!state.closed) {
1494
+ const update = state.pendingLocal.peekFront();
1495
+ if (update == null) return;
1496
+ let attempt = 0;
1497
+ while (!state.closed) try {
1498
+ await this.client.appendEphemeralUpdate(update);
1499
+ state.pendingLocal.popFront();
1500
+ this.notifySyncWaiters(state, 1);
1501
+ this.setWriteStatus(state, "ok");
1502
+ break;
1503
+ } catch (error) {
1504
+ const classified = classifyFetchError(error);
1505
+ if (!classified.retriable || isAuthOrProtocolError(error)) {
1506
+ this.rejectSyncWaiters(state, /* @__PURE__ */ new Error(`ephemeral append failed: ${classified.code}`));
1507
+ this.setWriteStatus(state, "error");
1508
+ return;
1509
+ }
1510
+ if (attempt >= this.reconnectConfig.maxAttempts) {
1511
+ this.rejectSyncWaiters(state, /* @__PURE__ */ new Error("ephemeral append retry budget exhausted"));
1512
+ this.setWriteStatus(state, "error");
1513
+ return;
1514
+ }
1515
+ this.setWriteStatus(state, "reconnecting");
1516
+ const delay = computeRetryDelay(attempt, this.reconnectConfig);
1517
+ attempt += 1;
1518
+ await sleep(delay, state.retrySleepController.signal);
1519
+ }
1520
+ }
1521
+ } finally {
1522
+ state.flushingLocal = false;
1523
+ }
1524
+ }
1525
+ notifySyncWaiters(state, flushedCount) {
1526
+ let i = 0;
1527
+ while (i < state.syncWaiters.length) {
1528
+ const waiter = state.syncWaiters[i];
1529
+ waiter.remaining -= flushedCount;
1530
+ if (waiter.remaining <= 0) {
1531
+ state.syncWaiters.splice(i, 1);
1532
+ waiter.resolve();
1533
+ } else i += 1;
1534
+ }
1535
+ }
1536
+ rejectSyncWaiters(state, error) {
1537
+ for (const waiter of state.syncWaiters.splice(0)) waiter.reject(error);
1538
+ }
1539
+ setReadStatus(state, readStatus) {
1540
+ if (state.readStatus === readStatus) return;
1541
+ state.readStatus = readStatus;
1542
+ this.recomputeStatus(state);
1543
+ }
1544
+ setWriteStatus(state, writeStatus) {
1545
+ if (state.writeStatus === writeStatus) return;
1546
+ state.writeStatus = writeStatus;
1547
+ this.recomputeStatus(state);
1548
+ }
1549
+ recomputeStatus(state) {
1550
+ const status = this.computeStatus(state);
1551
+ if (state.status === status) return;
1552
+ state.status = status;
1553
+ for (const listener of state.statusListeners) listener(status);
1554
+ }
1555
+ computeStatus(state) {
1556
+ if (state.readStatus === "error" || state.writeStatus === "error") return "error";
1557
+ if (state.readStatus === "disconnected") return "disconnected";
1558
+ if (state.readStatus === "connecting" || state.writeStatus === "connecting") return "connecting";
1559
+ if (state.readStatus === "reconnecting" || state.writeStatus === "reconnecting") return "reconnecting";
1560
+ return "joined";
1561
+ }
1562
+ logError(message, error, detail) {
1563
+ if (!this.debug) return;
1564
+ console.error(`[streams-crdt] ${message}`, error, detail);
1565
+ }
1566
+ };
1567
+ //#endregion
1568
+ //#region src/checkpoint.ts
1569
+ function composeBeforeRemoteCursorSaveHooks(...hooks) {
1570
+ const active = hooks.filter((hook) => hook != null);
1571
+ if (active.length === 0) return;
1572
+ return async (context) => {
1573
+ for (const hook of active) await hook(context);
1574
+ };
1575
+ }
1576
+ async function persistRemoteCursor(input) {
1577
+ const next = {
1578
+ ...input.cursor,
1579
+ updatedAtMs: (input.now ?? Date.now)()
1580
+ };
1581
+ if (input.beforeRemoteCursorSave != null) await input.beforeRemoteCursorSave({
1582
+ cursor: next,
1583
+ source: input.source
1584
+ });
1585
+ await input.remoteCursorStore.save(next);
1586
+ return next;
1587
+ }
1588
+ function key(streamUrl) {
1589
+ return streamUrl;
1590
+ }
1591
+ function isJsonObject(value) {
1592
+ return typeof value === "object" && value != null && !Array.isArray(value);
1593
+ }
1594
+ function cloneJsonValue(value) {
1595
+ return structuredClone(value);
1596
+ }
1597
+ function cloneRemoteCursor(cursor) {
1598
+ return {
1599
+ ...cursor,
1600
+ serverLowerBoundVersion: cloneJsonValue(cursor.serverLowerBoundVersion)
1601
+ };
1602
+ }
1603
+ function fromStoredRemoteCursor(record) {
1604
+ if (!isJsonObject(record.serverLowerBoundVersion)) throw new Error("invalid remote cursor record: serverLowerBoundVersion is not a JSON object");
1605
+ return {
1606
+ streamUrl: record.streamUrl,
1607
+ nextOffset: record.nextOffset,
1608
+ serverLowerBoundVersion: cloneJsonValue(record.serverLowerBoundVersion),
1609
+ updatedAtMs: record.updatedAtMs
1610
+ };
1611
+ }
1612
+ function toStoredRemoteCursor(cursor) {
1613
+ return {
1614
+ streamUrl: cursor.streamUrl,
1615
+ nextOffset: cursor.nextOffset,
1616
+ serverLowerBoundVersion: cloneJsonValue(cursor.serverLowerBoundVersion),
1617
+ updatedAtMs: cursor.updatedAtMs
1618
+ };
1619
+ }
1620
+ /**
1621
+ * In-memory cursor store useful for tests or ephemeral runtimes.
1622
+ */
1623
+ var InMemoryRemoteCursorStore = class {
1624
+ data = /* @__PURE__ */ new Map();
1625
+ async load(streamUrl) {
1626
+ const value = this.data.get(key(streamUrl));
1627
+ return value == null ? null : cloneRemoteCursor(value);
1628
+ }
1629
+ async save(cursor) {
1630
+ this.data.set(key(cursor.streamUrl), cloneRemoteCursor(cursor));
1631
+ }
1632
+ async delete(streamUrl) {
1633
+ this.data.delete(key(streamUrl));
1634
+ }
1635
+ };
1636
+ const DEFAULT_DB_NAME = "streams-crdt";
1637
+ const DEFAULT_STORE_NAME = "remote-cursors";
1638
+ const DEFAULT_DB_VERSION = 1;
1639
+ /**
1640
+ * IndexedDB-backed cursor store for browser clients that need replay progress persistence.
1641
+ *
1642
+ * This store only persists `RemoteCursor` metadata. It does not persist CRDT
1643
+ * payloads. Use it only when the application also persists the corresponding
1644
+ * local document state and restores that state before calling `sync()` or
1645
+ * `join()` after a reload.
1646
+ */
1647
+ var IndexedDbRemoteCursorStore = class {
1648
+ dbName;
1649
+ storeName;
1650
+ dbVersion;
1651
+ indexedDb;
1652
+ db = null;
1653
+ initPromise = null;
1654
+ constructor(options) {
1655
+ this.dbName = options?.dbName ?? DEFAULT_DB_NAME;
1656
+ this.storeName = options?.storeName ?? DEFAULT_STORE_NAME;
1657
+ this.dbVersion = options?.dbVersion ?? DEFAULT_DB_VERSION;
1658
+ this.indexedDb = options?.indexedDb ?? globalThis.indexedDB;
1659
+ }
1660
+ async load(streamUrl) {
1661
+ const db = await this.ensureDb();
1662
+ const record = await new Promise((resolve, reject) => {
1663
+ const request = db.transaction([this.storeName], "readonly").objectStore(this.storeName).get(key(streamUrl));
1664
+ request.onsuccess = () => {
1665
+ resolve(request.result ?? null);
1666
+ };
1667
+ request.onerror = () => {
1668
+ reject(request.error ?? /* @__PURE__ */ new Error("indexeddb load failed"));
1669
+ };
1041
1670
  });
1042
- if (!result.ok) throw this.toTransportFailure(result.result);
1671
+ return record == null ? null : fromStoredRemoteCursor(record);
1043
1672
  }
1044
- async appendUpdates(updates, producerSession) {
1045
- const result = await this.client.append({
1046
- part: {
1047
- contentType: BINARY_CONTENT_TYPE,
1048
- body: encodeItems(updates)
1049
- },
1050
- producer: {
1051
- producerId: producerSession.producerId,
1052
- epoch: producerSession.producerEpoch,
1053
- seq: producerSession.nextProducerSeq
1054
- }
1673
+ async save(cursor) {
1674
+ const db = await this.ensureDb();
1675
+ const record = toStoredRemoteCursor(cursor);
1676
+ await new Promise((resolve, reject) => {
1677
+ const tx = db.transaction([this.storeName], "readwrite");
1678
+ const request = tx.objectStore(this.storeName).put(record, key(cursor.streamUrl));
1679
+ tx.oncomplete = () => resolve();
1680
+ tx.onerror = () => reject(tx.error ?? request.error ?? /* @__PURE__ */ new Error("indexeddb save failed"));
1681
+ tx.onabort = () => reject(tx.error ?? request.error ?? /* @__PURE__ */ new Error("indexeddb save transaction aborted"));
1682
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("indexeddb save failed"));
1055
1683
  });
1056
- if (result.ok) return {
1057
- kind: "ok",
1058
- ackEpoch: result.result.producer?.epoch ?? producerSession.producerEpoch,
1059
- ackSeq: result.result.producer?.seq ?? producerSession.nextProducerSeq,
1060
- nextOffset: result.result.nextOffset
1061
- };
1062
- const error = result.result;
1063
- if (error.code === "conflict" && error.detail?.kind === "producer_seq_gap") return {
1064
- kind: "seq-gap",
1065
- expectedSeq: error.detail.expectedSeq
1066
- };
1067
- if (error.code === "forbidden" && error.detail?.kind === "stale_producer_epoch") return {
1068
- kind: "stale-epoch",
1069
- serverEpoch: error.detail.serverEpoch
1070
- };
1071
- throw this.toTransportFailure(error);
1072
1684
  }
1073
- toTransportFailure(error) {
1074
- switch (error.code) {
1075
- case "bad_request":
1076
- case "unauthorized":
1077
- case "forbidden":
1078
- case "not_found":
1079
- case "conflict":
1080
- case "gone":
1081
- case "rate_limited": return new HttpError(error.message, error.status);
1082
- case "timeout": return new TimeoutError(error.phase, error.phase === "poll" ? this.reconnectConfig.pollTimeoutMs : this.reconnectConfig.connectTimeoutMs);
1083
- case "network_error": return error.cause instanceof TypeError ? error.cause : new TypeError(error.message);
1084
- case "protocol_error": return new ProtocolError(error.message, error.status);
1085
- case "unknown":
1086
- if (typeof error.status === "number") return new HttpError(error.message, error.status);
1087
- if (error.cause instanceof TypeError) return error.cause;
1088
- if (error.cause instanceof Error) return error.cause;
1089
- return new Error(error.message);
1090
- default: {
1091
- const exhaustive = error;
1092
- return new Error(String(exhaustive));
1093
- }
1094
- }
1685
+ async delete(streamUrl) {
1686
+ const db = await this.ensureDb();
1687
+ await new Promise((resolve, reject) => {
1688
+ const tx = db.transaction([this.storeName], "readwrite");
1689
+ const request = tx.objectStore(this.storeName).delete(key(streamUrl));
1690
+ tx.oncomplete = () => resolve();
1691
+ tx.onerror = () => reject(tx.error ?? request.error ?? /* @__PURE__ */ new Error("indexeddb delete failed"));
1692
+ tx.onabort = () => reject(tx.error ?? request.error ?? /* @__PURE__ */ new Error("indexeddb delete transaction aborted"));
1693
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("indexeddb delete failed"));
1694
+ });
1095
1695
  }
1096
- async readItemsSse(initialOffset, cursor, onControl, onOpen, signal, onActivity) {
1097
- const opened = await this.openItemsSseSession(initialOffset, cursor, onOpen, signal, onActivity);
1098
- if (opened.kind !== "ok") return opened;
1099
- let abortHandler;
1100
- const abortRace = signal != null && !signal.aborted ? new Promise((_, reject) => {
1101
- abortHandler = () => {
1102
- opened.session.cancel();
1103
- reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
1696
+ async ensureDb() {
1697
+ if (this.db != null) return this.db;
1698
+ if (this.initPromise != null) {
1699
+ await this.initPromise;
1700
+ if (this.db == null) throw new Error("indexeddb is not available");
1701
+ return this.db;
1702
+ }
1703
+ if (this.indexedDb == null) throw new Error("indexeddb is not available in this environment");
1704
+ this.initPromise = new Promise((resolve, reject) => {
1705
+ const request = this.indexedDb.open(this.dbName, this.dbVersion);
1706
+ request.onupgradeneeded = () => {
1707
+ const db = request.result;
1708
+ if (!db.objectStoreNames.contains(this.storeName)) db.createObjectStore(this.storeName);
1104
1709
  };
1105
- signal.addEventListener("abort", abortHandler, { once: true });
1106
- }) : null;
1107
- try {
1108
- if (signal?.aborted) {
1109
- await opened.session.cancel();
1110
- throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
1111
- }
1112
- const result = abortRace != null ? await Promise.race([opened.session.continue(onControl), abortRace]) : await opened.session.continue(onControl);
1113
- if (result.paused) await opened.session.cancel();
1114
- return {
1115
- kind: "ok",
1116
- latestOffset: result.latestOffset,
1117
- latestCursor: result.latestCursor
1710
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("indexeddb open failed"));
1711
+ request.onsuccess = () => {
1712
+ this.db = request.result;
1713
+ this.db.onversionchange = () => {
1714
+ this.db?.close();
1715
+ this.db = null;
1716
+ };
1717
+ resolve();
1118
1718
  };
1119
- } finally {
1120
- if (abortHandler != null && signal != null) signal.removeEventListener("abort", abortHandler);
1121
- }
1122
- }
1123
- async openItemsSseSession(initialOffset, cursor, onOpen, signal, onActivity) {
1124
- const result = await this.client.openSseSession({
1125
- offset: initialOffset,
1126
- cursor,
1127
- signal,
1128
- onActivity
1719
+ }).finally(() => {
1720
+ this.initPromise = null;
1129
1721
  });
1130
- if (!result.ok) {
1131
- if (result.result.code === "gone") return { kind: "gone" };
1132
- if (result.result.code === "not_found") return { kind: "not-found" };
1133
- if (result.result.code === "bad_request" && isSseUnsupportedErrorText(result.result.message)) return { kind: "unsupported" };
1134
- throw this.toTransportFailure(result.result);
1135
- }
1136
- const session = result.result;
1137
- onOpen?.();
1138
- let pendingDataEvents = [];
1139
- let latestOffset = session.nextOffset ?? initialOffset;
1140
- let latestCursor = session.cursor;
1141
- let exhausted = false;
1142
- return {
1143
- kind: "ok",
1144
- session: {
1145
- continue: async (nextOnControl) => {
1146
- if (exhausted) return {
1147
- kind: "ok",
1148
- latestOffset,
1149
- latestCursor,
1150
- paused: false
1151
- };
1152
- for await (const event of session.events) {
1153
- if (event.event === "data") {
1154
- pendingDataEvents.push(event.data);
1155
- continue;
1156
- }
1157
- if (event.event !== "control") continue;
1158
- const control = parseSseControlData(event.data);
1159
- const updates = [];
1160
- for (const eventData of pendingDataEvents) updates.push(...decodeSseItemPayloads(eventData, session.dataEncoding));
1161
- pendingDataEvents = [];
1162
- const handlerResult = await nextOnControl(updates, control.nextOffset, control.upToDate, control.streamCursor);
1163
- latestOffset = control.nextOffset;
1164
- if (control.streamCursor != null) latestCursor = control.streamCursor;
1165
- if (handlerResult === "stop") {
1166
- exhausted = true;
1167
- await session.cancel();
1168
- return {
1169
- kind: "ok",
1170
- latestOffset,
1171
- latestCursor,
1172
- paused: false
1173
- };
1174
- }
1175
- if (handlerResult === "pause") return {
1176
- kind: "ok",
1177
- latestOffset,
1178
- latestCursor,
1179
- paused: true
1180
- };
1181
- }
1182
- exhausted = true;
1183
- return {
1184
- kind: "ok",
1185
- latestOffset,
1186
- latestCursor,
1187
- paused: false
1188
- };
1189
- },
1190
- cancel: async () => {
1191
- if (exhausted) return;
1192
- exhausted = true;
1193
- await session.cancel();
1194
- }
1195
- }
1196
- };
1722
+ await this.initPromise;
1723
+ if (this.db == null) throw new Error("indexeddb open succeeded without a database handle");
1724
+ return this.db;
1197
1725
  }
1198
1726
  };
1727
+ /**
1728
+ * Creates a cursor value suitable for seeding a `RemoteCursorStore`.
1729
+ */
1730
+ function createInitialRemoteCursor(input) {
1731
+ return {
1732
+ streamUrl: input.streamUrl,
1733
+ nextOffset: input.nextOffset,
1734
+ serverLowerBoundVersion: cloneJsonValue(input.serverLowerBoundVersion),
1735
+ updatedAtMs: Date.now()
1736
+ };
1737
+ }
1199
1738
  //#endregion
1200
1739
  //#region src/transport-cursor.ts
1201
1740
  var TransportCursorManager = class {
@@ -1462,6 +2001,8 @@ var StreamsCrdt = class StreamsCrdt {
1462
2001
  producerId = createProducerId();
1463
2002
  producerEpoch = 0;
1464
2003
  nextProducerSeq = 0;
2004
+ frozenLocalAppend;
2005
+ committedPendingCount;
1465
2006
  pendingWriteOnly = new Deque();
1466
2007
  writeOnlyCursor;
1467
2008
  unsubscribeWriteOnlyLocal;
@@ -1671,6 +2212,8 @@ var StreamsCrdt = class StreamsCrdt {
1671
2212
  await this.remoteCursorStore.delete?.(this.streamUrl);
1672
2213
  this.producerEpoch = 0;
1673
2214
  this.nextProducerSeq = 0;
2215
+ this.frozenLocalAppend = void 0;
2216
+ this.committedPendingCount = void 0;
1674
2217
  return deleted;
1675
2218
  }) }
1676
2219
  };
@@ -1718,15 +2261,17 @@ var StreamsCrdt = class StreamsCrdt {
1718
2261
  localVersion: this.writeOnlyCursor.serverLowerBoundVersion
1719
2262
  }
1720
2263
  };
1721
- const nextCursor = await this.appendLocalBatch(this.writeOnlyCursor, drained.batch, false);
1722
- for (let i = 0; i < drained.count; i += 1) this.pendingWriteOnly.popFront();
1723
- this.writeOnlyCursor = nextCursor;
2264
+ const appended = await this.appendLocalBatchRemote(this.writeOnlyCursor, drained.batch, drained.count, "write-only");
2265
+ const committedCount = appended.pendingCount ?? drained.count;
2266
+ for (let i = 0; i < committedCount; i += 1) this.pendingWriteOnly.popFront();
2267
+ this.writeOnlyCursor = appended.cursor;
2268
+ this.commitProducerAck(appended.producerAck);
1724
2269
  return {
1725
2270
  ok: true,
1726
2271
  value: {
1727
2272
  appended: true,
1728
- nextOffset: nextCursor.nextOffset,
1729
- localVersion: nextCursor.serverLowerBoundVersion
2273
+ nextOffset: appended.cursor.nextOffset,
2274
+ localVersion: appended.cursor.serverLowerBoundVersion
1730
2275
  }
1731
2276
  };
1732
2277
  } catch (error) {
@@ -1902,7 +2447,7 @@ var StreamsCrdt = class StreamsCrdt {
1902
2447
  }
1903
2448
  }
1904
2449
  async syncOnce() {
1905
- let cursor = (await this.resolveInitialRemoteState()).cursor;
2450
+ let cursor = (await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState())).cursor;
1906
2451
  const localBatch = this.exportUpdates(cursor.serverLowerBoundVersion);
1907
2452
  if (localBatch.batch != null) {
1908
2453
  cursor = await this.appendLocalBatch(cursor, localBatch.batch);
@@ -1911,7 +2456,7 @@ var StreamsCrdt = class StreamsCrdt {
1911
2456
  return cursor;
1912
2457
  }
1913
2458
  async performInitialJoinSync(preferredLiveMode) {
1914
- const initial = await this.resolveInitialRemoteState();
2459
+ const initial = await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState());
1915
2460
  let cursor = initial.cursor;
1916
2461
  let localExportRefVersion = cursor.serverLowerBoundVersion;
1917
2462
  let streamCursor = initial.streamCursor;
@@ -2007,17 +2552,52 @@ var StreamsCrdt = class StreamsCrdt {
2007
2552
  };
2008
2553
  }
2009
2554
  }
2010
- async appendLocalBatch(cursor, batch, persist = true) {
2011
- let producerSession = {
2012
- producerId: this.producerId,
2013
- producerEpoch: this.producerEpoch,
2014
- nextProducerSeq: this.nextProducerSeq
2555
+ async appendLocalBatch(cursor, batch, pendingCount) {
2556
+ const source = pendingCount == null ? "direct" : "pending";
2557
+ const committed = await this.appendLocalBatchDurably(cursor, batch, pendingCount, source);
2558
+ this.committedPendingCount = committed.pendingCount;
2559
+ return committed.cursor;
2560
+ }
2561
+ async appendLocalBatchDurably(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending") {
2562
+ const frozen = await this.getOrCreateFrozenLocalAppend(cursor, batch, pendingCount, source);
2563
+ const appended = await this.appendFrozenLocalBatchRemote(frozen);
2564
+ const saved = await this.saveRemoteCursor(appended.cursor, "local");
2565
+ this.commitProducerAck(appended.producerAck);
2566
+ return {
2567
+ cursor: saved,
2568
+ pendingCount: frozen.pendingCount
2569
+ };
2570
+ }
2571
+ async appendLocalBatchRemote(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending") {
2572
+ const frozen = await this.getOrCreateFrozenLocalAppend(cursor, batch, pendingCount, source);
2573
+ return {
2574
+ ...await this.appendFrozenLocalBatchRemote(frozen),
2575
+ pendingCount: frozen.pendingCount
2576
+ };
2577
+ }
2578
+ async getOrCreateFrozenLocalAppend(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending") {
2579
+ const existing = this.frozenLocalAppend;
2580
+ if (existing != null) {
2581
+ if (existing.source !== source) throw new Error(`${existing.source} local append must be finalized before ${source} append`);
2582
+ return existing;
2583
+ }
2584
+ const frozen = {
2585
+ source,
2586
+ baseCursor: cursor,
2587
+ batch,
2588
+ pendingCount,
2589
+ wireUpdates: this.encodeUpdateItems == null ? batch.updates : await this.encodeUpdateItems(batch.updates),
2590
+ producerSession: this.createCurrentProducerSession()
2015
2591
  };
2592
+ this.frozenLocalAppend = frozen;
2593
+ return frozen;
2594
+ }
2595
+ async appendFrozenLocalBatchRemote(frozen) {
2596
+ if (frozen.acknowledged != null) return frozen.acknowledged;
2016
2597
  for (let attempt = 0; attempt < 3; attempt += 1) {
2017
2598
  let result;
2018
2599
  try {
2019
- const wireUpdates = this.encodeUpdateItems == null ? batch.updates : await this.encodeUpdateItems(batch.updates);
2020
- result = await this.client.appendUpdates(wireUpdates, producerSession);
2600
+ result = await this.client.appendUpdates(frozen.wireUpdates, frozen.producerSession);
2021
2601
  } catch (error) {
2022
2602
  if (isStreamNotFoundError(error) && this.createStreamIfMissing) {
2023
2603
  await this.ensureStreamExists();
@@ -2026,30 +2606,66 @@ var StreamsCrdt = class StreamsCrdt {
2026
2606
  throw error;
2027
2607
  }
2028
2608
  if (result.kind === "ok") {
2029
- this.producerEpoch = result.ackEpoch;
2030
- this.nextProducerSeq = result.ackSeq + 1;
2031
- const next = {
2032
- ...cursor,
2033
- nextOffset: result.nextOffset,
2034
- serverLowerBoundVersion: this.adapter.mergeVersions(cursor.serverLowerBoundVersion, batch.version)
2609
+ const appended = {
2610
+ cursor: {
2611
+ ...frozen.baseCursor,
2612
+ nextOffset: result.nextOffset,
2613
+ serverLowerBoundVersion: this.adapter.mergeVersions(frozen.baseCursor.serverLowerBoundVersion, frozen.batch.version)
2614
+ },
2615
+ producerAck: {
2616
+ epoch: result.ackEpoch,
2617
+ seq: result.ackSeq
2618
+ }
2035
2619
  };
2036
- return persist ? await this.saveRemoteCursor(next, "local") : next;
2620
+ frozen.acknowledged = appended;
2621
+ return appended;
2037
2622
  }
2038
2623
  if (result.kind === "seq-gap") {
2039
- producerSession = {
2040
- ...producerSession,
2624
+ frozen.producerSession = {
2625
+ ...frozen.producerSession,
2041
2626
  nextProducerSeq: result.expectedSeq
2042
2627
  };
2043
2628
  continue;
2044
2629
  }
2045
- producerSession = {
2046
- ...producerSession,
2630
+ frozen.producerSession = {
2631
+ ...frozen.producerSession,
2047
2632
  producerEpoch: result.serverEpoch + 1,
2048
2633
  nextProducerSeq: 0
2049
2634
  };
2050
2635
  }
2051
2636
  throw new Error("append retry budget exhausted");
2052
2637
  }
2638
+ async finalizeDirectLocalAppendIfNeeded(state) {
2639
+ const frozen = this.frozenLocalAppend;
2640
+ if (frozen == null || frozen.pendingCount != null) return state;
2641
+ const appended = await this.appendFrozenLocalBatchRemote(frozen);
2642
+ const saved = await this.saveRemoteCursor(appended.cursor, "local");
2643
+ this.commitProducerAck(appended.producerAck);
2644
+ return await this.catchup(saved, void 0, state.streamCursor);
2645
+ }
2646
+ createCurrentProducerSession() {
2647
+ return {
2648
+ producerId: this.producerId,
2649
+ producerEpoch: this.producerEpoch,
2650
+ nextProducerSeq: this.nextProducerSeq
2651
+ };
2652
+ }
2653
+ commitProducerAck(ack) {
2654
+ this.producerEpoch = ack.epoch;
2655
+ this.nextProducerSeq = ack.seq + 1;
2656
+ this.frozenLocalAppend = void 0;
2657
+ }
2658
+ abandonFrozenLocalAppend(frozen) {
2659
+ if (this.frozenLocalAppend !== frozen) return;
2660
+ this.frozenLocalAppend = void 0;
2661
+ this.producerEpoch = Math.max(this.producerEpoch, frozen.producerSession.producerEpoch + 1);
2662
+ this.nextProducerSeq = 0;
2663
+ }
2664
+ consumeCommittedPendingCount() {
2665
+ const count = this.committedPendingCount;
2666
+ this.committedPendingCount = void 0;
2667
+ return count;
2668
+ }
2053
2669
  /**
2054
2670
  * Ensures the target stream exists, creating it if necessary.
2055
2671
  * Tolerates 409 Conflict (stream already created concurrently).
@@ -2097,9 +2713,17 @@ var StreamsCrdt = class StreamsCrdt {
2097
2713
  */
2098
2714
  async inferRemoteAndAppendLocal(bootstrapCursor, cursorManager) {
2099
2715
  const inferredRemote = await this.bootstrapState(bootstrapCursor, { cursorManager });
2100
- const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
2101
- if (localBatch.batch != null) await this.appendLocalBatch(inferredRemote.cursor, localBatch.batch, false);
2102
- return await this.bootstrapState(bootstrapCursor);
2716
+ let pendingAppend;
2717
+ if (this.frozenLocalAppend != null) {
2718
+ if (this.frozenLocalAppend.pendingCount != null) throw new Error("pending local append must be finalized by pending flush");
2719
+ pendingAppend = await this.appendFrozenLocalBatchRemote(this.frozenLocalAppend);
2720
+ } else {
2721
+ const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
2722
+ if (localBatch.batch != null) pendingAppend = await this.appendLocalBatchRemote(inferredRemote.cursor, localBatch.batch);
2723
+ }
2724
+ const result = await this.bootstrapState(bootstrapCursor);
2725
+ if (pendingAppend != null) this.commitProducerAck(pendingAppend.producerAck);
2726
+ return result;
2103
2727
  }
2104
2728
  async recoverFromGoneWithLocalBootstrap(cursor) {
2105
2729
  const isolatedCursorManager = await this.createIsolatedCursorManager();
@@ -2115,9 +2739,17 @@ var StreamsCrdt = class StreamsCrdt {
2115
2739
  const inferredRemote = await this.bootstrapState(bootstrapCursor, { cursorManager: this.volatileCursorManager });
2116
2740
  await this.adapter.applySnapshot(preservedLocalSnapshot);
2117
2741
  restoredLocalSnapshot = true;
2118
- const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
2119
- if (localBatch.batch != null) await this.appendLocalBatch(inferredRemote.cursor, localBatch.batch, false);
2120
- return await this.bootstrapState(bootstrapCursor);
2742
+ let pendingAppend;
2743
+ if (this.frozenLocalAppend != null) {
2744
+ if (this.frozenLocalAppend.pendingCount != null) throw new Error("pending local append must be finalized by pending flush");
2745
+ pendingAppend = await this.appendFrozenLocalBatchRemote(this.frozenLocalAppend);
2746
+ } else {
2747
+ const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
2748
+ if (localBatch.batch != null) pendingAppend = await this.appendLocalBatchRemote(inferredRemote.cursor, localBatch.batch);
2749
+ }
2750
+ const result = await this.bootstrapState(bootstrapCursor);
2751
+ if (pendingAppend != null) this.commitProducerAck(pendingAppend.producerAck);
2752
+ return result;
2121
2753
  } catch (error) {
2122
2754
  if (!restoredLocalSnapshot) await this.adapter.applySnapshot(preservedLocalSnapshot);
2123
2755
  throw error;
@@ -2332,7 +2964,6 @@ var StreamsCrdt = class StreamsCrdt {
2332
2964
  let mergedVersion = first.version;
2333
2965
  const allUpdates = Array.from(first.updates);
2334
2966
  let count = 1;
2335
- await this.measureAppendBodyByteLength(allUpdates);
2336
2967
  while (count < pendingLocal.length) {
2337
2968
  const next = pendingLocal.at(count);
2338
2969
  const candidateUpdates = [...allUpdates, ...next.updates];
@@ -2368,9 +2999,10 @@ var StreamsCrdt = class StreamsCrdt {
2368
2999
  if (drained == null) return;
2369
3000
  let attempts = 0;
2370
3001
  while (!state.closed) try {
2371
- state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch));
2372
- for (let i = 0; i < drained.count; i += 1) state.pendingLocal.popFront();
2373
- this.notifySyncWaiters(state, drained.count);
3002
+ state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch, drained.count));
3003
+ const committedCount = this.consumeCommittedPendingCount() ?? drained.count;
3004
+ for (let i = 0; i < committedCount; i += 1) state.pendingLocal.popFront();
3005
+ this.notifySyncWaiters(state, committedCount);
2374
3006
  this.setWriteSubStatus(state, "ok");
2375
3007
  this.snapshotManager.schedule(state);
2376
3008
  break;
@@ -2465,6 +3097,8 @@ var StreamsCrdt = class StreamsCrdt {
2465
3097
  return updates.reduce((sum, update) => sum + 4 + update.byteLength, 0);
2466
3098
  }
2467
3099
  disableWriteOnlyMode(reason) {
3100
+ const frozen = this.frozenLocalAppend;
3101
+ if (frozen?.source === "write-only") this.abandonFrozenLocalAppend(frozen);
2468
3102
  if (this.writeOnlyDisabledReason != null) return;
2469
3103
  this.writeOnlyDisabledReason = reason;
2470
3104
  this.pendingWriteOnly.clear();
@@ -2522,6 +3156,6 @@ function createStreamUrl(input) {
2522
3156
  return `${baseUrl}/ds/${encodeURIComponent(input.bucketId)}/${encodeURIComponent(streamId)}`;
2523
3157
  }
2524
3158
  //#endregion
2525
- export { PayloadProtectionError as a, createInitialRemoteCursor as c, StreamsCrdt as i, isValidBucketId as n, InMemoryRemoteCursorStore as o, isValidRillId as r, IndexedDbRemoteCursorStore as s, createStreamUrl as t };
3159
+ export { InMemoryRemoteCursorStore as a, EphemeralStreamCrdt as c, StreamsCrdt as i, PayloadProtectionError as l, isValidBucketId as n, IndexedDbRemoteCursorStore as o, isValidRillId as r, createInitialRemoteCursor as s, createStreamUrl as t };
2526
3160
 
2527
- //# sourceMappingURL=stream-id-D0qcbI5c.js.map
3161
+ //# sourceMappingURL=stream-id-CF1egQDz.js.map