@use-everywhere/core 0.7.0 → 0.9.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 +709 -157
- package/dist/index.d.cts +664 -74
- package/dist/index.d.ts +664 -74
- package/dist/index.js +703 -157
- package/package.json +29 -12
package/dist/index.cjs
CHANGED
|
@@ -26,18 +26,24 @@ __export(src_exports, {
|
|
|
26
26
|
HandshakeTimeoutError: () => HandshakeTimeoutError,
|
|
27
27
|
NoopTransport: () => NoopTransport,
|
|
28
28
|
StorageTransport: () => StorageTransport,
|
|
29
|
+
WIRE_VERSION: () => WIRE_VERSION,
|
|
29
30
|
WindowClosedError: () => WindowClosedError,
|
|
30
31
|
connectToOpener: () => connectToOpener,
|
|
31
32
|
createChannel: () => createChannel,
|
|
32
33
|
createLeader: () => createLeader,
|
|
34
|
+
createNamespace: () => createNamespace,
|
|
33
35
|
createPresence: () => createPresence,
|
|
36
|
+
createSharedReducer: () => createSharedReducer,
|
|
34
37
|
createSharedStore: () => createSharedStore,
|
|
35
38
|
defaultTransport: () => defaultTransport,
|
|
36
39
|
enableDebug: () => enableDebug,
|
|
37
40
|
getBusNames: () => getBusNames,
|
|
38
41
|
getTransportKind: () => getTransportKind,
|
|
42
|
+
getWireSkew: () => getWireSkew,
|
|
43
|
+
indexedDbAdapter: () => indexedDbAdapter,
|
|
39
44
|
isBroadcastChannelAvailable: () => isBroadcastChannelAvailable,
|
|
40
45
|
isStorageEventAvailable: () => isStorageEventAvailable,
|
|
46
|
+
jsonSerializer: () => jsonSerializer,
|
|
41
47
|
localStorageAdapter: () => localStorageAdapter,
|
|
42
48
|
newer: () => newer,
|
|
43
49
|
observeBus: () => observeBus,
|
|
@@ -50,6 +56,26 @@ module.exports = __toCommonJS(src_exports);
|
|
|
50
56
|
// src/defaults.ts
|
|
51
57
|
var DEFAULT_NAME = "use-everywhere";
|
|
52
58
|
|
|
59
|
+
// src/dev.ts
|
|
60
|
+
var inDev = false;
|
|
61
|
+
try {
|
|
62
|
+
inDev = process.env.NODE_ENV !== "production";
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
var warned = /* @__PURE__ */ new Set();
|
|
66
|
+
var DOCS = "https://rxova.org/packages/use-everywhere/errors";
|
|
67
|
+
function diagnostic(code, message) {
|
|
68
|
+
return `[use-everywhere] ${code}: ${message}
|
|
69
|
+
\u2192 ${DOCS}/#${code.toLowerCase()}`;
|
|
70
|
+
}
|
|
71
|
+
function devWarn(code, message) {
|
|
72
|
+
if (!inDev) return;
|
|
73
|
+
const line = diagnostic(code, message);
|
|
74
|
+
if (warned.has(line)) return;
|
|
75
|
+
warned.add(line);
|
|
76
|
+
console.warn(line);
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
// src/debug.ts
|
|
54
80
|
var observers = /* @__PURE__ */ new Map();
|
|
55
81
|
function emitBusEvent(name, direction, wire) {
|
|
@@ -60,7 +86,7 @@ function emitBusEvent(name, direction, wire) {
|
|
|
60
86
|
try {
|
|
61
87
|
fn(event);
|
|
62
88
|
} catch (error) {
|
|
63
|
-
console.error(`
|
|
89
|
+
console.error(diagnostic("UE1012", `a bus observer for "${name}" threw`), error);
|
|
64
90
|
}
|
|
65
91
|
}
|
|
66
92
|
}
|
|
@@ -87,19 +113,6 @@ function enableDebug(options = {}) {
|
|
|
87
113
|
});
|
|
88
114
|
}
|
|
89
115
|
|
|
90
|
-
// src/dev.ts
|
|
91
|
-
var inDev = false;
|
|
92
|
-
try {
|
|
93
|
-
inDev = process.env.NODE_ENV !== "production";
|
|
94
|
-
} catch {
|
|
95
|
-
}
|
|
96
|
-
var warned = /* @__PURE__ */ new Set();
|
|
97
|
-
function devWarn(message) {
|
|
98
|
-
if (!inDev || warned.has(message)) return;
|
|
99
|
-
warned.add(message);
|
|
100
|
-
console.warn(message);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
116
|
// src/ids.ts
|
|
104
117
|
function randomHex(bytes) {
|
|
105
118
|
const crypto = globalThis.crypto;
|
|
@@ -109,9 +122,12 @@ function randomHex(bytes) {
|
|
|
109
122
|
for (const b of buf) out2 += b.toString(16).padStart(2, "0");
|
|
110
123
|
return out2;
|
|
111
124
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
if (process.env.NODE_ENV !== "production") {
|
|
126
|
+
devWarn(
|
|
127
|
+
"UE1006",
|
|
128
|
+
"crypto.getRandomValues is unavailable; falling back to Math.random for ids. Cross-origin window nonces are not cryptographically strong in this environment."
|
|
129
|
+
);
|
|
130
|
+
}
|
|
115
131
|
let out = "";
|
|
116
132
|
while (out.length < bytes * 2)
|
|
117
133
|
out += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
|
|
@@ -128,15 +144,19 @@ function newMsgId() {
|
|
|
128
144
|
var PROTOCOL = 1;
|
|
129
145
|
var TABLE = /* @__PURE__ */ Symbol.for(`use-everywhere.rendezvous.${PROTOCOL}`);
|
|
130
146
|
var CENSUS = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.census");
|
|
147
|
+
var SKEW = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.skew");
|
|
131
148
|
function announce() {
|
|
132
149
|
const g = globalThis;
|
|
133
150
|
const census = g[CENSUS] ?? (g[CENSUS] = { protocols: [] });
|
|
134
151
|
if (census.protocols.includes(PROTOCOL)) return;
|
|
135
152
|
census.protocols.push(PROTOCOL);
|
|
136
153
|
if (census.protocols.length > 1) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
154
|
+
if (process.env.NODE_ENV !== "production") {
|
|
155
|
+
devWarn(
|
|
156
|
+
"UE1008",
|
|
157
|
+
`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.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
140
160
|
}
|
|
141
161
|
}
|
|
142
162
|
function busTable() {
|
|
@@ -144,6 +164,10 @@ function busTable() {
|
|
|
144
164
|
const g = globalThis;
|
|
145
165
|
return g[TABLE] ?? (g[TABLE] = /* @__PURE__ */ new Map());
|
|
146
166
|
}
|
|
167
|
+
function skewLedger() {
|
|
168
|
+
const g = globalThis;
|
|
169
|
+
return g[SKEW] ?? (g[SKEW] = /* @__PURE__ */ new Map());
|
|
170
|
+
}
|
|
147
171
|
|
|
148
172
|
// src/transport/broadcast-channel-transport.ts
|
|
149
173
|
var BroadcastChannelTransport = class {
|
|
@@ -183,24 +207,57 @@ var NoopTransport = class {
|
|
|
183
207
|
}
|
|
184
208
|
};
|
|
185
209
|
|
|
210
|
+
// src/serializer.ts
|
|
211
|
+
function lossyType(raw, value) {
|
|
212
|
+
if (value === void 0) return raw === void 0 ? "undefined" : "undefined after toJSON";
|
|
213
|
+
if (raw instanceof Date) return "Date";
|
|
214
|
+
if (raw instanceof Map) return "Map";
|
|
215
|
+
if (raw instanceof Set) return "Set";
|
|
216
|
+
if (raw instanceof RegExp) return "RegExp";
|
|
217
|
+
if (ArrayBuffer.isView(raw)) return "TypedArray";
|
|
218
|
+
const type = typeof raw;
|
|
219
|
+
return type === "function" || type === "symbol" ? type : void 0;
|
|
220
|
+
}
|
|
221
|
+
var jsonSerializer = {
|
|
222
|
+
stringify(value) {
|
|
223
|
+
if (value === void 0) {
|
|
224
|
+
throw new TypeError("use-everywhere: cannot serialize undefined");
|
|
225
|
+
}
|
|
226
|
+
return JSON.stringify(value, function replacer(key, forJson) {
|
|
227
|
+
const type = lossyType(this[key], forJson);
|
|
228
|
+
if (type) {
|
|
229
|
+
throw new TypeError(
|
|
230
|
+
`use-everywhere: ${key ? `"${key}" is ` : ""}${type}, which JSON cannot round-trip. https://rxova.org/packages/use-everywhere/guides/serialization/`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return forJson;
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
parse: (text) => JSON.parse(text)
|
|
237
|
+
};
|
|
238
|
+
|
|
186
239
|
// src/transport/storage-transport.ts
|
|
187
240
|
var StorageTransport = class {
|
|
188
|
-
constructor(name, storage = globalThis.localStorage) {
|
|
241
|
+
constructor(name, storage = globalThis.localStorage, serializer = jsonSerializer) {
|
|
189
242
|
this.kind = "storage";
|
|
190
243
|
this.listeners = /* @__PURE__ */ new Set();
|
|
191
244
|
this.seq = 0;
|
|
192
245
|
if (!storage) {
|
|
193
246
|
throw new Error(
|
|
194
|
-
|
|
247
|
+
diagnostic(
|
|
248
|
+
"UE1011",
|
|
249
|
+
"StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
|
|
250
|
+
)
|
|
195
251
|
);
|
|
196
252
|
}
|
|
253
|
+
this.serializer = serializer;
|
|
197
254
|
this.key = `use-everywhere:bus:${name}`;
|
|
198
255
|
this.storage = storage;
|
|
199
256
|
this.onStorage = (event) => {
|
|
200
257
|
if (event.key !== this.key || event.newValue === null) return;
|
|
201
258
|
let payload;
|
|
202
259
|
try {
|
|
203
|
-
payload =
|
|
260
|
+
payload = this.serializer.parse(event.newValue);
|
|
204
261
|
} catch {
|
|
205
262
|
return;
|
|
206
263
|
}
|
|
@@ -209,7 +266,7 @@ var StorageTransport = class {
|
|
|
209
266
|
addEventListener("storage", this.onStorage);
|
|
210
267
|
}
|
|
211
268
|
post(data) {
|
|
212
|
-
const envelope =
|
|
269
|
+
const envelope = this.serializer.stringify({ seq: this.seq++, data });
|
|
213
270
|
try {
|
|
214
271
|
this.storage.setItem(this.key, envelope);
|
|
215
272
|
this.storage.removeItem(this.key);
|
|
@@ -225,12 +282,6 @@ var StorageTransport = class {
|
|
|
225
282
|
removeEventListener("storage", this.onStorage);
|
|
226
283
|
}
|
|
227
284
|
};
|
|
228
|
-
function rejectUnrepresentable(_key, value) {
|
|
229
|
-
if (typeof value === "function" || typeof value === "symbol") {
|
|
230
|
-
throw new TypeError(`${typeof value} values cannot be shared`);
|
|
231
|
-
}
|
|
232
|
-
return value;
|
|
233
|
-
}
|
|
234
285
|
|
|
235
286
|
// src/transport/default-transport.ts
|
|
236
287
|
function isBroadcastChannelAvailable() {
|
|
@@ -252,27 +303,61 @@ function isStorageEventAvailable() {
|
|
|
252
303
|
function defaultTransport(name) {
|
|
253
304
|
if (isBroadcastChannelAvailable()) return new BroadcastChannelTransport(name);
|
|
254
305
|
if (isStorageEventAvailable()) {
|
|
306
|
+
if (process.env.NODE_ENV !== "production") {
|
|
307
|
+
devWarn(
|
|
308
|
+
"UE1009",
|
|
309
|
+
"no BroadcastChannel; using the storage-event fallback. Values serialise as JSON, not structured clone \u2014 keep them JSON-shaped."
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return new StorageTransport(name);
|
|
313
|
+
}
|
|
314
|
+
if (process.env.NODE_ENV !== "production") {
|
|
255
315
|
devWarn(
|
|
256
|
-
"
|
|
316
|
+
"UE1010",
|
|
317
|
+
"no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
257
318
|
);
|
|
258
|
-
return new StorageTransport(name);
|
|
259
319
|
}
|
|
260
|
-
devWarn(
|
|
261
|
-
"[use-everywhere] no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
262
|
-
);
|
|
263
320
|
return new NoopTransport();
|
|
264
321
|
}
|
|
265
322
|
|
|
266
|
-
// src/
|
|
323
|
+
// src/wire.ts
|
|
324
|
+
var WIRE_VERSION = 1;
|
|
325
|
+
function hasEnvelopeShape(wire) {
|
|
326
|
+
return typeof wire.scope === "string" && typeof wire.type === "string" && typeof wire.clientId === "string";
|
|
327
|
+
}
|
|
267
328
|
function isBusWire(data) {
|
|
268
329
|
if (typeof data !== "object" || data === null) return false;
|
|
269
330
|
const wire = data;
|
|
270
|
-
return wire.v ===
|
|
331
|
+
return wire.v === WIRE_VERSION && hasEnvelopeShape(wire);
|
|
271
332
|
}
|
|
333
|
+
function foreignWireVersion(data) {
|
|
334
|
+
if (typeof data !== "object" || data === null) return null;
|
|
335
|
+
const wire = data;
|
|
336
|
+
if (typeof wire.v !== "number" || wire.v === WIRE_VERSION) return null;
|
|
337
|
+
return hasEnvelopeShape(wire) ? wire.v : null;
|
|
338
|
+
}
|
|
339
|
+
function recordSkew(name, version) {
|
|
340
|
+
const seen = skewLedger();
|
|
341
|
+
const versions = seen.get(name) ?? /* @__PURE__ */ new Set();
|
|
342
|
+
seen.set(name, versions);
|
|
343
|
+
if (versions.has(version)) return;
|
|
344
|
+
versions.add(version);
|
|
345
|
+
if (process.env.NODE_ENV !== "production") {
|
|
346
|
+
devWarn(
|
|
347
|
+
"UE1007",
|
|
348
|
+
`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/`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function getWireSkew(name) {
|
|
353
|
+
return [...skewLedger().get(name) ?? []].sort((a, b) => a - b);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/bus.ts
|
|
272
357
|
function defaultKind() {
|
|
273
358
|
return typeof document === "undefined" ? "worker" : "tab";
|
|
274
359
|
}
|
|
275
|
-
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event"]);
|
|
360
|
+
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event", "op"]);
|
|
276
361
|
function createBusCore(name, options, onShutdown) {
|
|
277
362
|
const transport = (options.transport ?? defaultTransport)(name);
|
|
278
363
|
const clientId = newClientId();
|
|
@@ -293,6 +378,11 @@ function createBusCore(name, options, onShutdown) {
|
|
|
293
378
|
if (SHARED_WITHIN_CLIENT.has(wire.scope)) deliver(wire, from);
|
|
294
379
|
};
|
|
295
380
|
const unsubscribe = transport.subscribe((data) => {
|
|
381
|
+
const foreign = foreignWireVersion(data);
|
|
382
|
+
if (foreign !== null) {
|
|
383
|
+
recordSkew(name, foreign);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
296
386
|
if (!isBusWire(data)) return;
|
|
297
387
|
if (data.clientId === clientId) return;
|
|
298
388
|
emitBusEvent(name, "in", data);
|
|
@@ -385,57 +475,172 @@ function getBus(name, options = {}) {
|
|
|
385
475
|
table.set(name, core);
|
|
386
476
|
} else {
|
|
387
477
|
if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
478
|
+
if (process.env.NODE_ENV !== "production") {
|
|
479
|
+
devWarn(
|
|
480
|
+
"UE1004",
|
|
481
|
+
`bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
391
484
|
}
|
|
392
485
|
if (options.kind !== void 0 && options.kind !== core.kind) {
|
|
393
|
-
|
|
486
|
+
if (process.env.NODE_ENV !== "production") {
|
|
487
|
+
devWarn("UE1005", `bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
|
|
488
|
+
}
|
|
394
489
|
}
|
|
395
490
|
}
|
|
396
491
|
return core.connect();
|
|
397
492
|
}
|
|
398
493
|
|
|
494
|
+
// src/schema.ts
|
|
495
|
+
function validate(schema, payload) {
|
|
496
|
+
const props = schema["~standard"];
|
|
497
|
+
const result = props.validate(payload);
|
|
498
|
+
if (typeof result.then === "function") {
|
|
499
|
+
return { ok: false, issues: [`the "${props.vendor}" schema validates asynchronously`] };
|
|
500
|
+
}
|
|
501
|
+
const sync = result;
|
|
502
|
+
if (sync.issues) return { ok: false, issues: sync.issues.map((issue) => issue.message) };
|
|
503
|
+
return { ok: true, value: sync.value };
|
|
504
|
+
}
|
|
505
|
+
function createGate(name, schemas, onInvalid) {
|
|
506
|
+
if (!schemas) return void 0;
|
|
507
|
+
const check = (key, payload, direction) => {
|
|
508
|
+
const schema = schemas[key];
|
|
509
|
+
if (!schema) return void 0;
|
|
510
|
+
const result = validate(schema, payload);
|
|
511
|
+
if (result.ok) return void 0;
|
|
512
|
+
const info = { name, key, direction, payload, issues: result.issues };
|
|
513
|
+
if (onInvalid) onInvalid(info);
|
|
514
|
+
else if (process.env.NODE_ENV !== "production") {
|
|
515
|
+
devWarn(
|
|
516
|
+
"UE1003",
|
|
517
|
+
`${name}/${key}: ${direction}bound payload rejected by its schema \u2014 ${result.issues.join("; ")}. https://rxova.org/packages/use-everywhere/guides/validating-payloads/`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
return result.issues;
|
|
521
|
+
};
|
|
522
|
+
return {
|
|
523
|
+
accepts: (key, payload) => !check(key, payload, "in"),
|
|
524
|
+
assert(key, payload) {
|
|
525
|
+
const issues = check(key, payload, "out");
|
|
526
|
+
if (issues) {
|
|
527
|
+
throw new TypeError(
|
|
528
|
+
`use-everywhere: ${name}/${key} \u2014 the value does not match its schema, so nothing was sent or applied. ${issues.join("; ")}`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
399
535
|
// src/channel.ts
|
|
400
536
|
function createChannel(name, options = {}) {
|
|
401
537
|
const bus = getBus(name, options);
|
|
402
538
|
const handlers = /* @__PURE__ */ new Map();
|
|
539
|
+
const responders = /* @__PURE__ */ new Map();
|
|
540
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
541
|
+
const gate = createGate(name, options.schema, options.onInvalid);
|
|
542
|
+
const deliver = (type, payload, meta) => {
|
|
543
|
+
const set = handlers.get(type);
|
|
544
|
+
if (!set) return;
|
|
545
|
+
for (const fn of [...set]) fn(payload, meta);
|
|
546
|
+
};
|
|
403
547
|
const unsubscribe = bus.subscribe((wire) => {
|
|
404
548
|
if (wire.scope !== "event") return;
|
|
405
|
-
const set = handlers.get(wire.type);
|
|
406
|
-
if (!set) return;
|
|
407
549
|
const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
550
|
+
if (wire.replyTo !== void 0) {
|
|
551
|
+
const settle = waiting.get(wire.replyTo);
|
|
552
|
+
if (!settle) return;
|
|
553
|
+
waiting.delete(wire.replyTo);
|
|
554
|
+
settle(wire.payload);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const responder = responders.get(wire.type);
|
|
558
|
+
if (responder) {
|
|
415
559
|
bus.post({
|
|
416
560
|
v: 1,
|
|
417
561
|
scope: "event",
|
|
418
|
-
type,
|
|
419
|
-
payload,
|
|
562
|
+
type: wire.type,
|
|
563
|
+
payload: responder(wire.payload, meta),
|
|
420
564
|
clientId: bus.clientId,
|
|
421
565
|
kind: bus.kind,
|
|
422
|
-
msgId: newMsgId()
|
|
566
|
+
msgId: newMsgId(),
|
|
567
|
+
replyTo: wire.msgId
|
|
423
568
|
});
|
|
569
|
+
}
|
|
570
|
+
if (!handlers.has(wire.type)) return;
|
|
571
|
+
if (gate && !gate.accepts(wire.type, wire.payload)) return;
|
|
572
|
+
deliver(wire.type, wire.payload, meta);
|
|
573
|
+
});
|
|
574
|
+
let closed = false;
|
|
575
|
+
const send = (type, payload, msgId) => {
|
|
576
|
+
gate?.assert(type, payload);
|
|
577
|
+
bus.post({
|
|
578
|
+
v: 1,
|
|
579
|
+
scope: "event",
|
|
580
|
+
type,
|
|
581
|
+
payload,
|
|
582
|
+
clientId: bus.clientId,
|
|
583
|
+
kind: bus.kind,
|
|
584
|
+
msgId
|
|
585
|
+
});
|
|
586
|
+
};
|
|
587
|
+
return {
|
|
588
|
+
name,
|
|
589
|
+
clientId: bus.clientId,
|
|
590
|
+
post(type, payload, options2) {
|
|
591
|
+
send(type, payload, newMsgId());
|
|
592
|
+
if (options2?.echo) {
|
|
593
|
+
deliver(type, payload, { clientId: bus.clientId, kind: bus.kind, self: true });
|
|
594
|
+
}
|
|
424
595
|
},
|
|
425
|
-
on(type, handler) {
|
|
596
|
+
on(type, handler, options2) {
|
|
426
597
|
let set = handlers.get(type);
|
|
427
598
|
if (!set) {
|
|
428
599
|
set = /* @__PURE__ */ new Set();
|
|
429
600
|
handlers.set(type, set);
|
|
430
601
|
}
|
|
431
|
-
|
|
432
|
-
|
|
602
|
+
const entry = options2?.once ? (payload, meta) => {
|
|
603
|
+
set.delete(entry);
|
|
604
|
+
handler(payload, meta);
|
|
605
|
+
} : handler;
|
|
606
|
+
set.add(entry);
|
|
607
|
+
return () => set.delete(entry);
|
|
608
|
+
},
|
|
609
|
+
ask(type, payload, options2) {
|
|
610
|
+
const msgId = newMsgId();
|
|
611
|
+
return new Promise((resolve, reject) => {
|
|
612
|
+
const timer = setTimeout(() => {
|
|
613
|
+
waiting.delete(msgId);
|
|
614
|
+
reject(
|
|
615
|
+
new Error(
|
|
616
|
+
`use-everywhere: nobody answered "${type}" on "${name}" within ${String(options2?.timeoutMs ?? 5e3)}ms`
|
|
617
|
+
)
|
|
618
|
+
);
|
|
619
|
+
}, options2?.timeoutMs ?? 5e3);
|
|
620
|
+
waiting.set(msgId, (value) => {
|
|
621
|
+
clearTimeout(timer);
|
|
622
|
+
resolve(value);
|
|
623
|
+
});
|
|
624
|
+
try {
|
|
625
|
+
send(type, payload, msgId);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
clearTimeout(timer);
|
|
628
|
+
waiting.delete(msgId);
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
answer(type, responder) {
|
|
634
|
+
responders.set(type, responder);
|
|
635
|
+
return () => responders.delete(type);
|
|
433
636
|
},
|
|
434
637
|
close() {
|
|
435
638
|
if (closed) return;
|
|
436
639
|
closed = true;
|
|
437
640
|
unsubscribe();
|
|
438
641
|
handlers.clear();
|
|
642
|
+
responders.clear();
|
|
643
|
+
waiting.clear();
|
|
439
644
|
bus.release();
|
|
440
645
|
}
|
|
441
646
|
};
|
|
@@ -486,15 +691,19 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
486
691
|
if (onSharedBus) {
|
|
487
692
|
const live = liveStores.get(name) ?? 0;
|
|
488
693
|
if (live > 0) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
694
|
+
if (process.env.NODE_ENV !== "production") {
|
|
695
|
+
devWarn(
|
|
696
|
+
"UE1001",
|
|
697
|
+
`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.`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
492
700
|
}
|
|
493
701
|
liveStores.set(name, live + 1);
|
|
494
702
|
}
|
|
495
703
|
const bus = getBus(name, options);
|
|
496
704
|
const clientId = bus.clientId;
|
|
497
705
|
const accept = options.accept;
|
|
706
|
+
const gate = createGate(name, options.schema, options.onInvalid);
|
|
498
707
|
const state = { ...initial };
|
|
499
708
|
const versions = {};
|
|
500
709
|
for (const k in state) {
|
|
@@ -505,23 +714,61 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
505
714
|
let versionsSnapshot = Object.freeze({ ...versions });
|
|
506
715
|
const listeners = /* @__PURE__ */ new Set();
|
|
507
716
|
const keyListeners = /* @__PURE__ */ new Map();
|
|
508
|
-
|
|
717
|
+
let batchDepth = 0;
|
|
718
|
+
const batched = [];
|
|
719
|
+
const rebuild = () => {
|
|
509
720
|
snapshot = Object.freeze({ ...state });
|
|
510
721
|
versionsSnapshot = Object.freeze({ ...versions });
|
|
722
|
+
};
|
|
723
|
+
const emit = (key, value, meta) => {
|
|
511
724
|
for (const fn of listeners) fn(key, value, meta);
|
|
512
725
|
const set = keyListeners.get(key);
|
|
513
726
|
if (set) for (const fn of set) fn();
|
|
727
|
+
};
|
|
728
|
+
function notify(key, value, meta) {
|
|
729
|
+
if (batchDepth > 0) {
|
|
730
|
+
batched.push([key, value, meta]);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
rebuild();
|
|
734
|
+
emit(key, value, meta);
|
|
735
|
+
}
|
|
736
|
+
function batch(fn) {
|
|
737
|
+
batchDepth++;
|
|
738
|
+
try {
|
|
739
|
+
return fn();
|
|
740
|
+
} finally {
|
|
741
|
+
batchDepth--;
|
|
742
|
+
if (batchDepth === 0 && batched.length > 0) {
|
|
743
|
+
const changes = batched.splice(0, batched.length);
|
|
744
|
+
rebuild();
|
|
745
|
+
for (const [key, value, meta] of changes) emit(key, value, meta);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
514
748
|
}
|
|
515
749
|
function applyRemote(key, value, version, meta) {
|
|
516
750
|
if (!newer(version, versions[key])) return;
|
|
751
|
+
if (gate && !gate.accepts(key, value)) return;
|
|
517
752
|
versions[key] = version;
|
|
518
753
|
state[key] = freezeShared(value);
|
|
519
754
|
notify(key, value, meta);
|
|
520
755
|
}
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
756
|
+
const snapshotDelayMs = options.snapshotDelayMs ?? 40;
|
|
757
|
+
let snapshotTimer;
|
|
758
|
+
const written = () => Object.keys(versions).filter((k) => (versions[k]?.[0] ?? 0) > 0);
|
|
759
|
+
const coveredBy = (theirs) => written().every((key) => {
|
|
760
|
+
const mine = versions[key];
|
|
761
|
+
return mine !== void 0 && !newer(mine, theirs[key]);
|
|
762
|
+
});
|
|
763
|
+
const cancelSnapshot = () => {
|
|
764
|
+
clearTimeout(snapshotTimer);
|
|
765
|
+
snapshotTimer = void 0;
|
|
766
|
+
};
|
|
767
|
+
const scheduleSnapshot = () => {
|
|
768
|
+
if (written().length === 0) return;
|
|
769
|
+
if (snapshotTimer !== void 0) return;
|
|
770
|
+
snapshotTimer = setTimeout(() => {
|
|
771
|
+
snapshotTimer = void 0;
|
|
525
772
|
bus.post({
|
|
526
773
|
v: 1,
|
|
527
774
|
scope: "state",
|
|
@@ -531,22 +778,36 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
531
778
|
state: { ...state },
|
|
532
779
|
versions: { ...versions }
|
|
533
780
|
});
|
|
781
|
+
}, Math.random() * snapshotDelayMs);
|
|
782
|
+
};
|
|
783
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
784
|
+
if (wire.scope !== "state") return;
|
|
785
|
+
const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
|
|
786
|
+
if (wire.type === "hello") {
|
|
787
|
+
scheduleSnapshot();
|
|
534
788
|
return;
|
|
535
789
|
}
|
|
536
790
|
if (accept && !accept(meta)) return;
|
|
537
791
|
if (wire.type === "patch") {
|
|
538
792
|
if (typeof wire.key !== "string" || !isVersion(wire.version)) return;
|
|
539
793
|
applyRemote(wire.key, wire.value, wire.version, meta);
|
|
540
|
-
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (wire.type === "snapshot") {
|
|
541
797
|
const versions2 = wire.versions;
|
|
542
798
|
if (typeof versions2 !== "object" || versions2 === null) return;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
799
|
+
if (coveredBy(versions2)) cancelSnapshot();
|
|
800
|
+
batch(() => {
|
|
801
|
+
for (const k in wire.state) {
|
|
802
|
+
const version = versions2[k];
|
|
803
|
+
if (isVersion(version)) applyRemote(k, wire.state[k], version, meta);
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
return;
|
|
547
807
|
}
|
|
548
808
|
});
|
|
549
809
|
function setKey(key, value) {
|
|
810
|
+
gate?.assert(key, value);
|
|
550
811
|
const version = [(versions[key]?.[0] ?? 0) + 1, clientId];
|
|
551
812
|
try {
|
|
552
813
|
bus.post({
|
|
@@ -578,29 +839,74 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
578
839
|
});
|
|
579
840
|
const persist = options.persist;
|
|
580
841
|
let flushPersist;
|
|
842
|
+
let settle = () => {
|
|
843
|
+
};
|
|
844
|
+
const hydrated = persist ? new Promise((resolve) => {
|
|
845
|
+
settle = resolve;
|
|
846
|
+
}) : Promise.resolve();
|
|
581
847
|
if (persist) {
|
|
582
|
-
const {
|
|
848
|
+
const {
|
|
849
|
+
adapter,
|
|
850
|
+
keys,
|
|
851
|
+
debounceMs = 100,
|
|
852
|
+
version: schemaVersion = 0,
|
|
853
|
+
migrate,
|
|
854
|
+
onRestoreError
|
|
855
|
+
} = persist;
|
|
583
856
|
const shouldPersist = (key) => !keys || keys.includes(key);
|
|
584
|
-
const
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
857
|
+
const refuse = (error) => {
|
|
858
|
+
if (onRestoreError) onRestoreError(error);
|
|
859
|
+
else if (process.env.NODE_ENV !== "production") {
|
|
860
|
+
devWarn(
|
|
861
|
+
"UE1002",
|
|
862
|
+
`${name}: persisted schema v${error.found} not restored, expected v${error.expected} (${error.reason}). https://rxova.org/packages/use-everywhere/guides/persistence/`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
const reconcile = (saved2) => {
|
|
867
|
+
const found = saved2.schema ?? 0;
|
|
868
|
+
if (found === schemaVersion) return { state: saved2.state, migrated: false };
|
|
869
|
+
if (found > schemaVersion) {
|
|
870
|
+
refuse({ reason: "ahead", found, expected: schemaVersion });
|
|
871
|
+
return void 0;
|
|
872
|
+
}
|
|
873
|
+
if (!migrate) {
|
|
874
|
+
refuse({ reason: "no-migrate", found, expected: schemaVersion });
|
|
875
|
+
return void 0;
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
return { state: migrate(saved2.state, found), migrated: true };
|
|
879
|
+
} catch (cause) {
|
|
880
|
+
refuse({ reason: "migrate-threw", found, expected: schemaVersion, cause });
|
|
881
|
+
return void 0;
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
const hydrate = (raw) => {
|
|
885
|
+
const reconciled = raw && reconcile(raw);
|
|
886
|
+
if (!reconciled || !raw) {
|
|
887
|
+
settle();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const { state: restored, migrated } = reconciled;
|
|
891
|
+
for (const key in restored) {
|
|
892
|
+
const version = raw.versions[key] ?? (migrated ? [1, clientId] : void 0);
|
|
588
893
|
if (!version || !shouldPersist(key)) continue;
|
|
589
|
-
applyRemote(key,
|
|
894
|
+
applyRemote(key, restored[key], version, { clientId, kind: bus.kind, self: true });
|
|
590
895
|
bus.post({
|
|
591
896
|
v: 1,
|
|
592
897
|
scope: "state",
|
|
593
898
|
type: "patch",
|
|
594
899
|
key,
|
|
595
|
-
value:
|
|
900
|
+
value: restored[key],
|
|
596
901
|
version,
|
|
597
902
|
clientId,
|
|
598
903
|
kind: bus.kind
|
|
599
904
|
});
|
|
600
905
|
}
|
|
906
|
+
settle();
|
|
601
907
|
};
|
|
602
908
|
const collect = () => {
|
|
603
|
-
const out = { v: 1, state: {}, versions: {} };
|
|
909
|
+
const out = { v: 1, schema: schemaVersion, state: {}, versions: {} };
|
|
604
910
|
for (const key in versions) {
|
|
605
911
|
const version = versions[key];
|
|
606
912
|
if (!version || version[0] === 0 || !shouldPersist(key)) continue;
|
|
@@ -640,6 +946,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
640
946
|
let storeClosed = false;
|
|
641
947
|
return {
|
|
642
948
|
clientId,
|
|
949
|
+
hydrated,
|
|
643
950
|
state: proxy,
|
|
644
951
|
getSnapshot: () => snapshot,
|
|
645
952
|
getVersions: () => versionsSnapshot,
|
|
@@ -647,6 +954,9 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
647
954
|
const next = typeof value === "function" ? value(state[key]) : value;
|
|
648
955
|
setKey(key, next);
|
|
649
956
|
},
|
|
957
|
+
transaction(fn) {
|
|
958
|
+
return batch(fn);
|
|
959
|
+
},
|
|
650
960
|
subscribe(fn) {
|
|
651
961
|
listeners.add(fn);
|
|
652
962
|
return () => listeners.delete(fn);
|
|
@@ -678,6 +988,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
678
988
|
if (live > 0) liveStores.set(name, live);
|
|
679
989
|
else liveStores.delete(name);
|
|
680
990
|
}
|
|
991
|
+
cancelSnapshot();
|
|
681
992
|
if (hasWindow) removeEventListener("pageshow", onPageShow);
|
|
682
993
|
if (flushPersist) {
|
|
683
994
|
flushPersist();
|
|
@@ -693,83 +1004,6 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
693
1004
|
};
|
|
694
1005
|
}
|
|
695
1006
|
|
|
696
|
-
// src/presence.ts
|
|
697
|
-
function createPresence(name, options = {}) {
|
|
698
|
-
const pruneAfterMs = options.pruneAfterMs ?? 5e3;
|
|
699
|
-
const probeGraceMs = options.probeGraceMs ?? 1e3;
|
|
700
|
-
const bus = getBus(name, options);
|
|
701
|
-
const peers = /* @__PURE__ */ new Map();
|
|
702
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
703
|
-
let snapshot = [];
|
|
704
|
-
function notify() {
|
|
705
|
-
snapshot = Object.freeze([...peers.values()]);
|
|
706
|
-
for (const fn of listeners) fn();
|
|
707
|
-
}
|
|
708
|
-
const unsubscribe = bus.subscribe((wire) => {
|
|
709
|
-
if (wire.clientId === bus.clientId) return;
|
|
710
|
-
if (wire.scope === "presence" && wire.type === "bye") {
|
|
711
|
-
if (peers.delete(wire.clientId)) notify();
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
const existing = peers.get(wire.clientId);
|
|
715
|
-
peers.set(wire.clientId, { id: wire.clientId, kind: wire.kind, lastSeen: Date.now() });
|
|
716
|
-
if (!existing) notify();
|
|
717
|
-
});
|
|
718
|
-
bus.post({ v: 1, scope: "presence", type: "hello", clientId: bus.clientId, kind: bus.kind });
|
|
719
|
-
const probed = /* @__PURE__ */ new Map();
|
|
720
|
-
const prune = setInterval(
|
|
721
|
-
() => {
|
|
722
|
-
const now = Date.now();
|
|
723
|
-
const silentSince = now - pruneAfterMs;
|
|
724
|
-
let changed = false;
|
|
725
|
-
let needProbe = false;
|
|
726
|
-
for (const [id, peer] of peers) {
|
|
727
|
-
if (peer.lastSeen >= silentSince) {
|
|
728
|
-
probed.delete(id);
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
const askedAt = probed.get(id);
|
|
732
|
-
if (askedAt === void 0) {
|
|
733
|
-
probed.set(id, now);
|
|
734
|
-
needProbe = true;
|
|
735
|
-
} else if (now - askedAt >= probeGraceMs) {
|
|
736
|
-
peers.delete(id);
|
|
737
|
-
probed.delete(id);
|
|
738
|
-
changed = true;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
if (needProbe) {
|
|
742
|
-
bus.post({
|
|
743
|
-
v: 1,
|
|
744
|
-
scope: "presence",
|
|
745
|
-
type: "hello",
|
|
746
|
-
clientId: bus.clientId,
|
|
747
|
-
kind: bus.kind
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
if (changed) notify();
|
|
751
|
-
},
|
|
752
|
-
Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
|
|
753
|
-
);
|
|
754
|
-
let closed = false;
|
|
755
|
-
return {
|
|
756
|
-
clientId: bus.clientId,
|
|
757
|
-
getPeers: () => snapshot,
|
|
758
|
-
subscribe(fn) {
|
|
759
|
-
listeners.add(fn);
|
|
760
|
-
return () => listeners.delete(fn);
|
|
761
|
-
},
|
|
762
|
-
close() {
|
|
763
|
-
if (closed) return;
|
|
764
|
-
closed = true;
|
|
765
|
-
clearInterval(prune);
|
|
766
|
-
unsubscribe();
|
|
767
|
-
listeners.clear();
|
|
768
|
-
bus.release();
|
|
769
|
-
}
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
|
|
773
1007
|
// src/leader-web-locks.ts
|
|
774
1008
|
var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
|
|
775
1009
|
function createWebLocksLeader(name, options, locks) {
|
|
@@ -1054,6 +1288,246 @@ function createHeartbeatLeader(name, options) {
|
|
|
1054
1288
|
};
|
|
1055
1289
|
}
|
|
1056
1290
|
|
|
1291
|
+
// src/shared-reducer.ts
|
|
1292
|
+
function createSharedReducer(name, reducer, initial, options = {}) {
|
|
1293
|
+
const key = options.key ?? "default";
|
|
1294
|
+
const bus = getBus(name, options);
|
|
1295
|
+
const clientId = bus.clientId;
|
|
1296
|
+
const leader = options.leader ?? createLeader(name, {
|
|
1297
|
+
...options.leaderOptions,
|
|
1298
|
+
// Spread conditionally: `exactOptionalPropertyTypes` distinguishes an
|
|
1299
|
+
// absent transport from one explicitly set to undefined, and the leader
|
|
1300
|
+
// must default its own rather than be handed a hole.
|
|
1301
|
+
...options.transport ? { transport: options.transport } : {}
|
|
1302
|
+
});
|
|
1303
|
+
const ownsLeader = !options.leader;
|
|
1304
|
+
let committed = initial;
|
|
1305
|
+
let seq = 0;
|
|
1306
|
+
let pending = [];
|
|
1307
|
+
const early = /* @__PURE__ */ new Map();
|
|
1308
|
+
let lastIssued = 0;
|
|
1309
|
+
let view = committed;
|
|
1310
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1311
|
+
let closed = false;
|
|
1312
|
+
const notify = () => {
|
|
1313
|
+
const next = pending.reduce((state, op) => reducer(state, op.action), committed);
|
|
1314
|
+
if (Object.is(next, view)) return;
|
|
1315
|
+
view = next;
|
|
1316
|
+
for (const fn of listeners) fn();
|
|
1317
|
+
};
|
|
1318
|
+
const askForSnapshot = () => bus.post({ v: 1, scope: "op", type: "hello", key, clientId, kind: bus.kind });
|
|
1319
|
+
const receiveCommit = (at, action, opId) => {
|
|
1320
|
+
if (at <= seq) return;
|
|
1321
|
+
if (at > seq + 1) {
|
|
1322
|
+
early.set(at, { action, opId });
|
|
1323
|
+
askForSnapshot();
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
applyCommit(at, action, opId);
|
|
1327
|
+
notify();
|
|
1328
|
+
};
|
|
1329
|
+
const sequence = (action, opId) => {
|
|
1330
|
+
lastIssued = Math.max(lastIssued, seq) + 1;
|
|
1331
|
+
const at = lastIssued;
|
|
1332
|
+
bus.post({
|
|
1333
|
+
v: 1,
|
|
1334
|
+
scope: "op",
|
|
1335
|
+
type: "commit",
|
|
1336
|
+
key,
|
|
1337
|
+
action,
|
|
1338
|
+
opId,
|
|
1339
|
+
seq: at,
|
|
1340
|
+
clientId,
|
|
1341
|
+
kind: bus.kind
|
|
1342
|
+
});
|
|
1343
|
+
receiveCommit(at, action, opId);
|
|
1344
|
+
};
|
|
1345
|
+
const applyCommit = (at, action, opId) => {
|
|
1346
|
+
committed = reducer(committed, action);
|
|
1347
|
+
seq = at;
|
|
1348
|
+
pending = pending.filter((op) => op.opId !== opId);
|
|
1349
|
+
const next = early.get(seq + 1);
|
|
1350
|
+
if (next) {
|
|
1351
|
+
early.delete(seq + 1);
|
|
1352
|
+
applyCommit(seq + 1, next.action, next.opId);
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
1356
|
+
if (wire.scope !== "op" || wire.key !== key) return;
|
|
1357
|
+
if (wire.type === "propose") {
|
|
1358
|
+
if (!leader.getSnapshot().isLeader) return;
|
|
1359
|
+
sequence(wire.action, wire.opId);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (wire.type === "commit") {
|
|
1363
|
+
receiveCommit(wire.seq, wire.action, wire.opId);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (wire.type === "hello") {
|
|
1367
|
+
if (seq === 0) return;
|
|
1368
|
+
bus.post({
|
|
1369
|
+
v: 1,
|
|
1370
|
+
scope: "op",
|
|
1371
|
+
type: "snapshot",
|
|
1372
|
+
key,
|
|
1373
|
+
state: committed,
|
|
1374
|
+
seq,
|
|
1375
|
+
clientId,
|
|
1376
|
+
kind: bus.kind
|
|
1377
|
+
});
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (wire.type === "snapshot") {
|
|
1381
|
+
if (wire.seq <= seq) return;
|
|
1382
|
+
committed = wire.state;
|
|
1383
|
+
seq = wire.seq;
|
|
1384
|
+
for (const at of [...early.keys()]) if (at <= seq) early.delete(at);
|
|
1385
|
+
const next = early.get(seq + 1);
|
|
1386
|
+
if (next) {
|
|
1387
|
+
early.delete(seq + 1);
|
|
1388
|
+
applyCommit(seq + 1, next.action, next.opId);
|
|
1389
|
+
}
|
|
1390
|
+
notify();
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
const dispatch = (action) => {
|
|
1395
|
+
if (closed) return;
|
|
1396
|
+
const opId = newMsgId();
|
|
1397
|
+
pending.push({ opId, action });
|
|
1398
|
+
notify();
|
|
1399
|
+
if (leader.getSnapshot().isLeader) sequence(action, opId);
|
|
1400
|
+
else
|
|
1401
|
+
bus.post({ v: 1, scope: "op", type: "propose", key, action, opId, clientId, kind: bus.kind });
|
|
1402
|
+
};
|
|
1403
|
+
askForSnapshot();
|
|
1404
|
+
return {
|
|
1405
|
+
clientId,
|
|
1406
|
+
getSnapshot: () => view,
|
|
1407
|
+
dispatch,
|
|
1408
|
+
subscribe(fn) {
|
|
1409
|
+
listeners.add(fn);
|
|
1410
|
+
return () => listeners.delete(fn);
|
|
1411
|
+
},
|
|
1412
|
+
pendingCount: () => pending.length,
|
|
1413
|
+
close() {
|
|
1414
|
+
if (closed) return;
|
|
1415
|
+
closed = true;
|
|
1416
|
+
unsubscribe();
|
|
1417
|
+
listeners.clear();
|
|
1418
|
+
if (ownsLeader) leader.close();
|
|
1419
|
+
bus.release();
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/presence.ts
|
|
1425
|
+
function createPresence(name, options = {}) {
|
|
1426
|
+
const pruneAfterMs = options.pruneAfterMs ?? 5e3;
|
|
1427
|
+
const probeGraceMs = options.probeGraceMs ?? 1e3;
|
|
1428
|
+
const bus = getBus(name, options);
|
|
1429
|
+
const peers = /* @__PURE__ */ new Map();
|
|
1430
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1431
|
+
let snapshot = [];
|
|
1432
|
+
let metadata = options.metadata;
|
|
1433
|
+
const same = (a, b) => {
|
|
1434
|
+
if (Object.is(a, b)) return true;
|
|
1435
|
+
try {
|
|
1436
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1437
|
+
} catch {
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
const self = () => ({
|
|
1442
|
+
id: bus.clientId,
|
|
1443
|
+
kind: bus.kind,
|
|
1444
|
+
lastSeen: Date.now(),
|
|
1445
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1446
|
+
});
|
|
1447
|
+
function notify() {
|
|
1448
|
+
const roster = [...peers.values()];
|
|
1449
|
+
snapshot = Object.freeze(options.includeSelf ? [self(), ...roster] : roster);
|
|
1450
|
+
for (const fn of listeners) fn();
|
|
1451
|
+
}
|
|
1452
|
+
const announce2 = () => bus.post({
|
|
1453
|
+
v: 1,
|
|
1454
|
+
scope: "presence",
|
|
1455
|
+
type: "hello",
|
|
1456
|
+
clientId: bus.clientId,
|
|
1457
|
+
kind: bus.kind,
|
|
1458
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1459
|
+
});
|
|
1460
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
1461
|
+
if (wire.clientId === bus.clientId) return;
|
|
1462
|
+
if (wire.scope === "presence" && wire.type === "bye") {
|
|
1463
|
+
if (peers.delete(wire.clientId)) notify();
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
const existing = peers.get(wire.clientId);
|
|
1467
|
+
const announced = wire.scope === "presence" ? wire.metadata : void 0;
|
|
1468
|
+
const next = announced === void 0 ? existing?.metadata : announced;
|
|
1469
|
+
peers.set(wire.clientId, {
|
|
1470
|
+
id: wire.clientId,
|
|
1471
|
+
kind: wire.kind,
|
|
1472
|
+
lastSeen: Date.now(),
|
|
1473
|
+
...next === void 0 ? {} : { metadata: next }
|
|
1474
|
+
});
|
|
1475
|
+
if (!existing || !same(existing.metadata, next)) notify();
|
|
1476
|
+
});
|
|
1477
|
+
announce2();
|
|
1478
|
+
if (options.includeSelf) notify();
|
|
1479
|
+
const probed = /* @__PURE__ */ new Map();
|
|
1480
|
+
const prune = setInterval(
|
|
1481
|
+
() => {
|
|
1482
|
+
const now = Date.now();
|
|
1483
|
+
const silentSince = now - pruneAfterMs;
|
|
1484
|
+
let changed = false;
|
|
1485
|
+
let needProbe = false;
|
|
1486
|
+
for (const [id, peer] of peers) {
|
|
1487
|
+
if (peer.lastSeen >= silentSince) {
|
|
1488
|
+
probed.delete(id);
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
const askedAt = probed.get(id);
|
|
1492
|
+
if (askedAt === void 0) {
|
|
1493
|
+
probed.set(id, now);
|
|
1494
|
+
needProbe = true;
|
|
1495
|
+
} else if (now - askedAt >= probeGraceMs) {
|
|
1496
|
+
peers.delete(id);
|
|
1497
|
+
probed.delete(id);
|
|
1498
|
+
changed = true;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (needProbe) announce2();
|
|
1502
|
+
if (changed) notify();
|
|
1503
|
+
},
|
|
1504
|
+
Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
|
|
1505
|
+
);
|
|
1506
|
+
let closed = false;
|
|
1507
|
+
return {
|
|
1508
|
+
clientId: bus.clientId,
|
|
1509
|
+
getPeers: () => snapshot,
|
|
1510
|
+
setMetadata(next) {
|
|
1511
|
+
if (same(metadata, next)) return;
|
|
1512
|
+
metadata = next;
|
|
1513
|
+
announce2();
|
|
1514
|
+
notify();
|
|
1515
|
+
},
|
|
1516
|
+
subscribe(fn) {
|
|
1517
|
+
listeners.add(fn);
|
|
1518
|
+
return () => listeners.delete(fn);
|
|
1519
|
+
},
|
|
1520
|
+
close() {
|
|
1521
|
+
if (closed) return;
|
|
1522
|
+
closed = true;
|
|
1523
|
+
clearInterval(prune);
|
|
1524
|
+
unsubscribe();
|
|
1525
|
+
listeners.clear();
|
|
1526
|
+
bus.release();
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1057
1531
|
// src/persist-web-storage.ts
|
|
1058
1532
|
function isPersisted(value) {
|
|
1059
1533
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -1061,6 +1535,7 @@ function isPersisted(value) {
|
|
|
1061
1535
|
return candidate.v === 1 && typeof candidate.state === "object" && typeof candidate.versions === "object";
|
|
1062
1536
|
}
|
|
1063
1537
|
function webStorageAdapter(storage, key, options = {}) {
|
|
1538
|
+
const serializer = options.serializer ?? jsonSerializer;
|
|
1064
1539
|
const report = (error, operation) => {
|
|
1065
1540
|
try {
|
|
1066
1541
|
options.onError?.(error, operation);
|
|
@@ -1080,7 +1555,7 @@ function webStorageAdapter(storage, key, options = {}) {
|
|
|
1080
1555
|
try {
|
|
1081
1556
|
const raw = resolve("read")?.getItem(key);
|
|
1082
1557
|
if (!raw) return void 0;
|
|
1083
|
-
const parsed =
|
|
1558
|
+
const parsed = serializer.parse(raw);
|
|
1084
1559
|
return isPersisted(parsed) ? parsed : void 0;
|
|
1085
1560
|
} catch (error) {
|
|
1086
1561
|
report(error, "read");
|
|
@@ -1089,7 +1564,7 @@ function webStorageAdapter(storage, key, options = {}) {
|
|
|
1089
1564
|
},
|
|
1090
1565
|
write(snapshot) {
|
|
1091
1566
|
try {
|
|
1092
|
-
resolve("write")?.setItem(key,
|
|
1567
|
+
resolve("write")?.setItem(key, serializer.stringify(snapshot));
|
|
1093
1568
|
} catch (error) {
|
|
1094
1569
|
report(error, "write");
|
|
1095
1570
|
}
|
|
@@ -1110,6 +1585,58 @@ function sessionStorageAdapter(key, options) {
|
|
|
1110
1585
|
return webStorageAdapter(() => globalThis.sessionStorage, key, options);
|
|
1111
1586
|
}
|
|
1112
1587
|
|
|
1588
|
+
// src/persist-indexeddb.ts
|
|
1589
|
+
var STORE = "state";
|
|
1590
|
+
var request = (req) => new Promise((resolve, reject) => {
|
|
1591
|
+
req.onsuccess = () => resolve(req.result);
|
|
1592
|
+
req.onerror = () => reject(req.error);
|
|
1593
|
+
});
|
|
1594
|
+
function indexedDbAdapter(key, options = {}) {
|
|
1595
|
+
const database = options.database ?? "use-everywhere";
|
|
1596
|
+
let open;
|
|
1597
|
+
const report = (error, operation) => {
|
|
1598
|
+
try {
|
|
1599
|
+
options.onError?.(error, operation);
|
|
1600
|
+
} catch {
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
const db = () => open ?? (open = new Promise((resolve, reject) => {
|
|
1604
|
+
const req = indexedDB.open(database, 1);
|
|
1605
|
+
req.onupgradeneeded = () => req.result.createObjectStore(STORE);
|
|
1606
|
+
req.onsuccess = () => resolve(req.result);
|
|
1607
|
+
req.onerror = () => reject(req.error);
|
|
1608
|
+
req.onblocked = () => reject(new Error("use-everywhere: IndexedDB upgrade blocked"));
|
|
1609
|
+
}));
|
|
1610
|
+
const transact = async (mode, run) => {
|
|
1611
|
+
const connection = await db();
|
|
1612
|
+
return request(run(connection.transaction(STORE, mode).objectStore(STORE)));
|
|
1613
|
+
};
|
|
1614
|
+
return {
|
|
1615
|
+
async read() {
|
|
1616
|
+
try {
|
|
1617
|
+
return await transact("readonly", (store) => store.get(key));
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
report(error, "read");
|
|
1620
|
+
return void 0;
|
|
1621
|
+
}
|
|
1622
|
+
},
|
|
1623
|
+
async write(snapshot) {
|
|
1624
|
+
try {
|
|
1625
|
+
await transact("readwrite", (store) => store.put(snapshot, key));
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
report(error, "write");
|
|
1628
|
+
}
|
|
1629
|
+
},
|
|
1630
|
+
async remove() {
|
|
1631
|
+
try {
|
|
1632
|
+
await transact("readwrite", (store) => store.delete(key));
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
report(error, "remove");
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1113
1640
|
// src/errors/handshake-timeout-error.ts
|
|
1114
1641
|
var HandshakeTimeoutError = class extends Error {
|
|
1115
1642
|
constructor(message = "handshake with the other window timed out") {
|
|
@@ -1344,6 +1871,25 @@ function connectToOpener(options) {
|
|
|
1344
1871
|
}
|
|
1345
1872
|
};
|
|
1346
1873
|
}
|
|
1874
|
+
|
|
1875
|
+
// src/namespace.ts
|
|
1876
|
+
var SEPARATOR = ":";
|
|
1877
|
+
function createNamespace(namespace) {
|
|
1878
|
+
if (!namespace) {
|
|
1879
|
+
throw new TypeError(
|
|
1880
|
+
"use-everywhere: createNamespace() needs a non-empty name \u2014 an empty one would put every bus back on the shared defaults, which is what it exists to prevent."
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
const busName = (name = DEFAULT_NAME) => `${namespace}${SEPARATOR}${name}`;
|
|
1884
|
+
return {
|
|
1885
|
+
name: namespace,
|
|
1886
|
+
busName,
|
|
1887
|
+
createSharedStore: (name, initial, options) => createSharedStore(busName(name), initial, options),
|
|
1888
|
+
createChannel: (name, options) => createChannel(busName(name), options),
|
|
1889
|
+
createPresence: (name, options) => createPresence(busName(name), options),
|
|
1890
|
+
createLeader: (name, options) => createLeader(busName(name), options)
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1347
1893
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1348
1894
|
0 && (module.exports = {
|
|
1349
1895
|
BroadcastChannelTransport,
|
|
@@ -1352,18 +1898,24 @@ function connectToOpener(options) {
|
|
|
1352
1898
|
HandshakeTimeoutError,
|
|
1353
1899
|
NoopTransport,
|
|
1354
1900
|
StorageTransport,
|
|
1901
|
+
WIRE_VERSION,
|
|
1355
1902
|
WindowClosedError,
|
|
1356
1903
|
connectToOpener,
|
|
1357
1904
|
createChannel,
|
|
1358
1905
|
createLeader,
|
|
1906
|
+
createNamespace,
|
|
1359
1907
|
createPresence,
|
|
1908
|
+
createSharedReducer,
|
|
1360
1909
|
createSharedStore,
|
|
1361
1910
|
defaultTransport,
|
|
1362
1911
|
enableDebug,
|
|
1363
1912
|
getBusNames,
|
|
1364
1913
|
getTransportKind,
|
|
1914
|
+
getWireSkew,
|
|
1915
|
+
indexedDbAdapter,
|
|
1365
1916
|
isBroadcastChannelAvailable,
|
|
1366
1917
|
isStorageEventAvailable,
|
|
1918
|
+
jsonSerializer,
|
|
1367
1919
|
localStorageAdapter,
|
|
1368
1920
|
newer,
|
|
1369
1921
|
observeBus,
|