sliftutils 1.1.49 → 1.1.51

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/index.d.ts CHANGED
@@ -953,11 +953,6 @@ declare module "sliftutils/storage/BulkDatabase2/syncClient" {
953
953
 
954
954
  }
955
955
 
956
- declare module "sliftutils/storage/BulkDatabase2/syncWorker" {
957
- export {};
958
-
959
- }
960
-
961
956
  declare module "sliftutils/storage/CBORStorage" {
962
957
  /// <reference types="node" />
963
958
  /// <reference types="node" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.1.49",
3
+ "version": "1.1.51",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -51,7 +51,7 @@
51
51
  "mobx": "^6.13.3",
52
52
  "preact-old-types": "^10.28.1",
53
53
  "shell-quote": "^1.8.3",
54
- "socket-function": "^1.1.40",
54
+ "socket-function": "^1.1.41",
55
55
  "typenode": "*",
56
56
  "typesafecss": "*",
57
57
  "ws": "^8.18.3",
@@ -215,10 +215,11 @@ export class BulkDatabaseBase<T extends { key: string }> {
215
215
  return joined;
216
216
  });
217
217
 
218
- // Connects to the SharedWorker (browser only) so writes in other tabs of this collection update
219
- // our overlay. Runs once; no-op in Node / where SharedWorker is unavailable. We wait for the reader
220
- // (and thus streamTimes) first so conflict resolution can see disk timestamps, then apply the
221
- // worker's buffered recent writes (which may not be on disk yet).
218
+ // Connects to the cross-tab BroadcastChannel (browser only) so writes in other tabs of this
219
+ // collection update our overlay. Runs once; no-op in Node / where BroadcastChannel is unavailable.
220
+ // We wait for the reader (and thus streamTimes) first so conflict resolution can see disk
221
+ // timestamps, then peers reply to our hello with recent writes that may not be on disk yet (applied
222
+ // through the same applyRemote callback).
222
223
  private syncSetup = lazy(async () => {
223
224
  if (!isSyncSupported()) return;
224
225
  await this.reader();
@@ -1,62 +1,81 @@
1
- // Browser-side client for the BulkDatabase2 SharedWorker (syncWorker). One SharedWorker connection
2
- // per tab, multiplexed across collections. No-op when SharedWorker is unavailable (Node / unsupported
3
- // browsers), so BulkDatabase2 can call these unconditionally.
1
+ import { isNode } from "typesafecss";
2
+
3
+ // Browser-side cross-tab write sync for BulkDatabase2, over BroadcastChannel (one channel per
4
+ // collection, same-origin tabs). It does NOT persist anything — each tab writes to disk itself; this
5
+ // just relays live writes to other open tabs and, when a tab starts up, asks peers for writes they've
6
+ // made recently that may not be on disk yet. No-op in Node / where BroadcastChannel is unavailable, so
7
+ // BulkDatabase2 can call these unconditionally. This is an optional feature, so there is no fallback.
4
8
 
5
9
  export type RemoteWrite = { key: string; time: number; deleted?: boolean; value?: unknown };
6
10
 
7
- // Path the worker bundle is served from (alongside the page bundles).
8
- const WORKER_URL = "/syncWorker.js";
11
+ // Writes older than this are assumed already flushed to disk (a freshly-opened tab gets those by
12
+ // reading disk), so a tab only replays writes newer than this when a peer says hello.
13
+ const RECENT_WINDOW_MS = 60_000;
14
+
15
+ type Channel = {
16
+ bc: BroadcastChannel;
17
+ subscribers: ((write: RemoteWrite) => void)[];
18
+ // This tab's own recent writes, kept so it can answer another tab's "hello".
19
+ recent: RemoteWrite[];
20
+ };
9
21
 
10
- let port: MessagePort | undefined;
11
- let initialized = false;
12
- const subscribers = new Map<string, ((write: RemoteWrite) => void)[]>();
13
- const recentResolvers = new Map<string, ((writes: RemoteWrite[]) => void)[]>();
22
+ const channels = new Map<string, Channel>();
14
23
 
15
24
  export function isSyncSupported(): boolean {
16
- if (typeof SharedWorker === "undefined") return false;
17
- // Under file:// the SharedWorker constructor throws a SecurityError (cross-origin / opaque
18
- // origin), so skip it rather than logging that error on every load.
19
- if (typeof location !== "undefined" && location.protocol === "file:") return false;
20
- return true;
25
+ return !isNode() && typeof BroadcastChannel !== "undefined";
26
+ }
27
+
28
+ function pruneRecent(channel: Channel) {
29
+ const cutoff = Date.now() - RECENT_WINDOW_MS;
30
+ channel.recent = channel.recent.filter(w => w.time >= cutoff);
31
+ }
32
+
33
+ function deliver(channel: Channel, write: RemoteWrite) {
34
+ for (const sub of channel.subscribers) sub(write);
21
35
  }
22
36
 
23
- function ensure() {
24
- if (initialized) return;
25
- initialized = true;
26
- if (!isSyncSupported()) return;
27
- let worker = new SharedWorker(WORKER_URL);
28
- port = worker.port;
29
- port.onmessage = (ev: MessageEvent) => {
30
- let msg = ev.data as { type: string; collection: string; writes?: RemoteWrite[] } & RemoteWrite;
31
- if (msg.type === "recent") {
32
- let resolvers = recentResolvers.get(msg.collection) || [];
33
- recentResolvers.set(msg.collection, []);
34
- for (let resolve of resolvers) resolve(msg.writes || []);
35
- } else if (msg.type === "write") {
36
- for (let cb of subscribers.get(msg.collection) || []) cb(msg);
37
+ function ensure(collection: string): Channel | undefined {
38
+ if (!isSyncSupported()) return undefined;
39
+ let channel = channels.get(collection);
40
+ if (channel) return channel;
41
+ // BroadcastChannel never delivers a message back to the instance that sent it, so a tab never
42
+ // hears its own writes — only the other open tabs do.
43
+ const bc = new BroadcastChannel(`bulkDatabase2:${collection}`);
44
+ const created: Channel = { bc, subscribers: [], recent: [] };
45
+ bc.onmessage = (event: MessageEvent) => {
46
+ const msg = event.data as { type: string; write?: RemoteWrite; writes?: RemoteWrite[] };
47
+ if (!msg) return;
48
+ if (msg.type === "write" && msg.write) {
49
+ deliver(created, msg.write);
50
+ } else if (msg.type === "recent" && msg.writes) {
51
+ for (const write of msg.writes) deliver(created, write);
52
+ } else if (msg.type === "hello") {
53
+ // Another tab just started; replay our recent writes so it doesn't miss any. The reply goes
54
+ // to every tab, but peers that already have a write ignore it (its timestamp isn't newer
55
+ // than what they hold), so the redundant broadcast is harmless.
56
+ pruneRecent(created);
57
+ if (created.recent.length) created.bc.postMessage({ type: "recent", writes: created.recent });
37
58
  }
38
59
  };
39
- port.start();
60
+ channels.set(collection, created);
61
+ return created;
40
62
  }
41
63
 
42
- // Subscribe to remote writes for a collection and return the worker's buffered recent writes (so we
43
- // catch writes another tab broadcast but hasn't flushed to disk yet).
64
+ // Subscribe to remote writes for a collection. Recent writes from already-open tabs arrive through the
65
+ // same onWrite callback (as the reply to our hello), so the returned array is always empty — it's kept
66
+ // only for API compatibility with callers that await it.
44
67
  export function connect(collection: string, onWrite: (write: RemoteWrite) => void): Promise<RemoteWrite[]> {
45
- ensure();
46
- if (!port) return Promise.resolve([]);
47
- let subs = subscribers.get(collection);
48
- if (!subs) { subs = []; subscribers.set(collection, subs); }
49
- subs.push(onWrite);
50
- return new Promise(resolve => {
51
- let resolvers = recentResolvers.get(collection);
52
- if (!resolvers) { resolvers = []; recentResolvers.set(collection, resolvers); }
53
- resolvers.push(resolve);
54
- port!.postMessage({ type: "hello", collection });
55
- });
68
+ const channel = ensure(collection);
69
+ if (!channel) return Promise.resolve([]);
70
+ channel.subscribers.push(onWrite);
71
+ channel.bc.postMessage({ type: "hello" });
72
+ return Promise.resolve([]);
56
73
  }
57
74
 
58
- export function broadcast(collection: string, write: RemoteWrite) {
59
- ensure();
60
- if (!port) return;
61
- port.postMessage({ type: "write", collection, key: write.key, time: write.time, deleted: write.deleted, value: write.value });
75
+ export function broadcast(collection: string, write: RemoteWrite): void {
76
+ const channel = ensure(collection);
77
+ if (!channel) return;
78
+ channel.recent.push(write);
79
+ pruneRecent(channel);
80
+ channel.bc.postMessage({ type: "write", write });
62
81
  }
package/yarn.lock CHANGED
@@ -1940,10 +1940,10 @@ slash@^3.0.0:
1940
1940
  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
1941
1941
  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
1942
1942
 
1943
- socket-function@^1.1.40:
1944
- version "1.1.40"
1945
- resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.1.40.tgz#0994aa018b3fcc64a131d29e5926377b7baff280"
1946
- integrity sha512-PtU8csq90nbjM/Jr/irRHzj2PojUgcMXTctfBMDEHJrLKmeOM4flf87svn0M8ad5iwNiSyav8SdxCzCl9IwZJw==
1943
+ socket-function@^1.1.41:
1944
+ version "1.1.41"
1945
+ resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.1.41.tgz#03e7c187d16e8353f90cff511bb3432a4e297050"
1946
+ integrity sha512-d/c5wUKk/WEhvH7d4M7jtYEPcZifqulG/OkP3EkyYypS34cL0a40i2x9gqqYVxo6AYosJC2IuO7NR9fNgqfIKA==
1947
1947
  dependencies:
1948
1948
  "@types/pako" "^2.0.3"
1949
1949
  "@types/ws" "^8.5.3"
@@ -1 +0,0 @@
1
- export {};
@@ -1,65 +0,0 @@
1
- import { isNode } from "typesafecss";
2
-
3
- // SharedWorker that synchronizes BulkDatabase2 writes between tabs of the same origin. It does NOT
4
- // persist anything (each tab still writes to disk itself); it just relays writes to other tabs and
5
- // buffers the recent ones so a freshly-opened tab can catch writes that haven't been flushed to disk
6
- // yet. Per collection it keeps the latest write per key, pruned to a short recency window (older
7
- // writes are already on disk, so a new tab gets them from there).
8
-
9
- type RemoteWrite = { key: string; time: number; deleted?: boolean; value?: unknown };
10
- type Collection = { ports: Set<MessagePort>; recent: Map<string, RemoteWrite> };
11
-
12
- const RECENT_WINDOW_MS = 60_000;
13
- const collections = new Map<string, Collection>();
14
-
15
- function getCollection(name: string): Collection {
16
- let c = collections.get(name);
17
- if (!c) {
18
- c = { ports: new Set(), recent: new Map() };
19
- collections.set(name, c);
20
- }
21
- return c;
22
- }
23
-
24
- function prune(c: Collection) {
25
- let cutoff = Date.now() - RECENT_WINDOW_MS;
26
- for (let [key, write] of c.recent) {
27
- if (write.time < cutoff) c.recent.delete(key);
28
- }
29
- }
30
-
31
- function post(port: MessagePort, message: unknown) {
32
- try {
33
- port.postMessage(message);
34
- } catch {
35
- // Port closed (tab gone); it'll be dropped on the next failed send.
36
- }
37
- }
38
-
39
- function main() {
40
- if (isNode()) return;
41
- (self as unknown as { onconnect: (e: MessageEvent) => void }).onconnect = (event: MessageEvent) => {
42
- let port = (event as unknown as { ports: MessagePort[] }).ports[0];
43
- port.onmessage = (ev: MessageEvent) => {
44
- let msg = ev.data as { type: string; collection: string } & RemoteWrite;
45
- let c = getCollection(msg.collection);
46
- if (msg.type === "hello") {
47
- c.ports.add(port);
48
- prune(c);
49
- port.postMessage({ type: "recent", collection: msg.collection, writes: [...c.recent.values()] });
50
- } else if (msg.type === "write") {
51
- prune(c);
52
- let existing = c.recent.get(msg.key);
53
- if (!existing || msg.time > existing.time) {
54
- c.recent.set(msg.key, { key: msg.key, time: msg.time, deleted: msg.deleted, value: msg.value });
55
- }
56
- for (let other of c.ports) {
57
- if (other !== port) post(other, msg);
58
- }
59
- }
60
- };
61
- port.start();
62
- };
63
- }
64
-
65
- main();