@noy-db/by-tabs 0.2.0-pre.26 → 0.2.0-pre.28

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 CHANGED
@@ -21,9 +21,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  isTabsChannelAvailable: () => isTabsChannelAvailable,
24
- tabsChannel: () => tabsChannel
24
+ tabsChannel: () => tabsChannel,
25
+ tabsCoordination: () => tabsCoordination
25
26
  });
26
27
  module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/coordination.ts
30
+ var import_by_peer = require("@noy-db/by-peer");
31
+ function tabsCoordination(channel) {
32
+ return (0, import_by_peer.channelCoordination)(channel);
33
+ }
34
+
35
+ // src/index.ts
27
36
  function tabsChannel(options) {
28
37
  const name = options.name;
29
38
  const Bc = globalThis.BroadcastChannel;
@@ -89,6 +98,7 @@ function makeNoopChannel(name) {
89
98
  // Annotate the CommonJS export names for ESM import in node:
90
99
  0 && (module.exports = {
91
100
  isTabsChannelAvailable,
92
- tabsChannel
101
+ tabsChannel,
102
+ tabsCoordination
93
103
  });
94
104
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/by-tabs** — BroadcastChannel multi-tab transport for noy-db.\n *\n * Wraps the browser `BroadcastChannel` API as a `PeerChannel`, the\n * primitive every `by-*` transport implements. Sub-millisecond fan-out\n * across tabs of the same origin; same code shape as `@noy-db/by-peer`,\n * just a different wire.\n *\n * Compose with `peerStore()` / `servePeerStore()` from `@noy-db/by-peer`\n * for an asymmetric tab-as-remote-store topology, or use the channel\n * directly to publish change events to every other tab.\n *\n * ## Threat model\n *\n * - `BroadcastChannel` is same-origin by browser policy — only documents\n * served from the same scheme + host + port can subscribe.\n * - noy-db already encrypts at rest — every tab on the channel sees\n * only AES-256-GCM ciphertext envelopes. The transport never decrypts.\n * - Untrusted tabs (e.g. third-party iframes that share an origin via\n * `document.domain` legacy) can still see the channel. Treat\n * BroadcastChannel as origin-scoped, not session-scoped.\n *\n * @packageDocumentation\n */\n\nimport type { PeerChannel } from '@noy-db/by-peer'\n\nexport type { PeerChannel }\n\n/**\n * Options for `tabsChannel()`.\n */\nexport interface TabsChannelOptions {\n /**\n * BroadcastChannel name. Two tabs subscribed to the same name see\n * each other's messages. Use a stable, vault-scoped string like\n * `'noy-db:vault-acme'` to keep different vaults isolated.\n */\n readonly name: string\n}\n\n/**\n * Wrap a `BroadcastChannel` as a `PeerChannel`.\n *\n * Returns a no-op channel (always `isOpen: false`, no-op `send`) when\n * `BroadcastChannel` is undefined — so server-side rendering, Node\n * scripts, and older browsers can import the package without crashing\n * before they feature-detect.\n */\nexport function tabsChannel(options: TabsChannelOptions): PeerChannel {\n const name = options.name\n const Bc = (globalThis as { BroadcastChannel?: typeof BroadcastChannel }).BroadcastChannel\n if (!Bc) return makeNoopChannel(name)\n\n const bc = new Bc(name)\n type Listeners = {\n message: Set<(p: string) => void>\n close: Set<() => void>\n }\n const listeners: Listeners = { message: new Set(), close: new Set() }\n let closed = false\n\n bc.addEventListener('message', (ev: MessageEvent) => {\n if (typeof ev.data !== 'string') return\n for (const fn of listeners.message) fn(ev.data)\n })\n\n function fireClose(): void {\n if (closed) return\n closed = true\n for (const fn of listeners.close) fn()\n }\n\n return {\n get isOpen() {\n return !closed\n },\n send(payload: string) {\n if (closed) throw new Error('PeerChannel closed')\n bc.postMessage(payload)\n },\n on(event: 'message' | 'close', listener: ((payload: string) => void) | (() => void)): () => void {\n if (event === 'message') {\n listeners.message.add(listener as (p: string) => void)\n return () => listeners.message.delete(listener as (p: string) => void)\n }\n listeners.close.add(listener as () => void)\n return () => listeners.close.delete(listener as () => void)\n },\n close() {\n if (closed) return\n try {\n bc.close()\n } catch {\n // ignore — channel already torn down\n }\n fireClose()\n },\n } as PeerChannel\n}\n\n/**\n * Whether `BroadcastChannel` is available in the current runtime. Use\n * this as a pre-flight before rendering UI that relies on tab-to-tab\n * sync — Node, SSR, and a handful of older browsers don't ship it.\n */\nexport function isTabsChannelAvailable(): boolean {\n return typeof (globalThis as { BroadcastChannel?: unknown }).BroadcastChannel === 'function'\n}\n\n// ─── Internals ───────────────────────────────────────────────────────────\n\nfunction makeNoopChannel(name: string): PeerChannel {\n void name\n return {\n isOpen: false,\n send() {\n throw new Error(\n '[@noy-db/by-tabs] BroadcastChannel is not available in this runtime — use isTabsChannelAvailable() to feature-detect',\n )\n },\n on(_event: 'message' | 'close', _listener: ((p: string) => void) | (() => void)): () => void {\n return () => {}\n },\n close() {},\n } as PeerChannel\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiDO,SAAS,YAAY,SAA0C;AACpE,QAAM,OAAO,QAAQ;AACrB,QAAM,KAAM,WAA8D;AAC1E,MAAI,CAAC,GAAI,QAAO,gBAAgB,IAAI;AAEpC,QAAM,KAAK,IAAI,GAAG,IAAI;AAKtB,QAAM,YAAuB,EAAE,SAAS,oBAAI,IAAI,GAAG,OAAO,oBAAI,IAAI,EAAE;AACpE,MAAI,SAAS;AAEb,KAAG,iBAAiB,WAAW,CAAC,OAAqB;AACnD,QAAI,OAAO,GAAG,SAAS,SAAU;AACjC,eAAW,MAAM,UAAU,QAAS,IAAG,GAAG,IAAI;AAAA,EAChD,CAAC;AAED,WAAS,YAAkB;AACzB,QAAI,OAAQ;AACZ,aAAS;AACT,eAAW,MAAM,UAAU,MAAO,IAAG;AAAA,EACvC;AAEA,SAAO;AAAA,IACL,IAAI,SAAS;AACX,aAAO,CAAC;AAAA,IACV;AAAA,IACA,KAAK,SAAiB;AACpB,UAAI,OAAQ,OAAM,IAAI,MAAM,oBAAoB;AAChD,SAAG,YAAY,OAAO;AAAA,IACxB;AAAA,IACA,GAAG,OAA4B,UAAkE;AAC/F,UAAI,UAAU,WAAW;AACvB,kBAAU,QAAQ,IAAI,QAA+B;AACrD,eAAO,MAAM,UAAU,QAAQ,OAAO,QAA+B;AAAA,MACvE;AACA,gBAAU,MAAM,IAAI,QAAsB;AAC1C,aAAO,MAAM,UAAU,MAAM,OAAO,QAAsB;AAAA,IAC5D;AAAA,IACA,QAAQ;AACN,UAAI,OAAQ;AACZ,UAAI;AACF,WAAG,MAAM;AAAA,MACX,QAAQ;AAAA,MAER;AACA,gBAAU;AAAA,IACZ;AAAA,EACF;AACF;AAOO,SAAS,yBAAkC;AAChD,SAAO,OAAQ,WAA8C,qBAAqB;AACpF;AAIA,SAAS,gBAAgB,MAA2B;AAClD,OAAK;AACL,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AACL,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,IACA,GAAG,QAA6B,WAA6D;AAC3F,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAAA,IACA,QAAQ;AAAA,IAAC;AAAA,EACX;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/coordination.ts"],"sourcesContent":["/**\n * **@noy-db/by-tabs** — BroadcastChannel multi-tab transport for noy-db.\n *\n * Wraps the browser `BroadcastChannel` API as a `PeerChannel`, the\n * primitive every `by-*` transport implements. Sub-millisecond fan-out\n * across tabs of the same origin; same code shape as `@noy-db/by-peer`,\n * just a different wire.\n *\n * Compose with `peerStore()` / `servePeerStore()` from `@noy-db/by-peer`\n * for an asymmetric tab-as-remote-store topology, or use the channel\n * directly to publish change events to every other tab.\n *\n * ## Threat model\n *\n * - `BroadcastChannel` is same-origin by browser policy — only documents\n * served from the same scheme + host + port can subscribe.\n * - noy-db already encrypts at rest — every tab on the channel sees\n * only AES-256-GCM ciphertext envelopes. The transport never decrypts.\n * - Untrusted tabs (e.g. third-party iframes that share an origin via\n * `document.domain` legacy) can still see the channel. Treat\n * BroadcastChannel as origin-scoped, not session-scoped.\n *\n * @packageDocumentation\n */\n\nimport type { PeerChannel } from '@noy-db/by-peer'\n\nexport type { PeerChannel }\n\nexport { tabsCoordination } from './coordination.js'\n\n/**\n * Options for `tabsChannel()`.\n */\nexport interface TabsChannelOptions {\n /**\n * BroadcastChannel name. Two tabs subscribed to the same name see\n * each other's messages. Use a stable, vault-scoped string like\n * `'noy-db:vault-acme'` to keep different vaults isolated.\n */\n readonly name: string\n}\n\n/**\n * Wrap a `BroadcastChannel` as a `PeerChannel`.\n *\n * Returns a no-op channel (always `isOpen: false`, no-op `send`) when\n * `BroadcastChannel` is undefined — so server-side rendering, Node\n * scripts, and older browsers can import the package without crashing\n * before they feature-detect.\n */\nexport function tabsChannel(options: TabsChannelOptions): PeerChannel {\n const name = options.name\n const Bc = (globalThis as { BroadcastChannel?: typeof BroadcastChannel }).BroadcastChannel\n if (!Bc) return makeNoopChannel(name)\n\n const bc = new Bc(name)\n type Listeners = {\n message: Set<(p: string) => void>\n close: Set<() => void>\n }\n const listeners: Listeners = { message: new Set(), close: new Set() }\n let closed = false\n\n bc.addEventListener('message', (ev: MessageEvent) => {\n if (typeof ev.data !== 'string') return\n for (const fn of listeners.message) fn(ev.data)\n })\n\n function fireClose(): void {\n if (closed) return\n closed = true\n for (const fn of listeners.close) fn()\n }\n\n return {\n get isOpen() {\n return !closed\n },\n send(payload: string) {\n if (closed) throw new Error('PeerChannel closed')\n bc.postMessage(payload)\n },\n on(event: 'message' | 'close', listener: ((payload: string) => void) | (() => void)): () => void {\n if (event === 'message') {\n listeners.message.add(listener as (p: string) => void)\n return () => listeners.message.delete(listener as (p: string) => void)\n }\n listeners.close.add(listener as () => void)\n return () => listeners.close.delete(listener as () => void)\n },\n close() {\n if (closed) return\n try {\n bc.close()\n } catch {\n // ignore — channel already torn down\n }\n fireClose()\n },\n } as PeerChannel\n}\n\n/**\n * Whether `BroadcastChannel` is available in the current runtime. Use\n * this as a pre-flight before rendering UI that relies on tab-to-tab\n * sync — Node, SSR, and a handful of older browsers don't ship it.\n */\nexport function isTabsChannelAvailable(): boolean {\n return typeof (globalThis as { BroadcastChannel?: unknown }).BroadcastChannel === 'function'\n}\n\n// ─── Internals ───────────────────────────────────────────────────────────\n\nfunction makeNoopChannel(name: string): PeerChannel {\n void name\n return {\n isOpen: false,\n send() {\n throw new Error(\n '[@noy-db/by-tabs] BroadcastChannel is not available in this runtime — use isTabsChannelAvailable() to feature-detect',\n )\n },\n on(_event: 'message' | 'close', _listener: ((p: string) => void) | (() => void)): () => void {\n return () => {}\n },\n close() {},\n } as PeerChannel\n}\n","/**\n * **@noy-db/by-tabs** real-time {@link CoordinationProvider} — a drain-barrier\n * transport over a {@link PeerChannel} (BroadcastChannel between tabs).\n *\n * The kernel defines the {@link CoordinationProvider} port (#469); the store\n * default polls `_meta/schema-fence`, but a multi-tab app can inject this\n * push-based provider so a schema cutover fences **instantly** instead of via\n * store polling. The migration cutover and `@klum-db/lobby` both drive the same\n * port through `coordinationStrategy`, so neither names `by-tabs`.\n *\n * ## Shared core\n *\n * The protocol (JSON `co-fence`/`co-presence` envelopes, per-vault fence +\n * presence maps, local-update-before-broadcast, prune-on-read) is transport\n * agnostic — it is **identical** for a BroadcastChannel tab and a WebRTC peer.\n * To stay DRY, that core lives once in `@noy-db/by-peer` as\n * `channelCoordination`; `tabsCoordination` is a thin, named delegation. See\n * that module for the wire protocol and local-echo notes.\n *\n * @module\n */\n\nimport { channelCoordination } from '@noy-db/by-peer'\nimport type { PeerChannel } from '@noy-db/by-peer'\nimport type { CoordinationProvider } from '@noy-db/hub/kernel'\n\n/**\n * Build a real-time {@link CoordinationProvider} backed by a {@link PeerChannel}\n * (a BroadcastChannel between tabs of the same origin).\n *\n * Delegates to the shared `channelCoordination` core in `@noy-db/by-peer` — the\n * by-tabs and by-peer transports run the identical drain-barrier protocol, so\n * the logic is not duplicated. Pass it as\n * `createNoydb({ coordinationStrategy: tabsCoordination(ch) })`, or drive it\n * directly via `runDrainBarrier`.\n */\nexport function tabsCoordination(channel: PeerChannel): CoordinationProvider {\n return channelCoordination(channel)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACsBA,qBAAoC;AAc7B,SAAS,iBAAiB,SAA4C;AAC3E,aAAO,oCAAoB,OAAO;AACpC;;;ADaO,SAAS,YAAY,SAA0C;AACpE,QAAM,OAAO,QAAQ;AACrB,QAAM,KAAM,WAA8D;AAC1E,MAAI,CAAC,GAAI,QAAO,gBAAgB,IAAI;AAEpC,QAAM,KAAK,IAAI,GAAG,IAAI;AAKtB,QAAM,YAAuB,EAAE,SAAS,oBAAI,IAAI,GAAG,OAAO,oBAAI,IAAI,EAAE;AACpE,MAAI,SAAS;AAEb,KAAG,iBAAiB,WAAW,CAAC,OAAqB;AACnD,QAAI,OAAO,GAAG,SAAS,SAAU;AACjC,eAAW,MAAM,UAAU,QAAS,IAAG,GAAG,IAAI;AAAA,EAChD,CAAC;AAED,WAAS,YAAkB;AACzB,QAAI,OAAQ;AACZ,aAAS;AACT,eAAW,MAAM,UAAU,MAAO,IAAG;AAAA,EACvC;AAEA,SAAO;AAAA,IACL,IAAI,SAAS;AACX,aAAO,CAAC;AAAA,IACV;AAAA,IACA,KAAK,SAAiB;AACpB,UAAI,OAAQ,OAAM,IAAI,MAAM,oBAAoB;AAChD,SAAG,YAAY,OAAO;AAAA,IACxB;AAAA,IACA,GAAG,OAA4B,UAAkE;AAC/F,UAAI,UAAU,WAAW;AACvB,kBAAU,QAAQ,IAAI,QAA+B;AACrD,eAAO,MAAM,UAAU,QAAQ,OAAO,QAA+B;AAAA,MACvE;AACA,gBAAU,MAAM,IAAI,QAAsB;AAC1C,aAAO,MAAM,UAAU,MAAM,OAAO,QAAsB;AAAA,IAC5D;AAAA,IACA,QAAQ;AACN,UAAI,OAAQ;AACZ,UAAI;AACF,WAAG,MAAM;AAAA,MACX,QAAQ;AAAA,MAER;AACA,gBAAU;AAAA,IACZ;AAAA,EACF;AACF;AAOO,SAAS,yBAAkC;AAChD,SAAO,OAAQ,WAA8C,qBAAqB;AACpF;AAIA,SAAS,gBAAgB,MAA2B;AAClD,OAAK;AACL,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AACL,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,IACA,GAAG,QAA6B,WAA6D;AAC3F,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAAA,IACA,QAAQ;AAAA,IAAC;AAAA,EACX;AACF;","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,40 @@
1
1
  import { PeerChannel } from '@noy-db/by-peer';
2
2
  export { PeerChannel } from '@noy-db/by-peer';
3
+ import { CoordinationProvider } from '@noy-db/hub/kernel';
4
+
5
+ /**
6
+ * **@noy-db/by-tabs** real-time {@link CoordinationProvider} — a drain-barrier
7
+ * transport over a {@link PeerChannel} (BroadcastChannel between tabs).
8
+ *
9
+ * The kernel defines the {@link CoordinationProvider} port (#469); the store
10
+ * default polls `_meta/schema-fence`, but a multi-tab app can inject this
11
+ * push-based provider so a schema cutover fences **instantly** instead of via
12
+ * store polling. The migration cutover and `@klum-db/lobby` both drive the same
13
+ * port through `coordinationStrategy`, so neither names `by-tabs`.
14
+ *
15
+ * ## Shared core
16
+ *
17
+ * The protocol (JSON `co-fence`/`co-presence` envelopes, per-vault fence +
18
+ * presence maps, local-update-before-broadcast, prune-on-read) is transport
19
+ * agnostic — it is **identical** for a BroadcastChannel tab and a WebRTC peer.
20
+ * To stay DRY, that core lives once in `@noy-db/by-peer` as
21
+ * `channelCoordination`; `tabsCoordination` is a thin, named delegation. See
22
+ * that module for the wire protocol and local-echo notes.
23
+ *
24
+ * @module
25
+ */
26
+
27
+ /**
28
+ * Build a real-time {@link CoordinationProvider} backed by a {@link PeerChannel}
29
+ * (a BroadcastChannel between tabs of the same origin).
30
+ *
31
+ * Delegates to the shared `channelCoordination` core in `@noy-db/by-peer` — the
32
+ * by-tabs and by-peer transports run the identical drain-barrier protocol, so
33
+ * the logic is not duplicated. Pass it as
34
+ * `createNoydb({ coordinationStrategy: tabsCoordination(ch) })`, or drive it
35
+ * directly via `runDrainBarrier`.
36
+ */
37
+ declare function tabsCoordination(channel: PeerChannel): CoordinationProvider;
3
38
 
4
39
  /**
5
40
  * **@noy-db/by-tabs** — BroadcastChannel multi-tab transport for noy-db.
@@ -53,4 +88,4 @@ declare function tabsChannel(options: TabsChannelOptions): PeerChannel;
53
88
  */
54
89
  declare function isTabsChannelAvailable(): boolean;
55
90
 
56
- export { type TabsChannelOptions, isTabsChannelAvailable, tabsChannel };
91
+ export { type TabsChannelOptions, isTabsChannelAvailable, tabsChannel, tabsCoordination };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,40 @@
1
1
  import { PeerChannel } from '@noy-db/by-peer';
2
2
  export { PeerChannel } from '@noy-db/by-peer';
3
+ import { CoordinationProvider } from '@noy-db/hub/kernel';
4
+
5
+ /**
6
+ * **@noy-db/by-tabs** real-time {@link CoordinationProvider} — a drain-barrier
7
+ * transport over a {@link PeerChannel} (BroadcastChannel between tabs).
8
+ *
9
+ * The kernel defines the {@link CoordinationProvider} port (#469); the store
10
+ * default polls `_meta/schema-fence`, but a multi-tab app can inject this
11
+ * push-based provider so a schema cutover fences **instantly** instead of via
12
+ * store polling. The migration cutover and `@klum-db/lobby` both drive the same
13
+ * port through `coordinationStrategy`, so neither names `by-tabs`.
14
+ *
15
+ * ## Shared core
16
+ *
17
+ * The protocol (JSON `co-fence`/`co-presence` envelopes, per-vault fence +
18
+ * presence maps, local-update-before-broadcast, prune-on-read) is transport
19
+ * agnostic — it is **identical** for a BroadcastChannel tab and a WebRTC peer.
20
+ * To stay DRY, that core lives once in `@noy-db/by-peer` as
21
+ * `channelCoordination`; `tabsCoordination` is a thin, named delegation. See
22
+ * that module for the wire protocol and local-echo notes.
23
+ *
24
+ * @module
25
+ */
26
+
27
+ /**
28
+ * Build a real-time {@link CoordinationProvider} backed by a {@link PeerChannel}
29
+ * (a BroadcastChannel between tabs of the same origin).
30
+ *
31
+ * Delegates to the shared `channelCoordination` core in `@noy-db/by-peer` — the
32
+ * by-tabs and by-peer transports run the identical drain-barrier protocol, so
33
+ * the logic is not duplicated. Pass it as
34
+ * `createNoydb({ coordinationStrategy: tabsCoordination(ch) })`, or drive it
35
+ * directly via `runDrainBarrier`.
36
+ */
37
+ declare function tabsCoordination(channel: PeerChannel): CoordinationProvider;
3
38
 
4
39
  /**
5
40
  * **@noy-db/by-tabs** — BroadcastChannel multi-tab transport for noy-db.
@@ -53,4 +88,4 @@ declare function tabsChannel(options: TabsChannelOptions): PeerChannel;
53
88
  */
54
89
  declare function isTabsChannelAvailable(): boolean;
55
90
 
56
- export { type TabsChannelOptions, isTabsChannelAvailable, tabsChannel };
91
+ export { type TabsChannelOptions, isTabsChannelAvailable, tabsChannel, tabsCoordination };
package/dist/index.js CHANGED
@@ -1,3 +1,9 @@
1
+ // src/coordination.ts
2
+ import { channelCoordination } from "@noy-db/by-peer";
3
+ function tabsCoordination(channel) {
4
+ return channelCoordination(channel);
5
+ }
6
+
1
7
  // src/index.ts
2
8
  function tabsChannel(options) {
3
9
  const name = options.name;
@@ -63,6 +69,7 @@ function makeNoopChannel(name) {
63
69
  }
64
70
  export {
65
71
  isTabsChannelAvailable,
66
- tabsChannel
72
+ tabsChannel,
73
+ tabsCoordination
67
74
  };
68
75
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/by-tabs** — BroadcastChannel multi-tab transport for noy-db.\n *\n * Wraps the browser `BroadcastChannel` API as a `PeerChannel`, the\n * primitive every `by-*` transport implements. Sub-millisecond fan-out\n * across tabs of the same origin; same code shape as `@noy-db/by-peer`,\n * just a different wire.\n *\n * Compose with `peerStore()` / `servePeerStore()` from `@noy-db/by-peer`\n * for an asymmetric tab-as-remote-store topology, or use the channel\n * directly to publish change events to every other tab.\n *\n * ## Threat model\n *\n * - `BroadcastChannel` is same-origin by browser policy — only documents\n * served from the same scheme + host + port can subscribe.\n * - noy-db already encrypts at rest — every tab on the channel sees\n * only AES-256-GCM ciphertext envelopes. The transport never decrypts.\n * - Untrusted tabs (e.g. third-party iframes that share an origin via\n * `document.domain` legacy) can still see the channel. Treat\n * BroadcastChannel as origin-scoped, not session-scoped.\n *\n * @packageDocumentation\n */\n\nimport type { PeerChannel } from '@noy-db/by-peer'\n\nexport type { PeerChannel }\n\n/**\n * Options for `tabsChannel()`.\n */\nexport interface TabsChannelOptions {\n /**\n * BroadcastChannel name. Two tabs subscribed to the same name see\n * each other's messages. Use a stable, vault-scoped string like\n * `'noy-db:vault-acme'` to keep different vaults isolated.\n */\n readonly name: string\n}\n\n/**\n * Wrap a `BroadcastChannel` as a `PeerChannel`.\n *\n * Returns a no-op channel (always `isOpen: false`, no-op `send`) when\n * `BroadcastChannel` is undefined — so server-side rendering, Node\n * scripts, and older browsers can import the package without crashing\n * before they feature-detect.\n */\nexport function tabsChannel(options: TabsChannelOptions): PeerChannel {\n const name = options.name\n const Bc = (globalThis as { BroadcastChannel?: typeof BroadcastChannel }).BroadcastChannel\n if (!Bc) return makeNoopChannel(name)\n\n const bc = new Bc(name)\n type Listeners = {\n message: Set<(p: string) => void>\n close: Set<() => void>\n }\n const listeners: Listeners = { message: new Set(), close: new Set() }\n let closed = false\n\n bc.addEventListener('message', (ev: MessageEvent) => {\n if (typeof ev.data !== 'string') return\n for (const fn of listeners.message) fn(ev.data)\n })\n\n function fireClose(): void {\n if (closed) return\n closed = true\n for (const fn of listeners.close) fn()\n }\n\n return {\n get isOpen() {\n return !closed\n },\n send(payload: string) {\n if (closed) throw new Error('PeerChannel closed')\n bc.postMessage(payload)\n },\n on(event: 'message' | 'close', listener: ((payload: string) => void) | (() => void)): () => void {\n if (event === 'message') {\n listeners.message.add(listener as (p: string) => void)\n return () => listeners.message.delete(listener as (p: string) => void)\n }\n listeners.close.add(listener as () => void)\n return () => listeners.close.delete(listener as () => void)\n },\n close() {\n if (closed) return\n try {\n bc.close()\n } catch {\n // ignore — channel already torn down\n }\n fireClose()\n },\n } as PeerChannel\n}\n\n/**\n * Whether `BroadcastChannel` is available in the current runtime. Use\n * this as a pre-flight before rendering UI that relies on tab-to-tab\n * sync — Node, SSR, and a handful of older browsers don't ship it.\n */\nexport function isTabsChannelAvailable(): boolean {\n return typeof (globalThis as { BroadcastChannel?: unknown }).BroadcastChannel === 'function'\n}\n\n// ─── Internals ───────────────────────────────────────────────────────────\n\nfunction makeNoopChannel(name: string): PeerChannel {\n void name\n return {\n isOpen: false,\n send() {\n throw new Error(\n '[@noy-db/by-tabs] BroadcastChannel is not available in this runtime — use isTabsChannelAvailable() to feature-detect',\n )\n },\n on(_event: 'message' | 'close', _listener: ((p: string) => void) | (() => void)): () => void {\n return () => {}\n },\n close() {},\n } as PeerChannel\n}\n"],"mappings":";AAiDO,SAAS,YAAY,SAA0C;AACpE,QAAM,OAAO,QAAQ;AACrB,QAAM,KAAM,WAA8D;AAC1E,MAAI,CAAC,GAAI,QAAO,gBAAgB,IAAI;AAEpC,QAAM,KAAK,IAAI,GAAG,IAAI;AAKtB,QAAM,YAAuB,EAAE,SAAS,oBAAI,IAAI,GAAG,OAAO,oBAAI,IAAI,EAAE;AACpE,MAAI,SAAS;AAEb,KAAG,iBAAiB,WAAW,CAAC,OAAqB;AACnD,QAAI,OAAO,GAAG,SAAS,SAAU;AACjC,eAAW,MAAM,UAAU,QAAS,IAAG,GAAG,IAAI;AAAA,EAChD,CAAC;AAED,WAAS,YAAkB;AACzB,QAAI,OAAQ;AACZ,aAAS;AACT,eAAW,MAAM,UAAU,MAAO,IAAG;AAAA,EACvC;AAEA,SAAO;AAAA,IACL,IAAI,SAAS;AACX,aAAO,CAAC;AAAA,IACV;AAAA,IACA,KAAK,SAAiB;AACpB,UAAI,OAAQ,OAAM,IAAI,MAAM,oBAAoB;AAChD,SAAG,YAAY,OAAO;AAAA,IACxB;AAAA,IACA,GAAG,OAA4B,UAAkE;AAC/F,UAAI,UAAU,WAAW;AACvB,kBAAU,QAAQ,IAAI,QAA+B;AACrD,eAAO,MAAM,UAAU,QAAQ,OAAO,QAA+B;AAAA,MACvE;AACA,gBAAU,MAAM,IAAI,QAAsB;AAC1C,aAAO,MAAM,UAAU,MAAM,OAAO,QAAsB;AAAA,IAC5D;AAAA,IACA,QAAQ;AACN,UAAI,OAAQ;AACZ,UAAI;AACF,WAAG,MAAM;AAAA,MACX,QAAQ;AAAA,MAER;AACA,gBAAU;AAAA,IACZ;AAAA,EACF;AACF;AAOO,SAAS,yBAAkC;AAChD,SAAO,OAAQ,WAA8C,qBAAqB;AACpF;AAIA,SAAS,gBAAgB,MAA2B;AAClD,OAAK;AACL,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AACL,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,IACA,GAAG,QAA6B,WAA6D;AAC3F,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAAA,IACA,QAAQ;AAAA,IAAC;AAAA,EACX;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/coordination.ts","../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/by-tabs** real-time {@link CoordinationProvider} a drain-barrier\n * transport over a {@link PeerChannel} (BroadcastChannel between tabs).\n *\n * The kernel defines the {@link CoordinationProvider} port (#469); the store\n * default polls `_meta/schema-fence`, but a multi-tab app can inject this\n * push-based provider so a schema cutover fences **instantly** instead of via\n * store polling. The migration cutover and `@klum-db/lobby` both drive the same\n * port through `coordinationStrategy`, so neither names `by-tabs`.\n *\n * ## Shared core\n *\n * The protocol (JSON `co-fence`/`co-presence` envelopes, per-vault fence +\n * presence maps, local-update-before-broadcast, prune-on-read) is transport\n * agnostic — it is **identical** for a BroadcastChannel tab and a WebRTC peer.\n * To stay DRY, that core lives once in `@noy-db/by-peer` as\n * `channelCoordination`; `tabsCoordination` is a thin, named delegation. See\n * that module for the wire protocol and local-echo notes.\n *\n * @module\n */\n\nimport { channelCoordination } from '@noy-db/by-peer'\nimport type { PeerChannel } from '@noy-db/by-peer'\nimport type { CoordinationProvider } from '@noy-db/hub/kernel'\n\n/**\n * Build a real-time {@link CoordinationProvider} backed by a {@link PeerChannel}\n * (a BroadcastChannel between tabs of the same origin).\n *\n * Delegates to the shared `channelCoordination` core in `@noy-db/by-peer` — the\n * by-tabs and by-peer transports run the identical drain-barrier protocol, so\n * the logic is not duplicated. Pass it as\n * `createNoydb({ coordinationStrategy: tabsCoordination(ch) })`, or drive it\n * directly via `runDrainBarrier`.\n */\nexport function tabsCoordination(channel: PeerChannel): CoordinationProvider {\n return channelCoordination(channel)\n}\n","/**\n * **@noy-db/by-tabs** — BroadcastChannel multi-tab transport for noy-db.\n *\n * Wraps the browser `BroadcastChannel` API as a `PeerChannel`, the\n * primitive every `by-*` transport implements. Sub-millisecond fan-out\n * across tabs of the same origin; same code shape as `@noy-db/by-peer`,\n * just a different wire.\n *\n * Compose with `peerStore()` / `servePeerStore()` from `@noy-db/by-peer`\n * for an asymmetric tab-as-remote-store topology, or use the channel\n * directly to publish change events to every other tab.\n *\n * ## Threat model\n *\n * - `BroadcastChannel` is same-origin by browser policy — only documents\n * served from the same scheme + host + port can subscribe.\n * - noy-db already encrypts at rest — every tab on the channel sees\n * only AES-256-GCM ciphertext envelopes. The transport never decrypts.\n * - Untrusted tabs (e.g. third-party iframes that share an origin via\n * `document.domain` legacy) can still see the channel. Treat\n * BroadcastChannel as origin-scoped, not session-scoped.\n *\n * @packageDocumentation\n */\n\nimport type { PeerChannel } from '@noy-db/by-peer'\n\nexport type { PeerChannel }\n\nexport { tabsCoordination } from './coordination.js'\n\n/**\n * Options for `tabsChannel()`.\n */\nexport interface TabsChannelOptions {\n /**\n * BroadcastChannel name. Two tabs subscribed to the same name see\n * each other's messages. Use a stable, vault-scoped string like\n * `'noy-db:vault-acme'` to keep different vaults isolated.\n */\n readonly name: string\n}\n\n/**\n * Wrap a `BroadcastChannel` as a `PeerChannel`.\n *\n * Returns a no-op channel (always `isOpen: false`, no-op `send`) when\n * `BroadcastChannel` is undefined — so server-side rendering, Node\n * scripts, and older browsers can import the package without crashing\n * before they feature-detect.\n */\nexport function tabsChannel(options: TabsChannelOptions): PeerChannel {\n const name = options.name\n const Bc = (globalThis as { BroadcastChannel?: typeof BroadcastChannel }).BroadcastChannel\n if (!Bc) return makeNoopChannel(name)\n\n const bc = new Bc(name)\n type Listeners = {\n message: Set<(p: string) => void>\n close: Set<() => void>\n }\n const listeners: Listeners = { message: new Set(), close: new Set() }\n let closed = false\n\n bc.addEventListener('message', (ev: MessageEvent) => {\n if (typeof ev.data !== 'string') return\n for (const fn of listeners.message) fn(ev.data)\n })\n\n function fireClose(): void {\n if (closed) return\n closed = true\n for (const fn of listeners.close) fn()\n }\n\n return {\n get isOpen() {\n return !closed\n },\n send(payload: string) {\n if (closed) throw new Error('PeerChannel closed')\n bc.postMessage(payload)\n },\n on(event: 'message' | 'close', listener: ((payload: string) => void) | (() => void)): () => void {\n if (event === 'message') {\n listeners.message.add(listener as (p: string) => void)\n return () => listeners.message.delete(listener as (p: string) => void)\n }\n listeners.close.add(listener as () => void)\n return () => listeners.close.delete(listener as () => void)\n },\n close() {\n if (closed) return\n try {\n bc.close()\n } catch {\n // ignore — channel already torn down\n }\n fireClose()\n },\n } as PeerChannel\n}\n\n/**\n * Whether `BroadcastChannel` is available in the current runtime. Use\n * this as a pre-flight before rendering UI that relies on tab-to-tab\n * sync — Node, SSR, and a handful of older browsers don't ship it.\n */\nexport function isTabsChannelAvailable(): boolean {\n return typeof (globalThis as { BroadcastChannel?: unknown }).BroadcastChannel === 'function'\n}\n\n// ─── Internals ───────────────────────────────────────────────────────────\n\nfunction makeNoopChannel(name: string): PeerChannel {\n void name\n return {\n isOpen: false,\n send() {\n throw new Error(\n '[@noy-db/by-tabs] BroadcastChannel is not available in this runtime — use isTabsChannelAvailable() to feature-detect',\n )\n },\n on(_event: 'message' | 'close', _listener: ((p: string) => void) | (() => void)): () => void {\n return () => {}\n },\n close() {},\n } as PeerChannel\n}\n"],"mappings":";AAsBA,SAAS,2BAA2B;AAc7B,SAAS,iBAAiB,SAA4C;AAC3E,SAAO,oBAAoB,OAAO;AACpC;;;ACaO,SAAS,YAAY,SAA0C;AACpE,QAAM,OAAO,QAAQ;AACrB,QAAM,KAAM,WAA8D;AAC1E,MAAI,CAAC,GAAI,QAAO,gBAAgB,IAAI;AAEpC,QAAM,KAAK,IAAI,GAAG,IAAI;AAKtB,QAAM,YAAuB,EAAE,SAAS,oBAAI,IAAI,GAAG,OAAO,oBAAI,IAAI,EAAE;AACpE,MAAI,SAAS;AAEb,KAAG,iBAAiB,WAAW,CAAC,OAAqB;AACnD,QAAI,OAAO,GAAG,SAAS,SAAU;AACjC,eAAW,MAAM,UAAU,QAAS,IAAG,GAAG,IAAI;AAAA,EAChD,CAAC;AAED,WAAS,YAAkB;AACzB,QAAI,OAAQ;AACZ,aAAS;AACT,eAAW,MAAM,UAAU,MAAO,IAAG;AAAA,EACvC;AAEA,SAAO;AAAA,IACL,IAAI,SAAS;AACX,aAAO,CAAC;AAAA,IACV;AAAA,IACA,KAAK,SAAiB;AACpB,UAAI,OAAQ,OAAM,IAAI,MAAM,oBAAoB;AAChD,SAAG,YAAY,OAAO;AAAA,IACxB;AAAA,IACA,GAAG,OAA4B,UAAkE;AAC/F,UAAI,UAAU,WAAW;AACvB,kBAAU,QAAQ,IAAI,QAA+B;AACrD,eAAO,MAAM,UAAU,QAAQ,OAAO,QAA+B;AAAA,MACvE;AACA,gBAAU,MAAM,IAAI,QAAsB;AAC1C,aAAO,MAAM,UAAU,MAAM,OAAO,QAAsB;AAAA,IAC5D;AAAA,IACA,QAAQ;AACN,UAAI,OAAQ;AACZ,UAAI;AACF,WAAG,MAAM;AAAA,MACX,QAAQ;AAAA,MAER;AACA,gBAAU;AAAA,IACZ;AAAA,EACF;AACF;AAOO,SAAS,yBAAkC;AAChD,SAAO,OAAQ,WAA8C,qBAAqB;AACpF;AAIA,SAAS,gBAAgB,MAA2B;AAClD,OAAK;AACL,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AACL,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,IACA,GAAG,QAA6B,WAA6D;AAC3F,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAAA,IACA,QAAQ;AAAA,IAAC;AAAA,EACX;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/by-tabs",
3
- "version": "0.2.0-pre.26",
3
+ "version": "0.2.0-pre.28",
4
4
  "description": "BroadcastChannel multi-tab transport for noy-db — sync between tabs of the same browser without round-tripping the storage backend",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -39,13 +39,13 @@
39
39
  "node": ">=18.0.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "@noy-db/by-peer": "0.2.0-pre.26",
43
- "@noy-db/hub": "0.2.0-pre.26"
42
+ "@noy-db/by-peer": "0.2.0-pre.28",
43
+ "@noy-db/hub": "0.2.0-pre.28"
44
44
  },
45
45
  "devDependencies": {
46
- "@noy-db/by-peer": "0.2.0-pre.26",
47
- "@noy-db/hub": "0.2.0-pre.26",
48
- "@noy-db/to-memory": "0.2.0-pre.26"
46
+ "@noy-db/by-peer": "0.2.0-pre.28",
47
+ "@noy-db/to-memory": "0.2.0-pre.28",
48
+ "@noy-db/hub": "0.2.0-pre.28"
49
49
  },
50
50
  "keywords": [
51
51
  "noy-db",