@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.js
CHANGED
|
@@ -60,9 +60,11 @@ function randomHex(bytes) {
|
|
|
60
60
|
for (const b of buf) out2 += b.toString(16).padStart(2, "0");
|
|
61
61
|
return out2;
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
if (process.env.NODE_ENV !== "production") {
|
|
64
|
+
devWarn(
|
|
65
|
+
"[use-everywhere] crypto.getRandomValues is unavailable; falling back to Math.random for ids. Cross-origin window nonces are not cryptographically strong in this environment."
|
|
66
|
+
);
|
|
67
|
+
}
|
|
66
68
|
let out = "";
|
|
67
69
|
while (out.length < bytes * 2)
|
|
68
70
|
out += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
|
|
@@ -79,15 +81,18 @@ function newMsgId() {
|
|
|
79
81
|
var PROTOCOL = 1;
|
|
80
82
|
var TABLE = /* @__PURE__ */ Symbol.for(`use-everywhere.rendezvous.${PROTOCOL}`);
|
|
81
83
|
var CENSUS = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.census");
|
|
84
|
+
var SKEW = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.skew");
|
|
82
85
|
function announce() {
|
|
83
86
|
const g = globalThis;
|
|
84
87
|
const census = g[CENSUS] ?? (g[CENSUS] = { protocols: [] });
|
|
85
88
|
if (census.protocols.includes(PROTOCOL)) return;
|
|
86
89
|
census.protocols.push(PROTOCOL);
|
|
87
90
|
if (census.protocols.length > 1) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
if (process.env.NODE_ENV !== "production") {
|
|
92
|
+
devWarn(
|
|
93
|
+
`[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.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
function busTable() {
|
|
@@ -95,6 +100,10 @@ function busTable() {
|
|
|
95
100
|
const g = globalThis;
|
|
96
101
|
return g[TABLE] ?? (g[TABLE] = /* @__PURE__ */ new Map());
|
|
97
102
|
}
|
|
103
|
+
function skewLedger() {
|
|
104
|
+
const g = globalThis;
|
|
105
|
+
return g[SKEW] ?? (g[SKEW] = /* @__PURE__ */ new Map());
|
|
106
|
+
}
|
|
98
107
|
|
|
99
108
|
// src/transport/broadcast-channel-transport.ts
|
|
100
109
|
var BroadcastChannelTransport = class {
|
|
@@ -134,9 +143,38 @@ var NoopTransport = class {
|
|
|
134
143
|
}
|
|
135
144
|
};
|
|
136
145
|
|
|
146
|
+
// src/serializer.ts
|
|
147
|
+
function lossyType(raw, value) {
|
|
148
|
+
if (value === void 0) return raw === void 0 ? "undefined" : "undefined after toJSON";
|
|
149
|
+
if (raw instanceof Date) return "Date";
|
|
150
|
+
if (raw instanceof Map) return "Map";
|
|
151
|
+
if (raw instanceof Set) return "Set";
|
|
152
|
+
if (raw instanceof RegExp) return "RegExp";
|
|
153
|
+
if (ArrayBuffer.isView(raw)) return "TypedArray";
|
|
154
|
+
const type = typeof raw;
|
|
155
|
+
return type === "function" || type === "symbol" ? type : void 0;
|
|
156
|
+
}
|
|
157
|
+
var jsonSerializer = {
|
|
158
|
+
stringify(value) {
|
|
159
|
+
if (value === void 0) {
|
|
160
|
+
throw new TypeError("use-everywhere: cannot serialize undefined");
|
|
161
|
+
}
|
|
162
|
+
return JSON.stringify(value, function replacer(key, forJson) {
|
|
163
|
+
const type = lossyType(this[key], forJson);
|
|
164
|
+
if (type) {
|
|
165
|
+
throw new TypeError(
|
|
166
|
+
`use-everywhere: ${key ? `"${key}" is ` : ""}${type}, which JSON cannot round-trip. https://rxova.org/guides/serialization/`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return forJson;
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
parse: (text) => JSON.parse(text)
|
|
173
|
+
};
|
|
174
|
+
|
|
137
175
|
// src/transport/storage-transport.ts
|
|
138
176
|
var StorageTransport = class {
|
|
139
|
-
constructor(name, storage = globalThis.localStorage) {
|
|
177
|
+
constructor(name, storage = globalThis.localStorage, serializer = jsonSerializer) {
|
|
140
178
|
this.kind = "storage";
|
|
141
179
|
this.listeners = /* @__PURE__ */ new Set();
|
|
142
180
|
this.seq = 0;
|
|
@@ -145,13 +183,14 @@ var StorageTransport = class {
|
|
|
145
183
|
"[use-everywhere] StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
|
|
146
184
|
);
|
|
147
185
|
}
|
|
186
|
+
this.serializer = serializer;
|
|
148
187
|
this.key = `use-everywhere:bus:${name}`;
|
|
149
188
|
this.storage = storage;
|
|
150
189
|
this.onStorage = (event) => {
|
|
151
190
|
if (event.key !== this.key || event.newValue === null) return;
|
|
152
191
|
let payload;
|
|
153
192
|
try {
|
|
154
|
-
payload =
|
|
193
|
+
payload = this.serializer.parse(event.newValue);
|
|
155
194
|
} catch {
|
|
156
195
|
return;
|
|
157
196
|
}
|
|
@@ -160,7 +199,7 @@ var StorageTransport = class {
|
|
|
160
199
|
addEventListener("storage", this.onStorage);
|
|
161
200
|
}
|
|
162
201
|
post(data) {
|
|
163
|
-
const envelope =
|
|
202
|
+
const envelope = this.serializer.stringify({ seq: this.seq++, data });
|
|
164
203
|
try {
|
|
165
204
|
this.storage.setItem(this.key, envelope);
|
|
166
205
|
this.storage.removeItem(this.key);
|
|
@@ -176,12 +215,6 @@ var StorageTransport = class {
|
|
|
176
215
|
removeEventListener("storage", this.onStorage);
|
|
177
216
|
}
|
|
178
217
|
};
|
|
179
|
-
function rejectUnrepresentable(_key, value) {
|
|
180
|
-
if (typeof value === "function" || typeof value === "symbol") {
|
|
181
|
-
throw new TypeError(`${typeof value} values cannot be shared`);
|
|
182
|
-
}
|
|
183
|
-
return value;
|
|
184
|
-
}
|
|
185
218
|
|
|
186
219
|
// src/transport/default-transport.ts
|
|
187
220
|
function isBroadcastChannelAvailable() {
|
|
@@ -203,27 +236,58 @@ function isStorageEventAvailable() {
|
|
|
203
236
|
function defaultTransport(name) {
|
|
204
237
|
if (isBroadcastChannelAvailable()) return new BroadcastChannelTransport(name);
|
|
205
238
|
if (isStorageEventAvailable()) {
|
|
239
|
+
if (process.env.NODE_ENV !== "production") {
|
|
240
|
+
devWarn(
|
|
241
|
+
"[use-everywhere] no BroadcastChannel; using the storage-event fallback. Values serialise as JSON, not structured clone \u2014 keep them JSON-shaped."
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return new StorageTransport(name);
|
|
245
|
+
}
|
|
246
|
+
if (process.env.NODE_ENV !== "production") {
|
|
206
247
|
devWarn(
|
|
207
|
-
"[use-everywhere] no BroadcastChannel
|
|
248
|
+
"[use-everywhere] no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
208
249
|
);
|
|
209
|
-
return new StorageTransport(name);
|
|
210
250
|
}
|
|
211
|
-
devWarn(
|
|
212
|
-
"[use-everywhere] no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
213
|
-
);
|
|
214
251
|
return new NoopTransport();
|
|
215
252
|
}
|
|
216
253
|
|
|
217
|
-
// src/
|
|
254
|
+
// src/wire.ts
|
|
255
|
+
var WIRE_VERSION = 1;
|
|
256
|
+
function hasEnvelopeShape(wire) {
|
|
257
|
+
return typeof wire.scope === "string" && typeof wire.type === "string" && typeof wire.clientId === "string";
|
|
258
|
+
}
|
|
218
259
|
function isBusWire(data) {
|
|
219
260
|
if (typeof data !== "object" || data === null) return false;
|
|
220
261
|
const wire = data;
|
|
221
|
-
return wire.v ===
|
|
262
|
+
return wire.v === WIRE_VERSION && hasEnvelopeShape(wire);
|
|
263
|
+
}
|
|
264
|
+
function foreignWireVersion(data) {
|
|
265
|
+
if (typeof data !== "object" || data === null) return null;
|
|
266
|
+
const wire = data;
|
|
267
|
+
if (typeof wire.v !== "number" || wire.v === WIRE_VERSION) return null;
|
|
268
|
+
return hasEnvelopeShape(wire) ? wire.v : null;
|
|
269
|
+
}
|
|
270
|
+
function recordSkew(name, version) {
|
|
271
|
+
const seen = skewLedger();
|
|
272
|
+
const versions = seen.get(name) ?? /* @__PURE__ */ new Set();
|
|
273
|
+
seen.set(name, versions);
|
|
274
|
+
if (versions.has(version)) return;
|
|
275
|
+
versions.add(version);
|
|
276
|
+
if (process.env.NODE_ENV !== "production") {
|
|
277
|
+
devWarn(
|
|
278
|
+
`[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/`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function getWireSkew(name) {
|
|
283
|
+
return [...skewLedger().get(name) ?? []].sort((a, b) => a - b);
|
|
222
284
|
}
|
|
285
|
+
|
|
286
|
+
// src/bus.ts
|
|
223
287
|
function defaultKind() {
|
|
224
288
|
return typeof document === "undefined" ? "worker" : "tab";
|
|
225
289
|
}
|
|
226
|
-
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event"]);
|
|
290
|
+
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event", "op"]);
|
|
227
291
|
function createBusCore(name, options, onShutdown) {
|
|
228
292
|
const transport = (options.transport ?? defaultTransport)(name);
|
|
229
293
|
const clientId = newClientId();
|
|
@@ -244,6 +308,11 @@ function createBusCore(name, options, onShutdown) {
|
|
|
244
308
|
if (SHARED_WITHIN_CLIENT.has(wire.scope)) deliver(wire, from);
|
|
245
309
|
};
|
|
246
310
|
const unsubscribe = transport.subscribe((data) => {
|
|
311
|
+
const foreign = foreignWireVersion(data);
|
|
312
|
+
if (foreign !== null) {
|
|
313
|
+
recordSkew(name, foreign);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
247
316
|
if (!isBusWire(data)) return;
|
|
248
317
|
if (data.clientId === clientId) return;
|
|
249
318
|
emitBusEvent(name, "in", data);
|
|
@@ -336,57 +405,172 @@ function getBus(name, options = {}) {
|
|
|
336
405
|
table.set(name, core);
|
|
337
406
|
} else {
|
|
338
407
|
if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
408
|
+
if (process.env.NODE_ENV !== "production") {
|
|
409
|
+
devWarn(
|
|
410
|
+
`[use-everywhere] bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
342
413
|
}
|
|
343
414
|
if (options.kind !== void 0 && options.kind !== core.kind) {
|
|
344
|
-
|
|
415
|
+
if (process.env.NODE_ENV !== "production") {
|
|
416
|
+
devWarn(
|
|
417
|
+
`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
345
420
|
}
|
|
346
421
|
}
|
|
347
422
|
return core.connect();
|
|
348
423
|
}
|
|
349
424
|
|
|
425
|
+
// src/schema.ts
|
|
426
|
+
function validate(schema, payload) {
|
|
427
|
+
const props = schema["~standard"];
|
|
428
|
+
const result = props.validate(payload);
|
|
429
|
+
if (typeof result.then === "function") {
|
|
430
|
+
return { ok: false, issues: [`the "${props.vendor}" schema validates asynchronously`] };
|
|
431
|
+
}
|
|
432
|
+
const sync = result;
|
|
433
|
+
if (sync.issues) return { ok: false, issues: sync.issues.map((issue) => issue.message) };
|
|
434
|
+
return { ok: true, value: sync.value };
|
|
435
|
+
}
|
|
436
|
+
function createGate(name, schemas, onInvalid) {
|
|
437
|
+
if (!schemas) return void 0;
|
|
438
|
+
const check = (key, payload, direction) => {
|
|
439
|
+
const schema = schemas[key];
|
|
440
|
+
if (!schema) return void 0;
|
|
441
|
+
const result = validate(schema, payload);
|
|
442
|
+
if (result.ok) return void 0;
|
|
443
|
+
const info = { name, key, direction, payload, issues: result.issues };
|
|
444
|
+
if (onInvalid) onInvalid(info);
|
|
445
|
+
else if (process.env.NODE_ENV !== "production") {
|
|
446
|
+
devWarn(
|
|
447
|
+
`[use-everywhere] ${name}/${key}: ${direction}bound payload rejected by its schema \u2014 ${result.issues.join("; ")}. https://rxova.org/guides/validating-payloads/`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return result.issues;
|
|
451
|
+
};
|
|
452
|
+
return {
|
|
453
|
+
accepts: (key, payload) => !check(key, payload, "in"),
|
|
454
|
+
assert(key, payload) {
|
|
455
|
+
const issues = check(key, payload, "out");
|
|
456
|
+
if (issues) {
|
|
457
|
+
throw new TypeError(
|
|
458
|
+
`use-everywhere: ${name}/${key} \u2014 the value does not match its schema, so nothing was sent or applied. ${issues.join("; ")}`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
350
465
|
// src/channel.ts
|
|
351
466
|
function createChannel(name, options = {}) {
|
|
352
467
|
const bus = getBus(name, options);
|
|
353
468
|
const handlers = /* @__PURE__ */ new Map();
|
|
469
|
+
const responders = /* @__PURE__ */ new Map();
|
|
470
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
471
|
+
const gate = createGate(name, options.schema, options.onInvalid);
|
|
472
|
+
const deliver = (type, payload, meta) => {
|
|
473
|
+
const set = handlers.get(type);
|
|
474
|
+
if (!set) return;
|
|
475
|
+
for (const fn of [...set]) fn(payload, meta);
|
|
476
|
+
};
|
|
354
477
|
const unsubscribe = bus.subscribe((wire) => {
|
|
355
478
|
if (wire.scope !== "event") return;
|
|
356
|
-
const set = handlers.get(wire.type);
|
|
357
|
-
if (!set) return;
|
|
358
479
|
const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
480
|
+
if (wire.replyTo !== void 0) {
|
|
481
|
+
const settle = waiting.get(wire.replyTo);
|
|
482
|
+
if (!settle) return;
|
|
483
|
+
waiting.delete(wire.replyTo);
|
|
484
|
+
settle(wire.payload);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const responder = responders.get(wire.type);
|
|
488
|
+
if (responder) {
|
|
366
489
|
bus.post({
|
|
367
490
|
v: 1,
|
|
368
491
|
scope: "event",
|
|
369
|
-
type,
|
|
370
|
-
payload,
|
|
492
|
+
type: wire.type,
|
|
493
|
+
payload: responder(wire.payload, meta),
|
|
371
494
|
clientId: bus.clientId,
|
|
372
495
|
kind: bus.kind,
|
|
373
|
-
msgId: newMsgId()
|
|
496
|
+
msgId: newMsgId(),
|
|
497
|
+
replyTo: wire.msgId
|
|
374
498
|
});
|
|
499
|
+
}
|
|
500
|
+
if (!handlers.has(wire.type)) return;
|
|
501
|
+
if (gate && !gate.accepts(wire.type, wire.payload)) return;
|
|
502
|
+
deliver(wire.type, wire.payload, meta);
|
|
503
|
+
});
|
|
504
|
+
let closed = false;
|
|
505
|
+
const send = (type, payload, msgId) => {
|
|
506
|
+
gate?.assert(type, payload);
|
|
507
|
+
bus.post({
|
|
508
|
+
v: 1,
|
|
509
|
+
scope: "event",
|
|
510
|
+
type,
|
|
511
|
+
payload,
|
|
512
|
+
clientId: bus.clientId,
|
|
513
|
+
kind: bus.kind,
|
|
514
|
+
msgId
|
|
515
|
+
});
|
|
516
|
+
};
|
|
517
|
+
return {
|
|
518
|
+
name,
|
|
519
|
+
clientId: bus.clientId,
|
|
520
|
+
post(type, payload, options2) {
|
|
521
|
+
send(type, payload, newMsgId());
|
|
522
|
+
if (options2?.echo) {
|
|
523
|
+
deliver(type, payload, { clientId: bus.clientId, kind: bus.kind, self: true });
|
|
524
|
+
}
|
|
375
525
|
},
|
|
376
|
-
on(type, handler) {
|
|
526
|
+
on(type, handler, options2) {
|
|
377
527
|
let set = handlers.get(type);
|
|
378
528
|
if (!set) {
|
|
379
529
|
set = /* @__PURE__ */ new Set();
|
|
380
530
|
handlers.set(type, set);
|
|
381
531
|
}
|
|
382
|
-
|
|
383
|
-
|
|
532
|
+
const entry = options2?.once ? (payload, meta) => {
|
|
533
|
+
set.delete(entry);
|
|
534
|
+
handler(payload, meta);
|
|
535
|
+
} : handler;
|
|
536
|
+
set.add(entry);
|
|
537
|
+
return () => set.delete(entry);
|
|
538
|
+
},
|
|
539
|
+
ask(type, payload, options2) {
|
|
540
|
+
const msgId = newMsgId();
|
|
541
|
+
return new Promise((resolve, reject) => {
|
|
542
|
+
const timer = setTimeout(() => {
|
|
543
|
+
waiting.delete(msgId);
|
|
544
|
+
reject(
|
|
545
|
+
new Error(
|
|
546
|
+
`use-everywhere: nobody answered "${type}" on "${name}" within ${String(options2?.timeoutMs ?? 5e3)}ms`
|
|
547
|
+
)
|
|
548
|
+
);
|
|
549
|
+
}, options2?.timeoutMs ?? 5e3);
|
|
550
|
+
waiting.set(msgId, (value) => {
|
|
551
|
+
clearTimeout(timer);
|
|
552
|
+
resolve(value);
|
|
553
|
+
});
|
|
554
|
+
try {
|
|
555
|
+
send(type, payload, msgId);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
clearTimeout(timer);
|
|
558
|
+
waiting.delete(msgId);
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
},
|
|
563
|
+
answer(type, responder) {
|
|
564
|
+
responders.set(type, responder);
|
|
565
|
+
return () => responders.delete(type);
|
|
384
566
|
},
|
|
385
567
|
close() {
|
|
386
568
|
if (closed) return;
|
|
387
569
|
closed = true;
|
|
388
570
|
unsubscribe();
|
|
389
571
|
handlers.clear();
|
|
572
|
+
responders.clear();
|
|
573
|
+
waiting.clear();
|
|
390
574
|
bus.release();
|
|
391
575
|
}
|
|
392
576
|
};
|
|
@@ -437,15 +621,18 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
437
621
|
if (onSharedBus) {
|
|
438
622
|
const live = liveStores.get(name) ?? 0;
|
|
439
623
|
if (live > 0) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
624
|
+
if (process.env.NODE_ENV !== "production") {
|
|
625
|
+
devWarn(
|
|
626
|
+
`[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.`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
443
629
|
}
|
|
444
630
|
liveStores.set(name, live + 1);
|
|
445
631
|
}
|
|
446
632
|
const bus = getBus(name, options);
|
|
447
633
|
const clientId = bus.clientId;
|
|
448
634
|
const accept = options.accept;
|
|
635
|
+
const gate = createGate(name, options.schema, options.onInvalid);
|
|
449
636
|
const state = { ...initial };
|
|
450
637
|
const versions = {};
|
|
451
638
|
for (const k in state) {
|
|
@@ -456,23 +643,61 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
456
643
|
let versionsSnapshot = Object.freeze({ ...versions });
|
|
457
644
|
const listeners = /* @__PURE__ */ new Set();
|
|
458
645
|
const keyListeners = /* @__PURE__ */ new Map();
|
|
459
|
-
|
|
646
|
+
let batchDepth = 0;
|
|
647
|
+
const batched = [];
|
|
648
|
+
const rebuild = () => {
|
|
460
649
|
snapshot = Object.freeze({ ...state });
|
|
461
650
|
versionsSnapshot = Object.freeze({ ...versions });
|
|
651
|
+
};
|
|
652
|
+
const emit = (key, value, meta) => {
|
|
462
653
|
for (const fn of listeners) fn(key, value, meta);
|
|
463
654
|
const set = keyListeners.get(key);
|
|
464
655
|
if (set) for (const fn of set) fn();
|
|
656
|
+
};
|
|
657
|
+
function notify(key, value, meta) {
|
|
658
|
+
if (batchDepth > 0) {
|
|
659
|
+
batched.push([key, value, meta]);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
rebuild();
|
|
663
|
+
emit(key, value, meta);
|
|
664
|
+
}
|
|
665
|
+
function batch(fn) {
|
|
666
|
+
batchDepth++;
|
|
667
|
+
try {
|
|
668
|
+
return fn();
|
|
669
|
+
} finally {
|
|
670
|
+
batchDepth--;
|
|
671
|
+
if (batchDepth === 0 && batched.length > 0) {
|
|
672
|
+
const changes = batched.splice(0, batched.length);
|
|
673
|
+
rebuild();
|
|
674
|
+
for (const [key, value, meta] of changes) emit(key, value, meta);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
465
677
|
}
|
|
466
678
|
function applyRemote(key, value, version, meta) {
|
|
467
679
|
if (!newer(version, versions[key])) return;
|
|
680
|
+
if (gate && !gate.accepts(key, value)) return;
|
|
468
681
|
versions[key] = version;
|
|
469
682
|
state[key] = freezeShared(value);
|
|
470
683
|
notify(key, value, meta);
|
|
471
684
|
}
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
685
|
+
const snapshotDelayMs = options.snapshotDelayMs ?? 40;
|
|
686
|
+
let snapshotTimer;
|
|
687
|
+
const written = () => Object.keys(versions).filter((k) => (versions[k]?.[0] ?? 0) > 0);
|
|
688
|
+
const coveredBy = (theirs) => written().every((key) => {
|
|
689
|
+
const mine = versions[key];
|
|
690
|
+
return mine !== void 0 && !newer(mine, theirs[key]);
|
|
691
|
+
});
|
|
692
|
+
const cancelSnapshot = () => {
|
|
693
|
+
clearTimeout(snapshotTimer);
|
|
694
|
+
snapshotTimer = void 0;
|
|
695
|
+
};
|
|
696
|
+
const scheduleSnapshot = () => {
|
|
697
|
+
if (written().length === 0) return;
|
|
698
|
+
if (snapshotTimer !== void 0) return;
|
|
699
|
+
snapshotTimer = setTimeout(() => {
|
|
700
|
+
snapshotTimer = void 0;
|
|
476
701
|
bus.post({
|
|
477
702
|
v: 1,
|
|
478
703
|
scope: "state",
|
|
@@ -482,22 +707,36 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
482
707
|
state: { ...state },
|
|
483
708
|
versions: { ...versions }
|
|
484
709
|
});
|
|
710
|
+
}, Math.random() * snapshotDelayMs);
|
|
711
|
+
};
|
|
712
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
713
|
+
if (wire.scope !== "state") return;
|
|
714
|
+
const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
|
|
715
|
+
if (wire.type === "hello") {
|
|
716
|
+
scheduleSnapshot();
|
|
485
717
|
return;
|
|
486
718
|
}
|
|
487
719
|
if (accept && !accept(meta)) return;
|
|
488
720
|
if (wire.type === "patch") {
|
|
489
721
|
if (typeof wire.key !== "string" || !isVersion(wire.version)) return;
|
|
490
722
|
applyRemote(wire.key, wire.value, wire.version, meta);
|
|
491
|
-
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (wire.type === "snapshot") {
|
|
492
726
|
const versions2 = wire.versions;
|
|
493
727
|
if (typeof versions2 !== "object" || versions2 === null) return;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
728
|
+
if (coveredBy(versions2)) cancelSnapshot();
|
|
729
|
+
batch(() => {
|
|
730
|
+
for (const k in wire.state) {
|
|
731
|
+
const version = versions2[k];
|
|
732
|
+
if (isVersion(version)) applyRemote(k, wire.state[k], version, meta);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
return;
|
|
498
736
|
}
|
|
499
737
|
});
|
|
500
738
|
function setKey(key, value) {
|
|
739
|
+
gate?.assert(key, value);
|
|
501
740
|
const version = [(versions[key]?.[0] ?? 0) + 1, clientId];
|
|
502
741
|
try {
|
|
503
742
|
bus.post({
|
|
@@ -529,29 +768,73 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
529
768
|
});
|
|
530
769
|
const persist = options.persist;
|
|
531
770
|
let flushPersist;
|
|
771
|
+
let settle = () => {
|
|
772
|
+
};
|
|
773
|
+
const hydrated = persist ? new Promise((resolve) => {
|
|
774
|
+
settle = resolve;
|
|
775
|
+
}) : Promise.resolve();
|
|
532
776
|
if (persist) {
|
|
533
|
-
const {
|
|
777
|
+
const {
|
|
778
|
+
adapter,
|
|
779
|
+
keys,
|
|
780
|
+
debounceMs = 100,
|
|
781
|
+
version: schemaVersion = 0,
|
|
782
|
+
migrate,
|
|
783
|
+
onRestoreError
|
|
784
|
+
} = persist;
|
|
534
785
|
const shouldPersist = (key) => !keys || keys.includes(key);
|
|
535
|
-
const
|
|
536
|
-
if (
|
|
537
|
-
|
|
538
|
-
|
|
786
|
+
const refuse = (error) => {
|
|
787
|
+
if (onRestoreError) onRestoreError(error);
|
|
788
|
+
else if (process.env.NODE_ENV !== "production") {
|
|
789
|
+
devWarn(
|
|
790
|
+
`[use-everywhere] ${name}: persisted schema v${error.found} not restored, expected v${error.expected} (${error.reason}). https://rxova.org/guides/persistence/`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
const reconcile = (saved2) => {
|
|
795
|
+
const found = saved2.schema ?? 0;
|
|
796
|
+
if (found === schemaVersion) return { state: saved2.state, migrated: false };
|
|
797
|
+
if (found > schemaVersion) {
|
|
798
|
+
refuse({ reason: "ahead", found, expected: schemaVersion });
|
|
799
|
+
return void 0;
|
|
800
|
+
}
|
|
801
|
+
if (!migrate) {
|
|
802
|
+
refuse({ reason: "no-migrate", found, expected: schemaVersion });
|
|
803
|
+
return void 0;
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
return { state: migrate(saved2.state, found), migrated: true };
|
|
807
|
+
} catch (cause) {
|
|
808
|
+
refuse({ reason: "migrate-threw", found, expected: schemaVersion, cause });
|
|
809
|
+
return void 0;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
const hydrate = (raw) => {
|
|
813
|
+
const reconciled = raw && reconcile(raw);
|
|
814
|
+
if (!reconciled || !raw) {
|
|
815
|
+
settle();
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const { state: restored, migrated } = reconciled;
|
|
819
|
+
for (const key in restored) {
|
|
820
|
+
const version = raw.versions[key] ?? (migrated ? [1, clientId] : void 0);
|
|
539
821
|
if (!version || !shouldPersist(key)) continue;
|
|
540
|
-
applyRemote(key,
|
|
822
|
+
applyRemote(key, restored[key], version, { clientId, kind: bus.kind, self: true });
|
|
541
823
|
bus.post({
|
|
542
824
|
v: 1,
|
|
543
825
|
scope: "state",
|
|
544
826
|
type: "patch",
|
|
545
827
|
key,
|
|
546
|
-
value:
|
|
828
|
+
value: restored[key],
|
|
547
829
|
version,
|
|
548
830
|
clientId,
|
|
549
831
|
kind: bus.kind
|
|
550
832
|
});
|
|
551
833
|
}
|
|
834
|
+
settle();
|
|
552
835
|
};
|
|
553
836
|
const collect = () => {
|
|
554
|
-
const out = { v: 1, state: {}, versions: {} };
|
|
837
|
+
const out = { v: 1, schema: schemaVersion, state: {}, versions: {} };
|
|
555
838
|
for (const key in versions) {
|
|
556
839
|
const version = versions[key];
|
|
557
840
|
if (!version || version[0] === 0 || !shouldPersist(key)) continue;
|
|
@@ -591,6 +874,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
591
874
|
let storeClosed = false;
|
|
592
875
|
return {
|
|
593
876
|
clientId,
|
|
877
|
+
hydrated,
|
|
594
878
|
state: proxy,
|
|
595
879
|
getSnapshot: () => snapshot,
|
|
596
880
|
getVersions: () => versionsSnapshot,
|
|
@@ -598,6 +882,9 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
598
882
|
const next = typeof value === "function" ? value(state[key]) : value;
|
|
599
883
|
setKey(key, next);
|
|
600
884
|
},
|
|
885
|
+
transaction(fn) {
|
|
886
|
+
return batch(fn);
|
|
887
|
+
},
|
|
601
888
|
subscribe(fn) {
|
|
602
889
|
listeners.add(fn);
|
|
603
890
|
return () => listeners.delete(fn);
|
|
@@ -629,6 +916,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
629
916
|
if (live > 0) liveStores.set(name, live);
|
|
630
917
|
else liveStores.delete(name);
|
|
631
918
|
}
|
|
919
|
+
cancelSnapshot();
|
|
632
920
|
if (hasWindow) removeEventListener("pageshow", onPageShow);
|
|
633
921
|
if (flushPersist) {
|
|
634
922
|
flushPersist();
|
|
@@ -644,83 +932,6 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
644
932
|
};
|
|
645
933
|
}
|
|
646
934
|
|
|
647
|
-
// src/presence.ts
|
|
648
|
-
function createPresence(name, options = {}) {
|
|
649
|
-
const pruneAfterMs = options.pruneAfterMs ?? 5e3;
|
|
650
|
-
const probeGraceMs = options.probeGraceMs ?? 1e3;
|
|
651
|
-
const bus = getBus(name, options);
|
|
652
|
-
const peers = /* @__PURE__ */ new Map();
|
|
653
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
654
|
-
let snapshot = [];
|
|
655
|
-
function notify() {
|
|
656
|
-
snapshot = Object.freeze([...peers.values()]);
|
|
657
|
-
for (const fn of listeners) fn();
|
|
658
|
-
}
|
|
659
|
-
const unsubscribe = bus.subscribe((wire) => {
|
|
660
|
-
if (wire.clientId === bus.clientId) return;
|
|
661
|
-
if (wire.scope === "presence" && wire.type === "bye") {
|
|
662
|
-
if (peers.delete(wire.clientId)) notify();
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
const existing = peers.get(wire.clientId);
|
|
666
|
-
peers.set(wire.clientId, { id: wire.clientId, kind: wire.kind, lastSeen: Date.now() });
|
|
667
|
-
if (!existing) notify();
|
|
668
|
-
});
|
|
669
|
-
bus.post({ v: 1, scope: "presence", type: "hello", clientId: bus.clientId, kind: bus.kind });
|
|
670
|
-
const probed = /* @__PURE__ */ new Map();
|
|
671
|
-
const prune = setInterval(
|
|
672
|
-
() => {
|
|
673
|
-
const now = Date.now();
|
|
674
|
-
const silentSince = now - pruneAfterMs;
|
|
675
|
-
let changed = false;
|
|
676
|
-
let needProbe = false;
|
|
677
|
-
for (const [id, peer] of peers) {
|
|
678
|
-
if (peer.lastSeen >= silentSince) {
|
|
679
|
-
probed.delete(id);
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
const askedAt = probed.get(id);
|
|
683
|
-
if (askedAt === void 0) {
|
|
684
|
-
probed.set(id, now);
|
|
685
|
-
needProbe = true;
|
|
686
|
-
} else if (now - askedAt >= probeGraceMs) {
|
|
687
|
-
peers.delete(id);
|
|
688
|
-
probed.delete(id);
|
|
689
|
-
changed = true;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
if (needProbe) {
|
|
693
|
-
bus.post({
|
|
694
|
-
v: 1,
|
|
695
|
-
scope: "presence",
|
|
696
|
-
type: "hello",
|
|
697
|
-
clientId: bus.clientId,
|
|
698
|
-
kind: bus.kind
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
if (changed) notify();
|
|
702
|
-
},
|
|
703
|
-
Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
|
|
704
|
-
);
|
|
705
|
-
let closed = false;
|
|
706
|
-
return {
|
|
707
|
-
clientId: bus.clientId,
|
|
708
|
-
getPeers: () => snapshot,
|
|
709
|
-
subscribe(fn) {
|
|
710
|
-
listeners.add(fn);
|
|
711
|
-
return () => listeners.delete(fn);
|
|
712
|
-
},
|
|
713
|
-
close() {
|
|
714
|
-
if (closed) return;
|
|
715
|
-
closed = true;
|
|
716
|
-
clearInterval(prune);
|
|
717
|
-
unsubscribe();
|
|
718
|
-
listeners.clear();
|
|
719
|
-
bus.release();
|
|
720
|
-
}
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
|
|
724
935
|
// src/leader-web-locks.ts
|
|
725
936
|
var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
|
|
726
937
|
function createWebLocksLeader(name, options, locks) {
|
|
@@ -1005,6 +1216,246 @@ function createHeartbeatLeader(name, options) {
|
|
|
1005
1216
|
};
|
|
1006
1217
|
}
|
|
1007
1218
|
|
|
1219
|
+
// src/shared-reducer.ts
|
|
1220
|
+
function createSharedReducer(name, reducer, initial, options = {}) {
|
|
1221
|
+
const key = options.key ?? "default";
|
|
1222
|
+
const bus = getBus(name, options);
|
|
1223
|
+
const clientId = bus.clientId;
|
|
1224
|
+
const leader = options.leader ?? createLeader(name, {
|
|
1225
|
+
...options.leaderOptions,
|
|
1226
|
+
// Spread conditionally: `exactOptionalPropertyTypes` distinguishes an
|
|
1227
|
+
// absent transport from one explicitly set to undefined, and the leader
|
|
1228
|
+
// must default its own rather than be handed a hole.
|
|
1229
|
+
...options.transport ? { transport: options.transport } : {}
|
|
1230
|
+
});
|
|
1231
|
+
const ownsLeader = !options.leader;
|
|
1232
|
+
let committed = initial;
|
|
1233
|
+
let seq = 0;
|
|
1234
|
+
let pending = [];
|
|
1235
|
+
const early = /* @__PURE__ */ new Map();
|
|
1236
|
+
let lastIssued = 0;
|
|
1237
|
+
let view = committed;
|
|
1238
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1239
|
+
let closed = false;
|
|
1240
|
+
const notify = () => {
|
|
1241
|
+
const next = pending.reduce((state, op) => reducer(state, op.action), committed);
|
|
1242
|
+
if (Object.is(next, view)) return;
|
|
1243
|
+
view = next;
|
|
1244
|
+
for (const fn of listeners) fn();
|
|
1245
|
+
};
|
|
1246
|
+
const askForSnapshot = () => bus.post({ v: 1, scope: "op", type: "hello", key, clientId, kind: bus.kind });
|
|
1247
|
+
const receiveCommit = (at, action, opId) => {
|
|
1248
|
+
if (at <= seq) return;
|
|
1249
|
+
if (at > seq + 1) {
|
|
1250
|
+
early.set(at, { action, opId });
|
|
1251
|
+
askForSnapshot();
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
applyCommit(at, action, opId);
|
|
1255
|
+
notify();
|
|
1256
|
+
};
|
|
1257
|
+
const sequence = (action, opId) => {
|
|
1258
|
+
lastIssued = Math.max(lastIssued, seq) + 1;
|
|
1259
|
+
const at = lastIssued;
|
|
1260
|
+
bus.post({
|
|
1261
|
+
v: 1,
|
|
1262
|
+
scope: "op",
|
|
1263
|
+
type: "commit",
|
|
1264
|
+
key,
|
|
1265
|
+
action,
|
|
1266
|
+
opId,
|
|
1267
|
+
seq: at,
|
|
1268
|
+
clientId,
|
|
1269
|
+
kind: bus.kind
|
|
1270
|
+
});
|
|
1271
|
+
receiveCommit(at, action, opId);
|
|
1272
|
+
};
|
|
1273
|
+
const applyCommit = (at, action, opId) => {
|
|
1274
|
+
committed = reducer(committed, action);
|
|
1275
|
+
seq = at;
|
|
1276
|
+
pending = pending.filter((op) => op.opId !== opId);
|
|
1277
|
+
const next = early.get(seq + 1);
|
|
1278
|
+
if (next) {
|
|
1279
|
+
early.delete(seq + 1);
|
|
1280
|
+
applyCommit(seq + 1, next.action, next.opId);
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
1284
|
+
if (wire.scope !== "op" || wire.key !== key) return;
|
|
1285
|
+
if (wire.type === "propose") {
|
|
1286
|
+
if (!leader.getSnapshot().isLeader) return;
|
|
1287
|
+
sequence(wire.action, wire.opId);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
if (wire.type === "commit") {
|
|
1291
|
+
receiveCommit(wire.seq, wire.action, wire.opId);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (wire.type === "hello") {
|
|
1295
|
+
if (seq === 0) return;
|
|
1296
|
+
bus.post({
|
|
1297
|
+
v: 1,
|
|
1298
|
+
scope: "op",
|
|
1299
|
+
type: "snapshot",
|
|
1300
|
+
key,
|
|
1301
|
+
state: committed,
|
|
1302
|
+
seq,
|
|
1303
|
+
clientId,
|
|
1304
|
+
kind: bus.kind
|
|
1305
|
+
});
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (wire.type === "snapshot") {
|
|
1309
|
+
if (wire.seq <= seq) return;
|
|
1310
|
+
committed = wire.state;
|
|
1311
|
+
seq = wire.seq;
|
|
1312
|
+
for (const at of [...early.keys()]) if (at <= seq) early.delete(at);
|
|
1313
|
+
const next = early.get(seq + 1);
|
|
1314
|
+
if (next) {
|
|
1315
|
+
early.delete(seq + 1);
|
|
1316
|
+
applyCommit(seq + 1, next.action, next.opId);
|
|
1317
|
+
}
|
|
1318
|
+
notify();
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
const dispatch = (action) => {
|
|
1323
|
+
if (closed) return;
|
|
1324
|
+
const opId = newMsgId();
|
|
1325
|
+
pending.push({ opId, action });
|
|
1326
|
+
notify();
|
|
1327
|
+
if (leader.getSnapshot().isLeader) sequence(action, opId);
|
|
1328
|
+
else
|
|
1329
|
+
bus.post({ v: 1, scope: "op", type: "propose", key, action, opId, clientId, kind: bus.kind });
|
|
1330
|
+
};
|
|
1331
|
+
askForSnapshot();
|
|
1332
|
+
return {
|
|
1333
|
+
clientId,
|
|
1334
|
+
getSnapshot: () => view,
|
|
1335
|
+
dispatch,
|
|
1336
|
+
subscribe(fn) {
|
|
1337
|
+
listeners.add(fn);
|
|
1338
|
+
return () => listeners.delete(fn);
|
|
1339
|
+
},
|
|
1340
|
+
pendingCount: () => pending.length,
|
|
1341
|
+
close() {
|
|
1342
|
+
if (closed) return;
|
|
1343
|
+
closed = true;
|
|
1344
|
+
unsubscribe();
|
|
1345
|
+
listeners.clear();
|
|
1346
|
+
if (ownsLeader) leader.close();
|
|
1347
|
+
bus.release();
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// src/presence.ts
|
|
1353
|
+
function createPresence(name, options = {}) {
|
|
1354
|
+
const pruneAfterMs = options.pruneAfterMs ?? 5e3;
|
|
1355
|
+
const probeGraceMs = options.probeGraceMs ?? 1e3;
|
|
1356
|
+
const bus = getBus(name, options);
|
|
1357
|
+
const peers = /* @__PURE__ */ new Map();
|
|
1358
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1359
|
+
let snapshot = [];
|
|
1360
|
+
let metadata = options.metadata;
|
|
1361
|
+
const same = (a, b) => {
|
|
1362
|
+
if (Object.is(a, b)) return true;
|
|
1363
|
+
try {
|
|
1364
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1365
|
+
} catch {
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
const self = () => ({
|
|
1370
|
+
id: bus.clientId,
|
|
1371
|
+
kind: bus.kind,
|
|
1372
|
+
lastSeen: Date.now(),
|
|
1373
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1374
|
+
});
|
|
1375
|
+
function notify() {
|
|
1376
|
+
const roster = [...peers.values()];
|
|
1377
|
+
snapshot = Object.freeze(options.includeSelf ? [self(), ...roster] : roster);
|
|
1378
|
+
for (const fn of listeners) fn();
|
|
1379
|
+
}
|
|
1380
|
+
const announce2 = () => bus.post({
|
|
1381
|
+
v: 1,
|
|
1382
|
+
scope: "presence",
|
|
1383
|
+
type: "hello",
|
|
1384
|
+
clientId: bus.clientId,
|
|
1385
|
+
kind: bus.kind,
|
|
1386
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1387
|
+
});
|
|
1388
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
1389
|
+
if (wire.clientId === bus.clientId) return;
|
|
1390
|
+
if (wire.scope === "presence" && wire.type === "bye") {
|
|
1391
|
+
if (peers.delete(wire.clientId)) notify();
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const existing = peers.get(wire.clientId);
|
|
1395
|
+
const announced = wire.scope === "presence" ? wire.metadata : void 0;
|
|
1396
|
+
const next = announced === void 0 ? existing?.metadata : announced;
|
|
1397
|
+
peers.set(wire.clientId, {
|
|
1398
|
+
id: wire.clientId,
|
|
1399
|
+
kind: wire.kind,
|
|
1400
|
+
lastSeen: Date.now(),
|
|
1401
|
+
...next === void 0 ? {} : { metadata: next }
|
|
1402
|
+
});
|
|
1403
|
+
if (!existing || !same(existing.metadata, next)) notify();
|
|
1404
|
+
});
|
|
1405
|
+
announce2();
|
|
1406
|
+
if (options.includeSelf) notify();
|
|
1407
|
+
const probed = /* @__PURE__ */ new Map();
|
|
1408
|
+
const prune = setInterval(
|
|
1409
|
+
() => {
|
|
1410
|
+
const now = Date.now();
|
|
1411
|
+
const silentSince = now - pruneAfterMs;
|
|
1412
|
+
let changed = false;
|
|
1413
|
+
let needProbe = false;
|
|
1414
|
+
for (const [id, peer] of peers) {
|
|
1415
|
+
if (peer.lastSeen >= silentSince) {
|
|
1416
|
+
probed.delete(id);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
const askedAt = probed.get(id);
|
|
1420
|
+
if (askedAt === void 0) {
|
|
1421
|
+
probed.set(id, now);
|
|
1422
|
+
needProbe = true;
|
|
1423
|
+
} else if (now - askedAt >= probeGraceMs) {
|
|
1424
|
+
peers.delete(id);
|
|
1425
|
+
probed.delete(id);
|
|
1426
|
+
changed = true;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (needProbe) announce2();
|
|
1430
|
+
if (changed) notify();
|
|
1431
|
+
},
|
|
1432
|
+
Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
|
|
1433
|
+
);
|
|
1434
|
+
let closed = false;
|
|
1435
|
+
return {
|
|
1436
|
+
clientId: bus.clientId,
|
|
1437
|
+
getPeers: () => snapshot,
|
|
1438
|
+
setMetadata(next) {
|
|
1439
|
+
if (same(metadata, next)) return;
|
|
1440
|
+
metadata = next;
|
|
1441
|
+
announce2();
|
|
1442
|
+
notify();
|
|
1443
|
+
},
|
|
1444
|
+
subscribe(fn) {
|
|
1445
|
+
listeners.add(fn);
|
|
1446
|
+
return () => listeners.delete(fn);
|
|
1447
|
+
},
|
|
1448
|
+
close() {
|
|
1449
|
+
if (closed) return;
|
|
1450
|
+
closed = true;
|
|
1451
|
+
clearInterval(prune);
|
|
1452
|
+
unsubscribe();
|
|
1453
|
+
listeners.clear();
|
|
1454
|
+
bus.release();
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1008
1459
|
// src/persist-web-storage.ts
|
|
1009
1460
|
function isPersisted(value) {
|
|
1010
1461
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -1012,6 +1463,7 @@ function isPersisted(value) {
|
|
|
1012
1463
|
return candidate.v === 1 && typeof candidate.state === "object" && typeof candidate.versions === "object";
|
|
1013
1464
|
}
|
|
1014
1465
|
function webStorageAdapter(storage, key, options = {}) {
|
|
1466
|
+
const serializer = options.serializer ?? jsonSerializer;
|
|
1015
1467
|
const report = (error, operation) => {
|
|
1016
1468
|
try {
|
|
1017
1469
|
options.onError?.(error, operation);
|
|
@@ -1031,7 +1483,7 @@ function webStorageAdapter(storage, key, options = {}) {
|
|
|
1031
1483
|
try {
|
|
1032
1484
|
const raw = resolve("read")?.getItem(key);
|
|
1033
1485
|
if (!raw) return void 0;
|
|
1034
|
-
const parsed =
|
|
1486
|
+
const parsed = serializer.parse(raw);
|
|
1035
1487
|
return isPersisted(parsed) ? parsed : void 0;
|
|
1036
1488
|
} catch (error) {
|
|
1037
1489
|
report(error, "read");
|
|
@@ -1040,7 +1492,7 @@ function webStorageAdapter(storage, key, options = {}) {
|
|
|
1040
1492
|
},
|
|
1041
1493
|
write(snapshot) {
|
|
1042
1494
|
try {
|
|
1043
|
-
resolve("write")?.setItem(key,
|
|
1495
|
+
resolve("write")?.setItem(key, serializer.stringify(snapshot));
|
|
1044
1496
|
} catch (error) {
|
|
1045
1497
|
report(error, "write");
|
|
1046
1498
|
}
|
|
@@ -1061,6 +1513,58 @@ function sessionStorageAdapter(key, options) {
|
|
|
1061
1513
|
return webStorageAdapter(() => globalThis.sessionStorage, key, options);
|
|
1062
1514
|
}
|
|
1063
1515
|
|
|
1516
|
+
// src/persist-indexeddb.ts
|
|
1517
|
+
var STORE = "state";
|
|
1518
|
+
var request = (req) => new Promise((resolve, reject) => {
|
|
1519
|
+
req.onsuccess = () => resolve(req.result);
|
|
1520
|
+
req.onerror = () => reject(req.error);
|
|
1521
|
+
});
|
|
1522
|
+
function indexedDbAdapter(key, options = {}) {
|
|
1523
|
+
const database = options.database ?? "use-everywhere";
|
|
1524
|
+
let open;
|
|
1525
|
+
const report = (error, operation) => {
|
|
1526
|
+
try {
|
|
1527
|
+
options.onError?.(error, operation);
|
|
1528
|
+
} catch {
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
const db = () => open ?? (open = new Promise((resolve, reject) => {
|
|
1532
|
+
const req = indexedDB.open(database, 1);
|
|
1533
|
+
req.onupgradeneeded = () => req.result.createObjectStore(STORE);
|
|
1534
|
+
req.onsuccess = () => resolve(req.result);
|
|
1535
|
+
req.onerror = () => reject(req.error);
|
|
1536
|
+
req.onblocked = () => reject(new Error("use-everywhere: IndexedDB upgrade blocked"));
|
|
1537
|
+
}));
|
|
1538
|
+
const transact = async (mode, run) => {
|
|
1539
|
+
const connection = await db();
|
|
1540
|
+
return request(run(connection.transaction(STORE, mode).objectStore(STORE)));
|
|
1541
|
+
};
|
|
1542
|
+
return {
|
|
1543
|
+
async read() {
|
|
1544
|
+
try {
|
|
1545
|
+
return await transact("readonly", (store) => store.get(key));
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
report(error, "read");
|
|
1548
|
+
return void 0;
|
|
1549
|
+
}
|
|
1550
|
+
},
|
|
1551
|
+
async write(snapshot) {
|
|
1552
|
+
try {
|
|
1553
|
+
await transact("readwrite", (store) => store.put(snapshot, key));
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
report(error, "write");
|
|
1556
|
+
}
|
|
1557
|
+
},
|
|
1558
|
+
async remove() {
|
|
1559
|
+
try {
|
|
1560
|
+
await transact("readwrite", (store) => store.delete(key));
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
report(error, "remove");
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1064
1568
|
// src/errors/handshake-timeout-error.ts
|
|
1065
1569
|
var HandshakeTimeoutError = class extends Error {
|
|
1066
1570
|
constructor(message = "handshake with the other window timed out") {
|
|
@@ -1295,6 +1799,25 @@ function connectToOpener(options) {
|
|
|
1295
1799
|
}
|
|
1296
1800
|
};
|
|
1297
1801
|
}
|
|
1802
|
+
|
|
1803
|
+
// src/namespace.ts
|
|
1804
|
+
var SEPARATOR = ":";
|
|
1805
|
+
function createNamespace(namespace) {
|
|
1806
|
+
if (!namespace) {
|
|
1807
|
+
throw new TypeError(
|
|
1808
|
+
"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."
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
const busName = (name = DEFAULT_NAME) => `${namespace}${SEPARATOR}${name}`;
|
|
1812
|
+
return {
|
|
1813
|
+
name: namespace,
|
|
1814
|
+
busName,
|
|
1815
|
+
createSharedStore: (name, initial, options) => createSharedStore(busName(name), initial, options),
|
|
1816
|
+
createChannel: (name, options) => createChannel(busName(name), options),
|
|
1817
|
+
createPresence: (name, options) => createPresence(busName(name), options),
|
|
1818
|
+
createLeader: (name, options) => createLeader(busName(name), options)
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1298
1821
|
export {
|
|
1299
1822
|
BroadcastChannelTransport,
|
|
1300
1823
|
CID_PARAM,
|
|
@@ -1302,18 +1825,24 @@ export {
|
|
|
1302
1825
|
HandshakeTimeoutError,
|
|
1303
1826
|
NoopTransport,
|
|
1304
1827
|
StorageTransport,
|
|
1828
|
+
WIRE_VERSION,
|
|
1305
1829
|
WindowClosedError,
|
|
1306
1830
|
connectToOpener,
|
|
1307
1831
|
createChannel,
|
|
1308
1832
|
createLeader,
|
|
1833
|
+
createNamespace,
|
|
1309
1834
|
createPresence,
|
|
1835
|
+
createSharedReducer,
|
|
1310
1836
|
createSharedStore,
|
|
1311
1837
|
defaultTransport,
|
|
1312
1838
|
enableDebug,
|
|
1313
1839
|
getBusNames,
|
|
1314
1840
|
getTransportKind,
|
|
1841
|
+
getWireSkew,
|
|
1842
|
+
indexedDbAdapter,
|
|
1315
1843
|
isBroadcastChannelAvailable,
|
|
1316
1844
|
isStorageEventAvailable,
|
|
1845
|
+
jsonSerializer,
|
|
1317
1846
|
localStorageAdapter,
|
|
1318
1847
|
newer,
|
|
1319
1848
|
observeBus,
|