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