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.
- package/README.md +42 -1
- package/dist/index.cjs +511 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -6
- package/dist/index.d.ts +56 -6
- package/dist/index.js +511 -97
- package/dist/index.js.map +1 -1
- package/dist/storage/filesystem.cjs +17 -0
- package/dist/storage/filesystem.cjs.map +1 -1
- package/dist/storage/filesystem.d.cts +2 -1
- package/dist/storage/filesystem.d.ts +2 -1
- package/dist/storage/filesystem.js +17 -0
- package/dist/storage/filesystem.js.map +1 -1
- package/dist/storage/indexeddb.cjs +4 -0
- package/dist/storage/indexeddb.cjs.map +1 -1
- package/dist/storage/indexeddb.d.cts +2 -1
- package/dist/storage/indexeddb.d.ts +2 -1
- package/dist/storage/indexeddb.js +4 -0
- package/dist/storage/indexeddb.js.map +1 -1
- package/dist/transport/broadcast-channel.cjs +131 -1
- package/dist/transport/broadcast-channel.cjs.map +1 -1
- package/dist/transport/broadcast-channel.d.cts +20 -3
- package/dist/transport/broadcast-channel.d.ts +20 -3
- package/dist/transport/broadcast-channel.js +130 -1
- package/dist/transport/broadcast-channel.js.map +1 -1
- package/dist/transport/websocket.cjs +348 -24
- package/dist/transport/websocket.cjs.map +1 -1
- package/dist/transport/websocket.d.cts +47 -5
- package/dist/transport/websocket.d.ts +47 -5
- package/dist/transport/websocket.js +349 -24
- package/dist/transport/websocket.js.map +1 -1
- package/dist/types.d.cts +116 -4
- package/dist/types.d.ts +116 -4
- package/package.json +7 -7
|
@@ -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 {
|
|
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(
|
|
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 {
|
|
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(
|
|
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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"}
|