@use-everywhere/core 0.5.0 → 0.7.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 +379 -117
- package/dist/index.d.cts +112 -43
- package/dist/index.d.ts +112 -43
- package/dist/index.js +373 -112
- package/dist/testing.cjs +85 -0
- package/dist/testing.d.cts +31 -0
- package/dist/testing.d.ts +31 -0
- package/dist/testing.js +57 -0
- package/dist/transport.types-CV1WZOhy.d.cts +20 -0
- package/dist/transport.types-CV1WZOhy.d.ts +20 -0
- package/package.json +32 -14
package/dist/index.cjs
CHANGED
|
@@ -18,15 +18,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
|
|
20
20
|
// src/index.ts
|
|
21
|
-
var
|
|
22
|
-
__export(
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
23
|
BroadcastChannelTransport: () => BroadcastChannelTransport,
|
|
24
24
|
CID_PARAM: () => CID_PARAM,
|
|
25
25
|
DEFAULT_NAME: () => DEFAULT_NAME,
|
|
26
26
|
HandshakeTimeoutError: () => HandshakeTimeoutError,
|
|
27
|
-
MemoryHub: () => MemoryHub,
|
|
28
|
-
MemoryTransport: () => MemoryTransport,
|
|
29
27
|
NoopTransport: () => NoopTransport,
|
|
28
|
+
StorageTransport: () => StorageTransport,
|
|
30
29
|
WindowClosedError: () => WindowClosedError,
|
|
31
30
|
connectToOpener: () => connectToOpener,
|
|
32
31
|
createChannel: () => createChannel,
|
|
@@ -36,7 +35,9 @@ __export(index_exports, {
|
|
|
36
35
|
defaultTransport: () => defaultTransport,
|
|
37
36
|
enableDebug: () => enableDebug,
|
|
38
37
|
getBusNames: () => getBusNames,
|
|
38
|
+
getTransportKind: () => getTransportKind,
|
|
39
39
|
isBroadcastChannelAvailable: () => isBroadcastChannelAvailable,
|
|
40
|
+
isStorageEventAvailable: () => isStorageEventAvailable,
|
|
40
41
|
localStorageAdapter: () => localStorageAdapter,
|
|
41
42
|
newer: () => newer,
|
|
42
43
|
observeBus: () => observeBus,
|
|
@@ -44,7 +45,7 @@ __export(index_exports, {
|
|
|
44
45
|
sessionStorageAdapter: () => sessionStorageAdapter,
|
|
45
46
|
webStorageAdapter: () => webStorageAdapter
|
|
46
47
|
});
|
|
47
|
-
module.exports = __toCommonJS(
|
|
48
|
+
module.exports = __toCommonJS(src_exports);
|
|
48
49
|
|
|
49
50
|
// src/defaults.ts
|
|
50
51
|
var DEFAULT_NAME = "use-everywhere";
|
|
@@ -123,9 +124,31 @@ function newMsgId() {
|
|
|
123
124
|
return randomHex(16);
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
// src/rendezvous.ts
|
|
128
|
+
var PROTOCOL = 1;
|
|
129
|
+
var TABLE = /* @__PURE__ */ Symbol.for(`use-everywhere.rendezvous.${PROTOCOL}`);
|
|
130
|
+
var CENSUS = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.census");
|
|
131
|
+
function announce() {
|
|
132
|
+
const g = globalThis;
|
|
133
|
+
const census = g[CENSUS] ?? (g[CENSUS] = { protocols: [] });
|
|
134
|
+
if (census.protocols.includes(PROTOCOL)) return;
|
|
135
|
+
census.protocols.push(PROTOCOL);
|
|
136
|
+
if (census.protocols.length > 1) {
|
|
137
|
+
devWarn(
|
|
138
|
+
`[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.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function busTable() {
|
|
143
|
+
announce();
|
|
144
|
+
const g = globalThis;
|
|
145
|
+
return g[TABLE] ?? (g[TABLE] = /* @__PURE__ */ new Map());
|
|
146
|
+
}
|
|
147
|
+
|
|
126
148
|
// src/transport/broadcast-channel-transport.ts
|
|
127
149
|
var BroadcastChannelTransport = class {
|
|
128
150
|
constructor(name) {
|
|
151
|
+
this.kind = "broadcast-channel";
|
|
129
152
|
this.listeners = /* @__PURE__ */ new Set();
|
|
130
153
|
this.bc = new BroadcastChannel(name);
|
|
131
154
|
this.bc.onmessage = (event) => {
|
|
@@ -147,6 +170,9 @@ var BroadcastChannelTransport = class {
|
|
|
147
170
|
|
|
148
171
|
// src/transport/noop-transport.ts
|
|
149
172
|
var NoopTransport = class {
|
|
173
|
+
constructor() {
|
|
174
|
+
this.kind = "none";
|
|
175
|
+
}
|
|
150
176
|
post() {
|
|
151
177
|
}
|
|
152
178
|
subscribe() {
|
|
@@ -157,115 +183,217 @@ var NoopTransport = class {
|
|
|
157
183
|
}
|
|
158
184
|
};
|
|
159
185
|
|
|
186
|
+
// src/transport/storage-transport.ts
|
|
187
|
+
var StorageTransport = class {
|
|
188
|
+
constructor(name, storage = globalThis.localStorage) {
|
|
189
|
+
this.kind = "storage";
|
|
190
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
191
|
+
this.seq = 0;
|
|
192
|
+
if (!storage) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
"[use-everywhere] StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
this.key = `use-everywhere:bus:${name}`;
|
|
198
|
+
this.storage = storage;
|
|
199
|
+
this.onStorage = (event) => {
|
|
200
|
+
if (event.key !== this.key || event.newValue === null) return;
|
|
201
|
+
let payload;
|
|
202
|
+
try {
|
|
203
|
+
payload = JSON.parse(event.newValue);
|
|
204
|
+
} catch {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
for (const listener of this.listeners) listener(payload.data);
|
|
208
|
+
};
|
|
209
|
+
addEventListener("storage", this.onStorage);
|
|
210
|
+
}
|
|
211
|
+
post(data) {
|
|
212
|
+
const envelope = JSON.stringify({ seq: this.seq++, data }, rejectUnrepresentable);
|
|
213
|
+
try {
|
|
214
|
+
this.storage.setItem(this.key, envelope);
|
|
215
|
+
this.storage.removeItem(this.key);
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
subscribe(listener) {
|
|
220
|
+
this.listeners.add(listener);
|
|
221
|
+
return () => this.listeners.delete(listener);
|
|
222
|
+
}
|
|
223
|
+
close() {
|
|
224
|
+
this.listeners.clear();
|
|
225
|
+
removeEventListener("storage", this.onStorage);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
function rejectUnrepresentable(_key, value) {
|
|
229
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
230
|
+
throw new TypeError(`${typeof value} values cannot be shared`);
|
|
231
|
+
}
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
160
235
|
// src/transport/default-transport.ts
|
|
161
236
|
function isBroadcastChannelAvailable() {
|
|
162
237
|
return typeof BroadcastChannel !== "undefined";
|
|
163
238
|
}
|
|
239
|
+
function isStorageEventAvailable() {
|
|
240
|
+
if (typeof addEventListener !== "function") return false;
|
|
241
|
+
try {
|
|
242
|
+
const storage = globalThis.localStorage;
|
|
243
|
+
if (!storage) return false;
|
|
244
|
+
const probe = "use-everywhere:probe";
|
|
245
|
+
storage.setItem(probe, "1");
|
|
246
|
+
storage.removeItem(probe);
|
|
247
|
+
return true;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
164
252
|
function defaultTransport(name) {
|
|
165
|
-
|
|
253
|
+
if (isBroadcastChannelAvailable()) return new BroadcastChannelTransport(name);
|
|
254
|
+
if (isStorageEventAvailable()) {
|
|
255
|
+
devWarn(
|
|
256
|
+
"[use-everywhere] no BroadcastChannel; using the storage-event fallback. Values serialise as JSON, not structured clone \u2014 keep them JSON-shaped."
|
|
257
|
+
);
|
|
258
|
+
return new StorageTransport(name);
|
|
259
|
+
}
|
|
260
|
+
devWarn(
|
|
261
|
+
"[use-everywhere] no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
262
|
+
);
|
|
263
|
+
return new NoopTransport();
|
|
166
264
|
}
|
|
167
265
|
|
|
168
266
|
// src/bus.ts
|
|
169
267
|
function isBusWire(data) {
|
|
170
|
-
|
|
268
|
+
if (typeof data !== "object" || data === null) return false;
|
|
269
|
+
const wire = data;
|
|
270
|
+
return wire.v === 1 && typeof wire.scope === "string" && typeof wire.type === "string" && typeof wire.clientId === "string";
|
|
171
271
|
}
|
|
172
272
|
function defaultKind() {
|
|
173
273
|
return typeof document === "undefined" ? "worker" : "tab";
|
|
174
274
|
}
|
|
175
|
-
var
|
|
176
|
-
function
|
|
275
|
+
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event"]);
|
|
276
|
+
function createBusCore(name, options, onShutdown) {
|
|
177
277
|
const transport = (options.transport ?? defaultTransport)(name);
|
|
178
278
|
const clientId = newClientId();
|
|
179
279
|
const kind = options.kind ?? defaultKind();
|
|
180
|
-
const
|
|
280
|
+
const handles = /* @__PURE__ */ new Set();
|
|
181
281
|
let refs = 0;
|
|
182
282
|
let closed = false;
|
|
183
|
-
const
|
|
283
|
+
const deliver = (wire, from) => {
|
|
284
|
+
for (const handle of handles) {
|
|
285
|
+
if (handle === from) continue;
|
|
286
|
+
for (const fn of handle.listeners) fn(wire);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const post = (wire, from) => {
|
|
184
290
|
if (closed) return;
|
|
185
291
|
emitBusEvent(name, "out", wire);
|
|
186
292
|
transport.post(wire);
|
|
293
|
+
if (SHARED_WITHIN_CLIENT.has(wire.scope)) deliver(wire, from);
|
|
187
294
|
};
|
|
188
295
|
const unsubscribe = transport.subscribe((data) => {
|
|
189
296
|
if (!isBusWire(data)) return;
|
|
190
297
|
if (data.clientId === clientId) return;
|
|
191
298
|
emitBusEvent(name, "in", data);
|
|
192
299
|
if (data.scope === "presence" && data.type === "hello") {
|
|
193
|
-
post({ v: 1, scope: "presence", type: "ping", clientId, kind });
|
|
300
|
+
post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null);
|
|
194
301
|
}
|
|
195
|
-
|
|
302
|
+
deliver(data, null);
|
|
196
303
|
});
|
|
197
|
-
post({ v: 1, scope: "presence", type: "hello", clientId, kind });
|
|
304
|
+
post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
|
|
198
305
|
const heartbeat = setInterval(
|
|
199
|
-
() => post({ v: 1, scope: "presence", type: "ping", clientId, kind }),
|
|
306
|
+
() => post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null),
|
|
200
307
|
options.heartbeatMs ?? 2e3
|
|
201
308
|
);
|
|
202
|
-
const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind });
|
|
309
|
+
const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind }, null);
|
|
203
310
|
const onPageShow = (event) => {
|
|
204
311
|
if (!event.persisted) return;
|
|
205
|
-
post({ v: 1, scope: "presence", type: "hello", clientId, kind });
|
|
312
|
+
post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
|
|
313
|
+
};
|
|
314
|
+
const onVisible = () => {
|
|
315
|
+
if (document.visibilityState === "visible") {
|
|
316
|
+
post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
|
|
317
|
+
}
|
|
206
318
|
};
|
|
207
319
|
const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
|
|
208
320
|
if (hasWindow) {
|
|
209
321
|
addEventListener("pagehide", sayBye);
|
|
210
322
|
addEventListener("pageshow", onPageShow);
|
|
323
|
+
addEventListener("visibilitychange", onVisible);
|
|
211
324
|
}
|
|
325
|
+
const shutdown = () => {
|
|
326
|
+
sayBye();
|
|
327
|
+
closed = true;
|
|
328
|
+
clearInterval(heartbeat);
|
|
329
|
+
if (hasWindow) {
|
|
330
|
+
removeEventListener("pagehide", sayBye);
|
|
331
|
+
removeEventListener("pageshow", onPageShow);
|
|
332
|
+
removeEventListener("visibilitychange", onVisible);
|
|
333
|
+
}
|
|
334
|
+
unsubscribe();
|
|
335
|
+
transport.close();
|
|
336
|
+
handles.clear();
|
|
337
|
+
onShutdown();
|
|
338
|
+
};
|
|
212
339
|
return {
|
|
213
340
|
name,
|
|
214
341
|
clientId,
|
|
215
342
|
kind,
|
|
343
|
+
transportKind: transport.kind ?? "custom",
|
|
216
344
|
heartbeatMs: options.heartbeatMs ?? 2e3,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return () => listeners.delete(fn);
|
|
221
|
-
},
|
|
222
|
-
acquire() {
|
|
345
|
+
connect() {
|
|
346
|
+
const handle = { listeners: /* @__PURE__ */ new Set() };
|
|
347
|
+
handles.add(handle);
|
|
223
348
|
refs++;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
349
|
+
let released = false;
|
|
350
|
+
return {
|
|
351
|
+
name,
|
|
352
|
+
clientId,
|
|
353
|
+
kind,
|
|
354
|
+
transportKind: transport.kind ?? "custom",
|
|
355
|
+
post: (wire) => post(wire, handle),
|
|
356
|
+
subscribe(fn) {
|
|
357
|
+
handle.listeners.add(fn);
|
|
358
|
+
return () => handle.listeners.delete(fn);
|
|
359
|
+
},
|
|
360
|
+
release() {
|
|
361
|
+
if (released || closed) return;
|
|
362
|
+
released = true;
|
|
363
|
+
handles.delete(handle);
|
|
364
|
+
handle.listeners.clear();
|
|
365
|
+
refs--;
|
|
366
|
+
if (refs === 0) shutdown();
|
|
367
|
+
}
|
|
368
|
+
};
|
|
240
369
|
}
|
|
241
370
|
};
|
|
242
371
|
}
|
|
243
372
|
function getBusNames() {
|
|
244
|
-
return [...
|
|
373
|
+
return [...busTable().keys()];
|
|
374
|
+
}
|
|
375
|
+
function getTransportKind(name) {
|
|
376
|
+
return busTable().get(name)?.transportKind ?? null;
|
|
245
377
|
}
|
|
246
378
|
function getBus(name, options = {}) {
|
|
247
|
-
if (options.transport) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!bus) {
|
|
255
|
-
bus = createBus(name, options, () => registry.delete(name));
|
|
256
|
-
registry.set(name, bus);
|
|
379
|
+
if (options.transport) return createBusCore(name, options, () => {
|
|
380
|
+
}).connect();
|
|
381
|
+
const table = busTable();
|
|
382
|
+
let core = table.get(name);
|
|
383
|
+
if (!core) {
|
|
384
|
+
core = createBusCore(name, options, () => table.delete(name));
|
|
385
|
+
table.set(name, core);
|
|
257
386
|
} else {
|
|
258
|
-
if (options.heartbeatMs !== void 0 && options.heartbeatMs !==
|
|
387
|
+
if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
|
|
259
388
|
devWarn(
|
|
260
389
|
`[use-everywhere] bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
|
|
261
390
|
);
|
|
262
391
|
}
|
|
263
|
-
if (options.kind !== void 0 && options.kind !==
|
|
392
|
+
if (options.kind !== void 0 && options.kind !== core.kind) {
|
|
264
393
|
devWarn(`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
|
|
265
394
|
}
|
|
266
395
|
}
|
|
267
|
-
|
|
268
|
-
return bus;
|
|
396
|
+
return core.connect();
|
|
269
397
|
}
|
|
270
398
|
|
|
271
399
|
// src/channel.ts
|
|
@@ -317,6 +445,9 @@ function createChannel(name, options = {}) {
|
|
|
317
445
|
function newer(a, b) {
|
|
318
446
|
return !b || a[0] > b[0] || a[0] === b[0] && a[1] > b[1];
|
|
319
447
|
}
|
|
448
|
+
function isVersion(value) {
|
|
449
|
+
return Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && Number.isFinite(value[0]) && typeof value[1] === "string";
|
|
450
|
+
}
|
|
320
451
|
|
|
321
452
|
// src/dev-freeze.ts
|
|
322
453
|
var inDev2 = false;
|
|
@@ -356,7 +487,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
356
487
|
const live = liveStores.get(name) ?? 0;
|
|
357
488
|
if (live > 0) {
|
|
358
489
|
devWarn(
|
|
359
|
-
`[use-everywhere] second shared store for "${name}" in this tab \u2014
|
|
490
|
+
`[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.`
|
|
360
491
|
);
|
|
361
492
|
}
|
|
362
493
|
liveStores.set(name, live + 1);
|
|
@@ -404,11 +535,14 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
404
535
|
}
|
|
405
536
|
if (accept && !accept(meta)) return;
|
|
406
537
|
if (wire.type === "patch") {
|
|
538
|
+
if (typeof wire.key !== "string" || !isVersion(wire.version)) return;
|
|
407
539
|
applyRemote(wire.key, wire.value, wire.version, meta);
|
|
408
540
|
} else {
|
|
541
|
+
const versions2 = wire.versions;
|
|
542
|
+
if (typeof versions2 !== "object" || versions2 === null) return;
|
|
409
543
|
for (const k in wire.state) {
|
|
410
|
-
const version =
|
|
411
|
-
if (version) applyRemote(k, wire.state[k], version, meta);
|
|
544
|
+
const version = versions2[k];
|
|
545
|
+
if (isVersion(version)) applyRemote(k, wire.state[k], version, meta);
|
|
412
546
|
}
|
|
413
547
|
}
|
|
414
548
|
});
|
|
@@ -562,6 +696,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
562
696
|
// src/presence.ts
|
|
563
697
|
function createPresence(name, options = {}) {
|
|
564
698
|
const pruneAfterMs = options.pruneAfterMs ?? 5e3;
|
|
699
|
+
const probeGraceMs = options.probeGraceMs ?? 1e3;
|
|
565
700
|
const bus = getBus(name, options);
|
|
566
701
|
const peers = /* @__PURE__ */ new Map();
|
|
567
702
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -571,6 +706,7 @@ function createPresence(name, options = {}) {
|
|
|
571
706
|
for (const fn of listeners) fn();
|
|
572
707
|
}
|
|
573
708
|
const unsubscribe = bus.subscribe((wire) => {
|
|
709
|
+
if (wire.clientId === bus.clientId) return;
|
|
574
710
|
if (wire.scope === "presence" && wire.type === "bye") {
|
|
575
711
|
if (peers.delete(wire.clientId)) notify();
|
|
576
712
|
return;
|
|
@@ -579,19 +715,41 @@ function createPresence(name, options = {}) {
|
|
|
579
715
|
peers.set(wire.clientId, { id: wire.clientId, kind: wire.kind, lastSeen: Date.now() });
|
|
580
716
|
if (!existing) notify();
|
|
581
717
|
});
|
|
718
|
+
bus.post({ v: 1, scope: "presence", type: "hello", clientId: bus.clientId, kind: bus.kind });
|
|
719
|
+
const probed = /* @__PURE__ */ new Map();
|
|
582
720
|
const prune = setInterval(
|
|
583
721
|
() => {
|
|
584
|
-
const
|
|
722
|
+
const now = Date.now();
|
|
723
|
+
const silentSince = now - pruneAfterMs;
|
|
585
724
|
let changed = false;
|
|
725
|
+
let needProbe = false;
|
|
586
726
|
for (const [id, peer] of peers) {
|
|
587
|
-
if (peer.lastSeen
|
|
727
|
+
if (peer.lastSeen >= silentSince) {
|
|
728
|
+
probed.delete(id);
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const askedAt = probed.get(id);
|
|
732
|
+
if (askedAt === void 0) {
|
|
733
|
+
probed.set(id, now);
|
|
734
|
+
needProbe = true;
|
|
735
|
+
} else if (now - askedAt >= probeGraceMs) {
|
|
588
736
|
peers.delete(id);
|
|
737
|
+
probed.delete(id);
|
|
589
738
|
changed = true;
|
|
590
739
|
}
|
|
591
740
|
}
|
|
741
|
+
if (needProbe) {
|
|
742
|
+
bus.post({
|
|
743
|
+
v: 1,
|
|
744
|
+
scope: "presence",
|
|
745
|
+
type: "hello",
|
|
746
|
+
clientId: bus.clientId,
|
|
747
|
+
kind: bus.kind
|
|
748
|
+
});
|
|
749
|
+
}
|
|
592
750
|
if (changed) notify();
|
|
593
751
|
},
|
|
594
|
-
Math.max(
|
|
752
|
+
Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
|
|
595
753
|
);
|
|
596
754
|
let closed = false;
|
|
597
755
|
return {
|
|
@@ -612,9 +770,149 @@ function createPresence(name, options = {}) {
|
|
|
612
770
|
};
|
|
613
771
|
}
|
|
614
772
|
|
|
615
|
-
// src/leader.ts
|
|
773
|
+
// src/leader-web-locks.ts
|
|
616
774
|
var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
|
|
775
|
+
function createWebLocksLeader(name, options, locks) {
|
|
776
|
+
const busOptions = {
|
|
777
|
+
...options.transport ? { transport: options.transport } : {},
|
|
778
|
+
...options.kind ? { kind: options.kind } : {}
|
|
779
|
+
};
|
|
780
|
+
const bus = getBus(name, busOptions);
|
|
781
|
+
const clientId = bus.clientId;
|
|
782
|
+
let eligible = options.eligible ?? true;
|
|
783
|
+
let leaderId = null;
|
|
784
|
+
let snapshot = NO_LEADER;
|
|
785
|
+
let closed = false;
|
|
786
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
787
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
788
|
+
let releaseHeld = null;
|
|
789
|
+
let pending = null;
|
|
790
|
+
function setLeader(id) {
|
|
791
|
+
if (id === leaderId) return;
|
|
792
|
+
leaderId = id;
|
|
793
|
+
snapshot = Object.freeze({ leaderId: id, isLeader: id === clientId });
|
|
794
|
+
for (const fn of listeners) fn();
|
|
795
|
+
if (id === clientId) {
|
|
796
|
+
for (const waiter of waiters) waiter.resolve();
|
|
797
|
+
waiters.clear();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const term = [1, clientId];
|
|
801
|
+
const announce2 = () => bus.post({ v: 1, scope: "leader", type: "heartbeat", term, clientId, kind: bus.kind });
|
|
802
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
803
|
+
if (wire.scope !== "leader") return;
|
|
804
|
+
if (wire.type === "hello") {
|
|
805
|
+
if (leaderId === clientId) announce2();
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (wire.type === "resign") {
|
|
809
|
+
if (wire.clientId === leaderId) setLeader(null);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
setLeader(wire.clientId);
|
|
813
|
+
});
|
|
814
|
+
function joinQueue() {
|
|
815
|
+
if (closed || !eligible || releaseHeld || pending) return;
|
|
816
|
+
const controller = new AbortController();
|
|
817
|
+
pending = controller;
|
|
818
|
+
const held = new Promise((resolve) => {
|
|
819
|
+
releaseHeld = resolve;
|
|
820
|
+
});
|
|
821
|
+
void locks.request(name, { signal: controller.signal }, async () => {
|
|
822
|
+
pending = null;
|
|
823
|
+
if (closed || !eligible) return;
|
|
824
|
+
setLeader(clientId);
|
|
825
|
+
announce2();
|
|
826
|
+
await held;
|
|
827
|
+
}).catch(() => {
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
function letGo() {
|
|
831
|
+
const release = releaseHeld;
|
|
832
|
+
releaseHeld = null;
|
|
833
|
+
release?.();
|
|
834
|
+
}
|
|
835
|
+
function resign() {
|
|
836
|
+
if (leaderId !== clientId) return;
|
|
837
|
+
letGo();
|
|
838
|
+
setLeader(null);
|
|
839
|
+
bus.post({ v: 1, scope: "leader", type: "resign", term, clientId, kind: bus.kind });
|
|
840
|
+
joinQueue();
|
|
841
|
+
}
|
|
842
|
+
const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
|
|
843
|
+
const onPageHide = () => resign();
|
|
844
|
+
if (hasWindow) addEventListener("pagehide", onPageHide);
|
|
845
|
+
bus.post({ v: 1, scope: "leader", type: "hello", clientId, kind: bus.kind });
|
|
846
|
+
joinQueue();
|
|
847
|
+
return {
|
|
848
|
+
clientId,
|
|
849
|
+
strategy: "web-locks",
|
|
850
|
+
getSnapshot: () => snapshot,
|
|
851
|
+
subscribe(fn) {
|
|
852
|
+
listeners.add(fn);
|
|
853
|
+
return () => listeners.delete(fn);
|
|
854
|
+
},
|
|
855
|
+
waitForLeadership() {
|
|
856
|
+
if (leaderId === clientId) return Promise.resolve();
|
|
857
|
+
if (closed) return Promise.reject(new Error("leader is closed"));
|
|
858
|
+
return new Promise((resolve, reject) => {
|
|
859
|
+
waiters.add({ resolve, reject });
|
|
860
|
+
});
|
|
861
|
+
},
|
|
862
|
+
resign,
|
|
863
|
+
setEligible(next) {
|
|
864
|
+
if (next === eligible) return;
|
|
865
|
+
eligible = next;
|
|
866
|
+
if (next) {
|
|
867
|
+
joinQueue();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (leaderId === clientId) resignWithoutRequeue();
|
|
871
|
+
pending?.abort();
|
|
872
|
+
pending = null;
|
|
873
|
+
},
|
|
874
|
+
close() {
|
|
875
|
+
if (closed) return;
|
|
876
|
+
closed = true;
|
|
877
|
+
if (leaderId === clientId) resignWithoutRequeue();
|
|
878
|
+
pending?.abort();
|
|
879
|
+
pending = null;
|
|
880
|
+
if (hasWindow) removeEventListener("pagehide", onPageHide);
|
|
881
|
+
for (const waiter of waiters) waiter.reject(new Error("leader is closed"));
|
|
882
|
+
waiters.clear();
|
|
883
|
+
unsubscribe();
|
|
884
|
+
listeners.clear();
|
|
885
|
+
bus.release();
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
function resignWithoutRequeue() {
|
|
889
|
+
letGo();
|
|
890
|
+
setLeader(null);
|
|
891
|
+
bus.post({ v: 1, scope: "leader", type: "resign", term, clientId, kind: bus.kind });
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/leader.ts
|
|
896
|
+
var NO_LEADER2 = Object.freeze({ leaderId: null, isLeader: false });
|
|
897
|
+
function availableLocks(options) {
|
|
898
|
+
if (options.locks) return options.locks;
|
|
899
|
+
const locks = globalThis.navigator?.locks;
|
|
900
|
+
return typeof locks?.request === "function" ? locks : void 0;
|
|
901
|
+
}
|
|
617
902
|
function createLeader(name, options = {}) {
|
|
903
|
+
const strategy = options.strategy ?? "auto";
|
|
904
|
+
if (strategy !== "heartbeat") {
|
|
905
|
+
const locks = availableLocks(options);
|
|
906
|
+
if (locks) return createWebLocksLeader(name, options, locks);
|
|
907
|
+
if (strategy === "web-locks") {
|
|
908
|
+
throw new Error(
|
|
909
|
+
'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.'
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return createHeartbeatLeader(name, options);
|
|
914
|
+
}
|
|
915
|
+
function createHeartbeatLeader(name, options) {
|
|
618
916
|
const heartbeatMs = options.heartbeatMs ?? 1e3;
|
|
619
917
|
const leaseMs = options.leaseMs ?? 3e3;
|
|
620
918
|
const busOptions = {
|
|
@@ -626,16 +924,21 @@ function createLeader(name, options = {}) {
|
|
|
626
924
|
let eligible = options.eligible ?? true;
|
|
627
925
|
let leaderId = null;
|
|
628
926
|
let term = [0, clientId];
|
|
629
|
-
let snapshot =
|
|
927
|
+
let snapshot = NO_LEADER2;
|
|
630
928
|
let beat;
|
|
631
929
|
let lease;
|
|
632
930
|
let closed = false;
|
|
633
931
|
const listeners = /* @__PURE__ */ new Set();
|
|
932
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
634
933
|
function setLeader(id) {
|
|
635
934
|
if (id === leaderId) return;
|
|
636
935
|
leaderId = id;
|
|
637
936
|
snapshot = Object.freeze({ leaderId: id, isLeader: id === clientId });
|
|
638
937
|
for (const fn of listeners) fn();
|
|
938
|
+
if (id === clientId) {
|
|
939
|
+
for (const waiter of waiters) waiter.resolve();
|
|
940
|
+
waiters.clear();
|
|
941
|
+
}
|
|
639
942
|
}
|
|
640
943
|
function armLease(delay) {
|
|
641
944
|
clearTimeout(lease);
|
|
@@ -673,6 +976,7 @@ function createLeader(name, options = {}) {
|
|
|
673
976
|
armLease(0);
|
|
674
977
|
return;
|
|
675
978
|
}
|
|
979
|
+
if (!isVersion(wire.term)) return;
|
|
676
980
|
if (newer(wire.term, term)) {
|
|
677
981
|
term = wire.term;
|
|
678
982
|
stepDown(wire.clientId);
|
|
@@ -708,11 +1012,19 @@ function createLeader(name, options = {}) {
|
|
|
708
1012
|
armLease(heartbeatMs);
|
|
709
1013
|
return {
|
|
710
1014
|
clientId,
|
|
1015
|
+
strategy: "heartbeat",
|
|
711
1016
|
getSnapshot: () => snapshot,
|
|
712
1017
|
subscribe(fn) {
|
|
713
1018
|
listeners.add(fn);
|
|
714
1019
|
return () => listeners.delete(fn);
|
|
715
1020
|
},
|
|
1021
|
+
waitForLeadership() {
|
|
1022
|
+
if (leaderId === clientId) return Promise.resolve();
|
|
1023
|
+
if (closed) return Promise.reject(new Error("leader is closed"));
|
|
1024
|
+
return new Promise((resolve, reject) => {
|
|
1025
|
+
waiters.add({ resolve, reject });
|
|
1026
|
+
});
|
|
1027
|
+
},
|
|
716
1028
|
resign,
|
|
717
1029
|
setEligible(next) {
|
|
718
1030
|
if (next === eligible) return;
|
|
@@ -727,6 +1039,8 @@ function createLeader(name, options = {}) {
|
|
|
727
1039
|
if (closed) return;
|
|
728
1040
|
closed = true;
|
|
729
1041
|
resign();
|
|
1042
|
+
for (const waiter of waiters) waiter.reject(new Error("leader is closed"));
|
|
1043
|
+
waiters.clear();
|
|
730
1044
|
clearInterval(beat);
|
|
731
1045
|
clearTimeout(lease);
|
|
732
1046
|
if (hasWindow) {
|
|
@@ -1030,68 +1344,14 @@ function connectToOpener(options) {
|
|
|
1030
1344
|
}
|
|
1031
1345
|
};
|
|
1032
1346
|
}
|
|
1033
|
-
|
|
1034
|
-
// src/transport/memory-transport.ts
|
|
1035
|
-
var MemoryTransport = class {
|
|
1036
|
-
constructor(hub) {
|
|
1037
|
-
this.hub = hub;
|
|
1038
|
-
this.listeners = /* @__PURE__ */ new Set();
|
|
1039
|
-
this.closed = false;
|
|
1040
|
-
}
|
|
1041
|
-
post(data) {
|
|
1042
|
-
if (this.closed) return;
|
|
1043
|
-
this.hub.broadcast(this, data);
|
|
1044
|
-
}
|
|
1045
|
-
subscribe(listener) {
|
|
1046
|
-
this.listeners.add(listener);
|
|
1047
|
-
return () => this.listeners.delete(listener);
|
|
1048
|
-
}
|
|
1049
|
-
close() {
|
|
1050
|
-
this.closed = true;
|
|
1051
|
-
this.listeners.clear();
|
|
1052
|
-
this.hub.disconnect(this);
|
|
1053
|
-
}
|
|
1054
|
-
/** @internal */
|
|
1055
|
-
deliver(data) {
|
|
1056
|
-
if (this.closed) return;
|
|
1057
|
-
for (const listener of this.listeners) listener(data);
|
|
1058
|
-
}
|
|
1059
|
-
};
|
|
1060
|
-
|
|
1061
|
-
// src/transport/memory-hub.ts
|
|
1062
|
-
var MemoryHub = class {
|
|
1063
|
-
constructor() {
|
|
1064
|
-
this.transports = /* @__PURE__ */ new Set();
|
|
1065
|
-
}
|
|
1066
|
-
connect() {
|
|
1067
|
-
const transport = new MemoryTransport(this);
|
|
1068
|
-
this.transports.add(transport);
|
|
1069
|
-
return transport;
|
|
1070
|
-
}
|
|
1071
|
-
/** @internal */
|
|
1072
|
-
broadcast(from, data) {
|
|
1073
|
-
structuredClone(data);
|
|
1074
|
-
for (const transport of this.transports) {
|
|
1075
|
-
if (transport !== from) {
|
|
1076
|
-
const copy = structuredClone(data);
|
|
1077
|
-
queueMicrotask(() => transport.deliver(copy));
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
/** @internal */
|
|
1082
|
-
disconnect(transport) {
|
|
1083
|
-
this.transports.delete(transport);
|
|
1084
|
-
}
|
|
1085
|
-
};
|
|
1086
1347
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1087
1348
|
0 && (module.exports = {
|
|
1088
1349
|
BroadcastChannelTransport,
|
|
1089
1350
|
CID_PARAM,
|
|
1090
1351
|
DEFAULT_NAME,
|
|
1091
1352
|
HandshakeTimeoutError,
|
|
1092
|
-
MemoryHub,
|
|
1093
|
-
MemoryTransport,
|
|
1094
1353
|
NoopTransport,
|
|
1354
|
+
StorageTransport,
|
|
1095
1355
|
WindowClosedError,
|
|
1096
1356
|
connectToOpener,
|
|
1097
1357
|
createChannel,
|
|
@@ -1101,7 +1361,9 @@ var MemoryHub = class {
|
|
|
1101
1361
|
defaultTransport,
|
|
1102
1362
|
enableDebug,
|
|
1103
1363
|
getBusNames,
|
|
1364
|
+
getTransportKind,
|
|
1104
1365
|
isBroadcastChannelAvailable,
|
|
1366
|
+
isStorageEventAvailable,
|
|
1105
1367
|
localStorageAdapter,
|
|
1106
1368
|
newer,
|
|
1107
1369
|
observeBus,
|