@use-everywhere/core 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +677 -142
- package/dist/index.d.cts +658 -74
- package/dist/index.d.ts +658 -74
- package/dist/index.js +671 -142
- 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,
|
|
@@ -109,9 +115,11 @@ function randomHex(bytes) {
|
|
|
109
115
|
for (const b of buf) out2 += b.toString(16).padStart(2, "0");
|
|
110
116
|
return out2;
|
|
111
117
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
if (process.env.NODE_ENV !== "production") {
|
|
119
|
+
devWarn(
|
|
120
|
+
"[use-everywhere] crypto.getRandomValues is unavailable; falling back to Math.random for ids. Cross-origin window nonces are not cryptographically strong in this environment."
|
|
121
|
+
);
|
|
122
|
+
}
|
|
115
123
|
let out = "";
|
|
116
124
|
while (out.length < bytes * 2)
|
|
117
125
|
out += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
|
|
@@ -128,15 +136,18 @@ function newMsgId() {
|
|
|
128
136
|
var PROTOCOL = 1;
|
|
129
137
|
var TABLE = /* @__PURE__ */ Symbol.for(`use-everywhere.rendezvous.${PROTOCOL}`);
|
|
130
138
|
var CENSUS = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.census");
|
|
139
|
+
var SKEW = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.skew");
|
|
131
140
|
function announce() {
|
|
132
141
|
const g = globalThis;
|
|
133
142
|
const census = g[CENSUS] ?? (g[CENSUS] = { protocols: [] });
|
|
134
143
|
if (census.protocols.includes(PROTOCOL)) return;
|
|
135
144
|
census.protocols.push(PROTOCOL);
|
|
136
145
|
if (census.protocols.length > 1) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
if (process.env.NODE_ENV !== "production") {
|
|
147
|
+
devWarn(
|
|
148
|
+
`[use-everywhere] 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
|
+
);
|
|
150
|
+
}
|
|
140
151
|
}
|
|
141
152
|
}
|
|
142
153
|
function busTable() {
|
|
@@ -144,6 +155,10 @@ function busTable() {
|
|
|
144
155
|
const g = globalThis;
|
|
145
156
|
return g[TABLE] ?? (g[TABLE] = /* @__PURE__ */ new Map());
|
|
146
157
|
}
|
|
158
|
+
function skewLedger() {
|
|
159
|
+
const g = globalThis;
|
|
160
|
+
return g[SKEW] ?? (g[SKEW] = /* @__PURE__ */ new Map());
|
|
161
|
+
}
|
|
147
162
|
|
|
148
163
|
// src/transport/broadcast-channel-transport.ts
|
|
149
164
|
var BroadcastChannelTransport = class {
|
|
@@ -183,9 +198,38 @@ var NoopTransport = class {
|
|
|
183
198
|
}
|
|
184
199
|
};
|
|
185
200
|
|
|
201
|
+
// src/serializer.ts
|
|
202
|
+
function lossyType(raw, value) {
|
|
203
|
+
if (value === void 0) return raw === void 0 ? "undefined" : "undefined after toJSON";
|
|
204
|
+
if (raw instanceof Date) return "Date";
|
|
205
|
+
if (raw instanceof Map) return "Map";
|
|
206
|
+
if (raw instanceof Set) return "Set";
|
|
207
|
+
if (raw instanceof RegExp) return "RegExp";
|
|
208
|
+
if (ArrayBuffer.isView(raw)) return "TypedArray";
|
|
209
|
+
const type = typeof raw;
|
|
210
|
+
return type === "function" || type === "symbol" ? type : void 0;
|
|
211
|
+
}
|
|
212
|
+
var jsonSerializer = {
|
|
213
|
+
stringify(value) {
|
|
214
|
+
if (value === void 0) {
|
|
215
|
+
throw new TypeError("use-everywhere: cannot serialize undefined");
|
|
216
|
+
}
|
|
217
|
+
return JSON.stringify(value, function replacer(key, forJson) {
|
|
218
|
+
const type = lossyType(this[key], forJson);
|
|
219
|
+
if (type) {
|
|
220
|
+
throw new TypeError(
|
|
221
|
+
`use-everywhere: ${key ? `"${key}" is ` : ""}${type}, which JSON cannot round-trip. https://rxova.org/guides/serialization/`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return forJson;
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
parse: (text) => JSON.parse(text)
|
|
228
|
+
};
|
|
229
|
+
|
|
186
230
|
// src/transport/storage-transport.ts
|
|
187
231
|
var StorageTransport = class {
|
|
188
|
-
constructor(name, storage = globalThis.localStorage) {
|
|
232
|
+
constructor(name, storage = globalThis.localStorage, serializer = jsonSerializer) {
|
|
189
233
|
this.kind = "storage";
|
|
190
234
|
this.listeners = /* @__PURE__ */ new Set();
|
|
191
235
|
this.seq = 0;
|
|
@@ -194,13 +238,14 @@ var StorageTransport = class {
|
|
|
194
238
|
"[use-everywhere] StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
|
|
195
239
|
);
|
|
196
240
|
}
|
|
241
|
+
this.serializer = serializer;
|
|
197
242
|
this.key = `use-everywhere:bus:${name}`;
|
|
198
243
|
this.storage = storage;
|
|
199
244
|
this.onStorage = (event) => {
|
|
200
245
|
if (event.key !== this.key || event.newValue === null) return;
|
|
201
246
|
let payload;
|
|
202
247
|
try {
|
|
203
|
-
payload =
|
|
248
|
+
payload = this.serializer.parse(event.newValue);
|
|
204
249
|
} catch {
|
|
205
250
|
return;
|
|
206
251
|
}
|
|
@@ -209,7 +254,7 @@ var StorageTransport = class {
|
|
|
209
254
|
addEventListener("storage", this.onStorage);
|
|
210
255
|
}
|
|
211
256
|
post(data) {
|
|
212
|
-
const envelope =
|
|
257
|
+
const envelope = this.serializer.stringify({ seq: this.seq++, data });
|
|
213
258
|
try {
|
|
214
259
|
this.storage.setItem(this.key, envelope);
|
|
215
260
|
this.storage.removeItem(this.key);
|
|
@@ -225,12 +270,6 @@ var StorageTransport = class {
|
|
|
225
270
|
removeEventListener("storage", this.onStorage);
|
|
226
271
|
}
|
|
227
272
|
};
|
|
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
273
|
|
|
235
274
|
// src/transport/default-transport.ts
|
|
236
275
|
function isBroadcastChannelAvailable() {
|
|
@@ -252,27 +291,58 @@ function isStorageEventAvailable() {
|
|
|
252
291
|
function defaultTransport(name) {
|
|
253
292
|
if (isBroadcastChannelAvailable()) return new BroadcastChannelTransport(name);
|
|
254
293
|
if (isStorageEventAvailable()) {
|
|
294
|
+
if (process.env.NODE_ENV !== "production") {
|
|
295
|
+
devWarn(
|
|
296
|
+
"[use-everywhere] no BroadcastChannel; using the storage-event fallback. Values serialise as JSON, not structured clone \u2014 keep them JSON-shaped."
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return new StorageTransport(name);
|
|
300
|
+
}
|
|
301
|
+
if (process.env.NODE_ENV !== "production") {
|
|
255
302
|
devWarn(
|
|
256
|
-
"[use-everywhere] no BroadcastChannel
|
|
303
|
+
"[use-everywhere] no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
257
304
|
);
|
|
258
|
-
return new StorageTransport(name);
|
|
259
305
|
}
|
|
260
|
-
devWarn(
|
|
261
|
-
"[use-everywhere] no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
262
|
-
);
|
|
263
306
|
return new NoopTransport();
|
|
264
307
|
}
|
|
265
308
|
|
|
266
|
-
// src/
|
|
309
|
+
// src/wire.ts
|
|
310
|
+
var WIRE_VERSION = 1;
|
|
311
|
+
function hasEnvelopeShape(wire) {
|
|
312
|
+
return typeof wire.scope === "string" && typeof wire.type === "string" && typeof wire.clientId === "string";
|
|
313
|
+
}
|
|
267
314
|
function isBusWire(data) {
|
|
268
315
|
if (typeof data !== "object" || data === null) return false;
|
|
269
316
|
const wire = data;
|
|
270
|
-
return wire.v ===
|
|
317
|
+
return wire.v === WIRE_VERSION && hasEnvelopeShape(wire);
|
|
318
|
+
}
|
|
319
|
+
function foreignWireVersion(data) {
|
|
320
|
+
if (typeof data !== "object" || data === null) return null;
|
|
321
|
+
const wire = data;
|
|
322
|
+
if (typeof wire.v !== "number" || wire.v === WIRE_VERSION) return null;
|
|
323
|
+
return hasEnvelopeShape(wire) ? wire.v : null;
|
|
324
|
+
}
|
|
325
|
+
function recordSkew(name, version) {
|
|
326
|
+
const seen = skewLedger();
|
|
327
|
+
const versions = seen.get(name) ?? /* @__PURE__ */ new Set();
|
|
328
|
+
seen.set(name, versions);
|
|
329
|
+
if (versions.has(version)) return;
|
|
330
|
+
versions.add(version);
|
|
331
|
+
if (process.env.NODE_ENV !== "production") {
|
|
332
|
+
devWarn(
|
|
333
|
+
`[use-everywhere] 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/under-the-hood/version-skew/`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function getWireSkew(name) {
|
|
338
|
+
return [...skewLedger().get(name) ?? []].sort((a, b) => a - b);
|
|
271
339
|
}
|
|
340
|
+
|
|
341
|
+
// src/bus.ts
|
|
272
342
|
function defaultKind() {
|
|
273
343
|
return typeof document === "undefined" ? "worker" : "tab";
|
|
274
344
|
}
|
|
275
|
-
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event"]);
|
|
345
|
+
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event", "op"]);
|
|
276
346
|
function createBusCore(name, options, onShutdown) {
|
|
277
347
|
const transport = (options.transport ?? defaultTransport)(name);
|
|
278
348
|
const clientId = newClientId();
|
|
@@ -293,6 +363,11 @@ function createBusCore(name, options, onShutdown) {
|
|
|
293
363
|
if (SHARED_WITHIN_CLIENT.has(wire.scope)) deliver(wire, from);
|
|
294
364
|
};
|
|
295
365
|
const unsubscribe = transport.subscribe((data) => {
|
|
366
|
+
const foreign = foreignWireVersion(data);
|
|
367
|
+
if (foreign !== null) {
|
|
368
|
+
recordSkew(name, foreign);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
296
371
|
if (!isBusWire(data)) return;
|
|
297
372
|
if (data.clientId === clientId) return;
|
|
298
373
|
emitBusEvent(name, "in", data);
|
|
@@ -385,57 +460,172 @@ function getBus(name, options = {}) {
|
|
|
385
460
|
table.set(name, core);
|
|
386
461
|
} else {
|
|
387
462
|
if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
463
|
+
if (process.env.NODE_ENV !== "production") {
|
|
464
|
+
devWarn(
|
|
465
|
+
`[use-everywhere] bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
391
468
|
}
|
|
392
469
|
if (options.kind !== void 0 && options.kind !== core.kind) {
|
|
393
|
-
|
|
470
|
+
if (process.env.NODE_ENV !== "production") {
|
|
471
|
+
devWarn(
|
|
472
|
+
`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
394
475
|
}
|
|
395
476
|
}
|
|
396
477
|
return core.connect();
|
|
397
478
|
}
|
|
398
479
|
|
|
480
|
+
// src/schema.ts
|
|
481
|
+
function validate(schema, payload) {
|
|
482
|
+
const props = schema["~standard"];
|
|
483
|
+
const result = props.validate(payload);
|
|
484
|
+
if (typeof result.then === "function") {
|
|
485
|
+
return { ok: false, issues: [`the "${props.vendor}" schema validates asynchronously`] };
|
|
486
|
+
}
|
|
487
|
+
const sync = result;
|
|
488
|
+
if (sync.issues) return { ok: false, issues: sync.issues.map((issue) => issue.message) };
|
|
489
|
+
return { ok: true, value: sync.value };
|
|
490
|
+
}
|
|
491
|
+
function createGate(name, schemas, onInvalid) {
|
|
492
|
+
if (!schemas) return void 0;
|
|
493
|
+
const check = (key, payload, direction) => {
|
|
494
|
+
const schema = schemas[key];
|
|
495
|
+
if (!schema) return void 0;
|
|
496
|
+
const result = validate(schema, payload);
|
|
497
|
+
if (result.ok) return void 0;
|
|
498
|
+
const info = { name, key, direction, payload, issues: result.issues };
|
|
499
|
+
if (onInvalid) onInvalid(info);
|
|
500
|
+
else if (process.env.NODE_ENV !== "production") {
|
|
501
|
+
devWarn(
|
|
502
|
+
`[use-everywhere] ${name}/${key}: ${direction}bound payload rejected by its schema \u2014 ${result.issues.join("; ")}. https://rxova.org/guides/validating-payloads/`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
return result.issues;
|
|
506
|
+
};
|
|
507
|
+
return {
|
|
508
|
+
accepts: (key, payload) => !check(key, payload, "in"),
|
|
509
|
+
assert(key, payload) {
|
|
510
|
+
const issues = check(key, payload, "out");
|
|
511
|
+
if (issues) {
|
|
512
|
+
throw new TypeError(
|
|
513
|
+
`use-everywhere: ${name}/${key} \u2014 the value does not match its schema, so nothing was sent or applied. ${issues.join("; ")}`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
399
520
|
// src/channel.ts
|
|
400
521
|
function createChannel(name, options = {}) {
|
|
401
522
|
const bus = getBus(name, options);
|
|
402
523
|
const handlers = /* @__PURE__ */ new Map();
|
|
524
|
+
const responders = /* @__PURE__ */ new Map();
|
|
525
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
526
|
+
const gate = createGate(name, options.schema, options.onInvalid);
|
|
527
|
+
const deliver = (type, payload, meta) => {
|
|
528
|
+
const set = handlers.get(type);
|
|
529
|
+
if (!set) return;
|
|
530
|
+
for (const fn of [...set]) fn(payload, meta);
|
|
531
|
+
};
|
|
403
532
|
const unsubscribe = bus.subscribe((wire) => {
|
|
404
533
|
if (wire.scope !== "event") return;
|
|
405
|
-
const set = handlers.get(wire.type);
|
|
406
|
-
if (!set) return;
|
|
407
534
|
const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
535
|
+
if (wire.replyTo !== void 0) {
|
|
536
|
+
const settle = waiting.get(wire.replyTo);
|
|
537
|
+
if (!settle) return;
|
|
538
|
+
waiting.delete(wire.replyTo);
|
|
539
|
+
settle(wire.payload);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const responder = responders.get(wire.type);
|
|
543
|
+
if (responder) {
|
|
415
544
|
bus.post({
|
|
416
545
|
v: 1,
|
|
417
546
|
scope: "event",
|
|
418
|
-
type,
|
|
419
|
-
payload,
|
|
547
|
+
type: wire.type,
|
|
548
|
+
payload: responder(wire.payload, meta),
|
|
420
549
|
clientId: bus.clientId,
|
|
421
550
|
kind: bus.kind,
|
|
422
|
-
msgId: newMsgId()
|
|
551
|
+
msgId: newMsgId(),
|
|
552
|
+
replyTo: wire.msgId
|
|
423
553
|
});
|
|
554
|
+
}
|
|
555
|
+
if (!handlers.has(wire.type)) return;
|
|
556
|
+
if (gate && !gate.accepts(wire.type, wire.payload)) return;
|
|
557
|
+
deliver(wire.type, wire.payload, meta);
|
|
558
|
+
});
|
|
559
|
+
let closed = false;
|
|
560
|
+
const send = (type, payload, msgId) => {
|
|
561
|
+
gate?.assert(type, payload);
|
|
562
|
+
bus.post({
|
|
563
|
+
v: 1,
|
|
564
|
+
scope: "event",
|
|
565
|
+
type,
|
|
566
|
+
payload,
|
|
567
|
+
clientId: bus.clientId,
|
|
568
|
+
kind: bus.kind,
|
|
569
|
+
msgId
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
return {
|
|
573
|
+
name,
|
|
574
|
+
clientId: bus.clientId,
|
|
575
|
+
post(type, payload, options2) {
|
|
576
|
+
send(type, payload, newMsgId());
|
|
577
|
+
if (options2?.echo) {
|
|
578
|
+
deliver(type, payload, { clientId: bus.clientId, kind: bus.kind, self: true });
|
|
579
|
+
}
|
|
424
580
|
},
|
|
425
|
-
on(type, handler) {
|
|
581
|
+
on(type, handler, options2) {
|
|
426
582
|
let set = handlers.get(type);
|
|
427
583
|
if (!set) {
|
|
428
584
|
set = /* @__PURE__ */ new Set();
|
|
429
585
|
handlers.set(type, set);
|
|
430
586
|
}
|
|
431
|
-
|
|
432
|
-
|
|
587
|
+
const entry = options2?.once ? (payload, meta) => {
|
|
588
|
+
set.delete(entry);
|
|
589
|
+
handler(payload, meta);
|
|
590
|
+
} : handler;
|
|
591
|
+
set.add(entry);
|
|
592
|
+
return () => set.delete(entry);
|
|
593
|
+
},
|
|
594
|
+
ask(type, payload, options2) {
|
|
595
|
+
const msgId = newMsgId();
|
|
596
|
+
return new Promise((resolve, reject) => {
|
|
597
|
+
const timer = setTimeout(() => {
|
|
598
|
+
waiting.delete(msgId);
|
|
599
|
+
reject(
|
|
600
|
+
new Error(
|
|
601
|
+
`use-everywhere: nobody answered "${type}" on "${name}" within ${String(options2?.timeoutMs ?? 5e3)}ms`
|
|
602
|
+
)
|
|
603
|
+
);
|
|
604
|
+
}, options2?.timeoutMs ?? 5e3);
|
|
605
|
+
waiting.set(msgId, (value) => {
|
|
606
|
+
clearTimeout(timer);
|
|
607
|
+
resolve(value);
|
|
608
|
+
});
|
|
609
|
+
try {
|
|
610
|
+
send(type, payload, msgId);
|
|
611
|
+
} catch (error) {
|
|
612
|
+
clearTimeout(timer);
|
|
613
|
+
waiting.delete(msgId);
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
answer(type, responder) {
|
|
619
|
+
responders.set(type, responder);
|
|
620
|
+
return () => responders.delete(type);
|
|
433
621
|
},
|
|
434
622
|
close() {
|
|
435
623
|
if (closed) return;
|
|
436
624
|
closed = true;
|
|
437
625
|
unsubscribe();
|
|
438
626
|
handlers.clear();
|
|
627
|
+
responders.clear();
|
|
628
|
+
waiting.clear();
|
|
439
629
|
bus.release();
|
|
440
630
|
}
|
|
441
631
|
};
|
|
@@ -486,15 +676,18 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
486
676
|
if (onSharedBus) {
|
|
487
677
|
const live = liveStores.get(name) ?? 0;
|
|
488
678
|
if (live > 0) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
679
|
+
if (process.env.NODE_ENV !== "production") {
|
|
680
|
+
devWarn(
|
|
681
|
+
`[use-everywhere] 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
|
+
);
|
|
683
|
+
}
|
|
492
684
|
}
|
|
493
685
|
liveStores.set(name, live + 1);
|
|
494
686
|
}
|
|
495
687
|
const bus = getBus(name, options);
|
|
496
688
|
const clientId = bus.clientId;
|
|
497
689
|
const accept = options.accept;
|
|
690
|
+
const gate = createGate(name, options.schema, options.onInvalid);
|
|
498
691
|
const state = { ...initial };
|
|
499
692
|
const versions = {};
|
|
500
693
|
for (const k in state) {
|
|
@@ -505,23 +698,61 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
505
698
|
let versionsSnapshot = Object.freeze({ ...versions });
|
|
506
699
|
const listeners = /* @__PURE__ */ new Set();
|
|
507
700
|
const keyListeners = /* @__PURE__ */ new Map();
|
|
508
|
-
|
|
701
|
+
let batchDepth = 0;
|
|
702
|
+
const batched = [];
|
|
703
|
+
const rebuild = () => {
|
|
509
704
|
snapshot = Object.freeze({ ...state });
|
|
510
705
|
versionsSnapshot = Object.freeze({ ...versions });
|
|
706
|
+
};
|
|
707
|
+
const emit = (key, value, meta) => {
|
|
511
708
|
for (const fn of listeners) fn(key, value, meta);
|
|
512
709
|
const set = keyListeners.get(key);
|
|
513
710
|
if (set) for (const fn of set) fn();
|
|
711
|
+
};
|
|
712
|
+
function notify(key, value, meta) {
|
|
713
|
+
if (batchDepth > 0) {
|
|
714
|
+
batched.push([key, value, meta]);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
rebuild();
|
|
718
|
+
emit(key, value, meta);
|
|
719
|
+
}
|
|
720
|
+
function batch(fn) {
|
|
721
|
+
batchDepth++;
|
|
722
|
+
try {
|
|
723
|
+
return fn();
|
|
724
|
+
} finally {
|
|
725
|
+
batchDepth--;
|
|
726
|
+
if (batchDepth === 0 && batched.length > 0) {
|
|
727
|
+
const changes = batched.splice(0, batched.length);
|
|
728
|
+
rebuild();
|
|
729
|
+
for (const [key, value, meta] of changes) emit(key, value, meta);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
514
732
|
}
|
|
515
733
|
function applyRemote(key, value, version, meta) {
|
|
516
734
|
if (!newer(version, versions[key])) return;
|
|
735
|
+
if (gate && !gate.accepts(key, value)) return;
|
|
517
736
|
versions[key] = version;
|
|
518
737
|
state[key] = freezeShared(value);
|
|
519
738
|
notify(key, value, meta);
|
|
520
739
|
}
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
740
|
+
const snapshotDelayMs = options.snapshotDelayMs ?? 40;
|
|
741
|
+
let snapshotTimer;
|
|
742
|
+
const written = () => Object.keys(versions).filter((k) => (versions[k]?.[0] ?? 0) > 0);
|
|
743
|
+
const coveredBy = (theirs) => written().every((key) => {
|
|
744
|
+
const mine = versions[key];
|
|
745
|
+
return mine !== void 0 && !newer(mine, theirs[key]);
|
|
746
|
+
});
|
|
747
|
+
const cancelSnapshot = () => {
|
|
748
|
+
clearTimeout(snapshotTimer);
|
|
749
|
+
snapshotTimer = void 0;
|
|
750
|
+
};
|
|
751
|
+
const scheduleSnapshot = () => {
|
|
752
|
+
if (written().length === 0) return;
|
|
753
|
+
if (snapshotTimer !== void 0) return;
|
|
754
|
+
snapshotTimer = setTimeout(() => {
|
|
755
|
+
snapshotTimer = void 0;
|
|
525
756
|
bus.post({
|
|
526
757
|
v: 1,
|
|
527
758
|
scope: "state",
|
|
@@ -531,22 +762,36 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
531
762
|
state: { ...state },
|
|
532
763
|
versions: { ...versions }
|
|
533
764
|
});
|
|
765
|
+
}, Math.random() * snapshotDelayMs);
|
|
766
|
+
};
|
|
767
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
768
|
+
if (wire.scope !== "state") return;
|
|
769
|
+
const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
|
|
770
|
+
if (wire.type === "hello") {
|
|
771
|
+
scheduleSnapshot();
|
|
534
772
|
return;
|
|
535
773
|
}
|
|
536
774
|
if (accept && !accept(meta)) return;
|
|
537
775
|
if (wire.type === "patch") {
|
|
538
776
|
if (typeof wire.key !== "string" || !isVersion(wire.version)) return;
|
|
539
777
|
applyRemote(wire.key, wire.value, wire.version, meta);
|
|
540
|
-
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (wire.type === "snapshot") {
|
|
541
781
|
const versions2 = wire.versions;
|
|
542
782
|
if (typeof versions2 !== "object" || versions2 === null) return;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
783
|
+
if (coveredBy(versions2)) cancelSnapshot();
|
|
784
|
+
batch(() => {
|
|
785
|
+
for (const k in wire.state) {
|
|
786
|
+
const version = versions2[k];
|
|
787
|
+
if (isVersion(version)) applyRemote(k, wire.state[k], version, meta);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
return;
|
|
547
791
|
}
|
|
548
792
|
});
|
|
549
793
|
function setKey(key, value) {
|
|
794
|
+
gate?.assert(key, value);
|
|
550
795
|
const version = [(versions[key]?.[0] ?? 0) + 1, clientId];
|
|
551
796
|
try {
|
|
552
797
|
bus.post({
|
|
@@ -578,29 +823,73 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
578
823
|
});
|
|
579
824
|
const persist = options.persist;
|
|
580
825
|
let flushPersist;
|
|
826
|
+
let settle = () => {
|
|
827
|
+
};
|
|
828
|
+
const hydrated = persist ? new Promise((resolve) => {
|
|
829
|
+
settle = resolve;
|
|
830
|
+
}) : Promise.resolve();
|
|
581
831
|
if (persist) {
|
|
582
|
-
const {
|
|
832
|
+
const {
|
|
833
|
+
adapter,
|
|
834
|
+
keys,
|
|
835
|
+
debounceMs = 100,
|
|
836
|
+
version: schemaVersion = 0,
|
|
837
|
+
migrate,
|
|
838
|
+
onRestoreError
|
|
839
|
+
} = persist;
|
|
583
840
|
const shouldPersist = (key) => !keys || keys.includes(key);
|
|
584
|
-
const
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
841
|
+
const refuse = (error) => {
|
|
842
|
+
if (onRestoreError) onRestoreError(error);
|
|
843
|
+
else if (process.env.NODE_ENV !== "production") {
|
|
844
|
+
devWarn(
|
|
845
|
+
`[use-everywhere] ${name}: persisted schema v${error.found} not restored, expected v${error.expected} (${error.reason}). https://rxova.org/guides/persistence/`
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
const reconcile = (saved2) => {
|
|
850
|
+
const found = saved2.schema ?? 0;
|
|
851
|
+
if (found === schemaVersion) return { state: saved2.state, migrated: false };
|
|
852
|
+
if (found > schemaVersion) {
|
|
853
|
+
refuse({ reason: "ahead", found, expected: schemaVersion });
|
|
854
|
+
return void 0;
|
|
855
|
+
}
|
|
856
|
+
if (!migrate) {
|
|
857
|
+
refuse({ reason: "no-migrate", found, expected: schemaVersion });
|
|
858
|
+
return void 0;
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
return { state: migrate(saved2.state, found), migrated: true };
|
|
862
|
+
} catch (cause) {
|
|
863
|
+
refuse({ reason: "migrate-threw", found, expected: schemaVersion, cause });
|
|
864
|
+
return void 0;
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
const hydrate = (raw) => {
|
|
868
|
+
const reconciled = raw && reconcile(raw);
|
|
869
|
+
if (!reconciled || !raw) {
|
|
870
|
+
settle();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const { state: restored, migrated } = reconciled;
|
|
874
|
+
for (const key in restored) {
|
|
875
|
+
const version = raw.versions[key] ?? (migrated ? [1, clientId] : void 0);
|
|
588
876
|
if (!version || !shouldPersist(key)) continue;
|
|
589
|
-
applyRemote(key,
|
|
877
|
+
applyRemote(key, restored[key], version, { clientId, kind: bus.kind, self: true });
|
|
590
878
|
bus.post({
|
|
591
879
|
v: 1,
|
|
592
880
|
scope: "state",
|
|
593
881
|
type: "patch",
|
|
594
882
|
key,
|
|
595
|
-
value:
|
|
883
|
+
value: restored[key],
|
|
596
884
|
version,
|
|
597
885
|
clientId,
|
|
598
886
|
kind: bus.kind
|
|
599
887
|
});
|
|
600
888
|
}
|
|
889
|
+
settle();
|
|
601
890
|
};
|
|
602
891
|
const collect = () => {
|
|
603
|
-
const out = { v: 1, state: {}, versions: {} };
|
|
892
|
+
const out = { v: 1, schema: schemaVersion, state: {}, versions: {} };
|
|
604
893
|
for (const key in versions) {
|
|
605
894
|
const version = versions[key];
|
|
606
895
|
if (!version || version[0] === 0 || !shouldPersist(key)) continue;
|
|
@@ -640,6 +929,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
640
929
|
let storeClosed = false;
|
|
641
930
|
return {
|
|
642
931
|
clientId,
|
|
932
|
+
hydrated,
|
|
643
933
|
state: proxy,
|
|
644
934
|
getSnapshot: () => snapshot,
|
|
645
935
|
getVersions: () => versionsSnapshot,
|
|
@@ -647,6 +937,9 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
647
937
|
const next = typeof value === "function" ? value(state[key]) : value;
|
|
648
938
|
setKey(key, next);
|
|
649
939
|
},
|
|
940
|
+
transaction(fn) {
|
|
941
|
+
return batch(fn);
|
|
942
|
+
},
|
|
650
943
|
subscribe(fn) {
|
|
651
944
|
listeners.add(fn);
|
|
652
945
|
return () => listeners.delete(fn);
|
|
@@ -678,6 +971,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
678
971
|
if (live > 0) liveStores.set(name, live);
|
|
679
972
|
else liveStores.delete(name);
|
|
680
973
|
}
|
|
974
|
+
cancelSnapshot();
|
|
681
975
|
if (hasWindow) removeEventListener("pageshow", onPageShow);
|
|
682
976
|
if (flushPersist) {
|
|
683
977
|
flushPersist();
|
|
@@ -693,83 +987,6 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
693
987
|
};
|
|
694
988
|
}
|
|
695
989
|
|
|
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
990
|
// src/leader-web-locks.ts
|
|
774
991
|
var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
|
|
775
992
|
function createWebLocksLeader(name, options, locks) {
|
|
@@ -1054,6 +1271,246 @@ function createHeartbeatLeader(name, options) {
|
|
|
1054
1271
|
};
|
|
1055
1272
|
}
|
|
1056
1273
|
|
|
1274
|
+
// src/shared-reducer.ts
|
|
1275
|
+
function createSharedReducer(name, reducer, initial, options = {}) {
|
|
1276
|
+
const key = options.key ?? "default";
|
|
1277
|
+
const bus = getBus(name, options);
|
|
1278
|
+
const clientId = bus.clientId;
|
|
1279
|
+
const leader = options.leader ?? createLeader(name, {
|
|
1280
|
+
...options.leaderOptions,
|
|
1281
|
+
// Spread conditionally: `exactOptionalPropertyTypes` distinguishes an
|
|
1282
|
+
// absent transport from one explicitly set to undefined, and the leader
|
|
1283
|
+
// must default its own rather than be handed a hole.
|
|
1284
|
+
...options.transport ? { transport: options.transport } : {}
|
|
1285
|
+
});
|
|
1286
|
+
const ownsLeader = !options.leader;
|
|
1287
|
+
let committed = initial;
|
|
1288
|
+
let seq = 0;
|
|
1289
|
+
let pending = [];
|
|
1290
|
+
const early = /* @__PURE__ */ new Map();
|
|
1291
|
+
let lastIssued = 0;
|
|
1292
|
+
let view = committed;
|
|
1293
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1294
|
+
let closed = false;
|
|
1295
|
+
const notify = () => {
|
|
1296
|
+
const next = pending.reduce((state, op) => reducer(state, op.action), committed);
|
|
1297
|
+
if (Object.is(next, view)) return;
|
|
1298
|
+
view = next;
|
|
1299
|
+
for (const fn of listeners) fn();
|
|
1300
|
+
};
|
|
1301
|
+
const askForSnapshot = () => bus.post({ v: 1, scope: "op", type: "hello", key, clientId, kind: bus.kind });
|
|
1302
|
+
const receiveCommit = (at, action, opId) => {
|
|
1303
|
+
if (at <= seq) return;
|
|
1304
|
+
if (at > seq + 1) {
|
|
1305
|
+
early.set(at, { action, opId });
|
|
1306
|
+
askForSnapshot();
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
applyCommit(at, action, opId);
|
|
1310
|
+
notify();
|
|
1311
|
+
};
|
|
1312
|
+
const sequence = (action, opId) => {
|
|
1313
|
+
lastIssued = Math.max(lastIssued, seq) + 1;
|
|
1314
|
+
const at = lastIssued;
|
|
1315
|
+
bus.post({
|
|
1316
|
+
v: 1,
|
|
1317
|
+
scope: "op",
|
|
1318
|
+
type: "commit",
|
|
1319
|
+
key,
|
|
1320
|
+
action,
|
|
1321
|
+
opId,
|
|
1322
|
+
seq: at,
|
|
1323
|
+
clientId,
|
|
1324
|
+
kind: bus.kind
|
|
1325
|
+
});
|
|
1326
|
+
receiveCommit(at, action, opId);
|
|
1327
|
+
};
|
|
1328
|
+
const applyCommit = (at, action, opId) => {
|
|
1329
|
+
committed = reducer(committed, action);
|
|
1330
|
+
seq = at;
|
|
1331
|
+
pending = pending.filter((op) => op.opId !== opId);
|
|
1332
|
+
const next = early.get(seq + 1);
|
|
1333
|
+
if (next) {
|
|
1334
|
+
early.delete(seq + 1);
|
|
1335
|
+
applyCommit(seq + 1, next.action, next.opId);
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
1339
|
+
if (wire.scope !== "op" || wire.key !== key) return;
|
|
1340
|
+
if (wire.type === "propose") {
|
|
1341
|
+
if (!leader.getSnapshot().isLeader) return;
|
|
1342
|
+
sequence(wire.action, wire.opId);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (wire.type === "commit") {
|
|
1346
|
+
receiveCommit(wire.seq, wire.action, wire.opId);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (wire.type === "hello") {
|
|
1350
|
+
if (seq === 0) return;
|
|
1351
|
+
bus.post({
|
|
1352
|
+
v: 1,
|
|
1353
|
+
scope: "op",
|
|
1354
|
+
type: "snapshot",
|
|
1355
|
+
key,
|
|
1356
|
+
state: committed,
|
|
1357
|
+
seq,
|
|
1358
|
+
clientId,
|
|
1359
|
+
kind: bus.kind
|
|
1360
|
+
});
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
if (wire.type === "snapshot") {
|
|
1364
|
+
if (wire.seq <= seq) return;
|
|
1365
|
+
committed = wire.state;
|
|
1366
|
+
seq = wire.seq;
|
|
1367
|
+
for (const at of [...early.keys()]) if (at <= seq) early.delete(at);
|
|
1368
|
+
const next = early.get(seq + 1);
|
|
1369
|
+
if (next) {
|
|
1370
|
+
early.delete(seq + 1);
|
|
1371
|
+
applyCommit(seq + 1, next.action, next.opId);
|
|
1372
|
+
}
|
|
1373
|
+
notify();
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
const dispatch = (action) => {
|
|
1378
|
+
if (closed) return;
|
|
1379
|
+
const opId = newMsgId();
|
|
1380
|
+
pending.push({ opId, action });
|
|
1381
|
+
notify();
|
|
1382
|
+
if (leader.getSnapshot().isLeader) sequence(action, opId);
|
|
1383
|
+
else
|
|
1384
|
+
bus.post({ v: 1, scope: "op", type: "propose", key, action, opId, clientId, kind: bus.kind });
|
|
1385
|
+
};
|
|
1386
|
+
askForSnapshot();
|
|
1387
|
+
return {
|
|
1388
|
+
clientId,
|
|
1389
|
+
getSnapshot: () => view,
|
|
1390
|
+
dispatch,
|
|
1391
|
+
subscribe(fn) {
|
|
1392
|
+
listeners.add(fn);
|
|
1393
|
+
return () => listeners.delete(fn);
|
|
1394
|
+
},
|
|
1395
|
+
pendingCount: () => pending.length,
|
|
1396
|
+
close() {
|
|
1397
|
+
if (closed) return;
|
|
1398
|
+
closed = true;
|
|
1399
|
+
unsubscribe();
|
|
1400
|
+
listeners.clear();
|
|
1401
|
+
if (ownsLeader) leader.close();
|
|
1402
|
+
bus.release();
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// src/presence.ts
|
|
1408
|
+
function createPresence(name, options = {}) {
|
|
1409
|
+
const pruneAfterMs = options.pruneAfterMs ?? 5e3;
|
|
1410
|
+
const probeGraceMs = options.probeGraceMs ?? 1e3;
|
|
1411
|
+
const bus = getBus(name, options);
|
|
1412
|
+
const peers = /* @__PURE__ */ new Map();
|
|
1413
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1414
|
+
let snapshot = [];
|
|
1415
|
+
let metadata = options.metadata;
|
|
1416
|
+
const same = (a, b) => {
|
|
1417
|
+
if (Object.is(a, b)) return true;
|
|
1418
|
+
try {
|
|
1419
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1420
|
+
} catch {
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
const self = () => ({
|
|
1425
|
+
id: bus.clientId,
|
|
1426
|
+
kind: bus.kind,
|
|
1427
|
+
lastSeen: Date.now(),
|
|
1428
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1429
|
+
});
|
|
1430
|
+
function notify() {
|
|
1431
|
+
const roster = [...peers.values()];
|
|
1432
|
+
snapshot = Object.freeze(options.includeSelf ? [self(), ...roster] : roster);
|
|
1433
|
+
for (const fn of listeners) fn();
|
|
1434
|
+
}
|
|
1435
|
+
const announce2 = () => bus.post({
|
|
1436
|
+
v: 1,
|
|
1437
|
+
scope: "presence",
|
|
1438
|
+
type: "hello",
|
|
1439
|
+
clientId: bus.clientId,
|
|
1440
|
+
kind: bus.kind,
|
|
1441
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1442
|
+
});
|
|
1443
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
1444
|
+
if (wire.clientId === bus.clientId) return;
|
|
1445
|
+
if (wire.scope === "presence" && wire.type === "bye") {
|
|
1446
|
+
if (peers.delete(wire.clientId)) notify();
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
const existing = peers.get(wire.clientId);
|
|
1450
|
+
const announced = wire.scope === "presence" ? wire.metadata : void 0;
|
|
1451
|
+
const next = announced === void 0 ? existing?.metadata : announced;
|
|
1452
|
+
peers.set(wire.clientId, {
|
|
1453
|
+
id: wire.clientId,
|
|
1454
|
+
kind: wire.kind,
|
|
1455
|
+
lastSeen: Date.now(),
|
|
1456
|
+
...next === void 0 ? {} : { metadata: next }
|
|
1457
|
+
});
|
|
1458
|
+
if (!existing || !same(existing.metadata, next)) notify();
|
|
1459
|
+
});
|
|
1460
|
+
announce2();
|
|
1461
|
+
if (options.includeSelf) notify();
|
|
1462
|
+
const probed = /* @__PURE__ */ new Map();
|
|
1463
|
+
const prune = setInterval(
|
|
1464
|
+
() => {
|
|
1465
|
+
const now = Date.now();
|
|
1466
|
+
const silentSince = now - pruneAfterMs;
|
|
1467
|
+
let changed = false;
|
|
1468
|
+
let needProbe = false;
|
|
1469
|
+
for (const [id, peer] of peers) {
|
|
1470
|
+
if (peer.lastSeen >= silentSince) {
|
|
1471
|
+
probed.delete(id);
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
const askedAt = probed.get(id);
|
|
1475
|
+
if (askedAt === void 0) {
|
|
1476
|
+
probed.set(id, now);
|
|
1477
|
+
needProbe = true;
|
|
1478
|
+
} else if (now - askedAt >= probeGraceMs) {
|
|
1479
|
+
peers.delete(id);
|
|
1480
|
+
probed.delete(id);
|
|
1481
|
+
changed = true;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (needProbe) announce2();
|
|
1485
|
+
if (changed) notify();
|
|
1486
|
+
},
|
|
1487
|
+
Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
|
|
1488
|
+
);
|
|
1489
|
+
let closed = false;
|
|
1490
|
+
return {
|
|
1491
|
+
clientId: bus.clientId,
|
|
1492
|
+
getPeers: () => snapshot,
|
|
1493
|
+
setMetadata(next) {
|
|
1494
|
+
if (same(metadata, next)) return;
|
|
1495
|
+
metadata = next;
|
|
1496
|
+
announce2();
|
|
1497
|
+
notify();
|
|
1498
|
+
},
|
|
1499
|
+
subscribe(fn) {
|
|
1500
|
+
listeners.add(fn);
|
|
1501
|
+
return () => listeners.delete(fn);
|
|
1502
|
+
},
|
|
1503
|
+
close() {
|
|
1504
|
+
if (closed) return;
|
|
1505
|
+
closed = true;
|
|
1506
|
+
clearInterval(prune);
|
|
1507
|
+
unsubscribe();
|
|
1508
|
+
listeners.clear();
|
|
1509
|
+
bus.release();
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1057
1514
|
// src/persist-web-storage.ts
|
|
1058
1515
|
function isPersisted(value) {
|
|
1059
1516
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -1061,6 +1518,7 @@ function isPersisted(value) {
|
|
|
1061
1518
|
return candidate.v === 1 && typeof candidate.state === "object" && typeof candidate.versions === "object";
|
|
1062
1519
|
}
|
|
1063
1520
|
function webStorageAdapter(storage, key, options = {}) {
|
|
1521
|
+
const serializer = options.serializer ?? jsonSerializer;
|
|
1064
1522
|
const report = (error, operation) => {
|
|
1065
1523
|
try {
|
|
1066
1524
|
options.onError?.(error, operation);
|
|
@@ -1080,7 +1538,7 @@ function webStorageAdapter(storage, key, options = {}) {
|
|
|
1080
1538
|
try {
|
|
1081
1539
|
const raw = resolve("read")?.getItem(key);
|
|
1082
1540
|
if (!raw) return void 0;
|
|
1083
|
-
const parsed =
|
|
1541
|
+
const parsed = serializer.parse(raw);
|
|
1084
1542
|
return isPersisted(parsed) ? parsed : void 0;
|
|
1085
1543
|
} catch (error) {
|
|
1086
1544
|
report(error, "read");
|
|
@@ -1089,7 +1547,7 @@ function webStorageAdapter(storage, key, options = {}) {
|
|
|
1089
1547
|
},
|
|
1090
1548
|
write(snapshot) {
|
|
1091
1549
|
try {
|
|
1092
|
-
resolve("write")?.setItem(key,
|
|
1550
|
+
resolve("write")?.setItem(key, serializer.stringify(snapshot));
|
|
1093
1551
|
} catch (error) {
|
|
1094
1552
|
report(error, "write");
|
|
1095
1553
|
}
|
|
@@ -1110,6 +1568,58 @@ function sessionStorageAdapter(key, options) {
|
|
|
1110
1568
|
return webStorageAdapter(() => globalThis.sessionStorage, key, options);
|
|
1111
1569
|
}
|
|
1112
1570
|
|
|
1571
|
+
// src/persist-indexeddb.ts
|
|
1572
|
+
var STORE = "state";
|
|
1573
|
+
var request = (req) => new Promise((resolve, reject) => {
|
|
1574
|
+
req.onsuccess = () => resolve(req.result);
|
|
1575
|
+
req.onerror = () => reject(req.error);
|
|
1576
|
+
});
|
|
1577
|
+
function indexedDbAdapter(key, options = {}) {
|
|
1578
|
+
const database = options.database ?? "use-everywhere";
|
|
1579
|
+
let open;
|
|
1580
|
+
const report = (error, operation) => {
|
|
1581
|
+
try {
|
|
1582
|
+
options.onError?.(error, operation);
|
|
1583
|
+
} catch {
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
const db = () => open ?? (open = new Promise((resolve, reject) => {
|
|
1587
|
+
const req = indexedDB.open(database, 1);
|
|
1588
|
+
req.onupgradeneeded = () => req.result.createObjectStore(STORE);
|
|
1589
|
+
req.onsuccess = () => resolve(req.result);
|
|
1590
|
+
req.onerror = () => reject(req.error);
|
|
1591
|
+
req.onblocked = () => reject(new Error("use-everywhere: IndexedDB upgrade blocked"));
|
|
1592
|
+
}));
|
|
1593
|
+
const transact = async (mode, run) => {
|
|
1594
|
+
const connection = await db();
|
|
1595
|
+
return request(run(connection.transaction(STORE, mode).objectStore(STORE)));
|
|
1596
|
+
};
|
|
1597
|
+
return {
|
|
1598
|
+
async read() {
|
|
1599
|
+
try {
|
|
1600
|
+
return await transact("readonly", (store) => store.get(key));
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
report(error, "read");
|
|
1603
|
+
return void 0;
|
|
1604
|
+
}
|
|
1605
|
+
},
|
|
1606
|
+
async write(snapshot) {
|
|
1607
|
+
try {
|
|
1608
|
+
await transact("readwrite", (store) => store.put(snapshot, key));
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
report(error, "write");
|
|
1611
|
+
}
|
|
1612
|
+
},
|
|
1613
|
+
async remove() {
|
|
1614
|
+
try {
|
|
1615
|
+
await transact("readwrite", (store) => store.delete(key));
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
report(error, "remove");
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1113
1623
|
// src/errors/handshake-timeout-error.ts
|
|
1114
1624
|
var HandshakeTimeoutError = class extends Error {
|
|
1115
1625
|
constructor(message = "handshake with the other window timed out") {
|
|
@@ -1344,6 +1854,25 @@ function connectToOpener(options) {
|
|
|
1344
1854
|
}
|
|
1345
1855
|
};
|
|
1346
1856
|
}
|
|
1857
|
+
|
|
1858
|
+
// src/namespace.ts
|
|
1859
|
+
var SEPARATOR = ":";
|
|
1860
|
+
function createNamespace(namespace) {
|
|
1861
|
+
if (!namespace) {
|
|
1862
|
+
throw new TypeError(
|
|
1863
|
+
"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."
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
const busName = (name = DEFAULT_NAME) => `${namespace}${SEPARATOR}${name}`;
|
|
1867
|
+
return {
|
|
1868
|
+
name: namespace,
|
|
1869
|
+
busName,
|
|
1870
|
+
createSharedStore: (name, initial, options) => createSharedStore(busName(name), initial, options),
|
|
1871
|
+
createChannel: (name, options) => createChannel(busName(name), options),
|
|
1872
|
+
createPresence: (name, options) => createPresence(busName(name), options),
|
|
1873
|
+
createLeader: (name, options) => createLeader(busName(name), options)
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1347
1876
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1348
1877
|
0 && (module.exports = {
|
|
1349
1878
|
BroadcastChannelTransport,
|
|
@@ -1352,18 +1881,24 @@ function connectToOpener(options) {
|
|
|
1352
1881
|
HandshakeTimeoutError,
|
|
1353
1882
|
NoopTransport,
|
|
1354
1883
|
StorageTransport,
|
|
1884
|
+
WIRE_VERSION,
|
|
1355
1885
|
WindowClosedError,
|
|
1356
1886
|
connectToOpener,
|
|
1357
1887
|
createChannel,
|
|
1358
1888
|
createLeader,
|
|
1889
|
+
createNamespace,
|
|
1359
1890
|
createPresence,
|
|
1891
|
+
createSharedReducer,
|
|
1360
1892
|
createSharedStore,
|
|
1361
1893
|
defaultTransport,
|
|
1362
1894
|
enableDebug,
|
|
1363
1895
|
getBusNames,
|
|
1364
1896
|
getTransportKind,
|
|
1897
|
+
getWireSkew,
|
|
1898
|
+
indexedDbAdapter,
|
|
1365
1899
|
isBroadcastChannelAvailable,
|
|
1366
1900
|
isStorageEventAvailable,
|
|
1901
|
+
jsonSerializer,
|
|
1367
1902
|
localStorageAdapter,
|
|
1368
1903
|
newer,
|
|
1369
1904
|
observeBus,
|