@use-everywhere/core 0.10.1 → 0.11.1

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.
@@ -20,34 +20,80 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/shared-worker.ts
21
21
  var shared_worker_exports = {};
22
22
  __export(shared_worker_exports, {
23
+ relay: () => relay,
23
24
  startRelay: () => startRelay
24
25
  });
25
26
  module.exports = __toCommonJS(shared_worker_exports);
26
27
  function startRelay(scope) {
27
28
  const ports = /* @__PURE__ */ new Set();
29
+ const local = /* @__PURE__ */ new Set();
30
+ const fanOut = (from, data) => {
31
+ const dead = [];
32
+ for (const peer of ports) {
33
+ if (peer === from) continue;
34
+ try {
35
+ peer.postMessage(data);
36
+ } catch {
37
+ dead.push(peer);
38
+ }
39
+ }
40
+ for (const corpse of dead) {
41
+ ports.delete(corpse);
42
+ local.delete(corpse);
43
+ }
44
+ };
28
45
  scope.onconnect = (event) => {
29
46
  const port = event.ports[0];
30
47
  if (!port) return;
31
48
  ports.add(port);
32
- port.addEventListener("message", (message) => {
33
- const dead = [];
34
- for (const peer of ports) {
35
- if (peer === port) continue;
36
- try {
37
- peer.postMessage(message.data);
38
- } catch {
39
- dead.push(peer);
40
- }
41
- }
42
- for (const corpse of dead) ports.delete(corpse);
43
- });
49
+ port.addEventListener("message", (message) => fanOut(port, message.data));
44
50
  port.start?.();
45
51
  };
52
+ return {
53
+ broadcast: (data) => fanOut(null, data),
54
+ get size() {
55
+ return ports.size - local.size;
56
+ },
57
+ connect() {
58
+ const listeners = /* @__PURE__ */ new Set();
59
+ let closed = false;
60
+ const seat = {
61
+ // Asynchronous on purpose. `fanOut` runs inside a port's message
62
+ // listener, and delivering synchronously from there produces the
63
+ // re-entrancy a real MessagePort never could. Nothing is cloned: every
64
+ // value reaching this seat already crossed a port boundary and was
65
+ // structured-cloned there, so there is no live reference to leak.
66
+ postMessage: (data) => {
67
+ queueMicrotask(() => {
68
+ for (const listener of listeners) listener(data);
69
+ });
70
+ }
71
+ };
72
+ ports.add(seat);
73
+ local.add(seat);
74
+ return {
75
+ kind: "shared-worker",
76
+ post: (data) => {
77
+ if (!closed) fanOut(seat, data);
78
+ },
79
+ subscribe: (listener) => {
80
+ listeners.add(listener);
81
+ return () => listeners.delete(listener);
82
+ },
83
+ close: () => {
84
+ if (closed) return;
85
+ closed = true;
86
+ listeners.clear();
87
+ ports.delete(seat);
88
+ local.delete(seat);
89
+ }
90
+ };
91
+ }
92
+ };
46
93
  }
47
- if (typeof self !== "undefined" && "onconnect" in self) {
48
- startRelay(self);
49
- }
94
+ var relay = typeof self !== "undefined" && "onconnect" in self ? startRelay(self) : void 0;
50
95
  // Annotate the CommonJS export names for ESM import in node:
51
96
  0 && (module.exports = {
97
+ relay,
52
98
  startRelay
53
99
  });
@@ -1,3 +1,5 @@
1
+ import { T as Transport } from './transport.types-CV1WZOhy.cjs';
2
+
1
3
  /**
2
4
  * The relay that runs *inside* a SharedWorker. Host a one-line module and point
3
5
  * `SharedWorkerTransport` at it:
@@ -12,10 +14,25 @@
12
14
  * has one job, and making the caller remember to invoke it adds a way to get a
13
15
  * worker that accepts connections and forwards nothing.
14
16
  *
15
- * `startRelay` is exported anyway, for a worker that also does other work — an
16
- * app whose relay owns the WebSocket, say — and for tests, which cannot install
17
- * a global `onconnect`.
17
+ * For a worker that also does other work — one that owns the WebSocket, say —
18
+ * import {@link relay} and join the bus through it:
19
+ *
20
+ * ```js
21
+ * // socket-worker.js
22
+ * import { relay } from '@use-everywhere/core/shared-worker';
23
+ * import { createSharedStore } from '@use-everywhere/core';
24
+ *
25
+ * const store = createSharedStore('feed', { tick: null }, { transport: () => relay.connect() });
26
+ * new WebSocket('wss://example.com/feed').onmessage = (e) => store.set('tick', JSON.parse(e.data));
27
+ * ```
28
+ *
29
+ * `startRelay` is exported for tests, which cannot install a global `onconnect`,
30
+ * and for a scope this module cannot detect. Prefer {@link relay} inside a real
31
+ * worker: calling `startRelay(self)` there installs a *second* relay over the
32
+ * one this module already installed, and the first one's ports are then held by
33
+ * a handler nothing will ever call again.
18
34
  */
35
+
19
36
  interface RelayPort {
20
37
  postMessage(data: unknown): void;
21
38
  start?: () => void;
@@ -28,10 +45,36 @@ interface RelayScope {
28
45
  ports: readonly RelayPort[];
29
46
  }) => void) | null;
30
47
  }
48
+ /** A running relay: the ports it holds, and the worker's own seat on the bus. */
49
+ interface Relay {
50
+ /**
51
+ * Join the relay from inside the worker, as one more peer.
52
+ *
53
+ * Returns a `Transport`, so worker-side code uses `createSharedStore` and the
54
+ * rest of the library exactly as a tab does. That is the point: the wire
55
+ * format stays the engines' business, and a worker that wants to publish
56
+ * never has to hand-assemble an envelope the protocol might redefine.
57
+ */
58
+ connect(): Transport;
59
+ /**
60
+ * Fan raw data out to every connected port.
61
+ *
62
+ * The escape hatch, for a worker speaking some protocol of its own. Anything
63
+ * the library's engines will read should go through {@link connect} instead —
64
+ * this bypasses them entirely.
65
+ */
66
+ broadcast(data: unknown): void;
67
+ /**
68
+ * How many ports are attached. A live count of the contexts that opened this
69
+ * worker, which is what you want in order to idle a socket while nobody is
70
+ * looking. `connect()` seats are not counted: the worker is not its own tab.
71
+ */
72
+ readonly size: number;
73
+ }
31
74
  /**
32
- * Fan every message out to the other connected ports.
75
+ * Fan every message out to the other connected peers.
33
76
  *
34
- * Two properties the buses above this depend on:
77
+ * Three properties the buses above this depend on:
35
78
  *
36
79
  * 1. **No self-echo.** A sender never receives its own message, matching
37
80
  * BroadcastChannel exactly. Without it every store would apply its own
@@ -40,7 +83,17 @@ interface RelayScope {
40
83
  * closing its port leaves an entangled port whose `postMessage` throws; one
41
84
  * of those must not stop delivery to the tabs still listening, so the send
42
85
  * loop is individually guarded and the corpse is pruned.
86
+ * 3. **The worker is a peer, not a special case.** A local seat from
87
+ * {@link Relay.connect} is held in the same set as the ports, so both rules
88
+ * above apply to it without a second code path to keep in agreement.
89
+ */
90
+ declare function startRelay(scope: RelayScope): Relay;
91
+ /**
92
+ * The relay this module installed on import — present only when the module is
93
+ * running in a SharedWorker, and `undefined` anywhere else, so that importing
94
+ * it from a bundler tracing entry points, a test runner, or a server render is
95
+ * inert rather than a `ReferenceError`.
43
96
  */
44
- declare function startRelay(scope: RelayScope): void;
97
+ declare const relay: Relay | undefined;
45
98
 
46
- export { type RelayPort, type RelayScope, startRelay };
99
+ export { type Relay, type RelayPort, type RelayScope, relay, startRelay };
@@ -1,3 +1,5 @@
1
+ import { T as Transport } from './transport.types-CV1WZOhy.js';
2
+
1
3
  /**
2
4
  * The relay that runs *inside* a SharedWorker. Host a one-line module and point
3
5
  * `SharedWorkerTransport` at it:
@@ -12,10 +14,25 @@
12
14
  * has one job, and making the caller remember to invoke it adds a way to get a
13
15
  * worker that accepts connections and forwards nothing.
14
16
  *
15
- * `startRelay` is exported anyway, for a worker that also does other work — an
16
- * app whose relay owns the WebSocket, say — and for tests, which cannot install
17
- * a global `onconnect`.
17
+ * For a worker that also does other work — one that owns the WebSocket, say —
18
+ * import {@link relay} and join the bus through it:
19
+ *
20
+ * ```js
21
+ * // socket-worker.js
22
+ * import { relay } from '@use-everywhere/core/shared-worker';
23
+ * import { createSharedStore } from '@use-everywhere/core';
24
+ *
25
+ * const store = createSharedStore('feed', { tick: null }, { transport: () => relay.connect() });
26
+ * new WebSocket('wss://example.com/feed').onmessage = (e) => store.set('tick', JSON.parse(e.data));
27
+ * ```
28
+ *
29
+ * `startRelay` is exported for tests, which cannot install a global `onconnect`,
30
+ * and for a scope this module cannot detect. Prefer {@link relay} inside a real
31
+ * worker: calling `startRelay(self)` there installs a *second* relay over the
32
+ * one this module already installed, and the first one's ports are then held by
33
+ * a handler nothing will ever call again.
18
34
  */
35
+
19
36
  interface RelayPort {
20
37
  postMessage(data: unknown): void;
21
38
  start?: () => void;
@@ -28,10 +45,36 @@ interface RelayScope {
28
45
  ports: readonly RelayPort[];
29
46
  }) => void) | null;
30
47
  }
48
+ /** A running relay: the ports it holds, and the worker's own seat on the bus. */
49
+ interface Relay {
50
+ /**
51
+ * Join the relay from inside the worker, as one more peer.
52
+ *
53
+ * Returns a `Transport`, so worker-side code uses `createSharedStore` and the
54
+ * rest of the library exactly as a tab does. That is the point: the wire
55
+ * format stays the engines' business, and a worker that wants to publish
56
+ * never has to hand-assemble an envelope the protocol might redefine.
57
+ */
58
+ connect(): Transport;
59
+ /**
60
+ * Fan raw data out to every connected port.
61
+ *
62
+ * The escape hatch, for a worker speaking some protocol of its own. Anything
63
+ * the library's engines will read should go through {@link connect} instead —
64
+ * this bypasses them entirely.
65
+ */
66
+ broadcast(data: unknown): void;
67
+ /**
68
+ * How many ports are attached. A live count of the contexts that opened this
69
+ * worker, which is what you want in order to idle a socket while nobody is
70
+ * looking. `connect()` seats are not counted: the worker is not its own tab.
71
+ */
72
+ readonly size: number;
73
+ }
31
74
  /**
32
- * Fan every message out to the other connected ports.
75
+ * Fan every message out to the other connected peers.
33
76
  *
34
- * Two properties the buses above this depend on:
77
+ * Three properties the buses above this depend on:
35
78
  *
36
79
  * 1. **No self-echo.** A sender never receives its own message, matching
37
80
  * BroadcastChannel exactly. Without it every store would apply its own
@@ -40,7 +83,17 @@ interface RelayScope {
40
83
  * closing its port leaves an entangled port whose `postMessage` throws; one
41
84
  * of those must not stop delivery to the tabs still listening, so the send
42
85
  * loop is individually guarded and the corpse is pruned.
86
+ * 3. **The worker is a peer, not a special case.** A local seat from
87
+ * {@link Relay.connect} is held in the same set as the ports, so both rules
88
+ * above apply to it without a second code path to keep in agreement.
89
+ */
90
+ declare function startRelay(scope: RelayScope): Relay;
91
+ /**
92
+ * The relay this module installed on import — present only when the module is
93
+ * running in a SharedWorker, and `undefined` anywhere else, so that importing
94
+ * it from a bundler tracing entry points, a test runner, or a server render is
95
+ * inert rather than a `ReferenceError`.
43
96
  */
44
- declare function startRelay(scope: RelayScope): void;
97
+ declare const relay: Relay | undefined;
45
98
 
46
- export { type RelayPort, type RelayScope, startRelay };
99
+ export { type Relay, type RelayPort, type RelayScope, relay, startRelay };
@@ -1,28 +1,73 @@
1
1
  // src/shared-worker.ts
2
2
  function startRelay(scope) {
3
3
  const ports = /* @__PURE__ */ new Set();
4
+ const local = /* @__PURE__ */ new Set();
5
+ const fanOut = (from, data) => {
6
+ const dead = [];
7
+ for (const peer of ports) {
8
+ if (peer === from) continue;
9
+ try {
10
+ peer.postMessage(data);
11
+ } catch {
12
+ dead.push(peer);
13
+ }
14
+ }
15
+ for (const corpse of dead) {
16
+ ports.delete(corpse);
17
+ local.delete(corpse);
18
+ }
19
+ };
4
20
  scope.onconnect = (event) => {
5
21
  const port = event.ports[0];
6
22
  if (!port) return;
7
23
  ports.add(port);
8
- port.addEventListener("message", (message) => {
9
- const dead = [];
10
- for (const peer of ports) {
11
- if (peer === port) continue;
12
- try {
13
- peer.postMessage(message.data);
14
- } catch {
15
- dead.push(peer);
16
- }
17
- }
18
- for (const corpse of dead) ports.delete(corpse);
19
- });
24
+ port.addEventListener("message", (message) => fanOut(port, message.data));
20
25
  port.start?.();
21
26
  };
27
+ return {
28
+ broadcast: (data) => fanOut(null, data),
29
+ get size() {
30
+ return ports.size - local.size;
31
+ },
32
+ connect() {
33
+ const listeners = /* @__PURE__ */ new Set();
34
+ let closed = false;
35
+ const seat = {
36
+ // Asynchronous on purpose. `fanOut` runs inside a port's message
37
+ // listener, and delivering synchronously from there produces the
38
+ // re-entrancy a real MessagePort never could. Nothing is cloned: every
39
+ // value reaching this seat already crossed a port boundary and was
40
+ // structured-cloned there, so there is no live reference to leak.
41
+ postMessage: (data) => {
42
+ queueMicrotask(() => {
43
+ for (const listener of listeners) listener(data);
44
+ });
45
+ }
46
+ };
47
+ ports.add(seat);
48
+ local.add(seat);
49
+ return {
50
+ kind: "shared-worker",
51
+ post: (data) => {
52
+ if (!closed) fanOut(seat, data);
53
+ },
54
+ subscribe: (listener) => {
55
+ listeners.add(listener);
56
+ return () => listeners.delete(listener);
57
+ },
58
+ close: () => {
59
+ if (closed) return;
60
+ closed = true;
61
+ listeners.clear();
62
+ ports.delete(seat);
63
+ local.delete(seat);
64
+ }
65
+ };
66
+ }
67
+ };
22
68
  }
23
- if (typeof self !== "undefined" && "onconnect" in self) {
24
- startRelay(self);
25
- }
69
+ var relay = typeof self !== "undefined" && "onconnect" in self ? startRelay(self) : void 0;
26
70
  export {
71
+ relay,
27
72
  startRelay
28
73
  };
package/llms.txt ADDED
@@ -0,0 +1,84 @@
1
+ # @use-everywhere/core
2
+
3
+ > The framework-free engine behind `use-everywhere`: shared state, typed
4
+ > messages, presence, leader election and a cross-origin window channel, for any
5
+ > tab, window or worker on an origin. No React, no framework, no server. If you
6
+ > are using React, install `use-everywhere` instead — it re-exports all of this
7
+ > plus the hooks, so importing both is redundant. Ships ESM and CommonJS.
8
+
9
+ ## Install
10
+
11
+ npm install @use-everywhere/core
12
+
13
+ No runtime dependencies and no peers. Use this package directly only when you
14
+ are NOT using React.
15
+
16
+ ## Minimal working example
17
+
18
+ ```ts
19
+ import { createSharedStore } from '@use-everywhere/core';
20
+
21
+ const store = createSharedStore({ name: 'settings' }, { theme: 'light' });
22
+
23
+ // Fires whenever any tab on the origin writes.
24
+ store.subscribe(() => console.log(store.get()));
25
+
26
+ store.set({ theme: 'dark' });
27
+ ```
28
+
29
+ ## API
30
+
31
+ | Export | Kind | What it does |
32
+ | --- | --- | --- |
33
+ | `createSharedStore` | factory | Shared state, last-writer-wins per key |
34
+ | `createSharedReducer` | factory | The same, dispatch-shaped |
35
+ | `createChannel` | factory | Typed fire-and-forget pub/sub between tabs |
36
+ | `createPresence` | factory | Who else is open, with heartbeat and pruning |
37
+ | `createLeader` | factory | Lease-and-claim election with a sticky incumbent |
38
+ | `createNamespace` | factory | Prefix every bus name, for micro-frontends |
39
+ | `openWindow` | function | Open a cross-origin window on a typed 1:1 channel |
40
+ | `connectToOpener` | function | The other half, called by the opened page |
41
+ | `newer` | function | The version clock every conflict is settled by |
42
+ | `observeBus` | function | Every wire crossing a bus, in both directions |
43
+ | `enableDebug` | function | Turn on development diagnostics |
44
+ | `getBusNames` | function | Which buses exist right now |
45
+ | `getTransportKind` | function | Which transport a bus resolved to |
46
+ | `getWireSkew` | function | Version skew between this tab and its peers |
47
+ | `BroadcastChannelTransport` | class | The same-origin default |
48
+ | `StorageTransport` | class | `storage`-event fallback |
49
+ | `SharedWorkerTransport` | class | One worker instead of N tabs |
50
+ | `NoopTransport` | class | Does nothing; for SSR and tests |
51
+ | `WindowClosedError` | class | The opened window went away mid-flow |
52
+ | `HandshakeTimeoutError` | class | The cross-origin handshake never completed |
53
+ | `webStorageAdapter` | function | Persist to `localStorage` / `sessionStorage` |
54
+ | `indexedDbAdapter` | function | Persist to IndexedDB |
55
+ | `jsonSerializer` | function | The default serializer for the text paths |
56
+
57
+ ## Gotchas
58
+
59
+ - **Shared state never crosses origins.** That is deliberate — two origins are
60
+ two trust domains. Use `openWindow` / `connectToOpener` for that, which is
61
+ explicit, typed, per-message and validated by origin, envelope brand,
62
+ per-connection nonce and source window.
63
+ - **The wire is structured clone.** Functions, class instances, DOM nodes and
64
+ symbols do not survive. Dates, Maps, Sets and typed arrays do. The text
65
+ transports (`StorageTransport`, persistence) go through a serializer instead,
66
+ which is narrower still — see the serialization guide.
67
+ - **Last-writer-wins, per key**, with a `[counter, clientId]` tie-break. It is a
68
+ deterministic resolution, not a merge; use `createSharedReducer` when the
69
+ operations need to compose.
70
+ - **The name is the identity.** A bus name is global to the origin. Two
71
+ independently-written features picking `'store'` are one store. Use
72
+ `createNamespace` to keep them apart.
73
+ - **Call the factories once, at module scope.** They register singletons keyed by
74
+ name; calling one per render or per request builds a new bus each time.
75
+
76
+ ## Docs
77
+
78
+ - Full documentation: https://rxova.org/packages/use-everywhere/
79
+ - Agent-facing index: https://rxova.org/packages/use-everywhere/llms.txt
80
+ - Every page as raw markdown: add `.md` to any docs URL
81
+ - Core section: https://rxova.org/packages/use-everywhere/core/overview.md
82
+ - Generated reference: https://rxova.org/packages/use-everywhere/api/core/readme.md
83
+ - Error codes: https://rxova.org/packages/use-everywhere/errors.md
84
+ - Source: https://github.com/rxova/use-everywhere
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@use-everywhere/core",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "Cross-tab shared state, events, presence, and cross-origin window channels",
5
5
  "license": "MIT",
6
6
  "author": "Jonatan Kruszewski <jonakrusze@gmail.com>",
@@ -9,15 +9,23 @@
9
9
  "url": "git+https://github.com/rxova/use-everywhere.git",
10
10
  "directory": "packages/core"
11
11
  },
12
- "homepage": "https://github.com/rxova/use-everywhere#readme",
12
+ "homepage": "https://rxova.org/packages/use-everywhere/core/overview/",
13
13
  "bugs": "https://github.com/rxova/use-everywhere/issues",
14
14
  "keywords": [
15
15
  "broadcastchannel",
16
+ "broadcast-channel",
16
17
  "cross-tab",
18
+ "multi-tab",
19
+ "tabs",
20
+ "tab-sync",
21
+ "sync",
17
22
  "shared-state",
23
+ "state-management",
18
24
  "postmessage",
19
25
  "cross-origin",
26
+ "cross-window",
20
27
  "presence",
28
+ "leader-election",
21
29
  "pubsub"
22
30
  ],
23
31
  "publishConfig": {
@@ -64,7 +72,8 @@
64
72
  }
65
73
  },
66
74
  "files": [
67
- "dist"
75
+ "dist",
76
+ "llms.txt"
68
77
  ],
69
78
  "size-limit": [
70
79
  {
@@ -143,7 +152,7 @@
143
152
  "name": "SharedWorker relay (shared-worker subpath)",
144
153
  "path": "dist/shared-worker.js",
145
154
  "import": "*",
146
- "limit": "280 B"
155
+ "limit": "410 B"
147
156
  },
148
157
  {
149
158
  "name": "MemoryHub (testing subpath)",