loro-repo 0.5.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,6 @@
1
+ const require_chunk = require('../chunk.cjs');
2
+ let loro_crdt = require("loro-crdt");
3
+ loro_crdt = require_chunk.__toESM(loro_crdt);
1
4
 
2
5
  //#region src/transport/broadcast-channel.ts
3
6
  function deferred() {
@@ -38,12 +41,13 @@ var BroadcastChannelTransportAdapter = class {
38
41
  connected = false;
39
42
  metaState;
40
43
  docStates = /* @__PURE__ */ new Map();
44
+ ephemeralStates = /* @__PURE__ */ new Map();
41
45
  constructor(options = {}) {
42
46
  ensureBroadcastChannel();
43
47
  this.namespace = options.namespace ?? "loro-repo";
44
48
  this.metaChannelName = options.metaChannelName ?? `${this.namespace}-meta`;
45
49
  }
46
- async connect() {
50
+ async connect(_options) {
47
51
  this.connected = true;
48
52
  }
49
53
  async close() {
@@ -55,10 +59,28 @@ var BroadcastChannelTransportAdapter = class {
55
59
  }
56
60
  for (const [docId] of this.docStates) this.teardownDocChannel(docId);
57
61
  this.docStates.clear();
62
+ for (const [roomId] of this.ephemeralStates) this.teardownEphemeralChannel(roomId);
63
+ this.ephemeralStates.clear();
58
64
  }
59
65
  isConnected() {
60
66
  return this.connected;
61
67
  }
68
+ getStatus() {
69
+ return this.connected ? "connected" : "disconnected";
70
+ }
71
+ onStatusChange(listener) {
72
+ try {
73
+ listener(this.getStatus());
74
+ } catch {}
75
+ return () => {};
76
+ }
77
+ getLatency() {}
78
+ onLatency(_listener) {
79
+ return () => {};
80
+ }
81
+ async reconnect(_options) {
82
+ this.connected = true;
83
+ }
62
84
  async syncMeta(flock, _options) {
63
85
  const subscription = this.joinMetaRoom(flock);
64
86
  subscription.firstSyncedWithRemote.catch(() => void 0);
@@ -111,6 +133,13 @@ var BroadcastChannelTransportAdapter = class {
111
133
  firstSyncedWithRemote: listener.firstSynced,
112
134
  get connected() {
113
135
  return true;
136
+ },
137
+ status: "joined",
138
+ onStatusChange: (cb) => {
139
+ try {
140
+ cb("joined");
141
+ } catch {}
142
+ return () => {};
114
143
  }
115
144
  };
116
145
  }
@@ -164,6 +193,64 @@ var BroadcastChannelTransportAdapter = class {
164
193
  firstSyncedWithRemote: listener.firstSynced,
165
194
  get connected() {
166
195
  return true;
196
+ },
197
+ status: "joined",
198
+ onStatusChange: (cb) => {
199
+ try {
200
+ cb("joined");
201
+ } catch {}
202
+ return () => {};
203
+ }
204
+ };
205
+ }
206
+ joinEphemeralRoom(roomId) {
207
+ const state = this.ensureEphemeralChannel(roomId);
208
+ const { promise, resolve } = deferred();
209
+ const store = new loro_crdt.EphemeralStore();
210
+ const listener = {
211
+ store,
212
+ muted: false,
213
+ unsubscribe: store.subscribeLocalUpdates((payload) => {
214
+ if (listener.muted) return;
215
+ if (!payload || payload.length === 0) return;
216
+ postChannelMessage(state.channel, {
217
+ kind: "ephemeral-update",
218
+ from: this.instanceId,
219
+ payload
220
+ });
221
+ }),
222
+ resolveFirst: resolve,
223
+ firstSynced: promise
224
+ };
225
+ state.listeners.add(listener);
226
+ postChannelMessage(state.channel, {
227
+ kind: "ephemeral-request",
228
+ from: this.instanceId
229
+ });
230
+ const snapshot = store.encodeAll();
231
+ if (snapshot.length > 0) postChannelMessage(state.channel, {
232
+ kind: "ephemeral-update",
233
+ from: this.instanceId,
234
+ payload: snapshot
235
+ });
236
+ queueMicrotask(() => resolve());
237
+ return {
238
+ store,
239
+ unsubscribe: () => {
240
+ listener.unsubscribe();
241
+ state.listeners.delete(listener);
242
+ if (!state.listeners.size) this.teardownEphemeralChannel(roomId);
243
+ },
244
+ firstSyncedWithRemote: listener.firstSynced,
245
+ get connected() {
246
+ return true;
247
+ },
248
+ status: "joined",
249
+ onStatusChange: (cb) => {
250
+ try {
251
+ cb("joined");
252
+ } catch {}
253
+ return () => {};
167
254
  }
168
255
  };
169
256
  }
@@ -237,6 +324,41 @@ var BroadcastChannelTransportAdapter = class {
237
324
  this.docStates.set(docId, state);
238
325
  return state;
239
326
  }
327
+ ensureEphemeralChannel(roomId) {
328
+ const existing = this.ephemeralStates.get(roomId);
329
+ if (existing) return existing;
330
+ const channel = new (ensureBroadcastChannel())(`${this.namespace}-ephemeral-${encodeDocChannelId(roomId)}`);
331
+ const listeners = /* @__PURE__ */ new Set();
332
+ const onMessage = (event) => {
333
+ const message = event.data;
334
+ if (!message || message.from === this.instanceId) return;
335
+ if (message.kind === "ephemeral-update") for (const entry of listeners) {
336
+ entry.muted = true;
337
+ entry.store.apply(message.payload);
338
+ entry.muted = false;
339
+ entry.resolveFirst();
340
+ }
341
+ else if (message.kind === "ephemeral-request") {
342
+ const first = listeners.values().next().value;
343
+ if (!first) return;
344
+ const payload = first.store.encodeAll();
345
+ if (payload.length === 0) return;
346
+ postChannelMessage(channel, {
347
+ kind: "ephemeral-update",
348
+ from: this.instanceId,
349
+ payload
350
+ });
351
+ }
352
+ };
353
+ channel.addEventListener("message", onMessage);
354
+ const state = {
355
+ channel,
356
+ listeners,
357
+ onMessage
358
+ };
359
+ this.ephemeralStates.set(roomId, state);
360
+ return state;
361
+ }
240
362
  teardownDocChannel(docId) {
241
363
  const state = this.docStates.get(docId);
242
364
  if (!state) return;
@@ -245,6 +367,14 @@ var BroadcastChannelTransportAdapter = class {
245
367
  state.channel.close();
246
368
  this.docStates.delete(docId);
247
369
  }
370
+ teardownEphemeralChannel(roomId) {
371
+ const state = this.ephemeralStates.get(roomId);
372
+ if (!state) return;
373
+ for (const entry of state.listeners) entry.unsubscribe();
374
+ state.channel.removeEventListener("message", state.onMessage);
375
+ state.channel.close();
376
+ this.ephemeralStates.delete(roomId);
377
+ }
248
378
  };
249
379
 
250
380
  //#endregion
@@ -1 +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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"}
1
+ {"version":3,"file":"broadcast-channel.cjs","names":["resolve!: () => void","listener: MetaListener","listener: DocListener","EphemeralStore","listener: EphemeralListener","state: DocChannelState","state: EphemeralChannelState"],"sources":["../../src/transport/broadcast-channel.ts"],"sourcesContent":["import { Flock } from \"@loro-dev/flock\";\nimport { EphemeralStore, 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\ntype EphemeralListener = {\n store: EphemeralStore;\n unsubscribe: () => void;\n muted: boolean;\n resolveFirst: () => void;\n firstSynced: Promise<void>;\n};\n\ntype EphemeralChannelState = {\n channel: BroadcastChannelLike;\n listeners: Set<EphemeralListener>;\n onMessage: (event: BroadcastChannelMessageEvent) => void;\n};\n\ntype EphemeralMessage =\n | {\n kind: \"ephemeral-update\";\n from: string;\n payload: Uint8Array;\n }\n | {\n kind: \"ephemeral-request\";\n from: string;\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 | EphemeralMessage,\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 private readonly ephemeralStates = new Map<string, EphemeralChannelState>();\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(_options?: { timeout?: number; resetBackoff?: boolean }): 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 for (const [roomId] of this.ephemeralStates) {\n this.teardownEphemeralChannel(roomId);\n }\n this.ephemeralStates.clear();\n }\n\n isConnected(): boolean {\n return this.connected;\n }\n\n getStatus(): \"connected\" | \"disconnected\" {\n return this.connected ? \"connected\" : \"disconnected\";\n }\n\n onStatusChange(listener: (status: \"connected\" | \"disconnected\") => void): () => void {\n try {\n listener(this.getStatus());\n } catch {\n /* noop */\n }\n return () => {\n /* noop */\n };\n }\n\n getLatency(): undefined {\n return undefined;\n }\n\n onLatency(_listener: (latencyMs: number) => void): () => void {\n return () => {\n /* noop */\n };\n }\n\n async reconnect(_options?: { timeout?: number; resetBackoff?: boolean }): Promise<void> {\n this.connected = true;\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 status: \"joined\",\n onStatusChange: (cb) => {\n try {\n cb(\"joined\");\n } catch {\n /* noop */\n }\n return () => {\n /* noop */\n };\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 status: \"joined\",\n onStatusChange: (cb) => {\n try {\n cb(\"joined\");\n } catch {\n /* noop */\n }\n return () => {\n /* noop */\n };\n },\n };\n return subscription;\n }\n\n joinEphemeralRoom(\n roomId: string,\n ): TransportSubscription & { store: EphemeralStore } {\n const state = this.ensureEphemeralChannel(roomId);\n const { promise, resolve } = deferred();\n const store = new EphemeralStore();\n const listener: EphemeralListener = {\n store,\n muted: false,\n unsubscribe: store.subscribeLocalUpdates((payload) => {\n if (listener.muted) return;\n if (!payload || payload.length === 0) return;\n postChannelMessage(state.channel, {\n kind: \"ephemeral-update\",\n from: this.instanceId,\n payload,\n } satisfies EphemeralMessage);\n }),\n resolveFirst: resolve,\n firstSynced: promise,\n };\n state.listeners.add(listener);\n\n postChannelMessage(state.channel, {\n kind: \"ephemeral-request\",\n from: this.instanceId,\n } satisfies EphemeralMessage);\n const snapshot = store.encodeAll();\n if (snapshot.length > 0) {\n postChannelMessage(state.channel, {\n kind: \"ephemeral-update\",\n from: this.instanceId,\n payload: snapshot,\n } satisfies EphemeralMessage);\n }\n\n queueMicrotask(() => resolve());\n\n const subscription: TransportSubscription & { store: EphemeralStore } = {\n store,\n unsubscribe: () => {\n listener.unsubscribe();\n state.listeners.delete(listener);\n if (!state.listeners.size) {\n this.teardownEphemeralChannel(roomId);\n }\n },\n firstSyncedWithRemote: listener.firstSynced,\n get connected() {\n return true;\n },\n status: \"joined\",\n onStatusChange: (cb) => {\n try {\n cb(\"joined\");\n } catch {\n /* noop */\n }\n return () => {\n /* noop */\n };\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 ensureEphemeralChannel(roomId: string): EphemeralChannelState {\n const existing = this.ephemeralStates.get(roomId);\n if (existing) return existing;\n const Channel = ensureBroadcastChannel();\n const channelName = `${this.namespace}-ephemeral-${encodeDocChannelId(roomId)}`;\n const channel = new Channel(channelName);\n const listeners = new Set<EphemeralListener>();\n const onMessage = (event: BroadcastChannelMessageEvent) => {\n const message = event.data as EphemeralMessage | undefined;\n if (!message || message.from === this.instanceId) {\n return;\n }\n if (message.kind === \"ephemeral-update\") {\n for (const entry of listeners) {\n entry.muted = true;\n entry.store.apply(message.payload);\n entry.muted = false;\n entry.resolveFirst();\n }\n } else if (message.kind === \"ephemeral-request\") {\n const first = listeners.values().next().value;\n if (!first) return;\n const payload = first.store.encodeAll();\n if (payload.length === 0) return;\n postChannelMessage(channel, {\n kind: \"ephemeral-update\",\n from: this.instanceId,\n payload,\n } satisfies EphemeralMessage);\n }\n };\n channel.addEventListener(\"message\", onMessage);\n const state: EphemeralChannelState = { channel, listeners, onMessage };\n this.ephemeralStates.set(roomId, 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 private teardownEphemeralChannel(roomId: string): void {\n const state = this.ephemeralStates.get(roomId);\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.ephemeralStates.delete(roomId);\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
- import { A as TransportSyncResult, D as TransportJoinParams, E as TransportAdapter, O as TransportSubscription } from "../types.cjs";
1
+ import { F as TransportSyncResult, N as TransportSubscription, j as TransportJoinParams, k as TransportAdapter } from "../types.cjs";
2
2
  import { Flock } from "@loro-dev/flock";
3
- import { LoroDoc } from "loro-crdt";
3
+ import { EphemeralStore, LoroDoc } from "loro-crdt";
4
4
 
5
5
  //#region src/transport/broadcast-channel.d.ts
6
6
  interface BroadcastChannelTransportOptions {
@@ -24,10 +24,22 @@ declare class BroadcastChannelTransportAdapter implements TransportAdapter {
24
24
  private connected;
25
25
  private metaState?;
26
26
  private readonly docStates;
27
+ private readonly ephemeralStates;
27
28
  constructor(options?: BroadcastChannelTransportOptions);
28
- connect(): Promise<void>;
29
+ connect(_options?: {
30
+ timeout?: number;
31
+ resetBackoff?: boolean;
32
+ }): Promise<void>;
29
33
  close(): Promise<void>;
30
34
  isConnected(): boolean;
35
+ getStatus(): "connected" | "disconnected";
36
+ onStatusChange(listener: (status: "connected" | "disconnected") => void): () => void;
37
+ getLatency(): undefined;
38
+ onLatency(_listener: (latencyMs: number) => void): () => void;
39
+ reconnect(_options?: {
40
+ timeout?: number;
41
+ resetBackoff?: boolean;
42
+ }): Promise<void>;
31
43
  syncMeta(flock: Flock, _options?: {
32
44
  timeout?: number;
33
45
  }): Promise<TransportSyncResult>;
@@ -36,9 +48,14 @@ declare class BroadcastChannelTransportAdapter implements TransportAdapter {
36
48
  timeout?: number;
37
49
  }): Promise<TransportSyncResult>;
38
50
  joinDocRoom(docId: string, doc: LoroDoc, _params?: TransportJoinParams): TransportSubscription;
51
+ joinEphemeralRoom(roomId: string): TransportSubscription & {
52
+ store: EphemeralStore;
53
+ };
39
54
  private ensureMetaChannel;
40
55
  private ensureDocChannel;
56
+ private ensureEphemeralChannel;
41
57
  private teardownDocChannel;
58
+ private teardownEphemeralChannel;
42
59
  }
43
60
  //#endregion
44
61
  export { BroadcastChannelTransportAdapter, BroadcastChannelTransportOptions };
@@ -1,6 +1,6 @@
1
- import { A as TransportSyncResult, D as TransportJoinParams, E as TransportAdapter, O as TransportSubscription } from "../types.js";
1
+ import { F as TransportSyncResult, N as TransportSubscription, j as TransportJoinParams, k as TransportAdapter } from "../types.js";
2
2
  import { Flock } from "@loro-dev/flock";
3
- import { LoroDoc } from "loro-crdt";
3
+ import { EphemeralStore, LoroDoc } from "loro-crdt";
4
4
 
5
5
  //#region src/transport/broadcast-channel.d.ts
6
6
  interface BroadcastChannelTransportOptions {
@@ -24,10 +24,22 @@ declare class BroadcastChannelTransportAdapter implements TransportAdapter {
24
24
  private connected;
25
25
  private metaState?;
26
26
  private readonly docStates;
27
+ private readonly ephemeralStates;
27
28
  constructor(options?: BroadcastChannelTransportOptions);
28
- connect(): Promise<void>;
29
+ connect(_options?: {
30
+ timeout?: number;
31
+ resetBackoff?: boolean;
32
+ }): Promise<void>;
29
33
  close(): Promise<void>;
30
34
  isConnected(): boolean;
35
+ getStatus(): "connected" | "disconnected";
36
+ onStatusChange(listener: (status: "connected" | "disconnected") => void): () => void;
37
+ getLatency(): undefined;
38
+ onLatency(_listener: (latencyMs: number) => void): () => void;
39
+ reconnect(_options?: {
40
+ timeout?: number;
41
+ resetBackoff?: boolean;
42
+ }): Promise<void>;
31
43
  syncMeta(flock: Flock, _options?: {
32
44
  timeout?: number;
33
45
  }): Promise<TransportSyncResult>;
@@ -36,9 +48,14 @@ declare class BroadcastChannelTransportAdapter implements TransportAdapter {
36
48
  timeout?: number;
37
49
  }): Promise<TransportSyncResult>;
38
50
  joinDocRoom(docId: string, doc: LoroDoc, _params?: TransportJoinParams): TransportSubscription;
51
+ joinEphemeralRoom(roomId: string): TransportSubscription & {
52
+ store: EphemeralStore;
53
+ };
39
54
  private ensureMetaChannel;
40
55
  private ensureDocChannel;
56
+ private ensureEphemeralChannel;
41
57
  private teardownDocChannel;
58
+ private teardownEphemeralChannel;
42
59
  }
43
60
  //#endregion
44
61
  export { BroadcastChannelTransportAdapter, BroadcastChannelTransportOptions };
@@ -1,3 +1,5 @@
1
+ import { EphemeralStore } from "loro-crdt";
2
+
1
3
  //#region src/transport/broadcast-channel.ts
2
4
  function deferred() {
3
5
  let resolve;
@@ -37,12 +39,13 @@ var BroadcastChannelTransportAdapter = class {
37
39
  connected = false;
38
40
  metaState;
39
41
  docStates = /* @__PURE__ */ new Map();
42
+ ephemeralStates = /* @__PURE__ */ new Map();
40
43
  constructor(options = {}) {
41
44
  ensureBroadcastChannel();
42
45
  this.namespace = options.namespace ?? "loro-repo";
43
46
  this.metaChannelName = options.metaChannelName ?? `${this.namespace}-meta`;
44
47
  }
45
- async connect() {
48
+ async connect(_options) {
46
49
  this.connected = true;
47
50
  }
48
51
  async close() {
@@ -54,10 +57,28 @@ var BroadcastChannelTransportAdapter = class {
54
57
  }
55
58
  for (const [docId] of this.docStates) this.teardownDocChannel(docId);
56
59
  this.docStates.clear();
60
+ for (const [roomId] of this.ephemeralStates) this.teardownEphemeralChannel(roomId);
61
+ this.ephemeralStates.clear();
57
62
  }
58
63
  isConnected() {
59
64
  return this.connected;
60
65
  }
66
+ getStatus() {
67
+ return this.connected ? "connected" : "disconnected";
68
+ }
69
+ onStatusChange(listener) {
70
+ try {
71
+ listener(this.getStatus());
72
+ } catch {}
73
+ return () => {};
74
+ }
75
+ getLatency() {}
76
+ onLatency(_listener) {
77
+ return () => {};
78
+ }
79
+ async reconnect(_options) {
80
+ this.connected = true;
81
+ }
61
82
  async syncMeta(flock, _options) {
62
83
  const subscription = this.joinMetaRoom(flock);
63
84
  subscription.firstSyncedWithRemote.catch(() => void 0);
@@ -110,6 +131,13 @@ var BroadcastChannelTransportAdapter = class {
110
131
  firstSyncedWithRemote: listener.firstSynced,
111
132
  get connected() {
112
133
  return true;
134
+ },
135
+ status: "joined",
136
+ onStatusChange: (cb) => {
137
+ try {
138
+ cb("joined");
139
+ } catch {}
140
+ return () => {};
113
141
  }
114
142
  };
115
143
  }
@@ -163,6 +191,64 @@ var BroadcastChannelTransportAdapter = class {
163
191
  firstSyncedWithRemote: listener.firstSynced,
164
192
  get connected() {
165
193
  return true;
194
+ },
195
+ status: "joined",
196
+ onStatusChange: (cb) => {
197
+ try {
198
+ cb("joined");
199
+ } catch {}
200
+ return () => {};
201
+ }
202
+ };
203
+ }
204
+ joinEphemeralRoom(roomId) {
205
+ const state = this.ensureEphemeralChannel(roomId);
206
+ const { promise, resolve } = deferred();
207
+ const store = new EphemeralStore();
208
+ const listener = {
209
+ store,
210
+ muted: false,
211
+ unsubscribe: store.subscribeLocalUpdates((payload) => {
212
+ if (listener.muted) return;
213
+ if (!payload || payload.length === 0) return;
214
+ postChannelMessage(state.channel, {
215
+ kind: "ephemeral-update",
216
+ from: this.instanceId,
217
+ payload
218
+ });
219
+ }),
220
+ resolveFirst: resolve,
221
+ firstSynced: promise
222
+ };
223
+ state.listeners.add(listener);
224
+ postChannelMessage(state.channel, {
225
+ kind: "ephemeral-request",
226
+ from: this.instanceId
227
+ });
228
+ const snapshot = store.encodeAll();
229
+ if (snapshot.length > 0) postChannelMessage(state.channel, {
230
+ kind: "ephemeral-update",
231
+ from: this.instanceId,
232
+ payload: snapshot
233
+ });
234
+ queueMicrotask(() => resolve());
235
+ return {
236
+ store,
237
+ unsubscribe: () => {
238
+ listener.unsubscribe();
239
+ state.listeners.delete(listener);
240
+ if (!state.listeners.size) this.teardownEphemeralChannel(roomId);
241
+ },
242
+ firstSyncedWithRemote: listener.firstSynced,
243
+ get connected() {
244
+ return true;
245
+ },
246
+ status: "joined",
247
+ onStatusChange: (cb) => {
248
+ try {
249
+ cb("joined");
250
+ } catch {}
251
+ return () => {};
166
252
  }
167
253
  };
168
254
  }
@@ -236,6 +322,41 @@ var BroadcastChannelTransportAdapter = class {
236
322
  this.docStates.set(docId, state);
237
323
  return state;
238
324
  }
325
+ ensureEphemeralChannel(roomId) {
326
+ const existing = this.ephemeralStates.get(roomId);
327
+ if (existing) return existing;
328
+ const channel = new (ensureBroadcastChannel())(`${this.namespace}-ephemeral-${encodeDocChannelId(roomId)}`);
329
+ const listeners = /* @__PURE__ */ new Set();
330
+ const onMessage = (event) => {
331
+ const message = event.data;
332
+ if (!message || message.from === this.instanceId) return;
333
+ if (message.kind === "ephemeral-update") for (const entry of listeners) {
334
+ entry.muted = true;
335
+ entry.store.apply(message.payload);
336
+ entry.muted = false;
337
+ entry.resolveFirst();
338
+ }
339
+ else if (message.kind === "ephemeral-request") {
340
+ const first = listeners.values().next().value;
341
+ if (!first) return;
342
+ const payload = first.store.encodeAll();
343
+ if (payload.length === 0) return;
344
+ postChannelMessage(channel, {
345
+ kind: "ephemeral-update",
346
+ from: this.instanceId,
347
+ payload
348
+ });
349
+ }
350
+ };
351
+ channel.addEventListener("message", onMessage);
352
+ const state = {
353
+ channel,
354
+ listeners,
355
+ onMessage
356
+ };
357
+ this.ephemeralStates.set(roomId, state);
358
+ return state;
359
+ }
239
360
  teardownDocChannel(docId) {
240
361
  const state = this.docStates.get(docId);
241
362
  if (!state) return;
@@ -244,6 +365,14 @@ var BroadcastChannelTransportAdapter = class {
244
365
  state.channel.close();
245
366
  this.docStates.delete(docId);
246
367
  }
368
+ teardownEphemeralChannel(roomId) {
369
+ const state = this.ephemeralStates.get(roomId);
370
+ if (!state) return;
371
+ for (const entry of state.listeners) entry.unsubscribe();
372
+ state.channel.removeEventListener("message", state.onMessage);
373
+ state.channel.close();
374
+ this.ephemeralStates.delete(roomId);
375
+ }
247
376
  };
248
377
 
249
378
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"broadcast-channel.js","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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"}
1
+ {"version":3,"file":"broadcast-channel.js","names":["resolve!: () => void","listener: MetaListener","listener: DocListener","listener: EphemeralListener","state: DocChannelState","state: EphemeralChannelState"],"sources":["../../src/transport/broadcast-channel.ts"],"sourcesContent":["import { Flock } from \"@loro-dev/flock\";\nimport { EphemeralStore, 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\ntype EphemeralListener = {\n store: EphemeralStore;\n unsubscribe: () => void;\n muted: boolean;\n resolveFirst: () => void;\n firstSynced: Promise<void>;\n};\n\ntype EphemeralChannelState = {\n channel: BroadcastChannelLike;\n listeners: Set<EphemeralListener>;\n onMessage: (event: BroadcastChannelMessageEvent) => void;\n};\n\ntype EphemeralMessage =\n | {\n kind: \"ephemeral-update\";\n from: string;\n payload: Uint8Array;\n }\n | {\n kind: \"ephemeral-request\";\n from: string;\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 | EphemeralMessage,\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 private readonly ephemeralStates = new Map<string, EphemeralChannelState>();\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(_options?: { timeout?: number; resetBackoff?: boolean }): 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 for (const [roomId] of this.ephemeralStates) {\n this.teardownEphemeralChannel(roomId);\n }\n this.ephemeralStates.clear();\n }\n\n isConnected(): boolean {\n return this.connected;\n }\n\n getStatus(): \"connected\" | \"disconnected\" {\n return this.connected ? \"connected\" : \"disconnected\";\n }\n\n onStatusChange(listener: (status: \"connected\" | \"disconnected\") => void): () => void {\n try {\n listener(this.getStatus());\n } catch {\n /* noop */\n }\n return () => {\n /* noop */\n };\n }\n\n getLatency(): undefined {\n return undefined;\n }\n\n onLatency(_listener: (latencyMs: number) => void): () => void {\n return () => {\n /* noop */\n };\n }\n\n async reconnect(_options?: { timeout?: number; resetBackoff?: boolean }): Promise<void> {\n this.connected = true;\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 status: \"joined\",\n onStatusChange: (cb) => {\n try {\n cb(\"joined\");\n } catch {\n /* noop */\n }\n return () => {\n /* noop */\n };\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 status: \"joined\",\n onStatusChange: (cb) => {\n try {\n cb(\"joined\");\n } catch {\n /* noop */\n }\n return () => {\n /* noop */\n };\n },\n };\n return subscription;\n }\n\n joinEphemeralRoom(\n roomId: string,\n ): TransportSubscription & { store: EphemeralStore } {\n const state = this.ensureEphemeralChannel(roomId);\n const { promise, resolve } = deferred();\n const store = new EphemeralStore();\n const listener: EphemeralListener = {\n store,\n muted: false,\n unsubscribe: store.subscribeLocalUpdates((payload) => {\n if (listener.muted) return;\n if (!payload || payload.length === 0) return;\n postChannelMessage(state.channel, {\n kind: \"ephemeral-update\",\n from: this.instanceId,\n payload,\n } satisfies EphemeralMessage);\n }),\n resolveFirst: resolve,\n firstSynced: promise,\n };\n state.listeners.add(listener);\n\n postChannelMessage(state.channel, {\n kind: \"ephemeral-request\",\n from: this.instanceId,\n } satisfies EphemeralMessage);\n const snapshot = store.encodeAll();\n if (snapshot.length > 0) {\n postChannelMessage(state.channel, {\n kind: \"ephemeral-update\",\n from: this.instanceId,\n payload: snapshot,\n } satisfies EphemeralMessage);\n }\n\n queueMicrotask(() => resolve());\n\n const subscription: TransportSubscription & { store: EphemeralStore } = {\n store,\n unsubscribe: () => {\n listener.unsubscribe();\n state.listeners.delete(listener);\n if (!state.listeners.size) {\n this.teardownEphemeralChannel(roomId);\n }\n },\n firstSyncedWithRemote: listener.firstSynced,\n get connected() {\n return true;\n },\n status: \"joined\",\n onStatusChange: (cb) => {\n try {\n cb(\"joined\");\n } catch {\n /* noop */\n }\n return () => {\n /* noop */\n };\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 ensureEphemeralChannel(roomId: string): EphemeralChannelState {\n const existing = this.ephemeralStates.get(roomId);\n if (existing) return existing;\n const Channel = ensureBroadcastChannel();\n const channelName = `${this.namespace}-ephemeral-${encodeDocChannelId(roomId)}`;\n const channel = new Channel(channelName);\n const listeners = new Set<EphemeralListener>();\n const onMessage = (event: BroadcastChannelMessageEvent) => {\n const message = event.data as EphemeralMessage | undefined;\n if (!message || message.from === this.instanceId) {\n return;\n }\n if (message.kind === \"ephemeral-update\") {\n for (const entry of listeners) {\n entry.muted = true;\n entry.store.apply(message.payload);\n entry.muted = false;\n entry.resolveFirst();\n }\n } else if (message.kind === \"ephemeral-request\") {\n const first = listeners.values().next().value;\n if (!first) return;\n const payload = first.store.encodeAll();\n if (payload.length === 0) return;\n postChannelMessage(channel, {\n kind: \"ephemeral-update\",\n from: this.instanceId,\n payload,\n } satisfies EphemeralMessage);\n }\n };\n channel.addEventListener(\"message\", onMessage);\n const state: EphemeralChannelState = { channel, listeners, onMessage };\n this.ephemeralStates.set(roomId, 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 private teardownEphemeralChannel(roomId: string): void {\n const state = this.ephemeralStates.get(roomId);\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.ephemeralStates.delete(roomId);\n }\n}\n"],"mappings":"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"}