use-everywhere 0.2.0 → 0.4.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/dist/chunk-PNFJW7QK.js +92 -0
- package/dist/devtools/index.cjs +282 -0
- package/dist/devtools/index.d.cts +35 -0
- package/dist/devtools/index.d.ts +35 -0
- package/dist/devtools/index.js +217 -0
- package/dist/index.cjs +287 -0
- package/dist/index.d.cts +152 -0
- package/dist/index.d.ts +62 -8
- package/dist/index.js +63 -59
- package/package.json +58 -16
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/index.ts
|
|
23
|
+
var src_exports = {};
|
|
24
|
+
__export(src_exports, {
|
|
25
|
+
DEFAULT_NAME: () => import_core.DEFAULT_NAME,
|
|
26
|
+
defineChannel: () => defineChannel,
|
|
27
|
+
defineStore: () => defineStore,
|
|
28
|
+
getLeader: () => getLeader,
|
|
29
|
+
getSharedStore: () => getSharedStore,
|
|
30
|
+
useChannel: () => useChannel,
|
|
31
|
+
useClientId: () => useClientId,
|
|
32
|
+
useIsLeader: () => useIsLeader,
|
|
33
|
+
useLeader: () => useLeader,
|
|
34
|
+
useLeaderEffect: () => useLeaderEffect,
|
|
35
|
+
useMessage: () => useMessage,
|
|
36
|
+
useOpenedWindow: () => useOpenedWindow,
|
|
37
|
+
usePeers: () => usePeers,
|
|
38
|
+
useSend: () => useSend,
|
|
39
|
+
useSharedState: () => useSharedState
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(src_exports);
|
|
42
|
+
|
|
43
|
+
// src/use-shared-state.ts
|
|
44
|
+
var import_react = require("react");
|
|
45
|
+
|
|
46
|
+
// src/registry.ts
|
|
47
|
+
var import_core = require("@use-everywhere/core");
|
|
48
|
+
var stores = /* @__PURE__ */ new Map();
|
|
49
|
+
var presences = /* @__PURE__ */ new Map();
|
|
50
|
+
var channels = /* @__PURE__ */ new Map();
|
|
51
|
+
var leaders = /* @__PURE__ */ new Map();
|
|
52
|
+
var storeConfig = /* @__PURE__ */ new Map();
|
|
53
|
+
var scopeOptions = {
|
|
54
|
+
everywhere: {},
|
|
55
|
+
tabs: { accept: (meta) => meta.kind !== "worker" },
|
|
56
|
+
tab: { transport: () => new import_core.NoopTransport() }
|
|
57
|
+
};
|
|
58
|
+
function getSharedStore(name = import_core.DEFAULT_NAME, scope = "everywhere") {
|
|
59
|
+
return getStore(name, scope);
|
|
60
|
+
}
|
|
61
|
+
function configureStore(name, scope, options) {
|
|
62
|
+
const key = `${scope} ${name}`;
|
|
63
|
+
if (stores.has(key)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`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.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
storeConfig.set(key, options);
|
|
69
|
+
}
|
|
70
|
+
function getStore(name, scope = "everywhere") {
|
|
71
|
+
const key = `${scope} ${name}`;
|
|
72
|
+
let store = stores.get(key);
|
|
73
|
+
if (!store) {
|
|
74
|
+
store = (0, import_core.createSharedStore)(name, {}, { ...scopeOptions[scope], ...storeConfig.get(key) });
|
|
75
|
+
stores.set(key, store);
|
|
76
|
+
}
|
|
77
|
+
return store;
|
|
78
|
+
}
|
|
79
|
+
function getPresence(name) {
|
|
80
|
+
let presence = presences.get(name);
|
|
81
|
+
if (!presence) {
|
|
82
|
+
presence = (0, import_core.createPresence)(name);
|
|
83
|
+
presences.set(name, presence);
|
|
84
|
+
}
|
|
85
|
+
return presence;
|
|
86
|
+
}
|
|
87
|
+
function getLeader(name, options) {
|
|
88
|
+
let leader = leaders.get(name);
|
|
89
|
+
if (!leader) {
|
|
90
|
+
leader = (0, import_core.createLeader)(name, options);
|
|
91
|
+
leaders.set(name, leader);
|
|
92
|
+
}
|
|
93
|
+
return leader;
|
|
94
|
+
}
|
|
95
|
+
function getChannel(name) {
|
|
96
|
+
let channel = channels.get(name);
|
|
97
|
+
if (!channel) {
|
|
98
|
+
channel = (0, import_core.createChannel)(name);
|
|
99
|
+
channels.set(name, channel);
|
|
100
|
+
}
|
|
101
|
+
return channel;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/use-shared-state.ts
|
|
105
|
+
function useSharedState(key, initial, options) {
|
|
106
|
+
const store = getStore(options?.store ?? import_core.DEFAULT_NAME, options?.scope ?? "everywhere");
|
|
107
|
+
store.registerKey(key, initial);
|
|
108
|
+
const value = (0, import_react.useSyncExternalStore)(
|
|
109
|
+
(0, import_react.useCallback)((onChange) => store.subscribeKey(key, onChange), [store, key]),
|
|
110
|
+
() => store.getSnapshot()[key],
|
|
111
|
+
() => initial
|
|
112
|
+
);
|
|
113
|
+
const setValue = (0, import_react.useCallback)(
|
|
114
|
+
(next) => store.set(key, next),
|
|
115
|
+
[store, key]
|
|
116
|
+
);
|
|
117
|
+
return [value, setValue];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/use-message.ts
|
|
121
|
+
var import_react2 = require("react");
|
|
122
|
+
function useChannel(name) {
|
|
123
|
+
return getChannel(name);
|
|
124
|
+
}
|
|
125
|
+
function useMessage(channel, type, handler) {
|
|
126
|
+
const handlerRef = (0, import_react2.useRef)(handler);
|
|
127
|
+
(0, import_react2.useEffect)(() => {
|
|
128
|
+
handlerRef.current = handler;
|
|
129
|
+
});
|
|
130
|
+
(0, import_react2.useEffect)(
|
|
131
|
+
() => channel.on(type, (payload, meta) => handlerRef.current(payload, meta)),
|
|
132
|
+
[channel, type]
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
function useSend(channel) {
|
|
136
|
+
return channel.post;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/define-channel.ts
|
|
140
|
+
function defineChannel(name) {
|
|
141
|
+
const useBoundSend = () => useSend(useChannel(name));
|
|
142
|
+
const useBoundMessage = (type, handler) => useMessage(useChannel(name), type, handler);
|
|
143
|
+
return {
|
|
144
|
+
get: () => getChannel(name),
|
|
145
|
+
useSend: useBoundSend,
|
|
146
|
+
useMessage: useBoundMessage
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/define-store.ts
|
|
151
|
+
function defineStore(name, options = {}) {
|
|
152
|
+
const scope = options.scope ?? "everywhere";
|
|
153
|
+
if (options.persist) {
|
|
154
|
+
configureStore(name, scope, {
|
|
155
|
+
persist: {
|
|
156
|
+
adapter: options.persist,
|
|
157
|
+
...options.persistKeys ? { keys: options.persistKeys } : {},
|
|
158
|
+
...options.persistDebounceMs === void 0 ? {} : { debounceMs: options.persistDebounceMs }
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
get: () => getStore(name, scope),
|
|
164
|
+
useSharedState: (key, initial) => useSharedState(key, initial, { store: name, scope })
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/use-peers.ts
|
|
169
|
+
var import_react3 = require("react");
|
|
170
|
+
var NO_PEERS = Object.freeze([]);
|
|
171
|
+
function usePeers(options) {
|
|
172
|
+
const presence = getPresence(options?.name ?? import_core.DEFAULT_NAME);
|
|
173
|
+
return (0, import_react3.useSyncExternalStore)(
|
|
174
|
+
(0, import_react3.useCallback)((onChange) => presence.subscribe(onChange), [presence]),
|
|
175
|
+
() => presence.getPeers(),
|
|
176
|
+
() => NO_PEERS
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
function useClientId(options) {
|
|
180
|
+
return getPresence(options?.name ?? import_core.DEFAULT_NAME).clientId;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/use-leader.ts
|
|
184
|
+
var import_react4 = require("react");
|
|
185
|
+
var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
|
|
186
|
+
function useLeader(options) {
|
|
187
|
+
const leader = getLeader(options?.name ?? import_core.DEFAULT_NAME, options);
|
|
188
|
+
const eligible = options?.eligible;
|
|
189
|
+
(0, import_react4.useEffect)(() => {
|
|
190
|
+
if (eligible === void 0) return;
|
|
191
|
+
leader.setEligible(eligible);
|
|
192
|
+
}, [leader, eligible]);
|
|
193
|
+
return (0, import_react4.useSyncExternalStore)(
|
|
194
|
+
(0, import_react4.useCallback)((onChange) => leader.subscribe(onChange), [leader]),
|
|
195
|
+
() => leader.getSnapshot(),
|
|
196
|
+
() => NO_LEADER
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
function useIsLeader(options) {
|
|
200
|
+
return useLeader(options).isLeader;
|
|
201
|
+
}
|
|
202
|
+
function useLeaderEffect(effect, options) {
|
|
203
|
+
const { isLeader } = useLeader(options);
|
|
204
|
+
const effectRef = (0, import_react4.useRef)(effect);
|
|
205
|
+
(0, import_react4.useEffect)(() => {
|
|
206
|
+
effectRef.current = effect;
|
|
207
|
+
});
|
|
208
|
+
(0, import_react4.useEffect)(() => {
|
|
209
|
+
if (!isLeader) return;
|
|
210
|
+
return effectRef.current();
|
|
211
|
+
}, [isLeader]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/use-opened-window.ts
|
|
215
|
+
var import_react5 = require("react");
|
|
216
|
+
var import_core2 = require("@use-everywhere/core");
|
|
217
|
+
function useOpenedWindow(factory) {
|
|
218
|
+
const [status, setStatus] = (0, import_react5.useState)("idle");
|
|
219
|
+
const [result, setResult] = (0, import_react5.useState)(void 0);
|
|
220
|
+
const [error, setError] = (0, import_react5.useState)(void 0);
|
|
221
|
+
const current = (0, import_react5.useRef)(null);
|
|
222
|
+
const factoryRef = (0, import_react5.useRef)(factory);
|
|
223
|
+
factoryRef.current = factory;
|
|
224
|
+
const open = (0, import_react5.useCallback)(() => {
|
|
225
|
+
current.current?.close();
|
|
226
|
+
let opened;
|
|
227
|
+
try {
|
|
228
|
+
opened = factoryRef.current();
|
|
229
|
+
} catch (err) {
|
|
230
|
+
setStatus("error");
|
|
231
|
+
setError(err);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
current.current = opened;
|
|
235
|
+
setStatus("opening");
|
|
236
|
+
setResult(void 0);
|
|
237
|
+
setError(void 0);
|
|
238
|
+
const fresh = () => current.current === opened;
|
|
239
|
+
opened.ready.then(
|
|
240
|
+
() => {
|
|
241
|
+
if (fresh()) setStatus((s) => s === "opening" ? "connected" : s);
|
|
242
|
+
},
|
|
243
|
+
() => {
|
|
244
|
+
}
|
|
245
|
+
// surfaced through result below
|
|
246
|
+
);
|
|
247
|
+
opened.result.then(
|
|
248
|
+
(value) => {
|
|
249
|
+
if (!fresh()) return;
|
|
250
|
+
setResult(value);
|
|
251
|
+
setStatus("done");
|
|
252
|
+
},
|
|
253
|
+
(err) => {
|
|
254
|
+
if (!fresh()) return;
|
|
255
|
+
setError(err);
|
|
256
|
+
setStatus(err instanceof import_core2.WindowClosedError ? "closed-early" : "error");
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
}, []);
|
|
260
|
+
const post = (0, import_react5.useCallback)((type, payload) => {
|
|
261
|
+
current.current?.post(type, payload);
|
|
262
|
+
}, []);
|
|
263
|
+
const close = (0, import_react5.useCallback)(() => current.current?.close(), []);
|
|
264
|
+
return { open, status, result, error, post, close };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/index.ts
|
|
268
|
+
__reExport(src_exports, require("@use-everywhere/core"), module.exports);
|
|
269
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
270
|
+
0 && (module.exports = {
|
|
271
|
+
DEFAULT_NAME,
|
|
272
|
+
defineChannel,
|
|
273
|
+
defineStore,
|
|
274
|
+
getLeader,
|
|
275
|
+
getSharedStore,
|
|
276
|
+
useChannel,
|
|
277
|
+
useClientId,
|
|
278
|
+
useIsLeader,
|
|
279
|
+
useLeader,
|
|
280
|
+
useLeaderEffect,
|
|
281
|
+
useMessage,
|
|
282
|
+
useOpenedWindow,
|
|
283
|
+
usePeers,
|
|
284
|
+
useSend,
|
|
285
|
+
useSharedState,
|
|
286
|
+
...require("@use-everywhere/core")
|
|
287
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { SharedStore, LeaderOptions, Leader, MessageMap, Channel, MessageMeta, PersistAdapter, Peer, LeaderSnapshot, OpenedWindow } from '@use-everywhere/core';
|
|
2
|
+
export * from '@use-everywhere/core';
|
|
3
|
+
export { DEFAULT_NAME } from '@use-everywhere/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How far a shared value travels:
|
|
7
|
+
* - 'everywhere' — every tab, window, and worker on this origin (default)
|
|
8
|
+
* - 'tabs' — tabs and windows only; writes coming from workers are ignored
|
|
9
|
+
* - 'tab' — this tab only (state is still shared between components)
|
|
10
|
+
*/
|
|
11
|
+
type ShareScope = 'everywhere' | 'tabs' | 'tab';
|
|
12
|
+
interface UseSharedStateOptions {
|
|
13
|
+
/** Store name; keys live in a namespace per store. Default 'use-everywhere'. */
|
|
14
|
+
store?: string;
|
|
15
|
+
/** How much to share. Default 'everywhere'. */
|
|
16
|
+
scope?: ShareScope;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Like useState, but the value exists in every tab, window, and worker on
|
|
21
|
+
* this origin. Late-joining tabs hydrate to the current value; concurrent
|
|
22
|
+
* writes converge last-writer-wins. Pass options.scope to delimit how much
|
|
23
|
+
* is shared ('everywhere' | 'tabs' | 'tab').
|
|
24
|
+
*/
|
|
25
|
+
declare function useSharedState<T>(key: string, initial: T, options?: UseSharedStateOptions): [T, (next: T | ((prev: T) => T)) => void];
|
|
26
|
+
|
|
27
|
+
/** Registry stores hold arbitrary keys — shape is decided by the hooks using them. */
|
|
28
|
+
type AnyStore = SharedStore<Record<string, unknown>>;
|
|
29
|
+
|
|
30
|
+
/** Imperative access to the store behind useSharedState (patch logs, non-React code). */
|
|
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;
|
|
39
|
+
|
|
40
|
+
/** Get the page-wide typed channel for `name` (one instance per name). */
|
|
41
|
+
declare function useChannel<M extends MessageMap>(name: string): Channel<M>;
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to one message type. The handler is kept fresh without
|
|
44
|
+
* resubscribing, so it may close over render state.
|
|
45
|
+
*/
|
|
46
|
+
declare function useMessage<M extends MessageMap, K extends keyof M & string>(channel: Channel<M>, type: K, handler: (payload: M[K], meta: MessageMeta) => void): void;
|
|
47
|
+
/** The channel's post function (stable identity per channel). */
|
|
48
|
+
declare function useSend<M extends MessageMap>(channel: Channel<M>): Channel<M>['post'];
|
|
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
|
+
|
|
104
|
+
/** The other tabs/windows/workers currently alive on this origin. */
|
|
105
|
+
declare function usePeers(options?: {
|
|
106
|
+
name?: string;
|
|
107
|
+
}): readonly Peer[];
|
|
108
|
+
/** This client's own id on the presence bus (matches patch origin ids). */
|
|
109
|
+
declare function useClientId(options?: {
|
|
110
|
+
name?: string;
|
|
111
|
+
}): string;
|
|
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
|
+
|
|
132
|
+
type OpenedWindowStatus = 'idle' | 'opening' | 'connected' | 'done' | 'closed-early' | 'error';
|
|
133
|
+
interface UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> {
|
|
134
|
+
/** Call from a click handler (popup blockers require a user gesture). */
|
|
135
|
+
open: () => void;
|
|
136
|
+
status: OpenedWindowStatus;
|
|
137
|
+
/** The child's finish() value, once status is 'done'. */
|
|
138
|
+
result: R | undefined;
|
|
139
|
+
error: unknown;
|
|
140
|
+
/** Post to the child; no-op while nothing is open. */
|
|
141
|
+
post: OpenedWindow<Out, In, R>['post'];
|
|
142
|
+
close: () => void;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Drive an openWindow() flow from a component: the factory is called on
|
|
147
|
+
* open(), and the child window's lifecycle is folded into render state.
|
|
148
|
+
* Reopening replaces the previous window; a stale window's outcome is ignored.
|
|
149
|
+
*/
|
|
150
|
+
declare function useOpenedWindow<Out extends MessageMap, In extends MessageMap, R = unknown>(factory: () => OpenedWindow<Out, In, R>): UseOpenedWindow<Out, In, R>;
|
|
151
|
+
|
|
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.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>;
|
|
@@ -66,6 +68,39 @@ interface ChannelHooks<M extends MessageMap> {
|
|
|
66
68
|
*/
|
|
67
69
|
declare function defineChannel<M extends MessageMap>(name: string): ChannelHooks<M>;
|
|
68
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
|
+
|
|
69
104
|
/** The other tabs/windows/workers currently alive on this origin. */
|
|
70
105
|
declare function usePeers(options?: {
|
|
71
106
|
name?: string;
|
|
@@ -75,6 +110,25 @@ declare function useClientId(options?: {
|
|
|
75
110
|
name?: string;
|
|
76
111
|
}): string;
|
|
77
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
|
+
|
|
78
132
|
type OpenedWindowStatus = 'idle' | 'opening' | 'connected' | 'done' | 'closed-early' | 'error';
|
|
79
133
|
interface UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> {
|
|
80
134
|
/** Call from a click handler (popup blockers require a user gesture). */
|
|
@@ -95,4 +149,4 @@ interface UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> {
|
|
|
95
149
|
*/
|
|
96
150
|
declare function useOpenedWindow<Out extends MessageMap, In extends MessageMap, R = unknown>(factory: () => OpenedWindow<Out, In, R>): UseOpenedWindow<Out, In, R>;
|
|
97
151
|
|
|
98
|
-
export { type AnyStore, type ChannelHooks,
|
|
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 };
|