@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.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
// src/defaults.ts
|
|
2
2
|
var DEFAULT_NAME = "use-everywhere";
|
|
3
3
|
|
|
4
|
+
// src/dev.ts
|
|
5
|
+
var inDev = false;
|
|
6
|
+
try {
|
|
7
|
+
inDev = process.env.NODE_ENV !== "production";
|
|
8
|
+
} catch {
|
|
9
|
+
}
|
|
10
|
+
var warned = /* @__PURE__ */ new Set();
|
|
11
|
+
var DOCS = "https://rxova.org/packages/use-everywhere/errors";
|
|
12
|
+
function diagnostic(code, message) {
|
|
13
|
+
return `[use-everywhere] ${code}: ${message}
|
|
14
|
+
\u2192 ${DOCS}/#${code.toLowerCase()}`;
|
|
15
|
+
}
|
|
16
|
+
function devWarn(code, message) {
|
|
17
|
+
if (!inDev) return;
|
|
18
|
+
const line = diagnostic(code, message);
|
|
19
|
+
if (warned.has(line)) return;
|
|
20
|
+
warned.add(line);
|
|
21
|
+
console.warn(line);
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
// src/debug.ts
|
|
5
25
|
var observers = /* @__PURE__ */ new Map();
|
|
6
26
|
function emitBusEvent(name, direction, wire) {
|
|
@@ -11,7 +31,7 @@ function emitBusEvent(name, direction, wire) {
|
|
|
11
31
|
try {
|
|
12
32
|
fn(event);
|
|
13
33
|
} catch (error) {
|
|
14
|
-
console.error(`
|
|
34
|
+
console.error(diagnostic("UE1012", `a bus observer for "${name}" threw`), error);
|
|
15
35
|
}
|
|
16
36
|
}
|
|
17
37
|
}
|
|
@@ -38,19 +58,6 @@ function enableDebug(options = {}) {
|
|
|
38
58
|
});
|
|
39
59
|
}
|
|
40
60
|
|
|
41
|
-
// src/dev.ts
|
|
42
|
-
var inDev = false;
|
|
43
|
-
try {
|
|
44
|
-
inDev = process.env.NODE_ENV !== "production";
|
|
45
|
-
} catch {
|
|
46
|
-
}
|
|
47
|
-
var warned = /* @__PURE__ */ new Set();
|
|
48
|
-
function devWarn(message) {
|
|
49
|
-
if (!inDev || warned.has(message)) return;
|
|
50
|
-
warned.add(message);
|
|
51
|
-
console.warn(message);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
61
|
// src/ids.ts
|
|
55
62
|
function randomHex(bytes) {
|
|
56
63
|
const crypto = globalThis.crypto;
|
|
@@ -60,9 +67,12 @@ function randomHex(bytes) {
|
|
|
60
67
|
for (const b of buf) out2 += b.toString(16).padStart(2, "0");
|
|
61
68
|
return out2;
|
|
62
69
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
if (process.env.NODE_ENV !== "production") {
|
|
71
|
+
devWarn(
|
|
72
|
+
"UE1006",
|
|
73
|
+
"crypto.getRandomValues is unavailable; falling back to Math.random for ids. Cross-origin window nonces are not cryptographically strong in this environment."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
66
76
|
let out = "";
|
|
67
77
|
while (out.length < bytes * 2)
|
|
68
78
|
out += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
|
|
@@ -79,15 +89,19 @@ function newMsgId() {
|
|
|
79
89
|
var PROTOCOL = 1;
|
|
80
90
|
var TABLE = /* @__PURE__ */ Symbol.for(`use-everywhere.rendezvous.${PROTOCOL}`);
|
|
81
91
|
var CENSUS = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.census");
|
|
92
|
+
var SKEW = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.skew");
|
|
82
93
|
function announce() {
|
|
83
94
|
const g = globalThis;
|
|
84
95
|
const census = g[CENSUS] ?? (g[CENSUS] = { protocols: [] });
|
|
85
96
|
if (census.protocols.includes(PROTOCOL)) return;
|
|
86
97
|
census.protocols.push(PROTOCOL);
|
|
87
98
|
if (census.protocols.length > 1) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
if (process.env.NODE_ENV !== "production") {
|
|
100
|
+
devWarn(
|
|
101
|
+
"UE1008",
|
|
102
|
+
`two incompatible versions of this library are loaded on one page (rendezvous protocols ${census.protocols.join(", ")}). They will not share a client identity: expect one presence entry per version and no synchronous delivery between them. They still sync over the bus. Align the versions to fix it.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
function busTable() {
|
|
@@ -95,6 +109,10 @@ function busTable() {
|
|
|
95
109
|
const g = globalThis;
|
|
96
110
|
return g[TABLE] ?? (g[TABLE] = /* @__PURE__ */ new Map());
|
|
97
111
|
}
|
|
112
|
+
function skewLedger() {
|
|
113
|
+
const g = globalThis;
|
|
114
|
+
return g[SKEW] ?? (g[SKEW] = /* @__PURE__ */ new Map());
|
|
115
|
+
}
|
|
98
116
|
|
|
99
117
|
// src/transport/broadcast-channel-transport.ts
|
|
100
118
|
var BroadcastChannelTransport = class {
|
|
@@ -134,24 +152,57 @@ var NoopTransport = class {
|
|
|
134
152
|
}
|
|
135
153
|
};
|
|
136
154
|
|
|
155
|
+
// src/serializer.ts
|
|
156
|
+
function lossyType(raw, value) {
|
|
157
|
+
if (value === void 0) return raw === void 0 ? "undefined" : "undefined after toJSON";
|
|
158
|
+
if (raw instanceof Date) return "Date";
|
|
159
|
+
if (raw instanceof Map) return "Map";
|
|
160
|
+
if (raw instanceof Set) return "Set";
|
|
161
|
+
if (raw instanceof RegExp) return "RegExp";
|
|
162
|
+
if (ArrayBuffer.isView(raw)) return "TypedArray";
|
|
163
|
+
const type = typeof raw;
|
|
164
|
+
return type === "function" || type === "symbol" ? type : void 0;
|
|
165
|
+
}
|
|
166
|
+
var jsonSerializer = {
|
|
167
|
+
stringify(value) {
|
|
168
|
+
if (value === void 0) {
|
|
169
|
+
throw new TypeError("use-everywhere: cannot serialize undefined");
|
|
170
|
+
}
|
|
171
|
+
return JSON.stringify(value, function replacer(key, forJson) {
|
|
172
|
+
const type = lossyType(this[key], forJson);
|
|
173
|
+
if (type) {
|
|
174
|
+
throw new TypeError(
|
|
175
|
+
`use-everywhere: ${key ? `"${key}" is ` : ""}${type}, which JSON cannot round-trip. https://rxova.org/packages/use-everywhere/guides/serialization/`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return forJson;
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
parse: (text) => JSON.parse(text)
|
|
182
|
+
};
|
|
183
|
+
|
|
137
184
|
// src/transport/storage-transport.ts
|
|
138
185
|
var StorageTransport = class {
|
|
139
|
-
constructor(name, storage = globalThis.localStorage) {
|
|
186
|
+
constructor(name, storage = globalThis.localStorage, serializer = jsonSerializer) {
|
|
140
187
|
this.kind = "storage";
|
|
141
188
|
this.listeners = /* @__PURE__ */ new Set();
|
|
142
189
|
this.seq = 0;
|
|
143
190
|
if (!storage) {
|
|
144
191
|
throw new Error(
|
|
145
|
-
|
|
192
|
+
diagnostic(
|
|
193
|
+
"UE1011",
|
|
194
|
+
"StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
|
|
195
|
+
)
|
|
146
196
|
);
|
|
147
197
|
}
|
|
198
|
+
this.serializer = serializer;
|
|
148
199
|
this.key = `use-everywhere:bus:${name}`;
|
|
149
200
|
this.storage = storage;
|
|
150
201
|
this.onStorage = (event) => {
|
|
151
202
|
if (event.key !== this.key || event.newValue === null) return;
|
|
152
203
|
let payload;
|
|
153
204
|
try {
|
|
154
|
-
payload =
|
|
205
|
+
payload = this.serializer.parse(event.newValue);
|
|
155
206
|
} catch {
|
|
156
207
|
return;
|
|
157
208
|
}
|
|
@@ -160,7 +211,7 @@ var StorageTransport = class {
|
|
|
160
211
|
addEventListener("storage", this.onStorage);
|
|
161
212
|
}
|
|
162
213
|
post(data) {
|
|
163
|
-
const envelope =
|
|
214
|
+
const envelope = this.serializer.stringify({ seq: this.seq++, data });
|
|
164
215
|
try {
|
|
165
216
|
this.storage.setItem(this.key, envelope);
|
|
166
217
|
this.storage.removeItem(this.key);
|
|
@@ -176,12 +227,6 @@ var StorageTransport = class {
|
|
|
176
227
|
removeEventListener("storage", this.onStorage);
|
|
177
228
|
}
|
|
178
229
|
};
|
|
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
230
|
|
|
186
231
|
// src/transport/default-transport.ts
|
|
187
232
|
function isBroadcastChannelAvailable() {
|
|
@@ -203,27 +248,61 @@ function isStorageEventAvailable() {
|
|
|
203
248
|
function defaultTransport(name) {
|
|
204
249
|
if (isBroadcastChannelAvailable()) return new BroadcastChannelTransport(name);
|
|
205
250
|
if (isStorageEventAvailable()) {
|
|
251
|
+
if (process.env.NODE_ENV !== "production") {
|
|
252
|
+
devWarn(
|
|
253
|
+
"UE1009",
|
|
254
|
+
"no BroadcastChannel; using the storage-event fallback. Values serialise as JSON, not structured clone \u2014 keep them JSON-shaped."
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return new StorageTransport(name);
|
|
258
|
+
}
|
|
259
|
+
if (process.env.NODE_ENV !== "production") {
|
|
206
260
|
devWarn(
|
|
207
|
-
"
|
|
261
|
+
"UE1010",
|
|
262
|
+
"no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
208
263
|
);
|
|
209
|
-
return new StorageTransport(name);
|
|
210
264
|
}
|
|
211
|
-
devWarn(
|
|
212
|
-
"[use-everywhere] no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
213
|
-
);
|
|
214
265
|
return new NoopTransport();
|
|
215
266
|
}
|
|
216
267
|
|
|
217
|
-
// src/
|
|
268
|
+
// src/wire.ts
|
|
269
|
+
var WIRE_VERSION = 1;
|
|
270
|
+
function hasEnvelopeShape(wire) {
|
|
271
|
+
return typeof wire.scope === "string" && typeof wire.type === "string" && typeof wire.clientId === "string";
|
|
272
|
+
}
|
|
218
273
|
function isBusWire(data) {
|
|
219
274
|
if (typeof data !== "object" || data === null) return false;
|
|
220
275
|
const wire = data;
|
|
221
|
-
return wire.v ===
|
|
276
|
+
return wire.v === WIRE_VERSION && hasEnvelopeShape(wire);
|
|
222
277
|
}
|
|
278
|
+
function foreignWireVersion(data) {
|
|
279
|
+
if (typeof data !== "object" || data === null) return null;
|
|
280
|
+
const wire = data;
|
|
281
|
+
if (typeof wire.v !== "number" || wire.v === WIRE_VERSION) return null;
|
|
282
|
+
return hasEnvelopeShape(wire) ? wire.v : null;
|
|
283
|
+
}
|
|
284
|
+
function recordSkew(name, version) {
|
|
285
|
+
const seen = skewLedger();
|
|
286
|
+
const versions = seen.get(name) ?? /* @__PURE__ */ new Set();
|
|
287
|
+
seen.set(name, versions);
|
|
288
|
+
if (versions.has(version)) return;
|
|
289
|
+
versions.add(version);
|
|
290
|
+
if (process.env.NODE_ENV !== "production") {
|
|
291
|
+
devWarn(
|
|
292
|
+
"UE1007",
|
|
293
|
+
`bus "${name}": a peer speaks wire protocol v${version}, this build speaks v${WIRE_VERSION} \u2014 a ${version > WIRE_VERSION ? "newer" : "older"} deploy. They cannot share state, presence, or a leader seat. https://rxova.org/packages/use-everywhere/under-the-hood/version-skew/`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function getWireSkew(name) {
|
|
298
|
+
return [...skewLedger().get(name) ?? []].sort((a, b) => a - b);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/bus.ts
|
|
223
302
|
function defaultKind() {
|
|
224
303
|
return typeof document === "undefined" ? "worker" : "tab";
|
|
225
304
|
}
|
|
226
|
-
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event"]);
|
|
305
|
+
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event", "op"]);
|
|
227
306
|
function createBusCore(name, options, onShutdown) {
|
|
228
307
|
const transport = (options.transport ?? defaultTransport)(name);
|
|
229
308
|
const clientId = newClientId();
|
|
@@ -244,6 +323,11 @@ function createBusCore(name, options, onShutdown) {
|
|
|
244
323
|
if (SHARED_WITHIN_CLIENT.has(wire.scope)) deliver(wire, from);
|
|
245
324
|
};
|
|
246
325
|
const unsubscribe = transport.subscribe((data) => {
|
|
326
|
+
const foreign = foreignWireVersion(data);
|
|
327
|
+
if (foreign !== null) {
|
|
328
|
+
recordSkew(name, foreign);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
247
331
|
if (!isBusWire(data)) return;
|
|
248
332
|
if (data.clientId === clientId) return;
|
|
249
333
|
emitBusEvent(name, "in", data);
|
|
@@ -336,57 +420,172 @@ function getBus(name, options = {}) {
|
|
|
336
420
|
table.set(name, core);
|
|
337
421
|
} else {
|
|
338
422
|
if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
423
|
+
if (process.env.NODE_ENV !== "production") {
|
|
424
|
+
devWarn(
|
|
425
|
+
"UE1004",
|
|
426
|
+
`bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
342
429
|
}
|
|
343
430
|
if (options.kind !== void 0 && options.kind !== core.kind) {
|
|
344
|
-
|
|
431
|
+
if (process.env.NODE_ENV !== "production") {
|
|
432
|
+
devWarn("UE1005", `bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
|
|
433
|
+
}
|
|
345
434
|
}
|
|
346
435
|
}
|
|
347
436
|
return core.connect();
|
|
348
437
|
}
|
|
349
438
|
|
|
439
|
+
// src/schema.ts
|
|
440
|
+
function validate(schema, payload) {
|
|
441
|
+
const props = schema["~standard"];
|
|
442
|
+
const result = props.validate(payload);
|
|
443
|
+
if (typeof result.then === "function") {
|
|
444
|
+
return { ok: false, issues: [`the "${props.vendor}" schema validates asynchronously`] };
|
|
445
|
+
}
|
|
446
|
+
const sync = result;
|
|
447
|
+
if (sync.issues) return { ok: false, issues: sync.issues.map((issue) => issue.message) };
|
|
448
|
+
return { ok: true, value: sync.value };
|
|
449
|
+
}
|
|
450
|
+
function createGate(name, schemas, onInvalid) {
|
|
451
|
+
if (!schemas) return void 0;
|
|
452
|
+
const check = (key, payload, direction) => {
|
|
453
|
+
const schema = schemas[key];
|
|
454
|
+
if (!schema) return void 0;
|
|
455
|
+
const result = validate(schema, payload);
|
|
456
|
+
if (result.ok) return void 0;
|
|
457
|
+
const info = { name, key, direction, payload, issues: result.issues };
|
|
458
|
+
if (onInvalid) onInvalid(info);
|
|
459
|
+
else if (process.env.NODE_ENV !== "production") {
|
|
460
|
+
devWarn(
|
|
461
|
+
"UE1003",
|
|
462
|
+
`${name}/${key}: ${direction}bound payload rejected by its schema \u2014 ${result.issues.join("; ")}. https://rxova.org/packages/use-everywhere/guides/validating-payloads/`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
return result.issues;
|
|
466
|
+
};
|
|
467
|
+
return {
|
|
468
|
+
accepts: (key, payload) => !check(key, payload, "in"),
|
|
469
|
+
assert(key, payload) {
|
|
470
|
+
const issues = check(key, payload, "out");
|
|
471
|
+
if (issues) {
|
|
472
|
+
throw new TypeError(
|
|
473
|
+
`use-everywhere: ${name}/${key} \u2014 the value does not match its schema, so nothing was sent or applied. ${issues.join("; ")}`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
350
480
|
// src/channel.ts
|
|
351
481
|
function createChannel(name, options = {}) {
|
|
352
482
|
const bus = getBus(name, options);
|
|
353
483
|
const handlers = /* @__PURE__ */ new Map();
|
|
484
|
+
const responders = /* @__PURE__ */ new Map();
|
|
485
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
486
|
+
const gate = createGate(name, options.schema, options.onInvalid);
|
|
487
|
+
const deliver = (type, payload, meta) => {
|
|
488
|
+
const set = handlers.get(type);
|
|
489
|
+
if (!set) return;
|
|
490
|
+
for (const fn of [...set]) fn(payload, meta);
|
|
491
|
+
};
|
|
354
492
|
const unsubscribe = bus.subscribe((wire) => {
|
|
355
493
|
if (wire.scope !== "event") return;
|
|
356
|
-
const set = handlers.get(wire.type);
|
|
357
|
-
if (!set) return;
|
|
358
494
|
const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
495
|
+
if (wire.replyTo !== void 0) {
|
|
496
|
+
const settle = waiting.get(wire.replyTo);
|
|
497
|
+
if (!settle) return;
|
|
498
|
+
waiting.delete(wire.replyTo);
|
|
499
|
+
settle(wire.payload);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const responder = responders.get(wire.type);
|
|
503
|
+
if (responder) {
|
|
366
504
|
bus.post({
|
|
367
505
|
v: 1,
|
|
368
506
|
scope: "event",
|
|
369
|
-
type,
|
|
370
|
-
payload,
|
|
507
|
+
type: wire.type,
|
|
508
|
+
payload: responder(wire.payload, meta),
|
|
371
509
|
clientId: bus.clientId,
|
|
372
510
|
kind: bus.kind,
|
|
373
|
-
msgId: newMsgId()
|
|
511
|
+
msgId: newMsgId(),
|
|
512
|
+
replyTo: wire.msgId
|
|
374
513
|
});
|
|
514
|
+
}
|
|
515
|
+
if (!handlers.has(wire.type)) return;
|
|
516
|
+
if (gate && !gate.accepts(wire.type, wire.payload)) return;
|
|
517
|
+
deliver(wire.type, wire.payload, meta);
|
|
518
|
+
});
|
|
519
|
+
let closed = false;
|
|
520
|
+
const send = (type, payload, msgId) => {
|
|
521
|
+
gate?.assert(type, payload);
|
|
522
|
+
bus.post({
|
|
523
|
+
v: 1,
|
|
524
|
+
scope: "event",
|
|
525
|
+
type,
|
|
526
|
+
payload,
|
|
527
|
+
clientId: bus.clientId,
|
|
528
|
+
kind: bus.kind,
|
|
529
|
+
msgId
|
|
530
|
+
});
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
name,
|
|
534
|
+
clientId: bus.clientId,
|
|
535
|
+
post(type, payload, options2) {
|
|
536
|
+
send(type, payload, newMsgId());
|
|
537
|
+
if (options2?.echo) {
|
|
538
|
+
deliver(type, payload, { clientId: bus.clientId, kind: bus.kind, self: true });
|
|
539
|
+
}
|
|
375
540
|
},
|
|
376
|
-
on(type, handler) {
|
|
541
|
+
on(type, handler, options2) {
|
|
377
542
|
let set = handlers.get(type);
|
|
378
543
|
if (!set) {
|
|
379
544
|
set = /* @__PURE__ */ new Set();
|
|
380
545
|
handlers.set(type, set);
|
|
381
546
|
}
|
|
382
|
-
|
|
383
|
-
|
|
547
|
+
const entry = options2?.once ? (payload, meta) => {
|
|
548
|
+
set.delete(entry);
|
|
549
|
+
handler(payload, meta);
|
|
550
|
+
} : handler;
|
|
551
|
+
set.add(entry);
|
|
552
|
+
return () => set.delete(entry);
|
|
553
|
+
},
|
|
554
|
+
ask(type, payload, options2) {
|
|
555
|
+
const msgId = newMsgId();
|
|
556
|
+
return new Promise((resolve, reject) => {
|
|
557
|
+
const timer = setTimeout(() => {
|
|
558
|
+
waiting.delete(msgId);
|
|
559
|
+
reject(
|
|
560
|
+
new Error(
|
|
561
|
+
`use-everywhere: nobody answered "${type}" on "${name}" within ${String(options2?.timeoutMs ?? 5e3)}ms`
|
|
562
|
+
)
|
|
563
|
+
);
|
|
564
|
+
}, options2?.timeoutMs ?? 5e3);
|
|
565
|
+
waiting.set(msgId, (value) => {
|
|
566
|
+
clearTimeout(timer);
|
|
567
|
+
resolve(value);
|
|
568
|
+
});
|
|
569
|
+
try {
|
|
570
|
+
send(type, payload, msgId);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
clearTimeout(timer);
|
|
573
|
+
waiting.delete(msgId);
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
},
|
|
578
|
+
answer(type, responder) {
|
|
579
|
+
responders.set(type, responder);
|
|
580
|
+
return () => responders.delete(type);
|
|
384
581
|
},
|
|
385
582
|
close() {
|
|
386
583
|
if (closed) return;
|
|
387
584
|
closed = true;
|
|
388
585
|
unsubscribe();
|
|
389
586
|
handlers.clear();
|
|
587
|
+
responders.clear();
|
|
588
|
+
waiting.clear();
|
|
390
589
|
bus.release();
|
|
391
590
|
}
|
|
392
591
|
};
|
|
@@ -437,15 +636,19 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
437
636
|
if (onSharedBus) {
|
|
438
637
|
const live = liveStores.get(name) ?? 0;
|
|
439
638
|
if (live > 0) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
639
|
+
if (process.env.NODE_ENV !== "production") {
|
|
640
|
+
devWarn(
|
|
641
|
+
"UE1001",
|
|
642
|
+
`second shared store for "${name}" in this tab \u2014 they stay in sync, but you are paying twice for one store's state, subscriptions, and persistence writes. Reuse one per name.`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
443
645
|
}
|
|
444
646
|
liveStores.set(name, live + 1);
|
|
445
647
|
}
|
|
446
648
|
const bus = getBus(name, options);
|
|
447
649
|
const clientId = bus.clientId;
|
|
448
650
|
const accept = options.accept;
|
|
651
|
+
const gate = createGate(name, options.schema, options.onInvalid);
|
|
449
652
|
const state = { ...initial };
|
|
450
653
|
const versions = {};
|
|
451
654
|
for (const k in state) {
|
|
@@ -456,23 +659,61 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
456
659
|
let versionsSnapshot = Object.freeze({ ...versions });
|
|
457
660
|
const listeners = /* @__PURE__ */ new Set();
|
|
458
661
|
const keyListeners = /* @__PURE__ */ new Map();
|
|
459
|
-
|
|
662
|
+
let batchDepth = 0;
|
|
663
|
+
const batched = [];
|
|
664
|
+
const rebuild = () => {
|
|
460
665
|
snapshot = Object.freeze({ ...state });
|
|
461
666
|
versionsSnapshot = Object.freeze({ ...versions });
|
|
667
|
+
};
|
|
668
|
+
const emit = (key, value, meta) => {
|
|
462
669
|
for (const fn of listeners) fn(key, value, meta);
|
|
463
670
|
const set = keyListeners.get(key);
|
|
464
671
|
if (set) for (const fn of set) fn();
|
|
672
|
+
};
|
|
673
|
+
function notify(key, value, meta) {
|
|
674
|
+
if (batchDepth > 0) {
|
|
675
|
+
batched.push([key, value, meta]);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
rebuild();
|
|
679
|
+
emit(key, value, meta);
|
|
680
|
+
}
|
|
681
|
+
function batch(fn) {
|
|
682
|
+
batchDepth++;
|
|
683
|
+
try {
|
|
684
|
+
return fn();
|
|
685
|
+
} finally {
|
|
686
|
+
batchDepth--;
|
|
687
|
+
if (batchDepth === 0 && batched.length > 0) {
|
|
688
|
+
const changes = batched.splice(0, batched.length);
|
|
689
|
+
rebuild();
|
|
690
|
+
for (const [key, value, meta] of changes) emit(key, value, meta);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
465
693
|
}
|
|
466
694
|
function applyRemote(key, value, version, meta) {
|
|
467
695
|
if (!newer(version, versions[key])) return;
|
|
696
|
+
if (gate && !gate.accepts(key, value)) return;
|
|
468
697
|
versions[key] = version;
|
|
469
698
|
state[key] = freezeShared(value);
|
|
470
699
|
notify(key, value, meta);
|
|
471
700
|
}
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
701
|
+
const snapshotDelayMs = options.snapshotDelayMs ?? 40;
|
|
702
|
+
let snapshotTimer;
|
|
703
|
+
const written = () => Object.keys(versions).filter((k) => (versions[k]?.[0] ?? 0) > 0);
|
|
704
|
+
const coveredBy = (theirs) => written().every((key) => {
|
|
705
|
+
const mine = versions[key];
|
|
706
|
+
return mine !== void 0 && !newer(mine, theirs[key]);
|
|
707
|
+
});
|
|
708
|
+
const cancelSnapshot = () => {
|
|
709
|
+
clearTimeout(snapshotTimer);
|
|
710
|
+
snapshotTimer = void 0;
|
|
711
|
+
};
|
|
712
|
+
const scheduleSnapshot = () => {
|
|
713
|
+
if (written().length === 0) return;
|
|
714
|
+
if (snapshotTimer !== void 0) return;
|
|
715
|
+
snapshotTimer = setTimeout(() => {
|
|
716
|
+
snapshotTimer = void 0;
|
|
476
717
|
bus.post({
|
|
477
718
|
v: 1,
|
|
478
719
|
scope: "state",
|
|
@@ -482,22 +723,36 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
482
723
|
state: { ...state },
|
|
483
724
|
versions: { ...versions }
|
|
484
725
|
});
|
|
726
|
+
}, Math.random() * snapshotDelayMs);
|
|
727
|
+
};
|
|
728
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
729
|
+
if (wire.scope !== "state") return;
|
|
730
|
+
const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
|
|
731
|
+
if (wire.type === "hello") {
|
|
732
|
+
scheduleSnapshot();
|
|
485
733
|
return;
|
|
486
734
|
}
|
|
487
735
|
if (accept && !accept(meta)) return;
|
|
488
736
|
if (wire.type === "patch") {
|
|
489
737
|
if (typeof wire.key !== "string" || !isVersion(wire.version)) return;
|
|
490
738
|
applyRemote(wire.key, wire.value, wire.version, meta);
|
|
491
|
-
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (wire.type === "snapshot") {
|
|
492
742
|
const versions2 = wire.versions;
|
|
493
743
|
if (typeof versions2 !== "object" || versions2 === null) return;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
744
|
+
if (coveredBy(versions2)) cancelSnapshot();
|
|
745
|
+
batch(() => {
|
|
746
|
+
for (const k in wire.state) {
|
|
747
|
+
const version = versions2[k];
|
|
748
|
+
if (isVersion(version)) applyRemote(k, wire.state[k], version, meta);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
return;
|
|
498
752
|
}
|
|
499
753
|
});
|
|
500
754
|
function setKey(key, value) {
|
|
755
|
+
gate?.assert(key, value);
|
|
501
756
|
const version = [(versions[key]?.[0] ?? 0) + 1, clientId];
|
|
502
757
|
try {
|
|
503
758
|
bus.post({
|
|
@@ -529,29 +784,74 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
529
784
|
});
|
|
530
785
|
const persist = options.persist;
|
|
531
786
|
let flushPersist;
|
|
787
|
+
let settle = () => {
|
|
788
|
+
};
|
|
789
|
+
const hydrated = persist ? new Promise((resolve) => {
|
|
790
|
+
settle = resolve;
|
|
791
|
+
}) : Promise.resolve();
|
|
532
792
|
if (persist) {
|
|
533
|
-
const {
|
|
793
|
+
const {
|
|
794
|
+
adapter,
|
|
795
|
+
keys,
|
|
796
|
+
debounceMs = 100,
|
|
797
|
+
version: schemaVersion = 0,
|
|
798
|
+
migrate,
|
|
799
|
+
onRestoreError
|
|
800
|
+
} = persist;
|
|
534
801
|
const shouldPersist = (key) => !keys || keys.includes(key);
|
|
535
|
-
const
|
|
536
|
-
if (
|
|
537
|
-
|
|
538
|
-
|
|
802
|
+
const refuse = (error) => {
|
|
803
|
+
if (onRestoreError) onRestoreError(error);
|
|
804
|
+
else if (process.env.NODE_ENV !== "production") {
|
|
805
|
+
devWarn(
|
|
806
|
+
"UE1002",
|
|
807
|
+
`${name}: persisted schema v${error.found} not restored, expected v${error.expected} (${error.reason}). https://rxova.org/packages/use-everywhere/guides/persistence/`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
const reconcile = (saved2) => {
|
|
812
|
+
const found = saved2.schema ?? 0;
|
|
813
|
+
if (found === schemaVersion) return { state: saved2.state, migrated: false };
|
|
814
|
+
if (found > schemaVersion) {
|
|
815
|
+
refuse({ reason: "ahead", found, expected: schemaVersion });
|
|
816
|
+
return void 0;
|
|
817
|
+
}
|
|
818
|
+
if (!migrate) {
|
|
819
|
+
refuse({ reason: "no-migrate", found, expected: schemaVersion });
|
|
820
|
+
return void 0;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
return { state: migrate(saved2.state, found), migrated: true };
|
|
824
|
+
} catch (cause) {
|
|
825
|
+
refuse({ reason: "migrate-threw", found, expected: schemaVersion, cause });
|
|
826
|
+
return void 0;
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
const hydrate = (raw) => {
|
|
830
|
+
const reconciled = raw && reconcile(raw);
|
|
831
|
+
if (!reconciled || !raw) {
|
|
832
|
+
settle();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const { state: restored, migrated } = reconciled;
|
|
836
|
+
for (const key in restored) {
|
|
837
|
+
const version = raw.versions[key] ?? (migrated ? [1, clientId] : void 0);
|
|
539
838
|
if (!version || !shouldPersist(key)) continue;
|
|
540
|
-
applyRemote(key,
|
|
839
|
+
applyRemote(key, restored[key], version, { clientId, kind: bus.kind, self: true });
|
|
541
840
|
bus.post({
|
|
542
841
|
v: 1,
|
|
543
842
|
scope: "state",
|
|
544
843
|
type: "patch",
|
|
545
844
|
key,
|
|
546
|
-
value:
|
|
845
|
+
value: restored[key],
|
|
547
846
|
version,
|
|
548
847
|
clientId,
|
|
549
848
|
kind: bus.kind
|
|
550
849
|
});
|
|
551
850
|
}
|
|
851
|
+
settle();
|
|
552
852
|
};
|
|
553
853
|
const collect = () => {
|
|
554
|
-
const out = { v: 1, state: {}, versions: {} };
|
|
854
|
+
const out = { v: 1, schema: schemaVersion, state: {}, versions: {} };
|
|
555
855
|
for (const key in versions) {
|
|
556
856
|
const version = versions[key];
|
|
557
857
|
if (!version || version[0] === 0 || !shouldPersist(key)) continue;
|
|
@@ -591,6 +891,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
591
891
|
let storeClosed = false;
|
|
592
892
|
return {
|
|
593
893
|
clientId,
|
|
894
|
+
hydrated,
|
|
594
895
|
state: proxy,
|
|
595
896
|
getSnapshot: () => snapshot,
|
|
596
897
|
getVersions: () => versionsSnapshot,
|
|
@@ -598,6 +899,9 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
598
899
|
const next = typeof value === "function" ? value(state[key]) : value;
|
|
599
900
|
setKey(key, next);
|
|
600
901
|
},
|
|
902
|
+
transaction(fn) {
|
|
903
|
+
return batch(fn);
|
|
904
|
+
},
|
|
601
905
|
subscribe(fn) {
|
|
602
906
|
listeners.add(fn);
|
|
603
907
|
return () => listeners.delete(fn);
|
|
@@ -629,6 +933,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
629
933
|
if (live > 0) liveStores.set(name, live);
|
|
630
934
|
else liveStores.delete(name);
|
|
631
935
|
}
|
|
936
|
+
cancelSnapshot();
|
|
632
937
|
if (hasWindow) removeEventListener("pageshow", onPageShow);
|
|
633
938
|
if (flushPersist) {
|
|
634
939
|
flushPersist();
|
|
@@ -644,83 +949,6 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
644
949
|
};
|
|
645
950
|
}
|
|
646
951
|
|
|
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
952
|
// src/leader-web-locks.ts
|
|
725
953
|
var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
|
|
726
954
|
function createWebLocksLeader(name, options, locks) {
|
|
@@ -1005,6 +1233,246 @@ function createHeartbeatLeader(name, options) {
|
|
|
1005
1233
|
};
|
|
1006
1234
|
}
|
|
1007
1235
|
|
|
1236
|
+
// src/shared-reducer.ts
|
|
1237
|
+
function createSharedReducer(name, reducer, initial, options = {}) {
|
|
1238
|
+
const key = options.key ?? "default";
|
|
1239
|
+
const bus = getBus(name, options);
|
|
1240
|
+
const clientId = bus.clientId;
|
|
1241
|
+
const leader = options.leader ?? createLeader(name, {
|
|
1242
|
+
...options.leaderOptions,
|
|
1243
|
+
// Spread conditionally: `exactOptionalPropertyTypes` distinguishes an
|
|
1244
|
+
// absent transport from one explicitly set to undefined, and the leader
|
|
1245
|
+
// must default its own rather than be handed a hole.
|
|
1246
|
+
...options.transport ? { transport: options.transport } : {}
|
|
1247
|
+
});
|
|
1248
|
+
const ownsLeader = !options.leader;
|
|
1249
|
+
let committed = initial;
|
|
1250
|
+
let seq = 0;
|
|
1251
|
+
let pending = [];
|
|
1252
|
+
const early = /* @__PURE__ */ new Map();
|
|
1253
|
+
let lastIssued = 0;
|
|
1254
|
+
let view = committed;
|
|
1255
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1256
|
+
let closed = false;
|
|
1257
|
+
const notify = () => {
|
|
1258
|
+
const next = pending.reduce((state, op) => reducer(state, op.action), committed);
|
|
1259
|
+
if (Object.is(next, view)) return;
|
|
1260
|
+
view = next;
|
|
1261
|
+
for (const fn of listeners) fn();
|
|
1262
|
+
};
|
|
1263
|
+
const askForSnapshot = () => bus.post({ v: 1, scope: "op", type: "hello", key, clientId, kind: bus.kind });
|
|
1264
|
+
const receiveCommit = (at, action, opId) => {
|
|
1265
|
+
if (at <= seq) return;
|
|
1266
|
+
if (at > seq + 1) {
|
|
1267
|
+
early.set(at, { action, opId });
|
|
1268
|
+
askForSnapshot();
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
applyCommit(at, action, opId);
|
|
1272
|
+
notify();
|
|
1273
|
+
};
|
|
1274
|
+
const sequence = (action, opId) => {
|
|
1275
|
+
lastIssued = Math.max(lastIssued, seq) + 1;
|
|
1276
|
+
const at = lastIssued;
|
|
1277
|
+
bus.post({
|
|
1278
|
+
v: 1,
|
|
1279
|
+
scope: "op",
|
|
1280
|
+
type: "commit",
|
|
1281
|
+
key,
|
|
1282
|
+
action,
|
|
1283
|
+
opId,
|
|
1284
|
+
seq: at,
|
|
1285
|
+
clientId,
|
|
1286
|
+
kind: bus.kind
|
|
1287
|
+
});
|
|
1288
|
+
receiveCommit(at, action, opId);
|
|
1289
|
+
};
|
|
1290
|
+
const applyCommit = (at, action, opId) => {
|
|
1291
|
+
committed = reducer(committed, action);
|
|
1292
|
+
seq = at;
|
|
1293
|
+
pending = pending.filter((op) => op.opId !== opId);
|
|
1294
|
+
const next = early.get(seq + 1);
|
|
1295
|
+
if (next) {
|
|
1296
|
+
early.delete(seq + 1);
|
|
1297
|
+
applyCommit(seq + 1, next.action, next.opId);
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
1301
|
+
if (wire.scope !== "op" || wire.key !== key) return;
|
|
1302
|
+
if (wire.type === "propose") {
|
|
1303
|
+
if (!leader.getSnapshot().isLeader) return;
|
|
1304
|
+
sequence(wire.action, wire.opId);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (wire.type === "commit") {
|
|
1308
|
+
receiveCommit(wire.seq, wire.action, wire.opId);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (wire.type === "hello") {
|
|
1312
|
+
if (seq === 0) return;
|
|
1313
|
+
bus.post({
|
|
1314
|
+
v: 1,
|
|
1315
|
+
scope: "op",
|
|
1316
|
+
type: "snapshot",
|
|
1317
|
+
key,
|
|
1318
|
+
state: committed,
|
|
1319
|
+
seq,
|
|
1320
|
+
clientId,
|
|
1321
|
+
kind: bus.kind
|
|
1322
|
+
});
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (wire.type === "snapshot") {
|
|
1326
|
+
if (wire.seq <= seq) return;
|
|
1327
|
+
committed = wire.state;
|
|
1328
|
+
seq = wire.seq;
|
|
1329
|
+
for (const at of [...early.keys()]) if (at <= seq) early.delete(at);
|
|
1330
|
+
const next = early.get(seq + 1);
|
|
1331
|
+
if (next) {
|
|
1332
|
+
early.delete(seq + 1);
|
|
1333
|
+
applyCommit(seq + 1, next.action, next.opId);
|
|
1334
|
+
}
|
|
1335
|
+
notify();
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
const dispatch = (action) => {
|
|
1340
|
+
if (closed) return;
|
|
1341
|
+
const opId = newMsgId();
|
|
1342
|
+
pending.push({ opId, action });
|
|
1343
|
+
notify();
|
|
1344
|
+
if (leader.getSnapshot().isLeader) sequence(action, opId);
|
|
1345
|
+
else
|
|
1346
|
+
bus.post({ v: 1, scope: "op", type: "propose", key, action, opId, clientId, kind: bus.kind });
|
|
1347
|
+
};
|
|
1348
|
+
askForSnapshot();
|
|
1349
|
+
return {
|
|
1350
|
+
clientId,
|
|
1351
|
+
getSnapshot: () => view,
|
|
1352
|
+
dispatch,
|
|
1353
|
+
subscribe(fn) {
|
|
1354
|
+
listeners.add(fn);
|
|
1355
|
+
return () => listeners.delete(fn);
|
|
1356
|
+
},
|
|
1357
|
+
pendingCount: () => pending.length,
|
|
1358
|
+
close() {
|
|
1359
|
+
if (closed) return;
|
|
1360
|
+
closed = true;
|
|
1361
|
+
unsubscribe();
|
|
1362
|
+
listeners.clear();
|
|
1363
|
+
if (ownsLeader) leader.close();
|
|
1364
|
+
bus.release();
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/presence.ts
|
|
1370
|
+
function createPresence(name, options = {}) {
|
|
1371
|
+
const pruneAfterMs = options.pruneAfterMs ?? 5e3;
|
|
1372
|
+
const probeGraceMs = options.probeGraceMs ?? 1e3;
|
|
1373
|
+
const bus = getBus(name, options);
|
|
1374
|
+
const peers = /* @__PURE__ */ new Map();
|
|
1375
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1376
|
+
let snapshot = [];
|
|
1377
|
+
let metadata = options.metadata;
|
|
1378
|
+
const same = (a, b) => {
|
|
1379
|
+
if (Object.is(a, b)) return true;
|
|
1380
|
+
try {
|
|
1381
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1382
|
+
} catch {
|
|
1383
|
+
return false;
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
const self = () => ({
|
|
1387
|
+
id: bus.clientId,
|
|
1388
|
+
kind: bus.kind,
|
|
1389
|
+
lastSeen: Date.now(),
|
|
1390
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1391
|
+
});
|
|
1392
|
+
function notify() {
|
|
1393
|
+
const roster = [...peers.values()];
|
|
1394
|
+
snapshot = Object.freeze(options.includeSelf ? [self(), ...roster] : roster);
|
|
1395
|
+
for (const fn of listeners) fn();
|
|
1396
|
+
}
|
|
1397
|
+
const announce2 = () => bus.post({
|
|
1398
|
+
v: 1,
|
|
1399
|
+
scope: "presence",
|
|
1400
|
+
type: "hello",
|
|
1401
|
+
clientId: bus.clientId,
|
|
1402
|
+
kind: bus.kind,
|
|
1403
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1404
|
+
});
|
|
1405
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
1406
|
+
if (wire.clientId === bus.clientId) return;
|
|
1407
|
+
if (wire.scope === "presence" && wire.type === "bye") {
|
|
1408
|
+
if (peers.delete(wire.clientId)) notify();
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
const existing = peers.get(wire.clientId);
|
|
1412
|
+
const announced = wire.scope === "presence" ? wire.metadata : void 0;
|
|
1413
|
+
const next = announced === void 0 ? existing?.metadata : announced;
|
|
1414
|
+
peers.set(wire.clientId, {
|
|
1415
|
+
id: wire.clientId,
|
|
1416
|
+
kind: wire.kind,
|
|
1417
|
+
lastSeen: Date.now(),
|
|
1418
|
+
...next === void 0 ? {} : { metadata: next }
|
|
1419
|
+
});
|
|
1420
|
+
if (!existing || !same(existing.metadata, next)) notify();
|
|
1421
|
+
});
|
|
1422
|
+
announce2();
|
|
1423
|
+
if (options.includeSelf) notify();
|
|
1424
|
+
const probed = /* @__PURE__ */ new Map();
|
|
1425
|
+
const prune = setInterval(
|
|
1426
|
+
() => {
|
|
1427
|
+
const now = Date.now();
|
|
1428
|
+
const silentSince = now - pruneAfterMs;
|
|
1429
|
+
let changed = false;
|
|
1430
|
+
let needProbe = false;
|
|
1431
|
+
for (const [id, peer] of peers) {
|
|
1432
|
+
if (peer.lastSeen >= silentSince) {
|
|
1433
|
+
probed.delete(id);
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
const askedAt = probed.get(id);
|
|
1437
|
+
if (askedAt === void 0) {
|
|
1438
|
+
probed.set(id, now);
|
|
1439
|
+
needProbe = true;
|
|
1440
|
+
} else if (now - askedAt >= probeGraceMs) {
|
|
1441
|
+
peers.delete(id);
|
|
1442
|
+
probed.delete(id);
|
|
1443
|
+
changed = true;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (needProbe) announce2();
|
|
1447
|
+
if (changed) notify();
|
|
1448
|
+
},
|
|
1449
|
+
Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
|
|
1450
|
+
);
|
|
1451
|
+
let closed = false;
|
|
1452
|
+
return {
|
|
1453
|
+
clientId: bus.clientId,
|
|
1454
|
+
getPeers: () => snapshot,
|
|
1455
|
+
setMetadata(next) {
|
|
1456
|
+
if (same(metadata, next)) return;
|
|
1457
|
+
metadata = next;
|
|
1458
|
+
announce2();
|
|
1459
|
+
notify();
|
|
1460
|
+
},
|
|
1461
|
+
subscribe(fn) {
|
|
1462
|
+
listeners.add(fn);
|
|
1463
|
+
return () => listeners.delete(fn);
|
|
1464
|
+
},
|
|
1465
|
+
close() {
|
|
1466
|
+
if (closed) return;
|
|
1467
|
+
closed = true;
|
|
1468
|
+
clearInterval(prune);
|
|
1469
|
+
unsubscribe();
|
|
1470
|
+
listeners.clear();
|
|
1471
|
+
bus.release();
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1008
1476
|
// src/persist-web-storage.ts
|
|
1009
1477
|
function isPersisted(value) {
|
|
1010
1478
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -1012,6 +1480,7 @@ function isPersisted(value) {
|
|
|
1012
1480
|
return candidate.v === 1 && typeof candidate.state === "object" && typeof candidate.versions === "object";
|
|
1013
1481
|
}
|
|
1014
1482
|
function webStorageAdapter(storage, key, options = {}) {
|
|
1483
|
+
const serializer = options.serializer ?? jsonSerializer;
|
|
1015
1484
|
const report = (error, operation) => {
|
|
1016
1485
|
try {
|
|
1017
1486
|
options.onError?.(error, operation);
|
|
@@ -1031,7 +1500,7 @@ function webStorageAdapter(storage, key, options = {}) {
|
|
|
1031
1500
|
try {
|
|
1032
1501
|
const raw = resolve("read")?.getItem(key);
|
|
1033
1502
|
if (!raw) return void 0;
|
|
1034
|
-
const parsed =
|
|
1503
|
+
const parsed = serializer.parse(raw);
|
|
1035
1504
|
return isPersisted(parsed) ? parsed : void 0;
|
|
1036
1505
|
} catch (error) {
|
|
1037
1506
|
report(error, "read");
|
|
@@ -1040,7 +1509,7 @@ function webStorageAdapter(storage, key, options = {}) {
|
|
|
1040
1509
|
},
|
|
1041
1510
|
write(snapshot) {
|
|
1042
1511
|
try {
|
|
1043
|
-
resolve("write")?.setItem(key,
|
|
1512
|
+
resolve("write")?.setItem(key, serializer.stringify(snapshot));
|
|
1044
1513
|
} catch (error) {
|
|
1045
1514
|
report(error, "write");
|
|
1046
1515
|
}
|
|
@@ -1061,6 +1530,58 @@ function sessionStorageAdapter(key, options) {
|
|
|
1061
1530
|
return webStorageAdapter(() => globalThis.sessionStorage, key, options);
|
|
1062
1531
|
}
|
|
1063
1532
|
|
|
1533
|
+
// src/persist-indexeddb.ts
|
|
1534
|
+
var STORE = "state";
|
|
1535
|
+
var request = (req) => new Promise((resolve, reject) => {
|
|
1536
|
+
req.onsuccess = () => resolve(req.result);
|
|
1537
|
+
req.onerror = () => reject(req.error);
|
|
1538
|
+
});
|
|
1539
|
+
function indexedDbAdapter(key, options = {}) {
|
|
1540
|
+
const database = options.database ?? "use-everywhere";
|
|
1541
|
+
let open;
|
|
1542
|
+
const report = (error, operation) => {
|
|
1543
|
+
try {
|
|
1544
|
+
options.onError?.(error, operation);
|
|
1545
|
+
} catch {
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
const db = () => open ?? (open = new Promise((resolve, reject) => {
|
|
1549
|
+
const req = indexedDB.open(database, 1);
|
|
1550
|
+
req.onupgradeneeded = () => req.result.createObjectStore(STORE);
|
|
1551
|
+
req.onsuccess = () => resolve(req.result);
|
|
1552
|
+
req.onerror = () => reject(req.error);
|
|
1553
|
+
req.onblocked = () => reject(new Error("use-everywhere: IndexedDB upgrade blocked"));
|
|
1554
|
+
}));
|
|
1555
|
+
const transact = async (mode, run) => {
|
|
1556
|
+
const connection = await db();
|
|
1557
|
+
return request(run(connection.transaction(STORE, mode).objectStore(STORE)));
|
|
1558
|
+
};
|
|
1559
|
+
return {
|
|
1560
|
+
async read() {
|
|
1561
|
+
try {
|
|
1562
|
+
return await transact("readonly", (store) => store.get(key));
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
report(error, "read");
|
|
1565
|
+
return void 0;
|
|
1566
|
+
}
|
|
1567
|
+
},
|
|
1568
|
+
async write(snapshot) {
|
|
1569
|
+
try {
|
|
1570
|
+
await transact("readwrite", (store) => store.put(snapshot, key));
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
report(error, "write");
|
|
1573
|
+
}
|
|
1574
|
+
},
|
|
1575
|
+
async remove() {
|
|
1576
|
+
try {
|
|
1577
|
+
await transact("readwrite", (store) => store.delete(key));
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
report(error, "remove");
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1064
1585
|
// src/errors/handshake-timeout-error.ts
|
|
1065
1586
|
var HandshakeTimeoutError = class extends Error {
|
|
1066
1587
|
constructor(message = "handshake with the other window timed out") {
|
|
@@ -1295,6 +1816,25 @@ function connectToOpener(options) {
|
|
|
1295
1816
|
}
|
|
1296
1817
|
};
|
|
1297
1818
|
}
|
|
1819
|
+
|
|
1820
|
+
// src/namespace.ts
|
|
1821
|
+
var SEPARATOR = ":";
|
|
1822
|
+
function createNamespace(namespace) {
|
|
1823
|
+
if (!namespace) {
|
|
1824
|
+
throw new TypeError(
|
|
1825
|
+
"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."
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
const busName = (name = DEFAULT_NAME) => `${namespace}${SEPARATOR}${name}`;
|
|
1829
|
+
return {
|
|
1830
|
+
name: namespace,
|
|
1831
|
+
busName,
|
|
1832
|
+
createSharedStore: (name, initial, options) => createSharedStore(busName(name), initial, options),
|
|
1833
|
+
createChannel: (name, options) => createChannel(busName(name), options),
|
|
1834
|
+
createPresence: (name, options) => createPresence(busName(name), options),
|
|
1835
|
+
createLeader: (name, options) => createLeader(busName(name), options)
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1298
1838
|
export {
|
|
1299
1839
|
BroadcastChannelTransport,
|
|
1300
1840
|
CID_PARAM,
|
|
@@ -1302,18 +1842,24 @@ export {
|
|
|
1302
1842
|
HandshakeTimeoutError,
|
|
1303
1843
|
NoopTransport,
|
|
1304
1844
|
StorageTransport,
|
|
1845
|
+
WIRE_VERSION,
|
|
1305
1846
|
WindowClosedError,
|
|
1306
1847
|
connectToOpener,
|
|
1307
1848
|
createChannel,
|
|
1308
1849
|
createLeader,
|
|
1850
|
+
createNamespace,
|
|
1309
1851
|
createPresence,
|
|
1852
|
+
createSharedReducer,
|
|
1310
1853
|
createSharedStore,
|
|
1311
1854
|
defaultTransport,
|
|
1312
1855
|
enableDebug,
|
|
1313
1856
|
getBusNames,
|
|
1314
1857
|
getTransportKind,
|
|
1858
|
+
getWireSkew,
|
|
1859
|
+
indexedDbAdapter,
|
|
1315
1860
|
isBroadcastChannelAvailable,
|
|
1316
1861
|
isStorageEventAvailable,
|
|
1862
|
+
jsonSerializer,
|
|
1317
1863
|
localStorageAdapter,
|
|
1318
1864
|
newer,
|
|
1319
1865
|
observeBus,
|