@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.
- package/README.md +144 -8
- 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
|
|
16
|
-
clocks and a late-joiner handshake, typed pub/sub events, and peer
|
|
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
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|