@use-everywhere/core 0.6.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 +981 -127
- package/dist/index.d.cts +736 -47
- package/dist/index.d.ts +736 -47
- package/dist/index.js +972 -127
- package/dist/testing.cjs +1 -0
- package/dist/testing.d.cts +2 -1
- package/dist/testing.d.ts +2 -1
- package/dist/testing.js +1 -0
- package/dist/transport.types-CV1WZOhy.d.cts +20 -0
- package/dist/transport.types-CV1WZOhy.d.ts +20 -0
- package/package.json +30 -12
- package/dist/transport.types-tQ1cu6Xm.d.cts +0 -12
- package/dist/transport.types-tQ1cu6Xm.d.ts +0 -12
package/dist/index.cjs
CHANGED
|
@@ -25,16 +25,25 @@ __export(src_exports, {
|
|
|
25
25
|
DEFAULT_NAME: () => DEFAULT_NAME,
|
|
26
26
|
HandshakeTimeoutError: () => HandshakeTimeoutError,
|
|
27
27
|
NoopTransport: () => NoopTransport,
|
|
28
|
+
StorageTransport: () => StorageTransport,
|
|
29
|
+
WIRE_VERSION: () => WIRE_VERSION,
|
|
28
30
|
WindowClosedError: () => WindowClosedError,
|
|
29
31
|
connectToOpener: () => connectToOpener,
|
|
30
32
|
createChannel: () => createChannel,
|
|
31
33
|
createLeader: () => createLeader,
|
|
34
|
+
createNamespace: () => createNamespace,
|
|
32
35
|
createPresence: () => createPresence,
|
|
36
|
+
createSharedReducer: () => createSharedReducer,
|
|
33
37
|
createSharedStore: () => createSharedStore,
|
|
34
38
|
defaultTransport: () => defaultTransport,
|
|
35
39
|
enableDebug: () => enableDebug,
|
|
36
40
|
getBusNames: () => getBusNames,
|
|
41
|
+
getTransportKind: () => getTransportKind,
|
|
42
|
+
getWireSkew: () => getWireSkew,
|
|
43
|
+
indexedDbAdapter: () => indexedDbAdapter,
|
|
37
44
|
isBroadcastChannelAvailable: () => isBroadcastChannelAvailable,
|
|
45
|
+
isStorageEventAvailable: () => isStorageEventAvailable,
|
|
46
|
+
jsonSerializer: () => jsonSerializer,
|
|
38
47
|
localStorageAdapter: () => localStorageAdapter,
|
|
39
48
|
newer: () => newer,
|
|
40
49
|
observeBus: () => observeBus,
|
|
@@ -106,9 +115,11 @@ function randomHex(bytes) {
|
|
|
106
115
|
for (const b of buf) out2 += b.toString(16).padStart(2, "0");
|
|
107
116
|
return out2;
|
|
108
117
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
123
|
let out = "";
|
|
113
124
|
while (out.length < bytes * 2)
|
|
114
125
|
out += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
|
|
@@ -121,9 +132,38 @@ function newMsgId() {
|
|
|
121
132
|
return randomHex(16);
|
|
122
133
|
}
|
|
123
134
|
|
|
135
|
+
// src/rendezvous.ts
|
|
136
|
+
var PROTOCOL = 1;
|
|
137
|
+
var TABLE = /* @__PURE__ */ Symbol.for(`use-everywhere.rendezvous.${PROTOCOL}`);
|
|
138
|
+
var CENSUS = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.census");
|
|
139
|
+
var SKEW = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.skew");
|
|
140
|
+
function announce() {
|
|
141
|
+
const g = globalThis;
|
|
142
|
+
const census = g[CENSUS] ?? (g[CENSUS] = { protocols: [] });
|
|
143
|
+
if (census.protocols.includes(PROTOCOL)) return;
|
|
144
|
+
census.protocols.push(PROTOCOL);
|
|
145
|
+
if (census.protocols.length > 1) {
|
|
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
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function busTable() {
|
|
154
|
+
announce();
|
|
155
|
+
const g = globalThis;
|
|
156
|
+
return g[TABLE] ?? (g[TABLE] = /* @__PURE__ */ new Map());
|
|
157
|
+
}
|
|
158
|
+
function skewLedger() {
|
|
159
|
+
const g = globalThis;
|
|
160
|
+
return g[SKEW] ?? (g[SKEW] = /* @__PURE__ */ new Map());
|
|
161
|
+
}
|
|
162
|
+
|
|
124
163
|
// src/transport/broadcast-channel-transport.ts
|
|
125
164
|
var BroadcastChannelTransport = class {
|
|
126
165
|
constructor(name) {
|
|
166
|
+
this.kind = "broadcast-channel";
|
|
127
167
|
this.listeners = /* @__PURE__ */ new Set();
|
|
128
168
|
this.bc = new BroadcastChannel(name);
|
|
129
169
|
this.bc.onmessage = (event) => {
|
|
@@ -145,6 +185,9 @@ var BroadcastChannelTransport = class {
|
|
|
145
185
|
|
|
146
186
|
// src/transport/noop-transport.ts
|
|
147
187
|
var NoopTransport = class {
|
|
188
|
+
constructor() {
|
|
189
|
+
this.kind = "none";
|
|
190
|
+
}
|
|
148
191
|
post() {
|
|
149
192
|
}
|
|
150
193
|
subscribe() {
|
|
@@ -155,157 +198,434 @@ var NoopTransport = class {
|
|
|
155
198
|
}
|
|
156
199
|
};
|
|
157
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
|
+
|
|
230
|
+
// src/transport/storage-transport.ts
|
|
231
|
+
var StorageTransport = class {
|
|
232
|
+
constructor(name, storage = globalThis.localStorage, serializer = jsonSerializer) {
|
|
233
|
+
this.kind = "storage";
|
|
234
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
235
|
+
this.seq = 0;
|
|
236
|
+
if (!storage) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"[use-everywhere] StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
this.serializer = serializer;
|
|
242
|
+
this.key = `use-everywhere:bus:${name}`;
|
|
243
|
+
this.storage = storage;
|
|
244
|
+
this.onStorage = (event) => {
|
|
245
|
+
if (event.key !== this.key || event.newValue === null) return;
|
|
246
|
+
let payload;
|
|
247
|
+
try {
|
|
248
|
+
payload = this.serializer.parse(event.newValue);
|
|
249
|
+
} catch {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
for (const listener of this.listeners) listener(payload.data);
|
|
253
|
+
};
|
|
254
|
+
addEventListener("storage", this.onStorage);
|
|
255
|
+
}
|
|
256
|
+
post(data) {
|
|
257
|
+
const envelope = this.serializer.stringify({ seq: this.seq++, data });
|
|
258
|
+
try {
|
|
259
|
+
this.storage.setItem(this.key, envelope);
|
|
260
|
+
this.storage.removeItem(this.key);
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
subscribe(listener) {
|
|
265
|
+
this.listeners.add(listener);
|
|
266
|
+
return () => this.listeners.delete(listener);
|
|
267
|
+
}
|
|
268
|
+
close() {
|
|
269
|
+
this.listeners.clear();
|
|
270
|
+
removeEventListener("storage", this.onStorage);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
158
274
|
// src/transport/default-transport.ts
|
|
159
275
|
function isBroadcastChannelAvailable() {
|
|
160
276
|
return typeof BroadcastChannel !== "undefined";
|
|
161
277
|
}
|
|
278
|
+
function isStorageEventAvailable() {
|
|
279
|
+
if (typeof addEventListener !== "function") return false;
|
|
280
|
+
try {
|
|
281
|
+
const storage = globalThis.localStorage;
|
|
282
|
+
if (!storage) return false;
|
|
283
|
+
const probe = "use-everywhere:probe";
|
|
284
|
+
storage.setItem(probe, "1");
|
|
285
|
+
storage.removeItem(probe);
|
|
286
|
+
return true;
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
162
291
|
function defaultTransport(name) {
|
|
163
|
-
|
|
292
|
+
if (isBroadcastChannelAvailable()) return new BroadcastChannelTransport(name);
|
|
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") {
|
|
302
|
+
devWarn(
|
|
303
|
+
"[use-everywhere] no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return new NoopTransport();
|
|
164
307
|
}
|
|
165
308
|
|
|
166
|
-
// 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
|
+
}
|
|
167
314
|
function isBusWire(data) {
|
|
168
|
-
|
|
315
|
+
if (typeof data !== "object" || data === null) return false;
|
|
316
|
+
const wire = data;
|
|
317
|
+
return wire.v === WIRE_VERSION && hasEnvelopeShape(wire);
|
|
169
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);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/bus.ts
|
|
170
342
|
function defaultKind() {
|
|
171
343
|
return typeof document === "undefined" ? "worker" : "tab";
|
|
172
344
|
}
|
|
173
|
-
var
|
|
174
|
-
function
|
|
345
|
+
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event", "op"]);
|
|
346
|
+
function createBusCore(name, options, onShutdown) {
|
|
175
347
|
const transport = (options.transport ?? defaultTransport)(name);
|
|
176
348
|
const clientId = newClientId();
|
|
177
349
|
const kind = options.kind ?? defaultKind();
|
|
178
|
-
const
|
|
350
|
+
const handles = /* @__PURE__ */ new Set();
|
|
179
351
|
let refs = 0;
|
|
180
352
|
let closed = false;
|
|
181
|
-
const
|
|
353
|
+
const deliver = (wire, from) => {
|
|
354
|
+
for (const handle of handles) {
|
|
355
|
+
if (handle === from) continue;
|
|
356
|
+
for (const fn of handle.listeners) fn(wire);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const post = (wire, from) => {
|
|
182
360
|
if (closed) return;
|
|
183
361
|
emitBusEvent(name, "out", wire);
|
|
184
362
|
transport.post(wire);
|
|
363
|
+
if (SHARED_WITHIN_CLIENT.has(wire.scope)) deliver(wire, from);
|
|
185
364
|
};
|
|
186
365
|
const unsubscribe = transport.subscribe((data) => {
|
|
366
|
+
const foreign = foreignWireVersion(data);
|
|
367
|
+
if (foreign !== null) {
|
|
368
|
+
recordSkew(name, foreign);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
187
371
|
if (!isBusWire(data)) return;
|
|
188
372
|
if (data.clientId === clientId) return;
|
|
189
373
|
emitBusEvent(name, "in", data);
|
|
190
374
|
if (data.scope === "presence" && data.type === "hello") {
|
|
191
|
-
post({ v: 1, scope: "presence", type: "ping", clientId, kind });
|
|
375
|
+
post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null);
|
|
192
376
|
}
|
|
193
|
-
|
|
377
|
+
deliver(data, null);
|
|
194
378
|
});
|
|
195
|
-
post({ v: 1, scope: "presence", type: "hello", clientId, kind });
|
|
379
|
+
post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
|
|
196
380
|
const heartbeat = setInterval(
|
|
197
|
-
() => post({ v: 1, scope: "presence", type: "ping", clientId, kind }),
|
|
381
|
+
() => post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null),
|
|
198
382
|
options.heartbeatMs ?? 2e3
|
|
199
383
|
);
|
|
200
|
-
const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind });
|
|
384
|
+
const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind }, null);
|
|
201
385
|
const onPageShow = (event) => {
|
|
202
386
|
if (!event.persisted) return;
|
|
203
|
-
post({ v: 1, scope: "presence", type: "hello", clientId, kind });
|
|
387
|
+
post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
|
|
388
|
+
};
|
|
389
|
+
const onVisible = () => {
|
|
390
|
+
if (document.visibilityState === "visible") {
|
|
391
|
+
post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
|
|
392
|
+
}
|
|
204
393
|
};
|
|
205
394
|
const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
|
|
206
395
|
if (hasWindow) {
|
|
207
396
|
addEventListener("pagehide", sayBye);
|
|
208
397
|
addEventListener("pageshow", onPageShow);
|
|
398
|
+
addEventListener("visibilitychange", onVisible);
|
|
209
399
|
}
|
|
400
|
+
const shutdown = () => {
|
|
401
|
+
sayBye();
|
|
402
|
+
closed = true;
|
|
403
|
+
clearInterval(heartbeat);
|
|
404
|
+
if (hasWindow) {
|
|
405
|
+
removeEventListener("pagehide", sayBye);
|
|
406
|
+
removeEventListener("pageshow", onPageShow);
|
|
407
|
+
removeEventListener("visibilitychange", onVisible);
|
|
408
|
+
}
|
|
409
|
+
unsubscribe();
|
|
410
|
+
transport.close();
|
|
411
|
+
handles.clear();
|
|
412
|
+
onShutdown();
|
|
413
|
+
};
|
|
210
414
|
return {
|
|
211
415
|
name,
|
|
212
416
|
clientId,
|
|
213
417
|
kind,
|
|
418
|
+
transportKind: transport.kind ?? "custom",
|
|
214
419
|
heartbeatMs: options.heartbeatMs ?? 2e3,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return () => listeners.delete(fn);
|
|
219
|
-
},
|
|
220
|
-
acquire() {
|
|
420
|
+
connect() {
|
|
421
|
+
const handle = { listeners: /* @__PURE__ */ new Set() };
|
|
422
|
+
handles.add(handle);
|
|
221
423
|
refs++;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
424
|
+
let released = false;
|
|
425
|
+
return {
|
|
426
|
+
name,
|
|
427
|
+
clientId,
|
|
428
|
+
kind,
|
|
429
|
+
transportKind: transport.kind ?? "custom",
|
|
430
|
+
post: (wire) => post(wire, handle),
|
|
431
|
+
subscribe(fn) {
|
|
432
|
+
handle.listeners.add(fn);
|
|
433
|
+
return () => handle.listeners.delete(fn);
|
|
434
|
+
},
|
|
435
|
+
release() {
|
|
436
|
+
if (released || closed) return;
|
|
437
|
+
released = true;
|
|
438
|
+
handles.delete(handle);
|
|
439
|
+
handle.listeners.clear();
|
|
440
|
+
refs--;
|
|
441
|
+
if (refs === 0) shutdown();
|
|
442
|
+
}
|
|
443
|
+
};
|
|
238
444
|
}
|
|
239
445
|
};
|
|
240
446
|
}
|
|
241
447
|
function getBusNames() {
|
|
242
|
-
return [...
|
|
448
|
+
return [...busTable().keys()];
|
|
449
|
+
}
|
|
450
|
+
function getTransportKind(name) {
|
|
451
|
+
return busTable().get(name)?.transportKind ?? null;
|
|
243
452
|
}
|
|
244
453
|
function getBus(name, options = {}) {
|
|
245
|
-
if (options.transport) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (!bus) {
|
|
253
|
-
bus = createBus(name, options, () => registry.delete(name));
|
|
254
|
-
registry.set(name, bus);
|
|
454
|
+
if (options.transport) return createBusCore(name, options, () => {
|
|
455
|
+
}).connect();
|
|
456
|
+
const table = busTable();
|
|
457
|
+
let core = table.get(name);
|
|
458
|
+
if (!core) {
|
|
459
|
+
core = createBusCore(name, options, () => table.delete(name));
|
|
460
|
+
table.set(name, core);
|
|
255
461
|
} else {
|
|
256
|
-
if (options.heartbeatMs !== void 0 && options.heartbeatMs !==
|
|
462
|
+
if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
|
|
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
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (options.kind !== void 0 && options.kind !== core.kind) {
|
|
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
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return core.connect();
|
|
478
|
+
}
|
|
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") {
|
|
257
501
|
devWarn(
|
|
258
|
-
`[use-everywhere]
|
|
502
|
+
`[use-everywhere] ${name}/${key}: ${direction}bound payload rejected by its schema \u2014 ${result.issues.join("; ")}. https://rxova.org/guides/validating-payloads/`
|
|
259
503
|
);
|
|
260
504
|
}
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
}
|
|
263
516
|
}
|
|
264
|
-
}
|
|
265
|
-
bus.acquire();
|
|
266
|
-
return bus;
|
|
517
|
+
};
|
|
267
518
|
}
|
|
268
519
|
|
|
269
520
|
// src/channel.ts
|
|
270
521
|
function createChannel(name, options = {}) {
|
|
271
522
|
const bus = getBus(name, options);
|
|
272
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
|
+
};
|
|
273
532
|
const unsubscribe = bus.subscribe((wire) => {
|
|
274
533
|
if (wire.scope !== "event") return;
|
|
275
|
-
const set = handlers.get(wire.type);
|
|
276
|
-
if (!set) return;
|
|
277
534
|
const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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) {
|
|
285
544
|
bus.post({
|
|
286
545
|
v: 1,
|
|
287
546
|
scope: "event",
|
|
288
|
-
type,
|
|
289
|
-
payload,
|
|
547
|
+
type: wire.type,
|
|
548
|
+
payload: responder(wire.payload, meta),
|
|
290
549
|
clientId: bus.clientId,
|
|
291
550
|
kind: bus.kind,
|
|
292
|
-
msgId: newMsgId()
|
|
551
|
+
msgId: newMsgId(),
|
|
552
|
+
replyTo: wire.msgId
|
|
293
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
|
+
}
|
|
294
580
|
},
|
|
295
|
-
on(type, handler) {
|
|
581
|
+
on(type, handler, options2) {
|
|
296
582
|
let set = handlers.get(type);
|
|
297
583
|
if (!set) {
|
|
298
584
|
set = /* @__PURE__ */ new Set();
|
|
299
585
|
handlers.set(type, set);
|
|
300
586
|
}
|
|
301
|
-
|
|
302
|
-
|
|
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);
|
|
303
621
|
},
|
|
304
622
|
close() {
|
|
305
623
|
if (closed) return;
|
|
306
624
|
closed = true;
|
|
307
625
|
unsubscribe();
|
|
308
626
|
handlers.clear();
|
|
627
|
+
responders.clear();
|
|
628
|
+
waiting.clear();
|
|
309
629
|
bus.release();
|
|
310
630
|
}
|
|
311
631
|
};
|
|
@@ -315,6 +635,9 @@ function createChannel(name, options = {}) {
|
|
|
315
635
|
function newer(a, b) {
|
|
316
636
|
return !b || a[0] > b[0] || a[0] === b[0] && a[1] > b[1];
|
|
317
637
|
}
|
|
638
|
+
function isVersion(value) {
|
|
639
|
+
return Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && Number.isFinite(value[0]) && typeof value[1] === "string";
|
|
640
|
+
}
|
|
318
641
|
|
|
319
642
|
// src/dev-freeze.ts
|
|
320
643
|
var inDev2 = false;
|
|
@@ -353,15 +676,18 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
353
676
|
if (onSharedBus) {
|
|
354
677
|
const live = liveStores.get(name) ?? 0;
|
|
355
678
|
if (live > 0) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
+
}
|
|
359
684
|
}
|
|
360
685
|
liveStores.set(name, live + 1);
|
|
361
686
|
}
|
|
362
687
|
const bus = getBus(name, options);
|
|
363
688
|
const clientId = bus.clientId;
|
|
364
689
|
const accept = options.accept;
|
|
690
|
+
const gate = createGate(name, options.schema, options.onInvalid);
|
|
365
691
|
const state = { ...initial };
|
|
366
692
|
const versions = {};
|
|
367
693
|
for (const k in state) {
|
|
@@ -372,23 +698,61 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
372
698
|
let versionsSnapshot = Object.freeze({ ...versions });
|
|
373
699
|
const listeners = /* @__PURE__ */ new Set();
|
|
374
700
|
const keyListeners = /* @__PURE__ */ new Map();
|
|
375
|
-
|
|
701
|
+
let batchDepth = 0;
|
|
702
|
+
const batched = [];
|
|
703
|
+
const rebuild = () => {
|
|
376
704
|
snapshot = Object.freeze({ ...state });
|
|
377
705
|
versionsSnapshot = Object.freeze({ ...versions });
|
|
706
|
+
};
|
|
707
|
+
const emit = (key, value, meta) => {
|
|
378
708
|
for (const fn of listeners) fn(key, value, meta);
|
|
379
709
|
const set = keyListeners.get(key);
|
|
380
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
|
+
}
|
|
381
732
|
}
|
|
382
733
|
function applyRemote(key, value, version, meta) {
|
|
383
734
|
if (!newer(version, versions[key])) return;
|
|
735
|
+
if (gate && !gate.accepts(key, value)) return;
|
|
384
736
|
versions[key] = version;
|
|
385
737
|
state[key] = freezeShared(value);
|
|
386
738
|
notify(key, value, meta);
|
|
387
739
|
}
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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;
|
|
392
756
|
bus.post({
|
|
393
757
|
v: 1,
|
|
394
758
|
scope: "state",
|
|
@@ -398,19 +762,36 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
398
762
|
state: { ...state },
|
|
399
763
|
versions: { ...versions }
|
|
400
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();
|
|
401
772
|
return;
|
|
402
773
|
}
|
|
403
774
|
if (accept && !accept(meta)) return;
|
|
404
775
|
if (wire.type === "patch") {
|
|
776
|
+
if (typeof wire.key !== "string" || !isVersion(wire.version)) return;
|
|
405
777
|
applyRemote(wire.key, wire.value, wire.version, meta);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (wire.type === "snapshot") {
|
|
781
|
+
const versions2 = wire.versions;
|
|
782
|
+
if (typeof versions2 !== "object" || versions2 === null) return;
|
|
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;
|
|
411
791
|
}
|
|
412
792
|
});
|
|
413
793
|
function setKey(key, value) {
|
|
794
|
+
gate?.assert(key, value);
|
|
414
795
|
const version = [(versions[key]?.[0] ?? 0) + 1, clientId];
|
|
415
796
|
try {
|
|
416
797
|
bus.post({
|
|
@@ -442,29 +823,73 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
442
823
|
});
|
|
443
824
|
const persist = options.persist;
|
|
444
825
|
let flushPersist;
|
|
826
|
+
let settle = () => {
|
|
827
|
+
};
|
|
828
|
+
const hydrated = persist ? new Promise((resolve) => {
|
|
829
|
+
settle = resolve;
|
|
830
|
+
}) : Promise.resolve();
|
|
445
831
|
if (persist) {
|
|
446
|
-
const {
|
|
832
|
+
const {
|
|
833
|
+
adapter,
|
|
834
|
+
keys,
|
|
835
|
+
debounceMs = 100,
|
|
836
|
+
version: schemaVersion = 0,
|
|
837
|
+
migrate,
|
|
838
|
+
onRestoreError
|
|
839
|
+
} = persist;
|
|
447
840
|
const shouldPersist = (key) => !keys || keys.includes(key);
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
450
|
-
|
|
451
|
-
|
|
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);
|
|
452
876
|
if (!version || !shouldPersist(key)) continue;
|
|
453
|
-
applyRemote(key,
|
|
877
|
+
applyRemote(key, restored[key], version, { clientId, kind: bus.kind, self: true });
|
|
454
878
|
bus.post({
|
|
455
879
|
v: 1,
|
|
456
880
|
scope: "state",
|
|
457
881
|
type: "patch",
|
|
458
882
|
key,
|
|
459
|
-
value:
|
|
883
|
+
value: restored[key],
|
|
460
884
|
version,
|
|
461
885
|
clientId,
|
|
462
886
|
kind: bus.kind
|
|
463
887
|
});
|
|
464
888
|
}
|
|
889
|
+
settle();
|
|
465
890
|
};
|
|
466
891
|
const collect = () => {
|
|
467
|
-
const out = { v: 1, state: {}, versions: {} };
|
|
892
|
+
const out = { v: 1, schema: schemaVersion, state: {}, versions: {} };
|
|
468
893
|
for (const key in versions) {
|
|
469
894
|
const version = versions[key];
|
|
470
895
|
if (!version || version[0] === 0 || !shouldPersist(key)) continue;
|
|
@@ -504,6 +929,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
504
929
|
let storeClosed = false;
|
|
505
930
|
return {
|
|
506
931
|
clientId,
|
|
932
|
+
hydrated,
|
|
507
933
|
state: proxy,
|
|
508
934
|
getSnapshot: () => snapshot,
|
|
509
935
|
getVersions: () => versionsSnapshot,
|
|
@@ -511,6 +937,9 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
511
937
|
const next = typeof value === "function" ? value(state[key]) : value;
|
|
512
938
|
setKey(key, next);
|
|
513
939
|
},
|
|
940
|
+
transaction(fn) {
|
|
941
|
+
return batch(fn);
|
|
942
|
+
},
|
|
514
943
|
subscribe(fn) {
|
|
515
944
|
listeners.add(fn);
|
|
516
945
|
return () => listeners.delete(fn);
|
|
@@ -542,6 +971,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
542
971
|
if (live > 0) liveStores.set(name, live);
|
|
543
972
|
else liveStores.delete(name);
|
|
544
973
|
}
|
|
974
|
+
cancelSnapshot();
|
|
545
975
|
if (hasWindow) removeEventListener("pageshow", onPageShow);
|
|
546
976
|
if (flushPersist) {
|
|
547
977
|
flushPersist();
|
|
@@ -557,62 +987,149 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
557
987
|
};
|
|
558
988
|
}
|
|
559
989
|
|
|
560
|
-
// src/
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
|
|
990
|
+
// src/leader-web-locks.ts
|
|
991
|
+
var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
|
|
992
|
+
function createWebLocksLeader(name, options, locks) {
|
|
993
|
+
const busOptions = {
|
|
994
|
+
...options.transport ? { transport: options.transport } : {},
|
|
995
|
+
...options.kind ? { kind: options.kind } : {}
|
|
996
|
+
};
|
|
997
|
+
const bus = getBus(name, busOptions);
|
|
998
|
+
const clientId = bus.clientId;
|
|
999
|
+
let eligible = options.eligible ?? true;
|
|
1000
|
+
let leaderId = null;
|
|
1001
|
+
let snapshot = NO_LEADER;
|
|
1002
|
+
let closed = false;
|
|
565
1003
|
const listeners = /* @__PURE__ */ new Set();
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1004
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
1005
|
+
let releaseHeld = null;
|
|
1006
|
+
let pending = null;
|
|
1007
|
+
function setLeader(id) {
|
|
1008
|
+
if (id === leaderId) return;
|
|
1009
|
+
leaderId = id;
|
|
1010
|
+
snapshot = Object.freeze({ leaderId: id, isLeader: id === clientId });
|
|
569
1011
|
for (const fn of listeners) fn();
|
|
1012
|
+
if (id === clientId) {
|
|
1013
|
+
for (const waiter of waiters) waiter.resolve();
|
|
1014
|
+
waiters.clear();
|
|
1015
|
+
}
|
|
570
1016
|
}
|
|
1017
|
+
const term = [1, clientId];
|
|
1018
|
+
const announce2 = () => bus.post({ v: 1, scope: "leader", type: "heartbeat", term, clientId, kind: bus.kind });
|
|
571
1019
|
const unsubscribe = bus.subscribe((wire) => {
|
|
572
|
-
if (wire.scope
|
|
573
|
-
|
|
1020
|
+
if (wire.scope !== "leader") return;
|
|
1021
|
+
if (wire.type === "hello") {
|
|
1022
|
+
if (leaderId === clientId) announce2();
|
|
574
1023
|
return;
|
|
575
1024
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
1025
|
+
if (wire.type === "resign") {
|
|
1026
|
+
if (wire.clientId === leaderId) setLeader(null);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
setLeader(wire.clientId);
|
|
579
1030
|
});
|
|
580
|
-
|
|
581
|
-
()
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
1031
|
+
function joinQueue() {
|
|
1032
|
+
if (closed || !eligible || releaseHeld || pending) return;
|
|
1033
|
+
const controller = new AbortController();
|
|
1034
|
+
pending = controller;
|
|
1035
|
+
const held = new Promise((resolve) => {
|
|
1036
|
+
releaseHeld = resolve;
|
|
1037
|
+
});
|
|
1038
|
+
void locks.request(name, { signal: controller.signal }, async () => {
|
|
1039
|
+
pending = null;
|
|
1040
|
+
if (closed || !eligible) return;
|
|
1041
|
+
setLeader(clientId);
|
|
1042
|
+
announce2();
|
|
1043
|
+
await held;
|
|
1044
|
+
}).catch(() => {
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
function letGo() {
|
|
1048
|
+
const release = releaseHeld;
|
|
1049
|
+
releaseHeld = null;
|
|
1050
|
+
release?.();
|
|
1051
|
+
}
|
|
1052
|
+
function resign() {
|
|
1053
|
+
if (leaderId !== clientId) return;
|
|
1054
|
+
letGo();
|
|
1055
|
+
setLeader(null);
|
|
1056
|
+
bus.post({ v: 1, scope: "leader", type: "resign", term, clientId, kind: bus.kind });
|
|
1057
|
+
joinQueue();
|
|
1058
|
+
}
|
|
1059
|
+
const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
|
|
1060
|
+
const onPageHide = () => resign();
|
|
1061
|
+
if (hasWindow) addEventListener("pagehide", onPageHide);
|
|
1062
|
+
bus.post({ v: 1, scope: "leader", type: "hello", clientId, kind: bus.kind });
|
|
1063
|
+
joinQueue();
|
|
595
1064
|
return {
|
|
596
|
-
clientId
|
|
597
|
-
|
|
1065
|
+
clientId,
|
|
1066
|
+
strategy: "web-locks",
|
|
1067
|
+
getSnapshot: () => snapshot,
|
|
598
1068
|
subscribe(fn) {
|
|
599
1069
|
listeners.add(fn);
|
|
600
1070
|
return () => listeners.delete(fn);
|
|
601
1071
|
},
|
|
1072
|
+
waitForLeadership() {
|
|
1073
|
+
if (leaderId === clientId) return Promise.resolve();
|
|
1074
|
+
if (closed) return Promise.reject(new Error("leader is closed"));
|
|
1075
|
+
return new Promise((resolve, reject) => {
|
|
1076
|
+
waiters.add({ resolve, reject });
|
|
1077
|
+
});
|
|
1078
|
+
},
|
|
1079
|
+
resign,
|
|
1080
|
+
setEligible(next) {
|
|
1081
|
+
if (next === eligible) return;
|
|
1082
|
+
eligible = next;
|
|
1083
|
+
if (next) {
|
|
1084
|
+
joinQueue();
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (leaderId === clientId) resignWithoutRequeue();
|
|
1088
|
+
pending?.abort();
|
|
1089
|
+
pending = null;
|
|
1090
|
+
},
|
|
602
1091
|
close() {
|
|
603
1092
|
if (closed) return;
|
|
604
1093
|
closed = true;
|
|
605
|
-
|
|
1094
|
+
if (leaderId === clientId) resignWithoutRequeue();
|
|
1095
|
+
pending?.abort();
|
|
1096
|
+
pending = null;
|
|
1097
|
+
if (hasWindow) removeEventListener("pagehide", onPageHide);
|
|
1098
|
+
for (const waiter of waiters) waiter.reject(new Error("leader is closed"));
|
|
1099
|
+
waiters.clear();
|
|
606
1100
|
unsubscribe();
|
|
607
1101
|
listeners.clear();
|
|
608
1102
|
bus.release();
|
|
609
1103
|
}
|
|
610
1104
|
};
|
|
1105
|
+
function resignWithoutRequeue() {
|
|
1106
|
+
letGo();
|
|
1107
|
+
setLeader(null);
|
|
1108
|
+
bus.post({ v: 1, scope: "leader", type: "resign", term, clientId, kind: bus.kind });
|
|
1109
|
+
}
|
|
611
1110
|
}
|
|
612
1111
|
|
|
613
1112
|
// src/leader.ts
|
|
614
|
-
var
|
|
1113
|
+
var NO_LEADER2 = Object.freeze({ leaderId: null, isLeader: false });
|
|
1114
|
+
function availableLocks(options) {
|
|
1115
|
+
if (options.locks) return options.locks;
|
|
1116
|
+
const locks = globalThis.navigator?.locks;
|
|
1117
|
+
return typeof locks?.request === "function" ? locks : void 0;
|
|
1118
|
+
}
|
|
615
1119
|
function createLeader(name, options = {}) {
|
|
1120
|
+
const strategy = options.strategy ?? "auto";
|
|
1121
|
+
if (strategy !== "heartbeat") {
|
|
1122
|
+
const locks = availableLocks(options);
|
|
1123
|
+
if (locks) return createWebLocksLeader(name, options, locks);
|
|
1124
|
+
if (strategy === "web-locks") {
|
|
1125
|
+
throw new Error(
|
|
1126
|
+
'strategy: "web-locks" was requested but navigator.locks is unavailable. Web Locks needs a secure context (https, or localhost) \u2014 use "auto" to fall back to the heartbeat election.'
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return createHeartbeatLeader(name, options);
|
|
1131
|
+
}
|
|
1132
|
+
function createHeartbeatLeader(name, options) {
|
|
616
1133
|
const heartbeatMs = options.heartbeatMs ?? 1e3;
|
|
617
1134
|
const leaseMs = options.leaseMs ?? 3e3;
|
|
618
1135
|
const busOptions = {
|
|
@@ -624,16 +1141,21 @@ function createLeader(name, options = {}) {
|
|
|
624
1141
|
let eligible = options.eligible ?? true;
|
|
625
1142
|
let leaderId = null;
|
|
626
1143
|
let term = [0, clientId];
|
|
627
|
-
let snapshot =
|
|
1144
|
+
let snapshot = NO_LEADER2;
|
|
628
1145
|
let beat;
|
|
629
1146
|
let lease;
|
|
630
1147
|
let closed = false;
|
|
631
1148
|
const listeners = /* @__PURE__ */ new Set();
|
|
1149
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
632
1150
|
function setLeader(id) {
|
|
633
1151
|
if (id === leaderId) return;
|
|
634
1152
|
leaderId = id;
|
|
635
1153
|
snapshot = Object.freeze({ leaderId: id, isLeader: id === clientId });
|
|
636
1154
|
for (const fn of listeners) fn();
|
|
1155
|
+
if (id === clientId) {
|
|
1156
|
+
for (const waiter of waiters) waiter.resolve();
|
|
1157
|
+
waiters.clear();
|
|
1158
|
+
}
|
|
637
1159
|
}
|
|
638
1160
|
function armLease(delay) {
|
|
639
1161
|
clearTimeout(lease);
|
|
@@ -671,6 +1193,7 @@ function createLeader(name, options = {}) {
|
|
|
671
1193
|
armLease(0);
|
|
672
1194
|
return;
|
|
673
1195
|
}
|
|
1196
|
+
if (!isVersion(wire.term)) return;
|
|
674
1197
|
if (newer(wire.term, term)) {
|
|
675
1198
|
term = wire.term;
|
|
676
1199
|
stepDown(wire.clientId);
|
|
@@ -706,11 +1229,19 @@ function createLeader(name, options = {}) {
|
|
|
706
1229
|
armLease(heartbeatMs);
|
|
707
1230
|
return {
|
|
708
1231
|
clientId,
|
|
1232
|
+
strategy: "heartbeat",
|
|
709
1233
|
getSnapshot: () => snapshot,
|
|
710
1234
|
subscribe(fn) {
|
|
711
1235
|
listeners.add(fn);
|
|
712
1236
|
return () => listeners.delete(fn);
|
|
713
1237
|
},
|
|
1238
|
+
waitForLeadership() {
|
|
1239
|
+
if (leaderId === clientId) return Promise.resolve();
|
|
1240
|
+
if (closed) return Promise.reject(new Error("leader is closed"));
|
|
1241
|
+
return new Promise((resolve, reject) => {
|
|
1242
|
+
waiters.add({ resolve, reject });
|
|
1243
|
+
});
|
|
1244
|
+
},
|
|
714
1245
|
resign,
|
|
715
1246
|
setEligible(next) {
|
|
716
1247
|
if (next === eligible) return;
|
|
@@ -725,6 +1256,8 @@ function createLeader(name, options = {}) {
|
|
|
725
1256
|
if (closed) return;
|
|
726
1257
|
closed = true;
|
|
727
1258
|
resign();
|
|
1259
|
+
for (const waiter of waiters) waiter.reject(new Error("leader is closed"));
|
|
1260
|
+
waiters.clear();
|
|
728
1261
|
clearInterval(beat);
|
|
729
1262
|
clearTimeout(lease);
|
|
730
1263
|
if (hasWindow) {
|
|
@@ -738,6 +1271,246 @@ function createLeader(name, options = {}) {
|
|
|
738
1271
|
};
|
|
739
1272
|
}
|
|
740
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
|
+
|
|
741
1514
|
// src/persist-web-storage.ts
|
|
742
1515
|
function isPersisted(value) {
|
|
743
1516
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -745,6 +1518,7 @@ function isPersisted(value) {
|
|
|
745
1518
|
return candidate.v === 1 && typeof candidate.state === "object" && typeof candidate.versions === "object";
|
|
746
1519
|
}
|
|
747
1520
|
function webStorageAdapter(storage, key, options = {}) {
|
|
1521
|
+
const serializer = options.serializer ?? jsonSerializer;
|
|
748
1522
|
const report = (error, operation) => {
|
|
749
1523
|
try {
|
|
750
1524
|
options.onError?.(error, operation);
|
|
@@ -764,7 +1538,7 @@ function webStorageAdapter(storage, key, options = {}) {
|
|
|
764
1538
|
try {
|
|
765
1539
|
const raw = resolve("read")?.getItem(key);
|
|
766
1540
|
if (!raw) return void 0;
|
|
767
|
-
const parsed =
|
|
1541
|
+
const parsed = serializer.parse(raw);
|
|
768
1542
|
return isPersisted(parsed) ? parsed : void 0;
|
|
769
1543
|
} catch (error) {
|
|
770
1544
|
report(error, "read");
|
|
@@ -773,7 +1547,7 @@ function webStorageAdapter(storage, key, options = {}) {
|
|
|
773
1547
|
},
|
|
774
1548
|
write(snapshot) {
|
|
775
1549
|
try {
|
|
776
|
-
resolve("write")?.setItem(key,
|
|
1550
|
+
resolve("write")?.setItem(key, serializer.stringify(snapshot));
|
|
777
1551
|
} catch (error) {
|
|
778
1552
|
report(error, "write");
|
|
779
1553
|
}
|
|
@@ -794,6 +1568,58 @@ function sessionStorageAdapter(key, options) {
|
|
|
794
1568
|
return webStorageAdapter(() => globalThis.sessionStorage, key, options);
|
|
795
1569
|
}
|
|
796
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
|
+
|
|
797
1623
|
// src/errors/handshake-timeout-error.ts
|
|
798
1624
|
var HandshakeTimeoutError = class extends Error {
|
|
799
1625
|
constructor(message = "handshake with the other window timed out") {
|
|
@@ -1028,6 +1854,25 @@ function connectToOpener(options) {
|
|
|
1028
1854
|
}
|
|
1029
1855
|
};
|
|
1030
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
|
+
}
|
|
1031
1876
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1032
1877
|
0 && (module.exports = {
|
|
1033
1878
|
BroadcastChannelTransport,
|
|
@@ -1035,16 +1880,25 @@ function connectToOpener(options) {
|
|
|
1035
1880
|
DEFAULT_NAME,
|
|
1036
1881
|
HandshakeTimeoutError,
|
|
1037
1882
|
NoopTransport,
|
|
1883
|
+
StorageTransport,
|
|
1884
|
+
WIRE_VERSION,
|
|
1038
1885
|
WindowClosedError,
|
|
1039
1886
|
connectToOpener,
|
|
1040
1887
|
createChannel,
|
|
1041
1888
|
createLeader,
|
|
1889
|
+
createNamespace,
|
|
1042
1890
|
createPresence,
|
|
1891
|
+
createSharedReducer,
|
|
1043
1892
|
createSharedStore,
|
|
1044
1893
|
defaultTransport,
|
|
1045
1894
|
enableDebug,
|
|
1046
1895
|
getBusNames,
|
|
1896
|
+
getTransportKind,
|
|
1897
|
+
getWireSkew,
|
|
1898
|
+
indexedDbAdapter,
|
|
1047
1899
|
isBroadcastChannelAvailable,
|
|
1900
|
+
isStorageEventAvailable,
|
|
1901
|
+
jsonSerializer,
|
|
1048
1902
|
localStorageAdapter,
|
|
1049
1903
|
newer,
|
|
1050
1904
|
observeBus,
|