@use-everywhere/core 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 +21 -0
- package/README.md +35 -0
- package/dist/index.d.ts +251 -0
- package/dist/index.js +631 -0
- package/package.json +51 -0
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,35 @@
|
|
|
1
|
+
# @use-everywhere/core
|
|
2
|
+
|
|
3
|
+
Framework-agnostic engine for cross-tab shared state, typed events, peer
|
|
4
|
+
presence, and secure cross-origin window channels.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm i @use-everywhere/core
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
> Using React? Install [`use-everywhere`](https://www.npmjs.com/package/use-everywhere)
|
|
11
|
+
> instead — it provides hooks and re-exports this entire package.
|
|
12
|
+
|
|
13
|
+
Two transports behind one library:
|
|
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.
|
|
17
|
+
- **window.opener / postMessage** (cross-origin): a secure 1:1 channel to a
|
|
18
|
+
window you opened. Every message is validated by origin, envelope brand, a
|
|
19
|
+
per-connection nonce, and the source window.
|
|
20
|
+
|
|
21
|
+
## Design notes
|
|
22
|
+
|
|
23
|
+
- **Shared state never crosses origins.** Two origins are two trust domains;
|
|
24
|
+
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.).
|
|
29
|
+
|
|
30
|
+
Full docs, demo app (including a real cross-origin payment flow), and source:
|
|
31
|
+
[github.com/rxova/use-everywhere](https://github.com/rxova/use-everywhere)
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal message bus. Implementations: BroadcastChannelTransport (same-origin),
|
|
3
|
+
* NoopTransport (SSR / local-only), MemoryTransport (tests). A transport never
|
|
4
|
+
* echoes a client's own posts back to it.
|
|
5
|
+
*/
|
|
6
|
+
interface Transport {
|
|
7
|
+
post(data: unknown): void;
|
|
8
|
+
subscribe(listener: (data: unknown) => void): () => void;
|
|
9
|
+
close(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type MessageMap = Record<string, unknown>;
|
|
13
|
+
type PeerKind = 'tab' | 'worker' | (string & {});
|
|
14
|
+
/** Per-key logical clock: [counter, clientId]. Ties break by clientId. */
|
|
15
|
+
type Version = readonly [counter: number, clientId: string];
|
|
16
|
+
interface Peer {
|
|
17
|
+
id: string;
|
|
18
|
+
kind: PeerKind;
|
|
19
|
+
lastSeen: number;
|
|
20
|
+
}
|
|
21
|
+
interface MessageMeta {
|
|
22
|
+
clientId: string;
|
|
23
|
+
kind: PeerKind;
|
|
24
|
+
self: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface CommonOptions {
|
|
27
|
+
/** Transport factory, mainly for tests. Defaults to defaultTransport. */
|
|
28
|
+
transport?: (name: string) => Transport;
|
|
29
|
+
/** What this client announces itself as. Defaults to 'worker' when there is no document, else 'tab'. */
|
|
30
|
+
kind?: PeerKind;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Channel<M extends MessageMap> {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly clientId: string;
|
|
36
|
+
/** Fire-and-forget to every other tab/window/worker on this origin. Not echoed to self. */
|
|
37
|
+
post<K extends keyof M & string>(type: K, payload: M[K]): void;
|
|
38
|
+
on<K extends keyof M & string>(type: K, handler: (payload: M[K], meta: MessageMeta) => void): () => void;
|
|
39
|
+
close(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Typed pub/sub over the same-origin bus. */
|
|
43
|
+
declare function createChannel<M extends MessageMap>(name: string, options?: CommonOptions): Channel<M>;
|
|
44
|
+
|
|
45
|
+
interface SharedStoreOptions extends CommonOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Gatekeeper for incoming remote writes (patches and snapshot merges):
|
|
48
|
+
* return false to ignore them. Lets callers delimit how much is shared —
|
|
49
|
+
* e.g. accept only writes from other tabs, not from workers.
|
|
50
|
+
*/
|
|
51
|
+
accept?: (meta: MessageMeta) => boolean;
|
|
52
|
+
}
|
|
53
|
+
interface SharedStore<S extends Record<string, unknown>> {
|
|
54
|
+
readonly clientId: string;
|
|
55
|
+
/** Live proxy for imperative use: `store.state.count++` syncs everywhere. */
|
|
56
|
+
readonly state: S;
|
|
57
|
+
/** Immutable snapshot, replaced whenever a change is applied. Safe for useSyncExternalStore. */
|
|
58
|
+
getSnapshot(): Readonly<S>;
|
|
59
|
+
set<K extends keyof S & string>(key: K, value: S[K] | ((prev: S[K]) => S[K])): void;
|
|
60
|
+
subscribe(fn: (key: keyof S & string, value: unknown, meta: MessageMeta) => void): () => void;
|
|
61
|
+
subscribeKey(key: keyof S & string, fn: () => void): () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Register a key lazily at version [0, clientId] — any patch or snapshot a
|
|
64
|
+
* peer has already made for it wins over the initial value. No-op if the
|
|
65
|
+
* key already exists.
|
|
66
|
+
*/
|
|
67
|
+
registerKey<K extends keyof S & string>(key: K, initial: S[K]): void;
|
|
68
|
+
close(): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* State synced across every same-origin tab/window/worker: per-key
|
|
73
|
+
* last-writer-wins version clocks and a hello/snapshot late-joiner handshake.
|
|
74
|
+
* Create at most one store per name per tab (the React package memoizes).
|
|
75
|
+
*/
|
|
76
|
+
declare function createSharedStore<S extends Record<string, unknown>>(name: string, initial: S, options?: SharedStoreOptions): SharedStore<S>;
|
|
77
|
+
|
|
78
|
+
interface BusOptions extends CommonOptions {
|
|
79
|
+
/** Presence heartbeat interval in ms. Default 2000. */
|
|
80
|
+
heartbeatMs?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface PresenceOptions extends BusOptions {
|
|
84
|
+
/** Peers silent for longer than this are dropped. Default 5000ms. */
|
|
85
|
+
pruneAfterMs?: number;
|
|
86
|
+
}
|
|
87
|
+
interface Presence {
|
|
88
|
+
readonly clientId: string;
|
|
89
|
+
/** Stable array snapshot (replaced on change) — safe for useSyncExternalStore. */
|
|
90
|
+
getPeers(): readonly Peer[];
|
|
91
|
+
subscribe(fn: () => void): () => void;
|
|
92
|
+
close(): void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Tracks the other tabs/windows/workers on this bus. Any message from a peer
|
|
97
|
+
* counts as a liveness signal (state patches, events, and presence pings all
|
|
98
|
+
* piggyback); explicit 'bye' or silence past pruneAfterMs removes them.
|
|
99
|
+
*/
|
|
100
|
+
declare function createPresence(name: string, options?: PresenceOptions): Presence;
|
|
101
|
+
|
|
102
|
+
interface MessageEventLike {
|
|
103
|
+
data: unknown;
|
|
104
|
+
origin: string;
|
|
105
|
+
source: unknown;
|
|
106
|
+
}
|
|
107
|
+
/** The subset of Window we post to (the other side). */
|
|
108
|
+
interface WindowLike {
|
|
109
|
+
postMessage(data: unknown, targetOrigin: string): void;
|
|
110
|
+
closed?: boolean;
|
|
111
|
+
close?(): void;
|
|
112
|
+
}
|
|
113
|
+
/** The subset of Window we listen on (our side). */
|
|
114
|
+
interface WindowEventTarget {
|
|
115
|
+
addEventListener(type: string, listener: (event: MessageEventLike) => void): void;
|
|
116
|
+
removeEventListener(type: string, listener: (event: MessageEventLike) => void): void;
|
|
117
|
+
}
|
|
118
|
+
interface OpenWindowOptions {
|
|
119
|
+
/** Exact origin of the page being opened, e.g. 'https://pay.example.com'. Required. */
|
|
120
|
+
peerOrigin: string;
|
|
121
|
+
/** window.open feature string, e.g. 'popup,width=480,height=640'. */
|
|
122
|
+
features?: string;
|
|
123
|
+
/** Give up on the ready handshake after this long. Default 15000ms. */
|
|
124
|
+
readyTimeoutMs?: number;
|
|
125
|
+
/** Dev only: accept messages from any origin and post with targetOrigin '*'. */
|
|
126
|
+
allowAnyOrigin?: boolean;
|
|
127
|
+
/** Test seam. Defaults to window.open. */
|
|
128
|
+
openFn?: (url: string, target: string, features?: string) => WindowLike | null;
|
|
129
|
+
/** Test seam. Defaults to the global window. */
|
|
130
|
+
localWindow?: WindowEventTarget;
|
|
131
|
+
}
|
|
132
|
+
interface OpenedWindow<Out extends MessageMap, In extends MessageMap, R> {
|
|
133
|
+
/** The opened window, or null if the popup was blocked. */
|
|
134
|
+
readonly window: WindowLike | null;
|
|
135
|
+
/** Resolves once the child completes the ready handshake. */
|
|
136
|
+
readonly ready: Promise<void>;
|
|
137
|
+
/** Queued until the handshake completes — nothing is dropped while the child loads. */
|
|
138
|
+
post<K extends keyof Out & string>(type: K, payload: Out[K]): void;
|
|
139
|
+
on<K extends keyof In & string>(type: K, handler: (payload: In[K]) => void): () => void;
|
|
140
|
+
/** The child's finish() value. Rejects WindowClosedError / HandshakeTimeoutError. */
|
|
141
|
+
readonly result: Promise<R>;
|
|
142
|
+
/** Resolves when the child window is gone (with or without a result). */
|
|
143
|
+
readonly closed: Promise<void>;
|
|
144
|
+
/** Close the child window. */
|
|
145
|
+
close(): void;
|
|
146
|
+
}
|
|
147
|
+
interface ConnectToOpenerOptions {
|
|
148
|
+
/** Exact origin of the page that opened this window. Required. */
|
|
149
|
+
peerOrigin: string;
|
|
150
|
+
/** Give up on the ready handshake after this long. Default 15000ms. */
|
|
151
|
+
readyTimeoutMs?: number;
|
|
152
|
+
/** Dev only: accept messages from any origin and post with targetOrigin '*'. */
|
|
153
|
+
allowAnyOrigin?: boolean;
|
|
154
|
+
/** Test seam. Defaults to window.opener. */
|
|
155
|
+
opener?: WindowLike | null;
|
|
156
|
+
/** Test seam. Defaults to the global window. */
|
|
157
|
+
localWindow?: WindowEventTarget;
|
|
158
|
+
/** Test seam. Defaults to the ue-cid query parameter. */
|
|
159
|
+
cid?: string;
|
|
160
|
+
}
|
|
161
|
+
interface OpenerConnection<In extends MessageMap, Out extends MessageMap, R> {
|
|
162
|
+
/** Resolves once the opener acknowledges the ready handshake. */
|
|
163
|
+
readonly ready: Promise<void>;
|
|
164
|
+
/** Queued until the handshake completes. */
|
|
165
|
+
post<K extends keyof Out & string>(type: K, payload: Out[K]): void;
|
|
166
|
+
on<K extends keyof In & string>(type: K, handler: (payload: In[K]) => void): () => void;
|
|
167
|
+
/** Deliver the terminal result to the opener. Does not close the window. */
|
|
168
|
+
finish(result: R): void;
|
|
169
|
+
/** Tell the opener we're going away, then close this window. */
|
|
170
|
+
close(): void;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
declare const CID_PARAM = "ue-cid";
|
|
174
|
+
/**
|
|
175
|
+
* Open a window (possibly on another origin) and get a typed 1:1 channel to it.
|
|
176
|
+
* The child must call connectToOpener(). Every received message is validated:
|
|
177
|
+
* event.origin, envelope brand, per-connection nonce, and event.source.
|
|
178
|
+
*/
|
|
179
|
+
declare function openWindow<Out extends MessageMap, In extends MessageMap, R = unknown>(url: string | URL, options: OpenWindowOptions): OpenedWindow<Out, In, R>;
|
|
180
|
+
/**
|
|
181
|
+
* Call from the opened (child) window to connect back to its opener.
|
|
182
|
+
* Throws synchronously when there is no opener or no ue-cid parameter —
|
|
183
|
+
* i.e. the page was not opened via openWindow().
|
|
184
|
+
*/
|
|
185
|
+
declare function connectToOpener<In extends MessageMap, Out extends MessageMap, R = unknown>(options: ConnectToOpenerOptions): OpenerConnection<In, Out, R>;
|
|
186
|
+
|
|
187
|
+
/** The opened window closed before delivering a result. */
|
|
188
|
+
declare class WindowClosedError extends Error {
|
|
189
|
+
constructor(message?: string);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The ready/ready-ack handshake never completed. */
|
|
193
|
+
declare class HandshakeTimeoutError extends Error {
|
|
194
|
+
constructor(message?: string);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Is `a` newer than `b`? Last-writer-wins; equal counters break ties by clientId. */
|
|
198
|
+
declare function newer(a: Version, b: Version | undefined): boolean;
|
|
199
|
+
|
|
200
|
+
/** Same-origin transport over a real BroadcastChannel. */
|
|
201
|
+
declare class BroadcastChannelTransport implements Transport {
|
|
202
|
+
private bc;
|
|
203
|
+
private listeners;
|
|
204
|
+
constructor(name: string);
|
|
205
|
+
post(data: unknown): void;
|
|
206
|
+
subscribe(listener: (data: unknown) => void): () => void;
|
|
207
|
+
close(): void;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Silent local transport: nothing leaves this context, nothing arrives.
|
|
212
|
+
* Used for SSR and for state scoped to a single tab.
|
|
213
|
+
*/
|
|
214
|
+
declare class NoopTransport implements Transport {
|
|
215
|
+
post(): void;
|
|
216
|
+
subscribe(): () => void;
|
|
217
|
+
close(): void;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
declare function isBroadcastChannelAvailable(): boolean;
|
|
221
|
+
/** Default factory: real BroadcastChannel when available, otherwise a local no-op. */
|
|
222
|
+
declare function defaultTransport(name: string): Transport;
|
|
223
|
+
|
|
224
|
+
/** One simulated client on a MemoryHub. Create via hub.connect(). */
|
|
225
|
+
declare class MemoryTransport implements Transport {
|
|
226
|
+
private hub;
|
|
227
|
+
private listeners;
|
|
228
|
+
private closed;
|
|
229
|
+
constructor(hub: MemoryHub);
|
|
230
|
+
post(data: unknown): void;
|
|
231
|
+
subscribe(listener: (data: unknown) => void): () => void;
|
|
232
|
+
close(): void;
|
|
233
|
+
/** @internal */
|
|
234
|
+
deliver(data: unknown): void;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* In-memory hub for tests: N transports attached to one hub, each post is
|
|
239
|
+
* delivered to every *other* transport on a microtask (mirrors BroadcastChannel's
|
|
240
|
+
* async, no-self-echo delivery).
|
|
241
|
+
*/
|
|
242
|
+
declare class MemoryHub {
|
|
243
|
+
private transports;
|
|
244
|
+
connect(): MemoryTransport;
|
|
245
|
+
/** @internal */
|
|
246
|
+
broadcast(from: MemoryTransport, data: unknown): void;
|
|
247
|
+
/** @internal */
|
|
248
|
+
disconnect(transport: MemoryTransport): void;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export { BroadcastChannelTransport, CID_PARAM, type Channel, type CommonOptions, type ConnectToOpenerOptions, HandshakeTimeoutError, MemoryHub, MemoryTransport, type MessageEventLike, type MessageMap, type MessageMeta, NoopTransport, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type Presence, type PresenceOptions, type SharedStore, type SharedStoreOptions, type Transport, type Version, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createPresence, createSharedStore, defaultTransport, isBroadcastChannelAvailable, newer, openWindow };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
// src/ids.ts
|
|
2
|
+
function newClientId() {
|
|
3
|
+
return Math.random().toString(36).slice(2, 8);
|
|
4
|
+
}
|
|
5
|
+
function newMsgId() {
|
|
6
|
+
return Math.random().toString(36).slice(2, 10);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/transport/broadcast-channel-transport.ts
|
|
10
|
+
var BroadcastChannelTransport = class {
|
|
11
|
+
constructor(name) {
|
|
12
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
13
|
+
this.bc = new BroadcastChannel(name);
|
|
14
|
+
this.bc.onmessage = (event) => {
|
|
15
|
+
for (const listener of this.listeners) listener(event.data);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
post(data) {
|
|
19
|
+
this.bc.postMessage(data);
|
|
20
|
+
}
|
|
21
|
+
subscribe(listener) {
|
|
22
|
+
this.listeners.add(listener);
|
|
23
|
+
return () => this.listeners.delete(listener);
|
|
24
|
+
}
|
|
25
|
+
close() {
|
|
26
|
+
this.listeners.clear();
|
|
27
|
+
this.bc.close();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/transport/noop-transport.ts
|
|
32
|
+
var NoopTransport = class {
|
|
33
|
+
post() {
|
|
34
|
+
}
|
|
35
|
+
subscribe() {
|
|
36
|
+
return () => {
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
close() {
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/transport/default-transport.ts
|
|
44
|
+
function isBroadcastChannelAvailable() {
|
|
45
|
+
return typeof BroadcastChannel !== "undefined";
|
|
46
|
+
}
|
|
47
|
+
function defaultTransport(name) {
|
|
48
|
+
return isBroadcastChannelAvailable() ? new BroadcastChannelTransport(name) : new NoopTransport();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/bus.ts
|
|
52
|
+
function isBusWire(data) {
|
|
53
|
+
return typeof data === "object" && data !== null && data.v === 1;
|
|
54
|
+
}
|
|
55
|
+
function defaultKind() {
|
|
56
|
+
return typeof document === "undefined" ? "worker" : "tab";
|
|
57
|
+
}
|
|
58
|
+
var registry = /* @__PURE__ */ new Map();
|
|
59
|
+
function createBus(name, options, onShutdown) {
|
|
60
|
+
const transport = (options.transport ?? defaultTransport)(name);
|
|
61
|
+
const clientId = newClientId();
|
|
62
|
+
const kind = options.kind ?? defaultKind();
|
|
63
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
64
|
+
let refs = 0;
|
|
65
|
+
let closed = false;
|
|
66
|
+
const post = (wire) => {
|
|
67
|
+
if (!closed) transport.post(wire);
|
|
68
|
+
};
|
|
69
|
+
const unsubscribe = transport.subscribe((data) => {
|
|
70
|
+
if (!isBusWire(data)) return;
|
|
71
|
+
if (data.clientId === clientId) return;
|
|
72
|
+
if (data.scope === "presence" && data.type === "hello") {
|
|
73
|
+
post({ v: 1, scope: "presence", type: "ping", clientId, kind });
|
|
74
|
+
}
|
|
75
|
+
for (const fn of listeners) fn(data);
|
|
76
|
+
});
|
|
77
|
+
post({ v: 1, scope: "presence", type: "hello", clientId, kind });
|
|
78
|
+
const heartbeat = setInterval(
|
|
79
|
+
() => post({ v: 1, scope: "presence", type: "ping", clientId, kind }),
|
|
80
|
+
options.heartbeatMs ?? 2e3
|
|
81
|
+
);
|
|
82
|
+
const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind });
|
|
83
|
+
const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
|
|
84
|
+
if (hasWindow) addEventListener("pagehide", sayBye);
|
|
85
|
+
return {
|
|
86
|
+
name,
|
|
87
|
+
clientId,
|
|
88
|
+
kind,
|
|
89
|
+
post,
|
|
90
|
+
subscribe(fn) {
|
|
91
|
+
listeners.add(fn);
|
|
92
|
+
return () => listeners.delete(fn);
|
|
93
|
+
},
|
|
94
|
+
acquire() {
|
|
95
|
+
refs++;
|
|
96
|
+
},
|
|
97
|
+
release() {
|
|
98
|
+
refs--;
|
|
99
|
+
if (refs > 0) return;
|
|
100
|
+
sayBye();
|
|
101
|
+
closed = true;
|
|
102
|
+
clearInterval(heartbeat);
|
|
103
|
+
if (hasWindow) removeEventListener("pagehide", sayBye);
|
|
104
|
+
unsubscribe();
|
|
105
|
+
transport.close();
|
|
106
|
+
listeners.clear();
|
|
107
|
+
onShutdown();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function getBus(name, options = {}) {
|
|
112
|
+
if (options.transport) {
|
|
113
|
+
const bus2 = createBus(name, options, () => {
|
|
114
|
+
});
|
|
115
|
+
bus2.acquire();
|
|
116
|
+
return bus2;
|
|
117
|
+
}
|
|
118
|
+
let bus = registry.get(name);
|
|
119
|
+
if (!bus) {
|
|
120
|
+
bus = createBus(name, options, () => registry.delete(name));
|
|
121
|
+
registry.set(name, bus);
|
|
122
|
+
}
|
|
123
|
+
bus.acquire();
|
|
124
|
+
return bus;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/channel.ts
|
|
128
|
+
function createChannel(name, options = {}) {
|
|
129
|
+
const bus = getBus(name, options);
|
|
130
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
131
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
132
|
+
if (wire.scope !== "event") return;
|
|
133
|
+
const set = handlers.get(wire.type);
|
|
134
|
+
if (!set) return;
|
|
135
|
+
const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
|
|
136
|
+
for (const fn of set) fn(wire.payload, meta);
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
name,
|
|
140
|
+
clientId: bus.clientId,
|
|
141
|
+
post(type, payload) {
|
|
142
|
+
bus.post({
|
|
143
|
+
v: 1,
|
|
144
|
+
scope: "event",
|
|
145
|
+
type,
|
|
146
|
+
payload,
|
|
147
|
+
clientId: bus.clientId,
|
|
148
|
+
kind: bus.kind,
|
|
149
|
+
msgId: newMsgId()
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
on(type, handler) {
|
|
153
|
+
let set = handlers.get(type);
|
|
154
|
+
if (!set) {
|
|
155
|
+
set = /* @__PURE__ */ new Set();
|
|
156
|
+
handlers.set(type, set);
|
|
157
|
+
}
|
|
158
|
+
set.add(handler);
|
|
159
|
+
return () => set.delete(handler);
|
|
160
|
+
},
|
|
161
|
+
close() {
|
|
162
|
+
unsubscribe();
|
|
163
|
+
handlers.clear();
|
|
164
|
+
bus.release();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/clock.ts
|
|
170
|
+
function newer(a, b) {
|
|
171
|
+
return !b || a[0] > b[0] || a[0] === b[0] && a[1] > b[1];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/shared-store.ts
|
|
175
|
+
function createSharedStore(name, initial, options = {}) {
|
|
176
|
+
const bus = getBus(name, options);
|
|
177
|
+
const clientId = bus.clientId;
|
|
178
|
+
const accept = options.accept;
|
|
179
|
+
const state = { ...initial };
|
|
180
|
+
const versions = {};
|
|
181
|
+
for (const k in state) versions[k] = [0, clientId];
|
|
182
|
+
let snapshot = Object.freeze({ ...state });
|
|
183
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
184
|
+
const keyListeners = /* @__PURE__ */ new Map();
|
|
185
|
+
function notify(key, value, meta) {
|
|
186
|
+
snapshot = Object.freeze({ ...state });
|
|
187
|
+
for (const fn of listeners) fn(key, value, meta);
|
|
188
|
+
const set = keyListeners.get(key);
|
|
189
|
+
if (set) for (const fn of set) fn();
|
|
190
|
+
}
|
|
191
|
+
function applyRemote(key, value, version, meta) {
|
|
192
|
+
if (!newer(version, versions[key])) return;
|
|
193
|
+
versions[key] = version;
|
|
194
|
+
state[key] = value;
|
|
195
|
+
notify(key, value, meta);
|
|
196
|
+
}
|
|
197
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
198
|
+
if (wire.scope !== "state") return;
|
|
199
|
+
const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
|
|
200
|
+
if (wire.type === "hello") {
|
|
201
|
+
bus.post({
|
|
202
|
+
v: 1,
|
|
203
|
+
scope: "state",
|
|
204
|
+
type: "snapshot",
|
|
205
|
+
clientId,
|
|
206
|
+
kind: bus.kind,
|
|
207
|
+
state: { ...state },
|
|
208
|
+
versions: { ...versions }
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (accept && !accept(meta)) return;
|
|
213
|
+
if (wire.type === "patch") {
|
|
214
|
+
applyRemote(wire.key, wire.value, wire.version, meta);
|
|
215
|
+
} else {
|
|
216
|
+
for (const k in wire.state) {
|
|
217
|
+
const version = wire.versions[k];
|
|
218
|
+
if (version) applyRemote(k, wire.state[k], version, meta);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
function setKey(key, value) {
|
|
223
|
+
const version = [(versions[key]?.[0] ?? 0) + 1, clientId];
|
|
224
|
+
versions[key] = version;
|
|
225
|
+
state[key] = value;
|
|
226
|
+
bus.post({
|
|
227
|
+
v: 1,
|
|
228
|
+
scope: "state",
|
|
229
|
+
type: "patch",
|
|
230
|
+
key,
|
|
231
|
+
value,
|
|
232
|
+
version,
|
|
233
|
+
clientId,
|
|
234
|
+
kind: bus.kind
|
|
235
|
+
});
|
|
236
|
+
notify(key, value, { clientId, kind: bus.kind, self: true });
|
|
237
|
+
}
|
|
238
|
+
const proxy = new Proxy(state, {
|
|
239
|
+
set(_target, key, value) {
|
|
240
|
+
if (typeof key !== "string") return false;
|
|
241
|
+
setKey(key, value);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
bus.post({ v: 1, scope: "state", type: "hello", clientId, kind: bus.kind });
|
|
246
|
+
return {
|
|
247
|
+
clientId,
|
|
248
|
+
state: proxy,
|
|
249
|
+
getSnapshot: () => snapshot,
|
|
250
|
+
set(key, value) {
|
|
251
|
+
const next = typeof value === "function" ? value(state[key]) : value;
|
|
252
|
+
setKey(key, next);
|
|
253
|
+
},
|
|
254
|
+
subscribe(fn) {
|
|
255
|
+
listeners.add(fn);
|
|
256
|
+
return () => listeners.delete(fn);
|
|
257
|
+
},
|
|
258
|
+
subscribeKey(key, fn) {
|
|
259
|
+
let set = keyListeners.get(key);
|
|
260
|
+
if (!set) {
|
|
261
|
+
set = /* @__PURE__ */ new Set();
|
|
262
|
+
keyListeners.set(key, set);
|
|
263
|
+
}
|
|
264
|
+
set.add(fn);
|
|
265
|
+
return () => set.delete(fn);
|
|
266
|
+
},
|
|
267
|
+
registerKey(key, initialValue) {
|
|
268
|
+
if (key in versions) return;
|
|
269
|
+
versions[key] = [0, clientId];
|
|
270
|
+
state[key] = initialValue;
|
|
271
|
+
snapshot = Object.freeze({ ...state });
|
|
272
|
+
},
|
|
273
|
+
close() {
|
|
274
|
+
unsubscribe();
|
|
275
|
+
listeners.clear();
|
|
276
|
+
keyListeners.clear();
|
|
277
|
+
bus.release();
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/presence.ts
|
|
283
|
+
function createPresence(name, options = {}) {
|
|
284
|
+
const pruneAfterMs = options.pruneAfterMs ?? 5e3;
|
|
285
|
+
const bus = getBus(name, options);
|
|
286
|
+
const peers = /* @__PURE__ */ new Map();
|
|
287
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
288
|
+
let snapshot = [];
|
|
289
|
+
function notify() {
|
|
290
|
+
snapshot = Object.freeze([...peers.values()]);
|
|
291
|
+
for (const fn of listeners) fn();
|
|
292
|
+
}
|
|
293
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
294
|
+
if (wire.scope === "presence" && wire.type === "bye") {
|
|
295
|
+
if (peers.delete(wire.clientId)) notify();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const existing = peers.get(wire.clientId);
|
|
299
|
+
peers.set(wire.clientId, { id: wire.clientId, kind: wire.kind, lastSeen: Date.now() });
|
|
300
|
+
if (!existing) notify();
|
|
301
|
+
});
|
|
302
|
+
const prune = setInterval(
|
|
303
|
+
() => {
|
|
304
|
+
const cutoff = Date.now() - pruneAfterMs;
|
|
305
|
+
let changed = false;
|
|
306
|
+
for (const [id, peer] of peers) {
|
|
307
|
+
if (peer.lastSeen < cutoff) {
|
|
308
|
+
peers.delete(id);
|
|
309
|
+
changed = true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (changed) notify();
|
|
313
|
+
},
|
|
314
|
+
Math.max(500, Math.floor(pruneAfterMs / 2))
|
|
315
|
+
);
|
|
316
|
+
return {
|
|
317
|
+
clientId: bus.clientId,
|
|
318
|
+
getPeers: () => snapshot,
|
|
319
|
+
subscribe(fn) {
|
|
320
|
+
listeners.add(fn);
|
|
321
|
+
return () => listeners.delete(fn);
|
|
322
|
+
},
|
|
323
|
+
close() {
|
|
324
|
+
clearInterval(prune);
|
|
325
|
+
unsubscribe();
|
|
326
|
+
listeners.clear();
|
|
327
|
+
bus.release();
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/errors/handshake-timeout-error.ts
|
|
333
|
+
var HandshakeTimeoutError = class extends Error {
|
|
334
|
+
constructor(message = "handshake with the other window timed out") {
|
|
335
|
+
super(message);
|
|
336
|
+
this.name = "HandshakeTimeoutError";
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/errors/window-closed-error.ts
|
|
341
|
+
var WindowClosedError = class extends Error {
|
|
342
|
+
constructor(message = "window closed before a result was delivered") {
|
|
343
|
+
super(message);
|
|
344
|
+
this.name = "WindowClosedError";
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// src/window-channel.ts
|
|
349
|
+
var CID_PARAM = "ue-cid";
|
|
350
|
+
function isWindowWire(data, cid) {
|
|
351
|
+
return typeof data === "object" && data !== null && data.__ue === 1 && data.cid === cid;
|
|
352
|
+
}
|
|
353
|
+
function validatePeerOrigin(peerOrigin, allowAnyOrigin) {
|
|
354
|
+
if (peerOrigin === "*" && !allowAnyOrigin) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
"peerOrigin '*' is unsafe: any page could read your messages. Pass the exact origin, or set allowAnyOrigin: true (dev only)."
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function markHandled(promise) {
|
|
361
|
+
promise.catch(() => {
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function openWindow(url, options) {
|
|
365
|
+
const { peerOrigin, allowAnyOrigin } = options;
|
|
366
|
+
validatePeerOrigin(peerOrigin, allowAnyOrigin);
|
|
367
|
+
const base = typeof location !== "undefined" ? location.href : void 0;
|
|
368
|
+
const resolved = new URL(url, base);
|
|
369
|
+
if (!allowAnyOrigin && resolved.origin !== peerOrigin) {
|
|
370
|
+
throw new Error(`url origin ${resolved.origin} does not match peerOrigin ${peerOrigin}`);
|
|
371
|
+
}
|
|
372
|
+
const cid = newMsgId();
|
|
373
|
+
resolved.searchParams.set(CID_PARAM, cid);
|
|
374
|
+
const localWindow = options.localWindow ?? window;
|
|
375
|
+
const openFn = options.openFn ?? ((u, target, features) => window.open(u, target, features));
|
|
376
|
+
const targetOrigin = allowAnyOrigin ? "*" : peerOrigin;
|
|
377
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
378
|
+
const outQueue = [];
|
|
379
|
+
let isReady = false;
|
|
380
|
+
let resultSettled = false;
|
|
381
|
+
let closedSettled = false;
|
|
382
|
+
let resolveReady;
|
|
383
|
+
let rejectReady;
|
|
384
|
+
const ready = new Promise((res, rej) => (resolveReady = res, rejectReady = rej));
|
|
385
|
+
let resolveResult;
|
|
386
|
+
let rejectResult;
|
|
387
|
+
const result = new Promise((res, rej) => (resolveResult = res, rejectResult = rej));
|
|
388
|
+
let resolveClosed;
|
|
389
|
+
const closed = new Promise((res) => resolveClosed = res);
|
|
390
|
+
markHandled(ready);
|
|
391
|
+
markHandled(result);
|
|
392
|
+
const onMessage = (event) => {
|
|
393
|
+
if (!allowAnyOrigin && event.origin !== peerOrigin) return;
|
|
394
|
+
if (!isWindowWire(event.data, cid)) return;
|
|
395
|
+
if (childWindow && event.source !== childWindow) return;
|
|
396
|
+
const wire = event.data;
|
|
397
|
+
if (wire.t === "ready") {
|
|
398
|
+
childWindow?.postMessage({ __ue: 1, cid, t: "ready-ack" }, targetOrigin);
|
|
399
|
+
if (!isReady) {
|
|
400
|
+
isReady = true;
|
|
401
|
+
clearTimeout(readyTimer);
|
|
402
|
+
for (const queued of outQueue.splice(0)) childWindow?.postMessage(queued, targetOrigin);
|
|
403
|
+
resolveReady();
|
|
404
|
+
}
|
|
405
|
+
} else if (wire.t === "msg") {
|
|
406
|
+
const set = handlers.get(wire.type);
|
|
407
|
+
if (set) for (const fn of set) fn(wire.payload);
|
|
408
|
+
} else if (wire.t === "result") {
|
|
409
|
+
if (!resultSettled) {
|
|
410
|
+
resultSettled = true;
|
|
411
|
+
resolveResult(wire.payload);
|
|
412
|
+
}
|
|
413
|
+
} else if (wire.t === "close") {
|
|
414
|
+
settleClosed();
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
localWindow.addEventListener("message", onMessage);
|
|
418
|
+
function settleClosed() {
|
|
419
|
+
if (closedSettled) return;
|
|
420
|
+
closedSettled = true;
|
|
421
|
+
clearInterval(closePoller);
|
|
422
|
+
clearTimeout(readyTimer);
|
|
423
|
+
localWindow.removeEventListener("message", onMessage);
|
|
424
|
+
if (!resultSettled) {
|
|
425
|
+
resultSettled = true;
|
|
426
|
+
rejectResult(new WindowClosedError());
|
|
427
|
+
}
|
|
428
|
+
if (!isReady) rejectReady(new WindowClosedError());
|
|
429
|
+
resolveClosed();
|
|
430
|
+
}
|
|
431
|
+
const childWindow = openFn(resolved.toString(), "_blank", options.features);
|
|
432
|
+
if (!childWindow) {
|
|
433
|
+
const err = new Error("popup blocked: window.open returned null");
|
|
434
|
+
localWindow.removeEventListener("message", onMessage);
|
|
435
|
+
rejectReady(err);
|
|
436
|
+
resultSettled = true;
|
|
437
|
+
rejectResult(err);
|
|
438
|
+
return {
|
|
439
|
+
window: null,
|
|
440
|
+
ready,
|
|
441
|
+
post: () => {
|
|
442
|
+
},
|
|
443
|
+
on: () => () => {
|
|
444
|
+
},
|
|
445
|
+
result,
|
|
446
|
+
closed,
|
|
447
|
+
close: () => {
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const closePoller = setInterval(() => {
|
|
452
|
+
if (childWindow.closed) settleClosed();
|
|
453
|
+
}, 400);
|
|
454
|
+
const readyTimer = setTimeout(() => {
|
|
455
|
+
if (isReady || closedSettled) return;
|
|
456
|
+
const err = new HandshakeTimeoutError();
|
|
457
|
+
rejectReady(err);
|
|
458
|
+
if (!resultSettled) {
|
|
459
|
+
resultSettled = true;
|
|
460
|
+
rejectResult(err);
|
|
461
|
+
}
|
|
462
|
+
}, options.readyTimeoutMs ?? 15e3);
|
|
463
|
+
return {
|
|
464
|
+
window: childWindow,
|
|
465
|
+
ready,
|
|
466
|
+
result,
|
|
467
|
+
closed,
|
|
468
|
+
post(type, payload) {
|
|
469
|
+
const wire = { __ue: 1, cid, t: "msg", type, payload, msgId: newMsgId() };
|
|
470
|
+
if (isReady) childWindow.postMessage(wire, targetOrigin);
|
|
471
|
+
else outQueue.push(wire);
|
|
472
|
+
},
|
|
473
|
+
on(type, handler) {
|
|
474
|
+
let set = handlers.get(type);
|
|
475
|
+
if (!set) {
|
|
476
|
+
set = /* @__PURE__ */ new Set();
|
|
477
|
+
handlers.set(type, set);
|
|
478
|
+
}
|
|
479
|
+
set.add(handler);
|
|
480
|
+
return () => set.delete(handler);
|
|
481
|
+
},
|
|
482
|
+
close() {
|
|
483
|
+
childWindow.close?.();
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function connectToOpener(options) {
|
|
488
|
+
const { peerOrigin, allowAnyOrigin } = options;
|
|
489
|
+
validatePeerOrigin(peerOrigin, allowAnyOrigin);
|
|
490
|
+
const opener = options.opener !== void 0 ? options.opener : window.opener ?? null;
|
|
491
|
+
if (!opener) {
|
|
492
|
+
throw new Error("no window.opener \u2014 this page was not opened via openWindow()");
|
|
493
|
+
}
|
|
494
|
+
const cid = options.cid ?? (typeof location !== "undefined" ? new URLSearchParams(location.search).get(CID_PARAM) : null);
|
|
495
|
+
if (!cid) {
|
|
496
|
+
throw new Error(`missing ${CID_PARAM} parameter \u2014 this page was not opened via openWindow()`);
|
|
497
|
+
}
|
|
498
|
+
const localWindow = options.localWindow ?? window;
|
|
499
|
+
const targetOrigin = allowAnyOrigin ? "*" : peerOrigin;
|
|
500
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
501
|
+
const outQueue = [];
|
|
502
|
+
let isReady = false;
|
|
503
|
+
let resolveReady;
|
|
504
|
+
let rejectReady;
|
|
505
|
+
const ready = new Promise((res, rej) => (resolveReady = res, rejectReady = rej));
|
|
506
|
+
markHandled(ready);
|
|
507
|
+
const onMessage = (event) => {
|
|
508
|
+
if (!allowAnyOrigin && event.origin !== peerOrigin) return;
|
|
509
|
+
if (!isWindowWire(event.data, cid)) return;
|
|
510
|
+
const wire = event.data;
|
|
511
|
+
if (wire.t === "ready-ack") {
|
|
512
|
+
if (!isReady) {
|
|
513
|
+
isReady = true;
|
|
514
|
+
clearInterval(retryTimer);
|
|
515
|
+
clearTimeout(giveUpTimer);
|
|
516
|
+
for (const queued of outQueue.splice(0)) opener.postMessage(queued, targetOrigin);
|
|
517
|
+
resolveReady();
|
|
518
|
+
}
|
|
519
|
+
} else if (wire.t === "msg") {
|
|
520
|
+
const set = handlers.get(wire.type);
|
|
521
|
+
if (set) for (const fn of set) fn(wire.payload);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
localWindow.addEventListener("message", onMessage);
|
|
525
|
+
const sayReady = () => opener.postMessage({ __ue: 1, cid, t: "ready" }, targetOrigin);
|
|
526
|
+
sayReady();
|
|
527
|
+
const retryTimer = setInterval(sayReady, 250);
|
|
528
|
+
const giveUpTimer = setTimeout(() => {
|
|
529
|
+
clearInterval(retryTimer);
|
|
530
|
+
localWindow.removeEventListener("message", onMessage);
|
|
531
|
+
rejectReady(new HandshakeTimeoutError());
|
|
532
|
+
}, options.readyTimeoutMs ?? 15e3);
|
|
533
|
+
const sendOrQueue = (wire) => {
|
|
534
|
+
if (isReady) opener.postMessage(wire, targetOrigin);
|
|
535
|
+
else outQueue.push(wire);
|
|
536
|
+
};
|
|
537
|
+
const sayClose = () => opener.postMessage({ __ue: 1, cid, t: "close" }, targetOrigin);
|
|
538
|
+
localWindow.addEventListener("pagehide", sayClose);
|
|
539
|
+
return {
|
|
540
|
+
ready,
|
|
541
|
+
post(type, payload) {
|
|
542
|
+
sendOrQueue({ __ue: 1, cid, t: "msg", type, payload, msgId: newMsgId() });
|
|
543
|
+
},
|
|
544
|
+
on(type, handler) {
|
|
545
|
+
let set = handlers.get(type);
|
|
546
|
+
if (!set) {
|
|
547
|
+
set = /* @__PURE__ */ new Set();
|
|
548
|
+
handlers.set(type, set);
|
|
549
|
+
}
|
|
550
|
+
set.add(handler);
|
|
551
|
+
return () => set.delete(handler);
|
|
552
|
+
},
|
|
553
|
+
finish(value) {
|
|
554
|
+
sendOrQueue({ __ue: 1, cid, t: "result", payload: value });
|
|
555
|
+
},
|
|
556
|
+
close() {
|
|
557
|
+
sayClose();
|
|
558
|
+
localWindow.removeEventListener("message", onMessage);
|
|
559
|
+
localWindow.removeEventListener("pagehide", sayClose);
|
|
560
|
+
if (typeof window !== "undefined" && options.localWindow === void 0) window.close();
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/transport/memory-transport.ts
|
|
566
|
+
var MemoryTransport = class {
|
|
567
|
+
constructor(hub) {
|
|
568
|
+
this.hub = hub;
|
|
569
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
570
|
+
this.closed = false;
|
|
571
|
+
}
|
|
572
|
+
post(data) {
|
|
573
|
+
if (this.closed) return;
|
|
574
|
+
this.hub.broadcast(this, data);
|
|
575
|
+
}
|
|
576
|
+
subscribe(listener) {
|
|
577
|
+
this.listeners.add(listener);
|
|
578
|
+
return () => this.listeners.delete(listener);
|
|
579
|
+
}
|
|
580
|
+
close() {
|
|
581
|
+
this.closed = true;
|
|
582
|
+
this.listeners.clear();
|
|
583
|
+
this.hub.disconnect(this);
|
|
584
|
+
}
|
|
585
|
+
/** @internal */
|
|
586
|
+
deliver(data) {
|
|
587
|
+
if (this.closed) return;
|
|
588
|
+
for (const listener of this.listeners) listener(data);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// src/transport/memory-hub.ts
|
|
593
|
+
var MemoryHub = class {
|
|
594
|
+
constructor() {
|
|
595
|
+
this.transports = /* @__PURE__ */ new Set();
|
|
596
|
+
}
|
|
597
|
+
connect() {
|
|
598
|
+
const transport = new MemoryTransport(this);
|
|
599
|
+
this.transports.add(transport);
|
|
600
|
+
return transport;
|
|
601
|
+
}
|
|
602
|
+
/** @internal */
|
|
603
|
+
broadcast(from, data) {
|
|
604
|
+
for (const transport of this.transports) {
|
|
605
|
+
if (transport !== from) {
|
|
606
|
+
queueMicrotask(() => transport.deliver(data));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/** @internal */
|
|
611
|
+
disconnect(transport) {
|
|
612
|
+
this.transports.delete(transport);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
export {
|
|
616
|
+
BroadcastChannelTransport,
|
|
617
|
+
CID_PARAM,
|
|
618
|
+
HandshakeTimeoutError,
|
|
619
|
+
MemoryHub,
|
|
620
|
+
MemoryTransport,
|
|
621
|
+
NoopTransport,
|
|
622
|
+
WindowClosedError,
|
|
623
|
+
connectToOpener,
|
|
624
|
+
createChannel,
|
|
625
|
+
createPresence,
|
|
626
|
+
createSharedStore,
|
|
627
|
+
defaultTransport,
|
|
628
|
+
isBroadcastChannelAvailable,
|
|
629
|
+
newer,
|
|
630
|
+
openWindow
|
|
631
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@use-everywhere/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cross-tab shared state, events, presence, and cross-origin window channels",
|
|
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/core"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/rxova/use-everywhere#readme",
|
|
13
|
+
"bugs": "https://github.com/rxova/use-everywhere/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"broadcastchannel",
|
|
16
|
+
"cross-tab",
|
|
17
|
+
"shared-state",
|
|
18
|
+
"postmessage",
|
|
19
|
+
"cross-origin",
|
|
20
|
+
"presence",
|
|
21
|
+
"pubsub"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
41
|
+
"happy-dom": "^20.10.6",
|
|
42
|
+
"tsup": "^8.5.0",
|
|
43
|
+
"typescript": "^5.8.3",
|
|
44
|
+
"vitest": "^4.1.10"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"test": "vitest run --coverage",
|
|
49
|
+
"typecheck": "tsc --noEmit"
|
|
50
|
+
}
|
|
51
|
+
}
|