@use-everywhere/core 0.8.0 → 0.10.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 +1 -1
- package/dist/index.cjs +85 -28
- package/dist/index.d.cts +100 -2
- package/dist/index.d.ts +100 -2
- package/dist/index.js +83 -28
- package/dist/shared-worker.cjs +53 -0
- package/dist/shared-worker.d.cts +46 -0
- package/dist/shared-worker.d.ts +46 -0
- package/dist/shared-worker.js +28 -0
- package/package.json +30 -2
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ tabB.getSnapshot().n; // 1
|
|
|
161
161
|
- SSR-safe: without `BroadcastChannel`, engines fall back to a local no-op
|
|
162
162
|
transport.
|
|
163
163
|
|
|
164
|
-
📖 **[Documentation](https://rxova.
|
|
164
|
+
📖 **[Documentation](https://rxova.org/packages/use-everywhere/)** — mental
|
|
165
165
|
model, how sync works, security model, recipes, and generated API reference.
|
|
166
166
|
Source and demo app (with a real cross-origin payment flow):
|
|
167
167
|
[github.com/rxova/use-everywhere](https://github.com/rxova/use-everywhere)
|
package/dist/index.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(src_exports, {
|
|
|
25
25
|
DEFAULT_NAME: () => DEFAULT_NAME,
|
|
26
26
|
HandshakeTimeoutError: () => HandshakeTimeoutError,
|
|
27
27
|
NoopTransport: () => NoopTransport,
|
|
28
|
+
SharedWorkerTransport: () => SharedWorkerTransport,
|
|
28
29
|
StorageTransport: () => StorageTransport,
|
|
29
30
|
WIRE_VERSION: () => WIRE_VERSION,
|
|
30
31
|
WindowClosedError: () => WindowClosedError,
|
|
@@ -42,6 +43,7 @@ __export(src_exports, {
|
|
|
42
43
|
getWireSkew: () => getWireSkew,
|
|
43
44
|
indexedDbAdapter: () => indexedDbAdapter,
|
|
44
45
|
isBroadcastChannelAvailable: () => isBroadcastChannelAvailable,
|
|
46
|
+
isSharedWorkerAvailable: () => isSharedWorkerAvailable,
|
|
45
47
|
isStorageEventAvailable: () => isStorageEventAvailable,
|
|
46
48
|
jsonSerializer: () => jsonSerializer,
|
|
47
49
|
localStorageAdapter: () => localStorageAdapter,
|
|
@@ -56,6 +58,26 @@ module.exports = __toCommonJS(src_exports);
|
|
|
56
58
|
// src/defaults.ts
|
|
57
59
|
var DEFAULT_NAME = "use-everywhere";
|
|
58
60
|
|
|
61
|
+
// src/dev.ts
|
|
62
|
+
var inDev = false;
|
|
63
|
+
try {
|
|
64
|
+
inDev = process.env.NODE_ENV !== "production";
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
var warned = /* @__PURE__ */ new Set();
|
|
68
|
+
var DOCS = "https://rxova.org/packages/use-everywhere/errors";
|
|
69
|
+
function diagnostic(code, message) {
|
|
70
|
+
return `[use-everywhere] ${code}: ${message}
|
|
71
|
+
\u2192 ${DOCS}/#${code.toLowerCase()}`;
|
|
72
|
+
}
|
|
73
|
+
function devWarn(code, message) {
|
|
74
|
+
if (!inDev) return;
|
|
75
|
+
const line = diagnostic(code, message);
|
|
76
|
+
if (warned.has(line)) return;
|
|
77
|
+
warned.add(line);
|
|
78
|
+
console.warn(line);
|
|
79
|
+
}
|
|
80
|
+
|
|
59
81
|
// src/debug.ts
|
|
60
82
|
var observers = /* @__PURE__ */ new Map();
|
|
61
83
|
function emitBusEvent(name, direction, wire) {
|
|
@@ -66,7 +88,7 @@ function emitBusEvent(name, direction, wire) {
|
|
|
66
88
|
try {
|
|
67
89
|
fn(event);
|
|
68
90
|
} catch (error) {
|
|
69
|
-
console.error(`
|
|
91
|
+
console.error(diagnostic("UE1012", `a bus observer for "${name}" threw`), error);
|
|
70
92
|
}
|
|
71
93
|
}
|
|
72
94
|
}
|
|
@@ -93,19 +115,6 @@ function enableDebug(options = {}) {
|
|
|
93
115
|
});
|
|
94
116
|
}
|
|
95
117
|
|
|
96
|
-
// src/dev.ts
|
|
97
|
-
var inDev = false;
|
|
98
|
-
try {
|
|
99
|
-
inDev = process.env.NODE_ENV !== "production";
|
|
100
|
-
} catch {
|
|
101
|
-
}
|
|
102
|
-
var warned = /* @__PURE__ */ new Set();
|
|
103
|
-
function devWarn(message) {
|
|
104
|
-
if (!inDev || warned.has(message)) return;
|
|
105
|
-
warned.add(message);
|
|
106
|
-
console.warn(message);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
118
|
// src/ids.ts
|
|
110
119
|
function randomHex(bytes) {
|
|
111
120
|
const crypto = globalThis.crypto;
|
|
@@ -117,7 +126,8 @@ function randomHex(bytes) {
|
|
|
117
126
|
}
|
|
118
127
|
if (process.env.NODE_ENV !== "production") {
|
|
119
128
|
devWarn(
|
|
120
|
-
"
|
|
129
|
+
"UE1006",
|
|
130
|
+
"crypto.getRandomValues is unavailable; falling back to Math.random for ids. Cross-origin window nonces are not cryptographically strong in this environment."
|
|
121
131
|
);
|
|
122
132
|
}
|
|
123
133
|
let out = "";
|
|
@@ -145,7 +155,8 @@ function announce() {
|
|
|
145
155
|
if (census.protocols.length > 1) {
|
|
146
156
|
if (process.env.NODE_ENV !== "production") {
|
|
147
157
|
devWarn(
|
|
148
|
-
|
|
158
|
+
"UE1008",
|
|
159
|
+
`two incompatible versions of this library are loaded on one page (rendezvous protocols ${census.protocols.join(", ")}). They will not share a client identity: expect one presence entry per version and no synchronous delivery between them. They still sync over the bus. Align the versions to fix it.`
|
|
149
160
|
);
|
|
150
161
|
}
|
|
151
162
|
}
|
|
@@ -218,7 +229,7 @@ var jsonSerializer = {
|
|
|
218
229
|
const type = lossyType(this[key], forJson);
|
|
219
230
|
if (type) {
|
|
220
231
|
throw new TypeError(
|
|
221
|
-
`use-everywhere: ${key ? `"${key}" is ` : ""}${type}, which JSON cannot round-trip. https://rxova.org/guides/serialization/`
|
|
232
|
+
`use-everywhere: ${key ? `"${key}" is ` : ""}${type}, which JSON cannot round-trip. https://rxova.org/packages/use-everywhere/guides/serialization/`
|
|
222
233
|
);
|
|
223
234
|
}
|
|
224
235
|
return forJson;
|
|
@@ -235,7 +246,10 @@ var StorageTransport = class {
|
|
|
235
246
|
this.seq = 0;
|
|
236
247
|
if (!storage) {
|
|
237
248
|
throw new Error(
|
|
238
|
-
|
|
249
|
+
diagnostic(
|
|
250
|
+
"UE1011",
|
|
251
|
+
"StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
|
|
252
|
+
)
|
|
239
253
|
);
|
|
240
254
|
}
|
|
241
255
|
this.serializer = serializer;
|
|
@@ -293,14 +307,16 @@ function defaultTransport(name) {
|
|
|
293
307
|
if (isStorageEventAvailable()) {
|
|
294
308
|
if (process.env.NODE_ENV !== "production") {
|
|
295
309
|
devWarn(
|
|
296
|
-
"
|
|
310
|
+
"UE1009",
|
|
311
|
+
"no BroadcastChannel; using the storage-event fallback. Values serialise as JSON, not structured clone \u2014 keep them JSON-shaped."
|
|
297
312
|
);
|
|
298
313
|
}
|
|
299
314
|
return new StorageTransport(name);
|
|
300
315
|
}
|
|
301
316
|
if (process.env.NODE_ENV !== "production") {
|
|
302
317
|
devWarn(
|
|
303
|
-
"
|
|
318
|
+
"UE1010",
|
|
319
|
+
"no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
304
320
|
);
|
|
305
321
|
}
|
|
306
322
|
return new NoopTransport();
|
|
@@ -330,7 +346,8 @@ function recordSkew(name, version) {
|
|
|
330
346
|
versions.add(version);
|
|
331
347
|
if (process.env.NODE_ENV !== "production") {
|
|
332
348
|
devWarn(
|
|
333
|
-
|
|
349
|
+
"UE1007",
|
|
350
|
+
`bus "${name}": a peer speaks wire protocol v${version}, this build speaks v${WIRE_VERSION} \u2014 a ${version > WIRE_VERSION ? "newer" : "older"} deploy. They cannot share state, presence, or a leader seat. https://rxova.org/packages/use-everywhere/under-the-hood/version-skew/`
|
|
334
351
|
);
|
|
335
352
|
}
|
|
336
353
|
}
|
|
@@ -462,15 +479,14 @@ function getBus(name, options = {}) {
|
|
|
462
479
|
if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
|
|
463
480
|
if (process.env.NODE_ENV !== "production") {
|
|
464
481
|
devWarn(
|
|
465
|
-
|
|
482
|
+
"UE1004",
|
|
483
|
+
`bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
|
|
466
484
|
);
|
|
467
485
|
}
|
|
468
486
|
}
|
|
469
487
|
if (options.kind !== void 0 && options.kind !== core.kind) {
|
|
470
488
|
if (process.env.NODE_ENV !== "production") {
|
|
471
|
-
devWarn(
|
|
472
|
-
`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`
|
|
473
|
-
);
|
|
489
|
+
devWarn("UE1005", `bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
|
|
474
490
|
}
|
|
475
491
|
}
|
|
476
492
|
}
|
|
@@ -499,7 +515,8 @@ function createGate(name, schemas, onInvalid) {
|
|
|
499
515
|
if (onInvalid) onInvalid(info);
|
|
500
516
|
else if (process.env.NODE_ENV !== "production") {
|
|
501
517
|
devWarn(
|
|
502
|
-
|
|
518
|
+
"UE1003",
|
|
519
|
+
`${name}/${key}: ${direction}bound payload rejected by its schema \u2014 ${result.issues.join("; ")}. https://rxova.org/packages/use-everywhere/guides/validating-payloads/`
|
|
503
520
|
);
|
|
504
521
|
}
|
|
505
522
|
return result.issues;
|
|
@@ -678,7 +695,8 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
678
695
|
if (live > 0) {
|
|
679
696
|
if (process.env.NODE_ENV !== "production") {
|
|
680
697
|
devWarn(
|
|
681
|
-
|
|
698
|
+
"UE1001",
|
|
699
|
+
`second shared store for "${name}" in this tab \u2014 they stay in sync, but you are paying twice for one store's state, subscriptions, and persistence writes. Reuse one per name.`
|
|
682
700
|
);
|
|
683
701
|
}
|
|
684
702
|
}
|
|
@@ -842,7 +860,8 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
842
860
|
if (onRestoreError) onRestoreError(error);
|
|
843
861
|
else if (process.env.NODE_ENV !== "production") {
|
|
844
862
|
devWarn(
|
|
845
|
-
|
|
863
|
+
"UE1002",
|
|
864
|
+
`${name}: persisted schema v${error.found} not restored, expected v${error.expected} (${error.reason}). https://rxova.org/packages/use-everywhere/guides/persistence/`
|
|
846
865
|
);
|
|
847
866
|
}
|
|
848
867
|
};
|
|
@@ -1873,6 +1892,42 @@ function createNamespace(namespace) {
|
|
|
1873
1892
|
createLeader: (name, options) => createLeader(busName(name), options)
|
|
1874
1893
|
};
|
|
1875
1894
|
}
|
|
1895
|
+
|
|
1896
|
+
// src/transport/shared-worker-transport.ts
|
|
1897
|
+
var SharedWorkerTransport = class {
|
|
1898
|
+
constructor(options) {
|
|
1899
|
+
this.kind = "shared-worker";
|
|
1900
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1901
|
+
this.closed = false;
|
|
1902
|
+
const name = options.name ?? "use-everywhere";
|
|
1903
|
+
const factory = options.factory ?? ((url, workerName) => new SharedWorker(url, { name: workerName }));
|
|
1904
|
+
const worker = factory(String(options.url), name);
|
|
1905
|
+
this.port = worker.port;
|
|
1906
|
+
this.onMessage = (event) => {
|
|
1907
|
+
for (const listener of this.listeners) listener(event.data);
|
|
1908
|
+
};
|
|
1909
|
+
this.port.addEventListener("message", this.onMessage);
|
|
1910
|
+
this.port.start?.();
|
|
1911
|
+
}
|
|
1912
|
+
post(data) {
|
|
1913
|
+
if (this.closed) return;
|
|
1914
|
+
this.port.postMessage(data);
|
|
1915
|
+
}
|
|
1916
|
+
subscribe(listener) {
|
|
1917
|
+
this.listeners.add(listener);
|
|
1918
|
+
return () => this.listeners.delete(listener);
|
|
1919
|
+
}
|
|
1920
|
+
close() {
|
|
1921
|
+
if (this.closed) return;
|
|
1922
|
+
this.closed = true;
|
|
1923
|
+
this.listeners.clear();
|
|
1924
|
+
this.port.removeEventListener("message", this.onMessage);
|
|
1925
|
+
this.port.close();
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
function isSharedWorkerAvailable() {
|
|
1929
|
+
return typeof SharedWorker !== "undefined";
|
|
1930
|
+
}
|
|
1876
1931
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1877
1932
|
0 && (module.exports = {
|
|
1878
1933
|
BroadcastChannelTransport,
|
|
@@ -1880,6 +1935,7 @@ function createNamespace(namespace) {
|
|
|
1880
1935
|
DEFAULT_NAME,
|
|
1881
1936
|
HandshakeTimeoutError,
|
|
1882
1937
|
NoopTransport,
|
|
1938
|
+
SharedWorkerTransport,
|
|
1883
1939
|
StorageTransport,
|
|
1884
1940
|
WIRE_VERSION,
|
|
1885
1941
|
WindowClosedError,
|
|
@@ -1897,6 +1953,7 @@ function createNamespace(namespace) {
|
|
|
1897
1953
|
getWireSkew,
|
|
1898
1954
|
indexedDbAdapter,
|
|
1899
1955
|
isBroadcastChannelAvailable,
|
|
1956
|
+
isSharedWorkerAvailable,
|
|
1900
1957
|
isStorageEventAvailable,
|
|
1901
1958
|
jsonSerializer,
|
|
1902
1959
|
localStorageAdapter,
|
package/dist/index.d.cts
CHANGED
|
@@ -349,7 +349,13 @@ interface LeaderOptions extends CommonOptions {
|
|
|
349
349
|
eligible?: boolean;
|
|
350
350
|
/** How to arbitrate the seat. Default 'auto'. */
|
|
351
351
|
strategy?: LeaderStrategy;
|
|
352
|
-
/**
|
|
352
|
+
/**
|
|
353
|
+
* The Web Locks manager to elect on. Defaults to `navigator.locks`.
|
|
354
|
+
*
|
|
355
|
+
* A test seam: `@use-everywhere/test-utils` passes a `FakeLockManager` here
|
|
356
|
+
* so several simulated tabs can queue on one seat — and so a crashed tab's
|
|
357
|
+
* lock is reclaimed — in a plain test process.
|
|
358
|
+
*/
|
|
353
359
|
locks?: LockManagerLike;
|
|
354
360
|
}
|
|
355
361
|
/** The slice of the Web Locks API this library uses. */
|
|
@@ -1084,6 +1090,98 @@ declare class StorageTransport implements Transport {
|
|
|
1084
1090
|
close(): void;
|
|
1085
1091
|
}
|
|
1086
1092
|
|
|
1093
|
+
/**
|
|
1094
|
+
* The subset of `SharedWorker` this transport uses. Narrower than the DOM type
|
|
1095
|
+
* on purpose: tests supply a fake, and a fake that has to implement
|
|
1096
|
+
* `EventTarget` to be accepted is a fake nobody writes.
|
|
1097
|
+
*/
|
|
1098
|
+
interface SharedWorkerLike {
|
|
1099
|
+
readonly port: MessagePortLike;
|
|
1100
|
+
}
|
|
1101
|
+
interface MessagePortLike {
|
|
1102
|
+
postMessage(data: unknown): void;
|
|
1103
|
+
start?: () => void;
|
|
1104
|
+
close(): void;
|
|
1105
|
+
addEventListener(type: 'message', listener: (event: {
|
|
1106
|
+
data: unknown;
|
|
1107
|
+
}) => void): void;
|
|
1108
|
+
removeEventListener(type: 'message', listener: (event: {
|
|
1109
|
+
data: unknown;
|
|
1110
|
+
}) => void): void;
|
|
1111
|
+
}
|
|
1112
|
+
interface SharedWorkerTransportOptions {
|
|
1113
|
+
/**
|
|
1114
|
+
* The relay script. A `URL` rather than a string in the common case, because
|
|
1115
|
+
* `new URL('./relay.js', import.meta.url)` is what survives a bundler.
|
|
1116
|
+
*
|
|
1117
|
+
* It must be a *stable* URL, and that is the whole reason this option exists
|
|
1118
|
+
* rather than the library inlining a worker from a Blob the way it could for
|
|
1119
|
+
* a dedicated one: a SharedWorker's identity is its script URL plus its name,
|
|
1120
|
+
* and every tab that builds its own Blob URL gets its own worker. Inlining
|
|
1121
|
+
* would produce N private workers that share nothing — the failure this
|
|
1122
|
+
* transport exists to prevent, wearing the costume of a fix.
|
|
1123
|
+
*/
|
|
1124
|
+
url: string | URL;
|
|
1125
|
+
/**
|
|
1126
|
+
* Distinguishes workers loaded from the same script. Defaults to the bus
|
|
1127
|
+
* name, which is what makes two buses on one origin independent.
|
|
1128
|
+
*/
|
|
1129
|
+
name?: string;
|
|
1130
|
+
/**
|
|
1131
|
+
* Constructs the worker. Only tests should pass this; the default is
|
|
1132
|
+
* `globalThis.SharedWorker`.
|
|
1133
|
+
*/
|
|
1134
|
+
factory?: (url: string, name: string) => SharedWorkerLike;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Cross-tab delivery through one SharedWorker per origin.
|
|
1138
|
+
*
|
|
1139
|
+
* BroadcastChannel is the better default for pure fan-out and stays the
|
|
1140
|
+
* default. This transport buys something else: a single place that is not a
|
|
1141
|
+
* tab. The relay outlives any individual tab, so the thing every "the leader
|
|
1142
|
+
* owns the socket" design actually wants — one connection, held somewhere no
|
|
1143
|
+
* user can close mid-flight — becomes possible without electing a leader at
|
|
1144
|
+
* all.
|
|
1145
|
+
*
|
|
1146
|
+
* What it does not buy: durability. The worker is torn down when the last port
|
|
1147
|
+
* closes, exactly like a BroadcastChannel with no listeners. State still lives
|
|
1148
|
+
* in the tabs; this moves the *wire*, not the source of truth.
|
|
1149
|
+
*
|
|
1150
|
+
* The relay script is three lines and ships with the library:
|
|
1151
|
+
*
|
|
1152
|
+
* ```js
|
|
1153
|
+
* // sw-relay.js — your app hosts this file
|
|
1154
|
+
* import 'use-everywhere/shared-worker';
|
|
1155
|
+
* ```
|
|
1156
|
+
*
|
|
1157
|
+
* ```ts
|
|
1158
|
+
* createSharedStore('cart', {
|
|
1159
|
+
* transport: new SharedWorkerTransport({
|
|
1160
|
+
* url: new URL('./sw-relay.js', import.meta.url),
|
|
1161
|
+
* }),
|
|
1162
|
+
* });
|
|
1163
|
+
* ```
|
|
1164
|
+
*/
|
|
1165
|
+
declare class SharedWorkerTransport implements Transport {
|
|
1166
|
+
readonly kind: TransportKind;
|
|
1167
|
+
private port;
|
|
1168
|
+
private listeners;
|
|
1169
|
+
private onMessage;
|
|
1170
|
+
private closed;
|
|
1171
|
+
constructor(options: SharedWorkerTransportOptions);
|
|
1172
|
+
post(data: unknown): void;
|
|
1173
|
+
subscribe(listener: (data: unknown) => void): () => void;
|
|
1174
|
+
close(): void;
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Whether this context can construct a SharedWorker at all.
|
|
1178
|
+
*
|
|
1179
|
+
* Notably false in every dedicated worker (they cannot nest a SharedWorker) and
|
|
1180
|
+
* in Chrome on Android. Worth checking before choosing this transport, since
|
|
1181
|
+
* the constructor throws rather than degrading.
|
|
1182
|
+
*/
|
|
1183
|
+
declare function isSharedWorkerAvailable(): boolean;
|
|
1184
|
+
|
|
1087
1185
|
declare function isBroadcastChannelAvailable(): boolean;
|
|
1088
1186
|
/**
|
|
1089
1187
|
* Can we hear other tabs through the `storage` event?
|
|
@@ -1105,4 +1203,4 @@ declare function isStorageEventAvailable(): boolean;
|
|
|
1105
1203
|
*/
|
|
1106
1204
|
declare function defaultTransport(name: string): Transport;
|
|
1107
1205
|
|
|
1108
|
-
export { type AskOptions, BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type ChannelOptions, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type IndexedDbAdapterOptions, type InvalidPayload, type Leader, type LeaderOptions, type LeaderSnapshot, type LeaderStrategy, type MessageEventLike, type MessageMap, type MessageMeta, type Namespace, NoopTransport, type OnInvalid, type OnOptions, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type PostOptions, type Presence, type PresenceOptions, type ReplyMap, type RestoreError, type SchemaMap, type SchemaOptions, type Serializer, type SharedReducer, type SharedReducerOptions, type SharedStore, type SharedStoreOptions, type StandardSchemaV1, type StorageLike, StorageTransport, Transport, TransportKind, type Version, WIRE_VERSION, type WebStorageAdapterOptions, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createNamespace, createPresence, createSharedReducer, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };
|
|
1206
|
+
export { type AskOptions, BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type ChannelOptions, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type IndexedDbAdapterOptions, type InvalidPayload, type Leader, type LeaderOptions, type LeaderSnapshot, type LeaderStrategy, type LockManagerLike, type MessageEventLike, type MessageMap, type MessageMeta, type MessagePortLike, type Namespace, NoopTransport, type OnInvalid, type OnOptions, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type PostOptions, type Presence, type PresenceOptions, type ReplyMap, type RestoreError, type SchemaMap, type SchemaOptions, type Serializer, type SharedReducer, type SharedReducerOptions, type SharedStore, type SharedStoreOptions, type SharedWorkerLike, SharedWorkerTransport, type SharedWorkerTransportOptions, type StandardSchemaV1, type StorageLike, StorageTransport, Transport, TransportKind, type Version, WIRE_VERSION, type WebStorageAdapterOptions, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createNamespace, createPresence, createSharedReducer, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isSharedWorkerAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };
|
package/dist/index.d.ts
CHANGED
|
@@ -349,7 +349,13 @@ interface LeaderOptions extends CommonOptions {
|
|
|
349
349
|
eligible?: boolean;
|
|
350
350
|
/** How to arbitrate the seat. Default 'auto'. */
|
|
351
351
|
strategy?: LeaderStrategy;
|
|
352
|
-
/**
|
|
352
|
+
/**
|
|
353
|
+
* The Web Locks manager to elect on. Defaults to `navigator.locks`.
|
|
354
|
+
*
|
|
355
|
+
* A test seam: `@use-everywhere/test-utils` passes a `FakeLockManager` here
|
|
356
|
+
* so several simulated tabs can queue on one seat — and so a crashed tab's
|
|
357
|
+
* lock is reclaimed — in a plain test process.
|
|
358
|
+
*/
|
|
353
359
|
locks?: LockManagerLike;
|
|
354
360
|
}
|
|
355
361
|
/** The slice of the Web Locks API this library uses. */
|
|
@@ -1084,6 +1090,98 @@ declare class StorageTransport implements Transport {
|
|
|
1084
1090
|
close(): void;
|
|
1085
1091
|
}
|
|
1086
1092
|
|
|
1093
|
+
/**
|
|
1094
|
+
* The subset of `SharedWorker` this transport uses. Narrower than the DOM type
|
|
1095
|
+
* on purpose: tests supply a fake, and a fake that has to implement
|
|
1096
|
+
* `EventTarget` to be accepted is a fake nobody writes.
|
|
1097
|
+
*/
|
|
1098
|
+
interface SharedWorkerLike {
|
|
1099
|
+
readonly port: MessagePortLike;
|
|
1100
|
+
}
|
|
1101
|
+
interface MessagePortLike {
|
|
1102
|
+
postMessage(data: unknown): void;
|
|
1103
|
+
start?: () => void;
|
|
1104
|
+
close(): void;
|
|
1105
|
+
addEventListener(type: 'message', listener: (event: {
|
|
1106
|
+
data: unknown;
|
|
1107
|
+
}) => void): void;
|
|
1108
|
+
removeEventListener(type: 'message', listener: (event: {
|
|
1109
|
+
data: unknown;
|
|
1110
|
+
}) => void): void;
|
|
1111
|
+
}
|
|
1112
|
+
interface SharedWorkerTransportOptions {
|
|
1113
|
+
/**
|
|
1114
|
+
* The relay script. A `URL` rather than a string in the common case, because
|
|
1115
|
+
* `new URL('./relay.js', import.meta.url)` is what survives a bundler.
|
|
1116
|
+
*
|
|
1117
|
+
* It must be a *stable* URL, and that is the whole reason this option exists
|
|
1118
|
+
* rather than the library inlining a worker from a Blob the way it could for
|
|
1119
|
+
* a dedicated one: a SharedWorker's identity is its script URL plus its name,
|
|
1120
|
+
* and every tab that builds its own Blob URL gets its own worker. Inlining
|
|
1121
|
+
* would produce N private workers that share nothing — the failure this
|
|
1122
|
+
* transport exists to prevent, wearing the costume of a fix.
|
|
1123
|
+
*/
|
|
1124
|
+
url: string | URL;
|
|
1125
|
+
/**
|
|
1126
|
+
* Distinguishes workers loaded from the same script. Defaults to the bus
|
|
1127
|
+
* name, which is what makes two buses on one origin independent.
|
|
1128
|
+
*/
|
|
1129
|
+
name?: string;
|
|
1130
|
+
/**
|
|
1131
|
+
* Constructs the worker. Only tests should pass this; the default is
|
|
1132
|
+
* `globalThis.SharedWorker`.
|
|
1133
|
+
*/
|
|
1134
|
+
factory?: (url: string, name: string) => SharedWorkerLike;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Cross-tab delivery through one SharedWorker per origin.
|
|
1138
|
+
*
|
|
1139
|
+
* BroadcastChannel is the better default for pure fan-out and stays the
|
|
1140
|
+
* default. This transport buys something else: a single place that is not a
|
|
1141
|
+
* tab. The relay outlives any individual tab, so the thing every "the leader
|
|
1142
|
+
* owns the socket" design actually wants — one connection, held somewhere no
|
|
1143
|
+
* user can close mid-flight — becomes possible without electing a leader at
|
|
1144
|
+
* all.
|
|
1145
|
+
*
|
|
1146
|
+
* What it does not buy: durability. The worker is torn down when the last port
|
|
1147
|
+
* closes, exactly like a BroadcastChannel with no listeners. State still lives
|
|
1148
|
+
* in the tabs; this moves the *wire*, not the source of truth.
|
|
1149
|
+
*
|
|
1150
|
+
* The relay script is three lines and ships with the library:
|
|
1151
|
+
*
|
|
1152
|
+
* ```js
|
|
1153
|
+
* // sw-relay.js — your app hosts this file
|
|
1154
|
+
* import 'use-everywhere/shared-worker';
|
|
1155
|
+
* ```
|
|
1156
|
+
*
|
|
1157
|
+
* ```ts
|
|
1158
|
+
* createSharedStore('cart', {
|
|
1159
|
+
* transport: new SharedWorkerTransport({
|
|
1160
|
+
* url: new URL('./sw-relay.js', import.meta.url),
|
|
1161
|
+
* }),
|
|
1162
|
+
* });
|
|
1163
|
+
* ```
|
|
1164
|
+
*/
|
|
1165
|
+
declare class SharedWorkerTransport implements Transport {
|
|
1166
|
+
readonly kind: TransportKind;
|
|
1167
|
+
private port;
|
|
1168
|
+
private listeners;
|
|
1169
|
+
private onMessage;
|
|
1170
|
+
private closed;
|
|
1171
|
+
constructor(options: SharedWorkerTransportOptions);
|
|
1172
|
+
post(data: unknown): void;
|
|
1173
|
+
subscribe(listener: (data: unknown) => void): () => void;
|
|
1174
|
+
close(): void;
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Whether this context can construct a SharedWorker at all.
|
|
1178
|
+
*
|
|
1179
|
+
* Notably false in every dedicated worker (they cannot nest a SharedWorker) and
|
|
1180
|
+
* in Chrome on Android. Worth checking before choosing this transport, since
|
|
1181
|
+
* the constructor throws rather than degrading.
|
|
1182
|
+
*/
|
|
1183
|
+
declare function isSharedWorkerAvailable(): boolean;
|
|
1184
|
+
|
|
1087
1185
|
declare function isBroadcastChannelAvailable(): boolean;
|
|
1088
1186
|
/**
|
|
1089
1187
|
* Can we hear other tabs through the `storage` event?
|
|
@@ -1105,4 +1203,4 @@ declare function isStorageEventAvailable(): boolean;
|
|
|
1105
1203
|
*/
|
|
1106
1204
|
declare function defaultTransport(name: string): Transport;
|
|
1107
1205
|
|
|
1108
|
-
export { type AskOptions, BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type ChannelOptions, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type IndexedDbAdapterOptions, type InvalidPayload, type Leader, type LeaderOptions, type LeaderSnapshot, type LeaderStrategy, type MessageEventLike, type MessageMap, type MessageMeta, type Namespace, NoopTransport, type OnInvalid, type OnOptions, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type PostOptions, type Presence, type PresenceOptions, type ReplyMap, type RestoreError, type SchemaMap, type SchemaOptions, type Serializer, type SharedReducer, type SharedReducerOptions, type SharedStore, type SharedStoreOptions, type StandardSchemaV1, type StorageLike, StorageTransport, Transport, TransportKind, type Version, WIRE_VERSION, type WebStorageAdapterOptions, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createNamespace, createPresence, createSharedReducer, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };
|
|
1206
|
+
export { type AskOptions, BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type ChannelOptions, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type IndexedDbAdapterOptions, type InvalidPayload, type Leader, type LeaderOptions, type LeaderSnapshot, type LeaderStrategy, type LockManagerLike, type MessageEventLike, type MessageMap, type MessageMeta, type MessagePortLike, type Namespace, NoopTransport, type OnInvalid, type OnOptions, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type PostOptions, type Presence, type PresenceOptions, type ReplyMap, type RestoreError, type SchemaMap, type SchemaOptions, type Serializer, type SharedReducer, type SharedReducerOptions, type SharedStore, type SharedStoreOptions, type SharedWorkerLike, SharedWorkerTransport, type SharedWorkerTransportOptions, type StandardSchemaV1, type StorageLike, StorageTransport, Transport, TransportKind, type Version, WIRE_VERSION, type WebStorageAdapterOptions, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createNamespace, createPresence, createSharedReducer, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isSharedWorkerAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
// src/defaults.ts
|
|
2
2
|
var DEFAULT_NAME = "use-everywhere";
|
|
3
3
|
|
|
4
|
+
// src/dev.ts
|
|
5
|
+
var inDev = false;
|
|
6
|
+
try {
|
|
7
|
+
inDev = process.env.NODE_ENV !== "production";
|
|
8
|
+
} catch {
|
|
9
|
+
}
|
|
10
|
+
var warned = /* @__PURE__ */ new Set();
|
|
11
|
+
var DOCS = "https://rxova.org/packages/use-everywhere/errors";
|
|
12
|
+
function diagnostic(code, message) {
|
|
13
|
+
return `[use-everywhere] ${code}: ${message}
|
|
14
|
+
\u2192 ${DOCS}/#${code.toLowerCase()}`;
|
|
15
|
+
}
|
|
16
|
+
function devWarn(code, message) {
|
|
17
|
+
if (!inDev) return;
|
|
18
|
+
const line = diagnostic(code, message);
|
|
19
|
+
if (warned.has(line)) return;
|
|
20
|
+
warned.add(line);
|
|
21
|
+
console.warn(line);
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
// src/debug.ts
|
|
5
25
|
var observers = /* @__PURE__ */ new Map();
|
|
6
26
|
function emitBusEvent(name, direction, wire) {
|
|
@@ -11,7 +31,7 @@ function emitBusEvent(name, direction, wire) {
|
|
|
11
31
|
try {
|
|
12
32
|
fn(event);
|
|
13
33
|
} catch (error) {
|
|
14
|
-
console.error(`
|
|
34
|
+
console.error(diagnostic("UE1012", `a bus observer for "${name}" threw`), error);
|
|
15
35
|
}
|
|
16
36
|
}
|
|
17
37
|
}
|
|
@@ -38,19 +58,6 @@ function enableDebug(options = {}) {
|
|
|
38
58
|
});
|
|
39
59
|
}
|
|
40
60
|
|
|
41
|
-
// src/dev.ts
|
|
42
|
-
var inDev = false;
|
|
43
|
-
try {
|
|
44
|
-
inDev = process.env.NODE_ENV !== "production";
|
|
45
|
-
} catch {
|
|
46
|
-
}
|
|
47
|
-
var warned = /* @__PURE__ */ new Set();
|
|
48
|
-
function devWarn(message) {
|
|
49
|
-
if (!inDev || warned.has(message)) return;
|
|
50
|
-
warned.add(message);
|
|
51
|
-
console.warn(message);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
61
|
// src/ids.ts
|
|
55
62
|
function randomHex(bytes) {
|
|
56
63
|
const crypto = globalThis.crypto;
|
|
@@ -62,7 +69,8 @@ function randomHex(bytes) {
|
|
|
62
69
|
}
|
|
63
70
|
if (process.env.NODE_ENV !== "production") {
|
|
64
71
|
devWarn(
|
|
65
|
-
"
|
|
72
|
+
"UE1006",
|
|
73
|
+
"crypto.getRandomValues is unavailable; falling back to Math.random for ids. Cross-origin window nonces are not cryptographically strong in this environment."
|
|
66
74
|
);
|
|
67
75
|
}
|
|
68
76
|
let out = "";
|
|
@@ -90,7 +98,8 @@ function announce() {
|
|
|
90
98
|
if (census.protocols.length > 1) {
|
|
91
99
|
if (process.env.NODE_ENV !== "production") {
|
|
92
100
|
devWarn(
|
|
93
|
-
|
|
101
|
+
"UE1008",
|
|
102
|
+
`two incompatible versions of this library are loaded on one page (rendezvous protocols ${census.protocols.join(", ")}). They will not share a client identity: expect one presence entry per version and no synchronous delivery between them. They still sync over the bus. Align the versions to fix it.`
|
|
94
103
|
);
|
|
95
104
|
}
|
|
96
105
|
}
|
|
@@ -163,7 +172,7 @@ var jsonSerializer = {
|
|
|
163
172
|
const type = lossyType(this[key], forJson);
|
|
164
173
|
if (type) {
|
|
165
174
|
throw new TypeError(
|
|
166
|
-
`use-everywhere: ${key ? `"${key}" is ` : ""}${type}, which JSON cannot round-trip. https://rxova.org/guides/serialization/`
|
|
175
|
+
`use-everywhere: ${key ? `"${key}" is ` : ""}${type}, which JSON cannot round-trip. https://rxova.org/packages/use-everywhere/guides/serialization/`
|
|
167
176
|
);
|
|
168
177
|
}
|
|
169
178
|
return forJson;
|
|
@@ -180,7 +189,10 @@ var StorageTransport = class {
|
|
|
180
189
|
this.seq = 0;
|
|
181
190
|
if (!storage) {
|
|
182
191
|
throw new Error(
|
|
183
|
-
|
|
192
|
+
diagnostic(
|
|
193
|
+
"UE1011",
|
|
194
|
+
"StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
|
|
195
|
+
)
|
|
184
196
|
);
|
|
185
197
|
}
|
|
186
198
|
this.serializer = serializer;
|
|
@@ -238,14 +250,16 @@ function defaultTransport(name) {
|
|
|
238
250
|
if (isStorageEventAvailable()) {
|
|
239
251
|
if (process.env.NODE_ENV !== "production") {
|
|
240
252
|
devWarn(
|
|
241
|
-
"
|
|
253
|
+
"UE1009",
|
|
254
|
+
"no BroadcastChannel; using the storage-event fallback. Values serialise as JSON, not structured clone \u2014 keep them JSON-shaped."
|
|
242
255
|
);
|
|
243
256
|
}
|
|
244
257
|
return new StorageTransport(name);
|
|
245
258
|
}
|
|
246
259
|
if (process.env.NODE_ENV !== "production") {
|
|
247
260
|
devWarn(
|
|
248
|
-
"
|
|
261
|
+
"UE1010",
|
|
262
|
+
"no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
249
263
|
);
|
|
250
264
|
}
|
|
251
265
|
return new NoopTransport();
|
|
@@ -275,7 +289,8 @@ function recordSkew(name, version) {
|
|
|
275
289
|
versions.add(version);
|
|
276
290
|
if (process.env.NODE_ENV !== "production") {
|
|
277
291
|
devWarn(
|
|
278
|
-
|
|
292
|
+
"UE1007",
|
|
293
|
+
`bus "${name}": a peer speaks wire protocol v${version}, this build speaks v${WIRE_VERSION} \u2014 a ${version > WIRE_VERSION ? "newer" : "older"} deploy. They cannot share state, presence, or a leader seat. https://rxova.org/packages/use-everywhere/under-the-hood/version-skew/`
|
|
279
294
|
);
|
|
280
295
|
}
|
|
281
296
|
}
|
|
@@ -407,15 +422,14 @@ function getBus(name, options = {}) {
|
|
|
407
422
|
if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
|
|
408
423
|
if (process.env.NODE_ENV !== "production") {
|
|
409
424
|
devWarn(
|
|
410
|
-
|
|
425
|
+
"UE1004",
|
|
426
|
+
`bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
|
|
411
427
|
);
|
|
412
428
|
}
|
|
413
429
|
}
|
|
414
430
|
if (options.kind !== void 0 && options.kind !== core.kind) {
|
|
415
431
|
if (process.env.NODE_ENV !== "production") {
|
|
416
|
-
devWarn(
|
|
417
|
-
`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`
|
|
418
|
-
);
|
|
432
|
+
devWarn("UE1005", `bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
|
|
419
433
|
}
|
|
420
434
|
}
|
|
421
435
|
}
|
|
@@ -444,7 +458,8 @@ function createGate(name, schemas, onInvalid) {
|
|
|
444
458
|
if (onInvalid) onInvalid(info);
|
|
445
459
|
else if (process.env.NODE_ENV !== "production") {
|
|
446
460
|
devWarn(
|
|
447
|
-
|
|
461
|
+
"UE1003",
|
|
462
|
+
`${name}/${key}: ${direction}bound payload rejected by its schema \u2014 ${result.issues.join("; ")}. https://rxova.org/packages/use-everywhere/guides/validating-payloads/`
|
|
448
463
|
);
|
|
449
464
|
}
|
|
450
465
|
return result.issues;
|
|
@@ -623,7 +638,8 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
623
638
|
if (live > 0) {
|
|
624
639
|
if (process.env.NODE_ENV !== "production") {
|
|
625
640
|
devWarn(
|
|
626
|
-
|
|
641
|
+
"UE1001",
|
|
642
|
+
`second shared store for "${name}" in this tab \u2014 they stay in sync, but you are paying twice for one store's state, subscriptions, and persistence writes. Reuse one per name.`
|
|
627
643
|
);
|
|
628
644
|
}
|
|
629
645
|
}
|
|
@@ -787,7 +803,8 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
787
803
|
if (onRestoreError) onRestoreError(error);
|
|
788
804
|
else if (process.env.NODE_ENV !== "production") {
|
|
789
805
|
devWarn(
|
|
790
|
-
|
|
806
|
+
"UE1002",
|
|
807
|
+
`${name}: persisted schema v${error.found} not restored, expected v${error.expected} (${error.reason}). https://rxova.org/packages/use-everywhere/guides/persistence/`
|
|
791
808
|
);
|
|
792
809
|
}
|
|
793
810
|
};
|
|
@@ -1818,12 +1835,49 @@ function createNamespace(namespace) {
|
|
|
1818
1835
|
createLeader: (name, options) => createLeader(busName(name), options)
|
|
1819
1836
|
};
|
|
1820
1837
|
}
|
|
1838
|
+
|
|
1839
|
+
// src/transport/shared-worker-transport.ts
|
|
1840
|
+
var SharedWorkerTransport = class {
|
|
1841
|
+
constructor(options) {
|
|
1842
|
+
this.kind = "shared-worker";
|
|
1843
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1844
|
+
this.closed = false;
|
|
1845
|
+
const name = options.name ?? "use-everywhere";
|
|
1846
|
+
const factory = options.factory ?? ((url, workerName) => new SharedWorker(url, { name: workerName }));
|
|
1847
|
+
const worker = factory(String(options.url), name);
|
|
1848
|
+
this.port = worker.port;
|
|
1849
|
+
this.onMessage = (event) => {
|
|
1850
|
+
for (const listener of this.listeners) listener(event.data);
|
|
1851
|
+
};
|
|
1852
|
+
this.port.addEventListener("message", this.onMessage);
|
|
1853
|
+
this.port.start?.();
|
|
1854
|
+
}
|
|
1855
|
+
post(data) {
|
|
1856
|
+
if (this.closed) return;
|
|
1857
|
+
this.port.postMessage(data);
|
|
1858
|
+
}
|
|
1859
|
+
subscribe(listener) {
|
|
1860
|
+
this.listeners.add(listener);
|
|
1861
|
+
return () => this.listeners.delete(listener);
|
|
1862
|
+
}
|
|
1863
|
+
close() {
|
|
1864
|
+
if (this.closed) return;
|
|
1865
|
+
this.closed = true;
|
|
1866
|
+
this.listeners.clear();
|
|
1867
|
+
this.port.removeEventListener("message", this.onMessage);
|
|
1868
|
+
this.port.close();
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
function isSharedWorkerAvailable() {
|
|
1872
|
+
return typeof SharedWorker !== "undefined";
|
|
1873
|
+
}
|
|
1821
1874
|
export {
|
|
1822
1875
|
BroadcastChannelTransport,
|
|
1823
1876
|
CID_PARAM,
|
|
1824
1877
|
DEFAULT_NAME,
|
|
1825
1878
|
HandshakeTimeoutError,
|
|
1826
1879
|
NoopTransport,
|
|
1880
|
+
SharedWorkerTransport,
|
|
1827
1881
|
StorageTransport,
|
|
1828
1882
|
WIRE_VERSION,
|
|
1829
1883
|
WindowClosedError,
|
|
@@ -1841,6 +1895,7 @@ export {
|
|
|
1841
1895
|
getWireSkew,
|
|
1842
1896
|
indexedDbAdapter,
|
|
1843
1897
|
isBroadcastChannelAvailable,
|
|
1898
|
+
isSharedWorkerAvailable,
|
|
1844
1899
|
isStorageEventAvailable,
|
|
1845
1900
|
jsonSerializer,
|
|
1846
1901
|
localStorageAdapter,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/shared-worker.ts
|
|
21
|
+
var shared_worker_exports = {};
|
|
22
|
+
__export(shared_worker_exports, {
|
|
23
|
+
startRelay: () => startRelay
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(shared_worker_exports);
|
|
26
|
+
function startRelay(scope) {
|
|
27
|
+
const ports = /* @__PURE__ */ new Set();
|
|
28
|
+
scope.onconnect = (event) => {
|
|
29
|
+
const port = event.ports[0];
|
|
30
|
+
if (!port) return;
|
|
31
|
+
ports.add(port);
|
|
32
|
+
port.addEventListener("message", (message) => {
|
|
33
|
+
const dead = [];
|
|
34
|
+
for (const peer of ports) {
|
|
35
|
+
if (peer === port) continue;
|
|
36
|
+
try {
|
|
37
|
+
peer.postMessage(message.data);
|
|
38
|
+
} catch {
|
|
39
|
+
dead.push(peer);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const corpse of dead) ports.delete(corpse);
|
|
43
|
+
});
|
|
44
|
+
port.start?.();
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (typeof self !== "undefined" && "onconnect" in self) {
|
|
48
|
+
startRelay(self);
|
|
49
|
+
}
|
|
50
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
51
|
+
0 && (module.exports = {
|
|
52
|
+
startRelay
|
|
53
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The relay that runs *inside* a SharedWorker. Host a one-line module and point
|
|
3
|
+
* `SharedWorkerTransport` at it:
|
|
4
|
+
*
|
|
5
|
+
* ```js
|
|
6
|
+
* // sw-relay.js
|
|
7
|
+
* import '@use-everywhere/core/shared-worker';
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Importing this module installs the `connect` handler, which is why the entry
|
|
11
|
+
* point is a side effect rather than a function you call: a SharedWorker script
|
|
12
|
+
* has one job, and making the caller remember to invoke it adds a way to get a
|
|
13
|
+
* worker that accepts connections and forwards nothing.
|
|
14
|
+
*
|
|
15
|
+
* `startRelay` is exported anyway, for a worker that also does other work — an
|
|
16
|
+
* app whose relay owns the WebSocket, say — and for tests, which cannot install
|
|
17
|
+
* a global `onconnect`.
|
|
18
|
+
*/
|
|
19
|
+
interface RelayPort {
|
|
20
|
+
postMessage(data: unknown): void;
|
|
21
|
+
start?: () => void;
|
|
22
|
+
addEventListener(type: 'message', listener: (event: {
|
|
23
|
+
data: unknown;
|
|
24
|
+
}) => void): void;
|
|
25
|
+
}
|
|
26
|
+
interface RelayScope {
|
|
27
|
+
onconnect: ((event: {
|
|
28
|
+
ports: readonly RelayPort[];
|
|
29
|
+
}) => void) | null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fan every message out to the other connected ports.
|
|
33
|
+
*
|
|
34
|
+
* Two properties the buses above this depend on:
|
|
35
|
+
*
|
|
36
|
+
* 1. **No self-echo.** A sender never receives its own message, matching
|
|
37
|
+
* BroadcastChannel exactly. Without it every store would apply its own
|
|
38
|
+
* writes twice and every presence roster would count each tab as two.
|
|
39
|
+
* 2. **A dead port is dropped, not thrown over.** A tab that goes away without
|
|
40
|
+
* closing its port leaves an entangled port whose `postMessage` throws; one
|
|
41
|
+
* of those must not stop delivery to the tabs still listening, so the send
|
|
42
|
+
* loop is individually guarded and the corpse is pruned.
|
|
43
|
+
*/
|
|
44
|
+
declare function startRelay(scope: RelayScope): void;
|
|
45
|
+
|
|
46
|
+
export { type RelayPort, type RelayScope, startRelay };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The relay that runs *inside* a SharedWorker. Host a one-line module and point
|
|
3
|
+
* `SharedWorkerTransport` at it:
|
|
4
|
+
*
|
|
5
|
+
* ```js
|
|
6
|
+
* // sw-relay.js
|
|
7
|
+
* import '@use-everywhere/core/shared-worker';
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Importing this module installs the `connect` handler, which is why the entry
|
|
11
|
+
* point is a side effect rather than a function you call: a SharedWorker script
|
|
12
|
+
* has one job, and making the caller remember to invoke it adds a way to get a
|
|
13
|
+
* worker that accepts connections and forwards nothing.
|
|
14
|
+
*
|
|
15
|
+
* `startRelay` is exported anyway, for a worker that also does other work — an
|
|
16
|
+
* app whose relay owns the WebSocket, say — and for tests, which cannot install
|
|
17
|
+
* a global `onconnect`.
|
|
18
|
+
*/
|
|
19
|
+
interface RelayPort {
|
|
20
|
+
postMessage(data: unknown): void;
|
|
21
|
+
start?: () => void;
|
|
22
|
+
addEventListener(type: 'message', listener: (event: {
|
|
23
|
+
data: unknown;
|
|
24
|
+
}) => void): void;
|
|
25
|
+
}
|
|
26
|
+
interface RelayScope {
|
|
27
|
+
onconnect: ((event: {
|
|
28
|
+
ports: readonly RelayPort[];
|
|
29
|
+
}) => void) | null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fan every message out to the other connected ports.
|
|
33
|
+
*
|
|
34
|
+
* Two properties the buses above this depend on:
|
|
35
|
+
*
|
|
36
|
+
* 1. **No self-echo.** A sender never receives its own message, matching
|
|
37
|
+
* BroadcastChannel exactly. Without it every store would apply its own
|
|
38
|
+
* writes twice and every presence roster would count each tab as two.
|
|
39
|
+
* 2. **A dead port is dropped, not thrown over.** A tab that goes away without
|
|
40
|
+
* closing its port leaves an entangled port whose `postMessage` throws; one
|
|
41
|
+
* of those must not stop delivery to the tabs still listening, so the send
|
|
42
|
+
* loop is individually guarded and the corpse is pruned.
|
|
43
|
+
*/
|
|
44
|
+
declare function startRelay(scope: RelayScope): void;
|
|
45
|
+
|
|
46
|
+
export { type RelayPort, type RelayScope, startRelay };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/shared-worker.ts
|
|
2
|
+
function startRelay(scope) {
|
|
3
|
+
const ports = /* @__PURE__ */ new Set();
|
|
4
|
+
scope.onconnect = (event) => {
|
|
5
|
+
const port = event.ports[0];
|
|
6
|
+
if (!port) return;
|
|
7
|
+
ports.add(port);
|
|
8
|
+
port.addEventListener("message", (message) => {
|
|
9
|
+
const dead = [];
|
|
10
|
+
for (const peer of ports) {
|
|
11
|
+
if (peer === port) continue;
|
|
12
|
+
try {
|
|
13
|
+
peer.postMessage(message.data);
|
|
14
|
+
} catch {
|
|
15
|
+
dead.push(peer);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const corpse of dead) ports.delete(corpse);
|
|
19
|
+
});
|
|
20
|
+
port.start?.();
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (typeof self !== "undefined" && "onconnect" in self) {
|
|
24
|
+
startRelay(self);
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
startRelay
|
|
28
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@use-everywhere/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Cross-tab shared state, events, presence, and cross-origin window channels",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonatan Kruszewski <jonakrusze@gmail.com>",
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
26
|
"type": "module",
|
|
27
|
-
"sideEffects":
|
|
27
|
+
"sideEffects": [
|
|
28
|
+
"./dist/shared-worker.js",
|
|
29
|
+
"./dist/shared-worker.cjs"
|
|
30
|
+
],
|
|
28
31
|
"main": "./dist/index.cjs",
|
|
29
32
|
"module": "./dist/index.js",
|
|
30
33
|
"types": "./dist/index.d.ts",
|
|
@@ -39,6 +42,16 @@
|
|
|
39
42
|
"default": "./dist/index.cjs"
|
|
40
43
|
}
|
|
41
44
|
},
|
|
45
|
+
"./shared-worker": {
|
|
46
|
+
"import": {
|
|
47
|
+
"types": "./dist/shared-worker.d.ts",
|
|
48
|
+
"default": "./dist/shared-worker.js"
|
|
49
|
+
},
|
|
50
|
+
"require": {
|
|
51
|
+
"types": "./dist/shared-worker.d.cts",
|
|
52
|
+
"default": "./dist/shared-worker.cjs"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
42
55
|
"./testing": {
|
|
43
56
|
"import": {
|
|
44
57
|
"types": "./dist/testing.d.ts",
|
|
@@ -120,6 +133,18 @@
|
|
|
120
133
|
"import": "{ indexedDbAdapter }",
|
|
121
134
|
"limit": "447 B"
|
|
122
135
|
},
|
|
136
|
+
{
|
|
137
|
+
"name": "SharedWorkerTransport",
|
|
138
|
+
"path": "dist/index.js",
|
|
139
|
+
"import": "{ SharedWorkerTransport }",
|
|
140
|
+
"limit": "380 B"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "SharedWorker relay (shared-worker subpath)",
|
|
144
|
+
"path": "dist/shared-worker.js",
|
|
145
|
+
"import": "*",
|
|
146
|
+
"limit": "280 B"
|
|
147
|
+
},
|
|
123
148
|
{
|
|
124
149
|
"name": "MemoryHub (testing subpath)",
|
|
125
150
|
"path": "dist/testing.js",
|
|
@@ -145,6 +170,9 @@
|
|
|
145
170
|
},
|
|
146
171
|
"typesVersions": {
|
|
147
172
|
"*": {
|
|
173
|
+
"shared-worker": [
|
|
174
|
+
"./dist/shared-worker.d.ts"
|
|
175
|
+
],
|
|
148
176
|
"testing": [
|
|
149
177
|
"./dist/testing.d.ts"
|
|
150
178
|
]
|