@use-everywhere/core 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/README.md +144 -8
  2. package/package.json +49 -2
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @use-everywhere/core
2
2
 
3
3
  Framework-agnostic engine for cross-tab shared state, typed events, peer
4
- presence, and secure cross-origin window channels.
4
+ presence, and secure cross-origin window channels. Zero dependencies.
5
5
 
6
6
  ```bash
7
7
  npm i @use-everywhere/core
@@ -12,22 +12,158 @@ npm i @use-everywhere/core
12
12
 
13
13
  Two transports behind one library:
14
14
 
15
- - **BroadcastChannel** (same-origin): shared state with last-writer-wins version
16
- clocks and a late-joiner handshake, typed pub/sub events, and peer presence.
15
+ - **BroadcastChannel** (same-origin): shared state with last-writer-wins
16
+ version clocks and a late-joiner handshake, typed pub/sub events, and peer
17
+ presence.
17
18
  - **window.opener / postMessage** (cross-origin): a secure 1:1 channel to a
18
19
  window you opened. Every message is validated by origin, envelope brand, a
19
20
  per-connection nonce, and the source window.
20
21
 
22
+ ## Shared state
23
+
24
+ One object that exists in every tab, window, and worker on your origin.
25
+ Writes broadcast patches; replicas converge last-writer-wins; tabs opened
26
+ later hydrate to the current value via a hello/snapshot handshake.
27
+
28
+ ```ts
29
+ import { createSharedStore } from '@use-everywhere/core';
30
+
31
+ const store = createSharedStore('checkout', { step: 0, payment: 'idle' });
32
+
33
+ // Imperative writes through the proxy — they sync everywhere:
34
+ store.state.step++;
35
+
36
+ // Or explicit (supports functional updates):
37
+ store.set('payment', 'processing');
38
+ store.set('step', (prev) => prev + 1);
39
+
40
+ // React to changes from any tab, worker, or this one:
41
+ store.subscribe((key, value, meta) => {
42
+ console.log(`${String(key)} = ${value}`, meta.self ? '(me)' : `(peer ${meta.clientId})`);
43
+ });
44
+
45
+ // Immutable snapshot, replaced per change (useSyncExternalStore-compatible):
46
+ store.getSnapshot(); // { step: 1, payment: 'processing' }
47
+ ```
48
+
49
+ Options let you delimit what a store accepts — e.g. ignore writes coming from
50
+ workers:
51
+
52
+ ```ts
53
+ createSharedStore('ui', { theme: 'light' }, { accept: (meta) => meta.kind !== 'worker' });
54
+ ```
55
+
56
+ ## Typed events
57
+
58
+ Fire-and-forget messages between contexts. No history: a tab that joins later
59
+ never sees old events (use shared state for anything a late joiner must know).
60
+
61
+ ```ts
62
+ import { createChannel } from '@use-everywhere/core';
63
+
64
+ type AuthEvents = { 'logged-out': undefined; 'session-renewed': { expiresAt: number } };
65
+
66
+ const channel = createChannel<AuthEvents>('auth');
67
+
68
+ const off = channel.on('logged-out', (_payload, meta) => {
69
+ console.log(`tab ${meta.clientId} logged out`);
70
+ window.location.assign('/login');
71
+ });
72
+
73
+ channel.post('logged-out', undefined); // delivered to every OTHER context
74
+ ```
75
+
76
+ ## Presence
77
+
78
+ Who else is on this origin right now? Heartbeat-based, with instant goodbyes
79
+ on clean tab closes and pruning (~5s) for crashed ones.
80
+
81
+ ```ts
82
+ import { createPresence } from '@use-everywhere/core';
83
+
84
+ const presence = createPresence('app');
85
+ presence.subscribe(() => {
86
+ console.log(presence.getPeers()); // [{ id: 'p8m1q4', kind: 'tab', lastSeen: … }]
87
+ });
88
+ ```
89
+
90
+ ## Cross-origin window channel
91
+
92
+ The case BroadcastChannel cannot do: a checkout on domain A opens a payment
93
+ page on domain B, and the payment page must report back.
94
+
95
+ ```ts
96
+ // On the opener (https://shop.example.com):
97
+ import { openWindow } from '@use-everywhere/core';
98
+
99
+ type ToPayment = { order: { orderId: string; amount: string } };
100
+ type FromPayment = { progress: { step: string } };
101
+ type Receipt = { receiptId: string; last4: string };
102
+
103
+ const opened = openWindow<ToPayment, FromPayment, Receipt>(
104
+ 'https://pay.example.com/checkout',
105
+ { peerOrigin: 'https://pay.example.com' }, // required — '*' throws
106
+ );
107
+
108
+ opened.post('order', { orderId: '48-291', amount: '$69.03' }); // queued until the child is ready
109
+ opened.on('progress', ({ step }) => console.log('payment step:', step));
110
+
111
+ const receipt = await opened.result; // the child's finish() value
112
+ // rejects with WindowClosedError if the user closes the window first
113
+ ```
114
+
115
+ ```ts
116
+ // On the opened page (https://pay.example.com):
117
+ import { connectToOpener } from '@use-everywhere/core';
118
+
119
+ const conn = connectToOpener<ToPayment, FromPayment, Receipt>({
120
+ peerOrigin: 'https://shop.example.com',
121
+ });
122
+
123
+ conn.on('order', (order) => showOrderSummary(order)); // your UI code
124
+ conn.finish({ receiptId: 'r-123', last4: '4242' }); // resolves the opener's `result`
125
+ conn.close();
126
+ ```
127
+
128
+ The handshake retries until the (possibly slow-loading) child connects, and
129
+ both sides queue outgoing messages until then — nothing is dropped. Every
130
+ received message must pass four gates: exact origin, library envelope, a
131
+ per-connection nonce carried in the child URL, and (on the opener) the source
132
+ window itself.
133
+
134
+ ## Testing
135
+
136
+ Every engine accepts an injected transport, so "many tabs" fits in one test —
137
+ no browser required:
138
+
139
+ ```ts
140
+ import { createSharedStore, MemoryHub } from '@use-everywhere/core';
141
+
142
+ const hub = new MemoryHub();
143
+ const tabA = createSharedStore('t', { n: 0 }, { transport: () => hub.connect() });
144
+ const tabB = createSharedStore('t', { n: 0 }, { transport: () => hub.connect() });
145
+
146
+ tabA.set('n', 1);
147
+ await new Promise((r) => setTimeout(r, 0));
148
+ tabB.getSnapshot().n; // 1
149
+ ```
150
+
151
+ `openWindow`/`connectToOpener` take equivalent seams (`openFn`, `localWindow`,
152
+ `opener`, `cid`) for driving window flows with fakes.
153
+
21
154
  ## Design notes
22
155
 
23
156
  - **Shared state never crosses origins.** Two origins are two trust domains;
24
157
  the cross-origin channel is explicit, per-message, and typed.
25
- - Same-origin state sync uses per-key `[counter, clientId]` clocks
26
- (last-writer-wins, deterministic tie-break) and a hello/snapshot handshake so
27
- late-joining tabs hydrate instantly.
28
- - Values must survive structured clone (no functions, DOM nodes, etc.).
158
+ - Values must survive structured clone (no functions, DOM nodes, class
159
+ instances).
160
+ - State lives exactly as long as some context holds it — nothing is persisted.
161
+ - SSR-safe: without `BroadcastChannel`, engines fall back to a local no-op
162
+ transport.
29
163
 
30
- Full docs, demo app (including a real cross-origin payment flow), and source:
164
+ 📖 **[Documentation](https://rxova.github.io/use-everywhere/)** mental
165
+ model, how sync works, security model, recipes, and generated API reference.
166
+ Source and demo app (with a real cross-origin payment flow):
31
167
  [github.com/rxova/use-everywhere](https://github.com/rxova/use-everywhere)
32
168
 
33
169
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@use-everywhere/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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>",
@@ -36,9 +36,55 @@
36
36
  "files": [
37
37
  "dist"
38
38
  ],
39
+ "size-limit": [
40
+ {
41
+ "name": "everything (import *)",
42
+ "path": "dist/index.js",
43
+ "import": "*",
44
+ "limit": "3.5 kB"
45
+ },
46
+ {
47
+ "name": "createSharedStore",
48
+ "path": "dist/index.js",
49
+ "import": "{ createSharedStore }",
50
+ "limit": "1.3 kB"
51
+ },
52
+ {
53
+ "name": "createChannel",
54
+ "path": "dist/index.js",
55
+ "import": "{ createChannel }",
56
+ "limit": "900 B"
57
+ },
58
+ {
59
+ "name": "createPresence",
60
+ "path": "dist/index.js",
61
+ "import": "{ createPresence }",
62
+ "limit": "1 kB"
63
+ },
64
+ {
65
+ "name": "openWindow (opener side)",
66
+ "path": "dist/index.js",
67
+ "import": "{ openWindow }",
68
+ "limit": "1.15 kB"
69
+ },
70
+ {
71
+ "name": "connectToOpener (child side)",
72
+ "path": "dist/index.js",
73
+ "import": "{ connectToOpener }",
74
+ "limit": "1 kB"
75
+ },
76
+ {
77
+ "name": "MemoryHub (test transport)",
78
+ "path": "dist/index.js",
79
+ "import": "{ MemoryHub }",
80
+ "limit": "300 B"
81
+ }
82
+ ],
39
83
  "devDependencies": {
84
+ "@size-limit/preset-small-lib": "^12.1.0",
40
85
  "@vitest/coverage-v8": "^4.1.10",
41
86
  "happy-dom": "^20.10.6",
87
+ "size-limit": "^12.1.0",
42
88
  "tsup": "^8.5.0",
43
89
  "typescript": "^5.8.3",
44
90
  "vitest": "^4.1.10"
@@ -46,6 +92,7 @@
46
92
  "scripts": {
47
93
  "build": "tsup",
48
94
  "test": "vitest run --coverage",
49
- "typecheck": "tsc --noEmit"
95
+ "typecheck": "tsc --noEmit",
96
+ "size": "size-limit"
50
97
  }
51
98
  }