arrowbase 0.1.1
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/CHANGELOG.md +27 -0
- package/COMPATIBILITY.md +398 -0
- package/LICENSE +21 -0
- package/MIGRATING.md +238 -0
- package/README.md +897 -0
- package/RELEASE_NOTES.md +68 -0
- package/dist/arrow.d.ts +93 -0
- package/dist/arrow.js +5366 -0
- package/dist/arrow.js.map +1 -0
- package/dist/broadcast.d.ts +173 -0
- package/dist/broadcast.js +319 -0
- package/dist/broadcast.js.map +1 -0
- package/dist/collection-DGlKgOGi.d.ts +1932 -0
- package/dist/idb.d.ts +146 -0
- package/dist/idb.js +6193 -0
- package/dist/idb.js.map +1 -0
- package/dist/index.d.ts +1802 -0
- package/dist/index.js +13924 -0
- package/dist/index.js.map +1 -0
- package/dist/query-CzV9E57Y.d.ts +279 -0
- package/dist/react.d.ts +51 -0
- package/dist/react.js +1633 -0
- package/dist/react.js.map +1 -0
- package/dist/tanstack.d.ts +51 -0
- package/dist/tanstack.js +191 -0
- package/dist/tanstack.js.map +1 -0
- package/dist/types-CSoU6uED.d.ts +50 -0
- package/package.json +139 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { C as Collection } from './collection-DGlKgOGi.js';
|
|
2
|
+
import { D as Delta } from './types-CSoU6uED.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cross-tab sync via `BroadcastChannel`.
|
|
6
|
+
*
|
|
7
|
+
* Optional subpath (`arrowbase/broadcast`). Each participating document
|
|
8
|
+
* (tab, iframe, shared-worker) opens a `BroadcastSync` on the same
|
|
9
|
+
* channel name and shares deltas with the other participants. Reuses
|
|
10
|
+
* {@link ChangeLog} (outbound buffering) and {@link applyDeltas}
|
|
11
|
+
* (inbound) so the wire protocol is just the transport.
|
|
12
|
+
*
|
|
13
|
+
* ### Usage
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { Collection, defineSchema } from 'arrowbase';
|
|
17
|
+
* import { BroadcastSync } from 'arrowbase/broadcast';
|
|
18
|
+
*
|
|
19
|
+
* const schema = defineSchema({ ... });
|
|
20
|
+
* const collection = Collection.create(schema, { capacity: 10_000 });
|
|
21
|
+
*
|
|
22
|
+
* const bc = new BroadcastSync(collection, { channelName: 'users' });
|
|
23
|
+
* bc.start();
|
|
24
|
+
*
|
|
25
|
+
* // Later…
|
|
26
|
+
* bc.stop();
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* ### Protocol
|
|
30
|
+
*
|
|
31
|
+
* One channel carries four message kinds. Every message tags itself
|
|
32
|
+
* with a per-instance `nodeId` so a sender can filter out its own
|
|
33
|
+
* echoes and a receiver can dedupe per-peer deltas. When a message is
|
|
34
|
+
* *applied* locally, the sync layer flips a reentrancy guard that
|
|
35
|
+
* suppresses its re-broadcast — otherwise the resulting ChangeEvent
|
|
36
|
+
* would go round the ring forever.
|
|
37
|
+
*
|
|
38
|
+
* - `delta` — `{ type, nodeId, deltas }`. Ordinary mutation.
|
|
39
|
+
* - `sync-request` — `{ type, nodeId, sinceVersion }`. Emitted by
|
|
40
|
+
* `start()` (unless `requestSyncOnStart: false`)
|
|
41
|
+
* and by the public `requestSync()` method. Peers
|
|
42
|
+
* reply with a targeted `sync-response`.
|
|
43
|
+
* - `sync-response` — `{ type, nodeId, toNodeId, deltas, upTo }`.
|
|
44
|
+
* Reply addressed at the requester; others ignore.
|
|
45
|
+
* - `resync` — `{ type, nodeId, version }`. Published on
|
|
46
|
+
* `compact` / `rollback` events, since those
|
|
47
|
+
* break incremental delta coherence. Peers should
|
|
48
|
+
* refetch a snapshot out-of-band (e.g., via
|
|
49
|
+
* IdbPersistence) then rejoin.
|
|
50
|
+
*
|
|
51
|
+
* ### Non-goals
|
|
52
|
+
*
|
|
53
|
+
* - Convergence under concurrent edits. Last-writer-wins; if two tabs
|
|
54
|
+
* mutate the same row at the same version, the receiver applies
|
|
55
|
+
* whichever delta arrived later. For proper conflict resolution,
|
|
56
|
+
* layer a CRDT atop.
|
|
57
|
+
* - Delivery guarantees. `BroadcastChannel` is best-effort inside the
|
|
58
|
+
* same origin; messages may be dropped under extreme memory
|
|
59
|
+
* pressure. If this matters, pair with a persistence adapter and
|
|
60
|
+
* use `hello` + `sync-request` on reconnect.
|
|
61
|
+
* - Security. Every same-origin document on the channel sees every
|
|
62
|
+
* message. Do not broadcast secrets.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
interface DeltaMessage {
|
|
66
|
+
readonly type: 'delta';
|
|
67
|
+
readonly nodeId: string;
|
|
68
|
+
readonly deltas: readonly Delta[];
|
|
69
|
+
}
|
|
70
|
+
interface SyncRequestMessage {
|
|
71
|
+
readonly type: 'sync-request';
|
|
72
|
+
readonly nodeId: string;
|
|
73
|
+
readonly sinceVersion: number;
|
|
74
|
+
}
|
|
75
|
+
interface SyncResponseMessage {
|
|
76
|
+
readonly type: 'sync-response';
|
|
77
|
+
readonly nodeId: string;
|
|
78
|
+
/** Only the intended recipient processes this. Others ignore it. */
|
|
79
|
+
readonly toNodeId: string;
|
|
80
|
+
readonly deltas: readonly Delta[];
|
|
81
|
+
readonly upTo: number;
|
|
82
|
+
}
|
|
83
|
+
interface ResyncMessage {
|
|
84
|
+
readonly type: 'resync';
|
|
85
|
+
readonly nodeId: string;
|
|
86
|
+
readonly version: number;
|
|
87
|
+
}
|
|
88
|
+
type BroadcastMessage = DeltaMessage | SyncRequestMessage | SyncResponseMessage | ResyncMessage;
|
|
89
|
+
interface BroadcastSyncOptions {
|
|
90
|
+
/** Channel name. All participants on this channel share state. */
|
|
91
|
+
channelName: string;
|
|
92
|
+
/**
|
|
93
|
+
* Unique per-instance identifier. Defaults to a random string. Only
|
|
94
|
+
* used to filter echoed messages; two instances with the same id
|
|
95
|
+
* still work but self-echo skipping won't catch cross-instance
|
|
96
|
+
* duplicates (harmless because applyDeltas is idempotent).
|
|
97
|
+
*/
|
|
98
|
+
nodeId?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Optional callback fired when a peer broadcasts a `resync` message
|
|
101
|
+
* (triggered by `compact` / `rollback` events on the sender).
|
|
102
|
+
* Receivers typically re-hydrate from persistence and then rejoin.
|
|
103
|
+
*/
|
|
104
|
+
onResync?: (event: {
|
|
105
|
+
fromNodeId: string;
|
|
106
|
+
version: number;
|
|
107
|
+
}) => void;
|
|
108
|
+
/**
|
|
109
|
+
* Optional callback for any error in the channel pipeline (parse
|
|
110
|
+
* errors, serialization failures). Default: `console.error`.
|
|
111
|
+
*/
|
|
112
|
+
onError?: (err: unknown) => void;
|
|
113
|
+
/**
|
|
114
|
+
* Injected `BroadcastChannel` constructor (for tests that want a
|
|
115
|
+
* fake, or environments that polyfill under a different global).
|
|
116
|
+
* Defaults to `globalThis.BroadcastChannel`.
|
|
117
|
+
*/
|
|
118
|
+
BroadcastChannel?: typeof globalThis.BroadcastChannel;
|
|
119
|
+
/**
|
|
120
|
+
* If true, on `start()` emit a `sync-request(collection.globalVersion)`
|
|
121
|
+
* so existing peers can catch us up. Default `true`. Set to `false`
|
|
122
|
+
* when you are starting with a trusted snapshot (e.g., IdbPersistence)
|
|
123
|
+
* and only want to receive live deltas from now on.
|
|
124
|
+
*/
|
|
125
|
+
requestSyncOnStart?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* `ChangeLog.bufferLimit` — how many deltas we retain for sync
|
|
128
|
+
* responses. Defaults to the ChangeLog's own default.
|
|
129
|
+
*/
|
|
130
|
+
bufferLimit?: number;
|
|
131
|
+
}
|
|
132
|
+
declare class BroadcastSync {
|
|
133
|
+
private readonly collection;
|
|
134
|
+
readonly nodeId: string;
|
|
135
|
+
private readonly channel;
|
|
136
|
+
private readonly changeLog;
|
|
137
|
+
private readonly onError;
|
|
138
|
+
private readonly onResyncCallback;
|
|
139
|
+
/** Counter of nested apply calls; non-zero suppresses re-broadcast. */
|
|
140
|
+
private applying;
|
|
141
|
+
/** Cursor into ChangeLog — tracks "which deltas have been broadcast". */
|
|
142
|
+
private drainCursor;
|
|
143
|
+
/**
|
|
144
|
+
* Per-peer cursor of the highest delta.version we've applied from
|
|
145
|
+
* that peer. Prevents double-apply when a delta arrives both as a
|
|
146
|
+
* live broadcast AND in a sync-response that raced with our own
|
|
147
|
+
* sync-request.
|
|
148
|
+
*/
|
|
149
|
+
private appliedByPeer;
|
|
150
|
+
private unsubscribeLocal;
|
|
151
|
+
private unsubscribeResync;
|
|
152
|
+
private listener;
|
|
153
|
+
private stopped;
|
|
154
|
+
private started;
|
|
155
|
+
constructor(collection: Collection, options: BroadcastSyncOptions);
|
|
156
|
+
private requestSyncOnStart;
|
|
157
|
+
/** Begin listening for peer messages and broadcasting local changes. */
|
|
158
|
+
start(): void;
|
|
159
|
+
/** Stop listening and release resources. Idempotent. */
|
|
160
|
+
stop(): void;
|
|
161
|
+
/**
|
|
162
|
+
* Ask peers for any deltas newer than `sinceVersion`. Use this after
|
|
163
|
+
* a suspected desync (e.g., the app came back online) if you have
|
|
164
|
+
* not just started the adapter.
|
|
165
|
+
*/
|
|
166
|
+
requestSync(sinceVersion?: number): void;
|
|
167
|
+
private drainAndBroadcast;
|
|
168
|
+
private handleMessage;
|
|
169
|
+
private applyInbound;
|
|
170
|
+
private post;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export { type BroadcastMessage, BroadcastSync, type BroadcastSyncOptions };
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// src/sync/change-log.ts
|
|
2
|
+
var ChangeLog = class {
|
|
3
|
+
constructor(abColl, opts = {}) {
|
|
4
|
+
this.abColl = abColl;
|
|
5
|
+
this.bufferLimit = opts.bufferLimit ?? 65536;
|
|
6
|
+
this.unsubscribe = abColl.subscribe((event) => this.onChange(event));
|
|
7
|
+
}
|
|
8
|
+
abColl;
|
|
9
|
+
deltas = [];
|
|
10
|
+
bufferLimit;
|
|
11
|
+
resyncListeners = /* @__PURE__ */ new Set();
|
|
12
|
+
unsubscribe;
|
|
13
|
+
dispose() {
|
|
14
|
+
this.unsubscribe();
|
|
15
|
+
}
|
|
16
|
+
/** Returns deltas with `version > sinceVersion`, in order. */
|
|
17
|
+
drainSince(sinceVersion) {
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const d of this.deltas) {
|
|
20
|
+
if (d.version > sinceVersion) out.push(d);
|
|
21
|
+
}
|
|
22
|
+
const upTo = out.length > 0 ? out[out.length - 1].version : sinceVersion;
|
|
23
|
+
return { deltas: out, upTo };
|
|
24
|
+
}
|
|
25
|
+
/** Current number of retained deltas. */
|
|
26
|
+
get size() {
|
|
27
|
+
return this.deltas.length;
|
|
28
|
+
}
|
|
29
|
+
/** Register a resync listener (called on compact/rollback). */
|
|
30
|
+
onResync(listener) {
|
|
31
|
+
this.resyncListeners.add(listener);
|
|
32
|
+
return () => this.resyncListeners.delete(listener);
|
|
33
|
+
}
|
|
34
|
+
onChange(event) {
|
|
35
|
+
if (event.op === "compact" || event.op === "rollback") {
|
|
36
|
+
for (const l of this.resyncListeners) {
|
|
37
|
+
try {
|
|
38
|
+
l({ version: event.globalVersion });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error("[arrowbase] resync listener threw:", err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
this.deltas.length = 0;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (event.op === "delete") {
|
|
47
|
+
this.push({
|
|
48
|
+
op: "delete",
|
|
49
|
+
key: event.rowId,
|
|
50
|
+
version: event.globalVersion
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const row = this.abColl.get(event.rowId);
|
|
55
|
+
if (!row) return;
|
|
56
|
+
this.push({
|
|
57
|
+
op: event.op,
|
|
58
|
+
key: event.rowId,
|
|
59
|
+
value: row,
|
|
60
|
+
version: event.globalVersion
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
push(delta) {
|
|
64
|
+
this.deltas.push(delta);
|
|
65
|
+
if (this.deltas.length > this.bufferLimit) {
|
|
66
|
+
const overflow = this.deltas.length - this.bufferLimit;
|
|
67
|
+
this.deltas.splice(0, overflow);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/errors.ts
|
|
73
|
+
var ArrowBaseError = class extends Error {
|
|
74
|
+
constructor(message, code) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.code = code;
|
|
77
|
+
this.name = "ArrowBaseError";
|
|
78
|
+
}
|
|
79
|
+
code;
|
|
80
|
+
};
|
|
81
|
+
var NotFoundError = class extends ArrowBaseError {
|
|
82
|
+
constructor(message) {
|
|
83
|
+
super(message, "E_NOT_FOUND");
|
|
84
|
+
this.name = "NotFoundError";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var ValidationError = class extends ArrowBaseError {
|
|
88
|
+
constructor(message) {
|
|
89
|
+
super(message, "E_VALIDATION");
|
|
90
|
+
this.name = "ValidationError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/sync/memory.ts
|
|
95
|
+
function applyDeltas(abColl, deltas) {
|
|
96
|
+
let highest = -Infinity;
|
|
97
|
+
for (const d of deltas) {
|
|
98
|
+
if (d.op === "delete") {
|
|
99
|
+
try {
|
|
100
|
+
abColl.delete(d.key);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err instanceof NotFoundError) ; else {
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
if (!d.value) {
|
|
108
|
+
throw new ValidationError(`delta ${d.op} missing value`);
|
|
109
|
+
}
|
|
110
|
+
if (d.op === "insert") {
|
|
111
|
+
const existing = abColl.get(d.key);
|
|
112
|
+
if (existing) {
|
|
113
|
+
abColl.update(d.key, stripPrimaryKey(abColl, d.value));
|
|
114
|
+
} else {
|
|
115
|
+
abColl.insert(d.value);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
const existing = abColl.get(d.key);
|
|
119
|
+
if (!existing) {
|
|
120
|
+
abColl.insert(d.value);
|
|
121
|
+
} else {
|
|
122
|
+
abColl.update(d.key, stripPrimaryKey(abColl, d.value));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (d.version > highest) highest = d.version;
|
|
127
|
+
}
|
|
128
|
+
return highest;
|
|
129
|
+
}
|
|
130
|
+
function stripPrimaryKey(abColl, row) {
|
|
131
|
+
const pk = abColl.schema.primaryKey;
|
|
132
|
+
if (!(pk in row)) return row;
|
|
133
|
+
const out = {};
|
|
134
|
+
for (const [k, v] of Object.entries(row)) {
|
|
135
|
+
if (k === pk) continue;
|
|
136
|
+
out[k] = v;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/broadcast.ts
|
|
142
|
+
var BroadcastSync = class {
|
|
143
|
+
constructor(collection, options) {
|
|
144
|
+
this.collection = collection;
|
|
145
|
+
this.nodeId = options.nodeId ?? generateNodeId();
|
|
146
|
+
this.onError = options.onError ?? ((err) => {
|
|
147
|
+
console.error("[arrowbase/broadcast]", err);
|
|
148
|
+
});
|
|
149
|
+
this.onResyncCallback = options.onResync;
|
|
150
|
+
const Ctor = options.BroadcastChannel ?? globalThis.BroadcastChannel;
|
|
151
|
+
if (!Ctor) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
"BroadcastSync: no BroadcastChannel available. Pass options.BroadcastChannel or run in a browser / Node >=15."
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
this.channel = new Ctor(options.channelName);
|
|
157
|
+
this.changeLog = new ChangeLog(collection, {
|
|
158
|
+
...options.bufferLimit !== void 0 ? { bufferLimit: options.bufferLimit } : {}
|
|
159
|
+
});
|
|
160
|
+
this.requestSyncOnStart = options.requestSyncOnStart ?? true;
|
|
161
|
+
}
|
|
162
|
+
collection;
|
|
163
|
+
nodeId;
|
|
164
|
+
channel;
|
|
165
|
+
changeLog;
|
|
166
|
+
onError;
|
|
167
|
+
onResyncCallback;
|
|
168
|
+
/** Counter of nested apply calls; non-zero suppresses re-broadcast. */
|
|
169
|
+
applying = 0;
|
|
170
|
+
/** Cursor into ChangeLog — tracks "which deltas have been broadcast". */
|
|
171
|
+
drainCursor = 0;
|
|
172
|
+
/**
|
|
173
|
+
* Per-peer cursor of the highest delta.version we've applied from
|
|
174
|
+
* that peer. Prevents double-apply when a delta arrives both as a
|
|
175
|
+
* live broadcast AND in a sync-response that raced with our own
|
|
176
|
+
* sync-request.
|
|
177
|
+
*/
|
|
178
|
+
appliedByPeer = /* @__PURE__ */ new Map();
|
|
179
|
+
unsubscribeLocal = null;
|
|
180
|
+
unsubscribeResync = null;
|
|
181
|
+
listener = null;
|
|
182
|
+
stopped = false;
|
|
183
|
+
started = false;
|
|
184
|
+
requestSyncOnStart;
|
|
185
|
+
/** Begin listening for peer messages and broadcasting local changes. */
|
|
186
|
+
start() {
|
|
187
|
+
if (this.stopped) {
|
|
188
|
+
throw new Error("BroadcastSync: cannot start(); already stopped");
|
|
189
|
+
}
|
|
190
|
+
if (this.started) return;
|
|
191
|
+
this.started = true;
|
|
192
|
+
this.listener = (ev) => {
|
|
193
|
+
try {
|
|
194
|
+
this.handleMessage(ev.data);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this.onError(err);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
this.channel.addEventListener("message", this.listener);
|
|
200
|
+
this.unsubscribeLocal = this.collection.subscribe(() => {
|
|
201
|
+
if (this.applying > 0) return;
|
|
202
|
+
this.drainAndBroadcast();
|
|
203
|
+
});
|
|
204
|
+
this.unsubscribeResync = this.changeLog.onResync(({ version }) => {
|
|
205
|
+
if (this.applying > 0) return;
|
|
206
|
+
this.post({ type: "resync", nodeId: this.nodeId, version });
|
|
207
|
+
});
|
|
208
|
+
if (this.requestSyncOnStart) {
|
|
209
|
+
this.post({
|
|
210
|
+
type: "sync-request",
|
|
211
|
+
nodeId: this.nodeId,
|
|
212
|
+
sinceVersion: this.collection.globalVersion
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Stop listening and release resources. Idempotent. */
|
|
217
|
+
stop() {
|
|
218
|
+
if (this.stopped) return;
|
|
219
|
+
this.stopped = true;
|
|
220
|
+
if (this.listener) {
|
|
221
|
+
this.channel.removeEventListener("message", this.listener);
|
|
222
|
+
this.listener = null;
|
|
223
|
+
}
|
|
224
|
+
this.unsubscribeLocal?.();
|
|
225
|
+
this.unsubscribeLocal = null;
|
|
226
|
+
this.unsubscribeResync?.();
|
|
227
|
+
this.unsubscribeResync = null;
|
|
228
|
+
this.changeLog.dispose();
|
|
229
|
+
this.channel.close();
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Ask peers for any deltas newer than `sinceVersion`. Use this after
|
|
233
|
+
* a suspected desync (e.g., the app came back online) if you have
|
|
234
|
+
* not just started the adapter.
|
|
235
|
+
*/
|
|
236
|
+
requestSync(sinceVersion = this.collection.globalVersion) {
|
|
237
|
+
this.post({
|
|
238
|
+
type: "sync-request",
|
|
239
|
+
nodeId: this.nodeId,
|
|
240
|
+
sinceVersion
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// -------------------------------------------------------------------
|
|
244
|
+
// Internals
|
|
245
|
+
// -------------------------------------------------------------------
|
|
246
|
+
drainAndBroadcast() {
|
|
247
|
+
const { deltas, upTo } = this.changeLog.drainSince(this.drainCursor);
|
|
248
|
+
if (deltas.length === 0) return;
|
|
249
|
+
this.drainCursor = upTo;
|
|
250
|
+
this.post({ type: "delta", nodeId: this.nodeId, deltas });
|
|
251
|
+
}
|
|
252
|
+
handleMessage(msg) {
|
|
253
|
+
if (!msg || typeof msg !== "object" || !("type" in msg)) return;
|
|
254
|
+
if ("nodeId" in msg && msg.nodeId === this.nodeId) return;
|
|
255
|
+
switch (msg.type) {
|
|
256
|
+
case "delta":
|
|
257
|
+
this.applyInbound(msg.deltas, msg.nodeId);
|
|
258
|
+
return;
|
|
259
|
+
case "sync-request": {
|
|
260
|
+
const { deltas, upTo } = this.changeLog.drainSince(msg.sinceVersion);
|
|
261
|
+
if (deltas.length === 0) return;
|
|
262
|
+
this.post({
|
|
263
|
+
type: "sync-response",
|
|
264
|
+
nodeId: this.nodeId,
|
|
265
|
+
toNodeId: msg.nodeId,
|
|
266
|
+
deltas,
|
|
267
|
+
upTo
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
case "sync-response":
|
|
272
|
+
if (msg.toNodeId !== this.nodeId) return;
|
|
273
|
+
this.applyInbound(msg.deltas, msg.nodeId);
|
|
274
|
+
return;
|
|
275
|
+
case "resync":
|
|
276
|
+
this.onResyncCallback?.({
|
|
277
|
+
fromNodeId: msg.nodeId,
|
|
278
|
+
version: msg.version
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
applyInbound(deltas, fromNodeId) {
|
|
284
|
+
if (deltas.length === 0) return;
|
|
285
|
+
const lastApplied = this.appliedByPeer.get(fromNodeId) ?? -Infinity;
|
|
286
|
+
const fresh = [];
|
|
287
|
+
let highestSeen = lastApplied;
|
|
288
|
+
for (const d of deltas) {
|
|
289
|
+
if (d.version <= lastApplied) continue;
|
|
290
|
+
fresh.push(d);
|
|
291
|
+
if (d.version > highestSeen) highestSeen = d.version;
|
|
292
|
+
}
|
|
293
|
+
if (fresh.length === 0) return;
|
|
294
|
+
this.applying++;
|
|
295
|
+
try {
|
|
296
|
+
applyDeltas(this.collection, fresh);
|
|
297
|
+
} finally {
|
|
298
|
+
this.applying--;
|
|
299
|
+
}
|
|
300
|
+
this.appliedByPeer.set(fromNodeId, highestSeen);
|
|
301
|
+
this.drainCursor = this.collection.globalVersion;
|
|
302
|
+
}
|
|
303
|
+
post(msg) {
|
|
304
|
+
try {
|
|
305
|
+
this.channel.postMessage(msg);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
this.onError(err);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
function generateNodeId() {
|
|
312
|
+
const g = globalThis;
|
|
313
|
+
if (g.crypto?.randomUUID) return g.crypto.randomUUID();
|
|
314
|
+
return "node-" + Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 10);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export { BroadcastSync };
|
|
318
|
+
//# sourceMappingURL=broadcast.js.map
|
|
319
|
+
//# sourceMappingURL=broadcast.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sync/change-log.ts","../src/errors.ts","../src/sync/memory.ts","../src/broadcast.ts"],"names":[],"mappings":"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","file":"broadcast.js","sourcesContent":["import type { Collection } from '../collection/collection.js';\nimport type { ChangeEvent } from '../collection/events.js';\nimport type { Delta } from './types.js';\n\n/**\n * Records outgoing deltas from a Collection so a SyncAdapter can push\n * them to a remote. Attaches to the collection's change feed on\n * construction; call `dispose()` when done.\n *\n * `bufferLimit` caps the number of retained deltas — when exceeded the\n * oldest entries are discarded. Consumers that may fall arbitrarily\n * behind must call `drainSince(version)` fast enough, or they will see\n * their cursor predate the log's oldest entry (surfaced as an empty\n * return; a persistence-backed adapter should then resync via\n * snapshot).\n *\n * `compact` and `rollback` events cannot be represented as incremental\n * deltas. Callers receive a `{ type: 'resync', version }` sentinel via\n * `onResync(listener)`, which prompts them to snapshot-sync instead of\n * stream deltas.\n */\nexport interface ChangeLogOptions {\n /** Max deltas retained. Default 65536. */\n bufferLimit?: number;\n}\n\nexport type ResyncListener = (event: { version: number }) => void;\n\nexport class ChangeLog {\n private readonly deltas: Delta[] = [];\n private readonly bufferLimit: number;\n private resyncListeners: Set<ResyncListener> = new Set();\n private readonly unsubscribe: () => void;\n\n constructor(private readonly abColl: Collection, opts: ChangeLogOptions = {}) {\n this.bufferLimit = opts.bufferLimit ?? 65536;\n this.unsubscribe = abColl.subscribe((event) => this.onChange(event));\n }\n\n dispose(): void {\n this.unsubscribe();\n }\n\n /** Returns deltas with `version > sinceVersion`, in order. */\n drainSince(sinceVersion: number): { deltas: Delta[]; upTo: number } {\n const out: Delta[] = [];\n for (const d of this.deltas) {\n if (d.version > sinceVersion) out.push(d);\n }\n const upTo =\n out.length > 0\n ? (out[out.length - 1] as Delta).version\n : sinceVersion;\n return { deltas: out, upTo };\n }\n\n /** Current number of retained deltas. */\n get size(): number {\n return this.deltas.length;\n }\n\n /** Register a resync listener (called on compact/rollback). */\n onResync(listener: ResyncListener): () => void {\n this.resyncListeners.add(listener);\n return () => this.resyncListeners.delete(listener);\n }\n\n private onChange(event: ChangeEvent): void {\n if (event.op === 'compact' || event.op === 'rollback') {\n for (const l of this.resyncListeners) {\n try {\n l({ version: event.globalVersion });\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[arrowbase] resync listener threw:', err);\n }\n }\n // Also clear the log — prior deltas are no longer coherent with\n // the post-compact live set.\n this.deltas.length = 0;\n return;\n }\n\n if (event.op === 'delete') {\n this.push({\n op: 'delete',\n key: event.rowId,\n version: event.globalVersion,\n });\n return;\n }\n\n // insert / update: capture the post-image row snapshot.\n const row = this.abColl.get(event.rowId);\n if (!row) return;\n this.push({\n op: event.op,\n key: event.rowId,\n value: row,\n version: event.globalVersion,\n });\n }\n\n private push(delta: Delta): void {\n this.deltas.push(delta);\n if (this.deltas.length > this.bufferLimit) {\n // Drop oldest. Excess is unbounded in theory, but we only get\n // here if a sync consumer fell arbitrarily behind; the caller\n // should pick up the slack via snapshot.\n const overflow = this.deltas.length - this.bufferLimit;\n this.deltas.splice(0, overflow);\n }\n }\n}\n","/** Error types thrown by ArrowBase and TanStack-compatible surfaces. */\nexport class ArrowBaseError extends Error {\n constructor(message: string, public readonly code: string) {\n super(message);\n this.name = 'ArrowBaseError';\n }\n}\n\nexport class TanStackDBError extends ArrowBaseError {\n constructor(message: string, code = 'E_TANSTACK_DB') {\n super(message, code);\n this.name = 'TanStackDBError';\n }\n}\n\nexport class NonRetriableError extends TanStackDBError {\n constructor(message: string) {\n super(message);\n this.name = 'NonRetriableError';\n }\n}\n\ninterface SchemaIssue {\n message: string;\n path: unknown;\n}\n\nexport class SchemaValidationError extends TanStackDBError {\n readonly type: 'insert' | 'update';\n readonly issues: Array<SchemaIssue>;\n\n constructor(type: 'insert' | 'update', issues: Array<SchemaIssue>, message?: string) {\n const defaultMessage = `${type === 'insert' ? 'Insert' : 'Update'} validation failed: ${issues\n .map((issue) => `\\n- ${issue.message} - path: ${issue.path}`)\n .join('')}`;\n super(message || defaultMessage);\n this.name = 'SchemaValidationError';\n this.type = type;\n this.issues = issues;\n }\n}\n\nexport class DuplicateDbInstanceError extends TanStackDBError {\n constructor() {\n super(`Multiple instances of @tanstack/db detected!\n\nThis causes transaction context to be lost because each instance maintains its own transaction stack.\n\nCommon causes:\n1. Different versions of @tanstack/db installed\n2. Incompatible peer dependency versions in packages\n3. Module resolution issues in bundler configuration\n\nTo fix:\n1. Check installed versions: npm list @tanstack/db (or pnpm/yarn list)\n2. Force a single version using package manager overrides:\n - npm: \"overrides\" in package.json\n - pnpm: \"pnpm.overrides\" in package.json\n - yarn: \"resolutions\" in package.json\n3. Clear node_modules and lockfile, then reinstall\n\nTo temporarily disable this check (not recommended):\nSet environment variable: TANSTACK_DB_DISABLE_DUP_CHECK=1\n\nSee: https://tanstack.com/db/latest/docs/troubleshooting#duplicate-instances`);\n this.name = 'DuplicateDbInstanceError';\n }\n}\n\nexport class SchemaError extends ArrowBaseError {\n constructor(message: string) {\n super(message, 'E_SCHEMA');\n this.name = 'SchemaError';\n }\n}\n\nexport class AllocatorError extends ArrowBaseError {\n constructor(message: string) {\n super(message, 'E_ALLOC');\n this.name = 'AllocatorError';\n }\n}\n\nexport class OutOfSpaceError extends AllocatorError {\n constructor(message = 'segment out of space') {\n super(message);\n this.name = 'OutOfSpaceError';\n }\n}\n\nexport class NotFoundError extends ArrowBaseError {\n constructor(message: string) {\n super(message, 'E_NOT_FOUND');\n this.name = 'NotFoundError';\n }\n}\n\nexport class UndoError extends ArrowBaseError {\n constructor(message: string) {\n super(message, 'E_UNDO');\n this.name = 'UndoError';\n }\n}\n\nexport class ValidationError extends ArrowBaseError {\n constructor(message: string) {\n super(message, 'E_VALIDATION');\n this.name = 'ValidationError';\n }\n}\n\nexport class PersistenceError extends ArrowBaseError {\n constructor(message: string) {\n super(message, 'E_PERSIST');\n this.name = 'PersistenceError';\n }\n}\n\nexport class CollectionConfigurationError extends TanStackDBError {\n constructor(message: string) {\n super(message, 'E_COLLECTION_CONFIG');\n this.name = 'CollectionConfigurationError';\n }\n}\nexport class CollectionRequiresConfigError extends CollectionConfigurationError { constructor() { super('Collection requires a config'); } }\nexport class CollectionRequiresSyncConfigError extends CollectionConfigurationError { constructor() { super('Collection requires a sync config'); } }\nexport class InvalidSchemaError extends CollectionConfigurationError { constructor() { super('Schema must implement the standard-schema interface'); } }\nexport class SchemaMustBeSynchronousError extends CollectionConfigurationError { constructor() { super('Schema validation must be synchronous'); } }\n\nexport class CollectionStateError extends TanStackDBError {\n constructor(message: string) {\n super(message, 'E_COLLECTION_STATE');\n this.name = 'CollectionStateError';\n }\n}\nexport class CollectionInErrorStateError extends CollectionStateError {\n constructor(operation: string, collectionId: string) {\n super(`Cannot perform ${operation} on collection \"${collectionId}\" - collection is in error state. Try calling cleanup() and restarting the collection.`);\n }\n}\nexport class InvalidCollectionStatusTransitionError extends CollectionStateError {\n constructor(from: string, to: string, collectionId: string) {\n super(`Invalid collection status transition from \"${from}\" to \"${to}\" for collection \"${collectionId}\"`);\n }\n}\nexport class CollectionIsInErrorStateError extends CollectionStateError { constructor() { super('Collection is in error state'); } }\nexport class NegativeActiveSubscribersError extends CollectionStateError { constructor() { super('Active subscribers count is negative - this should never happen'); } }\n\nexport class CollectionOperationError extends TanStackDBError {\n constructor(message: string) {\n super(message, 'E_COLLECTION_OPERATION');\n this.name = 'CollectionOperationError';\n }\n}\nexport class UndefinedKeyError extends CollectionOperationError { constructor(item: unknown) { super(`An object was created without a defined key: ${JSON.stringify(item)}`); } }\nexport class InvalidKeyError extends CollectionOperationError {\n constructor(key: unknown, item: unknown) {\n const keyType = key === null ? 'null' : typeof key;\n super(`getKey returned an invalid key type. Expected string or number, but got ${keyType}: ${JSON.stringify(key)}. Item: ${JSON.stringify(item)}`);\n }\n}\nexport class DuplicateKeyError extends CollectionOperationError { constructor(key: string | number) { super(`Cannot insert document with ID \"${key}\" because it already exists in the collection`); } }\nexport class DuplicateKeySyncError extends CollectionOperationError {\n constructor(key: string | number, collectionId: string, options?: { hasCustomGetKey?: boolean; hasDistinct?: boolean; hasJoins?: boolean }) {\n const baseMessage = `Cannot insert document with key \"${key}\" from sync because it already exists in the collection \"${collectionId}\"`;\n if (options?.hasCustomGetKey && options.hasDistinct) {\n super(`${baseMessage}. This collection uses a custom getKey with .distinct(). The .distinct() operator deduplicates by the ENTIRE selected object (standard SQL behavior), but your custom getKey extracts only a subset of fields. This causes multiple distinct rows (with different values in non-key fields) to receive the same key. To fix this, either: (1) ensure your SELECT only includes fields that uniquely identify each row, (2) use .groupBy() with min()/max() aggregates to select one value per group, or (3) remove the custom getKey to use the default key behavior.`);\n } else if (options?.hasCustomGetKey && options.hasJoins) {\n super(`${baseMessage}. This collection uses a custom getKey with joined queries. Joined queries can produce multiple rows with the same key when relationships are not 1:1. Consider: (1) using a composite key in your getKey function (e.g., \\`\\${item.key1}-\\${item.key2}\\`), (2) ensuring your join produces unique rows per key, or (3) removing the custom getKey to use the default composite key behavior.`);\n } else {\n super(baseMessage);\n }\n }\n}\nexport class MissingUpdateArgumentError extends CollectionOperationError { constructor() { super('The first argument to update is missing'); } }\nexport class NoKeysPassedToUpdateError extends CollectionOperationError { constructor() { super('No keys were passed to update'); } }\nexport class UpdateKeyNotFoundError extends CollectionOperationError { constructor(key: string | number) { super(`The key \"${key}\" was passed to update but an object for this key was not found in the collection`); } }\nexport class KeyUpdateNotAllowedError extends CollectionOperationError { constructor(originalKey: string | number, newKey: string | number) { super(`Updating the key of an item is not allowed. Original key: \"${originalKey}\", Attempted new key: \"${newKey}\". Please delete the old item and create a new one if a key change is necessary.`); } }\nexport class NoKeysPassedToDeleteError extends CollectionOperationError { constructor() { super('No keys were passed to delete'); } }\nexport class DeleteKeyNotFoundError extends CollectionOperationError { constructor(key: string | number) { super(`Collection.delete was called with key '${key}' but there is no item in the collection with this key`); } }\n\nexport class MissingHandlerError extends TanStackDBError { constructor(message: string) { super(message, 'E_MISSING_HANDLER'); this.name = 'MissingHandlerError'; } }\nexport class MissingInsertHandlerError extends MissingHandlerError { constructor() { super(\"Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configured.\"); } }\nexport class MissingUpdateHandlerError extends MissingHandlerError { constructor() { super(\"Collection.update called directly (not within an explicit transaction) but no 'onUpdate' handler is configured.\"); } }\nexport class MissingDeleteHandlerError extends MissingHandlerError { constructor() { super(\"Collection.delete called directly (not within an explicit transaction) but no 'onDelete' handler is configured.\"); } }\n\nexport class TransactionError extends TanStackDBError { constructor(message: string) { super(message, 'E_TRANSACTION'); this.name = 'TransactionError'; } }\nexport class MissingMutationFunctionError extends TransactionError { constructor() { super('mutationFn is required when creating a transaction'); } }\nexport class OnMutateMustBeSynchronousError extends TransactionError { constructor() { super('onMutate must be synchronous and cannot return a promise. Remove async/await or returned promises from onMutate.'); } }\nexport class TransactionNotPendingMutateError extends TransactionError { constructor() { super('You can no longer call .mutate() as the transaction is no longer pending'); } }\nexport class TransactionAlreadyCompletedRollbackError extends TransactionError { constructor() { super('You can no longer call .rollback() as the transaction is already completed'); } }\nexport class TransactionNotPendingCommitError extends TransactionError { constructor() { super('You can no longer call .commit() as the transaction is no longer pending'); } }\nexport class NoPendingSyncTransactionWriteError extends TransactionError { constructor() { super('No pending sync transaction to write to'); } }\nexport class SyncTransactionAlreadyCommittedWriteError extends TransactionError { constructor() { super(\"The pending sync transaction is already committed, you can't still write to it.\"); } }\nexport class NoPendingSyncTransactionCommitError extends TransactionError { constructor() { super('No pending sync transaction to commit'); } }\nexport class SyncTransactionAlreadyCommittedError extends TransactionError { constructor() { super(\"The pending sync transaction is already committed, you can't commit it again.\"); } }\n\nexport class QueryBuilderError extends TanStackDBError { constructor(message: string) { super(message, 'E_QUERY_BUILDER'); this.name = 'QueryBuilderError'; } }\nexport class OnlyOneSourceAllowedError extends QueryBuilderError { constructor(context: string) { super(`Only one source is allowed in the ${context}`); } }\nexport class SubQueryMustHaveFromClauseError extends QueryBuilderError { constructor(context: string) { super(`A sub query passed to a ${context} must have a from clause itself`); } }\nexport class InvalidSourceError extends QueryBuilderError { constructor(alias: string) { super(`Invalid source for live query: The value provided for alias \"${alias}\" is not a Collection or subquery. Live queries only accept Collection instances or subqueries. Please ensure you're passing a valid Collection or QueryBuilder, not a plain array or other data type.`); } }\nexport class InvalidSourceTypeError extends QueryBuilderError { constructor(context: string, type: string) { super(`Invalid source for ${context}: Expected an object with a single key-value pair like { alias: collection }. For example: .from({ todos: todosCollection }). Got: ${type}`); } }\nexport class JoinConditionMustBeEqualityError extends QueryBuilderError { constructor() { super('Join condition must be an equality expression'); } }\nexport class QueryMustHaveFromClauseError extends QueryBuilderError { constructor() { super('Query must have a from clause'); } }\nexport class InvalidWhereExpressionError extends QueryBuilderError {\n constructor(valueType: string) {\n super(`Invalid where() expression: Expected a query expression, but received a ${valueType}. This usually happens when using JavaScript's comparison operators (===, !==, <, >, etc.) directly. Instead, use the query builder functions:\\n\\n ❌ .where(({ user }) => user.id === 'abc')\\n ✅ .where(({ user }) => eq(user.id, 'abc'))\\n\\nAvailable comparison functions: eq, gt, gte, lt, lte, and, or, not, like, ilike, isNull, isUndefined`);\n }\n}\n\nexport class QueryCompilationError extends TanStackDBError { constructor(message: string) { super(message, 'E_QUERY_COMPILATION'); this.name = 'QueryCompilationError'; } }\nexport class UnsafeAliasPathError extends QueryCompilationError {\n constructor(segment: string) {\n super(\n `Unsafe alias path segment \"${segment}\" is not allowed in .select(). ` +\n `Aliases must not contain \"__proto__\", \"prototype\", or \"constructor\".`,\n );\n this.name = 'UnsafeAliasPathError';\n }\n}\nexport class DistinctRequiresSelectError extends QueryCompilationError { constructor() { super('DISTINCT requires a SELECT clause.'); } }\nexport class FnSelectWithGroupByError extends QueryCompilationError { constructor() { super('fn.select() cannot be used with groupBy(). groupBy requires the compiler to statically analyze aggregate functions (count, sum, max, etc.) in the SELECT clause, which is not possible with fn.select() since it is an opaque function. Use .select() instead of .fn.select() when combining with groupBy().'); } }\nexport class UnsupportedRootScalarSelectError extends QueryCompilationError { constructor() { super('Top-level scalar select() is not supported by createLiveQueryCollection() or queryOnce(). Return an object from .select(), or use the scalar query inside toArray(...) or concat(toArray(...)).'); } }\nexport class HavingRequiresGroupByError extends QueryCompilationError { constructor() { super('HAVING clause requires GROUP BY clause'); } }\nexport class LimitOffsetRequireOrderByError extends QueryCompilationError { constructor() { super('LIMIT and OFFSET require an ORDER BY clause to ensure deterministic results'); } }\nexport class CollectionInputNotFoundError extends QueryCompilationError { constructor(alias: string, collectionId?: string, availableKeys?: Array<string>) { const details = collectionId ? `alias \"${alias}\" (collection \"${collectionId}\")` : `collection \"${alias}\"`; const availableKeysMsg = availableKeys?.length ? `. Available keys: ${availableKeys.join(', ')}` : ''; super(`Input for ${details} not found in inputs map${availableKeysMsg}`); } }\nexport class DuplicateAliasInSubqueryError extends QueryCompilationError { constructor(alias: string, parentAliases: Array<string>) { super(`Subquery uses alias \"${alias}\" which is already used in the parent query. Each alias must be unique across parent and subquery contexts. Parent query aliases: ${parentAliases.join(', ')}. Please rename \"${alias}\" in either the parent query or subquery to avoid conflicts.`); } }\nexport class UnsupportedFromTypeError extends QueryCompilationError { constructor(type: string) { super(`Unsupported FROM type: ${type}`); } }\nexport class UnknownExpressionTypeError extends QueryCompilationError { constructor(type: string) { super(`Unknown expression type: ${type}`); } }\nexport class EmptyReferencePathError extends QueryCompilationError { constructor() { super('Reference path cannot be empty'); } }\nexport class UnknownFunctionError extends QueryCompilationError { constructor(functionName: string) { super(`Unknown function: ${functionName}`); } }\nexport class JoinCollectionNotFoundError extends QueryCompilationError { constructor(collectionId: string) { super(`Collection \"${collectionId}\" not found during compilation of join`); } }\n\nexport class JoinError extends TanStackDBError { constructor(message: string) { super(message, 'E_JOIN'); this.name = 'JoinError'; } }\nexport class UnsupportedJoinTypeError extends JoinError { constructor(joinType: string) { super(`Unsupported join type: ${joinType}`); } }\nexport class InvalidJoinConditionSameSourceError extends JoinError { constructor(sourceAlias: string) { super(`Invalid join condition: both expressions refer to the same source \"${sourceAlias}\"`); } }\nexport class InvalidJoinConditionSourceMismatchError extends JoinError { constructor() { super('Invalid join condition: expressions must reference source aliases'); } }\nexport class InvalidJoinConditionLeftSourceError extends JoinError { constructor(sourceAlias: string) { super(`Invalid join condition: left expression refers to an unavailable source \"${sourceAlias}\"`); } }\nexport class InvalidJoinConditionRightSourceError extends JoinError { constructor(sourceAlias: string) { super(`Invalid join condition: right expression does not refer to the joined source \"${sourceAlias}\"`); } }\nexport class InvalidJoinCondition extends JoinError { constructor() { super('Invalid join condition'); } }\nexport class UnsupportedJoinSourceTypeError extends JoinError { constructor(type: string) { super(`Unsupported join source type: ${type}`); } }\n\nexport class GroupByError extends TanStackDBError { constructor(message: string) { super(message, 'E_GROUP_BY'); this.name = 'GroupByError'; } }\nexport class NonAggregateExpressionNotInGroupByError extends GroupByError { constructor(alias: string) { super(`Non-aggregate expression '${alias}' in SELECT must also appear in GROUP BY clause`); } }\nexport class UnsupportedAggregateFunctionError extends GroupByError { constructor(functionName: string) { super(`Unsupported aggregate function: ${functionName}`); } }\nexport class AggregateFunctionNotInSelectError extends GroupByError { constructor(functionName: string) { super(`Aggregate function in HAVING clause must also be in SELECT clause: ${functionName}`); } }\nexport class UnknownHavingExpressionTypeError extends GroupByError { constructor(type: string) { super(`Unknown expression type in HAVING clause: ${type}`); } }\n\nexport class StorageError extends TanStackDBError { constructor(message: string) { super(message, 'E_STORAGE'); this.name = 'StorageError'; } }\nexport class SerializationError extends StorageError { constructor(operation: string, originalError: unknown) { super(`Cannot ${operation} item because it cannot be JSON serialized: ${originalError}`); } }\nexport class LocalStorageCollectionError extends StorageError { constructor(message: string) { super(message); this.name = 'LocalStorageCollectionError'; } }\nexport class StorageKeyRequiredError extends LocalStorageCollectionError { constructor() { super('[LocalStorageCollection] storageKey must be provided.'); } }\nexport class InvalidStorageDataFormatError extends LocalStorageCollectionError { constructor(storageKey: string, key: string) { super(`[LocalStorageCollection] Invalid data format in storage key \"${storageKey}\" for key \"${key}\".`); } }\nexport class InvalidStorageObjectFormatError extends LocalStorageCollectionError { constructor(storageKey: string) { super(`[LocalStorageCollection] Invalid data format in storage key \"${storageKey}\". Expected object format.`); } }\n\nexport class SyncCleanupError extends TanStackDBError { constructor(collectionId: string, error: unknown) { const message = error instanceof Error ? error.message : String(error); super(`Collection \"${collectionId}\" sync cleanup function threw an error: ${message}`); this.name = 'SyncCleanupError'; } }\nexport class QueryOptimizerError extends TanStackDBError { constructor(message: string) { super(message, 'E_QUERY_OPTIMIZER'); this.name = 'QueryOptimizerError'; } }\nexport class CannotCombineEmptyExpressionListError extends QueryOptimizerError { constructor() { super('Cannot combine empty expression list'); } }\nexport class WhereClauseConversionError extends QueryOptimizerError { constructor(collectionId: string, alias: string) { super(`Failed to convert WHERE clause to collection filter for collection '${collectionId}' alias '${alias}'. This indicates a bug in the query optimization logic.`); } }\nexport class SubscriptionNotFoundError extends QueryCompilationError { constructor(resolvedAlias: string, originalAlias: string, collectionId: string, availableAliases: Array<string>) { super(`Internal error: subscription for alias '${resolvedAlias}' (remapped from '${originalAlias}', collection '${collectionId}') is missing in join pipeline. Available aliases: ${availableAliases.join(', ')}. This indicates a bug in alias tracking.`); } }\nexport class AggregateNotSupportedError extends QueryCompilationError { constructor() { super('Aggregate expressions are not supported in this context. Use GROUP BY clause for aggregates.'); } }\nexport class MissingAliasInputsError extends QueryCompilationError { constructor(missingAliases: Array<string>) { super(`Internal error: compiler returned aliases without inputs: ${missingAliases.join(', ')}. This indicates a bug in query compilation. Please report this issue.`); } }\nexport class SetWindowRequiresOrderByError extends QueryCompilationError { constructor() { super('setWindow() can only be called on collections with an ORDER BY clause. Add .orderBy() to your query to enable window movement.'); } }\n","import { NotFoundError, ValidationError } from '../errors.js';\nimport type { Collection, RowValue } from '../collection/collection.js';\nimport type { Delta, SyncAdapter } from './types.js';\nimport { ChangeLog } from './change-log.js';\n\n/**\n * Apply an ordered list of deltas to a Collection, preserving order.\n *\n * Semantics:\n * - insert → Collection.insert(delta.value). Duplicate key throws;\n * caller may want to retry via update. v1 is strict.\n * - update → Collection.update(delta.key, delta.value).\n * - delete → Collection.delete(delta.key). Missing key is ignored.\n *\n * Inserts with a key that already exists (because the local side wrote\n * to the same key after the delta was emitted) are promoted to updates\n * by default (TanStack DB does the same thing in its sync write path).\n *\n * Returns the highest version applied (or -Infinity if the list was empty).\n */\nexport function applyDeltas(\n abColl: Collection,\n deltas: readonly Delta[],\n): number {\n let highest = -Infinity;\n for (const d of deltas) {\n if (d.op === 'delete') {\n try {\n abColl.delete(d.key);\n } catch (err) {\n if (err instanceof NotFoundError) {\n // silently skip\n } else {\n throw err;\n }\n }\n } else {\n if (!d.value) {\n throw new ValidationError(`delta ${d.op} missing value`);\n }\n if (d.op === 'insert') {\n const existing = abColl.get(d.key);\n if (existing) {\n abColl.update(d.key, stripPrimaryKey(abColl, d.value as RowValue));\n } else {\n abColl.insert(d.value as RowValue);\n }\n } else {\n // update\n const existing = abColl.get(d.key);\n if (!existing) {\n abColl.insert(d.value as RowValue); // upsert: treat as insert\n } else {\n abColl.update(d.key, stripPrimaryKey(abColl, d.value as RowValue));\n }\n }\n }\n if (d.version > highest) highest = d.version;\n }\n return highest;\n}\n\n/**\n * In-memory sync adapter suitable for tests and same-process mirroring.\n *\n * Wraps two (or more) sides of a sync pair via a shared in-memory log.\n * Each side gets its own `MemorySyncAdapter` instance that reads/writes\n * the same log. `pull(since)` returns deltas newer than the cursor;\n * `push(deltas)` appends to the log; `apply(deltas)` writes through to\n * the local Collection.\n *\n * This is *not* a replica-set: there is no ordering guarantee beyond\n * what callers themselves impose via `push` order. Last writer wins.\n */\nexport class MemorySyncAdapter implements SyncAdapter {\n constructor(\n private readonly local: Collection,\n private readonly log: SharedDeltaLog,\n /** Optional outbound change log; if provided, local mutations\n * auto-push into the shared log. */\n private readonly outbound?: ChangeLog,\n ) {\n if (outbound) {\n // Forward every new delta into the shared log so the other\n // side(s) can pull it.\n let cursor = 0;\n const step = (): void => {\n const { deltas, upTo } = outbound.drainSince(cursor);\n if (deltas.length > 0) {\n this.log.push(deltas);\n cursor = upTo;\n }\n };\n // Step once immediately to flush any backlog, then hook every\n // local change via a dedicated listener.\n step();\n const unsub = local.subscribe(() => step());\n // Attach a dispose hook on adapter instance.\n this.disposeLocalListener = () => unsub();\n }\n }\n\n private disposeLocalListener: (() => void) | null = null;\n\n dispose(): void {\n this.disposeLocalListener?.();\n }\n\n async apply(deltas: readonly Delta[]): Promise<void> {\n applyDeltas(this.local, deltas);\n }\n\n async pull(sinceVersion: number): Promise<{ deltas: Delta[]; upTo: number }> {\n return this.log.drainSince(sinceVersion);\n }\n\n async push(deltas: readonly Delta[]): Promise<void> {\n this.log.push(deltas);\n }\n}\n\n/**\n * Return a copy of `row` with the collection's primary-key column\n * removed. Needed before handing the object to `Collection.update`,\n * which rejects PK mutations.\n */\nfunction stripPrimaryKey(abColl: Collection, row: RowValue): Partial<RowValue> {\n const pk = abColl.schema.primaryKey;\n if (!(pk in row)) return row;\n const out: RowValue = {};\n for (const [k, v] of Object.entries(row)) {\n if (k === pk) continue;\n out[k] = v;\n }\n return out;\n}\n\n/**\n * Shared log of deltas for `MemorySyncAdapter`. Ordering is preserved\n * by append order. No compaction — the log grows unboundedly. Tests\n * only.\n */\nexport class SharedDeltaLog {\n private readonly entries: Delta[] = [];\n\n push(deltas: readonly Delta[]): void {\n for (const d of deltas) this.entries.push(d);\n }\n\n drainSince(sinceVersion: number): { deltas: Delta[]; upTo: number } {\n const out: Delta[] = [];\n for (const d of this.entries) {\n if (d.version > sinceVersion) out.push(d);\n }\n const upTo =\n out.length > 0\n ? (out[out.length - 1] as Delta).version\n : sinceVersion;\n return { deltas: out, upTo };\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n","/**\n * Cross-tab sync via `BroadcastChannel`.\n *\n * Optional subpath (`arrowbase/broadcast`). Each participating document\n * (tab, iframe, shared-worker) opens a `BroadcastSync` on the same\n * channel name and shares deltas with the other participants. Reuses\n * {@link ChangeLog} (outbound buffering) and {@link applyDeltas}\n * (inbound) so the wire protocol is just the transport.\n *\n * ### Usage\n *\n * ```ts\n * import { Collection, defineSchema } from 'arrowbase';\n * import { BroadcastSync } from 'arrowbase/broadcast';\n *\n * const schema = defineSchema({ ... });\n * const collection = Collection.create(schema, { capacity: 10_000 });\n *\n * const bc = new BroadcastSync(collection, { channelName: 'users' });\n * bc.start();\n *\n * // Later…\n * bc.stop();\n * ```\n *\n * ### Protocol\n *\n * One channel carries four message kinds. Every message tags itself\n * with a per-instance `nodeId` so a sender can filter out its own\n * echoes and a receiver can dedupe per-peer deltas. When a message is\n * *applied* locally, the sync layer flips a reentrancy guard that\n * suppresses its re-broadcast — otherwise the resulting ChangeEvent\n * would go round the ring forever.\n *\n * - `delta` — `{ type, nodeId, deltas }`. Ordinary mutation.\n * - `sync-request` — `{ type, nodeId, sinceVersion }`. Emitted by\n * `start()` (unless `requestSyncOnStart: false`)\n * and by the public `requestSync()` method. Peers\n * reply with a targeted `sync-response`.\n * - `sync-response` — `{ type, nodeId, toNodeId, deltas, upTo }`.\n * Reply addressed at the requester; others ignore.\n * - `resync` — `{ type, nodeId, version }`. Published on\n * `compact` / `rollback` events, since those\n * break incremental delta coherence. Peers should\n * refetch a snapshot out-of-band (e.g., via\n * IdbPersistence) then rejoin.\n *\n * ### Non-goals\n *\n * - Convergence under concurrent edits. Last-writer-wins; if two tabs\n * mutate the same row at the same version, the receiver applies\n * whichever delta arrived later. For proper conflict resolution,\n * layer a CRDT atop.\n * - Delivery guarantees. `BroadcastChannel` is best-effort inside the\n * same origin; messages may be dropped under extreme memory\n * pressure. If this matters, pair with a persistence adapter and\n * use `hello` + `sync-request` on reconnect.\n * - Security. Every same-origin document on the channel sees every\n * message. Do not broadcast secrets.\n */\n\nimport type { Collection } from './collection/collection.js';\nimport { ChangeLog } from './sync/change-log.js';\nimport { applyDeltas } from './sync/memory.js';\nimport type { Delta } from './sync/types.js';\n\n// ---------------------------------------------------------------------\n// Wire types\n// ---------------------------------------------------------------------\n\ninterface DeltaMessage {\n readonly type: 'delta';\n readonly nodeId: string;\n readonly deltas: readonly Delta[];\n}\ninterface SyncRequestMessage {\n readonly type: 'sync-request';\n readonly nodeId: string;\n readonly sinceVersion: number;\n}\ninterface SyncResponseMessage {\n readonly type: 'sync-response';\n readonly nodeId: string;\n /** Only the intended recipient processes this. Others ignore it. */\n readonly toNodeId: string;\n readonly deltas: readonly Delta[];\n readonly upTo: number;\n}\ninterface ResyncMessage {\n readonly type: 'resync';\n readonly nodeId: string;\n readonly version: number;\n}\n\nexport type BroadcastMessage =\n | DeltaMessage\n | SyncRequestMessage\n | SyncResponseMessage\n | ResyncMessage;\n\n// ---------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------\n\nexport interface BroadcastSyncOptions {\n /** Channel name. All participants on this channel share state. */\n channelName: string;\n /**\n * Unique per-instance identifier. Defaults to a random string. Only\n * used to filter echoed messages; two instances with the same id\n * still work but self-echo skipping won't catch cross-instance\n * duplicates (harmless because applyDeltas is idempotent).\n */\n nodeId?: string;\n /**\n * Optional callback fired when a peer broadcasts a `resync` message\n * (triggered by `compact` / `rollback` events on the sender).\n * Receivers typically re-hydrate from persistence and then rejoin.\n */\n onResync?: (event: { fromNodeId: string; version: number }) => void;\n /**\n * Optional callback for any error in the channel pipeline (parse\n * errors, serialization failures). Default: `console.error`.\n */\n onError?: (err: unknown) => void;\n /**\n * Injected `BroadcastChannel` constructor (for tests that want a\n * fake, or environments that polyfill under a different global).\n * Defaults to `globalThis.BroadcastChannel`.\n */\n BroadcastChannel?: typeof globalThis.BroadcastChannel;\n /**\n * If true, on `start()` emit a `sync-request(collection.globalVersion)`\n * so existing peers can catch us up. Default `true`. Set to `false`\n * when you are starting with a trusted snapshot (e.g., IdbPersistence)\n * and only want to receive live deltas from now on.\n */\n requestSyncOnStart?: boolean;\n /**\n * `ChangeLog.bufferLimit` — how many deltas we retain for sync\n * responses. Defaults to the ChangeLog's own default.\n */\n bufferLimit?: number;\n}\n\nexport class BroadcastSync {\n readonly nodeId: string;\n private readonly channel: BroadcastChannel;\n private readonly changeLog: ChangeLog;\n private readonly onError: (err: unknown) => void;\n private readonly onResyncCallback:\n | ((event: { fromNodeId: string; version: number }) => void)\n | undefined;\n /** Counter of nested apply calls; non-zero suppresses re-broadcast. */\n private applying = 0;\n /** Cursor into ChangeLog — tracks \"which deltas have been broadcast\". */\n private drainCursor = 0;\n /**\n * Per-peer cursor of the highest delta.version we've applied from\n * that peer. Prevents double-apply when a delta arrives both as a\n * live broadcast AND in a sync-response that raced with our own\n * sync-request.\n */\n private appliedByPeer = new Map<string, number>();\n private unsubscribeLocal: (() => void) | null = null;\n private unsubscribeResync: (() => void) | null = null;\n private listener: ((ev: MessageEvent) => void) | null = null;\n private stopped = false;\n private started = false;\n\n constructor(\n private readonly collection: Collection,\n options: BroadcastSyncOptions,\n ) {\n this.nodeId = options.nodeId ?? generateNodeId();\n this.onError = options.onError ?? ((err) => {\n // eslint-disable-next-line no-console\n console.error('[arrowbase/broadcast]', err);\n });\n this.onResyncCallback = options.onResync;\n const Ctor =\n options.BroadcastChannel ?? (globalThis as { BroadcastChannel?: typeof BroadcastChannel }).BroadcastChannel;\n if (!Ctor) {\n throw new Error(\n 'BroadcastSync: no BroadcastChannel available. Pass options.BroadcastChannel ' +\n 'or run in a browser / Node >=15.',\n );\n }\n this.channel = new Ctor(options.channelName);\n this.changeLog = new ChangeLog(collection, {\n ...(options.bufferLimit !== undefined\n ? { bufferLimit: options.bufferLimit }\n : {}),\n });\n this.requestSyncOnStart = options.requestSyncOnStart ?? true;\n }\n\n private requestSyncOnStart: boolean;\n\n /** Begin listening for peer messages and broadcasting local changes. */\n start(): void {\n if (this.stopped) {\n throw new Error('BroadcastSync: cannot start(); already stopped');\n }\n if (this.started) return;\n this.started = true;\n\n // Inbound.\n this.listener = (ev: MessageEvent): void => {\n try {\n this.handleMessage(ev.data as BroadcastMessage);\n } catch (err) {\n this.onError(err);\n }\n };\n this.channel.addEventListener('message', this.listener);\n\n // Outbound: drain ChangeLog on each local change, skipping whatever\n // was triggered by our own apply() path.\n this.unsubscribeLocal = this.collection.subscribe(() => {\n if (this.applying > 0) return;\n this.drainAndBroadcast();\n });\n // Forward compact / rollback as a resync sentinel so peers know\n // their cached deltas are no longer coherent.\n this.unsubscribeResync = this.changeLog.onResync(({ version }) => {\n if (this.applying > 0) return;\n this.post({ type: 'resync', nodeId: this.nodeId, version });\n });\n\n if (this.requestSyncOnStart) {\n this.post({\n type: 'sync-request',\n nodeId: this.nodeId,\n sinceVersion: this.collection.globalVersion,\n });\n }\n }\n\n /** Stop listening and release resources. Idempotent. */\n stop(): void {\n if (this.stopped) return;\n this.stopped = true;\n if (this.listener) {\n this.channel.removeEventListener('message', this.listener);\n this.listener = null;\n }\n this.unsubscribeLocal?.();\n this.unsubscribeLocal = null;\n this.unsubscribeResync?.();\n this.unsubscribeResync = null;\n this.changeLog.dispose();\n this.channel.close();\n }\n\n /**\n * Ask peers for any deltas newer than `sinceVersion`. Use this after\n * a suspected desync (e.g., the app came back online) if you have\n * not just started the adapter.\n */\n requestSync(sinceVersion: number = this.collection.globalVersion): void {\n this.post({\n type: 'sync-request',\n nodeId: this.nodeId,\n sinceVersion,\n });\n }\n\n // -------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------\n\n private drainAndBroadcast(): void {\n const { deltas, upTo } = this.changeLog.drainSince(this.drainCursor);\n if (deltas.length === 0) return;\n this.drainCursor = upTo;\n this.post({ type: 'delta', nodeId: this.nodeId, deltas });\n }\n\n private handleMessage(msg: BroadcastMessage | null | undefined): void {\n if (!msg || typeof msg !== 'object' || !('type' in msg)) return;\n // Self-echo: every BroadcastChannel implementation skips this by\n // spec, but defensively drop anything tagged with our own nodeId.\n if ('nodeId' in msg && msg.nodeId === this.nodeId) return;\n\n switch (msg.type) {\n case 'delta':\n this.applyInbound(msg.deltas, msg.nodeId);\n return;\n case 'sync-request': {\n // Honor the requested cursor literally. A peer calling\n // requestSync(V) knows it has received nothing from us at\n // version > V and expects the full log back. We do not\n // filter against drainCursor here because that cursor only\n // tracks *outgoing* broadcasts — not whether the requester\n // has seen them. (They may have missed deltas we already\n // broadcast, e.g., because they started before we did or\n // suppressed announceOnStart.)\n const { deltas, upTo } = this.changeLog.drainSince(msg.sinceVersion);\n if (deltas.length === 0) return;\n this.post({\n type: 'sync-response',\n nodeId: this.nodeId,\n toNodeId: msg.nodeId,\n deltas,\n upTo,\n });\n return;\n }\n case 'sync-response':\n if (msg.toNodeId !== this.nodeId) return;\n this.applyInbound(msg.deltas, msg.nodeId);\n return;\n case 'resync':\n this.onResyncCallback?.({\n fromNodeId: msg.nodeId,\n version: msg.version,\n });\n return;\n }\n }\n\n private applyInbound(\n deltas: readonly Delta[],\n fromNodeId: string,\n ): void {\n if (deltas.length === 0) return;\n // Filter deltas we've already applied from this peer, so a\n // sync-response that races with live broadcasts can't cause\n // double-apply (which would promote duplicate inserts to\n // updates and bump globalVersion redundantly).\n const lastApplied = this.appliedByPeer.get(fromNodeId) ?? -Infinity;\n const fresh: Delta[] = [];\n let highestSeen = lastApplied;\n for (const d of deltas) {\n if (d.version <= lastApplied) continue;\n fresh.push(d);\n if (d.version > highestSeen) highestSeen = d.version;\n }\n if (fresh.length === 0) return;\n this.applying++;\n try {\n applyDeltas(this.collection, fresh);\n } finally {\n this.applying--;\n }\n this.appliedByPeer.set(fromNodeId, highestSeen);\n // After the apply, the ChangeLog will have captured echoes of the\n // deltas we just ingested. Advance our own drain cursor past them\n // so we don't re-emit on the next local mutation.\n this.drainCursor = this.collection.globalVersion;\n }\n\n private post(msg: BroadcastMessage): void {\n try {\n this.channel.postMessage(msg);\n } catch (err) {\n this.onError(err);\n }\n }\n}\n\n// ---------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------\n\nfunction generateNodeId(): string {\n // crypto.randomUUID is widely available; fall back to Math.random\n // for environments that lack it.\n const g = globalThis as {\n crypto?: { randomUUID?: () => string };\n };\n if (g.crypto?.randomUUID) return g.crypto.randomUUID();\n return (\n 'node-' +\n Math.random().toString(36).slice(2, 10) +\n Math.random().toString(36).slice(2, 10)\n );\n}\n"]}
|