@use-everywhere/core 0.6.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 +376 -57
- package/dist/index.d.cts +118 -13
- package/dist/index.d.ts +118 -13
- package/dist/index.js +373 -57
- 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 +9 -8
- package/dist/transport.types-tQ1cu6Xm.d.cts +0 -12
- package/dist/transport.types-tQ1cu6Xm.d.ts +0 -12
package/dist/index.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(src_exports, {
|
|
|
25
25
|
DEFAULT_NAME: () => DEFAULT_NAME,
|
|
26
26
|
HandshakeTimeoutError: () => HandshakeTimeoutError,
|
|
27
27
|
NoopTransport: () => NoopTransport,
|
|
28
|
+
StorageTransport: () => StorageTransport,
|
|
28
29
|
WindowClosedError: () => WindowClosedError,
|
|
29
30
|
connectToOpener: () => connectToOpener,
|
|
30
31
|
createChannel: () => createChannel,
|
|
@@ -34,7 +35,9 @@ __export(src_exports, {
|
|
|
34
35
|
defaultTransport: () => defaultTransport,
|
|
35
36
|
enableDebug: () => enableDebug,
|
|
36
37
|
getBusNames: () => getBusNames,
|
|
38
|
+
getTransportKind: () => getTransportKind,
|
|
37
39
|
isBroadcastChannelAvailable: () => isBroadcastChannelAvailable,
|
|
40
|
+
isStorageEventAvailable: () => isStorageEventAvailable,
|
|
38
41
|
localStorageAdapter: () => localStorageAdapter,
|
|
39
42
|
newer: () => newer,
|
|
40
43
|
observeBus: () => observeBus,
|
|
@@ -121,9 +124,31 @@ function newMsgId() {
|
|
|
121
124
|
return randomHex(16);
|
|
122
125
|
}
|
|
123
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
|
+
|
|
124
148
|
// src/transport/broadcast-channel-transport.ts
|
|
125
149
|
var BroadcastChannelTransport = class {
|
|
126
150
|
constructor(name) {
|
|
151
|
+
this.kind = "broadcast-channel";
|
|
127
152
|
this.listeners = /* @__PURE__ */ new Set();
|
|
128
153
|
this.bc = new BroadcastChannel(name);
|
|
129
154
|
this.bc.onmessage = (event) => {
|
|
@@ -145,6 +170,9 @@ var BroadcastChannelTransport = class {
|
|
|
145
170
|
|
|
146
171
|
// src/transport/noop-transport.ts
|
|
147
172
|
var NoopTransport = class {
|
|
173
|
+
constructor() {
|
|
174
|
+
this.kind = "none";
|
|
175
|
+
}
|
|
148
176
|
post() {
|
|
149
177
|
}
|
|
150
178
|
subscribe() {
|
|
@@ -155,115 +183,217 @@ var NoopTransport = class {
|
|
|
155
183
|
}
|
|
156
184
|
};
|
|
157
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
|
+
|
|
158
235
|
// src/transport/default-transport.ts
|
|
159
236
|
function isBroadcastChannelAvailable() {
|
|
160
237
|
return typeof BroadcastChannel !== "undefined";
|
|
161
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
|
+
}
|
|
162
252
|
function defaultTransport(name) {
|
|
163
|
-
|
|
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();
|
|
164
264
|
}
|
|
165
265
|
|
|
166
266
|
// src/bus.ts
|
|
167
267
|
function isBusWire(data) {
|
|
168
|
-
|
|
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";
|
|
169
271
|
}
|
|
170
272
|
function defaultKind() {
|
|
171
273
|
return typeof document === "undefined" ? "worker" : "tab";
|
|
172
274
|
}
|
|
173
|
-
var
|
|
174
|
-
function
|
|
275
|
+
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event"]);
|
|
276
|
+
function createBusCore(name, options, onShutdown) {
|
|
175
277
|
const transport = (options.transport ?? defaultTransport)(name);
|
|
176
278
|
const clientId = newClientId();
|
|
177
279
|
const kind = options.kind ?? defaultKind();
|
|
178
|
-
const
|
|
280
|
+
const handles = /* @__PURE__ */ new Set();
|
|
179
281
|
let refs = 0;
|
|
180
282
|
let closed = false;
|
|
181
|
-
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) => {
|
|
182
290
|
if (closed) return;
|
|
183
291
|
emitBusEvent(name, "out", wire);
|
|
184
292
|
transport.post(wire);
|
|
293
|
+
if (SHARED_WITHIN_CLIENT.has(wire.scope)) deliver(wire, from);
|
|
185
294
|
};
|
|
186
295
|
const unsubscribe = transport.subscribe((data) => {
|
|
187
296
|
if (!isBusWire(data)) return;
|
|
188
297
|
if (data.clientId === clientId) return;
|
|
189
298
|
emitBusEvent(name, "in", data);
|
|
190
299
|
if (data.scope === "presence" && data.type === "hello") {
|
|
191
|
-
post({ v: 1, scope: "presence", type: "ping", clientId, kind });
|
|
300
|
+
post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null);
|
|
192
301
|
}
|
|
193
|
-
|
|
302
|
+
deliver(data, null);
|
|
194
303
|
});
|
|
195
|
-
post({ v: 1, scope: "presence", type: "hello", clientId, kind });
|
|
304
|
+
post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
|
|
196
305
|
const heartbeat = setInterval(
|
|
197
|
-
() => post({ v: 1, scope: "presence", type: "ping", clientId, kind }),
|
|
306
|
+
() => post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null),
|
|
198
307
|
options.heartbeatMs ?? 2e3
|
|
199
308
|
);
|
|
200
|
-
const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind });
|
|
309
|
+
const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind }, null);
|
|
201
310
|
const onPageShow = (event) => {
|
|
202
311
|
if (!event.persisted) return;
|
|
203
|
-
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
|
+
}
|
|
204
318
|
};
|
|
205
319
|
const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
|
|
206
320
|
if (hasWindow) {
|
|
207
321
|
addEventListener("pagehide", sayBye);
|
|
208
322
|
addEventListener("pageshow", onPageShow);
|
|
323
|
+
addEventListener("visibilitychange", onVisible);
|
|
209
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
|
+
};
|
|
210
339
|
return {
|
|
211
340
|
name,
|
|
212
341
|
clientId,
|
|
213
342
|
kind,
|
|
343
|
+
transportKind: transport.kind ?? "custom",
|
|
214
344
|
heartbeatMs: options.heartbeatMs ?? 2e3,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return () => listeners.delete(fn);
|
|
219
|
-
},
|
|
220
|
-
acquire() {
|
|
345
|
+
connect() {
|
|
346
|
+
const handle = { listeners: /* @__PURE__ */ new Set() };
|
|
347
|
+
handles.add(handle);
|
|
221
348
|
refs++;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
};
|
|
238
369
|
}
|
|
239
370
|
};
|
|
240
371
|
}
|
|
241
372
|
function getBusNames() {
|
|
242
|
-
return [...
|
|
373
|
+
return [...busTable().keys()];
|
|
374
|
+
}
|
|
375
|
+
function getTransportKind(name) {
|
|
376
|
+
return busTable().get(name)?.transportKind ?? null;
|
|
243
377
|
}
|
|
244
378
|
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);
|
|
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);
|
|
255
386
|
} else {
|
|
256
|
-
if (options.heartbeatMs !== void 0 && options.heartbeatMs !==
|
|
387
|
+
if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
|
|
257
388
|
devWarn(
|
|
258
389
|
`[use-everywhere] bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
|
|
259
390
|
);
|
|
260
391
|
}
|
|
261
|
-
if (options.kind !== void 0 && options.kind !==
|
|
392
|
+
if (options.kind !== void 0 && options.kind !== core.kind) {
|
|
262
393
|
devWarn(`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
|
|
263
394
|
}
|
|
264
395
|
}
|
|
265
|
-
|
|
266
|
-
return bus;
|
|
396
|
+
return core.connect();
|
|
267
397
|
}
|
|
268
398
|
|
|
269
399
|
// src/channel.ts
|
|
@@ -315,6 +445,9 @@ function createChannel(name, options = {}) {
|
|
|
315
445
|
function newer(a, b) {
|
|
316
446
|
return !b || a[0] > b[0] || a[0] === b[0] && a[1] > b[1];
|
|
317
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
|
+
}
|
|
318
451
|
|
|
319
452
|
// src/dev-freeze.ts
|
|
320
453
|
var inDev2 = false;
|
|
@@ -354,7 +487,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
354
487
|
const live = liveStores.get(name) ?? 0;
|
|
355
488
|
if (live > 0) {
|
|
356
489
|
devWarn(
|
|
357
|
-
`[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.`
|
|
358
491
|
);
|
|
359
492
|
}
|
|
360
493
|
liveStores.set(name, live + 1);
|
|
@@ -402,11 +535,14 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
402
535
|
}
|
|
403
536
|
if (accept && !accept(meta)) return;
|
|
404
537
|
if (wire.type === "patch") {
|
|
538
|
+
if (typeof wire.key !== "string" || !isVersion(wire.version)) return;
|
|
405
539
|
applyRemote(wire.key, wire.value, wire.version, meta);
|
|
406
540
|
} else {
|
|
541
|
+
const versions2 = wire.versions;
|
|
542
|
+
if (typeof versions2 !== "object" || versions2 === null) return;
|
|
407
543
|
for (const k in wire.state) {
|
|
408
|
-
const version =
|
|
409
|
-
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);
|
|
410
546
|
}
|
|
411
547
|
}
|
|
412
548
|
});
|
|
@@ -560,6 +696,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
560
696
|
// src/presence.ts
|
|
561
697
|
function createPresence(name, options = {}) {
|
|
562
698
|
const pruneAfterMs = options.pruneAfterMs ?? 5e3;
|
|
699
|
+
const probeGraceMs = options.probeGraceMs ?? 1e3;
|
|
563
700
|
const bus = getBus(name, options);
|
|
564
701
|
const peers = /* @__PURE__ */ new Map();
|
|
565
702
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -569,6 +706,7 @@ function createPresence(name, options = {}) {
|
|
|
569
706
|
for (const fn of listeners) fn();
|
|
570
707
|
}
|
|
571
708
|
const unsubscribe = bus.subscribe((wire) => {
|
|
709
|
+
if (wire.clientId === bus.clientId) return;
|
|
572
710
|
if (wire.scope === "presence" && wire.type === "bye") {
|
|
573
711
|
if (peers.delete(wire.clientId)) notify();
|
|
574
712
|
return;
|
|
@@ -577,19 +715,41 @@ function createPresence(name, options = {}) {
|
|
|
577
715
|
peers.set(wire.clientId, { id: wire.clientId, kind: wire.kind, lastSeen: Date.now() });
|
|
578
716
|
if (!existing) notify();
|
|
579
717
|
});
|
|
718
|
+
bus.post({ v: 1, scope: "presence", type: "hello", clientId: bus.clientId, kind: bus.kind });
|
|
719
|
+
const probed = /* @__PURE__ */ new Map();
|
|
580
720
|
const prune = setInterval(
|
|
581
721
|
() => {
|
|
582
|
-
const
|
|
722
|
+
const now = Date.now();
|
|
723
|
+
const silentSince = now - pruneAfterMs;
|
|
583
724
|
let changed = false;
|
|
725
|
+
let needProbe = false;
|
|
584
726
|
for (const [id, peer] of peers) {
|
|
585
|
-
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) {
|
|
586
736
|
peers.delete(id);
|
|
737
|
+
probed.delete(id);
|
|
587
738
|
changed = true;
|
|
588
739
|
}
|
|
589
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
|
+
}
|
|
590
750
|
if (changed) notify();
|
|
591
751
|
},
|
|
592
|
-
Math.max(
|
|
752
|
+
Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
|
|
593
753
|
);
|
|
594
754
|
let closed = false;
|
|
595
755
|
return {
|
|
@@ -610,9 +770,149 @@ function createPresence(name, options = {}) {
|
|
|
610
770
|
};
|
|
611
771
|
}
|
|
612
772
|
|
|
613
|
-
// src/leader.ts
|
|
773
|
+
// src/leader-web-locks.ts
|
|
614
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
|
+
}
|
|
615
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) {
|
|
616
916
|
const heartbeatMs = options.heartbeatMs ?? 1e3;
|
|
617
917
|
const leaseMs = options.leaseMs ?? 3e3;
|
|
618
918
|
const busOptions = {
|
|
@@ -624,16 +924,21 @@ function createLeader(name, options = {}) {
|
|
|
624
924
|
let eligible = options.eligible ?? true;
|
|
625
925
|
let leaderId = null;
|
|
626
926
|
let term = [0, clientId];
|
|
627
|
-
let snapshot =
|
|
927
|
+
let snapshot = NO_LEADER2;
|
|
628
928
|
let beat;
|
|
629
929
|
let lease;
|
|
630
930
|
let closed = false;
|
|
631
931
|
const listeners = /* @__PURE__ */ new Set();
|
|
932
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
632
933
|
function setLeader(id) {
|
|
633
934
|
if (id === leaderId) return;
|
|
634
935
|
leaderId = id;
|
|
635
936
|
snapshot = Object.freeze({ leaderId: id, isLeader: id === clientId });
|
|
636
937
|
for (const fn of listeners) fn();
|
|
938
|
+
if (id === clientId) {
|
|
939
|
+
for (const waiter of waiters) waiter.resolve();
|
|
940
|
+
waiters.clear();
|
|
941
|
+
}
|
|
637
942
|
}
|
|
638
943
|
function armLease(delay) {
|
|
639
944
|
clearTimeout(lease);
|
|
@@ -671,6 +976,7 @@ function createLeader(name, options = {}) {
|
|
|
671
976
|
armLease(0);
|
|
672
977
|
return;
|
|
673
978
|
}
|
|
979
|
+
if (!isVersion(wire.term)) return;
|
|
674
980
|
if (newer(wire.term, term)) {
|
|
675
981
|
term = wire.term;
|
|
676
982
|
stepDown(wire.clientId);
|
|
@@ -706,11 +1012,19 @@ function createLeader(name, options = {}) {
|
|
|
706
1012
|
armLease(heartbeatMs);
|
|
707
1013
|
return {
|
|
708
1014
|
clientId,
|
|
1015
|
+
strategy: "heartbeat",
|
|
709
1016
|
getSnapshot: () => snapshot,
|
|
710
1017
|
subscribe(fn) {
|
|
711
1018
|
listeners.add(fn);
|
|
712
1019
|
return () => listeners.delete(fn);
|
|
713
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
|
+
},
|
|
714
1028
|
resign,
|
|
715
1029
|
setEligible(next) {
|
|
716
1030
|
if (next === eligible) return;
|
|
@@ -725,6 +1039,8 @@ function createLeader(name, options = {}) {
|
|
|
725
1039
|
if (closed) return;
|
|
726
1040
|
closed = true;
|
|
727
1041
|
resign();
|
|
1042
|
+
for (const waiter of waiters) waiter.reject(new Error("leader is closed"));
|
|
1043
|
+
waiters.clear();
|
|
728
1044
|
clearInterval(beat);
|
|
729
1045
|
clearTimeout(lease);
|
|
730
1046
|
if (hasWindow) {
|
|
@@ -1035,6 +1351,7 @@ function connectToOpener(options) {
|
|
|
1035
1351
|
DEFAULT_NAME,
|
|
1036
1352
|
HandshakeTimeoutError,
|
|
1037
1353
|
NoopTransport,
|
|
1354
|
+
StorageTransport,
|
|
1038
1355
|
WindowClosedError,
|
|
1039
1356
|
connectToOpener,
|
|
1040
1357
|
createChannel,
|
|
@@ -1044,7 +1361,9 @@ function connectToOpener(options) {
|
|
|
1044
1361
|
defaultTransport,
|
|
1045
1362
|
enableDebug,
|
|
1046
1363
|
getBusNames,
|
|
1364
|
+
getTransportKind,
|
|
1047
1365
|
isBroadcastChannelAvailable,
|
|
1366
|
+
isStorageEventAvailable,
|
|
1048
1367
|
localStorageAdapter,
|
|
1049
1368
|
newer,
|
|
1050
1369
|
observeBus,
|