@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.js
CHANGED
|
@@ -75,9 +75,31 @@ function newMsgId() {
|
|
|
75
75
|
return randomHex(16);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// src/rendezvous.ts
|
|
79
|
+
var PROTOCOL = 1;
|
|
80
|
+
var TABLE = /* @__PURE__ */ Symbol.for(`use-everywhere.rendezvous.${PROTOCOL}`);
|
|
81
|
+
var CENSUS = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.census");
|
|
82
|
+
function announce() {
|
|
83
|
+
const g = globalThis;
|
|
84
|
+
const census = g[CENSUS] ?? (g[CENSUS] = { protocols: [] });
|
|
85
|
+
if (census.protocols.includes(PROTOCOL)) return;
|
|
86
|
+
census.protocols.push(PROTOCOL);
|
|
87
|
+
if (census.protocols.length > 1) {
|
|
88
|
+
devWarn(
|
|
89
|
+
`[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.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function busTable() {
|
|
94
|
+
announce();
|
|
95
|
+
const g = globalThis;
|
|
96
|
+
return g[TABLE] ?? (g[TABLE] = /* @__PURE__ */ new Map());
|
|
97
|
+
}
|
|
98
|
+
|
|
78
99
|
// src/transport/broadcast-channel-transport.ts
|
|
79
100
|
var BroadcastChannelTransport = class {
|
|
80
101
|
constructor(name) {
|
|
102
|
+
this.kind = "broadcast-channel";
|
|
81
103
|
this.listeners = /* @__PURE__ */ new Set();
|
|
82
104
|
this.bc = new BroadcastChannel(name);
|
|
83
105
|
this.bc.onmessage = (event) => {
|
|
@@ -99,6 +121,9 @@ var BroadcastChannelTransport = class {
|
|
|
99
121
|
|
|
100
122
|
// src/transport/noop-transport.ts
|
|
101
123
|
var NoopTransport = class {
|
|
124
|
+
constructor() {
|
|
125
|
+
this.kind = "none";
|
|
126
|
+
}
|
|
102
127
|
post() {
|
|
103
128
|
}
|
|
104
129
|
subscribe() {
|
|
@@ -109,115 +134,217 @@ var NoopTransport = class {
|
|
|
109
134
|
}
|
|
110
135
|
};
|
|
111
136
|
|
|
137
|
+
// src/transport/storage-transport.ts
|
|
138
|
+
var StorageTransport = class {
|
|
139
|
+
constructor(name, storage = globalThis.localStorage) {
|
|
140
|
+
this.kind = "storage";
|
|
141
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
142
|
+
this.seq = 0;
|
|
143
|
+
if (!storage) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"[use-everywhere] StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
this.key = `use-everywhere:bus:${name}`;
|
|
149
|
+
this.storage = storage;
|
|
150
|
+
this.onStorage = (event) => {
|
|
151
|
+
if (event.key !== this.key || event.newValue === null) return;
|
|
152
|
+
let payload;
|
|
153
|
+
try {
|
|
154
|
+
payload = JSON.parse(event.newValue);
|
|
155
|
+
} catch {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
for (const listener of this.listeners) listener(payload.data);
|
|
159
|
+
};
|
|
160
|
+
addEventListener("storage", this.onStorage);
|
|
161
|
+
}
|
|
162
|
+
post(data) {
|
|
163
|
+
const envelope = JSON.stringify({ seq: this.seq++, data }, rejectUnrepresentable);
|
|
164
|
+
try {
|
|
165
|
+
this.storage.setItem(this.key, envelope);
|
|
166
|
+
this.storage.removeItem(this.key);
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
subscribe(listener) {
|
|
171
|
+
this.listeners.add(listener);
|
|
172
|
+
return () => this.listeners.delete(listener);
|
|
173
|
+
}
|
|
174
|
+
close() {
|
|
175
|
+
this.listeners.clear();
|
|
176
|
+
removeEventListener("storage", this.onStorage);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
function rejectUnrepresentable(_key, value) {
|
|
180
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
181
|
+
throw new TypeError(`${typeof value} values cannot be shared`);
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
|
|
112
186
|
// src/transport/default-transport.ts
|
|
113
187
|
function isBroadcastChannelAvailable() {
|
|
114
188
|
return typeof BroadcastChannel !== "undefined";
|
|
115
189
|
}
|
|
190
|
+
function isStorageEventAvailable() {
|
|
191
|
+
if (typeof addEventListener !== "function") return false;
|
|
192
|
+
try {
|
|
193
|
+
const storage = globalThis.localStorage;
|
|
194
|
+
if (!storage) return false;
|
|
195
|
+
const probe = "use-everywhere:probe";
|
|
196
|
+
storage.setItem(probe, "1");
|
|
197
|
+
storage.removeItem(probe);
|
|
198
|
+
return true;
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
116
203
|
function defaultTransport(name) {
|
|
117
|
-
|
|
204
|
+
if (isBroadcastChannelAvailable()) return new BroadcastChannelTransport(name);
|
|
205
|
+
if (isStorageEventAvailable()) {
|
|
206
|
+
devWarn(
|
|
207
|
+
"[use-everywhere] no BroadcastChannel; using the storage-event fallback. Values serialise as JSON, not structured clone \u2014 keep them JSON-shaped."
|
|
208
|
+
);
|
|
209
|
+
return new StorageTransport(name);
|
|
210
|
+
}
|
|
211
|
+
devWarn(
|
|
212
|
+
"[use-everywhere] no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
|
|
213
|
+
);
|
|
214
|
+
return new NoopTransport();
|
|
118
215
|
}
|
|
119
216
|
|
|
120
217
|
// src/bus.ts
|
|
121
218
|
function isBusWire(data) {
|
|
122
|
-
|
|
219
|
+
if (typeof data !== "object" || data === null) return false;
|
|
220
|
+
const wire = data;
|
|
221
|
+
return wire.v === 1 && typeof wire.scope === "string" && typeof wire.type === "string" && typeof wire.clientId === "string";
|
|
123
222
|
}
|
|
124
223
|
function defaultKind() {
|
|
125
224
|
return typeof document === "undefined" ? "worker" : "tab";
|
|
126
225
|
}
|
|
127
|
-
var
|
|
128
|
-
function
|
|
226
|
+
var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event"]);
|
|
227
|
+
function createBusCore(name, options, onShutdown) {
|
|
129
228
|
const transport = (options.transport ?? defaultTransport)(name);
|
|
130
229
|
const clientId = newClientId();
|
|
131
230
|
const kind = options.kind ?? defaultKind();
|
|
132
|
-
const
|
|
231
|
+
const handles = /* @__PURE__ */ new Set();
|
|
133
232
|
let refs = 0;
|
|
134
233
|
let closed = false;
|
|
135
|
-
const
|
|
234
|
+
const deliver = (wire, from) => {
|
|
235
|
+
for (const handle of handles) {
|
|
236
|
+
if (handle === from) continue;
|
|
237
|
+
for (const fn of handle.listeners) fn(wire);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const post = (wire, from) => {
|
|
136
241
|
if (closed) return;
|
|
137
242
|
emitBusEvent(name, "out", wire);
|
|
138
243
|
transport.post(wire);
|
|
244
|
+
if (SHARED_WITHIN_CLIENT.has(wire.scope)) deliver(wire, from);
|
|
139
245
|
};
|
|
140
246
|
const unsubscribe = transport.subscribe((data) => {
|
|
141
247
|
if (!isBusWire(data)) return;
|
|
142
248
|
if (data.clientId === clientId) return;
|
|
143
249
|
emitBusEvent(name, "in", data);
|
|
144
250
|
if (data.scope === "presence" && data.type === "hello") {
|
|
145
|
-
post({ v: 1, scope: "presence", type: "ping", clientId, kind });
|
|
251
|
+
post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null);
|
|
146
252
|
}
|
|
147
|
-
|
|
253
|
+
deliver(data, null);
|
|
148
254
|
});
|
|
149
|
-
post({ v: 1, scope: "presence", type: "hello", clientId, kind });
|
|
255
|
+
post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
|
|
150
256
|
const heartbeat = setInterval(
|
|
151
|
-
() => post({ v: 1, scope: "presence", type: "ping", clientId, kind }),
|
|
257
|
+
() => post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null),
|
|
152
258
|
options.heartbeatMs ?? 2e3
|
|
153
259
|
);
|
|
154
|
-
const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind });
|
|
260
|
+
const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind }, null);
|
|
155
261
|
const onPageShow = (event) => {
|
|
156
262
|
if (!event.persisted) return;
|
|
157
|
-
post({ v: 1, scope: "presence", type: "hello", clientId, kind });
|
|
263
|
+
post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
|
|
264
|
+
};
|
|
265
|
+
const onVisible = () => {
|
|
266
|
+
if (document.visibilityState === "visible") {
|
|
267
|
+
post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
|
|
268
|
+
}
|
|
158
269
|
};
|
|
159
270
|
const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
|
|
160
271
|
if (hasWindow) {
|
|
161
272
|
addEventListener("pagehide", sayBye);
|
|
162
273
|
addEventListener("pageshow", onPageShow);
|
|
274
|
+
addEventListener("visibilitychange", onVisible);
|
|
163
275
|
}
|
|
276
|
+
const shutdown = () => {
|
|
277
|
+
sayBye();
|
|
278
|
+
closed = true;
|
|
279
|
+
clearInterval(heartbeat);
|
|
280
|
+
if (hasWindow) {
|
|
281
|
+
removeEventListener("pagehide", sayBye);
|
|
282
|
+
removeEventListener("pageshow", onPageShow);
|
|
283
|
+
removeEventListener("visibilitychange", onVisible);
|
|
284
|
+
}
|
|
285
|
+
unsubscribe();
|
|
286
|
+
transport.close();
|
|
287
|
+
handles.clear();
|
|
288
|
+
onShutdown();
|
|
289
|
+
};
|
|
164
290
|
return {
|
|
165
291
|
name,
|
|
166
292
|
clientId,
|
|
167
293
|
kind,
|
|
294
|
+
transportKind: transport.kind ?? "custom",
|
|
168
295
|
heartbeatMs: options.heartbeatMs ?? 2e3,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return () => listeners.delete(fn);
|
|
173
|
-
},
|
|
174
|
-
acquire() {
|
|
296
|
+
connect() {
|
|
297
|
+
const handle = { listeners: /* @__PURE__ */ new Set() };
|
|
298
|
+
handles.add(handle);
|
|
175
299
|
refs++;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
300
|
+
let released = false;
|
|
301
|
+
return {
|
|
302
|
+
name,
|
|
303
|
+
clientId,
|
|
304
|
+
kind,
|
|
305
|
+
transportKind: transport.kind ?? "custom",
|
|
306
|
+
post: (wire) => post(wire, handle),
|
|
307
|
+
subscribe(fn) {
|
|
308
|
+
handle.listeners.add(fn);
|
|
309
|
+
return () => handle.listeners.delete(fn);
|
|
310
|
+
},
|
|
311
|
+
release() {
|
|
312
|
+
if (released || closed) return;
|
|
313
|
+
released = true;
|
|
314
|
+
handles.delete(handle);
|
|
315
|
+
handle.listeners.clear();
|
|
316
|
+
refs--;
|
|
317
|
+
if (refs === 0) shutdown();
|
|
318
|
+
}
|
|
319
|
+
};
|
|
192
320
|
}
|
|
193
321
|
};
|
|
194
322
|
}
|
|
195
323
|
function getBusNames() {
|
|
196
|
-
return [...
|
|
324
|
+
return [...busTable().keys()];
|
|
325
|
+
}
|
|
326
|
+
function getTransportKind(name) {
|
|
327
|
+
return busTable().get(name)?.transportKind ?? null;
|
|
197
328
|
}
|
|
198
329
|
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);
|
|
330
|
+
if (options.transport) return createBusCore(name, options, () => {
|
|
331
|
+
}).connect();
|
|
332
|
+
const table = busTable();
|
|
333
|
+
let core = table.get(name);
|
|
334
|
+
if (!core) {
|
|
335
|
+
core = createBusCore(name, options, () => table.delete(name));
|
|
336
|
+
table.set(name, core);
|
|
209
337
|
} else {
|
|
210
|
-
if (options.heartbeatMs !== void 0 && options.heartbeatMs !==
|
|
338
|
+
if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
|
|
211
339
|
devWarn(
|
|
212
340
|
`[use-everywhere] bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
|
|
213
341
|
);
|
|
214
342
|
}
|
|
215
|
-
if (options.kind !== void 0 && options.kind !==
|
|
343
|
+
if (options.kind !== void 0 && options.kind !== core.kind) {
|
|
216
344
|
devWarn(`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
|
|
217
345
|
}
|
|
218
346
|
}
|
|
219
|
-
|
|
220
|
-
return bus;
|
|
347
|
+
return core.connect();
|
|
221
348
|
}
|
|
222
349
|
|
|
223
350
|
// src/channel.ts
|
|
@@ -269,6 +396,9 @@ function createChannel(name, options = {}) {
|
|
|
269
396
|
function newer(a, b) {
|
|
270
397
|
return !b || a[0] > b[0] || a[0] === b[0] && a[1] > b[1];
|
|
271
398
|
}
|
|
399
|
+
function isVersion(value) {
|
|
400
|
+
return Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && Number.isFinite(value[0]) && typeof value[1] === "string";
|
|
401
|
+
}
|
|
272
402
|
|
|
273
403
|
// src/dev-freeze.ts
|
|
274
404
|
var inDev2 = false;
|
|
@@ -308,7 +438,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
308
438
|
const live = liveStores.get(name) ?? 0;
|
|
309
439
|
if (live > 0) {
|
|
310
440
|
devWarn(
|
|
311
|
-
`[use-everywhere] second shared store for "${name}" in this tab \u2014
|
|
441
|
+
`[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.`
|
|
312
442
|
);
|
|
313
443
|
}
|
|
314
444
|
liveStores.set(name, live + 1);
|
|
@@ -356,11 +486,14 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
356
486
|
}
|
|
357
487
|
if (accept && !accept(meta)) return;
|
|
358
488
|
if (wire.type === "patch") {
|
|
489
|
+
if (typeof wire.key !== "string" || !isVersion(wire.version)) return;
|
|
359
490
|
applyRemote(wire.key, wire.value, wire.version, meta);
|
|
360
491
|
} else {
|
|
492
|
+
const versions2 = wire.versions;
|
|
493
|
+
if (typeof versions2 !== "object" || versions2 === null) return;
|
|
361
494
|
for (const k in wire.state) {
|
|
362
|
-
const version =
|
|
363
|
-
if (version) applyRemote(k, wire.state[k], version, meta);
|
|
495
|
+
const version = versions2[k];
|
|
496
|
+
if (isVersion(version)) applyRemote(k, wire.state[k], version, meta);
|
|
364
497
|
}
|
|
365
498
|
}
|
|
366
499
|
});
|
|
@@ -514,6 +647,7 @@ function createSharedStore(name, initial, options = {}) {
|
|
|
514
647
|
// src/presence.ts
|
|
515
648
|
function createPresence(name, options = {}) {
|
|
516
649
|
const pruneAfterMs = options.pruneAfterMs ?? 5e3;
|
|
650
|
+
const probeGraceMs = options.probeGraceMs ?? 1e3;
|
|
517
651
|
const bus = getBus(name, options);
|
|
518
652
|
const peers = /* @__PURE__ */ new Map();
|
|
519
653
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -523,6 +657,7 @@ function createPresence(name, options = {}) {
|
|
|
523
657
|
for (const fn of listeners) fn();
|
|
524
658
|
}
|
|
525
659
|
const unsubscribe = bus.subscribe((wire) => {
|
|
660
|
+
if (wire.clientId === bus.clientId) return;
|
|
526
661
|
if (wire.scope === "presence" && wire.type === "bye") {
|
|
527
662
|
if (peers.delete(wire.clientId)) notify();
|
|
528
663
|
return;
|
|
@@ -531,19 +666,41 @@ function createPresence(name, options = {}) {
|
|
|
531
666
|
peers.set(wire.clientId, { id: wire.clientId, kind: wire.kind, lastSeen: Date.now() });
|
|
532
667
|
if (!existing) notify();
|
|
533
668
|
});
|
|
669
|
+
bus.post({ v: 1, scope: "presence", type: "hello", clientId: bus.clientId, kind: bus.kind });
|
|
670
|
+
const probed = /* @__PURE__ */ new Map();
|
|
534
671
|
const prune = setInterval(
|
|
535
672
|
() => {
|
|
536
|
-
const
|
|
673
|
+
const now = Date.now();
|
|
674
|
+
const silentSince = now - pruneAfterMs;
|
|
537
675
|
let changed = false;
|
|
676
|
+
let needProbe = false;
|
|
538
677
|
for (const [id, peer] of peers) {
|
|
539
|
-
if (peer.lastSeen
|
|
678
|
+
if (peer.lastSeen >= silentSince) {
|
|
679
|
+
probed.delete(id);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const askedAt = probed.get(id);
|
|
683
|
+
if (askedAt === void 0) {
|
|
684
|
+
probed.set(id, now);
|
|
685
|
+
needProbe = true;
|
|
686
|
+
} else if (now - askedAt >= probeGraceMs) {
|
|
540
687
|
peers.delete(id);
|
|
688
|
+
probed.delete(id);
|
|
541
689
|
changed = true;
|
|
542
690
|
}
|
|
543
691
|
}
|
|
692
|
+
if (needProbe) {
|
|
693
|
+
bus.post({
|
|
694
|
+
v: 1,
|
|
695
|
+
scope: "presence",
|
|
696
|
+
type: "hello",
|
|
697
|
+
clientId: bus.clientId,
|
|
698
|
+
kind: bus.kind
|
|
699
|
+
});
|
|
700
|
+
}
|
|
544
701
|
if (changed) notify();
|
|
545
702
|
},
|
|
546
|
-
Math.max(
|
|
703
|
+
Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
|
|
547
704
|
);
|
|
548
705
|
let closed = false;
|
|
549
706
|
return {
|
|
@@ -564,9 +721,149 @@ function createPresence(name, options = {}) {
|
|
|
564
721
|
};
|
|
565
722
|
}
|
|
566
723
|
|
|
567
|
-
// src/leader.ts
|
|
724
|
+
// src/leader-web-locks.ts
|
|
568
725
|
var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
|
|
726
|
+
function createWebLocksLeader(name, options, locks) {
|
|
727
|
+
const busOptions = {
|
|
728
|
+
...options.transport ? { transport: options.transport } : {},
|
|
729
|
+
...options.kind ? { kind: options.kind } : {}
|
|
730
|
+
};
|
|
731
|
+
const bus = getBus(name, busOptions);
|
|
732
|
+
const clientId = bus.clientId;
|
|
733
|
+
let eligible = options.eligible ?? true;
|
|
734
|
+
let leaderId = null;
|
|
735
|
+
let snapshot = NO_LEADER;
|
|
736
|
+
let closed = false;
|
|
737
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
738
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
739
|
+
let releaseHeld = null;
|
|
740
|
+
let pending = null;
|
|
741
|
+
function setLeader(id) {
|
|
742
|
+
if (id === leaderId) return;
|
|
743
|
+
leaderId = id;
|
|
744
|
+
snapshot = Object.freeze({ leaderId: id, isLeader: id === clientId });
|
|
745
|
+
for (const fn of listeners) fn();
|
|
746
|
+
if (id === clientId) {
|
|
747
|
+
for (const waiter of waiters) waiter.resolve();
|
|
748
|
+
waiters.clear();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const term = [1, clientId];
|
|
752
|
+
const announce2 = () => bus.post({ v: 1, scope: "leader", type: "heartbeat", term, clientId, kind: bus.kind });
|
|
753
|
+
const unsubscribe = bus.subscribe((wire) => {
|
|
754
|
+
if (wire.scope !== "leader") return;
|
|
755
|
+
if (wire.type === "hello") {
|
|
756
|
+
if (leaderId === clientId) announce2();
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (wire.type === "resign") {
|
|
760
|
+
if (wire.clientId === leaderId) setLeader(null);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
setLeader(wire.clientId);
|
|
764
|
+
});
|
|
765
|
+
function joinQueue() {
|
|
766
|
+
if (closed || !eligible || releaseHeld || pending) return;
|
|
767
|
+
const controller = new AbortController();
|
|
768
|
+
pending = controller;
|
|
769
|
+
const held = new Promise((resolve) => {
|
|
770
|
+
releaseHeld = resolve;
|
|
771
|
+
});
|
|
772
|
+
void locks.request(name, { signal: controller.signal }, async () => {
|
|
773
|
+
pending = null;
|
|
774
|
+
if (closed || !eligible) return;
|
|
775
|
+
setLeader(clientId);
|
|
776
|
+
announce2();
|
|
777
|
+
await held;
|
|
778
|
+
}).catch(() => {
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
function letGo() {
|
|
782
|
+
const release = releaseHeld;
|
|
783
|
+
releaseHeld = null;
|
|
784
|
+
release?.();
|
|
785
|
+
}
|
|
786
|
+
function resign() {
|
|
787
|
+
if (leaderId !== clientId) return;
|
|
788
|
+
letGo();
|
|
789
|
+
setLeader(null);
|
|
790
|
+
bus.post({ v: 1, scope: "leader", type: "resign", term, clientId, kind: bus.kind });
|
|
791
|
+
joinQueue();
|
|
792
|
+
}
|
|
793
|
+
const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
|
|
794
|
+
const onPageHide = () => resign();
|
|
795
|
+
if (hasWindow) addEventListener("pagehide", onPageHide);
|
|
796
|
+
bus.post({ v: 1, scope: "leader", type: "hello", clientId, kind: bus.kind });
|
|
797
|
+
joinQueue();
|
|
798
|
+
return {
|
|
799
|
+
clientId,
|
|
800
|
+
strategy: "web-locks",
|
|
801
|
+
getSnapshot: () => snapshot,
|
|
802
|
+
subscribe(fn) {
|
|
803
|
+
listeners.add(fn);
|
|
804
|
+
return () => listeners.delete(fn);
|
|
805
|
+
},
|
|
806
|
+
waitForLeadership() {
|
|
807
|
+
if (leaderId === clientId) return Promise.resolve();
|
|
808
|
+
if (closed) return Promise.reject(new Error("leader is closed"));
|
|
809
|
+
return new Promise((resolve, reject) => {
|
|
810
|
+
waiters.add({ resolve, reject });
|
|
811
|
+
});
|
|
812
|
+
},
|
|
813
|
+
resign,
|
|
814
|
+
setEligible(next) {
|
|
815
|
+
if (next === eligible) return;
|
|
816
|
+
eligible = next;
|
|
817
|
+
if (next) {
|
|
818
|
+
joinQueue();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (leaderId === clientId) resignWithoutRequeue();
|
|
822
|
+
pending?.abort();
|
|
823
|
+
pending = null;
|
|
824
|
+
},
|
|
825
|
+
close() {
|
|
826
|
+
if (closed) return;
|
|
827
|
+
closed = true;
|
|
828
|
+
if (leaderId === clientId) resignWithoutRequeue();
|
|
829
|
+
pending?.abort();
|
|
830
|
+
pending = null;
|
|
831
|
+
if (hasWindow) removeEventListener("pagehide", onPageHide);
|
|
832
|
+
for (const waiter of waiters) waiter.reject(new Error("leader is closed"));
|
|
833
|
+
waiters.clear();
|
|
834
|
+
unsubscribe();
|
|
835
|
+
listeners.clear();
|
|
836
|
+
bus.release();
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
function resignWithoutRequeue() {
|
|
840
|
+
letGo();
|
|
841
|
+
setLeader(null);
|
|
842
|
+
bus.post({ v: 1, scope: "leader", type: "resign", term, clientId, kind: bus.kind });
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/leader.ts
|
|
847
|
+
var NO_LEADER2 = Object.freeze({ leaderId: null, isLeader: false });
|
|
848
|
+
function availableLocks(options) {
|
|
849
|
+
if (options.locks) return options.locks;
|
|
850
|
+
const locks = globalThis.navigator?.locks;
|
|
851
|
+
return typeof locks?.request === "function" ? locks : void 0;
|
|
852
|
+
}
|
|
569
853
|
function createLeader(name, options = {}) {
|
|
854
|
+
const strategy = options.strategy ?? "auto";
|
|
855
|
+
if (strategy !== "heartbeat") {
|
|
856
|
+
const locks = availableLocks(options);
|
|
857
|
+
if (locks) return createWebLocksLeader(name, options, locks);
|
|
858
|
+
if (strategy === "web-locks") {
|
|
859
|
+
throw new Error(
|
|
860
|
+
'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.'
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return createHeartbeatLeader(name, options);
|
|
865
|
+
}
|
|
866
|
+
function createHeartbeatLeader(name, options) {
|
|
570
867
|
const heartbeatMs = options.heartbeatMs ?? 1e3;
|
|
571
868
|
const leaseMs = options.leaseMs ?? 3e3;
|
|
572
869
|
const busOptions = {
|
|
@@ -578,16 +875,21 @@ function createLeader(name, options = {}) {
|
|
|
578
875
|
let eligible = options.eligible ?? true;
|
|
579
876
|
let leaderId = null;
|
|
580
877
|
let term = [0, clientId];
|
|
581
|
-
let snapshot =
|
|
878
|
+
let snapshot = NO_LEADER2;
|
|
582
879
|
let beat;
|
|
583
880
|
let lease;
|
|
584
881
|
let closed = false;
|
|
585
882
|
const listeners = /* @__PURE__ */ new Set();
|
|
883
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
586
884
|
function setLeader(id) {
|
|
587
885
|
if (id === leaderId) return;
|
|
588
886
|
leaderId = id;
|
|
589
887
|
snapshot = Object.freeze({ leaderId: id, isLeader: id === clientId });
|
|
590
888
|
for (const fn of listeners) fn();
|
|
889
|
+
if (id === clientId) {
|
|
890
|
+
for (const waiter of waiters) waiter.resolve();
|
|
891
|
+
waiters.clear();
|
|
892
|
+
}
|
|
591
893
|
}
|
|
592
894
|
function armLease(delay) {
|
|
593
895
|
clearTimeout(lease);
|
|
@@ -625,6 +927,7 @@ function createLeader(name, options = {}) {
|
|
|
625
927
|
armLease(0);
|
|
626
928
|
return;
|
|
627
929
|
}
|
|
930
|
+
if (!isVersion(wire.term)) return;
|
|
628
931
|
if (newer(wire.term, term)) {
|
|
629
932
|
term = wire.term;
|
|
630
933
|
stepDown(wire.clientId);
|
|
@@ -660,11 +963,19 @@ function createLeader(name, options = {}) {
|
|
|
660
963
|
armLease(heartbeatMs);
|
|
661
964
|
return {
|
|
662
965
|
clientId,
|
|
966
|
+
strategy: "heartbeat",
|
|
663
967
|
getSnapshot: () => snapshot,
|
|
664
968
|
subscribe(fn) {
|
|
665
969
|
listeners.add(fn);
|
|
666
970
|
return () => listeners.delete(fn);
|
|
667
971
|
},
|
|
972
|
+
waitForLeadership() {
|
|
973
|
+
if (leaderId === clientId) return Promise.resolve();
|
|
974
|
+
if (closed) return Promise.reject(new Error("leader is closed"));
|
|
975
|
+
return new Promise((resolve, reject) => {
|
|
976
|
+
waiters.add({ resolve, reject });
|
|
977
|
+
});
|
|
978
|
+
},
|
|
668
979
|
resign,
|
|
669
980
|
setEligible(next) {
|
|
670
981
|
if (next === eligible) return;
|
|
@@ -679,6 +990,8 @@ function createLeader(name, options = {}) {
|
|
|
679
990
|
if (closed) return;
|
|
680
991
|
closed = true;
|
|
681
992
|
resign();
|
|
993
|
+
for (const waiter of waiters) waiter.reject(new Error("leader is closed"));
|
|
994
|
+
waiters.clear();
|
|
682
995
|
clearInterval(beat);
|
|
683
996
|
clearTimeout(lease);
|
|
684
997
|
if (hasWindow) {
|
|
@@ -988,6 +1301,7 @@ export {
|
|
|
988
1301
|
DEFAULT_NAME,
|
|
989
1302
|
HandshakeTimeoutError,
|
|
990
1303
|
NoopTransport,
|
|
1304
|
+
StorageTransport,
|
|
991
1305
|
WindowClosedError,
|
|
992
1306
|
connectToOpener,
|
|
993
1307
|
createChannel,
|
|
@@ -997,7 +1311,9 @@ export {
|
|
|
997
1311
|
defaultTransport,
|
|
998
1312
|
enableDebug,
|
|
999
1313
|
getBusNames,
|
|
1314
|
+
getTransportKind,
|
|
1000
1315
|
isBroadcastChannelAvailable,
|
|
1316
|
+
isStorageEventAvailable,
|
|
1001
1317
|
localStorageAdapter,
|
|
1002
1318
|
newer,
|
|
1003
1319
|
observeBus,
|
package/dist/testing.cjs
CHANGED
package/dist/testing.d.cts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { T as Transport } from './transport.types-
|
|
1
|
+
import { T as Transport, a as TransportKind } from './transport.types-CV1WZOhy.cjs';
|
|
2
2
|
|
|
3
3
|
/** One simulated client on a MemoryHub. Create via hub.connect(). */
|
|
4
4
|
declare class MemoryTransport implements Transport {
|
|
5
5
|
private hub;
|
|
6
|
+
readonly kind: TransportKind;
|
|
6
7
|
private listeners;
|
|
7
8
|
private closed;
|
|
8
9
|
constructor(hub: MemoryHub);
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { T as Transport } from './transport.types-
|
|
1
|
+
import { T as Transport, a as TransportKind } from './transport.types-CV1WZOhy.js';
|
|
2
2
|
|
|
3
3
|
/** One simulated client on a MemoryHub. Create via hub.connect(). */
|
|
4
4
|
declare class MemoryTransport implements Transport {
|
|
5
5
|
private hub;
|
|
6
|
+
readonly kind: TransportKind;
|
|
6
7
|
private listeners;
|
|
7
8
|
private closed;
|
|
8
9
|
constructor(hub: MemoryHub);
|
package/dist/testing.js
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which mechanism a transport actually uses. Worth surfacing, because they are
|
|
3
|
+
* not equivalent: `storage` is a fallback with lower fidelity than
|
|
4
|
+
* `broadcast-channel`, and `none` means nothing is being shared at all.
|
|
5
|
+
*/
|
|
6
|
+
type TransportKind = 'broadcast-channel' | 'storage' | 'memory' | 'none' | (string & {});
|
|
7
|
+
/**
|
|
8
|
+
* Minimal message bus. Implementations: BroadcastChannelTransport (same-origin),
|
|
9
|
+
* StorageTransport (fallback), NoopTransport (SSR / local-only), MemoryTransport
|
|
10
|
+
* (tests). A transport never echoes a client's own posts back to it.
|
|
11
|
+
*/
|
|
12
|
+
interface Transport {
|
|
13
|
+
/** What this transport is. Optional so a custom transport need not declare one. */
|
|
14
|
+
readonly kind?: TransportKind;
|
|
15
|
+
post(data: unknown): void;
|
|
16
|
+
subscribe(listener: (data: unknown) => void): () => void;
|
|
17
|
+
close(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type { Transport as T, TransportKind as a };
|