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.
|
|
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.
|
|
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
|
|
219
|
-
// our overlay. Runs once; no-op in Node / where
|
|
220
|
-
// (and thus streamTimes) first so conflict resolution can see disk
|
|
221
|
-
//
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
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
|
-
//
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 (
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
60
|
+
channels.set(collection, created);
|
|
61
|
+
return created;
|
|
40
62
|
}
|
|
41
63
|
|
|
42
|
-
// Subscribe to remote writes for a collection
|
|
43
|
-
//
|
|
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 (!
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 (!
|
|
61
|
-
|
|
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.
|
|
1944
|
-
version "1.1.
|
|
1945
|
-
resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.1.
|
|
1946
|
-
integrity sha512-
|
|
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();
|