use-everywhere 0.1.0 → 0.3.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 +118 -41
- package/dist/chunk-Y67BCTU6.js +90 -0
- package/dist/devtools/index.d.ts +35 -0
- package/dist/devtools/index.js +216 -0
- package/dist/index.d.ts +83 -8
- package/dist/index.js +74 -59
- package/package.json +67 -3
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# use-everywhere
|
|
2
2
|
|
|
3
|
-
React hooks for state and messages that exist in every tab, window, and
|
|
3
|
+
React hooks for state and messages that exist in every tab, window, and
|
|
4
|
+
worker — plus a secure channel to windows on other origins.
|
|
4
5
|
|
|
5
6
|
```bash
|
|
6
7
|
npm i use-everywhere
|
|
@@ -8,72 +9,148 @@ npm i use-everywhere
|
|
|
8
9
|
|
|
9
10
|
Two transports behind one library:
|
|
10
11
|
|
|
11
|
-
- **BroadcastChannel** (same-origin): shared state with last-writer-wins
|
|
12
|
-
clocks and a late-joiner handshake, typed pub/sub events, and peer
|
|
12
|
+
- **BroadcastChannel** (same-origin): shared state with last-writer-wins
|
|
13
|
+
version clocks and a late-joiner handshake, typed pub/sub events, and peer
|
|
14
|
+
presence.
|
|
13
15
|
- **window.opener / postMessage** (cross-origin): a secure 1:1 channel to a
|
|
14
16
|
window you opened — e.g. a payment page on another domain that must report
|
|
15
|
-
back to the checkout that opened it.
|
|
16
|
-
envelope brand, a per-connection nonce, and the source window.
|
|
17
|
+
back to the checkout that opened it.
|
|
17
18
|
|
|
18
|
-
##
|
|
19
|
+
## Shared state: `useSharedState`
|
|
20
|
+
|
|
21
|
+
`useState`, but the value exists in every tab on your origin. Late-joining
|
|
22
|
+
tabs hydrate to the current value; concurrent writes converge to one winner.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { useSharedState } from 'use-everywhere';
|
|
26
|
+
|
|
27
|
+
function Counter() {
|
|
28
|
+
const [count, setCount] = useSharedState('count', 0);
|
|
29
|
+
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The third argument delimits how far a value travels:
|
|
19
34
|
|
|
20
35
|
```tsx
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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'.
|
|
36
|
+
useSharedState('draft', '', { scope: 'everywhere' }); // tabs + windows + workers (default)
|
|
37
|
+
useSharedState('draft', '', { scope: 'tabs' }); // ignore writes from workers
|
|
38
|
+
useSharedState('draft', '', { scope: 'tab' }); // this tab only
|
|
49
39
|
```
|
|
50
40
|
|
|
51
|
-
|
|
41
|
+
## Events: `defineChannel`
|
|
42
|
+
|
|
43
|
+
Typed fire-and-forget messages for things that _happen_ (state is for things
|
|
44
|
+
that _are_). Not echoed to the sender; no history for late joiners. Bind the
|
|
45
|
+
channel's name and message map once at module level; every component gets
|
|
46
|
+
fully typed hooks with nothing to repeat:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { defineChannel } from 'use-everywhere';
|
|
50
|
+
import { useState } from 'react';
|
|
51
|
+
|
|
52
|
+
type ShopEvents = { 'cart-updated': { items: number } };
|
|
53
|
+
const shop = defineChannel<ShopEvents>('shop');
|
|
54
|
+
|
|
55
|
+
function CartBadge() {
|
|
56
|
+
const [items, setItems] = useState(0);
|
|
57
|
+
const send = shop.useSend();
|
|
58
|
+
|
|
59
|
+
// Fires when any OTHER tab posts 'cart-updated'.
|
|
60
|
+
shop.useMessage('cart-updated', (payload) => setItems(payload.items));
|
|
61
|
+
|
|
62
|
+
const addToCart = () => {
|
|
63
|
+
setItems(items + 1); // this tab
|
|
64
|
+
send('cart-updated', { items: items + 1 }); // every other tab
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return <button onClick={addToCart}>Cart ({items})</button>;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
52
70
|
|
|
53
|
-
|
|
71
|
+
The standalone hooks — `useChannel(name)`, `useMessage(channel, type,
|
|
72
|
+
handler)`, `useSend(channel)` — are the same machinery without the
|
|
73
|
+
module-level binding, for one-off use.
|
|
74
|
+
|
|
75
|
+
## Presence: `usePeers`
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { usePeers } from 'use-everywhere';
|
|
79
|
+
|
|
80
|
+
function DuplicateTabWarning() {
|
|
81
|
+
const peers = usePeers();
|
|
82
|
+
if (peers.length === 0) return null;
|
|
83
|
+
return <p>⚠ This page is open in {peers.length} other tab(s).</p>;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Cross-origin windows: `useOpenedWindow`
|
|
88
|
+
|
|
89
|
+
Open a window on another domain, exchange typed messages, and await its
|
|
90
|
+
result — the whole lifecycle folded into render state.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { openWindow, useOpenedWindow } from 'use-everywhere';
|
|
94
|
+
|
|
95
|
+
type ToPayment = { order: { orderId: string; amount: string } };
|
|
96
|
+
type FromPayment = { progress: { step: string } };
|
|
97
|
+
type Receipt = { receiptId: string; last4: string };
|
|
98
|
+
|
|
99
|
+
function PayButton() {
|
|
100
|
+
const pay = useOpenedWindow<ToPayment, FromPayment, Receipt>(() =>
|
|
101
|
+
openWindow('https://pay.example.com/checkout', {
|
|
102
|
+
peerOrigin: 'https://pay.example.com', // required — '*' throws
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (pay.status === 'done') return <p>Paid — receipt {pay.result!.receiptId}</p>;
|
|
107
|
+
if (pay.status === 'closed-early') return <p>Payment window was closed.</p>;
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<button onClick={pay.open} disabled={pay.status !== 'idle'}>
|
|
111
|
+
{pay.status === 'idle' ? 'Pay in secure window' : 'Waiting for payment…'}
|
|
112
|
+
</button>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
On the opened page (the other domain), use the core API:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
54
120
|
import { connectToOpener } from 'use-everywhere';
|
|
55
121
|
|
|
56
122
|
const conn = connectToOpener<ToPayment, FromPayment, Receipt>({
|
|
57
123
|
peerOrigin: 'https://shop.example.com',
|
|
58
124
|
});
|
|
59
|
-
conn.on('order', (order) =>
|
|
60
|
-
conn.finish({ receiptId: 'r-123', last4: '4242' }); // resolves the opener's result
|
|
125
|
+
conn.on('order', (order) => setOrder(order)); // e.g. a useState setter
|
|
126
|
+
conn.finish({ receiptId: 'r-123', last4: '4242' }); // resolves the opener's pay.result
|
|
61
127
|
```
|
|
62
128
|
|
|
129
|
+
Messages sent before the (possibly slow-loading) child connects are queued,
|
|
130
|
+
never dropped, and every received message is validated by origin, envelope,
|
|
131
|
+
per-connection nonce, and source window.
|
|
132
|
+
|
|
63
133
|
## Design notes
|
|
64
134
|
|
|
65
135
|
- **Shared state never crosses origins.** Two origins are two trust domains;
|
|
66
136
|
the cross-origin channel is explicit, per-message, and typed.
|
|
67
137
|
- **No Provider.** A BroadcastChannel is already global to the origin —
|
|
68
138
|
identity is the channel name, so hooks share module-level singletons.
|
|
69
|
-
|
|
139
|
+
Imperative access to the same stores: `getSharedStore(name)`.
|
|
140
|
+
- SSR-safe: hooks render initial values on the server via
|
|
141
|
+
`getServerSnapshot`; no `BroadcastChannel` needed there.
|
|
142
|
+
- Values must survive structured clone (no functions, DOM nodes); state lives
|
|
143
|
+
as long as at least one context holds it — nothing is persisted.
|
|
144
|
+
- Testing is first-class: inject a `MemoryHub` transport to simulate many tabs
|
|
145
|
+
in one test. See the [testing guide](https://rxova.github.io/use-everywhere/guides/testing).
|
|
70
146
|
|
|
71
147
|
This package re-exports the full framework-agnostic surface of
|
|
72
148
|
[`@use-everywhere/core`](https://www.npmjs.com/package/@use-everywhere/core),
|
|
73
149
|
so you never need to install core directly.
|
|
74
150
|
|
|
75
|
-
|
|
76
|
-
|
|
151
|
+
📖 **[Documentation](https://rxova.github.io/use-everywhere/)** — mental
|
|
152
|
+
model, how sync works, security model, recipes, and generated API reference.
|
|
153
|
+
Source and demo app: [github.com/rxova/use-everywhere](https://github.com/rxova/use-everywhere)
|
|
77
154
|
|
|
78
155
|
## License
|
|
79
156
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/registry.ts
|
|
2
|
+
import {
|
|
3
|
+
createChannel,
|
|
4
|
+
createLeader,
|
|
5
|
+
createPresence,
|
|
6
|
+
createSharedStore,
|
|
7
|
+
DEFAULT_NAME,
|
|
8
|
+
NoopTransport
|
|
9
|
+
} from "@use-everywhere/core";
|
|
10
|
+
var stores = /* @__PURE__ */ new Map();
|
|
11
|
+
var presences = /* @__PURE__ */ new Map();
|
|
12
|
+
var channels = /* @__PURE__ */ new Map();
|
|
13
|
+
var leaders = /* @__PURE__ */ new Map();
|
|
14
|
+
var storeConfig = /* @__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 configureStore(name, scope, options) {
|
|
24
|
+
const key = `${scope} ${name}`;
|
|
25
|
+
if (stores.has(key)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`defineStore('${name}') ran after that store was already created. Move it to module scope \u2014 configuring a live store would silently hand you one without persistence.`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
storeConfig.set(key, options);
|
|
31
|
+
}
|
|
32
|
+
function getStore(name, scope = "everywhere") {
|
|
33
|
+
const key = `${scope} ${name}`;
|
|
34
|
+
let store = stores.get(key);
|
|
35
|
+
if (!store) {
|
|
36
|
+
store = createSharedStore(name, {}, { ...scopeOptions[scope], ...storeConfig.get(key) });
|
|
37
|
+
stores.set(key, store);
|
|
38
|
+
}
|
|
39
|
+
return store;
|
|
40
|
+
}
|
|
41
|
+
function getPresence(name) {
|
|
42
|
+
let presence = presences.get(name);
|
|
43
|
+
if (!presence) {
|
|
44
|
+
presence = createPresence(name);
|
|
45
|
+
presences.set(name, presence);
|
|
46
|
+
}
|
|
47
|
+
return presence;
|
|
48
|
+
}
|
|
49
|
+
function getLeader(name, options) {
|
|
50
|
+
let leader = leaders.get(name);
|
|
51
|
+
if (!leader) {
|
|
52
|
+
leader = createLeader(name, options);
|
|
53
|
+
leaders.set(name, leader);
|
|
54
|
+
}
|
|
55
|
+
return leader;
|
|
56
|
+
}
|
|
57
|
+
function getChannel(name) {
|
|
58
|
+
let channel = channels.get(name);
|
|
59
|
+
if (!channel) {
|
|
60
|
+
channel = createChannel(name);
|
|
61
|
+
channels.set(name, channel);
|
|
62
|
+
}
|
|
63
|
+
return channel;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/use-peers.ts
|
|
67
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
68
|
+
var NO_PEERS = Object.freeze([]);
|
|
69
|
+
function usePeers(options) {
|
|
70
|
+
const presence = getPresence(options?.name ?? DEFAULT_NAME);
|
|
71
|
+
return useSyncExternalStore(
|
|
72
|
+
useCallback((onChange) => presence.subscribe(onChange), [presence]),
|
|
73
|
+
() => presence.getPeers(),
|
|
74
|
+
() => NO_PEERS
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
function useClientId(options) {
|
|
78
|
+
return getPresence(options?.name ?? DEFAULT_NAME).clientId;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
DEFAULT_NAME,
|
|
83
|
+
getSharedStore,
|
|
84
|
+
configureStore,
|
|
85
|
+
getStore,
|
|
86
|
+
getLeader,
|
|
87
|
+
getChannel,
|
|
88
|
+
usePeers,
|
|
89
|
+
useClientId
|
|
90
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
interface InspectorProps {
|
|
4
|
+
/** Which bus to watch. Defaults to the shared default name. */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Corner to dock in. Default 'bottom-right'. */
|
|
7
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
8
|
+
/** How many wires to keep in the log. Default 50. */
|
|
9
|
+
limit?: number;
|
|
10
|
+
/** Start expanded. Default false. */
|
|
11
|
+
defaultOpen?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* How long a leader wire keeps the crown before it is treated as stale.
|
|
14
|
+
* Should match the Leader's leaseMs. Default 3000.
|
|
15
|
+
*/
|
|
16
|
+
leaseMs?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A floating panel showing what this tab is saying and hearing on the bus:
|
|
21
|
+
* peers, the leader, store keys with their version clocks, and a live wire log
|
|
22
|
+
* in both directions.
|
|
23
|
+
*
|
|
24
|
+
* It deliberately does **not** create a Leader. Under dynamic eligibility,
|
|
25
|
+
* mounting one with `eligible: false` would disable candidacy for the whole
|
|
26
|
+
* tab, and mounting a plain one would enrol a tab that never asked to be a
|
|
27
|
+
* candidate — a devtool must not change what it measures. Instead it reads the
|
|
28
|
+
* crown out of the wire log, which it already sees in both directions.
|
|
29
|
+
*
|
|
30
|
+
* Presence is fine to use: the bus heartbeats regardless of whether anything
|
|
31
|
+
* created a Presence, so usePeers observes rather than perturbs.
|
|
32
|
+
*/
|
|
33
|
+
declare function Inspector({ name, position, limit, defaultOpen, leaseMs, }?: InspectorProps): react.JSX.Element;
|
|
34
|
+
|
|
35
|
+
export { Inspector, type InspectorProps };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSharedStore,
|
|
3
|
+
usePeers
|
|
4
|
+
} from "../chunk-Y67BCTU6.js";
|
|
5
|
+
|
|
6
|
+
// src/devtools/inspector.tsx
|
|
7
|
+
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
8
|
+
import { DEFAULT_NAME, observeBus } from "@use-everywhere/core";
|
|
9
|
+
|
|
10
|
+
// src/devtools/styles.ts
|
|
11
|
+
var STYLES = `
|
|
12
|
+
.ue-ins {
|
|
13
|
+
position: fixed;
|
|
14
|
+
z-index: 2147483000;
|
|
15
|
+
font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
16
|
+
color: #e6edf3;
|
|
17
|
+
background: #0d1117;
|
|
18
|
+
border: 1px solid #30363d;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
box-shadow: 0 8px 32px rgb(0 0 0 / 0.4);
|
|
21
|
+
max-width: min(420px, calc(100vw - 24px));
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
}
|
|
24
|
+
.ue-ins--bottom-right { bottom: 12px; right: 12px; }
|
|
25
|
+
.ue-ins--bottom-left { bottom: 12px; left: 12px; }
|
|
26
|
+
.ue-ins--top-right { top: 12px; right: 12px; }
|
|
27
|
+
.ue-ins--top-left { top: 12px; left: 12px; }
|
|
28
|
+
|
|
29
|
+
.ue-ins__bar {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 8px;
|
|
33
|
+
width: 100%;
|
|
34
|
+
padding: 7px 10px;
|
|
35
|
+
background: #161b22;
|
|
36
|
+
border: 0;
|
|
37
|
+
color: inherit;
|
|
38
|
+
font: inherit;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
text-align: left;
|
|
41
|
+
}
|
|
42
|
+
.ue-ins__dot { width: 7px; height: 7px; border-radius: 50%; background: #3fb950; flex: none; }
|
|
43
|
+
.ue-ins__title { font-weight: 600; }
|
|
44
|
+
.ue-ins__muted { color: #8b949e; }
|
|
45
|
+
.ue-ins__crown { margin-left: auto; color: #d29922; }
|
|
46
|
+
|
|
47
|
+
.ue-ins__body { max-height: 60vh; overflow-y: auto; }
|
|
48
|
+
.ue-ins__section { border-top: 1px solid #21262d; padding: 8px 10px; }
|
|
49
|
+
.ue-ins__h {
|
|
50
|
+
color: #8b949e;
|
|
51
|
+
text-transform: uppercase;
|
|
52
|
+
letter-spacing: 0.06em;
|
|
53
|
+
font-size: 10px;
|
|
54
|
+
margin-bottom: 5px;
|
|
55
|
+
}
|
|
56
|
+
.ue-ins__row { display: flex; gap: 8px; padding: 1px 0; }
|
|
57
|
+
.ue-ins__k { color: #79c0ff; flex: none; }
|
|
58
|
+
.ue-ins__v { color: #e6edf3; overflow-wrap: anywhere; }
|
|
59
|
+
.ue-ins__ver { color: #6e7681; margin-left: auto; flex: none; }
|
|
60
|
+
.ue-ins__empty { color: #6e7681; }
|
|
61
|
+
|
|
62
|
+
.ue-ins__log { display: flex; flex-direction: column-reverse; max-height: 190px; overflow-y: auto; }
|
|
63
|
+
.ue-ins__wire { display: flex; gap: 7px; padding: 1px 0; white-space: nowrap; }
|
|
64
|
+
.ue-ins__dir { flex: none; width: 9px; }
|
|
65
|
+
.ue-ins__dir--out { color: #d29922; }
|
|
66
|
+
.ue-ins__dir--in { color: #3fb950; }
|
|
67
|
+
.ue-ins__scope { color: #e6edf3; }
|
|
68
|
+
.ue-ins__from { color: #6e7681; margin-left: auto; }
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
// src/devtools/inspector.tsx
|
|
72
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
73
|
+
var short = (id) => id.slice(0, 6);
|
|
74
|
+
function wireLabel(wire) {
|
|
75
|
+
return `${wire.scope}/${wire.type}`;
|
|
76
|
+
}
|
|
77
|
+
function Inspector({
|
|
78
|
+
name = DEFAULT_NAME,
|
|
79
|
+
position = "bottom-right",
|
|
80
|
+
limit = 50,
|
|
81
|
+
defaultOpen = false,
|
|
82
|
+
leaseMs = 3e3
|
|
83
|
+
} = {}) {
|
|
84
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
85
|
+
const [wires, setWires] = useState([]);
|
|
86
|
+
const [crown, setCrown] = useState(null);
|
|
87
|
+
const nextId = useRef(0);
|
|
88
|
+
const crownAt = useRef(0);
|
|
89
|
+
const peers = usePeers({ name });
|
|
90
|
+
const store = getSharedStore(name);
|
|
91
|
+
const subscribe = useCallback((onChange) => store.subscribe(onChange), [store]);
|
|
92
|
+
const snapshot = useSyncExternalStore(
|
|
93
|
+
subscribe,
|
|
94
|
+
() => store.getSnapshot(),
|
|
95
|
+
() => store.getSnapshot()
|
|
96
|
+
);
|
|
97
|
+
const versions = useSyncExternalStore(
|
|
98
|
+
subscribe,
|
|
99
|
+
() => store.getVersions(),
|
|
100
|
+
() => store.getVersions()
|
|
101
|
+
);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
return observeBus(name, (event) => {
|
|
104
|
+
const { wire, direction } = event;
|
|
105
|
+
if (wire.scope === "leader") {
|
|
106
|
+
if (wire.type === "resign") {
|
|
107
|
+
setCrown(null);
|
|
108
|
+
crownAt.current = 0;
|
|
109
|
+
} else if (wire.type === "claim" || wire.type === "heartbeat") {
|
|
110
|
+
setCrown(wire.clientId);
|
|
111
|
+
crownAt.current = Date.now();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
setWires(
|
|
115
|
+
(prev) => [
|
|
116
|
+
...prev.slice(-(limit - 1)),
|
|
117
|
+
{
|
|
118
|
+
id: nextId.current++,
|
|
119
|
+
direction,
|
|
120
|
+
label: wireLabel(wire),
|
|
121
|
+
from: short(wire.clientId)
|
|
122
|
+
}
|
|
123
|
+
].slice(-limit)
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
}, [name, limit]);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const timer = setInterval(
|
|
129
|
+
() => {
|
|
130
|
+
if (crownAt.current && Date.now() - crownAt.current > leaseMs) {
|
|
131
|
+
setCrown(null);
|
|
132
|
+
crownAt.current = 0;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
// Quarter of the lease, so a vacated crown clears well within it. The
|
|
136
|
+
// floor is low enough that a short lease still works rather than being
|
|
137
|
+
// silently rounded up to something coarser than the lease itself.
|
|
138
|
+
Math.max(50, Math.floor(leaseMs / 4))
|
|
139
|
+
);
|
|
140
|
+
return () => clearInterval(timer);
|
|
141
|
+
}, [leaseMs]);
|
|
142
|
+
const selfId = store.clientId;
|
|
143
|
+
const entries = Object.entries(versions);
|
|
144
|
+
return /* @__PURE__ */ jsxs("div", { className: `ue-ins ue-ins--${position}`, "data-testid": "ue-inspector", children: [
|
|
145
|
+
/* @__PURE__ */ jsx("style", { children: STYLES }),
|
|
146
|
+
/* @__PURE__ */ jsxs(
|
|
147
|
+
"button",
|
|
148
|
+
{
|
|
149
|
+
type: "button",
|
|
150
|
+
className: "ue-ins__bar",
|
|
151
|
+
onClick: () => setOpen((v) => !v),
|
|
152
|
+
"aria-expanded": open,
|
|
153
|
+
children: [
|
|
154
|
+
/* @__PURE__ */ jsx("span", { className: "ue-ins__dot" }),
|
|
155
|
+
/* @__PURE__ */ jsx("span", { className: "ue-ins__title", children: "use-everywhere" }),
|
|
156
|
+
/* @__PURE__ */ jsx("span", { className: "ue-ins__muted", children: name }),
|
|
157
|
+
crown ? /* @__PURE__ */ jsxs("span", { className: "ue-ins__crown", "data-testid": "ue-crown", children: [
|
|
158
|
+
"\u2654 ",
|
|
159
|
+
crown === selfId ? "this tab" : short(crown)
|
|
160
|
+
] }) : null
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
),
|
|
164
|
+
open ? /* @__PURE__ */ jsxs("div", { className: "ue-ins__body", children: [
|
|
165
|
+
/* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
|
|
166
|
+
/* @__PURE__ */ jsx("div", { className: "ue-ins__h", children: "This tab" }),
|
|
167
|
+
/* @__PURE__ */ jsxs("div", { className: "ue-ins__row", children: [
|
|
168
|
+
/* @__PURE__ */ jsx("span", { className: "ue-ins__k", children: short(selfId) }),
|
|
169
|
+
/* @__PURE__ */ jsx("span", { className: "ue-ins__v", children: crown === selfId ? "leader" : crown ? "follower" : "no leader" })
|
|
170
|
+
] })
|
|
171
|
+
] }),
|
|
172
|
+
/* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
|
|
173
|
+
/* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
|
|
174
|
+
"Peers (",
|
|
175
|
+
peers.length,
|
|
176
|
+
")"
|
|
177
|
+
] }),
|
|
178
|
+
peers.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", children: "nobody else here" }) : peers.map((peer) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__row", children: [
|
|
179
|
+
/* @__PURE__ */ jsx("span", { className: "ue-ins__k", children: short(peer.id) }),
|
|
180
|
+
/* @__PURE__ */ jsx("span", { className: "ue-ins__v", children: peer.kind })
|
|
181
|
+
] }, peer.id))
|
|
182
|
+
] }),
|
|
183
|
+
/* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
|
|
184
|
+
/* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
|
|
185
|
+
"State (",
|
|
186
|
+
entries.length,
|
|
187
|
+
")"
|
|
188
|
+
] }),
|
|
189
|
+
entries.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", children: "no keys yet" }) : entries.map(([key, version]) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__row", children: [
|
|
190
|
+
/* @__PURE__ */ jsx("span", { className: "ue-ins__k", children: key }),
|
|
191
|
+
/* @__PURE__ */ jsx("span", { className: "ue-ins__v", children: JSON.stringify(snapshot[key]) }),
|
|
192
|
+
/* @__PURE__ */ jsxs("span", { className: "ue-ins__ver", children: [
|
|
193
|
+
version[0],
|
|
194
|
+
"\xB7",
|
|
195
|
+
short(version[1])
|
|
196
|
+
] })
|
|
197
|
+
] }, key))
|
|
198
|
+
] }),
|
|
199
|
+
/* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
|
|
200
|
+
/* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
|
|
201
|
+
"Wires (",
|
|
202
|
+
wires.length,
|
|
203
|
+
")"
|
|
204
|
+
] }),
|
|
205
|
+
wires.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", children: "nothing yet" }) : /* @__PURE__ */ jsx("div", { className: "ue-ins__log", children: wires.map((wire) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__wire", children: [
|
|
206
|
+
/* @__PURE__ */ jsx("span", { className: `ue-ins__dir ue-ins__dir--${wire.direction}`, children: wire.direction === "out" ? "\u2192" : "\u2190" }),
|
|
207
|
+
/* @__PURE__ */ jsx("span", { className: "ue-ins__scope", children: wire.label }),
|
|
208
|
+
/* @__PURE__ */ jsx("span", { className: "ue-ins__from", children: wire.from })
|
|
209
|
+
] }, wire.id)) })
|
|
210
|
+
] })
|
|
211
|
+
] }) : null
|
|
212
|
+
] });
|
|
213
|
+
}
|
|
214
|
+
export {
|
|
215
|
+
Inspector
|
|
216
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { SharedStore, MessageMap, Channel, MessageMeta, Peer, OpenedWindow } from '@use-everywhere/core';
|
|
1
|
+
import { SharedStore, LeaderOptions, Leader, MessageMap, Channel, MessageMeta, PersistAdapter, Peer, LeaderSnapshot, OpenedWindow } from '@use-everywhere/core';
|
|
2
2
|
export * from '@use-everywhere/core';
|
|
3
|
+
export { DEFAULT_NAME } from '@use-everywhere/core';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* How far a shared value travels:
|
|
@@ -26,14 +27,15 @@ declare function useSharedState<T>(key: string, initial: T, options?: UseSharedS
|
|
|
26
27
|
/** Registry stores hold arbitrary keys — shape is decided by the hooks using them. */
|
|
27
28
|
type AnyStore = SharedStore<Record<string, unknown>>;
|
|
28
29
|
|
|
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
30
|
/** Imperative access to the store behind useSharedState (patch logs, non-React code). */
|
|
36
31
|
declare function getSharedStore(name?: string, scope?: ShareScope): AnyStore;
|
|
32
|
+
/**
|
|
33
|
+
* One Leader per name per tab. Eligibility is deliberately *not* part of the
|
|
34
|
+
* key: two Leaders on one name would share a bus and a clientId, and since a
|
|
35
|
+
* post never loops back locally, neither would ever see the other's claims.
|
|
36
|
+
* Timing options are first-wins, like every other engine here.
|
|
37
|
+
*/
|
|
38
|
+
declare function getLeader(name: string, options?: LeaderOptions): Leader;
|
|
37
39
|
|
|
38
40
|
/** Get the page-wide typed channel for `name` (one instance per name). */
|
|
39
41
|
declare function useChannel<M extends MessageMap>(name: string): Channel<M>;
|
|
@@ -45,6 +47,60 @@ declare function useMessage<M extends MessageMap, K extends keyof M & string>(ch
|
|
|
45
47
|
/** The channel's post function (stable identity per channel). */
|
|
46
48
|
declare function useSend<M extends MessageMap>(channel: Channel<M>): Channel<M>['post'];
|
|
47
49
|
|
|
50
|
+
/** A channel bound to a name and message map: typed hooks with no per-call generics. */
|
|
51
|
+
interface ChannelHooks<M extends MessageMap> {
|
|
52
|
+
/** The underlying channel instance (the same one the hooks use) — for non-React code. */
|
|
53
|
+
get: () => Channel<M>;
|
|
54
|
+
/** The channel's post function (stable identity). */
|
|
55
|
+
useSend: () => Channel<M>['post'];
|
|
56
|
+
/**
|
|
57
|
+
* Subscribe to one message type. Same contract as the standalone
|
|
58
|
+
* `useMessage`: the handler is kept fresh without resubscribing.
|
|
59
|
+
*/
|
|
60
|
+
useMessage: <K extends keyof M & string>(type: K, handler: (payload: M[K], meta: MessageMeta) => void) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Bind a channel name and message map once, at module level, and get fully
|
|
65
|
+
* typed hooks back. Sugar over useChannel/useMessage/useSend — the same
|
|
66
|
+
* page-wide channel singleton is shared, so mixing bound and standalone
|
|
67
|
+
* hooks for one name is safe.
|
|
68
|
+
*/
|
|
69
|
+
declare function defineChannel<M extends MessageMap>(name: string): ChannelHooks<M>;
|
|
70
|
+
|
|
71
|
+
interface DefineStoreOptions {
|
|
72
|
+
/** Restore this store from disk on first use, and write it back as it changes. */
|
|
73
|
+
persist?: PersistAdapter;
|
|
74
|
+
/** Persist only these keys. Default: every key that has been written. */
|
|
75
|
+
persistKeys?: string[];
|
|
76
|
+
/** Coalesce disk writes for this long. Default 100. */
|
|
77
|
+
persistDebounceMs?: number;
|
|
78
|
+
/** How far this store is shared. Default 'everywhere'. */
|
|
79
|
+
scope?: ShareScope;
|
|
80
|
+
}
|
|
81
|
+
/** A store bound to a name and a shape: typed hooks with no per-call generics. */
|
|
82
|
+
interface StoreHooks<S extends Record<string, unknown>> {
|
|
83
|
+
/** The underlying store instance (the same one the hooks use) — for non-React code. */
|
|
84
|
+
get: () => AnyStore;
|
|
85
|
+
useSharedState: <K extends keyof S & string>(key: K, initial: S[K]) => [S[K], (next: S[K] | ((prev: S[K]) => S[K])) => void];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Bind a store name — and optionally persistence — once, at module level.
|
|
90
|
+
*
|
|
91
|
+
* Like defineChannel, this does not construct anything: it registers the
|
|
92
|
+
* options the registry will use when the store is first needed, so importing
|
|
93
|
+
* the module has no side effect. The store stays a singleton per name, so
|
|
94
|
+
* `defineStore('settings', { persist })` and a bare
|
|
95
|
+
* `useSharedState('theme', 'dark', { store: 'settings' })` elsewhere resolve to
|
|
96
|
+
* the same store, and both get persistence.
|
|
97
|
+
*
|
|
98
|
+
* Must run before that store exists. Module evaluation always precedes render,
|
|
99
|
+
* so intended usage is automatic; if it does run late it throws rather than
|
|
100
|
+
* quietly handing back a store with no persistence.
|
|
101
|
+
*/
|
|
102
|
+
declare function defineStore<S extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: DefineStoreOptions): StoreHooks<S>;
|
|
103
|
+
|
|
48
104
|
/** The other tabs/windows/workers currently alive on this origin. */
|
|
49
105
|
declare function usePeers(options?: {
|
|
50
106
|
name?: string;
|
|
@@ -54,6 +110,25 @@ declare function useClientId(options?: {
|
|
|
54
110
|
name?: string;
|
|
55
111
|
}): string;
|
|
56
112
|
|
|
113
|
+
interface UseLeaderOptions extends LeaderOptions {
|
|
114
|
+
/** Which bus to elect on. Defaults to the shared default name. */
|
|
115
|
+
name?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Who currently holds the seat on this bus, and whether it is us. Exactly one
|
|
120
|
+
* tab leads; opening another does not steal it, and closing the leader hands it
|
|
121
|
+
* over at once.
|
|
122
|
+
*/
|
|
123
|
+
declare function useLeader(options?: UseLeaderOptions): LeaderSnapshot;
|
|
124
|
+
/** Is this tab the leader? */
|
|
125
|
+
declare function useIsLeader(options?: UseLeaderOptions): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Run an effect only in the tab that holds the seat, and tear it down when the
|
|
128
|
+
* seat moves. The one place to put "exactly one tab owns the socket".
|
|
129
|
+
*/
|
|
130
|
+
declare function useLeaderEffect(effect: () => void | (() => void), options?: UseLeaderOptions): void;
|
|
131
|
+
|
|
57
132
|
type OpenedWindowStatus = 'idle' | 'opening' | 'connected' | 'done' | 'closed-early' | 'error';
|
|
58
133
|
interface UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> {
|
|
59
134
|
/** Call from a click handler (popup blockers require a user gesture). */
|
|
@@ -74,4 +149,4 @@ interface UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> {
|
|
|
74
149
|
*/
|
|
75
150
|
declare function useOpenedWindow<Out extends MessageMap, In extends MessageMap, R = unknown>(factory: () => OpenedWindow<Out, In, R>): UseOpenedWindow<Out, In, R>;
|
|
76
151
|
|
|
77
|
-
export { type AnyStore,
|
|
152
|
+
export { type AnyStore, type ChannelHooks, type DefineStoreOptions, type OpenedWindowStatus, type ShareScope, type StoreHooks, type UseLeaderOptions, type UseOpenedWindow, type UseSharedStateOptions, defineChannel, defineStore, getLeader, getSharedStore, useChannel, useClientId, useIsLeader, useLeader, useLeaderEffect, useMessage, useOpenedWindow, usePeers, useSend, useSharedState };
|
package/dist/index.js
CHANGED
|
@@ -1,52 +1,16 @@
|
|
|
1
|
-
// src/use-shared-state.ts
|
|
2
|
-
import { useCallback, useSyncExternalStore } from "react";
|
|
3
|
-
|
|
4
|
-
// src/registry.ts
|
|
5
1
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
}
|
|
2
|
+
DEFAULT_NAME,
|
|
3
|
+
configureStore,
|
|
4
|
+
getChannel,
|
|
5
|
+
getLeader,
|
|
6
|
+
getSharedStore,
|
|
7
|
+
getStore,
|
|
8
|
+
useClientId,
|
|
9
|
+
usePeers
|
|
10
|
+
} from "./chunk-Y67BCTU6.js";
|
|
48
11
|
|
|
49
12
|
// src/use-shared-state.ts
|
|
13
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
50
14
|
function useSharedState(key, initial, options) {
|
|
51
15
|
const store = getStore(options?.store ?? DEFAULT_NAME, options?.scope ?? "everywhere");
|
|
52
16
|
store.registerKey(key, initial);
|
|
@@ -81,30 +45,75 @@ function useSend(channel) {
|
|
|
81
45
|
return channel.post;
|
|
82
46
|
}
|
|
83
47
|
|
|
84
|
-
// src/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
48
|
+
// src/define-channel.ts
|
|
49
|
+
function defineChannel(name) {
|
|
50
|
+
const useBoundSend = () => useSend(useChannel(name));
|
|
51
|
+
const useBoundMessage = (type, handler) => useMessage(useChannel(name), type, handler);
|
|
52
|
+
return {
|
|
53
|
+
get: () => getChannel(name),
|
|
54
|
+
useSend: useBoundSend,
|
|
55
|
+
useMessage: useBoundMessage
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/define-store.ts
|
|
60
|
+
function defineStore(name, options = {}) {
|
|
61
|
+
const scope = options.scope ?? "everywhere";
|
|
62
|
+
if (options.persist) {
|
|
63
|
+
configureStore(name, scope, {
|
|
64
|
+
persist: {
|
|
65
|
+
adapter: options.persist,
|
|
66
|
+
...options.persistKeys ? { keys: options.persistKeys } : {},
|
|
67
|
+
...options.persistDebounceMs === void 0 ? {} : { debounceMs: options.persistDebounceMs }
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
get: () => getStore(name, scope),
|
|
73
|
+
useSharedState: (key, initial) => useSharedState(key, initial, { store: name, scope })
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/use-leader.ts
|
|
78
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
79
|
+
var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
|
|
80
|
+
function useLeader(options) {
|
|
81
|
+
const leader = getLeader(options?.name ?? DEFAULT_NAME, options);
|
|
82
|
+
const eligible = options?.eligible;
|
|
83
|
+
useEffect2(() => {
|
|
84
|
+
if (eligible === void 0) return;
|
|
85
|
+
leader.setEligible(eligible);
|
|
86
|
+
}, [leader, eligible]);
|
|
89
87
|
return useSyncExternalStore2(
|
|
90
|
-
useCallback2((onChange) =>
|
|
91
|
-
() =>
|
|
92
|
-
() =>
|
|
88
|
+
useCallback2((onChange) => leader.subscribe(onChange), [leader]),
|
|
89
|
+
() => leader.getSnapshot(),
|
|
90
|
+
() => NO_LEADER
|
|
93
91
|
);
|
|
94
92
|
}
|
|
95
|
-
function
|
|
96
|
-
return
|
|
93
|
+
function useIsLeader(options) {
|
|
94
|
+
return useLeader(options).isLeader;
|
|
95
|
+
}
|
|
96
|
+
function useLeaderEffect(effect, options) {
|
|
97
|
+
const { isLeader } = useLeader(options);
|
|
98
|
+
const effectRef = useRef2(effect);
|
|
99
|
+
useEffect2(() => {
|
|
100
|
+
effectRef.current = effect;
|
|
101
|
+
});
|
|
102
|
+
useEffect2(() => {
|
|
103
|
+
if (!isLeader) return;
|
|
104
|
+
return effectRef.current();
|
|
105
|
+
}, [isLeader]);
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
// src/use-opened-window.ts
|
|
100
|
-
import { useCallback as useCallback3, useRef as
|
|
109
|
+
import { useCallback as useCallback3, useRef as useRef3, useState } from "react";
|
|
101
110
|
import { WindowClosedError } from "@use-everywhere/core";
|
|
102
111
|
function useOpenedWindow(factory) {
|
|
103
112
|
const [status, setStatus] = useState("idle");
|
|
104
113
|
const [result, setResult] = useState(void 0);
|
|
105
114
|
const [error, setError] = useState(void 0);
|
|
106
|
-
const current =
|
|
107
|
-
const factoryRef =
|
|
115
|
+
const current = useRef3(null);
|
|
116
|
+
const factoryRef = useRef3(factory);
|
|
108
117
|
factoryRef.current = factory;
|
|
109
118
|
const open = useCallback3(() => {
|
|
110
119
|
current.current?.close();
|
|
@@ -153,9 +162,15 @@ function useOpenedWindow(factory) {
|
|
|
153
162
|
export * from "@use-everywhere/core";
|
|
154
163
|
export {
|
|
155
164
|
DEFAULT_NAME,
|
|
165
|
+
defineChannel,
|
|
166
|
+
defineStore,
|
|
167
|
+
getLeader,
|
|
156
168
|
getSharedStore,
|
|
157
169
|
useChannel,
|
|
158
170
|
useClientId,
|
|
171
|
+
useIsLeader,
|
|
172
|
+
useLeader,
|
|
173
|
+
useLeaderEffect,
|
|
159
174
|
useMessage,
|
|
160
175
|
useOpenedWindow,
|
|
161
176
|
usePeers,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-everywhere",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "React hooks for state and messages shared across tabs, windows, and workers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonatan Kruszewski <jonakrusze@gmail.com>",
|
|
@@ -32,18 +32,73 @@
|
|
|
32
32
|
".": {
|
|
33
33
|
"types": "./dist/index.d.ts",
|
|
34
34
|
"import": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./devtools": {
|
|
37
|
+
"types": "./dist/devtools/index.d.ts",
|
|
38
|
+
"import": "./dist/devtools/index.js"
|
|
35
39
|
}
|
|
36
40
|
},
|
|
37
41
|
"files": [
|
|
38
42
|
"dist"
|
|
39
43
|
],
|
|
44
|
+
"size-limit": [
|
|
45
|
+
{
|
|
46
|
+
"name": "everything (import *)",
|
|
47
|
+
"path": "dist/index.js",
|
|
48
|
+
"import": "*",
|
|
49
|
+
"limit": "5.4 kB"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "useSharedState",
|
|
53
|
+
"path": "dist/index.js",
|
|
54
|
+
"import": "{ useSharedState }",
|
|
55
|
+
"limit": "1.8 kB"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "useChannel + useMessage + useSend",
|
|
59
|
+
"path": "dist/index.js",
|
|
60
|
+
"import": "{ useChannel, useMessage, useSend }",
|
|
61
|
+
"limit": "1 kB"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "usePeers + useClientId",
|
|
65
|
+
"path": "dist/index.js",
|
|
66
|
+
"import": "{ usePeers, useClientId }",
|
|
67
|
+
"limit": "1.1 kB"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "useOpenedWindow + openWindow",
|
|
71
|
+
"path": "dist/index.js",
|
|
72
|
+
"import": "{ useOpenedWindow, openWindow }",
|
|
73
|
+
"limit": "1.4 kB"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "useLeader + useLeaderEffect",
|
|
77
|
+
"path": "dist/index.js",
|
|
78
|
+
"import": "{ useLeader, useLeaderEffect }",
|
|
79
|
+
"limit": "1.8 kB"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "defineStore (persisted)",
|
|
83
|
+
"path": "dist/index.js",
|
|
84
|
+
"import": "{ defineStore, localStorageAdapter }",
|
|
85
|
+
"limit": "2.3 kB"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "Inspector (devtools subpath)",
|
|
89
|
+
"path": "dist/devtools/index.js",
|
|
90
|
+
"import": "{ Inspector }",
|
|
91
|
+
"limit": "5 kB"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
40
94
|
"dependencies": {
|
|
41
|
-
"@use-everywhere/core": "0.
|
|
95
|
+
"@use-everywhere/core": "0.3.0"
|
|
42
96
|
},
|
|
43
97
|
"peerDependencies": {
|
|
44
98
|
"react": ">=18"
|
|
45
99
|
},
|
|
46
100
|
"devDependencies": {
|
|
101
|
+
"@size-limit/preset-small-lib": "^12.1.0",
|
|
47
102
|
"@testing-library/react": "^16.3.0",
|
|
48
103
|
"@types/react": "^19.1.8",
|
|
49
104
|
"@types/react-dom": "^19.2.3",
|
|
@@ -51,13 +106,22 @@
|
|
|
51
106
|
"happy-dom": "^20.10.6",
|
|
52
107
|
"react": "^19.1.0",
|
|
53
108
|
"react-dom": "^19.1.0",
|
|
109
|
+
"size-limit": "^12.1.0",
|
|
54
110
|
"tsup": "^8.5.0",
|
|
55
111
|
"typescript": "^5.8.3",
|
|
56
112
|
"vitest": "^4.1.10"
|
|
57
113
|
},
|
|
114
|
+
"typesVersions": {
|
|
115
|
+
"*": {
|
|
116
|
+
"devtools": [
|
|
117
|
+
"./dist/devtools/index.d.ts"
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
58
121
|
"scripts": {
|
|
59
122
|
"build": "tsup",
|
|
60
123
|
"test": "vitest run --coverage",
|
|
61
|
-
"typecheck": "tsc --noEmit"
|
|
124
|
+
"typecheck": "tsc --noEmit",
|
|
125
|
+
"size": "size-limit"
|
|
62
126
|
}
|
|
63
127
|
}
|