use-everywhere 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonatan Kruszewski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # use-everywhere
2
+
3
+ React hooks for state and messages that exist in every tab, window, and worker.
4
+
5
+ ```bash
6
+ npm i use-everywhere
7
+ ```
8
+
9
+ Two transports behind one library:
10
+
11
+ - **BroadcastChannel** (same-origin): shared state with last-writer-wins version
12
+ clocks and a late-joiner handshake, typed pub/sub events, and peer presence.
13
+ - **window.opener / postMessage** (cross-origin): a secure 1:1 channel to a
14
+ window you opened — e.g. a payment page on another domain that must report
15
+ back to the checkout that opened it. Every message is validated by origin,
16
+ envelope brand, a per-connection nonce, and the source window.
17
+
18
+ ## Usage
19
+
20
+ ```tsx
21
+ import {
22
+ useSharedState,
23
+ useChannel,
24
+ useMessage,
25
+ usePeers,
26
+ useOpenedWindow,
27
+ openWindow,
28
+ } from 'use-everywhere';
29
+
30
+ // useState, but the value exists in every tab/window/worker on this origin.
31
+ const [count, setCount] = useSharedState('count', 0);
32
+
33
+ // Typed fire-and-forget events between tabs.
34
+ const channel = useChannel<{ 'cart-updated': { items: number } }>('shop');
35
+ useMessage(channel, 'cart-updated', ({ items }) => refresh(items));
36
+ channel.post('cart-updated', { items: 3 });
37
+
38
+ // Who else is here?
39
+ const peers = usePeers();
40
+
41
+ // Open a window on ANOTHER origin and await its result.
42
+ const pay = useOpenedWindow(() =>
43
+ openWindow<ToPayment, FromPayment, Receipt>('https://pay.example.com/checkout', {
44
+ peerOrigin: 'https://pay.example.com',
45
+ }),
46
+ );
47
+ // pay.open() from a click handler; pay.status: idle → opening → connected → done
48
+ // pay.result is the child's finish() value; closing early yields 'closed-early'.
49
+ ```
50
+
51
+ On the opened (child) page:
52
+
53
+ ```ts
54
+ import { connectToOpener } from 'use-everywhere';
55
+
56
+ const conn = connectToOpener<ToPayment, FromPayment, Receipt>({
57
+ peerOrigin: 'https://shop.example.com',
58
+ });
59
+ conn.on('order', (order) => render(order));
60
+ conn.finish({ receiptId: 'r-123', last4: '4242' }); // resolves the opener's result
61
+ ```
62
+
63
+ ## Design notes
64
+
65
+ - **Shared state never crosses origins.** Two origins are two trust domains;
66
+ the cross-origin channel is explicit, per-message, and typed.
67
+ - **No Provider.** A BroadcastChannel is already global to the origin —
68
+ identity is the channel name, so hooks share module-level singletons.
69
+ - Values must survive structured clone (no functions, DOM nodes, etc.).
70
+
71
+ This package re-exports the full framework-agnostic surface of
72
+ [`@use-everywhere/core`](https://www.npmjs.com/package/@use-everywhere/core),
73
+ so you never need to install core directly.
74
+
75
+ Full docs, demo app (including a real cross-origin payment flow), and source:
76
+ [github.com/rxova/use-everywhere](https://github.com/rxova/use-everywhere)
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,77 @@
1
+ import { SharedStore, MessageMap, Channel, MessageMeta, Peer, OpenedWindow } from '@use-everywhere/core';
2
+ export * from '@use-everywhere/core';
3
+
4
+ /**
5
+ * How far a shared value travels:
6
+ * - 'everywhere' — every tab, window, and worker on this origin (default)
7
+ * - 'tabs' — tabs and windows only; writes coming from workers are ignored
8
+ * - 'tab' — this tab only (state is still shared between components)
9
+ */
10
+ type ShareScope = 'everywhere' | 'tabs' | 'tab';
11
+ interface UseSharedStateOptions {
12
+ /** Store name; keys live in a namespace per store. Default 'use-everywhere'. */
13
+ store?: string;
14
+ /** How much to share. Default 'everywhere'. */
15
+ scope?: ShareScope;
16
+ }
17
+
18
+ /**
19
+ * Like useState, but the value exists in every tab, window, and worker on
20
+ * this origin. Late-joining tabs hydrate to the current value; concurrent
21
+ * writes converge last-writer-wins. Pass options.scope to delimit how much
22
+ * is shared ('everywhere' | 'tabs' | 'tab').
23
+ */
24
+ declare function useSharedState<T>(key: string, initial: T, options?: UseSharedStateOptions): [T, (next: T | ((prev: T) => T)) => void];
25
+
26
+ /** Registry stores hold arbitrary keys — shape is decided by the hooks using them. */
27
+ type AnyStore = SharedStore<Record<string, unknown>>;
28
+
29
+ /**
30
+ * A BroadcastChannel is already global to the origin — identity is the name
31
+ * string, not the React tree — so hooks share module-level singletons per
32
+ * name instead of requiring a Provider. Instances live for the page lifetime.
33
+ */
34
+ declare const DEFAULT_NAME = "use-everywhere";
35
+ /** Imperative access to the store behind useSharedState (patch logs, non-React code). */
36
+ declare function getSharedStore(name?: string, scope?: ShareScope): AnyStore;
37
+
38
+ /** Get the page-wide typed channel for `name` (one instance per name). */
39
+ declare function useChannel<M extends MessageMap>(name: string): Channel<M>;
40
+ /**
41
+ * Subscribe to one message type. The handler is kept fresh without
42
+ * resubscribing, so it may close over render state.
43
+ */
44
+ declare function useMessage<M extends MessageMap, K extends keyof M & string>(channel: Channel<M>, type: K, handler: (payload: M[K], meta: MessageMeta) => void): void;
45
+ /** The channel's post function (stable identity per channel). */
46
+ declare function useSend<M extends MessageMap>(channel: Channel<M>): Channel<M>['post'];
47
+
48
+ /** The other tabs/windows/workers currently alive on this origin. */
49
+ declare function usePeers(options?: {
50
+ name?: string;
51
+ }): readonly Peer[];
52
+ /** This client's own id on the presence bus (matches patch origin ids). */
53
+ declare function useClientId(options?: {
54
+ name?: string;
55
+ }): string;
56
+
57
+ type OpenedWindowStatus = 'idle' | 'opening' | 'connected' | 'done' | 'closed-early' | 'error';
58
+ interface UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> {
59
+ /** Call from a click handler (popup blockers require a user gesture). */
60
+ open: () => void;
61
+ status: OpenedWindowStatus;
62
+ /** The child's finish() value, once status is 'done'. */
63
+ result: R | undefined;
64
+ error: unknown;
65
+ /** Post to the child; no-op while nothing is open. */
66
+ post: OpenedWindow<Out, In, R>['post'];
67
+ close: () => void;
68
+ }
69
+
70
+ /**
71
+ * Drive an openWindow() flow from a component: the factory is called on
72
+ * open(), and the child window's lifecycle is folded into render state.
73
+ * Reopening replaces the previous window; a stale window's outcome is ignored.
74
+ */
75
+ declare function useOpenedWindow<Out extends MessageMap, In extends MessageMap, R = unknown>(factory: () => OpenedWindow<Out, In, R>): UseOpenedWindow<Out, In, R>;
76
+
77
+ export { type AnyStore, DEFAULT_NAME, type OpenedWindowStatus, type ShareScope, type UseOpenedWindow, type UseSharedStateOptions, getSharedStore, useChannel, useClientId, useMessage, useOpenedWindow, usePeers, useSend, useSharedState };
package/dist/index.js ADDED
@@ -0,0 +1,164 @@
1
+ // src/use-shared-state.ts
2
+ import { useCallback, useSyncExternalStore } from "react";
3
+
4
+ // src/registry.ts
5
+ import {
6
+ createChannel,
7
+ createPresence,
8
+ createSharedStore,
9
+ NoopTransport
10
+ } from "@use-everywhere/core";
11
+ var DEFAULT_NAME = "use-everywhere";
12
+ var stores = /* @__PURE__ */ new Map();
13
+ var presences = /* @__PURE__ */ new Map();
14
+ var channels = /* @__PURE__ */ new Map();
15
+ var scopeOptions = {
16
+ everywhere: {},
17
+ tabs: { accept: (meta) => meta.kind !== "worker" },
18
+ tab: { transport: () => new NoopTransport() }
19
+ };
20
+ function getSharedStore(name = DEFAULT_NAME, scope = "everywhere") {
21
+ return getStore(name, scope);
22
+ }
23
+ function getStore(name, scope = "everywhere") {
24
+ const key = `${scope}\0${name}`;
25
+ let store = stores.get(key);
26
+ if (!store) {
27
+ store = createSharedStore(name, {}, scopeOptions[scope]);
28
+ stores.set(key, store);
29
+ }
30
+ return store;
31
+ }
32
+ function getPresence(name) {
33
+ let presence = presences.get(name);
34
+ if (!presence) {
35
+ presence = createPresence(name);
36
+ presences.set(name, presence);
37
+ }
38
+ return presence;
39
+ }
40
+ function getChannel(name) {
41
+ let channel = channels.get(name);
42
+ if (!channel) {
43
+ channel = createChannel(name);
44
+ channels.set(name, channel);
45
+ }
46
+ return channel;
47
+ }
48
+
49
+ // src/use-shared-state.ts
50
+ function useSharedState(key, initial, options) {
51
+ const store = getStore(options?.store ?? DEFAULT_NAME, options?.scope ?? "everywhere");
52
+ store.registerKey(key, initial);
53
+ const value = useSyncExternalStore(
54
+ useCallback((onChange) => store.subscribeKey(key, onChange), [store, key]),
55
+ () => store.getSnapshot()[key],
56
+ () => initial
57
+ );
58
+ const setValue = useCallback(
59
+ (next) => store.set(key, next),
60
+ [store, key]
61
+ );
62
+ return [value, setValue];
63
+ }
64
+
65
+ // src/use-message.ts
66
+ import { useEffect, useRef } from "react";
67
+ function useChannel(name) {
68
+ return getChannel(name);
69
+ }
70
+ function useMessage(channel, type, handler) {
71
+ const handlerRef = useRef(handler);
72
+ useEffect(() => {
73
+ handlerRef.current = handler;
74
+ });
75
+ useEffect(
76
+ () => channel.on(type, (payload, meta) => handlerRef.current(payload, meta)),
77
+ [channel, type]
78
+ );
79
+ }
80
+ function useSend(channel) {
81
+ return channel.post;
82
+ }
83
+
84
+ // src/use-peers.ts
85
+ import { useCallback as useCallback2, useSyncExternalStore as useSyncExternalStore2 } from "react";
86
+ var NO_PEERS = Object.freeze([]);
87
+ function usePeers(options) {
88
+ const presence = getPresence(options?.name ?? DEFAULT_NAME);
89
+ return useSyncExternalStore2(
90
+ useCallback2((onChange) => presence.subscribe(onChange), [presence]),
91
+ () => presence.getPeers(),
92
+ () => NO_PEERS
93
+ );
94
+ }
95
+ function useClientId(options) {
96
+ return getPresence(options?.name ?? DEFAULT_NAME).clientId;
97
+ }
98
+
99
+ // src/use-opened-window.ts
100
+ import { useCallback as useCallback3, useRef as useRef2, useState } from "react";
101
+ import { WindowClosedError } from "@use-everywhere/core";
102
+ function useOpenedWindow(factory) {
103
+ const [status, setStatus] = useState("idle");
104
+ const [result, setResult] = useState(void 0);
105
+ const [error, setError] = useState(void 0);
106
+ const current = useRef2(null);
107
+ const factoryRef = useRef2(factory);
108
+ factoryRef.current = factory;
109
+ const open = useCallback3(() => {
110
+ current.current?.close();
111
+ let opened;
112
+ try {
113
+ opened = factoryRef.current();
114
+ } catch (err) {
115
+ setStatus("error");
116
+ setError(err);
117
+ return;
118
+ }
119
+ current.current = opened;
120
+ setStatus("opening");
121
+ setResult(void 0);
122
+ setError(void 0);
123
+ const fresh = () => current.current === opened;
124
+ opened.ready.then(
125
+ () => {
126
+ if (fresh()) setStatus((s) => s === "opening" ? "connected" : s);
127
+ },
128
+ () => {
129
+ }
130
+ // surfaced through result below
131
+ );
132
+ opened.result.then(
133
+ (value) => {
134
+ if (!fresh()) return;
135
+ setResult(value);
136
+ setStatus("done");
137
+ },
138
+ (err) => {
139
+ if (!fresh()) return;
140
+ setError(err);
141
+ setStatus(err instanceof WindowClosedError ? "closed-early" : "error");
142
+ }
143
+ );
144
+ }, []);
145
+ const post = useCallback3((type, payload) => {
146
+ current.current?.post(type, payload);
147
+ }, []);
148
+ const close = useCallback3(() => current.current?.close(), []);
149
+ return { open, status, result, error, post, close };
150
+ }
151
+
152
+ // src/index.ts
153
+ export * from "@use-everywhere/core";
154
+ export {
155
+ DEFAULT_NAME,
156
+ getSharedStore,
157
+ useChannel,
158
+ useClientId,
159
+ useMessage,
160
+ useOpenedWindow,
161
+ usePeers,
162
+ useSend,
163
+ useSharedState
164
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "use-everywhere",
3
+ "version": "0.1.0",
4
+ "description": "React hooks for state and messages shared across tabs, windows, and workers",
5
+ "license": "MIT",
6
+ "author": "Jonatan Kruszewski <jonakrusze@gmail.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/rxova/use-everywhere.git",
10
+ "directory": "packages/react"
11
+ },
12
+ "homepage": "https://github.com/rxova/use-everywhere#readme",
13
+ "bugs": "https://github.com/rxova/use-everywhere/issues",
14
+ "keywords": [
15
+ "react",
16
+ "hooks",
17
+ "broadcastchannel",
18
+ "cross-tab",
19
+ "shared-state",
20
+ "postmessage",
21
+ "cross-origin",
22
+ "presence"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "dependencies": {
41
+ "@use-everywhere/core": "0.1.0"
42
+ },
43
+ "peerDependencies": {
44
+ "react": ">=18"
45
+ },
46
+ "devDependencies": {
47
+ "@testing-library/react": "^16.3.0",
48
+ "@types/react": "^19.1.8",
49
+ "@types/react-dom": "^19.2.3",
50
+ "@vitest/coverage-v8": "^4.1.10",
51
+ "happy-dom": "^20.10.6",
52
+ "react": "^19.1.0",
53
+ "react-dom": "^19.1.0",
54
+ "tsup": "^8.5.0",
55
+ "typescript": "^5.8.3",
56
+ "vitest": "^4.1.10"
57
+ },
58
+ "scripts": {
59
+ "build": "tsup",
60
+ "test": "vitest run --coverage",
61
+ "typecheck": "tsc --noEmit"
62
+ }
63
+ }