@canvas-harness/sync-broadcast 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +83 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var createBroadcastSyncAdapter = ({
|
|
5
|
+
channelName,
|
|
6
|
+
clientId,
|
|
7
|
+
initialPresence
|
|
8
|
+
}) => {
|
|
9
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
10
|
+
throw new Error("BroadcastChannel is not available in this environment.");
|
|
11
|
+
}
|
|
12
|
+
const channel = new BroadcastChannel(channelName);
|
|
13
|
+
const batchListeners = /* @__PURE__ */ new Set();
|
|
14
|
+
const presenceListeners = /* @__PURE__ */ new Set();
|
|
15
|
+
let lastLocalPresence = initialPresence;
|
|
16
|
+
const post = (msg) => {
|
|
17
|
+
channel.postMessage(msg);
|
|
18
|
+
};
|
|
19
|
+
channel.addEventListener("message", (e) => {
|
|
20
|
+
const msg = e.data;
|
|
21
|
+
if (msg.kind === "batch") {
|
|
22
|
+
if (msg.batch.clientId === clientId) return;
|
|
23
|
+
for (const cb of batchListeners) cb(msg.batch);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (msg.kind === "presence") {
|
|
27
|
+
if (msg.clientId === clientId) return;
|
|
28
|
+
for (const cb of presenceListeners) cb(msg.clientId, msg.state);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (msg.kind === "presence-leave") {
|
|
32
|
+
if (msg.clientId === clientId) return;
|
|
33
|
+
for (const cb of presenceListeners) cb(msg.clientId, null);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (msg.kind === "hello" && msg.clientId !== clientId && lastLocalPresence) {
|
|
37
|
+
post({ kind: "presence", clientId, state: lastLocalPresence });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
post({ kind: "hello", clientId });
|
|
41
|
+
const onPageHide = () => {
|
|
42
|
+
post({ kind: "presence-leave", clientId });
|
|
43
|
+
};
|
|
44
|
+
if (typeof window !== "undefined") window.addEventListener("pagehide", onPageHide);
|
|
45
|
+
return {
|
|
46
|
+
capabilities: { causalOrdering: true },
|
|
47
|
+
sendBatch(batch) {
|
|
48
|
+
post({ kind: "batch", batch });
|
|
49
|
+
},
|
|
50
|
+
sendPresence(patch) {
|
|
51
|
+
const state = {
|
|
52
|
+
...lastLocalPresence ?? {},
|
|
53
|
+
...patch,
|
|
54
|
+
clientId
|
|
55
|
+
};
|
|
56
|
+
lastLocalPresence = state;
|
|
57
|
+
post({ kind: "presence", clientId, state });
|
|
58
|
+
},
|
|
59
|
+
onBatch(cb) {
|
|
60
|
+
batchListeners.add(cb);
|
|
61
|
+
return () => {
|
|
62
|
+
batchListeners.delete(cb);
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
onPresence(cb) {
|
|
66
|
+
presenceListeners.add(cb);
|
|
67
|
+
return () => {
|
|
68
|
+
presenceListeners.delete(cb);
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
destroy() {
|
|
72
|
+
if (typeof window !== "undefined") window.removeEventListener("pagehide", onPageHide);
|
|
73
|
+
post({ kind: "presence-leave", clientId });
|
|
74
|
+
channel.close();
|
|
75
|
+
batchListeners.clear();
|
|
76
|
+
presenceListeners.clear();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
exports.createBroadcastSyncAdapter = createBroadcastSyncAdapter;
|
|
82
|
+
//# sourceMappingURL=index.cjs.map
|
|
83
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type {\n ClientId,\n OpBatch,\n PresencePatch,\n PresenceState,\n SyncAdapter,\n Unsubscribe,\n} from '@canvas-harness/core'\n\n/**\n * BroadcastChannel-backed SyncAdapter — see ARCHITECTURE.md §10.6.\n *\n * The simplest possible adapter: serializes op batches + presence\n * patches to JSON and broadcasts over the BroadcastChannel API. Same\n * machine, multiple tabs of the same origin. Good for demos + dev.\n *\n * Causal-ordering guarantee: BroadcastChannel within a single browser\n * process delivers messages in send order, so we can advertise\n * `capabilities.causalOrdering: true`. For cross-machine / cross-origin\n * collab use a real transport (WebSocket + sequenced server, Yjs, etc.).\n */\n\ntype Wire =\n | { kind: 'batch'; batch: OpBatch }\n | { kind: 'presence'; clientId: ClientId; state: PresenceState }\n | { kind: 'presence-leave'; clientId: ClientId }\n | { kind: 'hello'; clientId: ClientId }\n\nexport type BroadcastSyncOptions = {\n /** Channel name — peers join by sharing the same name. */\n channelName: string\n /** This client's id. Used to filter self-echoes + announce on attach. */\n clientId: ClientId\n /** Optional initial presence to broadcast on attach. */\n initialPresence?: PresenceState\n}\n\nexport const createBroadcastSyncAdapter = ({\n channelName,\n clientId,\n initialPresence,\n}: BroadcastSyncOptions): SyncAdapter => {\n if (typeof BroadcastChannel === 'undefined') {\n throw new Error('BroadcastChannel is not available in this environment.')\n }\n const channel = new BroadcastChannel(channelName)\n\n const batchListeners = new Set<(batch: OpBatch) => void>()\n const presenceListeners = new Set<(clientId: ClientId, state: PresenceState | null) => void>()\n // Latest presence per remote client. We keep a snapshot so when a new\n // peer announces 'hello' we can replay our own presence to them.\n let lastLocalPresence: PresenceState | undefined = initialPresence\n\n const post = (msg: Wire): void => {\n channel.postMessage(msg)\n }\n\n channel.addEventListener('message', e => {\n const msg = e.data as Wire\n // BroadcastChannel doesn't echo to the sender, but in case a future\n // transport changes that, defensively drop self-messages.\n if (msg.kind === 'batch') {\n if (msg.batch.clientId === clientId) return\n for (const cb of batchListeners) cb(msg.batch)\n return\n }\n if (msg.kind === 'presence') {\n if (msg.clientId === clientId) return\n for (const cb of presenceListeners) cb(msg.clientId, msg.state)\n return\n }\n if (msg.kind === 'presence-leave') {\n if (msg.clientId === clientId) return\n for (const cb of presenceListeners) cb(msg.clientId, null)\n return\n }\n if (msg.kind === 'hello' && msg.clientId !== clientId && lastLocalPresence) {\n // A new peer joined; replay our presence so they can paint our cursor.\n post({ kind: 'presence', clientId, state: lastLocalPresence })\n }\n })\n\n // Announce arrival so existing peers can replay their presence to us.\n post({ kind: 'hello', clientId })\n\n // Best-effort departure: BroadcastChannel doesn't have a leave event,\n // but pagehide is close enough for a demo.\n const onPageHide = (): void => {\n post({ kind: 'presence-leave', clientId })\n }\n if (typeof window !== 'undefined') window.addEventListener('pagehide', onPageHide)\n\n return {\n capabilities: { causalOrdering: true },\n\n sendBatch(batch: OpBatch) {\n post({ kind: 'batch', batch })\n },\n\n sendPresence(patch: PresencePatch) {\n const state: PresenceState = {\n ...(lastLocalPresence ?? ({} as PresenceState)),\n ...patch,\n clientId,\n }\n lastLocalPresence = state\n post({ kind: 'presence', clientId, state })\n },\n\n onBatch(cb): Unsubscribe {\n batchListeners.add(cb)\n return () => {\n batchListeners.delete(cb)\n }\n },\n\n onPresence(cb): Unsubscribe {\n presenceListeners.add(cb)\n return () => {\n presenceListeners.delete(cb)\n }\n },\n\n destroy() {\n if (typeof window !== 'undefined') window.removeEventListener('pagehide', onPageHide)\n post({ kind: 'presence-leave', clientId })\n channel.close()\n batchListeners.clear()\n presenceListeners.clear()\n },\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ClientId, PresenceState, SyncAdapter } from '@canvas-harness/core';
|
|
2
|
+
|
|
3
|
+
type BroadcastSyncOptions = {
|
|
4
|
+
/** Channel name — peers join by sharing the same name. */
|
|
5
|
+
channelName: string;
|
|
6
|
+
/** This client's id. Used to filter self-echoes + announce on attach. */
|
|
7
|
+
clientId: ClientId;
|
|
8
|
+
/** Optional initial presence to broadcast on attach. */
|
|
9
|
+
initialPresence?: PresenceState;
|
|
10
|
+
};
|
|
11
|
+
declare const createBroadcastSyncAdapter: ({ channelName, clientId, initialPresence, }: BroadcastSyncOptions) => SyncAdapter;
|
|
12
|
+
|
|
13
|
+
export { type BroadcastSyncOptions, createBroadcastSyncAdapter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ClientId, PresenceState, SyncAdapter } from '@canvas-harness/core';
|
|
2
|
+
|
|
3
|
+
type BroadcastSyncOptions = {
|
|
4
|
+
/** Channel name — peers join by sharing the same name. */
|
|
5
|
+
channelName: string;
|
|
6
|
+
/** This client's id. Used to filter self-echoes + announce on attach. */
|
|
7
|
+
clientId: ClientId;
|
|
8
|
+
/** Optional initial presence to broadcast on attach. */
|
|
9
|
+
initialPresence?: PresenceState;
|
|
10
|
+
};
|
|
11
|
+
declare const createBroadcastSyncAdapter: ({ channelName, clientId, initialPresence, }: BroadcastSyncOptions) => SyncAdapter;
|
|
12
|
+
|
|
13
|
+
export { type BroadcastSyncOptions, createBroadcastSyncAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var createBroadcastSyncAdapter = ({
|
|
3
|
+
channelName,
|
|
4
|
+
clientId,
|
|
5
|
+
initialPresence
|
|
6
|
+
}) => {
|
|
7
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
8
|
+
throw new Error("BroadcastChannel is not available in this environment.");
|
|
9
|
+
}
|
|
10
|
+
const channel = new BroadcastChannel(channelName);
|
|
11
|
+
const batchListeners = /* @__PURE__ */ new Set();
|
|
12
|
+
const presenceListeners = /* @__PURE__ */ new Set();
|
|
13
|
+
let lastLocalPresence = initialPresence;
|
|
14
|
+
const post = (msg) => {
|
|
15
|
+
channel.postMessage(msg);
|
|
16
|
+
};
|
|
17
|
+
channel.addEventListener("message", (e) => {
|
|
18
|
+
const msg = e.data;
|
|
19
|
+
if (msg.kind === "batch") {
|
|
20
|
+
if (msg.batch.clientId === clientId) return;
|
|
21
|
+
for (const cb of batchListeners) cb(msg.batch);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (msg.kind === "presence") {
|
|
25
|
+
if (msg.clientId === clientId) return;
|
|
26
|
+
for (const cb of presenceListeners) cb(msg.clientId, msg.state);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (msg.kind === "presence-leave") {
|
|
30
|
+
if (msg.clientId === clientId) return;
|
|
31
|
+
for (const cb of presenceListeners) cb(msg.clientId, null);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (msg.kind === "hello" && msg.clientId !== clientId && lastLocalPresence) {
|
|
35
|
+
post({ kind: "presence", clientId, state: lastLocalPresence });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
post({ kind: "hello", clientId });
|
|
39
|
+
const onPageHide = () => {
|
|
40
|
+
post({ kind: "presence-leave", clientId });
|
|
41
|
+
};
|
|
42
|
+
if (typeof window !== "undefined") window.addEventListener("pagehide", onPageHide);
|
|
43
|
+
return {
|
|
44
|
+
capabilities: { causalOrdering: true },
|
|
45
|
+
sendBatch(batch) {
|
|
46
|
+
post({ kind: "batch", batch });
|
|
47
|
+
},
|
|
48
|
+
sendPresence(patch) {
|
|
49
|
+
const state = {
|
|
50
|
+
...lastLocalPresence ?? {},
|
|
51
|
+
...patch,
|
|
52
|
+
clientId
|
|
53
|
+
};
|
|
54
|
+
lastLocalPresence = state;
|
|
55
|
+
post({ kind: "presence", clientId, state });
|
|
56
|
+
},
|
|
57
|
+
onBatch(cb) {
|
|
58
|
+
batchListeners.add(cb);
|
|
59
|
+
return () => {
|
|
60
|
+
batchListeners.delete(cb);
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
onPresence(cb) {
|
|
64
|
+
presenceListeners.add(cb);
|
|
65
|
+
return () => {
|
|
66
|
+
presenceListeners.delete(cb);
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
destroy() {
|
|
70
|
+
if (typeof window !== "undefined") window.removeEventListener("pagehide", onPageHide);
|
|
71
|
+
post({ kind: "presence-leave", clientId });
|
|
72
|
+
channel.close();
|
|
73
|
+
batchListeners.clear();
|
|
74
|
+
presenceListeners.clear();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export { createBroadcastSyncAdapter };
|
|
80
|
+
//# sourceMappingURL=index.js.map
|
|
81
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AAqCO,IAAM,6BAA6B,CAAC;AAAA,EACzC,WAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,KAAyC;AACvC,EAAA,IAAI,OAAO,qBAAqB,WAAA,EAAa;AAC3C,IAAA,MAAM,IAAI,MAAM,wDAAwD,CAAA;AAAA,EAC1E;AACA,EAAA,MAAM,OAAA,GAAU,IAAI,gBAAA,CAAiB,WAAW,CAAA;AAEhD,EAAA,MAAM,cAAA,uBAAqB,GAAA,EAA8B;AACzD,EAAA,MAAM,iBAAA,uBAAwB,GAAA,EAA+D;AAG7F,EAAA,IAAI,iBAAA,GAA+C,eAAA;AAEnD,EAAA,MAAM,IAAA,GAAO,CAAC,GAAA,KAAoB;AAChC,IAAA,OAAA,CAAQ,YAAY,GAAG,CAAA;AAAA,EACzB,CAAA;AAEA,EAAA,OAAA,CAAQ,gBAAA,CAAiB,WAAW,CAAA,CAAA,KAAK;AACvC,IAAA,MAAM,MAAM,CAAA,CAAE,IAAA;AAGd,IAAA,IAAI,GAAA,CAAI,SAAS,OAAA,EAAS;AACxB,MAAA,IAAI,GAAA,CAAI,KAAA,CAAM,QAAA,KAAa,QAAA,EAAU;AACrC,MAAA,KAAA,MAAW,EAAA,IAAM,cAAA,EAAgB,EAAA,CAAG,GAAA,CAAI,KAAK,CAAA;AAC7C,MAAA;AAAA,IACF;AACA,IAAA,IAAI,GAAA,CAAI,SAAS,UAAA,EAAY;AAC3B,MAAA,IAAI,GAAA,CAAI,aAAa,QAAA,EAAU;AAC/B,MAAA,KAAA,MAAW,MAAM,iBAAA,EAAmB,EAAA,CAAG,GAAA,CAAI,QAAA,EAAU,IAAI,KAAK,CAAA;AAC9D,MAAA;AAAA,IACF;AACA,IAAA,IAAI,GAAA,CAAI,SAAS,gBAAA,EAAkB;AACjC,MAAA,IAAI,GAAA,CAAI,aAAa,QAAA,EAAU;AAC/B,MAAA,KAAA,MAAW,EAAA,IAAM,iBAAA,EAAmB,EAAA,CAAG,GAAA,CAAI,UAAU,IAAI,CAAA;AACzD,MAAA;AAAA,IACF;AACA,IAAA,IAAI,IAAI,IAAA,KAAS,OAAA,IAAW,GAAA,CAAI,QAAA,KAAa,YAAY,iBAAA,EAAmB;AAE1E,MAAA,IAAA,CAAK,EAAE,IAAA,EAAM,UAAA,EAAY,QAAA,EAAU,KAAA,EAAO,mBAAmB,CAAA;AAAA,IAC/D;AAAA,EACF,CAAC,CAAA;AAGD,EAAA,IAAA,CAAK,EAAE,IAAA,EAAM,OAAA,EAAS,QAAA,EAAU,CAAA;AAIhC,EAAA,MAAM,aAAa,MAAY;AAC7B,IAAA,IAAA,CAAK,EAAE,IAAA,EAAM,gBAAA,EAAkB,QAAA,EAAU,CAAA;AAAA,EAC3C,CAAA;AACA,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,MAAA,CAAO,gBAAA,CAAiB,YAAY,UAAU,CAAA;AAEjF,EAAA,OAAO;AAAA,IACL,YAAA,EAAc,EAAE,cAAA,EAAgB,IAAA,EAAK;AAAA,IAErC,UAAU,KAAA,EAAgB;AACxB,MAAA,IAAA,CAAK,EAAE,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,CAAA;AAAA,IAC/B,CAAA;AAAA,IAEA,aAAa,KAAA,EAAsB;AACjC,MAAA,MAAM,KAAA,GAAuB;AAAA,QAC3B,GAAI,qBAAsB,EAAC;AAAA,QAC3B,GAAG,KAAA;AAAA,QACH;AAAA,OACF;AACA,MAAA,iBAAA,GAAoB,KAAA;AACpB,MAAA,IAAA,CAAK,EAAE,IAAA,EAAM,UAAA,EAAY,QAAA,EAAU,OAAO,CAAA;AAAA,IAC5C,CAAA;AAAA,IAEA,QAAQ,EAAA,EAAiB;AACvB,MAAA,cAAA,CAAe,IAAI,EAAE,CAAA;AACrB,MAAA,OAAO,MAAM;AACX,QAAA,cAAA,CAAe,OAAO,EAAE,CAAA;AAAA,MAC1B,CAAA;AAAA,IACF,CAAA;AAAA,IAEA,WAAW,EAAA,EAAiB;AAC1B,MAAA,iBAAA,CAAkB,IAAI,EAAE,CAAA;AACxB,MAAA,OAAO,MAAM;AACX,QAAA,iBAAA,CAAkB,OAAO,EAAE,CAAA;AAAA,MAC7B,CAAA;AAAA,IACF,CAAA;AAAA,IAEA,OAAA,GAAU;AACR,MAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,MAAA,CAAO,mBAAA,CAAoB,YAAY,UAAU,CAAA;AACpF,MAAA,IAAA,CAAK,EAAE,IAAA,EAAM,gBAAA,EAAkB,QAAA,EAAU,CAAA;AACzC,MAAA,OAAA,CAAQ,KAAA,EAAM;AACd,MAAA,cAAA,CAAe,KAAA,EAAM;AACrB,MAAA,iBAAA,CAAkB,KAAA,EAAM;AAAA,IAC1B;AAAA,GACF;AACF","file":"index.js","sourcesContent":["import type {\n ClientId,\n OpBatch,\n PresencePatch,\n PresenceState,\n SyncAdapter,\n Unsubscribe,\n} from '@canvas-harness/core'\n\n/**\n * BroadcastChannel-backed SyncAdapter — see ARCHITECTURE.md §10.6.\n *\n * The simplest possible adapter: serializes op batches + presence\n * patches to JSON and broadcasts over the BroadcastChannel API. Same\n * machine, multiple tabs of the same origin. Good for demos + dev.\n *\n * Causal-ordering guarantee: BroadcastChannel within a single browser\n * process delivers messages in send order, so we can advertise\n * `capabilities.causalOrdering: true`. For cross-machine / cross-origin\n * collab use a real transport (WebSocket + sequenced server, Yjs, etc.).\n */\n\ntype Wire =\n | { kind: 'batch'; batch: OpBatch }\n | { kind: 'presence'; clientId: ClientId; state: PresenceState }\n | { kind: 'presence-leave'; clientId: ClientId }\n | { kind: 'hello'; clientId: ClientId }\n\nexport type BroadcastSyncOptions = {\n /** Channel name — peers join by sharing the same name. */\n channelName: string\n /** This client's id. Used to filter self-echoes + announce on attach. */\n clientId: ClientId\n /** Optional initial presence to broadcast on attach. */\n initialPresence?: PresenceState\n}\n\nexport const createBroadcastSyncAdapter = ({\n channelName,\n clientId,\n initialPresence,\n}: BroadcastSyncOptions): SyncAdapter => {\n if (typeof BroadcastChannel === 'undefined') {\n throw new Error('BroadcastChannel is not available in this environment.')\n }\n const channel = new BroadcastChannel(channelName)\n\n const batchListeners = new Set<(batch: OpBatch) => void>()\n const presenceListeners = new Set<(clientId: ClientId, state: PresenceState | null) => void>()\n // Latest presence per remote client. We keep a snapshot so when a new\n // peer announces 'hello' we can replay our own presence to them.\n let lastLocalPresence: PresenceState | undefined = initialPresence\n\n const post = (msg: Wire): void => {\n channel.postMessage(msg)\n }\n\n channel.addEventListener('message', e => {\n const msg = e.data as Wire\n // BroadcastChannel doesn't echo to the sender, but in case a future\n // transport changes that, defensively drop self-messages.\n if (msg.kind === 'batch') {\n if (msg.batch.clientId === clientId) return\n for (const cb of batchListeners) cb(msg.batch)\n return\n }\n if (msg.kind === 'presence') {\n if (msg.clientId === clientId) return\n for (const cb of presenceListeners) cb(msg.clientId, msg.state)\n return\n }\n if (msg.kind === 'presence-leave') {\n if (msg.clientId === clientId) return\n for (const cb of presenceListeners) cb(msg.clientId, null)\n return\n }\n if (msg.kind === 'hello' && msg.clientId !== clientId && lastLocalPresence) {\n // A new peer joined; replay our presence so they can paint our cursor.\n post({ kind: 'presence', clientId, state: lastLocalPresence })\n }\n })\n\n // Announce arrival so existing peers can replay their presence to us.\n post({ kind: 'hello', clientId })\n\n // Best-effort departure: BroadcastChannel doesn't have a leave event,\n // but pagehide is close enough for a demo.\n const onPageHide = (): void => {\n post({ kind: 'presence-leave', clientId })\n }\n if (typeof window !== 'undefined') window.addEventListener('pagehide', onPageHide)\n\n return {\n capabilities: { causalOrdering: true },\n\n sendBatch(batch: OpBatch) {\n post({ kind: 'batch', batch })\n },\n\n sendPresence(patch: PresencePatch) {\n const state: PresenceState = {\n ...(lastLocalPresence ?? ({} as PresenceState)),\n ...patch,\n clientId,\n }\n lastLocalPresence = state\n post({ kind: 'presence', clientId, state })\n },\n\n onBatch(cb): Unsubscribe {\n batchListeners.add(cb)\n return () => {\n batchListeners.delete(cb)\n }\n },\n\n onPresence(cb): Unsubscribe {\n presenceListeners.add(cb)\n return () => {\n presenceListeners.delete(cb)\n }\n },\n\n destroy() {\n if (typeof window !== 'undefined') window.removeEventListener('pagehide', onPageHide)\n post({ kind: 'presence-leave', clientId })\n channel.close()\n batchListeners.clear()\n presenceListeners.clear()\n },\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@canvas-harness/sync-broadcast",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "BroadcastChannel-backed SyncAdapter for canvas-harness — single-machine multi-tab demos.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist", "README.md"],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"canvas-harness",
|
|
19
|
+
"sync",
|
|
20
|
+
"broadcastchannel",
|
|
21
|
+
"collab",
|
|
22
|
+
"multi-tab"
|
|
23
|
+
],
|
|
24
|
+
"author": "Le Ha Quang <winlp4ever@gmail.com>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"homepage": "https://github.com/winlp4ever/canvas-harness#readme",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/winlp4ever/canvas-harness.git",
|
|
30
|
+
"directory": "packages/sync-broadcast"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/winlp4ever/canvas-harness/issues"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"test": "vitest run --passWithNoTests",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@canvas-harness/core": "workspace:*"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"tsup": "^8.3.0",
|
|
46
|
+
"typescript": "^5.7.0",
|
|
47
|
+
"vitest": "^3.0.0"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public",
|
|
51
|
+
"provenance": true
|
|
52
|
+
}
|
|
53
|
+
}
|