loro-repo 0.5.0 → 0.5.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexeddb.js","names":["doc: LoroDoc","consolidated: Uint8Array","queue: Uint8Array[]"],"sources":["../../src/storage/indexeddb.ts"],"sourcesContent":["import { Flock } from \"@loro-dev/flock\";\nimport { LoroDoc } from \"loro-crdt\";\n\nimport type {\n AssetId,\n StorageAdapter,\n StorageSavePayload,\n} from \"../types\";\nimport type { ExportBundle } from \"@loro-dev/flock\";\n\nconst DEFAULT_DB_NAME = \"loro-repo\";\nconst DEFAULT_DB_VERSION = 1;\nconst DEFAULT_DOC_STORE = \"docs\";\nconst DEFAULT_META_STORE = \"meta\";\nconst DEFAULT_ASSET_STORE = \"assets\";\nconst DEFAULT_DOC_UPDATE_STORE = \"doc-updates\";\nconst DEFAULT_META_KEY = \"snapshot\";\n\ntype EventListenerOptions = {\n once?: boolean;\n};\n\ntype IDBFactory = {\n open(name: string, version?: number): IDBOpenDBRequest;\n};\n\ntype IDBOpenDBRequest = {\n result: IDBDatabase;\n error: unknown;\n onupgradeneeded: ((event: unknown) => void) | null;\n onsuccess: ((event: unknown) => void) | null;\n onerror: ((event: unknown) => void) | null;\n addEventListener(\n type: string,\n listener: (event: unknown) => void,\n options?: EventListenerOptions,\n ): void;\n};\n\ntype ObjectStoreNames = {\n contains?(name: string): boolean;\n length?: number;\n item?(index: number): string | null;\n};\n\ntype IDBDatabase = {\n close(): void;\n createObjectStore(name: string): IDBObjectStore;\n transaction(\n storeName: string,\n mode?: IDBTransactionMode,\n ): IDBTransaction;\n objectStoreNames: ObjectStoreNames;\n};\n\ntype IDBTransactionMode = \"readonly\" | \"readwrite\";\n\ntype IDBTransaction = {\n objectStore(name: string): IDBObjectStore;\n oncomplete: ((event: unknown) => void) | null;\n onerror: ((event: unknown) => void) | null;\n onabort: ((event: unknown) => void) | null;\n error: unknown;\n addEventListener(\n type: string,\n listener: (event: unknown) => void,\n options?: EventListenerOptions,\n ): void;\n};\n\ntype IDBObjectStore = {\n put(value: unknown, key?: unknown): IDBRequest<unknown>;\n get(key: unknown): IDBRequest<unknown>;\n delete(key: unknown): IDBRequest<unknown>;\n};\n\ntype IDBRequest<T> = {\n onsuccess: ((event: unknown) => void) | null;\n onerror: ((event: unknown) => void) | null;\n result: T;\n error: unknown;\n addEventListener(\n type: string,\n listener: (event: unknown) => void,\n options?: EventListenerOptions,\n ): void;\n};\n\nconst textDecoder = new TextDecoder();\n\nfunction describeUnknown(cause: unknown): string {\n if (typeof cause === \"string\") return cause;\n if (typeof cause === \"number\" || typeof cause === \"boolean\") {\n return String(cause);\n }\n if (typeof cause === \"bigint\") {\n return cause.toString();\n }\n if (typeof cause === \"symbol\") {\n return cause.description ?? cause.toString();\n }\n if (typeof cause === \"function\") {\n return `[function ${cause.name ?? \"anonymous\"}]`;\n }\n if (cause && typeof cause === \"object\") {\n try {\n return JSON.stringify(cause);\n } catch {\n return \"[object]\";\n }\n }\n return String(cause);\n}\n\nexport interface IndexedDBStorageAdaptorOptions {\n readonly dbName?: string;\n readonly version?: number;\n readonly docStoreName?: string;\n readonly docUpdateStoreName?: string;\n readonly metaStoreName?: string;\n readonly assetStoreName?: string;\n readonly metaKey?: string;\n}\n\nexport class IndexedDBStorageAdaptor implements StorageAdapter {\n private readonly idb: IDBFactory;\n private readonly dbName: string;\n private readonly version: number;\n private readonly docStore: string;\n private readonly docUpdateStore: string;\n private readonly metaStore: string;\n private readonly assetStore: string;\n private readonly metaKey: string;\n private dbPromise?: Promise<IDBDatabase>;\n private closed = false;\n\n constructor(options: IndexedDBStorageAdaptorOptions = {}) {\n const idbFactory = (globalThis as { indexedDB?: IDBFactory }).indexedDB;\n if (!idbFactory) {\n throw new Error(\"IndexedDB is not available in this environment\");\n }\n this.idb = idbFactory;\n this.dbName = options.dbName ?? DEFAULT_DB_NAME;\n this.version = options.version ?? DEFAULT_DB_VERSION;\n this.docStore = options.docStoreName ?? DEFAULT_DOC_STORE;\n this.docUpdateStore = options.docUpdateStoreName ?? DEFAULT_DOC_UPDATE_STORE;\n this.metaStore = options.metaStoreName ?? DEFAULT_META_STORE;\n this.assetStore = options.assetStoreName ?? DEFAULT_ASSET_STORE;\n this.metaKey = options.metaKey ?? DEFAULT_META_KEY;\n }\n\n async save(payload: StorageSavePayload): Promise<void> {\n const db = await this.ensureDb();\n switch (payload.type) {\n case \"doc-snapshot\": {\n const snapshot = payload.snapshot.slice();\n await this.storeMergedSnapshot(db, payload.docId, snapshot);\n break;\n }\n case \"doc-update\": {\n const update = payload.update.slice();\n await this.appendDocUpdate(db, payload.docId, update);\n break;\n }\n case \"asset\": {\n const bytes = payload.data.slice();\n await this.putBinary(db, this.assetStore, payload.assetId, bytes);\n break;\n }\n case \"meta\": {\n const bytes = payload.update.slice();\n await this.putBinary(db, this.metaStore, this.metaKey, bytes);\n break;\n }\n default:\n throw new Error(\"Unsupported storage payload type\");\n }\n }\n\n async deleteAsset(assetId: AssetId): Promise<void> {\n const db = await this.ensureDb();\n await this.deleteKey(db, this.assetStore, assetId);\n }\n\n async loadDoc(docId: string): Promise<LoroDoc | undefined> {\n const db = await this.ensureDb();\n const snapshot = await this.getBinaryFromDb(db, this.docStore, docId);\n const pendingUpdates = await this.getDocUpdates(db, docId);\n\n if (!snapshot && pendingUpdates.length === 0) {\n return undefined;\n }\n\n let doc: LoroDoc;\n try {\n doc = snapshot ? LoroDoc.fromSnapshot(snapshot) : new LoroDoc();\n } catch (error) {\n throw this.createError(\n `Failed to hydrate document snapshot for \"${docId}\"`,\n error,\n );\n }\n\n let appliedUpdates = false;\n for (const update of pendingUpdates) {\n try {\n doc.import(update);\n appliedUpdates = true;\n } catch (error) {\n throw this.createError(\n `Failed to apply queued document update for \"${docId}\"`,\n error,\n );\n }\n }\n\n if (appliedUpdates) {\n let consolidated: Uint8Array;\n try {\n consolidated = doc.export({ mode: \"snapshot\" });\n } catch (error) {\n throw this.createError(\n `Failed to export consolidated snapshot for \"${docId}\"`,\n error,\n );\n }\n await this.writeSnapshot(db, docId, consolidated);\n await this.clearDocUpdates(db, docId);\n }\n\n return doc;\n }\n\n async loadMeta(): Promise<Flock | undefined> {\n const bytes = await this.getBinary(this.metaStore, this.metaKey);\n if (!bytes) return undefined;\n try {\n const json = textDecoder.decode(bytes);\n const bundle = JSON.parse(json) as ExportBundle;\n const flock = new Flock();\n flock.importJson(bundle);\n return flock;\n } catch (error) {\n throw this.createError(\"Failed to hydrate metadata snapshot\", error);\n }\n }\n\n async loadAsset(assetId: AssetId): Promise<Uint8Array | undefined> {\n const bytes = await this.getBinary(this.assetStore, assetId);\n return bytes ?? undefined;\n }\n\n async close(): Promise<void> {\n this.closed = true;\n const db = await this.dbPromise;\n if (db) {\n db.close();\n }\n this.dbPromise = undefined;\n }\n\n private async ensureDb(): Promise<IDBDatabase> {\n if (this.closed) {\n throw new Error(\"IndexedDBStorageAdaptor has been closed\");\n }\n if (!this.dbPromise) {\n this.dbPromise = new Promise((resolve, reject) => {\n const request = this.idb.open(this.dbName, this.version);\n request.addEventListener(\"upgradeneeded\", () => {\n const db = request.result;\n this.ensureStore(db, this.docStore);\n this.ensureStore(db, this.docUpdateStore);\n this.ensureStore(db, this.metaStore);\n this.ensureStore(db, this.assetStore);\n });\n request.addEventListener(\n \"success\",\n () => resolve(request.result),\n { once: true },\n );\n request.addEventListener(\n \"error\",\n () => {\n reject(\n this.createError(\n `Failed to open IndexedDB database \"${this.dbName}\"`,\n request.error,\n ),\n );\n },\n { once: true },\n );\n });\n }\n return this.dbPromise;\n }\n\n private ensureStore(db: IDBDatabase, storeName: string): void {\n const names = db.objectStoreNames;\n if (this.storeExists(names, storeName)) return;\n db.createObjectStore(storeName);\n }\n\n private storeExists(names: ObjectStoreNames, storeName: string): boolean {\n if (typeof names.contains === \"function\") {\n return names.contains(storeName);\n }\n const length = names.length ?? 0;\n for (let index = 0; index < length; index += 1) {\n const value = names.item?.(index);\n if (value === storeName) return true;\n }\n return false;\n }\n\n private async storeMergedSnapshot(\n db: IDBDatabase,\n docId: string,\n incoming: Uint8Array,\n ): Promise<void> {\n await this.runInTransaction(db, this.docStore, \"readwrite\", async (store) => {\n const existingRaw = await this.wrapRequest(store.get(docId), \"read\");\n const existing = await this.normalizeBinary(existingRaw);\n const merged = this.mergeSnapshots(docId, existing, incoming);\n await this.wrapRequest(store.put(merged, docId), \"write\");\n });\n }\n\n private mergeSnapshots(\n docId: string,\n existing: Uint8Array | undefined,\n incoming: Uint8Array,\n ): Uint8Array {\n try {\n const doc = existing ? LoroDoc.fromSnapshot(existing) : new LoroDoc();\n doc.import(incoming);\n return doc.export({ mode: \"snapshot\" });\n } catch (error) {\n throw this.createError(`Failed to merge snapshot for \"${docId}\"`, error);\n }\n }\n\n private async appendDocUpdate(\n db: IDBDatabase,\n docId: string,\n update: Uint8Array,\n ): Promise<void> {\n await this.runInTransaction(\n db,\n this.docUpdateStore,\n \"readwrite\",\n async (store) => {\n const raw = await this.wrapRequest(store.get(docId), \"read\");\n const queue = await this.normalizeUpdateQueue(raw);\n queue.push(update.slice());\n await this.wrapRequest(store.put({ updates: queue }, docId), \"write\");\n },\n );\n }\n\n private async getDocUpdates(\n db: IDBDatabase,\n docId: string,\n ): Promise<Uint8Array[]> {\n const raw = await this.runInTransaction(\n db,\n this.docUpdateStore,\n \"readonly\",\n (store) => this.wrapRequest(store.get(docId), \"read\"),\n );\n return this.normalizeUpdateQueue(raw);\n }\n\n private async clearDocUpdates(\n db: IDBDatabase,\n docId: string,\n ): Promise<void> {\n await this.runInTransaction(\n db,\n this.docUpdateStore,\n \"readwrite\",\n (store) => this.wrapRequest(store.delete(docId), \"delete\"),\n );\n }\n\n private async writeSnapshot(\n db: IDBDatabase,\n docId: string,\n snapshot: Uint8Array,\n ): Promise<void> {\n await this.putBinary(db, this.docStore, docId, snapshot.slice());\n }\n\n private async getBinaryFromDb(\n db: IDBDatabase,\n storeName: string,\n key: string,\n ): Promise<Uint8Array | undefined> {\n const value = await this.runInTransaction(\n db,\n storeName,\n \"readonly\",\n (store) => this.wrapRequest(store.get(key), \"read\"),\n );\n return this.normalizeBinary(value);\n }\n\n private async normalizeUpdateQueue(value: unknown): Promise<Uint8Array[]> {\n if (value == null) return [];\n const list = Array.isArray(value)\n ? value\n : typeof value === \"object\" && value !== null\n ? (value as { updates?: unknown }).updates\n : undefined;\n\n if (!Array.isArray(list)) return [];\n\n const queue: Uint8Array[] = [];\n for (const entry of list) {\n const bytes = await this.normalizeBinary(entry);\n if (bytes) {\n queue.push(bytes);\n }\n }\n return queue;\n }\n\n private async putBinary(\n db: IDBDatabase,\n storeName: string,\n key: string,\n value: Uint8Array,\n ): Promise<void> {\n await this.runInTransaction(db, storeName, \"readwrite\", (store) =>\n this.wrapRequest(store.put(value, key), \"write\"),\n );\n }\n\n private async deleteKey(\n db: IDBDatabase,\n storeName: string,\n key: string,\n ): Promise<void> {\n await this.runInTransaction(db, storeName, \"readwrite\", (store) =>\n this.wrapRequest(store.delete(key), \"delete\"),\n );\n }\n\n private async getBinary(\n storeName: string,\n key: string,\n ): Promise<Uint8Array | undefined> {\n const db = await this.ensureDb();\n return this.getBinaryFromDb(db, storeName, key);\n }\n\n private runInTransaction<T>(\n db: IDBDatabase,\n storeName: string,\n mode: IDBTransactionMode,\n executor: (store: IDBObjectStore) => Promise<T>,\n ): Promise<T> {\n const tx = db.transaction(storeName, mode);\n const store = tx.objectStore(storeName);\n const completion = new Promise<void>((resolve, reject) => {\n tx.addEventListener(\n \"complete\",\n () => resolve(),\n { once: true },\n );\n tx.addEventListener(\n \"abort\",\n () =>\n reject(\n this.createError(\"IndexedDB transaction aborted\", tx.error),\n ),\n { once: true },\n );\n tx.addEventListener(\n \"error\",\n () =>\n reject(\n this.createError(\"IndexedDB transaction failed\", tx.error),\n ),\n { once: true },\n );\n });\n return Promise.all([executor(store), completion]).then(([result]) => result);\n }\n\n private wrapRequest<T>(\n request: IDBRequest<T>,\n action: string,\n ): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n request.addEventListener(\n \"success\",\n () => resolve(request.result),\n { once: true },\n );\n request.addEventListener(\n \"error\",\n () =>\n reject(\n this.createError(\n `IndexedDB request failed during ${action}`,\n request.error,\n ),\n ),\n { once: true },\n );\n });\n }\n\n private async normalizeBinary(value: unknown): Promise<Uint8Array | undefined> {\n if (value == null) return undefined;\n if (value instanceof Uint8Array) {\n return value.slice();\n }\n if (ArrayBuffer.isView(value)) {\n return new Uint8Array(\n value.buffer,\n value.byteOffset,\n value.byteLength,\n ).slice();\n }\n if (value instanceof ArrayBuffer) {\n return new Uint8Array(value.slice(0));\n }\n if (\n typeof value === \"object\" &&\n value !== null &&\n \"arrayBuffer\" in value\n ) {\n const candidate = value as {\n arrayBuffer?: unknown;\n };\n if (typeof candidate.arrayBuffer === \"function\") {\n const buffer = await candidate.arrayBuffer();\n return new Uint8Array(buffer);\n }\n }\n return undefined;\n }\n\n private createError(message: string, cause: unknown): Error {\n if (cause instanceof Error) {\n return new Error(`${message}: ${cause.message}`, { cause });\n }\n if (cause !== undefined && cause !== null) {\n return new Error(`${message}: ${describeUnknown(cause)}`);\n }\n return new Error(message);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,252 @@
1
+
2
+ //#region src/transport/broadcast-channel.ts
3
+ function deferred() {
4
+ let resolve;
5
+ return {
6
+ promise: new Promise((res) => {
7
+ resolve = res;
8
+ }),
9
+ resolve
10
+ };
11
+ }
12
+ function randomInstanceId() {
13
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
14
+ return Math.random().toString(36).slice(2);
15
+ }
16
+ function ensureBroadcastChannel() {
17
+ if (typeof BroadcastChannel === "undefined") throw new Error("BroadcastChannel API is not available in this environment");
18
+ return BroadcastChannel;
19
+ }
20
+ function encodeDocChannelId(docId) {
21
+ try {
22
+ return encodeURIComponent(docId);
23
+ } catch {
24
+ return docId.replace(/[^a-z0-9_-]/gi, "_");
25
+ }
26
+ }
27
+ function postChannelMessage(channel, message) {
28
+ channel.postMessage(message);
29
+ }
30
+ /**
31
+ * TransportAdapter that relies on the BroadcastChannel API to fan out metadata
32
+ * and document updates between browser tabs within the same origin.
33
+ */
34
+ var BroadcastChannelTransportAdapter = class {
35
+ instanceId = randomInstanceId();
36
+ namespace;
37
+ metaChannelName;
38
+ connected = false;
39
+ metaState;
40
+ docStates = /* @__PURE__ */ new Map();
41
+ constructor(options = {}) {
42
+ ensureBroadcastChannel();
43
+ this.namespace = options.namespace ?? "loro-repo";
44
+ this.metaChannelName = options.metaChannelName ?? `${this.namespace}-meta`;
45
+ }
46
+ async connect() {
47
+ this.connected = true;
48
+ }
49
+ async close() {
50
+ this.connected = false;
51
+ if (this.metaState) {
52
+ for (const entry of this.metaState.listeners) entry.unsubscribe();
53
+ this.metaState.channel.close();
54
+ this.metaState = void 0;
55
+ }
56
+ for (const [docId] of this.docStates) this.teardownDocChannel(docId);
57
+ this.docStates.clear();
58
+ }
59
+ isConnected() {
60
+ return this.connected;
61
+ }
62
+ async syncMeta(flock, _options) {
63
+ const subscription = this.joinMetaRoom(flock);
64
+ subscription.firstSyncedWithRemote.catch(() => void 0);
65
+ await subscription.firstSyncedWithRemote;
66
+ subscription.unsubscribe();
67
+ return { ok: true };
68
+ }
69
+ joinMetaRoom(flock, _params) {
70
+ const state = this.ensureMetaChannel();
71
+ const { promise, resolve } = deferred();
72
+ const listener = {
73
+ flock,
74
+ muted: false,
75
+ unsubscribe: flock.subscribe(() => {
76
+ if (listener.muted) return;
77
+ Promise.resolve(flock.exportJson()).then((bundle) => {
78
+ postChannelMessage(state.channel, {
79
+ kind: "meta-export",
80
+ from: this.instanceId,
81
+ bundle
82
+ });
83
+ });
84
+ }),
85
+ resolveFirst: resolve,
86
+ firstSynced: promise
87
+ };
88
+ state.listeners.add(listener);
89
+ postChannelMessage(state.channel, {
90
+ kind: "meta-request",
91
+ from: this.instanceId
92
+ });
93
+ Promise.resolve(flock.exportJson()).then((bundle) => {
94
+ postChannelMessage(state.channel, {
95
+ kind: "meta-export",
96
+ from: this.instanceId,
97
+ bundle
98
+ });
99
+ });
100
+ queueMicrotask(() => resolve());
101
+ return {
102
+ unsubscribe: () => {
103
+ listener.unsubscribe();
104
+ state.listeners.delete(listener);
105
+ if (!state.listeners.size) {
106
+ state.channel.removeEventListener("message", state.onMessage);
107
+ state.channel.close();
108
+ this.metaState = void 0;
109
+ }
110
+ },
111
+ firstSyncedWithRemote: listener.firstSynced,
112
+ get connected() {
113
+ return true;
114
+ }
115
+ };
116
+ }
117
+ async syncDoc(docId, doc, _options) {
118
+ const subscription = this.joinDocRoom(docId, doc);
119
+ subscription.firstSyncedWithRemote.catch(() => void 0);
120
+ await subscription.firstSyncedWithRemote;
121
+ subscription.unsubscribe();
122
+ return { ok: true };
123
+ }
124
+ joinDocRoom(docId, doc, _params) {
125
+ const state = this.ensureDocChannel(docId);
126
+ const { promise, resolve } = deferred();
127
+ const listener = {
128
+ doc,
129
+ muted: false,
130
+ unsubscribe: doc.subscribe(() => {
131
+ if (listener.muted) return;
132
+ const payload = doc.export({ mode: "update" });
133
+ postChannelMessage(state.channel, {
134
+ kind: "doc-update",
135
+ docId,
136
+ from: this.instanceId,
137
+ mode: "update",
138
+ payload
139
+ });
140
+ }),
141
+ resolveFirst: resolve,
142
+ firstSynced: promise
143
+ };
144
+ state.listeners.add(listener);
145
+ postChannelMessage(state.channel, {
146
+ kind: "doc-request",
147
+ docId,
148
+ from: this.instanceId
149
+ });
150
+ postChannelMessage(state.channel, {
151
+ kind: "doc-update",
152
+ docId,
153
+ from: this.instanceId,
154
+ mode: "snapshot",
155
+ payload: doc.export({ mode: "snapshot" })
156
+ });
157
+ queueMicrotask(() => resolve());
158
+ return {
159
+ unsubscribe: () => {
160
+ listener.unsubscribe();
161
+ state.listeners.delete(listener);
162
+ if (!state.listeners.size) this.teardownDocChannel(docId);
163
+ },
164
+ firstSyncedWithRemote: listener.firstSynced,
165
+ get connected() {
166
+ return true;
167
+ }
168
+ };
169
+ }
170
+ ensureMetaChannel() {
171
+ if (this.metaState) return this.metaState;
172
+ const channel = new (ensureBroadcastChannel())(this.metaChannelName);
173
+ const listeners = /* @__PURE__ */ new Set();
174
+ const onMessage = (event) => {
175
+ const message = event.data;
176
+ if (!message || message.from === this.instanceId) return;
177
+ if (message.kind === "meta-export") for (const entry of listeners) {
178
+ entry.muted = true;
179
+ entry.flock.importJson(message.bundle);
180
+ entry.muted = false;
181
+ entry.resolveFirst();
182
+ }
183
+ else if (message.kind === "meta-request") {
184
+ const first = listeners.values().next().value;
185
+ if (!first) return;
186
+ Promise.resolve(first.flock.exportJson()).then((bundle) => {
187
+ postChannelMessage(channel, {
188
+ kind: "meta-export",
189
+ from: this.instanceId,
190
+ bundle
191
+ });
192
+ });
193
+ }
194
+ };
195
+ channel.addEventListener("message", onMessage);
196
+ this.metaState = {
197
+ channel,
198
+ listeners,
199
+ onMessage
200
+ };
201
+ return this.metaState;
202
+ }
203
+ ensureDocChannel(docId) {
204
+ const existing = this.docStates.get(docId);
205
+ if (existing) return existing;
206
+ const channel = new (ensureBroadcastChannel())(`${this.namespace}-doc-${encodeDocChannelId(docId)}`);
207
+ const listeners = /* @__PURE__ */ new Set();
208
+ const onMessage = (event) => {
209
+ const message = event.data;
210
+ if (!message || message.from === this.instanceId) return;
211
+ if (message.kind === "doc-update") for (const entry of listeners) {
212
+ entry.muted = true;
213
+ entry.doc.import(message.payload);
214
+ entry.muted = false;
215
+ entry.resolveFirst();
216
+ }
217
+ else if (message.kind === "doc-request") {
218
+ const first = listeners.values().next().value;
219
+ if (!first) return;
220
+ const payload = message.docId === docId ? first.doc.export({ mode: "snapshot" }) : void 0;
221
+ if (!payload) return;
222
+ postChannelMessage(channel, {
223
+ kind: "doc-update",
224
+ docId,
225
+ from: this.instanceId,
226
+ mode: "snapshot",
227
+ payload
228
+ });
229
+ }
230
+ };
231
+ channel.addEventListener("message", onMessage);
232
+ const state = {
233
+ channel,
234
+ listeners,
235
+ onMessage
236
+ };
237
+ this.docStates.set(docId, state);
238
+ return state;
239
+ }
240
+ teardownDocChannel(docId) {
241
+ const state = this.docStates.get(docId);
242
+ if (!state) return;
243
+ for (const entry of state.listeners) entry.unsubscribe();
244
+ state.channel.removeEventListener("message", state.onMessage);
245
+ state.channel.close();
246
+ this.docStates.delete(docId);
247
+ }
248
+ };
249
+
250
+ //#endregion
251
+ exports.BroadcastChannelTransportAdapter = BroadcastChannelTransportAdapter;
252
+ //# sourceMappingURL=broadcast-channel.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broadcast-channel.cjs","names":["resolve!: () => void","listener: MetaListener","listener: DocListener","state: DocChannelState"],"sources":["../../src/transport/broadcast-channel.ts"],"sourcesContent":["import { Flock } from \"@loro-dev/flock\";\nimport { LoroDoc } from \"loro-crdt\";\nimport type {\n TransportAdapter,\n TransportJoinParams,\n TransportSubscription,\n TransportSyncResult,\n} from \"../types\";\n\ntype BroadcastChannelMessageEvent<T = unknown> = {\n data: T;\n};\n\ninterface BroadcastChannelLike {\n readonly name?: string;\n onmessage?: ((event: BroadcastChannelMessageEvent) => void) | null;\n onmessageerror?: ((event: BroadcastChannelMessageEvent) => void) | null;\n postMessage(message: unknown): void;\n addEventListener(\n type: \"message\",\n listener: (event: BroadcastChannelMessageEvent) => void,\n ): void;\n removeEventListener(\n type: \"message\",\n listener: (event: BroadcastChannelMessageEvent) => void,\n ): void;\n close(): void;\n}\n\ntype BroadcastChannelConstructor = new (name: string) => BroadcastChannelLike;\n\ndeclare const BroadcastChannel:\n | BroadcastChannelConstructor\n | undefined;\n\ntype FlockExport = Awaited<ReturnType<Flock[\"exportJson\"]>>;\n\ntype BroadcastMessage =\n | {\n kind: \"meta-export\";\n from: string;\n bundle: FlockExport;\n }\n | {\n kind: \"meta-request\";\n from: string;\n };\n\ntype DocMessage =\n | {\n kind: \"doc-update\";\n docId: string;\n from: string;\n mode: \"snapshot\" | \"update\";\n payload: Uint8Array;\n }\n | {\n kind: \"doc-request\";\n docId: string;\n from: string;\n };\n\ntype MetaListener = {\n flock: Flock;\n unsubscribe: () => void;\n muted: boolean;\n resolveFirst: () => void;\n firstSynced: Promise<void>;\n};\n\ntype MetaChannelState = {\n channel: BroadcastChannelLike;\n listeners: Set<MetaListener>;\n onMessage: (event: BroadcastChannelMessageEvent) => void;\n};\n\ntype DocListener = {\n doc: LoroDoc;\n unsubscribe: () => void;\n muted: boolean;\n resolveFirst: () => void;\n firstSynced: Promise<void>;\n};\n\ntype DocChannelState = {\n channel: BroadcastChannelLike;\n listeners: Set<DocListener>;\n onMessage: (event: BroadcastChannelMessageEvent) => void;\n};\n\nfunction deferred(): {\n promise: Promise<void>;\n resolve: () => void;\n} {\n let resolve!: () => void;\n const promise = new Promise<void>((res) => {\n resolve = res;\n });\n return { promise, resolve };\n}\n\nfunction randomInstanceId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return Math.random().toString(36).slice(2);\n}\n\nfunction ensureBroadcastChannel(): BroadcastChannelConstructor {\n if (typeof BroadcastChannel === \"undefined\") {\n throw new Error(\"BroadcastChannel API is not available in this environment\");\n }\n return BroadcastChannel;\n}\n\nfunction encodeDocChannelId(docId: string): string {\n try {\n return encodeURIComponent(docId);\n } catch {\n return docId.replace(/[^a-z0-9_-]/gi, \"_\");\n }\n}\n\nfunction postChannelMessage(\n channel: BroadcastChannelLike,\n message: BroadcastMessage | DocMessage,\n): void {\n // BroadcastChannel.postMessage does not accept targetOrigin, so we intentionally\n // bypass the unicorn/require-post-message-target-origin rule here.\n // eslint-disable-next-line unicorn/require-post-message-target-origin\n channel.postMessage(message);\n}\n\nexport interface BroadcastChannelTransportOptions {\n /**\n * Namespace used to derive broadcast channel names. Defaults to `loro-repo`.\n */\n readonly namespace?: string;\n /**\n * Explicit channel name for metadata broadcasts. When omitted, resolves to `${namespace}-meta`.\n */\n readonly metaChannelName?: string;\n}\n\n/**\n * TransportAdapter that relies on the BroadcastChannel API to fan out metadata\n * and document updates between browser tabs within the same origin.\n */\nexport class BroadcastChannelTransportAdapter implements TransportAdapter {\n private readonly instanceId = randomInstanceId();\n private readonly namespace: string;\n private readonly metaChannelName: string;\n private connected = false;\n\n private metaState?: MetaChannelState;\n private readonly docStates = new Map<string, DocChannelState>();\n\n constructor(options: BroadcastChannelTransportOptions = {}) {\n ensureBroadcastChannel();\n this.namespace = options.namespace ?? \"loro-repo\";\n this.metaChannelName = options.metaChannelName ?? `${this.namespace}-meta`;\n }\n\n async connect(): Promise<void> {\n this.connected = true;\n }\n\n async close(): Promise<void> {\n this.connected = false;\n if (this.metaState) {\n for (const entry of this.metaState.listeners) {\n entry.unsubscribe();\n }\n this.metaState.channel.close();\n this.metaState = undefined;\n }\n for (const [docId] of this.docStates) {\n this.teardownDocChannel(docId);\n }\n this.docStates.clear();\n }\n\n isConnected(): boolean {\n return this.connected;\n }\n\n async syncMeta(\n flock: Flock,\n _options?: { timeout?: number },\n ): Promise<TransportSyncResult> {\n const subscription = this.joinMetaRoom(flock);\n subscription.firstSyncedWithRemote.catch(() => undefined);\n await subscription.firstSyncedWithRemote;\n subscription.unsubscribe();\n return { ok: true };\n }\n\n joinMetaRoom(\n flock: Flock,\n _params?: TransportJoinParams,\n ): TransportSubscription {\n const state = this.ensureMetaChannel();\n const { promise, resolve } = deferred();\n const listener: MetaListener = {\n flock,\n muted: false,\n unsubscribe: flock.subscribe(() => {\n if (listener.muted) return;\n void Promise.resolve(flock.exportJson()).then((bundle) => {\n postChannelMessage(state.channel, {\n kind: \"meta-export\",\n from: this.instanceId,\n bundle,\n } satisfies BroadcastMessage);\n });\n }),\n resolveFirst: resolve,\n firstSynced: promise,\n };\n state.listeners.add(listener);\n\n // Request current state from peers and share our snapshot.\n postChannelMessage(state.channel, {\n kind: \"meta-request\",\n from: this.instanceId,\n } satisfies BroadcastMessage);\n void Promise.resolve(flock.exportJson()).then((bundle) => {\n postChannelMessage(state.channel, {\n kind: \"meta-export\",\n from: this.instanceId,\n bundle,\n } satisfies BroadcastMessage);\n });\n\n // Resolve immediately if nothing arrives.\n queueMicrotask(() => resolve());\n\n const subscription: TransportSubscription = {\n unsubscribe: () => {\n listener.unsubscribe();\n state.listeners.delete(listener);\n if (!state.listeners.size) {\n state.channel.removeEventListener(\"message\", state.onMessage);\n state.channel.close();\n this.metaState = undefined;\n }\n },\n firstSyncedWithRemote: listener.firstSynced,\n get connected() {\n return true;\n },\n };\n return subscription;\n }\n\n async syncDoc(\n docId: string,\n doc: LoroDoc,\n _options?: { timeout?: number },\n ): Promise<TransportSyncResult> {\n const subscription = this.joinDocRoom(docId, doc);\n subscription.firstSyncedWithRemote.catch(() => undefined);\n await subscription.firstSyncedWithRemote;\n subscription.unsubscribe();\n return { ok: true };\n }\n\n joinDocRoom(\n docId: string,\n doc: LoroDoc,\n _params?: TransportJoinParams,\n ): TransportSubscription {\n const state = this.ensureDocChannel(docId);\n const { promise, resolve } = deferred();\n const listener: DocListener = {\n doc,\n muted: false,\n unsubscribe: doc.subscribe(() => {\n if (listener.muted) return;\n const payload = doc.export({ mode: \"update\" });\n postChannelMessage(state.channel, {\n kind: \"doc-update\",\n docId,\n from: this.instanceId,\n mode: \"update\",\n payload,\n } satisfies DocMessage);\n }),\n resolveFirst: resolve,\n firstSynced: promise,\n };\n state.listeners.add(listener);\n\n postChannelMessage(state.channel, {\n kind: \"doc-request\",\n docId,\n from: this.instanceId,\n } satisfies DocMessage);\n postChannelMessage(state.channel, {\n kind: \"doc-update\",\n docId,\n from: this.instanceId,\n mode: \"snapshot\",\n payload: doc.export({ mode: \"snapshot\" }),\n } satisfies DocMessage);\n\n queueMicrotask(() => resolve());\n\n const subscription: TransportSubscription = {\n unsubscribe: () => {\n listener.unsubscribe();\n state.listeners.delete(listener);\n if (!state.listeners.size) {\n this.teardownDocChannel(docId);\n }\n },\n firstSyncedWithRemote: listener.firstSynced,\n get connected() {\n return true;\n },\n };\n return subscription;\n }\n\n private ensureMetaChannel(): MetaChannelState {\n if (this.metaState) {\n return this.metaState;\n }\n const Channel = ensureBroadcastChannel();\n const channel = new Channel(this.metaChannelName);\n const listeners = new Set<MetaListener>();\n const onMessage = (event: BroadcastChannelMessageEvent) => {\n const message = event.data as BroadcastMessage | undefined;\n if (!message || message.from === this.instanceId) {\n return;\n }\n if (message.kind === \"meta-export\") {\n for (const entry of listeners) {\n entry.muted = true;\n entry.flock.importJson(message.bundle);\n entry.muted = false;\n entry.resolveFirst();\n }\n } else if (message.kind === \"meta-request\") {\n const first = listeners.values().next().value;\n if (!first) return;\n void Promise.resolve(first.flock.exportJson()).then((bundle) => {\n postChannelMessage(channel, {\n kind: \"meta-export\",\n from: this.instanceId,\n bundle,\n } satisfies BroadcastMessage);\n });\n }\n };\n channel.addEventListener(\"message\", onMessage);\n this.metaState = { channel, listeners, onMessage };\n return this.metaState;\n }\n\n private ensureDocChannel(docId: string): DocChannelState {\n const existing = this.docStates.get(docId);\n if (existing) return existing;\n const Channel = ensureBroadcastChannel();\n const channelName = `${this.namespace}-doc-${encodeDocChannelId(docId)}`;\n const channel = new Channel(channelName);\n const listeners = new Set<DocListener>();\n const onMessage = (event: BroadcastChannelMessageEvent) => {\n const message = event.data as DocMessage | undefined;\n if (!message || message.from === this.instanceId) {\n return;\n }\n if (message.kind === \"doc-update\") {\n for (const entry of listeners) {\n entry.muted = true;\n entry.doc.import(message.payload);\n entry.muted = false;\n entry.resolveFirst();\n }\n } else if (message.kind === \"doc-request\") {\n const first = listeners.values().next().value;\n if (!first) return;\n const payload =\n message.docId === docId\n ? first.doc.export({ mode: \"snapshot\" })\n : undefined;\n if (!payload) return;\n postChannelMessage(channel, {\n kind: \"doc-update\",\n docId,\n from: this.instanceId,\n mode: \"snapshot\",\n payload,\n } satisfies DocMessage);\n }\n };\n channel.addEventListener(\"message\", onMessage);\n const state: DocChannelState = { channel, listeners, onMessage };\n this.docStates.set(docId, state);\n return state;\n }\n\n private teardownDocChannel(docId: string): void {\n const state = this.docStates.get(docId);\n if (!state) return;\n for (const entry of state.listeners) {\n entry.unsubscribe();\n }\n state.channel.removeEventListener(\"message\", state.onMessage);\n state.channel.close();\n this.docStates.delete(docId);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import { A as TransportSyncResult, D as TransportJoinParams, E as TransportAdapter, O as TransportSubscription } from "../types.cjs";
2
+ import { Flock } from "@loro-dev/flock";
3
+ import { LoroDoc } from "loro-crdt";
4
+
5
+ //#region src/transport/broadcast-channel.d.ts
6
+ interface BroadcastChannelTransportOptions {
7
+ /**
8
+ * Namespace used to derive broadcast channel names. Defaults to `loro-repo`.
9
+ */
10
+ readonly namespace?: string;
11
+ /**
12
+ * Explicit channel name for metadata broadcasts. When omitted, resolves to `${namespace}-meta`.
13
+ */
14
+ readonly metaChannelName?: string;
15
+ }
16
+ /**
17
+ * TransportAdapter that relies on the BroadcastChannel API to fan out metadata
18
+ * and document updates between browser tabs within the same origin.
19
+ */
20
+ declare class BroadcastChannelTransportAdapter implements TransportAdapter {
21
+ private readonly instanceId;
22
+ private readonly namespace;
23
+ private readonly metaChannelName;
24
+ private connected;
25
+ private metaState?;
26
+ private readonly docStates;
27
+ constructor(options?: BroadcastChannelTransportOptions);
28
+ connect(): Promise<void>;
29
+ close(): Promise<void>;
30
+ isConnected(): boolean;
31
+ syncMeta(flock: Flock, _options?: {
32
+ timeout?: number;
33
+ }): Promise<TransportSyncResult>;
34
+ joinMetaRoom(flock: Flock, _params?: TransportJoinParams): TransportSubscription;
35
+ syncDoc(docId: string, doc: LoroDoc, _options?: {
36
+ timeout?: number;
37
+ }): Promise<TransportSyncResult>;
38
+ joinDocRoom(docId: string, doc: LoroDoc, _params?: TransportJoinParams): TransportSubscription;
39
+ private ensureMetaChannel;
40
+ private ensureDocChannel;
41
+ private teardownDocChannel;
42
+ }
43
+ //#endregion
44
+ export { BroadcastChannelTransportAdapter, BroadcastChannelTransportOptions };
45
+ //# sourceMappingURL=broadcast-channel.d.cts.map
@@ -0,0 +1,45 @@
1
+ import { A as TransportSyncResult, D as TransportJoinParams, E as TransportAdapter, O as TransportSubscription } from "../types.js";
2
+ import { Flock } from "@loro-dev/flock";
3
+ import { LoroDoc } from "loro-crdt";
4
+
5
+ //#region src/transport/broadcast-channel.d.ts
6
+ interface BroadcastChannelTransportOptions {
7
+ /**
8
+ * Namespace used to derive broadcast channel names. Defaults to `loro-repo`.
9
+ */
10
+ readonly namespace?: string;
11
+ /**
12
+ * Explicit channel name for metadata broadcasts. When omitted, resolves to `${namespace}-meta`.
13
+ */
14
+ readonly metaChannelName?: string;
15
+ }
16
+ /**
17
+ * TransportAdapter that relies on the BroadcastChannel API to fan out metadata
18
+ * and document updates between browser tabs within the same origin.
19
+ */
20
+ declare class BroadcastChannelTransportAdapter implements TransportAdapter {
21
+ private readonly instanceId;
22
+ private readonly namespace;
23
+ private readonly metaChannelName;
24
+ private connected;
25
+ private metaState?;
26
+ private readonly docStates;
27
+ constructor(options?: BroadcastChannelTransportOptions);
28
+ connect(): Promise<void>;
29
+ close(): Promise<void>;
30
+ isConnected(): boolean;
31
+ syncMeta(flock: Flock, _options?: {
32
+ timeout?: number;
33
+ }): Promise<TransportSyncResult>;
34
+ joinMetaRoom(flock: Flock, _params?: TransportJoinParams): TransportSubscription;
35
+ syncDoc(docId: string, doc: LoroDoc, _options?: {
36
+ timeout?: number;
37
+ }): Promise<TransportSyncResult>;
38
+ joinDocRoom(docId: string, doc: LoroDoc, _params?: TransportJoinParams): TransportSubscription;
39
+ private ensureMetaChannel;
40
+ private ensureDocChannel;
41
+ private teardownDocChannel;
42
+ }
43
+ //#endregion
44
+ export { BroadcastChannelTransportAdapter, BroadcastChannelTransportOptions };
45
+ //# sourceMappingURL=broadcast-channel.d.ts.map
@@ -0,0 +1,251 @@
1
+ //#region src/transport/broadcast-channel.ts
2
+ function deferred() {
3
+ let resolve;
4
+ return {
5
+ promise: new Promise((res) => {
6
+ resolve = res;
7
+ }),
8
+ resolve
9
+ };
10
+ }
11
+ function randomInstanceId() {
12
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
13
+ return Math.random().toString(36).slice(2);
14
+ }
15
+ function ensureBroadcastChannel() {
16
+ if (typeof BroadcastChannel === "undefined") throw new Error("BroadcastChannel API is not available in this environment");
17
+ return BroadcastChannel;
18
+ }
19
+ function encodeDocChannelId(docId) {
20
+ try {
21
+ return encodeURIComponent(docId);
22
+ } catch {
23
+ return docId.replace(/[^a-z0-9_-]/gi, "_");
24
+ }
25
+ }
26
+ function postChannelMessage(channel, message) {
27
+ channel.postMessage(message);
28
+ }
29
+ /**
30
+ * TransportAdapter that relies on the BroadcastChannel API to fan out metadata
31
+ * and document updates between browser tabs within the same origin.
32
+ */
33
+ var BroadcastChannelTransportAdapter = class {
34
+ instanceId = randomInstanceId();
35
+ namespace;
36
+ metaChannelName;
37
+ connected = false;
38
+ metaState;
39
+ docStates = /* @__PURE__ */ new Map();
40
+ constructor(options = {}) {
41
+ ensureBroadcastChannel();
42
+ this.namespace = options.namespace ?? "loro-repo";
43
+ this.metaChannelName = options.metaChannelName ?? `${this.namespace}-meta`;
44
+ }
45
+ async connect() {
46
+ this.connected = true;
47
+ }
48
+ async close() {
49
+ this.connected = false;
50
+ if (this.metaState) {
51
+ for (const entry of this.metaState.listeners) entry.unsubscribe();
52
+ this.metaState.channel.close();
53
+ this.metaState = void 0;
54
+ }
55
+ for (const [docId] of this.docStates) this.teardownDocChannel(docId);
56
+ this.docStates.clear();
57
+ }
58
+ isConnected() {
59
+ return this.connected;
60
+ }
61
+ async syncMeta(flock, _options) {
62
+ const subscription = this.joinMetaRoom(flock);
63
+ subscription.firstSyncedWithRemote.catch(() => void 0);
64
+ await subscription.firstSyncedWithRemote;
65
+ subscription.unsubscribe();
66
+ return { ok: true };
67
+ }
68
+ joinMetaRoom(flock, _params) {
69
+ const state = this.ensureMetaChannel();
70
+ const { promise, resolve } = deferred();
71
+ const listener = {
72
+ flock,
73
+ muted: false,
74
+ unsubscribe: flock.subscribe(() => {
75
+ if (listener.muted) return;
76
+ Promise.resolve(flock.exportJson()).then((bundle) => {
77
+ postChannelMessage(state.channel, {
78
+ kind: "meta-export",
79
+ from: this.instanceId,
80
+ bundle
81
+ });
82
+ });
83
+ }),
84
+ resolveFirst: resolve,
85
+ firstSynced: promise
86
+ };
87
+ state.listeners.add(listener);
88
+ postChannelMessage(state.channel, {
89
+ kind: "meta-request",
90
+ from: this.instanceId
91
+ });
92
+ Promise.resolve(flock.exportJson()).then((bundle) => {
93
+ postChannelMessage(state.channel, {
94
+ kind: "meta-export",
95
+ from: this.instanceId,
96
+ bundle
97
+ });
98
+ });
99
+ queueMicrotask(() => resolve());
100
+ return {
101
+ unsubscribe: () => {
102
+ listener.unsubscribe();
103
+ state.listeners.delete(listener);
104
+ if (!state.listeners.size) {
105
+ state.channel.removeEventListener("message", state.onMessage);
106
+ state.channel.close();
107
+ this.metaState = void 0;
108
+ }
109
+ },
110
+ firstSyncedWithRemote: listener.firstSynced,
111
+ get connected() {
112
+ return true;
113
+ }
114
+ };
115
+ }
116
+ async syncDoc(docId, doc, _options) {
117
+ const subscription = this.joinDocRoom(docId, doc);
118
+ subscription.firstSyncedWithRemote.catch(() => void 0);
119
+ await subscription.firstSyncedWithRemote;
120
+ subscription.unsubscribe();
121
+ return { ok: true };
122
+ }
123
+ joinDocRoom(docId, doc, _params) {
124
+ const state = this.ensureDocChannel(docId);
125
+ const { promise, resolve } = deferred();
126
+ const listener = {
127
+ doc,
128
+ muted: false,
129
+ unsubscribe: doc.subscribe(() => {
130
+ if (listener.muted) return;
131
+ const payload = doc.export({ mode: "update" });
132
+ postChannelMessage(state.channel, {
133
+ kind: "doc-update",
134
+ docId,
135
+ from: this.instanceId,
136
+ mode: "update",
137
+ payload
138
+ });
139
+ }),
140
+ resolveFirst: resolve,
141
+ firstSynced: promise
142
+ };
143
+ state.listeners.add(listener);
144
+ postChannelMessage(state.channel, {
145
+ kind: "doc-request",
146
+ docId,
147
+ from: this.instanceId
148
+ });
149
+ postChannelMessage(state.channel, {
150
+ kind: "doc-update",
151
+ docId,
152
+ from: this.instanceId,
153
+ mode: "snapshot",
154
+ payload: doc.export({ mode: "snapshot" })
155
+ });
156
+ queueMicrotask(() => resolve());
157
+ return {
158
+ unsubscribe: () => {
159
+ listener.unsubscribe();
160
+ state.listeners.delete(listener);
161
+ if (!state.listeners.size) this.teardownDocChannel(docId);
162
+ },
163
+ firstSyncedWithRemote: listener.firstSynced,
164
+ get connected() {
165
+ return true;
166
+ }
167
+ };
168
+ }
169
+ ensureMetaChannel() {
170
+ if (this.metaState) return this.metaState;
171
+ const channel = new (ensureBroadcastChannel())(this.metaChannelName);
172
+ const listeners = /* @__PURE__ */ new Set();
173
+ const onMessage = (event) => {
174
+ const message = event.data;
175
+ if (!message || message.from === this.instanceId) return;
176
+ if (message.kind === "meta-export") for (const entry of listeners) {
177
+ entry.muted = true;
178
+ entry.flock.importJson(message.bundle);
179
+ entry.muted = false;
180
+ entry.resolveFirst();
181
+ }
182
+ else if (message.kind === "meta-request") {
183
+ const first = listeners.values().next().value;
184
+ if (!first) return;
185
+ Promise.resolve(first.flock.exportJson()).then((bundle) => {
186
+ postChannelMessage(channel, {
187
+ kind: "meta-export",
188
+ from: this.instanceId,
189
+ bundle
190
+ });
191
+ });
192
+ }
193
+ };
194
+ channel.addEventListener("message", onMessage);
195
+ this.metaState = {
196
+ channel,
197
+ listeners,
198
+ onMessage
199
+ };
200
+ return this.metaState;
201
+ }
202
+ ensureDocChannel(docId) {
203
+ const existing = this.docStates.get(docId);
204
+ if (existing) return existing;
205
+ const channel = new (ensureBroadcastChannel())(`${this.namespace}-doc-${encodeDocChannelId(docId)}`);
206
+ const listeners = /* @__PURE__ */ new Set();
207
+ const onMessage = (event) => {
208
+ const message = event.data;
209
+ if (!message || message.from === this.instanceId) return;
210
+ if (message.kind === "doc-update") for (const entry of listeners) {
211
+ entry.muted = true;
212
+ entry.doc.import(message.payload);
213
+ entry.muted = false;
214
+ entry.resolveFirst();
215
+ }
216
+ else if (message.kind === "doc-request") {
217
+ const first = listeners.values().next().value;
218
+ if (!first) return;
219
+ const payload = message.docId === docId ? first.doc.export({ mode: "snapshot" }) : void 0;
220
+ if (!payload) return;
221
+ postChannelMessage(channel, {
222
+ kind: "doc-update",
223
+ docId,
224
+ from: this.instanceId,
225
+ mode: "snapshot",
226
+ payload
227
+ });
228
+ }
229
+ };
230
+ channel.addEventListener("message", onMessage);
231
+ const state = {
232
+ channel,
233
+ listeners,
234
+ onMessage
235
+ };
236
+ this.docStates.set(docId, state);
237
+ return state;
238
+ }
239
+ teardownDocChannel(docId) {
240
+ const state = this.docStates.get(docId);
241
+ if (!state) return;
242
+ for (const entry of state.listeners) entry.unsubscribe();
243
+ state.channel.removeEventListener("message", state.onMessage);
244
+ state.channel.close();
245
+ this.docStates.delete(docId);
246
+ }
247
+ };
248
+
249
+ //#endregion
250
+ export { BroadcastChannelTransportAdapter };
251
+ //# sourceMappingURL=broadcast-channel.js.map