pbtsdb 0.11.0 → 2.0.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/README.md +76 -57
- package/dist/chunk-4CRCARB3.js +2954 -0
- package/dist/chunk-4CRCARB3.js.map +1 -0
- package/dist/core.d.ts +180 -86
- package/dist/core.js +1 -1
- package/dist/index.d.ts +6 -10
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/llms.txt +29 -25
- package/package.json +6 -11
- package/dist/chunk-OBDOVDCI.js +0 -1596
- package/dist/chunk-OBDOVDCI.js.map +0 -1
|
@@ -0,0 +1,2954 @@
|
|
|
1
|
+
import { parseWhereExpression, parseOrderByExpression, IR, createCollection as createCollection$1, BTreeIndex, whenSyncAccepted, LoadSubsetOperationAbortedError, compileSingleRowExpression, toBooleanPredicate } from '@tanstack/db';
|
|
2
|
+
export { BTreeIndex, BasicIndex, ReverseIndex, createEffect, materialize, toArray } from '@tanstack/db';
|
|
3
|
+
|
|
4
|
+
// src/core.ts
|
|
5
|
+
|
|
6
|
+
// src/expand-paths.ts
|
|
7
|
+
function normalizePaths(paths) {
|
|
8
|
+
const set = /* @__PURE__ */ new Set();
|
|
9
|
+
for (const path of paths) {
|
|
10
|
+
const trimmed = path.trim();
|
|
11
|
+
if (trimmed) set.add(trimmed);
|
|
12
|
+
}
|
|
13
|
+
return [...set].sort();
|
|
14
|
+
}
|
|
15
|
+
function splitPaths(csv) {
|
|
16
|
+
return csv ? normalizePaths(csv.split(",")) : [];
|
|
17
|
+
}
|
|
18
|
+
function joinPaths(paths) {
|
|
19
|
+
const normalized = normalizePaths(paths);
|
|
20
|
+
return normalized.length > 0 ? normalized.join(",") : void 0;
|
|
21
|
+
}
|
|
22
|
+
var BACK_RELATION_EXPAND_CAP = 1e3;
|
|
23
|
+
function parseViaKey(key) {
|
|
24
|
+
const index = key.indexOf("_via_");
|
|
25
|
+
if (index < 0) return void 0;
|
|
26
|
+
const field = key.slice(index + "_via_".length);
|
|
27
|
+
return field ? { field } : void 0;
|
|
28
|
+
}
|
|
29
|
+
function markFiledSubset(target, key, values, parentId) {
|
|
30
|
+
const via = parseViaKey(key);
|
|
31
|
+
if (!via || values.length >= BACK_RELATION_EXPAND_CAP) return;
|
|
32
|
+
target.markSubsetLoaded(via.field, parentId);
|
|
33
|
+
}
|
|
34
|
+
function validateExpandPath(collectionName, targets, path) {
|
|
35
|
+
if (!targets) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Cannot expand "${path}" on collection "${collectionName}": no relations declared`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
let current = targets;
|
|
41
|
+
for (const segment of path.split(".")) {
|
|
42
|
+
const next = current?.[segment];
|
|
43
|
+
if (!next) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Cannot expand "${path}" on collection "${collectionName}": segment "${segment}" is not a declared relation`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
current = next.relationTargets;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/logger.ts
|
|
53
|
+
var defaultLogger = {
|
|
54
|
+
debug: (msg, context) => {
|
|
55
|
+
if (process.env.NODE_ENV === "development") {
|
|
56
|
+
console.debug(`[pbtsdb] ${msg}`, context || "");
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
info: (msg, context) => {
|
|
60
|
+
console.info(`[pbtsdb] ${msg}`, context || "");
|
|
61
|
+
},
|
|
62
|
+
warn: (msg, context) => {
|
|
63
|
+
console.warn(`[pbtsdb] ${msg}`, context || "");
|
|
64
|
+
},
|
|
65
|
+
error: (msg, context) => {
|
|
66
|
+
console.error(`[pbtsdb] ${msg}`, context || "");
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var currentLogger = defaultLogger;
|
|
70
|
+
var logger = {
|
|
71
|
+
debug: (msg, context) => currentLogger.debug(msg, context),
|
|
72
|
+
info: (msg, context) => currentLogger.info(msg, context),
|
|
73
|
+
warn: (msg, context) => currentLogger.warn(msg, context),
|
|
74
|
+
error: (msg, context) => currentLogger.error(msg, context)
|
|
75
|
+
};
|
|
76
|
+
function setLogger(customLogger) {
|
|
77
|
+
currentLogger = customLogger;
|
|
78
|
+
}
|
|
79
|
+
function resetLogger() {
|
|
80
|
+
currentLogger = defaultLogger;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/records.ts
|
|
84
|
+
function idOf(value) {
|
|
85
|
+
if (!value || typeof value !== "object" || !("id" in value)) return void 0;
|
|
86
|
+
return typeof value.id === "string" ? value.id : void 0;
|
|
87
|
+
}
|
|
88
|
+
function idsOf(rows) {
|
|
89
|
+
return rows.map(idOf).filter((id) => id !== void 0);
|
|
90
|
+
}
|
|
91
|
+
function updatedAtOf(value) {
|
|
92
|
+
if (!value || typeof value !== "object" || !("updated" in value)) return void 0;
|
|
93
|
+
return typeof value.updated === "string" && value.updated !== "" ? value.updated : void 0;
|
|
94
|
+
}
|
|
95
|
+
function expandOf(value) {
|
|
96
|
+
if (!value || typeof value !== "object" || !("expand" in value)) return void 0;
|
|
97
|
+
const { expand } = value;
|
|
98
|
+
return expand && typeof expand === "object" ? expand : void 0;
|
|
99
|
+
}
|
|
100
|
+
function isObject(value) {
|
|
101
|
+
return typeof value === "object" && value !== null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/expand-filing.ts
|
|
105
|
+
function backRelationHeads(expandString, targets) {
|
|
106
|
+
if (!targets) return [];
|
|
107
|
+
const heads = [];
|
|
108
|
+
for (const key of splitPaths(expandString).map((path) => path.split(".")[0])) {
|
|
109
|
+
const target = targets[key];
|
|
110
|
+
const via = target && parseViaKey(key);
|
|
111
|
+
if (target && via) heads.push({ target, key, field: via.field });
|
|
112
|
+
}
|
|
113
|
+
return heads;
|
|
114
|
+
}
|
|
115
|
+
function targetsAlong(paths, targets) {
|
|
116
|
+
const found = /* @__PURE__ */ new Set();
|
|
117
|
+
for (const path of paths) {
|
|
118
|
+
let current = targets;
|
|
119
|
+
for (const segment of path.split(".")) {
|
|
120
|
+
const target = current?.[segment];
|
|
121
|
+
if (!target) break;
|
|
122
|
+
found.add(target);
|
|
123
|
+
current = target.relationTargets;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return found;
|
|
127
|
+
}
|
|
128
|
+
function withoutExpand(values) {
|
|
129
|
+
return values.map((value) => {
|
|
130
|
+
const { expand: _expand, ...plain } = value;
|
|
131
|
+
return plain;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function stripFetchedRelations(items, expandString) {
|
|
135
|
+
const heads = new Set(splitPaths(expandString).map((path) => path.split(".")[0]));
|
|
136
|
+
if (heads.size === 0) return items;
|
|
137
|
+
return items.map((item) => {
|
|
138
|
+
const { expand, ...plain } = item;
|
|
139
|
+
if (!expand) return item;
|
|
140
|
+
const kept = Object.fromEntries(Object.entries(expand).filter(([key]) => !heads.has(key)));
|
|
141
|
+
return Object.keys(kept).length > 0 ? { ...plain, expand: kept } : plain;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function groupExpandedByKey(records) {
|
|
145
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
146
|
+
for (const record of records) {
|
|
147
|
+
const expand = expandOf(record);
|
|
148
|
+
if (!expand) continue;
|
|
149
|
+
const parentId = idOf(record);
|
|
150
|
+
for (const [key, value] of Object.entries(expand)) {
|
|
151
|
+
const values = (Array.isArray(value) ? value : [value]).filter(isObject);
|
|
152
|
+
const group = byKey.get(key) ?? { values: [], byParent: [] };
|
|
153
|
+
group.values.push(...values);
|
|
154
|
+
if (parentId) group.byParent.push([parentId, values]);
|
|
155
|
+
byKey.set(key, group);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return byKey;
|
|
159
|
+
}
|
|
160
|
+
function lastById(values) {
|
|
161
|
+
const byId = /* @__PURE__ */ new Map();
|
|
162
|
+
for (const value of values) byId.set(idOf(value), value);
|
|
163
|
+
return [...byId.values()];
|
|
164
|
+
}
|
|
165
|
+
function tailsOf(paths, key) {
|
|
166
|
+
return paths.filter((path) => path.startsWith(`${key}.`)).map((path) => path.slice(key.length + 1));
|
|
167
|
+
}
|
|
168
|
+
function createExpandFiler(deps) {
|
|
169
|
+
const { collectionName, setFiled, holder } = deps;
|
|
170
|
+
function entryFor2(perRoot, root) {
|
|
171
|
+
let found = perRoot.get(root);
|
|
172
|
+
if (!found) {
|
|
173
|
+
found = { rows: /* @__PURE__ */ new Set(), parents: /* @__PURE__ */ new Set() };
|
|
174
|
+
perRoot.set(root, found);
|
|
175
|
+
}
|
|
176
|
+
return found;
|
|
177
|
+
}
|
|
178
|
+
function addTo(index, key, value) {
|
|
179
|
+
let values = index.get(key);
|
|
180
|
+
if (!values) {
|
|
181
|
+
values = /* @__PURE__ */ new Set();
|
|
182
|
+
index.set(key, values);
|
|
183
|
+
}
|
|
184
|
+
values.add(value);
|
|
185
|
+
}
|
|
186
|
+
function rootsReaching(records, via, rootsOf, perRoot) {
|
|
187
|
+
for (const record of records) {
|
|
188
|
+
const parentId = idOf(record);
|
|
189
|
+
if (!parentId) continue;
|
|
190
|
+
for (const root of rootsOf(parentId)) {
|
|
191
|
+
const found = entryFor2(perRoot, root);
|
|
192
|
+
if (via) found.parents.add(parentId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function collectRoots(records, group, requested, via, rootsOf) {
|
|
197
|
+
const perRoot = /* @__PURE__ */ new Map();
|
|
198
|
+
const childRoots = /* @__PURE__ */ new Map();
|
|
199
|
+
if (requested) rootsReaching(records, via, rootsOf, perRoot);
|
|
200
|
+
for (const [parentId, values] of group.byParent) {
|
|
201
|
+
const ids = idsOf(values);
|
|
202
|
+
for (const root of rootsOf(parentId)) {
|
|
203
|
+
const found = entryFor2(perRoot, root);
|
|
204
|
+
found.parents.add(parentId);
|
|
205
|
+
for (const id of ids) {
|
|
206
|
+
found.rows.add(id);
|
|
207
|
+
addTo(childRoots, id, root);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { perRoot, childRoots };
|
|
212
|
+
}
|
|
213
|
+
function clearDeeper(root, targets, paths, prefix, releases) {
|
|
214
|
+
if (!targets) return;
|
|
215
|
+
for (const head of new Set(paths.map((path) => path.split(".")[0]))) {
|
|
216
|
+
const target = targets[head];
|
|
217
|
+
if (!target) continue;
|
|
218
|
+
releases.push(setFiled(root, target, `${prefix}${head}`, [], []));
|
|
219
|
+
clearDeeper(
|
|
220
|
+
root,
|
|
221
|
+
target.relationTargets,
|
|
222
|
+
tailsOf(paths, head),
|
|
223
|
+
`${prefix}${head}.`,
|
|
224
|
+
releases
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function fileKey(records, level, key, target, group, requested) {
|
|
229
|
+
const via = parseViaKey(key) !== void 0;
|
|
230
|
+
const { perRoot, childRoots } = collectRoots(records, group, requested, via, level.rootsOf);
|
|
231
|
+
const values = lastById(group.values);
|
|
232
|
+
const tails = tailsOf(level.requestedPaths, key);
|
|
233
|
+
const path = `${level.prefix}${key}`;
|
|
234
|
+
if (values.length > 0) {
|
|
235
|
+
await fileLevel(values, target.relationTargets, {
|
|
236
|
+
requestedPaths: tails,
|
|
237
|
+
releases: level.releases,
|
|
238
|
+
rootsOf: (id) => childRoots.get(id) ?? [],
|
|
239
|
+
prefix: `${path}.`
|
|
240
|
+
});
|
|
241
|
+
if (!await target.writeFiled(values, holder)) return;
|
|
242
|
+
}
|
|
243
|
+
for (const [root, { rows, parents }] of perRoot) {
|
|
244
|
+
level.releases.push(setFiled(root, target, path, rows, via ? parents : rows));
|
|
245
|
+
if (rows.size === 0) {
|
|
246
|
+
clearDeeper(root, target.relationTargets, tails, `${path}.`, level.releases);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const [parentId, parentValues] of group.byParent) {
|
|
250
|
+
markFiledSubset(target, key, parentValues, parentId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function fileLevel(records, targets, level) {
|
|
254
|
+
if (!targets) return;
|
|
255
|
+
const grouped = groupExpandedByKey(records);
|
|
256
|
+
const heads = new Set(level.requestedPaths.map((path) => path.split(".")[0]));
|
|
257
|
+
for (const key of /* @__PURE__ */ new Set([...grouped.keys(), ...heads])) {
|
|
258
|
+
const target = targets[key];
|
|
259
|
+
if (!target) {
|
|
260
|
+
if (grouped.has(key)) {
|
|
261
|
+
logger.debug("No relation target for expanded field", { collectionName, key });
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const group = grouped.get(key) ?? { values: [], byParent: [] };
|
|
266
|
+
await fileKey(records, level, key, target, group, heads.has(key));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function fileExpanded(records, targets, requestedPaths) {
|
|
270
|
+
const releases = [];
|
|
271
|
+
try {
|
|
272
|
+
await fileLevel(
|
|
273
|
+
records.filter((record) => !deps.isStale(record)),
|
|
274
|
+
targets,
|
|
275
|
+
{ requestedPaths, releases, rootsOf: (id) => [id], prefix: "" }
|
|
276
|
+
);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
for (const change of [...releases].reverse()) change.undo();
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
commit: () => {
|
|
283
|
+
for (const change of releases) change.commit();
|
|
284
|
+
},
|
|
285
|
+
undo: () => {
|
|
286
|
+
for (const change of [...releases].reverse()) change.undo();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
async function upsertExpanded(records, targets, requestedPaths) {
|
|
291
|
+
(await fileExpanded(records, targets, requestedPaths)).commit();
|
|
292
|
+
}
|
|
293
|
+
function targetsReady(records, targets) {
|
|
294
|
+
for (const [key, group] of groupExpandedByKey(records)) {
|
|
295
|
+
const target = targets[key];
|
|
296
|
+
if (!target) continue;
|
|
297
|
+
if (!target.isReady()) return false;
|
|
298
|
+
const nested = target.relationTargets;
|
|
299
|
+
if (nested && !targetsReady(lastById(group.values), nested)) return false;
|
|
300
|
+
}
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
function canFileFirst(records, targets) {
|
|
304
|
+
if (!targets) return false;
|
|
305
|
+
const toFile = [...groupExpandedByKey(records).keys()].some((key) => targets[key]);
|
|
306
|
+
return toFile && targetsReady(records, targets);
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
fileExpanded,
|
|
310
|
+
upsertExpanded,
|
|
311
|
+
canFileFirst,
|
|
312
|
+
markEmptyBackRelations(items, heads) {
|
|
313
|
+
if (heads.length === 0) return;
|
|
314
|
+
for (const item of items) {
|
|
315
|
+
const id = idOf(item);
|
|
316
|
+
if (!id) continue;
|
|
317
|
+
const expand = expandOf(item);
|
|
318
|
+
for (const { target, key, field } of heads) {
|
|
319
|
+
if (expand?.[key] === void 0) target.markSubsetLoaded(field, id);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/keyed-where.ts
|
|
327
|
+
function fieldRef(node) {
|
|
328
|
+
return node?.type === "ref" && node.path?.length === 1 ? node.path[0] : void 0;
|
|
329
|
+
}
|
|
330
|
+
function stringValue(node) {
|
|
331
|
+
return node?.type === "val" && typeof node.value === "string" ? node.value : void 0;
|
|
332
|
+
}
|
|
333
|
+
function collectEq(a, b) {
|
|
334
|
+
const field = fieldRef(a) ?? fieldRef(b);
|
|
335
|
+
if (field === void 0) return void 0;
|
|
336
|
+
const value = stringValue(fieldRef(a) !== void 0 ? b : a);
|
|
337
|
+
return value === void 0 ? void 0 : { field, values: [value] };
|
|
338
|
+
}
|
|
339
|
+
function collectIn(a, b) {
|
|
340
|
+
const field = fieldRef(a);
|
|
341
|
+
if (field === void 0 || b?.type !== "val" || !Array.isArray(b.value)) return void 0;
|
|
342
|
+
return b.value.every((v) => typeof v === "string") ? { field, values: b.value } : void 0;
|
|
343
|
+
}
|
|
344
|
+
function collectOr(args) {
|
|
345
|
+
let field;
|
|
346
|
+
const values = [];
|
|
347
|
+
for (const arg of args) {
|
|
348
|
+
const sub = collect(arg);
|
|
349
|
+
if (!sub || field !== void 0 && sub.field !== field) return void 0;
|
|
350
|
+
field = sub.field;
|
|
351
|
+
values.push(...sub.values);
|
|
352
|
+
}
|
|
353
|
+
return field === void 0 ? void 0 : { field, values };
|
|
354
|
+
}
|
|
355
|
+
function collect(node) {
|
|
356
|
+
if (node.type !== "func" || !node.args) return void 0;
|
|
357
|
+
const [a, b] = node.args;
|
|
358
|
+
switch (node.name) {
|
|
359
|
+
case "eq":
|
|
360
|
+
return collectEq(a, b);
|
|
361
|
+
case "in":
|
|
362
|
+
return collectIn(a, b);
|
|
363
|
+
case "or":
|
|
364
|
+
return collectOr(node.args);
|
|
365
|
+
default:
|
|
366
|
+
return void 0;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function subsetFromWhere(where) {
|
|
370
|
+
if (!where) return void 0;
|
|
371
|
+
const subset = collect(where);
|
|
372
|
+
return subset ? { field: subset.field, values: [...new Set(subset.values)].sort() } : void 0;
|
|
373
|
+
}
|
|
374
|
+
function matchesSubset(row, field, wanted) {
|
|
375
|
+
const value = row[field];
|
|
376
|
+
if (typeof value === "string") return wanted.has(value);
|
|
377
|
+
if (Array.isArray(value))
|
|
378
|
+
return value.some((item) => typeof item === "string" && wanted.has(item));
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
function escapeValue(value) {
|
|
382
|
+
if (value === null) {
|
|
383
|
+
return "null";
|
|
384
|
+
}
|
|
385
|
+
if (typeof value === "boolean") {
|
|
386
|
+
return value ? "true" : "false";
|
|
387
|
+
}
|
|
388
|
+
if (typeof value === "number") {
|
|
389
|
+
return value.toString();
|
|
390
|
+
}
|
|
391
|
+
if (typeof value === "string") {
|
|
392
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
393
|
+
}
|
|
394
|
+
if (value instanceof Date) {
|
|
395
|
+
return `"${value.toISOString()}"`;
|
|
396
|
+
}
|
|
397
|
+
if (Array.isArray(value)) {
|
|
398
|
+
return `[${value.map(escapeValue).join(",")}]`;
|
|
399
|
+
}
|
|
400
|
+
return `"${String(value)}"`;
|
|
401
|
+
}
|
|
402
|
+
function fieldPathToString(path) {
|
|
403
|
+
return path.join(".");
|
|
404
|
+
}
|
|
405
|
+
var NEVER_MATCH = "1 = 2";
|
|
406
|
+
var ALWAYS_MATCH = "1 = 1";
|
|
407
|
+
function membership(field, values, operator, joiner, empty) {
|
|
408
|
+
const valueArray = Array.isArray(values) ? values : [values];
|
|
409
|
+
const fieldStr = fieldPathToString(field);
|
|
410
|
+
const conditions = [...new Set(valueArray)].map(
|
|
411
|
+
(v) => `${fieldStr} ${operator} ${escapeValue(v)}`
|
|
412
|
+
);
|
|
413
|
+
if (conditions.length === 0) return empty;
|
|
414
|
+
return conditions.length > 1 ? `(${conditions.join(` ${joiner} `)})` : conditions[0];
|
|
415
|
+
}
|
|
416
|
+
var NEGATED_OPERATORS = {
|
|
417
|
+
eq: "neq",
|
|
418
|
+
neq: "eq",
|
|
419
|
+
gt: "lte",
|
|
420
|
+
gte: "lt",
|
|
421
|
+
lt: "gte",
|
|
422
|
+
lte: "gt",
|
|
423
|
+
in: "notIn",
|
|
424
|
+
notIn: "in",
|
|
425
|
+
like: "notLike",
|
|
426
|
+
notLike: "like",
|
|
427
|
+
isNull: "isNotNull",
|
|
428
|
+
isUndefined: "isNotNull",
|
|
429
|
+
isNotNull: "isNull"
|
|
430
|
+
};
|
|
431
|
+
function negate(expr) {
|
|
432
|
+
if (expr.type !== "func") {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`Unsupported operand '${expr.type}' inside not() for PocketBase filter conversion`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
if (expr.name === "not") {
|
|
438
|
+
return pushDownNot(expr.args[0]);
|
|
439
|
+
}
|
|
440
|
+
if (expr.name === "and" || expr.name === "or") {
|
|
441
|
+
return new IR.Func(expr.name === "and" ? "or" : "and", expr.args.map(negate));
|
|
442
|
+
}
|
|
443
|
+
const negated = NEGATED_OPERATORS[expr.name];
|
|
444
|
+
if (!negated) {
|
|
445
|
+
throw new Error(`Unsupported operator 'not(${expr.name})' for PocketBase filter conversion`);
|
|
446
|
+
}
|
|
447
|
+
return new IR.Func(negated, expr.args);
|
|
448
|
+
}
|
|
449
|
+
function pushDownNot(expr) {
|
|
450
|
+
if (expr.type !== "func") {
|
|
451
|
+
return expr;
|
|
452
|
+
}
|
|
453
|
+
if (expr.name === "not") {
|
|
454
|
+
return negate(expr.args[0]);
|
|
455
|
+
}
|
|
456
|
+
if (expr.name === "and" || expr.name === "or") {
|
|
457
|
+
return new IR.Func(expr.name, expr.args.map(pushDownNot));
|
|
458
|
+
}
|
|
459
|
+
return expr;
|
|
460
|
+
}
|
|
461
|
+
function convertToPocketBaseFilter(where) {
|
|
462
|
+
if (!where) {
|
|
463
|
+
return void 0;
|
|
464
|
+
}
|
|
465
|
+
const result = parseWhereExpression(pushDownNot(where), {
|
|
466
|
+
handlers: {
|
|
467
|
+
eq: (field, value) => {
|
|
468
|
+
return `${fieldPathToString(field)} = ${escapeValue(value)}`;
|
|
469
|
+
},
|
|
470
|
+
gt: (field, value) => {
|
|
471
|
+
return `${fieldPathToString(field)} > ${escapeValue(value)}`;
|
|
472
|
+
},
|
|
473
|
+
gte: (field, value) => {
|
|
474
|
+
return `${fieldPathToString(field)} >= ${escapeValue(value)}`;
|
|
475
|
+
},
|
|
476
|
+
lt: (field, value) => {
|
|
477
|
+
return `${fieldPathToString(field)} < ${escapeValue(value)}`;
|
|
478
|
+
},
|
|
479
|
+
lte: (field, value) => {
|
|
480
|
+
return `${fieldPathToString(field)} <= ${escapeValue(value)}`;
|
|
481
|
+
},
|
|
482
|
+
and: (...conditions) => {
|
|
483
|
+
if (conditions.length === 0) return "";
|
|
484
|
+
if (conditions.length === 1) return conditions[0];
|
|
485
|
+
return `(${conditions.join(" && ")})`;
|
|
486
|
+
},
|
|
487
|
+
or: (...conditions) => {
|
|
488
|
+
if (conditions.length === 0) return "";
|
|
489
|
+
if (conditions.length === 1) return conditions[0];
|
|
490
|
+
return `(${conditions.join(" || ")})`;
|
|
491
|
+
},
|
|
492
|
+
neq: (field, value) => {
|
|
493
|
+
return `${fieldPathToString(field)} != ${escapeValue(value)}`;
|
|
494
|
+
},
|
|
495
|
+
in: (field, values) => membership(field, values, "=", "||", NEVER_MATCH),
|
|
496
|
+
notIn: (field, values) => membership(field, values, "!=", "&&", ALWAYS_MATCH),
|
|
497
|
+
notLike: (field, value) => {
|
|
498
|
+
return `${fieldPathToString(field)} !~ ${escapeValue(value)}`;
|
|
499
|
+
},
|
|
500
|
+
like: (field, value) => {
|
|
501
|
+
return `${fieldPathToString(field)} ~ ${escapeValue(value)}`;
|
|
502
|
+
},
|
|
503
|
+
isNull: (field) => {
|
|
504
|
+
return `${fieldPathToString(field)} = null`;
|
|
505
|
+
},
|
|
506
|
+
isUndefined: (field) => {
|
|
507
|
+
return `${fieldPathToString(field)} = null`;
|
|
508
|
+
},
|
|
509
|
+
isNotNull: (field) => {
|
|
510
|
+
return `${fieldPathToString(field)} != null`;
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
onUnknownOperator: (operator, _args) => {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`Unsupported operator '${operator}' for PocketBase filter conversion. Supported operators: eq, gt, gte, lt, lte, in, like, and, or, not, isNull, isUndefined`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
return result || void 0;
|
|
520
|
+
}
|
|
521
|
+
function convertToPocketBaseSort(orderBy) {
|
|
522
|
+
if (!orderBy) {
|
|
523
|
+
return void 0;
|
|
524
|
+
}
|
|
525
|
+
const sorts = parseOrderByExpression(orderBy);
|
|
526
|
+
if (sorts.length === 0) {
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
return sorts.map((sort) => {
|
|
530
|
+
const field = fieldPathToString(sort.field);
|
|
531
|
+
return sort.direction === "desc" ? `-${field}` : field;
|
|
532
|
+
}).join(",");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/pocketbase-limits.ts
|
|
536
|
+
var MAX_FILTER_LENGTH = 2500;
|
|
537
|
+
var REALTIME_MAX_FILTER_LENGTH = 1e3;
|
|
538
|
+
var REALTIME_TOPIC_MAX_LENGTH = 2500;
|
|
539
|
+
var SDK_RESERVED_OPTIONS = /* @__PURE__ */ new Set([
|
|
540
|
+
"requestKey",
|
|
541
|
+
"$cancelKey",
|
|
542
|
+
"$autoCancel",
|
|
543
|
+
"fetch",
|
|
544
|
+
"headers",
|
|
545
|
+
"body",
|
|
546
|
+
"query",
|
|
547
|
+
"params",
|
|
548
|
+
"cache",
|
|
549
|
+
"credentials",
|
|
550
|
+
"integrity",
|
|
551
|
+
"keepalive",
|
|
552
|
+
"method",
|
|
553
|
+
"mode",
|
|
554
|
+
"redirect",
|
|
555
|
+
"referrer",
|
|
556
|
+
"referrerPolicy",
|
|
557
|
+
"signal",
|
|
558
|
+
"window"
|
|
559
|
+
]);
|
|
560
|
+
function realtimeTopic(collectionName, options) {
|
|
561
|
+
const topic = `${collectionName}/*`;
|
|
562
|
+
if (!options) return topic;
|
|
563
|
+
const query = { ...options.query };
|
|
564
|
+
for (const [key, value] of Object.entries(options)) {
|
|
565
|
+
if (!SDK_RESERVED_OPTIONS.has(key)) query[key] = value;
|
|
566
|
+
}
|
|
567
|
+
const encoded = encodeURIComponent(JSON.stringify({ query, headers: options.headers }));
|
|
568
|
+
return `${topic}?options=${encoded}`;
|
|
569
|
+
}
|
|
570
|
+
function realtimeTopicLength(collectionName, options) {
|
|
571
|
+
return realtimeTopic(collectionName, options).length;
|
|
572
|
+
}
|
|
573
|
+
function subsetFilters({ field, values }, maxLength = MAX_FILTER_LENGTH) {
|
|
574
|
+
const filters = [];
|
|
575
|
+
let current = "";
|
|
576
|
+
for (const value of values) {
|
|
577
|
+
const clause = `${field} = ${escapeValue(value)}`;
|
|
578
|
+
const joined = current ? `${current} || ${clause}` : clause;
|
|
579
|
+
if (current && joined.length > maxLength) {
|
|
580
|
+
filters.push(current);
|
|
581
|
+
current = clause;
|
|
582
|
+
} else {
|
|
583
|
+
current = joined;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (current) filters.push(current);
|
|
587
|
+
return filters;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/fetch-records.ts
|
|
591
|
+
var NO_FILINGS = { commit: () => void 0, undo: () => void 0 };
|
|
592
|
+
var FetchAbortedError = class extends Error {
|
|
593
|
+
constructor(collectionName) {
|
|
594
|
+
super(`${collectionName}: fetch aborted`);
|
|
595
|
+
this.name = "AbortError";
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
function createFetcher(deps) {
|
|
599
|
+
const { pb, collectionName, relationTargets, filer, subsets } = deps;
|
|
600
|
+
const pendingFilings = /* @__PURE__ */ new Map();
|
|
601
|
+
let requestSeq = 0;
|
|
602
|
+
function expectFiling(field, settles) {
|
|
603
|
+
let pending = pendingFilings.get(field);
|
|
604
|
+
if (!pending) {
|
|
605
|
+
pending = [];
|
|
606
|
+
pendingFilings.set(field, pending);
|
|
607
|
+
}
|
|
608
|
+
pending.push(settles);
|
|
609
|
+
return () => {
|
|
610
|
+
const index = pending.indexOf(settles);
|
|
611
|
+
if (index !== -1) pending.splice(index, 1);
|
|
612
|
+
if (pending.length === 0) pendingFilings.delete(field);
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
async function awaitPendingFiling(field) {
|
|
616
|
+
const pending = pendingFilings.get(field);
|
|
617
|
+
if (pending && pending.length > 0) await Promise.all(pending);
|
|
618
|
+
}
|
|
619
|
+
function rowsFromStore(ids) {
|
|
620
|
+
const rows = [];
|
|
621
|
+
for (const id of ids) {
|
|
622
|
+
const row = deps.syncedRow(id);
|
|
623
|
+
if (!row) return void 0;
|
|
624
|
+
rows.push(row);
|
|
625
|
+
}
|
|
626
|
+
return rows;
|
|
627
|
+
}
|
|
628
|
+
function fieldSubsetFromStore(subset) {
|
|
629
|
+
if (!subsets.isLoaded(subset)) return void 0;
|
|
630
|
+
const wanted = new Set(subset.values);
|
|
631
|
+
return [...deps.syncedRows()].filter((row) => matchesSubset(row, subset.field, wanted));
|
|
632
|
+
}
|
|
633
|
+
function servedFromStore(request) {
|
|
634
|
+
const { subset, sort, limit } = request;
|
|
635
|
+
if (!subset) return void 0;
|
|
636
|
+
if (subset.values.length === 0) return [];
|
|
637
|
+
if (request.expand || request.cursor || sort && limit) return void 0;
|
|
638
|
+
const rows = subset.field === "id" ? rowsFromStore(subset.values) : fieldSubsetFromStore(subset);
|
|
639
|
+
return rows && limit ? rows.slice(0, limit) : rows;
|
|
640
|
+
}
|
|
641
|
+
function withCursor(filter, cursor) {
|
|
642
|
+
if (!cursor) return filter;
|
|
643
|
+
return filter ? `(${filter}) && (${cursor})` : cursor;
|
|
644
|
+
}
|
|
645
|
+
async function fetchPage(request, expand, chunkFilter, requestKey, offset, signal) {
|
|
646
|
+
const { sort, limit } = request;
|
|
647
|
+
const filter = withCursor(chunkFilter, request.cursor);
|
|
648
|
+
if (limit) {
|
|
649
|
+
if (offset % limit === 0) {
|
|
650
|
+
const result2 = await pb.collection(collectionName).getList(offset / limit + 1, limit, {
|
|
651
|
+
filter,
|
|
652
|
+
sort,
|
|
653
|
+
skipTotal: true,
|
|
654
|
+
expand,
|
|
655
|
+
requestKey,
|
|
656
|
+
signal
|
|
657
|
+
});
|
|
658
|
+
return result2.items;
|
|
659
|
+
}
|
|
660
|
+
const result = await pb.collection(collectionName).getList(1, offset + limit, {
|
|
661
|
+
filter,
|
|
662
|
+
sort,
|
|
663
|
+
skipTotal: true,
|
|
664
|
+
expand,
|
|
665
|
+
requestKey,
|
|
666
|
+
signal
|
|
667
|
+
});
|
|
668
|
+
return result.items.slice(offset);
|
|
669
|
+
}
|
|
670
|
+
const items = await pb.collection(collectionName).getFullList({ filter, sort, expand, requestKey, signal });
|
|
671
|
+
return items.slice(offset);
|
|
672
|
+
}
|
|
673
|
+
function chunkRequest(request, chunks) {
|
|
674
|
+
const offset = request.offset ?? 0;
|
|
675
|
+
if (chunks === 1 || offset === 0) return { request, offset };
|
|
676
|
+
const limit = request.limit ? offset + request.limit : void 0;
|
|
677
|
+
return { request: { ...request, limit }, offset: 0 };
|
|
678
|
+
}
|
|
679
|
+
async function fetchItems(request, heads, refetch, cancel) {
|
|
680
|
+
const served = refetch ? void 0 : servedFromStore(request);
|
|
681
|
+
if (served) return { rows: served, fromStore: true };
|
|
682
|
+
if (!refetch && request.subset && request.subset.field !== "id" && heads.length === 0) {
|
|
683
|
+
await awaitPendingFiling(request.subset.field);
|
|
684
|
+
const servedAfterFiling = servedFromStore(request);
|
|
685
|
+
if (servedAfterFiling) return { rows: servedAfterFiling, fromStore: true };
|
|
686
|
+
}
|
|
687
|
+
const filters = request.subset ? subsetFilters(request.subset) : [request.filter];
|
|
688
|
+
const expand = deps.activeExpand(request);
|
|
689
|
+
const chunk = chunkRequest(request, filters.length);
|
|
690
|
+
const load = ++requestSeq;
|
|
691
|
+
const pages = await Promise.all(
|
|
692
|
+
filters.map((chunkFilter, index) => {
|
|
693
|
+
const requestKey = `${collectionName}:${load}:${index}`;
|
|
694
|
+
cancel.keys.push(requestKey);
|
|
695
|
+
return fetchPage(
|
|
696
|
+
chunk.request,
|
|
697
|
+
expand,
|
|
698
|
+
chunkFilter,
|
|
699
|
+
requestKey,
|
|
700
|
+
chunk.offset,
|
|
701
|
+
cancel.controller.signal
|
|
702
|
+
);
|
|
703
|
+
})
|
|
704
|
+
);
|
|
705
|
+
const items = pages.flat();
|
|
706
|
+
filer.markEmptyBackRelations(items, heads);
|
|
707
|
+
return { rows: items, fromStore: false };
|
|
708
|
+
}
|
|
709
|
+
async function fetchRecords(request, options = {}) {
|
|
710
|
+
const { signals = [] } = options;
|
|
711
|
+
const aborted = () => signals.some((signal) => signal.aborted);
|
|
712
|
+
const cancel = { controller: new AbortController(), keys: [] };
|
|
713
|
+
const onAbort = () => {
|
|
714
|
+
cancel.controller.abort();
|
|
715
|
+
for (const key of cancel.keys) pb.cancelRequest(key);
|
|
716
|
+
};
|
|
717
|
+
for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true });
|
|
718
|
+
let settleFiling = () => void 0;
|
|
719
|
+
const filed = new Promise((resolve) => {
|
|
720
|
+
settleFiling = resolve;
|
|
721
|
+
});
|
|
722
|
+
const heads = backRelationHeads(deps.activeExpand(request), relationTargets);
|
|
723
|
+
const unregister = heads.map(({ target, field }) => target.expectFiling(field, filed));
|
|
724
|
+
try {
|
|
725
|
+
if (aborted()) throw new FetchAbortedError(collectionName);
|
|
726
|
+
const result = await fetchItems(request, heads, options.refetch === true, cancel).catch(
|
|
727
|
+
(error) => {
|
|
728
|
+
if (aborted()) throw new FetchAbortedError(collectionName);
|
|
729
|
+
throw error;
|
|
730
|
+
}
|
|
731
|
+
);
|
|
732
|
+
if (aborted()) throw new FetchAbortedError(collectionName);
|
|
733
|
+
const filings = result.fromStore ? NO_FILINGS : await filer.fileExpanded(
|
|
734
|
+
result.rows,
|
|
735
|
+
relationTargets,
|
|
736
|
+
splitPaths(deps.activeExpand(request))
|
|
737
|
+
);
|
|
738
|
+
if (aborted()) {
|
|
739
|
+
filings.undo();
|
|
740
|
+
throw new FetchAbortedError(collectionName);
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
rows: stripFetchedRelations(result.rows, deps.activeExpand(request)),
|
|
744
|
+
fromStore: result.fromStore,
|
|
745
|
+
filings
|
|
746
|
+
};
|
|
747
|
+
} finally {
|
|
748
|
+
for (const signal of signals) signal.removeEventListener("abort", onAbort);
|
|
749
|
+
settleFiling();
|
|
750
|
+
for (const fn of unregister) fn();
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return { fetchRecords, serveFromStore: servedFromStore, expectFiling };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/held-targets.ts
|
|
757
|
+
function createHeldTargets(collectionName, holder) {
|
|
758
|
+
const held = /* @__PURE__ */ new Map();
|
|
759
|
+
const filings = /* @__PURE__ */ new Map();
|
|
760
|
+
const rowRefs = /* @__PURE__ */ new Map();
|
|
761
|
+
const filterRefs = /* @__PURE__ */ new Map();
|
|
762
|
+
function filtersFor(target) {
|
|
763
|
+
const byField = filterRefs.get(target);
|
|
764
|
+
if (!byField) return [];
|
|
765
|
+
const filters = [];
|
|
766
|
+
for (const [field, values] of byField) {
|
|
767
|
+
if (values.size === 0) continue;
|
|
768
|
+
filters.push(
|
|
769
|
+
...subsetFilters({ field, values: [...values.keys()] }, REALTIME_MAX_FILTER_LENGTH)
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
return filters;
|
|
773
|
+
}
|
|
774
|
+
function bump(counts, key, delta) {
|
|
775
|
+
const next = (counts.get(key) ?? 0) + delta;
|
|
776
|
+
if (next <= 0) {
|
|
777
|
+
counts.delete(key);
|
|
778
|
+
return delta < 0;
|
|
779
|
+
}
|
|
780
|
+
counts.set(key, next);
|
|
781
|
+
return delta > 0 && next === 1;
|
|
782
|
+
}
|
|
783
|
+
function filterRef(target, field, value, delta) {
|
|
784
|
+
let byField = filterRefs.get(target);
|
|
785
|
+
if (!byField) {
|
|
786
|
+
byField = /* @__PURE__ */ new Map();
|
|
787
|
+
filterRefs.set(target, byField);
|
|
788
|
+
}
|
|
789
|
+
let values = byField.get(field);
|
|
790
|
+
if (!values) {
|
|
791
|
+
values = /* @__PURE__ */ new Map();
|
|
792
|
+
byField.set(field, values);
|
|
793
|
+
}
|
|
794
|
+
return bump(values, value, delta);
|
|
795
|
+
}
|
|
796
|
+
function rowRef(target, id, delta) {
|
|
797
|
+
let counts = rowRefs.get(target);
|
|
798
|
+
if (!counts) {
|
|
799
|
+
counts = /* @__PURE__ */ new Map();
|
|
800
|
+
rowRefs.set(target, counts);
|
|
801
|
+
}
|
|
802
|
+
return bump(counts, id, delta);
|
|
803
|
+
}
|
|
804
|
+
function filedUnder(rootId, target) {
|
|
805
|
+
let byTarget = filings.get(rootId);
|
|
806
|
+
if (!byTarget) {
|
|
807
|
+
byTarget = /* @__PURE__ */ new Map();
|
|
808
|
+
filings.set(rootId, byTarget);
|
|
809
|
+
}
|
|
810
|
+
let byKey = byTarget.get(target);
|
|
811
|
+
if (!byKey) {
|
|
812
|
+
byKey = /* @__PURE__ */ new Map();
|
|
813
|
+
byTarget.set(target, byKey);
|
|
814
|
+
}
|
|
815
|
+
return byKey;
|
|
816
|
+
}
|
|
817
|
+
function fieldFor(key) {
|
|
818
|
+
const last = key.split(".").at(-1) ?? key;
|
|
819
|
+
return parseViaKey(last)?.field ?? "id";
|
|
820
|
+
}
|
|
821
|
+
function diff(previous, next) {
|
|
822
|
+
return {
|
|
823
|
+
added: [...next].filter((value) => !previous.has(value)),
|
|
824
|
+
removed: [...previous].filter((value) => !next.has(value))
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function swapFilterRefs(target, field, previous, next) {
|
|
828
|
+
const { added, removed } = diff(previous, next);
|
|
829
|
+
let changed = false;
|
|
830
|
+
for (const value of added) if (filterRef(target, field, value, 1)) changed = true;
|
|
831
|
+
for (const value of removed) if (filterRef(target, field, value, -1)) changed = true;
|
|
832
|
+
return changed;
|
|
833
|
+
}
|
|
834
|
+
function swapRowRefs(target, previous, next) {
|
|
835
|
+
const { added, removed } = diff(previous, next);
|
|
836
|
+
for (const id of added) rowRef(target, id, 1);
|
|
837
|
+
return removed.filter((id) => rowRef(target, id, -1));
|
|
838
|
+
}
|
|
839
|
+
function dropEntry(rootId, target, key) {
|
|
840
|
+
const byTarget = filings.get(rootId);
|
|
841
|
+
const byKey = byTarget?.get(target);
|
|
842
|
+
byKey?.delete(key);
|
|
843
|
+
if (byKey?.size === 0) byTarget?.delete(target);
|
|
844
|
+
if (byTarget?.size === 0) filings.delete(rootId);
|
|
845
|
+
}
|
|
846
|
+
function setFiled(rootId, target, key, rowIds, filterValues) {
|
|
847
|
+
const rows = new Set(rowIds);
|
|
848
|
+
const next = { rows, filters: new Set(filterValues ?? rows) };
|
|
849
|
+
const byKey = filedUnder(rootId, target);
|
|
850
|
+
const before = byKey.get(key);
|
|
851
|
+
const previous = before ?? { rows: /* @__PURE__ */ new Set(), filters: /* @__PURE__ */ new Set() };
|
|
852
|
+
byKey.set(key, next);
|
|
853
|
+
const released = swapRowRefs(target, previous.rows, next.rows);
|
|
854
|
+
if (swapFilterRefs(target, fieldFor(key), previous.filters, next.filters)) {
|
|
855
|
+
held.get(target)?.setFilters(filtersFor(target));
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
// Another root may file one of these rows again before the
|
|
859
|
+
// commit, so it releases only the rows still at zero then.
|
|
860
|
+
commit: () => {
|
|
861
|
+
const counts = rowRefs.get(target);
|
|
862
|
+
const unreferenced = released.filter((id) => !counts?.has(id));
|
|
863
|
+
if (unreferenced.length > 0) target.releaseFiled(unreferenced, holder);
|
|
864
|
+
},
|
|
865
|
+
// Only while this change is still the current filing: a later
|
|
866
|
+
// change owns the state after it.
|
|
867
|
+
undo: () => {
|
|
868
|
+
if (byKey.get(key) !== next) return;
|
|
869
|
+
setFiled(rootId, target, key, previous.rows, previous.filters).commit();
|
|
870
|
+
if (!before) dropEntry(rootId, target, key);
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function forgetParentRow(rootId) {
|
|
875
|
+
const byTarget = filings.get(rootId);
|
|
876
|
+
if (!byTarget) return;
|
|
877
|
+
for (const [target, byKey] of byTarget) {
|
|
878
|
+
for (const key of [...byKey.keys()]) setFiled(rootId, target, key, [], []).commit();
|
|
879
|
+
}
|
|
880
|
+
filings.delete(rootId);
|
|
881
|
+
}
|
|
882
|
+
function release(target) {
|
|
883
|
+
const hold2 = held.get(target);
|
|
884
|
+
if (!hold2) return;
|
|
885
|
+
held.delete(target);
|
|
886
|
+
try {
|
|
887
|
+
hold2.release();
|
|
888
|
+
} catch (error) {
|
|
889
|
+
logger.error("Failed to release relation target subscription", {
|
|
890
|
+
collectionName,
|
|
891
|
+
error
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function hold(target) {
|
|
896
|
+
if (held.has(target)) return;
|
|
897
|
+
try {
|
|
898
|
+
const hold2 = target.holdLive(holder);
|
|
899
|
+
held.set(target, hold2);
|
|
900
|
+
hold2.setFilters(filtersFor(target));
|
|
901
|
+
} catch (error) {
|
|
902
|
+
logger.error("Failed to hold relation target subscription", { collectionName, error });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function sync(desired) {
|
|
906
|
+
for (const target of [...held.keys()]) if (!desired.has(target)) release(target);
|
|
907
|
+
for (const target of desired) hold(target);
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
setFiled,
|
|
911
|
+
forgetParentRow,
|
|
912
|
+
sync,
|
|
913
|
+
releaseAll: () => sync(/* @__PURE__ */ new Set()),
|
|
914
|
+
clearFiled: () => {
|
|
915
|
+
filings.clear();
|
|
916
|
+
rowRefs.clear();
|
|
917
|
+
filterRefs.clear();
|
|
918
|
+
},
|
|
919
|
+
count: () => held.size
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// src/ledger.ts
|
|
924
|
+
var ACCEPTED = { holder: "accepted" };
|
|
925
|
+
var EAGER = { holder: "eager" };
|
|
926
|
+
function createLedger() {
|
|
927
|
+
const entries2 = /* @__PURE__ */ new Map();
|
|
928
|
+
const byHolder = /* @__PURE__ */ new Map();
|
|
929
|
+
let journal;
|
|
930
|
+
function link(id, entry, holder) {
|
|
931
|
+
if (entry.holders.has(holder)) return;
|
|
932
|
+
entry.holders.add(holder);
|
|
933
|
+
let ids = byHolder.get(holder);
|
|
934
|
+
if (!ids) {
|
|
935
|
+
ids = /* @__PURE__ */ new Set();
|
|
936
|
+
byHolder.set(holder, ids);
|
|
937
|
+
}
|
|
938
|
+
ids.add(id);
|
|
939
|
+
journal?.push(() => unlink(id, entry, holder));
|
|
940
|
+
}
|
|
941
|
+
function unlink(id, entry, holder) {
|
|
942
|
+
if (!entry.holders.delete(holder)) return;
|
|
943
|
+
const ids = byHolder.get(holder);
|
|
944
|
+
ids?.delete(id);
|
|
945
|
+
if (ids?.size === 0) byHolder.delete(holder);
|
|
946
|
+
journal?.push(() => link(id, entry, holder));
|
|
947
|
+
}
|
|
948
|
+
function put(id, entry) {
|
|
949
|
+
entries2.set(id, entry);
|
|
950
|
+
journal?.push(() => {
|
|
951
|
+
entries2.delete(id);
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
function remove(id, entry) {
|
|
955
|
+
entries2.delete(id);
|
|
956
|
+
journal?.push(() => {
|
|
957
|
+
entries2.set(id, entry);
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
function setRow(entry, row) {
|
|
961
|
+
const previous = entry.row;
|
|
962
|
+
entry.row = row;
|
|
963
|
+
journal?.push(() => {
|
|
964
|
+
entry.row = previous;
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
function isOlder(incoming, current) {
|
|
968
|
+
const next = updatedAtOf(incoming);
|
|
969
|
+
const now = updatedAtOf(current);
|
|
970
|
+
return next !== void 0 && now !== void 0 && next < now;
|
|
971
|
+
}
|
|
972
|
+
function update(entry, incoming, writes) {
|
|
973
|
+
if (entry.row === incoming || isOlder(incoming, entry.row)) return;
|
|
974
|
+
setRow(entry, { ...entry.row, ...incoming });
|
|
975
|
+
writes.updated.push(entry.row);
|
|
976
|
+
}
|
|
977
|
+
function retain(holder, rows) {
|
|
978
|
+
const writes = { inserted: [], updated: [] };
|
|
979
|
+
for (const incoming of rows) {
|
|
980
|
+
const id = idOf(incoming);
|
|
981
|
+
if (id === void 0) continue;
|
|
982
|
+
const entry = entries2.get(id);
|
|
983
|
+
if (!entry) {
|
|
984
|
+
const created = { row: { ...incoming }, holders: /* @__PURE__ */ new Set() };
|
|
985
|
+
put(id, created);
|
|
986
|
+
link(id, created, holder);
|
|
987
|
+
writes.inserted.push(created.row);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
link(id, entry, holder);
|
|
991
|
+
update(entry, incoming, writes);
|
|
992
|
+
}
|
|
993
|
+
return writes;
|
|
994
|
+
}
|
|
995
|
+
function refresh(rows) {
|
|
996
|
+
const writes = { inserted: [], updated: [] };
|
|
997
|
+
for (const incoming of rows) {
|
|
998
|
+
const id = idOf(incoming);
|
|
999
|
+
const entry = id === void 0 ? void 0 : entries2.get(id);
|
|
1000
|
+
if (entry) update(entry, incoming, writes);
|
|
1001
|
+
}
|
|
1002
|
+
return writes;
|
|
1003
|
+
}
|
|
1004
|
+
function release(holder, ids) {
|
|
1005
|
+
const held = byHolder.get(holder);
|
|
1006
|
+
if (!held) return [];
|
|
1007
|
+
const removed = [];
|
|
1008
|
+
for (const id of ids ? [...ids] : [...held]) {
|
|
1009
|
+
const entry = entries2.get(id);
|
|
1010
|
+
if (!entry || !held.has(id)) continue;
|
|
1011
|
+
unlink(id, entry, holder);
|
|
1012
|
+
if (entry.holders.size === 0) {
|
|
1013
|
+
remove(id, entry);
|
|
1014
|
+
removed.push(id);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return removed;
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
row: (id) => entries2.get(id)?.row,
|
|
1021
|
+
has: (id) => entries2.has(id),
|
|
1022
|
+
rows: function* rows() {
|
|
1023
|
+
for (const entry of entries2.values()) yield entry.row;
|
|
1024
|
+
},
|
|
1025
|
+
size: () => entries2.size,
|
|
1026
|
+
idsOf: (holder) => [...byHolder.get(holder) ?? []],
|
|
1027
|
+
holderCount: (id) => entries2.get(id)?.holders.size ?? 0,
|
|
1028
|
+
retain,
|
|
1029
|
+
refresh,
|
|
1030
|
+
release,
|
|
1031
|
+
replace(holder, rows) {
|
|
1032
|
+
const keep = new Set(idsOf(rows));
|
|
1033
|
+
const stale = [...byHolder.get(holder) ?? []].filter((id) => !keep.has(id));
|
|
1034
|
+
const writes = retain(holder, rows);
|
|
1035
|
+
return { ...writes, removed: release(holder, stale) };
|
|
1036
|
+
},
|
|
1037
|
+
hold(holder, ids) {
|
|
1038
|
+
for (const id of ids) {
|
|
1039
|
+
const entry = entries2.get(id);
|
|
1040
|
+
if (entry) link(id, entry, holder);
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
releaseAll(id) {
|
|
1044
|
+
const entry = entries2.get(id);
|
|
1045
|
+
if (!entry) return false;
|
|
1046
|
+
for (const holder of [...entry.holders]) unlink(id, entry, holder);
|
|
1047
|
+
remove(id, entry);
|
|
1048
|
+
return true;
|
|
1049
|
+
},
|
|
1050
|
+
holders: () => [...byHolder.keys()],
|
|
1051
|
+
track() {
|
|
1052
|
+
const undo = [];
|
|
1053
|
+
journal = undo;
|
|
1054
|
+
const stop = () => {
|
|
1055
|
+
if (journal === undo) journal = void 0;
|
|
1056
|
+
};
|
|
1057
|
+
return {
|
|
1058
|
+
commit() {
|
|
1059
|
+
stop();
|
|
1060
|
+
undo.length = 0;
|
|
1061
|
+
},
|
|
1062
|
+
rollback() {
|
|
1063
|
+
stop();
|
|
1064
|
+
for (const step of undo.reverse()) step();
|
|
1065
|
+
undo.length = 0;
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
},
|
|
1069
|
+
clear() {
|
|
1070
|
+
entries2.clear();
|
|
1071
|
+
byHolder.clear();
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/loaded-subsets.ts
|
|
1077
|
+
function createLoadedSubsets() {
|
|
1078
|
+
const marks = /* @__PURE__ */ new Map();
|
|
1079
|
+
function forgetValue(marked, value) {
|
|
1080
|
+
if (typeof value === "string") marked.delete(value);
|
|
1081
|
+
else if (Array.isArray(value)) {
|
|
1082
|
+
for (const item of value) if (typeof item === "string") marked.delete(item);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
mark(field, value) {
|
|
1087
|
+
let values = marks.get(field);
|
|
1088
|
+
if (!values) {
|
|
1089
|
+
values = /* @__PURE__ */ new Set();
|
|
1090
|
+
marks.set(field, values);
|
|
1091
|
+
}
|
|
1092
|
+
values.add(value);
|
|
1093
|
+
},
|
|
1094
|
+
count() {
|
|
1095
|
+
let count = 0;
|
|
1096
|
+
for (const values of marks.values()) count += values.size;
|
|
1097
|
+
return count;
|
|
1098
|
+
},
|
|
1099
|
+
isLoaded({ field, values }) {
|
|
1100
|
+
const marked = marks.get(field);
|
|
1101
|
+
return marked !== void 0 && values.every((value) => marked.has(value));
|
|
1102
|
+
},
|
|
1103
|
+
forgetRow(row) {
|
|
1104
|
+
if (!row || typeof row !== "object") return;
|
|
1105
|
+
for (const [field, marked] of marks) {
|
|
1106
|
+
forgetValue(marked, row[field]);
|
|
1107
|
+
}
|
|
1108
|
+
},
|
|
1109
|
+
clear() {
|
|
1110
|
+
marks.clear();
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// src/membership.ts
|
|
1116
|
+
var NO_WRITES = { inserted: [], updated: [] };
|
|
1117
|
+
function createMembership(deps) {
|
|
1118
|
+
const { ledger, store } = deps;
|
|
1119
|
+
function writesFor(rows, changes, removed) {
|
|
1120
|
+
const inserted = new Set(changes.inserted.map(idOf));
|
|
1121
|
+
const updated = new Set(changes.updated.map(idOf));
|
|
1122
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1123
|
+
const writes = [];
|
|
1124
|
+
for (const row of rows) {
|
|
1125
|
+
const id = idOf(row);
|
|
1126
|
+
if (id === void 0 || seen.has(id) || !(inserted.has(id) || updated.has(id))) continue;
|
|
1127
|
+
const stored = ledger.row(id);
|
|
1128
|
+
if (stored === void 0) continue;
|
|
1129
|
+
seen.add(id);
|
|
1130
|
+
writes.push({ type: inserted.has(id) ? "insert" : "update", value: stored });
|
|
1131
|
+
}
|
|
1132
|
+
for (const key of removed) writes.push({ type: "delete", key });
|
|
1133
|
+
return writes;
|
|
1134
|
+
}
|
|
1135
|
+
function rowsBefore(ids) {
|
|
1136
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1137
|
+
for (const id of ids) {
|
|
1138
|
+
const row = ledger.row(id);
|
|
1139
|
+
if (row) rows.set(id, row);
|
|
1140
|
+
}
|
|
1141
|
+
return rows;
|
|
1142
|
+
}
|
|
1143
|
+
function pick(rows, ids) {
|
|
1144
|
+
return ids.map((id) => rows.get(id)).filter((row) => row !== void 0);
|
|
1145
|
+
}
|
|
1146
|
+
function releaseKept(keeper, holders, ids) {
|
|
1147
|
+
const kept = new Set(ledger.idsOf(keeper));
|
|
1148
|
+
const covered = [...ids].filter((id) => kept.has(id));
|
|
1149
|
+
for (const holder of holders) {
|
|
1150
|
+
const removed = ledger.release(holder, covered);
|
|
1151
|
+
if (removed.length > 0) {
|
|
1152
|
+
throw new Error(
|
|
1153
|
+
`${deps.collectionName}: releasing a kept row removed ${removed.join(", ")}`
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
function apply(change) {
|
|
1159
|
+
if (!store.isAttached()) return false;
|
|
1160
|
+
const tracked = ledger.track();
|
|
1161
|
+
let receipt;
|
|
1162
|
+
let gone;
|
|
1163
|
+
try {
|
|
1164
|
+
const result = change();
|
|
1165
|
+
gone = result.gone;
|
|
1166
|
+
receipt = store.transact(result.writes);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
tracked.rollback();
|
|
1169
|
+
throw error;
|
|
1170
|
+
}
|
|
1171
|
+
tracked.commit();
|
|
1172
|
+
if (gone.length > 0) deps.onRemoved(gone);
|
|
1173
|
+
return receipt;
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
land: (holder, rows) => apply(() => ({ writes: writesFor(rows, ledger.retain(holder, rows), []), gone: [] })),
|
|
1177
|
+
accept: (rows) => apply(() => {
|
|
1178
|
+
const held = new Set(idsOf(rows).filter((id) => ledger.has(id)));
|
|
1179
|
+
const fresh = rows.filter((row) => !held.has(idOf(row) ?? ""));
|
|
1180
|
+
const stale = rows.filter((row) => held.has(idOf(row) ?? ""));
|
|
1181
|
+
const retained = ledger.retain(ACCEPTED, fresh);
|
|
1182
|
+
const refreshed = ledger.refresh(stale);
|
|
1183
|
+
const changes = {
|
|
1184
|
+
inserted: [...retained.inserted, ...refreshed.inserted],
|
|
1185
|
+
updated: [...retained.updated, ...refreshed.updated]
|
|
1186
|
+
};
|
|
1187
|
+
return { writes: writesFor(rows, changes, []), gone: [] };
|
|
1188
|
+
}),
|
|
1189
|
+
reconcile: (holder, rows, releaseAlso = []) => apply(() => {
|
|
1190
|
+
const before = rowsBefore(ledger.idsOf(holder));
|
|
1191
|
+
const { removed, ...changes } = ledger.replace(holder, rows);
|
|
1192
|
+
releaseKept(holder, releaseAlso, idsOf(rows));
|
|
1193
|
+
return { writes: writesFor(rows, changes, removed), gone: pick(before, removed) };
|
|
1194
|
+
}),
|
|
1195
|
+
confirm: (keeper, holders, ids) => apply(() => {
|
|
1196
|
+
releaseKept(keeper, holders, ids);
|
|
1197
|
+
return { writes: [], gone: [] };
|
|
1198
|
+
}),
|
|
1199
|
+
drop: (holders, ids) => apply(() => {
|
|
1200
|
+
const listed = ids ? [...ids] : void 0;
|
|
1201
|
+
const removed = [];
|
|
1202
|
+
const gone = [];
|
|
1203
|
+
for (const holder of holders) {
|
|
1204
|
+
const held = listed ?? ledger.idsOf(holder);
|
|
1205
|
+
const before = rowsBefore(held);
|
|
1206
|
+
const released = ledger.release(holder, held);
|
|
1207
|
+
removed.push(...released);
|
|
1208
|
+
gone.push(...pick(before, released));
|
|
1209
|
+
}
|
|
1210
|
+
return { writes: writesFor([], NO_WRITES, removed), gone };
|
|
1211
|
+
}),
|
|
1212
|
+
hold: (holders, ids) => apply(() => {
|
|
1213
|
+
const listed = [...ids];
|
|
1214
|
+
for (const holder of holders) ledger.hold(holder, listed);
|
|
1215
|
+
return { writes: [], gone: [] };
|
|
1216
|
+
}),
|
|
1217
|
+
dropAll: (ids) => apply(() => {
|
|
1218
|
+
const present = [...new Set(ids)].filter((id) => ledger.has(id));
|
|
1219
|
+
const gone = pick(rowsBefore(present), present);
|
|
1220
|
+
for (const id of present) ledger.releaseAll(id);
|
|
1221
|
+
return { writes: writesFor([], NO_WRITES, present), gone };
|
|
1222
|
+
})
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/filter-refs.ts
|
|
1227
|
+
var RefCounter = class {
|
|
1228
|
+
constructor() {
|
|
1229
|
+
this.counts = /* @__PURE__ */ new Map();
|
|
1230
|
+
}
|
|
1231
|
+
add(key) {
|
|
1232
|
+
this.counts.set(key, (this.counts.get(key) ?? 0) + 1);
|
|
1233
|
+
}
|
|
1234
|
+
/** Drops one reference. Returns true when none remain. */
|
|
1235
|
+
drop(key) {
|
|
1236
|
+
const count = (this.counts.get(key) ?? 0) - 1;
|
|
1237
|
+
if (count > 0) {
|
|
1238
|
+
this.counts.set(key, count);
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
this.counts.delete(key);
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
has(key) {
|
|
1245
|
+
return this.counts.has(key);
|
|
1246
|
+
}
|
|
1247
|
+
keys() {
|
|
1248
|
+
return this.counts.keys();
|
|
1249
|
+
}
|
|
1250
|
+
clear() {
|
|
1251
|
+
this.counts.clear();
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
// src/realtime-subscription.ts
|
|
1256
|
+
function createRealtimeSubscription(deps) {
|
|
1257
|
+
const { collectionName } = deps;
|
|
1258
|
+
let starUnsubscribe = null;
|
|
1259
|
+
let starExpand;
|
|
1260
|
+
const filterEntries = /* @__PURE__ */ new Map();
|
|
1261
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
1262
|
+
let collectionSubscribers = 0;
|
|
1263
|
+
let unfilteredQueryRefs = 0;
|
|
1264
|
+
const queryRefs = new RefCounter();
|
|
1265
|
+
const holdRefs = new RefCounter();
|
|
1266
|
+
const oversizedFilters = /* @__PURE__ */ new Set();
|
|
1267
|
+
const delivered = /* @__PURE__ */ new Set();
|
|
1268
|
+
const isStarOpen = () => starUnsubscribe !== null;
|
|
1269
|
+
const isOpen = () => isStarOpen() || filterEntries.size > 0;
|
|
1270
|
+
const wantedFilters = () => /* @__PURE__ */ new Set([...queryRefs.keys(), ...holdRefs.keys()]);
|
|
1271
|
+
const wantsStar = () => collectionSubscribers > 0 || unfilteredQueryRefs > 0 || oversizedFilters.size > 0;
|
|
1272
|
+
function dropRef(refs, filter) {
|
|
1273
|
+
if (refs.drop(filter) && !queryRefs.has(filter) && !holdRefs.has(filter)) {
|
|
1274
|
+
oversizedFilters.delete(filter);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function notifyOpened() {
|
|
1278
|
+
deps.onEntryOpened();
|
|
1279
|
+
for (const resolve of waiters) resolve();
|
|
1280
|
+
waiters.clear();
|
|
1281
|
+
}
|
|
1282
|
+
let work = Promise.resolve();
|
|
1283
|
+
function enqueue(fn) {
|
|
1284
|
+
work = work.then(fn, fn).catch((error) => {
|
|
1285
|
+
logger.error("Subscription work failed", { collectionName, error });
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
async function openStar() {
|
|
1289
|
+
if (isStarOpen()) return;
|
|
1290
|
+
const expand = deps.pendingExpand();
|
|
1291
|
+
try {
|
|
1292
|
+
starUnsubscribe = await deps.transport.subscribe(
|
|
1293
|
+
collectionName,
|
|
1294
|
+
deps.subscribeOptions(),
|
|
1295
|
+
(event) => deps.handleEvent(event, "*", expand)
|
|
1296
|
+
);
|
|
1297
|
+
delivered.add("*");
|
|
1298
|
+
starExpand = expand;
|
|
1299
|
+
logger.debug("Subscription started", { collectionName });
|
|
1300
|
+
notifyOpened();
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
logger.error("Failed to start subscription", { collectionName, error });
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
async function closeStar() {
|
|
1306
|
+
const unsubscribe = starUnsubscribe;
|
|
1307
|
+
if (!unsubscribe) return;
|
|
1308
|
+
starUnsubscribe = null;
|
|
1309
|
+
starExpand = void 0;
|
|
1310
|
+
try {
|
|
1311
|
+
await unsubscribe();
|
|
1312
|
+
logger.debug("Subscription stopped", { collectionName });
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
logger.debug("Unsubscribe failed (expected if connection closed)", {
|
|
1315
|
+
collectionName,
|
|
1316
|
+
error
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function filteredOptions(filter) {
|
|
1321
|
+
const base = deps.subscribeOptions();
|
|
1322
|
+
const combined = base?.filter ? `(${base.filter}) && (${filter})` : filter;
|
|
1323
|
+
return { ...base, filter: combined };
|
|
1324
|
+
}
|
|
1325
|
+
async function openFilter(filter) {
|
|
1326
|
+
if (filterEntries.has(filter)) return;
|
|
1327
|
+
const options = filteredOptions(filter);
|
|
1328
|
+
const expand = deps.pendingExpand();
|
|
1329
|
+
try {
|
|
1330
|
+
const unsubscribe = await deps.transport.subscribe(
|
|
1331
|
+
collectionName,
|
|
1332
|
+
options,
|
|
1333
|
+
(event) => deps.handleEvent(event, filter, expand)
|
|
1334
|
+
);
|
|
1335
|
+
delivered.add(filter);
|
|
1336
|
+
filterEntries.set(filter, { unsubscribe, expand });
|
|
1337
|
+
logger.debug("Filtered subscription started", { collectionName, filter });
|
|
1338
|
+
notifyOpened();
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
logger.error("Failed to start filtered subscription", {
|
|
1341
|
+
collectionName,
|
|
1342
|
+
filter,
|
|
1343
|
+
error
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
async function closeFilter(filter) {
|
|
1348
|
+
const entry = filterEntries.get(filter);
|
|
1349
|
+
if (!entry) return;
|
|
1350
|
+
filterEntries.delete(filter);
|
|
1351
|
+
try {
|
|
1352
|
+
await entry.unsubscribe();
|
|
1353
|
+
logger.debug("Filtered subscription stopped", { collectionName, filter });
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
logger.debug("Unsubscribe failed (expected if connection closed)", {
|
|
1356
|
+
collectionName,
|
|
1357
|
+
filter,
|
|
1358
|
+
error
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
async function closeFilters(keep = () => false) {
|
|
1363
|
+
await Promise.all([...filterEntries.keys()].filter((f) => !keep(f)).map(closeFilter));
|
|
1364
|
+
}
|
|
1365
|
+
function releaseClosedTopics() {
|
|
1366
|
+
const wanted = wantedFilters();
|
|
1367
|
+
for (const topic of delivered) {
|
|
1368
|
+
const live = topic === "*" ? isStarOpen() || wantsStar() : filterEntries.has(topic) || wanted.has(topic);
|
|
1369
|
+
if (live) continue;
|
|
1370
|
+
delivered.delete(topic);
|
|
1371
|
+
deps.onEntryClosed(topic);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
async function openWantedFilters() {
|
|
1375
|
+
if (wantsStar()) return;
|
|
1376
|
+
const wanted = [...wantedFilters()];
|
|
1377
|
+
for (const filter of wanted) {
|
|
1378
|
+
if (filterEntries.has(filter) || oversizedFilters.has(filter)) continue;
|
|
1379
|
+
const topicLength = realtimeTopicLength(collectionName, filteredOptions(filter));
|
|
1380
|
+
if (topicLength > REALTIME_TOPIC_MAX_LENGTH) {
|
|
1381
|
+
oversizedFilters.add(filter);
|
|
1382
|
+
logger.warn("Realtime filter too long; subscribing to the whole collection", {
|
|
1383
|
+
collectionName,
|
|
1384
|
+
filterLength: filter.length,
|
|
1385
|
+
topicLength
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (wantsStar()) return;
|
|
1390
|
+
await Promise.all(wanted.map(openFilter));
|
|
1391
|
+
}
|
|
1392
|
+
function expandDrifted() {
|
|
1393
|
+
const pending = deps.pendingExpand();
|
|
1394
|
+
if (isStarOpen() && starExpand !== pending) return true;
|
|
1395
|
+
for (const entry of filterEntries.values()) if (entry.expand !== pending) return true;
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
async function doReconcile() {
|
|
1399
|
+
await openWantedFilters();
|
|
1400
|
+
if (wantsStar()) {
|
|
1401
|
+
await openStar();
|
|
1402
|
+
if (isStarOpen()) await closeFilters();
|
|
1403
|
+
} else {
|
|
1404
|
+
await closeStar();
|
|
1405
|
+
const wanted = wantedFilters();
|
|
1406
|
+
await closeFilters((filter) => wanted.has(filter));
|
|
1407
|
+
}
|
|
1408
|
+
releaseClosedTopics();
|
|
1409
|
+
if (!isOpen() && !wantsStar() && wantedFilters().size === 0) deps.onIdle();
|
|
1410
|
+
if (expandDrifted()) restart();
|
|
1411
|
+
}
|
|
1412
|
+
async function doRestart() {
|
|
1413
|
+
if (!isOpen()) return;
|
|
1414
|
+
await closeStar();
|
|
1415
|
+
await closeFilters();
|
|
1416
|
+
await doReconcile();
|
|
1417
|
+
}
|
|
1418
|
+
const reconcile = () => enqueue(doReconcile);
|
|
1419
|
+
const restart = () => enqueue(doRestart);
|
|
1420
|
+
return {
|
|
1421
|
+
addCollectionSubscriber() {
|
|
1422
|
+
collectionSubscribers += 1;
|
|
1423
|
+
},
|
|
1424
|
+
dropCollectionSubscriber() {
|
|
1425
|
+
collectionSubscribers -= 1;
|
|
1426
|
+
},
|
|
1427
|
+
retainQueryFilters(filters) {
|
|
1428
|
+
if (!filters) unfilteredQueryRefs += 1;
|
|
1429
|
+
else for (const filter of filters) queryRefs.add(filter);
|
|
1430
|
+
reconcile();
|
|
1431
|
+
},
|
|
1432
|
+
releaseQueryFilters(filters) {
|
|
1433
|
+
if (!filters) unfilteredQueryRefs -= 1;
|
|
1434
|
+
else for (const filter of filters) dropRef(queryRefs, filter);
|
|
1435
|
+
reconcile();
|
|
1436
|
+
},
|
|
1437
|
+
resetQueryFilters() {
|
|
1438
|
+
queryRefs.clear();
|
|
1439
|
+
unfilteredQueryRefs = 0;
|
|
1440
|
+
for (const filter of oversizedFilters) {
|
|
1441
|
+
if (!holdRefs.has(filter)) oversizedFilters.delete(filter);
|
|
1442
|
+
}
|
|
1443
|
+
reconcile();
|
|
1444
|
+
},
|
|
1445
|
+
swapHoldFilters(previous, next) {
|
|
1446
|
+
for (const filter of next) if (!previous.includes(filter)) holdRefs.add(filter);
|
|
1447
|
+
for (const filter of previous) if (!next.includes(filter)) dropRef(holdRefs, filter);
|
|
1448
|
+
reconcile();
|
|
1449
|
+
},
|
|
1450
|
+
reconcile,
|
|
1451
|
+
restart,
|
|
1452
|
+
isOpen,
|
|
1453
|
+
wait(timeout = 5e3) {
|
|
1454
|
+
if (isOpen()) return Promise.resolve();
|
|
1455
|
+
return new Promise((resolve, reject) => {
|
|
1456
|
+
const onOpen = () => {
|
|
1457
|
+
clearTimeout(timer);
|
|
1458
|
+
resolve();
|
|
1459
|
+
};
|
|
1460
|
+
const timer = setTimeout(() => {
|
|
1461
|
+
waiters.delete(onOpen);
|
|
1462
|
+
reject(new Error("Subscription timeout"));
|
|
1463
|
+
}, timeout);
|
|
1464
|
+
waiters.add(onOpen);
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// src/realtime-where.ts
|
|
1471
|
+
function subscriptionWhere(opts) {
|
|
1472
|
+
const sub = opts.subscription;
|
|
1473
|
+
if (!sub?.options || !("whereExpression" in sub.options)) return { known: false };
|
|
1474
|
+
return { known: true, where: sub.options.whereExpression };
|
|
1475
|
+
}
|
|
1476
|
+
function chunkExtra(where, subWhere) {
|
|
1477
|
+
if (where === subWhere) return void 0;
|
|
1478
|
+
if (subWhere === void 0) return where;
|
|
1479
|
+
const node = where;
|
|
1480
|
+
if (node?.type === "func" && node.name === "and" && node.args?.length === 2 && node.args[0] === subWhere) {
|
|
1481
|
+
return node.args[1];
|
|
1482
|
+
}
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
function realtimeWhereFor(opts) {
|
|
1486
|
+
if (!opts.subscription) return opts.where;
|
|
1487
|
+
const sub = subscriptionWhere(opts);
|
|
1488
|
+
if (!sub.known) return opts.where;
|
|
1489
|
+
const extra = chunkExtra(opts.where, sub.where);
|
|
1490
|
+
if (extra === null) return opts.where;
|
|
1491
|
+
if (extra === void 0) return sub.where;
|
|
1492
|
+
const node = extra;
|
|
1493
|
+
return node?.type === "func" && node.name === "in" ? opts.where : sub.where;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// src/request.ts
|
|
1497
|
+
function toRequest(opts) {
|
|
1498
|
+
const request = {};
|
|
1499
|
+
const subset = subsetFromWhere(opts?.where);
|
|
1500
|
+
const filter = subset ? void 0 : convertToPocketBaseFilter(opts?.where);
|
|
1501
|
+
const sort = convertToPocketBaseSort(opts?.orderBy);
|
|
1502
|
+
const expand = joinPaths(opts?.expand ?? []);
|
|
1503
|
+
const cursor = convertToPocketBaseFilter(opts?.cursor?.whereFrom);
|
|
1504
|
+
if (subset) request.subset = subset;
|
|
1505
|
+
if (filter) request.filter = filter;
|
|
1506
|
+
if (sort) request.sort = sort;
|
|
1507
|
+
if (opts?.limit) request.limit = opts.limit;
|
|
1508
|
+
if (expand) request.expand = expand;
|
|
1509
|
+
if (cursor) request.cursor = cursor;
|
|
1510
|
+
else if (opts?.offset) request.offset = opts.offset;
|
|
1511
|
+
return request;
|
|
1512
|
+
}
|
|
1513
|
+
function realtimeFiltersFor(request) {
|
|
1514
|
+
if (request.subset) return subsetFilters(request.subset, REALTIME_MAX_FILTER_LENGTH);
|
|
1515
|
+
return request.filter ? [request.filter] : void 0;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/sync-adapter.ts
|
|
1519
|
+
function createSyncAdapter(deps) {
|
|
1520
|
+
const { collectionName, syncMode, ledger, store, membership: membership2, fetcher, subsets, realtime } = deps;
|
|
1521
|
+
const { subsetGcTime, loadRetryDelays } = deps;
|
|
1522
|
+
let current;
|
|
1523
|
+
const tombstones = /* @__PURE__ */ new Map();
|
|
1524
|
+
function demandFor(opts) {
|
|
1525
|
+
const request = toRequest(deps.registry().withViewExpand(opts));
|
|
1526
|
+
const mode = deps.registry().tagFor(opts)?.realtime ?? deps.realtimeMode;
|
|
1527
|
+
const base = {
|
|
1528
|
+
request,
|
|
1529
|
+
seq: 0,
|
|
1530
|
+
loading: void 0,
|
|
1531
|
+
parked: void 0,
|
|
1532
|
+
landed: false,
|
|
1533
|
+
wake: void 0
|
|
1534
|
+
};
|
|
1535
|
+
if (mode !== "query") return { ...base, filters: void 0, counted: false };
|
|
1536
|
+
const filters = realtimeFiltersFor(toRequest({ where: realtimeWhereFor(opts) }));
|
|
1537
|
+
return { ...base, filters, counted: true };
|
|
1538
|
+
}
|
|
1539
|
+
function keyOf(demand) {
|
|
1540
|
+
return JSON.stringify(
|
|
1541
|
+
[demand.request, demand.filters],
|
|
1542
|
+
(_key, value) => value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
async function settle(applied, wait) {
|
|
1546
|
+
if (applied === true || applied === false) return;
|
|
1547
|
+
const receipt = wait === "accepted" ? whenSyncAccepted(applied) : applied;
|
|
1548
|
+
if (receipt !== true) await receipt;
|
|
1549
|
+
}
|
|
1550
|
+
async function fetchRows(run, request, signal, refetch, filters) {
|
|
1551
|
+
const deleted = /* @__PURE__ */ new Set();
|
|
1552
|
+
tombstones.set(deleted, filters);
|
|
1553
|
+
try {
|
|
1554
|
+
const { rows, fromStore, filings } = await fetcher.fetchRecords(request, {
|
|
1555
|
+
signals: signal ? [run.abort.signal, signal] : [run.abort.signal],
|
|
1556
|
+
refetch
|
|
1557
|
+
});
|
|
1558
|
+
return {
|
|
1559
|
+
rows: rows.filter((row) => !deleted.has(idOf(row) ?? "")),
|
|
1560
|
+
fromStore,
|
|
1561
|
+
filings
|
|
1562
|
+
};
|
|
1563
|
+
} finally {
|
|
1564
|
+
tombstones.delete(deleted);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
function isStopped(run, opts) {
|
|
1568
|
+
return run.abort.signal.aborted || opts.signal?.aborted === true;
|
|
1569
|
+
}
|
|
1570
|
+
function stopped(run, opts, wait) {
|
|
1571
|
+
if (!isStopped(run, opts)) return false;
|
|
1572
|
+
if (wait === "visible") throw new LoadSubsetOperationAbortedError();
|
|
1573
|
+
return true;
|
|
1574
|
+
}
|
|
1575
|
+
function matcherFor(where, unknown) {
|
|
1576
|
+
if (where === void 0) return () => true;
|
|
1577
|
+
let evaluate;
|
|
1578
|
+
try {
|
|
1579
|
+
evaluate = compileSingleRowExpression(where);
|
|
1580
|
+
} catch {
|
|
1581
|
+
return () => unknown;
|
|
1582
|
+
}
|
|
1583
|
+
return (row) => {
|
|
1584
|
+
try {
|
|
1585
|
+
return toBooleanPredicate(evaluate(row));
|
|
1586
|
+
} catch {
|
|
1587
|
+
return unknown;
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
function uncoveredRows(demand) {
|
|
1592
|
+
const holders = [ACCEPTED, ...demand.filters ?? []];
|
|
1593
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1594
|
+
for (const holder of holders) {
|
|
1595
|
+
for (const id of ledger.idsOf(holder)) {
|
|
1596
|
+
const stored = ledger.row(id);
|
|
1597
|
+
if (stored) rows.set(id, stored);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
return { holders, rows };
|
|
1601
|
+
}
|
|
1602
|
+
function releaseOmitted(opts, before, ids) {
|
|
1603
|
+
if (opts.limit !== void 0 || opts.cursor || opts.offset !== void 0) return true;
|
|
1604
|
+
const returned = new Set(ids);
|
|
1605
|
+
const matches = matcherFor(opts.where, false);
|
|
1606
|
+
const omitted = [...before.rows].filter(([id, stored]) => !returned.has(id) && ledger.row(id) === stored).filter(([, stored]) => matches(stored)).map(([id]) => id);
|
|
1607
|
+
return omitted.length > 0 ? membership2.drop(before.holders, omitted) : true;
|
|
1608
|
+
}
|
|
1609
|
+
async function install(run, opts, wait, before, result) {
|
|
1610
|
+
const applied = membership2.reconcile(opts, result.rows);
|
|
1611
|
+
if (applied === false) {
|
|
1612
|
+
result.filings.undo();
|
|
1613
|
+
return [];
|
|
1614
|
+
}
|
|
1615
|
+
result.filings.commit();
|
|
1616
|
+
const demand = run.demands.get(opts);
|
|
1617
|
+
if (demand) demand.landed = true;
|
|
1618
|
+
const ids = idsOf(result.rows);
|
|
1619
|
+
const released = result.fromStore ? true : releaseOmitted(opts, before, ids);
|
|
1620
|
+
await settle(applied, wait);
|
|
1621
|
+
await settle(released, wait);
|
|
1622
|
+
if (!stopped(run, opts, wait)) membership2.confirm(opts, [ACCEPTED], ids);
|
|
1623
|
+
return ids;
|
|
1624
|
+
}
|
|
1625
|
+
function superseded(run, opts, demand, seq) {
|
|
1626
|
+
return isStopped(run, opts) || run.demands.get(opts) !== demand || demand.seq !== seq;
|
|
1627
|
+
}
|
|
1628
|
+
function statusOf(error) {
|
|
1629
|
+
const status = error?.status;
|
|
1630
|
+
return typeof status === "number" ? status : void 0;
|
|
1631
|
+
}
|
|
1632
|
+
function isRetryable(error) {
|
|
1633
|
+
const status = statusOf(error);
|
|
1634
|
+
if (status === void 0 || status < 400 || status >= 500) return true;
|
|
1635
|
+
return status === 401 || status === 408 || status === 429;
|
|
1636
|
+
}
|
|
1637
|
+
function retryDelay(attempt, error) {
|
|
1638
|
+
if (statusOf(error) === 401) return void 0;
|
|
1639
|
+
const base = loadRetryDelays[Math.min(attempt, loadRetryDelays.length - 1)];
|
|
1640
|
+
return base === void 0 ? void 0 : Math.round(base * (0.75 + Math.random() * 0.5));
|
|
1641
|
+
}
|
|
1642
|
+
function sleep(ms, onWake) {
|
|
1643
|
+
return new Promise((resolve) => {
|
|
1644
|
+
const timer = ms === void 0 ? void 0 : setTimeout(resolve, ms);
|
|
1645
|
+
onWake(() => {
|
|
1646
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1647
|
+
resolve();
|
|
1648
|
+
});
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
function wakeRetries(run) {
|
|
1652
|
+
run.eagerWake?.();
|
|
1653
|
+
for (const demand of run.demands.values()) demand.wake?.();
|
|
1654
|
+
}
|
|
1655
|
+
async function retryAllowed(run, opts, demand, seq, attempt, error) {
|
|
1656
|
+
if (superseded(run, opts, demand, seq)) return false;
|
|
1657
|
+
if (loadRetryDelays.length === 0 || !isRetryable(error)) throw error;
|
|
1658
|
+
const delay = retryDelay(attempt, error);
|
|
1659
|
+
logger.warn("Subset load failed; retrying", { collectionName, attempt, delay, error });
|
|
1660
|
+
await sleep(delay, (wake) => {
|
|
1661
|
+
demand.wake = wake;
|
|
1662
|
+
});
|
|
1663
|
+
demand.wake = void 0;
|
|
1664
|
+
return !superseded(run, opts, demand, seq);
|
|
1665
|
+
}
|
|
1666
|
+
async function fetchWithRetry(run, opts, demand, seq, refetch) {
|
|
1667
|
+
for (let attempt = 0; ; attempt++) {
|
|
1668
|
+
try {
|
|
1669
|
+
return await fetchRows(run, demand.request, opts.signal, refetch, demand.filters);
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
if (!await retryAllowed(run, opts, demand, seq, attempt, error)) return void 0;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
function load(run, opts, demand, wait, refetch) {
|
|
1676
|
+
demand.wake?.();
|
|
1677
|
+
const seq = ++demand.seq;
|
|
1678
|
+
const before = uncoveredRows(demand);
|
|
1679
|
+
const loading = (async () => {
|
|
1680
|
+
const result = await fetchWithRetry(run, opts, demand, seq, refetch);
|
|
1681
|
+
if (result !== void 0 && !superseded(run, opts, demand, seq)) {
|
|
1682
|
+
return install(run, opts, wait, before, result);
|
|
1683
|
+
}
|
|
1684
|
+
result?.filings.undo();
|
|
1685
|
+
if (demand.seq !== seq) return demand.loading ?? [];
|
|
1686
|
+
stopped(run, opts, wait);
|
|
1687
|
+
return [];
|
|
1688
|
+
})();
|
|
1689
|
+
demand.loading = loading;
|
|
1690
|
+
return loading;
|
|
1691
|
+
}
|
|
1692
|
+
function expire(run, opts) {
|
|
1693
|
+
const demand = run.demands.get(opts);
|
|
1694
|
+
if (!demand) return;
|
|
1695
|
+
if (demand.parked !== void 0) clearTimeout(demand.parked);
|
|
1696
|
+
demand.wake?.();
|
|
1697
|
+
run.demands.delete(opts);
|
|
1698
|
+
if (demand.counted) realtime.releaseQueryFilters(demand.filters);
|
|
1699
|
+
membership2.drop([opts]);
|
|
1700
|
+
}
|
|
1701
|
+
function unloadSubset(run, opts) {
|
|
1702
|
+
try {
|
|
1703
|
+
const demand = run.demands.get(opts);
|
|
1704
|
+
if (!demand) return;
|
|
1705
|
+
if (subsetGcTime <= 0 || demand.parked !== void 0 || !demand.landed) {
|
|
1706
|
+
expire(run, opts);
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
demand.parked = setTimeout(() => {
|
|
1710
|
+
if (current === run) expire(run, opts);
|
|
1711
|
+
}, subsetGcTime);
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
logger.error("unloadSubset failed", { collectionName, error });
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
function expireParked(run) {
|
|
1717
|
+
for (const [opts, demand] of [...run.demands]) {
|
|
1718
|
+
if (demand.parked !== void 0) expire(run, opts);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
function unpark(run, demand) {
|
|
1722
|
+
const key = keyOf(demand);
|
|
1723
|
+
for (const [opts, parked] of run.demands) {
|
|
1724
|
+
if (parked.parked === void 0 || keyOf(parked) !== key) continue;
|
|
1725
|
+
clearTimeout(parked.parked);
|
|
1726
|
+
run.demands.delete(opts);
|
|
1727
|
+
return opts;
|
|
1728
|
+
}
|
|
1729
|
+
return void 0;
|
|
1730
|
+
}
|
|
1731
|
+
function serve(opts, demand) {
|
|
1732
|
+
if (opts.refetch) return false;
|
|
1733
|
+
const served = fetcher.serveFromStore(demand.request);
|
|
1734
|
+
if (!served) return false;
|
|
1735
|
+
demand.seq += 1;
|
|
1736
|
+
demand.loading = void 0;
|
|
1737
|
+
demand.landed = true;
|
|
1738
|
+
return membership2.reconcile(opts, served, [ACCEPTED]);
|
|
1739
|
+
}
|
|
1740
|
+
function adopt(opts, parkedOpts) {
|
|
1741
|
+
const rows = [];
|
|
1742
|
+
for (const id of ledger.idsOf(parkedOpts)) {
|
|
1743
|
+
const row = ledger.row(id);
|
|
1744
|
+
if (row) rows.push(row);
|
|
1745
|
+
}
|
|
1746
|
+
return membership2.reconcile(opts, rows, [ACCEPTED, parkedOpts]);
|
|
1747
|
+
}
|
|
1748
|
+
function startFromHeld(opts, demand, parkedOpts) {
|
|
1749
|
+
if (parkedOpts === void 0) return serve(opts, demand);
|
|
1750
|
+
const adopted = adopt(opts, parkedOpts);
|
|
1751
|
+
demand.landed = true;
|
|
1752
|
+
return opts.refetch === true ? false : adopted;
|
|
1753
|
+
}
|
|
1754
|
+
function refetchAdopted(run, opts, demand) {
|
|
1755
|
+
void load(run, opts, demand, "accepted", true).catch(
|
|
1756
|
+
(error) => logger.error("Refetch after adopting a parked subset failed", { collectionName, error })
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
function startHeld(run, opts, demand, parkedOpts) {
|
|
1760
|
+
try {
|
|
1761
|
+
const applied = startFromHeld(opts, demand, parkedOpts);
|
|
1762
|
+
if (applied !== false && parkedOpts !== void 0 && reloading) {
|
|
1763
|
+
refetchAdopted(run, opts, demand);
|
|
1764
|
+
}
|
|
1765
|
+
return applied;
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
expire(run, opts);
|
|
1768
|
+
throw error;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function loadSubsetIn(run) {
|
|
1772
|
+
return (opts) => {
|
|
1773
|
+
if (run.demands.has(opts)) expire(run, opts);
|
|
1774
|
+
const demand = demandFor(opts);
|
|
1775
|
+
const parkedOpts = unpark(run, demand);
|
|
1776
|
+
run.demands.set(opts, demand);
|
|
1777
|
+
if (parkedOpts === void 0 && demand.counted) {
|
|
1778
|
+
realtime.retainQueryFilters(demand.filters);
|
|
1779
|
+
}
|
|
1780
|
+
const applied = startHeld(run, opts, demand, parkedOpts);
|
|
1781
|
+
if (applied !== false) return applied;
|
|
1782
|
+
return load(run, opts, demand, "visible", opts.refetch === true).then(() => void 0);
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
async function fetchEagerWithRetry(run, seq) {
|
|
1786
|
+
for (let attempt = 0; ; attempt++) {
|
|
1787
|
+
try {
|
|
1788
|
+
return await fetchRows(run, {}, void 0, false, void 0);
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
const stale = () => run.abort.signal.aborted || run.eagerSeq !== seq;
|
|
1791
|
+
if (loadRetryDelays.length === 0 || !isRetryable(error) || stale()) throw error;
|
|
1792
|
+
const delay = retryDelay(attempt, error);
|
|
1793
|
+
logger.warn("Eager load failed; retrying", {
|
|
1794
|
+
collectionName,
|
|
1795
|
+
attempt,
|
|
1796
|
+
delay,
|
|
1797
|
+
error
|
|
1798
|
+
});
|
|
1799
|
+
await sleep(delay, (wake) => {
|
|
1800
|
+
run.eagerWake = wake;
|
|
1801
|
+
});
|
|
1802
|
+
run.eagerWake = void 0;
|
|
1803
|
+
if (stale()) throw error;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
function loadEagerRows(run) {
|
|
1808
|
+
run.eagerWake?.();
|
|
1809
|
+
const seq = ++run.eagerSeq;
|
|
1810
|
+
const loading = (async () => {
|
|
1811
|
+
const { rows, filings } = await fetchEagerWithRetry(run, seq);
|
|
1812
|
+
if (run.abort.signal.aborted || run.eagerSeq !== seq) {
|
|
1813
|
+
filings.undo();
|
|
1814
|
+
return run.eagerSeq !== seq ? run.eagerLoad ?? [] : [];
|
|
1815
|
+
}
|
|
1816
|
+
const applied = membership2.reconcile(EAGER, rows, [ACCEPTED]);
|
|
1817
|
+
if (applied === false) {
|
|
1818
|
+
filings.undo();
|
|
1819
|
+
return [];
|
|
1820
|
+
}
|
|
1821
|
+
filings.commit();
|
|
1822
|
+
await settle(applied, "accepted");
|
|
1823
|
+
return idsOf(rows);
|
|
1824
|
+
})();
|
|
1825
|
+
run.eagerLoad = loading;
|
|
1826
|
+
return loading;
|
|
1827
|
+
}
|
|
1828
|
+
async function startEager(run, markReady, markError) {
|
|
1829
|
+
try {
|
|
1830
|
+
await loadEagerRows(run);
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
if (run.abort.signal.aborted) return;
|
|
1833
|
+
logger.error("Initial load failed", { collectionName, error });
|
|
1834
|
+
markError(error);
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
if (!run.abort.signal.aborted) markReady();
|
|
1838
|
+
}
|
|
1839
|
+
function releasable() {
|
|
1840
|
+
return ledger.holders().filter((holder) => holder === ACCEPTED || typeof holder === "string").map((holder) => [holder, ledger.idsOf(holder)]);
|
|
1841
|
+
}
|
|
1842
|
+
async function reloadRows(run) {
|
|
1843
|
+
const loaded = syncMode === "eager" ? [await loadEagerRows(run)] : await Promise.all(
|
|
1844
|
+
[...run.demands].map(
|
|
1845
|
+
([opts, demand]) => load(run, opts, demand, "accepted", true)
|
|
1846
|
+
)
|
|
1847
|
+
);
|
|
1848
|
+
return new Set(loaded.flat());
|
|
1849
|
+
}
|
|
1850
|
+
async function releaseUnconfirmed(run, before, confirmed) {
|
|
1851
|
+
for (const [holder, ids] of before) {
|
|
1852
|
+
if (run.abort.signal.aborted) return;
|
|
1853
|
+
const unconfirmed = ids.filter((id) => !confirmed.has(id));
|
|
1854
|
+
if (unconfirmed.length > 0) {
|
|
1855
|
+
await settle(membership2.drop([holder], unconfirmed), "accepted");
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
async function reloadNow() {
|
|
1860
|
+
const run = current;
|
|
1861
|
+
if (!run || !store.isAttached()) return;
|
|
1862
|
+
expireParked(run);
|
|
1863
|
+
const before = releasable();
|
|
1864
|
+
let confirmed;
|
|
1865
|
+
try {
|
|
1866
|
+
confirmed = await reloadRows(run);
|
|
1867
|
+
} catch (error) {
|
|
1868
|
+
if (run.abort.signal.aborted) return;
|
|
1869
|
+
throw error;
|
|
1870
|
+
}
|
|
1871
|
+
await releaseUnconfirmed(run, before, confirmed);
|
|
1872
|
+
}
|
|
1873
|
+
let reloading;
|
|
1874
|
+
let queued;
|
|
1875
|
+
function reload() {
|
|
1876
|
+
if (current) wakeRetries(current);
|
|
1877
|
+
if (!reloading) {
|
|
1878
|
+
reloading = reloadNow().finally(() => {
|
|
1879
|
+
reloading = void 0;
|
|
1880
|
+
});
|
|
1881
|
+
return reloading;
|
|
1882
|
+
}
|
|
1883
|
+
queued ?? (queued = reloading.catch(() => void 0).then(() => {
|
|
1884
|
+
queued = void 0;
|
|
1885
|
+
return reload();
|
|
1886
|
+
}));
|
|
1887
|
+
return queued;
|
|
1888
|
+
}
|
|
1889
|
+
return {
|
|
1890
|
+
sync: (params) => {
|
|
1891
|
+
const channel = params;
|
|
1892
|
+
const run = {
|
|
1893
|
+
abort: new AbortController(),
|
|
1894
|
+
demands: /* @__PURE__ */ new Map(),
|
|
1895
|
+
eagerSeq: 0,
|
|
1896
|
+
eagerLoad: void 0,
|
|
1897
|
+
eagerWake: void 0
|
|
1898
|
+
};
|
|
1899
|
+
current = run;
|
|
1900
|
+
store.attach(channel);
|
|
1901
|
+
const cleanup = () => {
|
|
1902
|
+
run.abort.abort();
|
|
1903
|
+
run.eagerWake?.();
|
|
1904
|
+
for (const demand of run.demands.values()) {
|
|
1905
|
+
if (demand.parked !== void 0) clearTimeout(demand.parked);
|
|
1906
|
+
demand.wake?.();
|
|
1907
|
+
}
|
|
1908
|
+
run.demands.clear();
|
|
1909
|
+
if (current === run) current = void 0;
|
|
1910
|
+
store.detach(channel);
|
|
1911
|
+
ledger.clear();
|
|
1912
|
+
subsets.clear();
|
|
1913
|
+
if (syncMode === "on-demand") realtime.resetQueryFilters();
|
|
1914
|
+
deps.onCleanup();
|
|
1915
|
+
};
|
|
1916
|
+
if (syncMode === "eager") {
|
|
1917
|
+
void startEager(run, params.markReady, params.markError);
|
|
1918
|
+
return { cleanup };
|
|
1919
|
+
}
|
|
1920
|
+
params.markReady();
|
|
1921
|
+
return {
|
|
1922
|
+
loadSubset: loadSubsetIn(run),
|
|
1923
|
+
unloadSubset: (opts) => unloadSubset(run, opts),
|
|
1924
|
+
cleanup
|
|
1925
|
+
};
|
|
1926
|
+
},
|
|
1927
|
+
reload,
|
|
1928
|
+
subsetsFor: (filter) => [...current?.demands ?? []].filter(([, demand]) => demand.filters?.includes(filter)).map(([opts]) => opts),
|
|
1929
|
+
noteDeleted: (id, topic) => {
|
|
1930
|
+
for (const [deleted, filters] of tombstones) {
|
|
1931
|
+
if (topic === "*" || filters?.includes(topic)) deleted.add(id);
|
|
1932
|
+
}
|
|
1933
|
+
},
|
|
1934
|
+
isDeleted: (id) => [...tombstones.keys()].some((deleted) => deleted.has(id)),
|
|
1935
|
+
expireParked: () => {
|
|
1936
|
+
if (current) expireParked(current);
|
|
1937
|
+
},
|
|
1938
|
+
wakeRetries: () => {
|
|
1939
|
+
if (current) wakeRetries(current);
|
|
1940
|
+
},
|
|
1941
|
+
holdersFor: (row) => {
|
|
1942
|
+
if (!current) return [];
|
|
1943
|
+
if (syncMode === "eager") return [EAGER];
|
|
1944
|
+
return [...current.demands].filter(
|
|
1945
|
+
([opts, demand]) => demand.parked === void 0 && demand.landed && matcherFor(opts.where, true)(row)
|
|
1946
|
+
).map(([opts]) => opts);
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// src/synced-store.ts
|
|
1952
|
+
function createSyncedStore(collectionName) {
|
|
1953
|
+
let session;
|
|
1954
|
+
return {
|
|
1955
|
+
attach(channel) {
|
|
1956
|
+
session = channel;
|
|
1957
|
+
},
|
|
1958
|
+
detach(channel) {
|
|
1959
|
+
if (session === channel) session = void 0;
|
|
1960
|
+
},
|
|
1961
|
+
isAttached: () => session !== void 0,
|
|
1962
|
+
transact(writes) {
|
|
1963
|
+
const channel = session;
|
|
1964
|
+
if (!channel) return false;
|
|
1965
|
+
if (writes.length === 0) return true;
|
|
1966
|
+
channel.begin();
|
|
1967
|
+
try {
|
|
1968
|
+
for (const write of writes) channel.write(write);
|
|
1969
|
+
} catch (error) {
|
|
1970
|
+
try {
|
|
1971
|
+
const cancel = new AbortController();
|
|
1972
|
+
cancel.abort();
|
|
1973
|
+
channel.commit(cancel.signal);
|
|
1974
|
+
} catch {
|
|
1975
|
+
}
|
|
1976
|
+
throw new Error(`${collectionName}: sync write failed`, { cause: error });
|
|
1977
|
+
}
|
|
1978
|
+
return channel.commit();
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/realtime-client.ts
|
|
1984
|
+
var RealtimeTopicTooLongError = class extends Error {
|
|
1985
|
+
constructor(length) {
|
|
1986
|
+
super(
|
|
1987
|
+
`Realtime topic is ${length} characters; the server accepts ${REALTIME_TOPIC_MAX_LENGTH}`
|
|
1988
|
+
);
|
|
1989
|
+
this.name = "RealtimeTopicTooLongError";
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
var RealtimeDisconnectedError = class extends Error {
|
|
1993
|
+
constructor() {
|
|
1994
|
+
super("Realtime client was disconnected before this subscription was confirmed");
|
|
1995
|
+
this.name = "RealtimeDisconnectedError";
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
var REALTIME_BACKOFF_MS = [200, 300, 500, 1e3, 1200, 1500, 2e3];
|
|
1999
|
+
function createRealtimeClient(deps) {
|
|
2000
|
+
const backoff = deps.backoff ?? REALTIME_BACKOFF_MS;
|
|
2001
|
+
const openSource = deps.eventSource ?? ((url) => new EventSource(url));
|
|
2002
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
2003
|
+
const dispatchers = /* @__PURE__ */ new Map();
|
|
2004
|
+
let source;
|
|
2005
|
+
let connected = false;
|
|
2006
|
+
let clientId;
|
|
2007
|
+
let lastSeq;
|
|
2008
|
+
let confirmed = [];
|
|
2009
|
+
let waiters = [];
|
|
2010
|
+
let submitQueued = false;
|
|
2011
|
+
let submitting = false;
|
|
2012
|
+
let resubmit = false;
|
|
2013
|
+
let attempts = 0;
|
|
2014
|
+
let reconnectTimer;
|
|
2015
|
+
let everConnected = false;
|
|
2016
|
+
let session = 0;
|
|
2017
|
+
let disabled = false;
|
|
2018
|
+
function topics() {
|
|
2019
|
+
return [...listeners.keys()];
|
|
2020
|
+
}
|
|
2021
|
+
function dispatcherFor(topic) {
|
|
2022
|
+
let dispatch = dispatchers.get(topic);
|
|
2023
|
+
if (!dispatch) {
|
|
2024
|
+
dispatch = (ev) => {
|
|
2025
|
+
const message = ev;
|
|
2026
|
+
let event;
|
|
2027
|
+
try {
|
|
2028
|
+
event = JSON.parse(String(message.data));
|
|
2029
|
+
} catch {
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
if (typeof event.seq === "number") lastSeq = event.seq;
|
|
2033
|
+
for (const listener of listeners.get(topic) ?? []) listener(event);
|
|
2034
|
+
};
|
|
2035
|
+
dispatchers.set(topic, dispatch);
|
|
2036
|
+
}
|
|
2037
|
+
return dispatch;
|
|
2038
|
+
}
|
|
2039
|
+
function sameList(a, b) {
|
|
2040
|
+
return a.length === b.length && a.every((topic) => b.includes(topic));
|
|
2041
|
+
}
|
|
2042
|
+
function settleWaiters(pending, error) {
|
|
2043
|
+
for (const waiter of pending) error === void 0 ? waiter.resolve() : waiter.reject(error);
|
|
2044
|
+
}
|
|
2045
|
+
async function sendTopics() {
|
|
2046
|
+
const list = topics();
|
|
2047
|
+
if (list.length === 0) {
|
|
2048
|
+
close(true);
|
|
2049
|
+
clientId = void 0;
|
|
2050
|
+
lastSeq = void 0;
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
if (sameList(list, confirmed)) return;
|
|
2054
|
+
const id = clientId;
|
|
2055
|
+
if (!id) return;
|
|
2056
|
+
await deps.send({ clientId: id, subscriptions: list });
|
|
2057
|
+
if (clientId === id) confirmed = list;
|
|
2058
|
+
}
|
|
2059
|
+
function requeueStale(pending) {
|
|
2060
|
+
if (disabled) {
|
|
2061
|
+
settleWaiters(pending);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
waiters = [...pending, ...waiters];
|
|
2065
|
+
resubmit = true;
|
|
2066
|
+
}
|
|
2067
|
+
function settleWhileDisconnected() {
|
|
2068
|
+
if (listeners.size > 0) return;
|
|
2069
|
+
const pending = waiters;
|
|
2070
|
+
waiters = [];
|
|
2071
|
+
close(true);
|
|
2072
|
+
clientId = void 0;
|
|
2073
|
+
lastSeq = void 0;
|
|
2074
|
+
settleWaiters(pending);
|
|
2075
|
+
}
|
|
2076
|
+
async function postTopics(pending, startedSession) {
|
|
2077
|
+
try {
|
|
2078
|
+
await sendTopics();
|
|
2079
|
+
if (session !== startedSession) requeueStale(pending);
|
|
2080
|
+
else settleWaiters(pending);
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
if (session !== startedSession) {
|
|
2083
|
+
requeueStale(pending);
|
|
2084
|
+
} else {
|
|
2085
|
+
logger.error("Failed to set realtime subscriptions", { error });
|
|
2086
|
+
settleWaiters(pending, error);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
async function runSubmit() {
|
|
2091
|
+
if (submitting) {
|
|
2092
|
+
resubmit = true;
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
if (!connected || !clientId) {
|
|
2096
|
+
settleWhileDisconnected();
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
submitting = true;
|
|
2100
|
+
const pending = waiters;
|
|
2101
|
+
waiters = [];
|
|
2102
|
+
try {
|
|
2103
|
+
await postTopics(pending, session);
|
|
2104
|
+
} finally {
|
|
2105
|
+
submitting = false;
|
|
2106
|
+
if (resubmit) {
|
|
2107
|
+
resubmit = false;
|
|
2108
|
+
void runSubmit();
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
function submit() {
|
|
2113
|
+
const done = new Promise((resolve, reject) => {
|
|
2114
|
+
waiters.push({ resolve, reject });
|
|
2115
|
+
});
|
|
2116
|
+
if (!submitQueued) {
|
|
2117
|
+
submitQueued = true;
|
|
2118
|
+
queueMicrotask(() => {
|
|
2119
|
+
submitQueued = false;
|
|
2120
|
+
void runSubmit();
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
return done;
|
|
2124
|
+
}
|
|
2125
|
+
function attachAll(target) {
|
|
2126
|
+
for (const topic of listeners.keys()) target.addEventListener(topic, dispatcherFor(topic));
|
|
2127
|
+
}
|
|
2128
|
+
function connectUrl() {
|
|
2129
|
+
if (!clientId) return deps.url;
|
|
2130
|
+
const params = new URLSearchParams({ resume: clientId });
|
|
2131
|
+
if (lastSeq !== void 0) params.set("after", String(lastSeq));
|
|
2132
|
+
return `${deps.url}${deps.url.includes("?") ? "&" : "?"}${params}`;
|
|
2133
|
+
}
|
|
2134
|
+
function close(forget) {
|
|
2135
|
+
if (reconnectTimer !== void 0) clearTimeout(reconnectTimer);
|
|
2136
|
+
reconnectTimer = void 0;
|
|
2137
|
+
source?.close();
|
|
2138
|
+
source = void 0;
|
|
2139
|
+
connected = false;
|
|
2140
|
+
if (forget) {
|
|
2141
|
+
confirmed = [];
|
|
2142
|
+
everConnected = false;
|
|
2143
|
+
session += 1;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
function forgetSession() {
|
|
2147
|
+
if (reconnectTimer !== void 0) clearTimeout(reconnectTimer);
|
|
2148
|
+
reconnectTimer = void 0;
|
|
2149
|
+
source?.close();
|
|
2150
|
+
source = void 0;
|
|
2151
|
+
connected = false;
|
|
2152
|
+
confirmed = [];
|
|
2153
|
+
clientId = void 0;
|
|
2154
|
+
lastSeq = void 0;
|
|
2155
|
+
attempts = 0;
|
|
2156
|
+
session += 1;
|
|
2157
|
+
if (listeners.size > 0) {
|
|
2158
|
+
try {
|
|
2159
|
+
connect();
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
logger.error("Failed to reconnect after reset", { error });
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
function scheduleReconnect() {
|
|
2166
|
+
const delay = backoff[Math.min(attempts, backoff.length - 1)] ?? 0;
|
|
2167
|
+
attempts += 1;
|
|
2168
|
+
reconnectTimer = setTimeout(() => {
|
|
2169
|
+
reconnectTimer = void 0;
|
|
2170
|
+
if (listeners.size > 0) connect();
|
|
2171
|
+
}, delay);
|
|
2172
|
+
}
|
|
2173
|
+
function handleConnectionLost(target) {
|
|
2174
|
+
if (source !== target) return;
|
|
2175
|
+
logger.debug("Realtime connection lost", { clientId });
|
|
2176
|
+
close(false);
|
|
2177
|
+
scheduleReconnect();
|
|
2178
|
+
}
|
|
2179
|
+
function connect() {
|
|
2180
|
+
if (source || disabled) return;
|
|
2181
|
+
const previousId = clientId;
|
|
2182
|
+
const target = openSource(connectUrl());
|
|
2183
|
+
source = target;
|
|
2184
|
+
target.addEventListener("error", () => handleConnectionLost(target));
|
|
2185
|
+
target.addEventListener("PB_CONNECT", (ev) => {
|
|
2186
|
+
if (source !== target) return;
|
|
2187
|
+
const message = ev;
|
|
2188
|
+
let data = {};
|
|
2189
|
+
try {
|
|
2190
|
+
data = JSON.parse(String(message.data));
|
|
2191
|
+
} catch {
|
|
2192
|
+
data = {};
|
|
2193
|
+
}
|
|
2194
|
+
const id = message.lastEventId || data.clientId;
|
|
2195
|
+
if (!id) return;
|
|
2196
|
+
const resumed = data.resumed === true && id === previousId;
|
|
2197
|
+
clientId = id;
|
|
2198
|
+
connected = true;
|
|
2199
|
+
if (!resumed) {
|
|
2200
|
+
lastSeq = void 0;
|
|
2201
|
+
confirmed = [];
|
|
2202
|
+
}
|
|
2203
|
+
attachAll(target);
|
|
2204
|
+
const isReconnect = everConnected;
|
|
2205
|
+
everConnected = true;
|
|
2206
|
+
submit().then(
|
|
2207
|
+
() => {
|
|
2208
|
+
if (source !== target) return;
|
|
2209
|
+
attempts = 0;
|
|
2210
|
+
if (isReconnect) deps.onReconnect?.(resumed);
|
|
2211
|
+
},
|
|
2212
|
+
() => {
|
|
2213
|
+
if (source !== target || !isReconnect) return;
|
|
2214
|
+
close(false);
|
|
2215
|
+
scheduleReconnect();
|
|
2216
|
+
}
|
|
2217
|
+
);
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
return {
|
|
2221
|
+
subscribe(topic, listener) {
|
|
2222
|
+
if (topic.length > REALTIME_TOPIC_MAX_LENGTH) {
|
|
2223
|
+
return Promise.reject(new RealtimeTopicTooLongError(topic.length));
|
|
2224
|
+
}
|
|
2225
|
+
if (!source) {
|
|
2226
|
+
try {
|
|
2227
|
+
connect();
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
return Promise.reject(error);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
let set = listeners.get(topic);
|
|
2233
|
+
const isNew = set === void 0;
|
|
2234
|
+
if (!set) {
|
|
2235
|
+
set = /* @__PURE__ */ new Set();
|
|
2236
|
+
listeners.set(topic, set);
|
|
2237
|
+
}
|
|
2238
|
+
set.add(listener);
|
|
2239
|
+
if (isNew && source && connected) source.addEventListener(topic, dispatcherFor(topic));
|
|
2240
|
+
const ready = disabled ? Promise.resolve() : submit();
|
|
2241
|
+
const unsubscribe = async () => {
|
|
2242
|
+
const current = listeners.get(topic);
|
|
2243
|
+
if (!current?.delete(listener)) return;
|
|
2244
|
+
if (current.size > 0) return;
|
|
2245
|
+
listeners.delete(topic);
|
|
2246
|
+
const dispatch = dispatchers.get(topic);
|
|
2247
|
+
if (dispatch) {
|
|
2248
|
+
source?.removeEventListener(topic, dispatch);
|
|
2249
|
+
dispatchers.delete(topic);
|
|
2250
|
+
}
|
|
2251
|
+
await submit().catch(() => void 0);
|
|
2252
|
+
};
|
|
2253
|
+
return ready.then(
|
|
2254
|
+
() => unsubscribe,
|
|
2255
|
+
(error) => {
|
|
2256
|
+
const current = listeners.get(topic);
|
|
2257
|
+
if (current?.delete(listener) && current.size === 0) {
|
|
2258
|
+
listeners.delete(topic);
|
|
2259
|
+
const dispatch = dispatchers.get(topic);
|
|
2260
|
+
if (dispatch) {
|
|
2261
|
+
source?.removeEventListener(topic, dispatch);
|
|
2262
|
+
dispatchers.delete(topic);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
throw error;
|
|
2266
|
+
}
|
|
2267
|
+
);
|
|
2268
|
+
},
|
|
2269
|
+
topics,
|
|
2270
|
+
isConnected: () => connected,
|
|
2271
|
+
clientId: () => clientId,
|
|
2272
|
+
disconnect() {
|
|
2273
|
+
close(true);
|
|
2274
|
+
clientId = void 0;
|
|
2275
|
+
lastSeq = void 0;
|
|
2276
|
+
const pending = waiters;
|
|
2277
|
+
waiters = [];
|
|
2278
|
+
listeners.clear();
|
|
2279
|
+
dispatchers.clear();
|
|
2280
|
+
settleWaiters(pending, new RealtimeDisconnectedError());
|
|
2281
|
+
},
|
|
2282
|
+
simulateDisconnect() {
|
|
2283
|
+
if (source) handleConnectionLost(source);
|
|
2284
|
+
},
|
|
2285
|
+
reset: forgetSession,
|
|
2286
|
+
// Pending subscribes settle: their topics are registered and go out
|
|
2287
|
+
// with the first POST after enable(), the same as a subscribe made
|
|
2288
|
+
// while disabled.
|
|
2289
|
+
disable() {
|
|
2290
|
+
disabled = true;
|
|
2291
|
+
forgetSession();
|
|
2292
|
+
const pending = waiters;
|
|
2293
|
+
waiters = [];
|
|
2294
|
+
settleWaiters(pending);
|
|
2295
|
+
},
|
|
2296
|
+
enable() {
|
|
2297
|
+
if (!disabled) return;
|
|
2298
|
+
disabled = false;
|
|
2299
|
+
if (listeners.size > 0) connect();
|
|
2300
|
+
},
|
|
2301
|
+
isEnabled: () => !disabled
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// src/transport.ts
|
|
2306
|
+
var entries = /* @__PURE__ */ new WeakMap();
|
|
2307
|
+
function watchAuth(pb, entry) {
|
|
2308
|
+
let authId = pb.authStore.record?.id;
|
|
2309
|
+
pb.authStore.onChange((_token, record) => {
|
|
2310
|
+
if (record?.id === authId) return;
|
|
2311
|
+
authId = record?.id;
|
|
2312
|
+
for (const listener of entry.authListeners) listener();
|
|
2313
|
+
entry.client.reset();
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
function entryFor(pb) {
|
|
2317
|
+
let entry = entries.get(pb);
|
|
2318
|
+
if (!entry) {
|
|
2319
|
+
const reconnectListeners = /* @__PURE__ */ new Set();
|
|
2320
|
+
const client = createRealtimeClient({
|
|
2321
|
+
url: pb.buildURL("/api/realtime"),
|
|
2322
|
+
send: (body) => pb.send("/api/realtime", { method: "POST", body, requestKey: null }),
|
|
2323
|
+
onReconnect: (resumed) => {
|
|
2324
|
+
for (const listener of reconnectListeners) listener(resumed);
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
entry = { client, reconnectListeners, authListeners: /* @__PURE__ */ new Set() };
|
|
2328
|
+
watchAuth(pb, entry);
|
|
2329
|
+
entries.set(pb, entry);
|
|
2330
|
+
}
|
|
2331
|
+
return entry;
|
|
2332
|
+
}
|
|
2333
|
+
function resetRealtime(pb) {
|
|
2334
|
+
const client = entries.get(pb)?.client;
|
|
2335
|
+
if (!client) return;
|
|
2336
|
+
client.reset();
|
|
2337
|
+
client.enable();
|
|
2338
|
+
}
|
|
2339
|
+
function disconnectRealtime(pb) {
|
|
2340
|
+
entryFor(pb).client.disable();
|
|
2341
|
+
}
|
|
2342
|
+
function transportFor(pb) {
|
|
2343
|
+
const entry = entryFor(pb);
|
|
2344
|
+
return {
|
|
2345
|
+
subscribe: (collectionName, options, handler) => entry.client.subscribe(
|
|
2346
|
+
realtimeTopic(collectionName, options),
|
|
2347
|
+
(event) => handler(event)
|
|
2348
|
+
),
|
|
2349
|
+
onReconnect(listener) {
|
|
2350
|
+
entry.reconnectListeners.add(listener);
|
|
2351
|
+
return () => {
|
|
2352
|
+
entry.reconnectListeners.delete(listener);
|
|
2353
|
+
};
|
|
2354
|
+
},
|
|
2355
|
+
onAuthChange(listener) {
|
|
2356
|
+
entry.authListeners.add(listener);
|
|
2357
|
+
return () => {
|
|
2358
|
+
entry.authListeners.delete(listener);
|
|
2359
|
+
};
|
|
2360
|
+
},
|
|
2361
|
+
reconnectListenerCount: () => entry.reconnectListeners.size
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// src/views.ts
|
|
2366
|
+
function createViewRegistry(subscribe) {
|
|
2367
|
+
const tags = /* @__PURE__ */ new WeakMap();
|
|
2368
|
+
let subscribing;
|
|
2369
|
+
function tagFor(opts) {
|
|
2370
|
+
return (opts.subscription && tags.get(opts.subscription)) ?? subscribing;
|
|
2371
|
+
}
|
|
2372
|
+
return {
|
|
2373
|
+
tagFor,
|
|
2374
|
+
withViewExpand(opts) {
|
|
2375
|
+
const paths = tagFor(opts)?.paths;
|
|
2376
|
+
return paths ? { ...opts, expand: paths } : opts;
|
|
2377
|
+
},
|
|
2378
|
+
subscribeTagged(tag, args) {
|
|
2379
|
+
subscribing = tag;
|
|
2380
|
+
try {
|
|
2381
|
+
const subscription = subscribe(...args);
|
|
2382
|
+
if (tag) tags.set(subscription, tag);
|
|
2383
|
+
return subscription;
|
|
2384
|
+
} finally {
|
|
2385
|
+
subscribing = void 0;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
function createViews(deps) {
|
|
2391
|
+
const { collectionName, collection, alwaysFetch, relationTargets, realtimeMode } = deps;
|
|
2392
|
+
const views = /* @__PURE__ */ new Map();
|
|
2393
|
+
function createView(tag) {
|
|
2394
|
+
const query = [];
|
|
2395
|
+
if (tag.paths.length > 0) query.push(`expand=${tag.paths.join(",")}`);
|
|
2396
|
+
if (tag.realtime) query.push(`realtime=${tag.realtime}`);
|
|
2397
|
+
const view = Object.create(collection);
|
|
2398
|
+
Object.defineProperties(view, {
|
|
2399
|
+
id: { value: `${collectionName}?${query.join("&")}` },
|
|
2400
|
+
subscribeChanges: { value: deps.subscribeChangesFor(tag) },
|
|
2401
|
+
fetchRelations: {
|
|
2402
|
+
value: (...more) => viewFor([...tag.paths, ...more], tag.realtime)
|
|
2403
|
+
},
|
|
2404
|
+
withRealtime: {
|
|
2405
|
+
value: (mode) => {
|
|
2406
|
+
deps.assertRealtimeMode(mode);
|
|
2407
|
+
const override = mode === realtimeMode ? void 0 : mode;
|
|
2408
|
+
if (tag.realtime !== void 0 && tag.realtime !== override) {
|
|
2409
|
+
throw new Error(
|
|
2410
|
+
`A view of "${collectionName}" already uses realtime '${tag.realtime}'`
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2413
|
+
return viewFor(tag.paths, mode);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
2417
|
+
return view;
|
|
2418
|
+
}
|
|
2419
|
+
function viewFor(paths, realtime) {
|
|
2420
|
+
for (const path of paths) validateExpandPath(collectionName, relationTargets, path);
|
|
2421
|
+
const all = normalizePaths([...alwaysFetch, ...paths]);
|
|
2422
|
+
const override = realtime === realtimeMode ? void 0 : realtime;
|
|
2423
|
+
if (override === void 0 && all.every((path) => alwaysFetch.includes(path))) {
|
|
2424
|
+
return collection;
|
|
2425
|
+
}
|
|
2426
|
+
const key = `${all.join(",")}|${override ?? ""}`;
|
|
2427
|
+
let view = views.get(key);
|
|
2428
|
+
if (!view) {
|
|
2429
|
+
view = createView({ paths: all, realtime: override });
|
|
2430
|
+
views.set(key, view);
|
|
2431
|
+
}
|
|
2432
|
+
return view;
|
|
2433
|
+
}
|
|
2434
|
+
return {
|
|
2435
|
+
fetchRelations: (...paths) => viewFor(paths, void 0),
|
|
2436
|
+
withRealtime: (mode) => {
|
|
2437
|
+
deps.assertRealtimeMode(mode);
|
|
2438
|
+
return viewFor([], mode);
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// src/build-collection.ts
|
|
2444
|
+
var REALTIME_MODES = ["collection", "query"];
|
|
2445
|
+
function buildCollection(input) {
|
|
2446
|
+
const { pb, factoryOptions, collectionName, options } = input;
|
|
2447
|
+
const relationTargets = options?.relations;
|
|
2448
|
+
for (const [key, target] of Object.entries(relationTargets ?? {})) {
|
|
2449
|
+
if (typeof target?.writeFiled !== "function" || typeof target.holdLive !== "function" || typeof target.expectFiling !== "function") {
|
|
2450
|
+
throw new Error(
|
|
2451
|
+
`Collection '${collectionName}': relation '${key}' is not a pbtsdb collection`
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
const alwaysFetch = normalizePaths(options?.alwaysFetchRelations ?? []);
|
|
2456
|
+
for (const path of alwaysFetch) validateExpandPath(collectionName, relationTargets, path);
|
|
2457
|
+
const syncMode = options?.syncMode ?? "eager";
|
|
2458
|
+
const realtimeMode = options?.realtime ?? "collection";
|
|
2459
|
+
const refetchOnMutation = options?.refetchOnMutation ?? false;
|
|
2460
|
+
const subsetGcTime = options?.subsetGcTime ?? 5e3;
|
|
2461
|
+
const loadRetryDelays = options?.loadRetryDelays ?? [1e3, 2e3, 4e3, 8e3, 15e3, 3e4];
|
|
2462
|
+
function assertRealtimeMode(mode) {
|
|
2463
|
+
if (!REALTIME_MODES.includes(mode)) {
|
|
2464
|
+
throw new Error(
|
|
2465
|
+
`Collection '${collectionName}': unknown realtime mode '${String(mode)}'`
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
if (mode === "query" && syncMode !== "on-demand") {
|
|
2469
|
+
throw new Error(
|
|
2470
|
+
`Collection '${collectionName}': realtime 'query' requires syncMode 'on-demand'`
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
assertRealtimeMode(realtimeMode);
|
|
2475
|
+
const requestedExpand = /* @__PURE__ */ new Set();
|
|
2476
|
+
const pendingSubscribeExpand = () => joinPaths([...alwaysFetch, ...requestedExpand]);
|
|
2477
|
+
function activeExpand(request) {
|
|
2478
|
+
return joinPaths([
|
|
2479
|
+
...alwaysFetch,
|
|
2480
|
+
...splitPaths(request.expand),
|
|
2481
|
+
...syncMode === "eager" ? requestedExpand : []
|
|
2482
|
+
]);
|
|
2483
|
+
}
|
|
2484
|
+
function realtimeSubscribeOptions() {
|
|
2485
|
+
const base = factoryOptions?.subscribeOptions?.();
|
|
2486
|
+
const expand = joinPaths([
|
|
2487
|
+
...splitPaths(pendingSubscribeExpand()),
|
|
2488
|
+
...splitPaths(base?.expand)
|
|
2489
|
+
]);
|
|
2490
|
+
return expand ? { ...base, expand } : base;
|
|
2491
|
+
}
|
|
2492
|
+
function releaseAll() {
|
|
2493
|
+
held.releaseAll();
|
|
2494
|
+
subsets.clear();
|
|
2495
|
+
if (syncMode === "on-demand") membership2.drop([ACCEPTED]);
|
|
2496
|
+
}
|
|
2497
|
+
function releaseTopic(topic) {
|
|
2498
|
+
if (syncMode === "eager") return;
|
|
2499
|
+
if (membership2.drop([topic]) === false) {
|
|
2500
|
+
logger.debug("Ignoring topic release while sync is not running", {
|
|
2501
|
+
collectionName,
|
|
2502
|
+
topic
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
const parentHolder = { parent: collectionName };
|
|
2507
|
+
const ledger = createLedger();
|
|
2508
|
+
const store = createSyncedStore(collectionName);
|
|
2509
|
+
const subsets = createLoadedSubsets();
|
|
2510
|
+
const membership2 = createMembership({
|
|
2511
|
+
collectionName,
|
|
2512
|
+
ledger,
|
|
2513
|
+
store,
|
|
2514
|
+
onRemoved: (rows) => {
|
|
2515
|
+
for (const row of rows) {
|
|
2516
|
+
subsets.forgetRow(row);
|
|
2517
|
+
const id = idOf(row);
|
|
2518
|
+
if (id !== void 0) held.forgetParentRow(id);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
const held = createHeldTargets(collectionName, parentHolder);
|
|
2523
|
+
const filer = createExpandFiler({
|
|
2524
|
+
collectionName,
|
|
2525
|
+
setFiled: held.setFiled,
|
|
2526
|
+
holder: parentHolder,
|
|
2527
|
+
isStale: (record) => {
|
|
2528
|
+
const id = idOf(record);
|
|
2529
|
+
if (id === void 0) return true;
|
|
2530
|
+
if (adapter.isDeleted(id)) return true;
|
|
2531
|
+
const incoming = updatedAtOf(record);
|
|
2532
|
+
const stored = updatedAtOf(ledger.row(id));
|
|
2533
|
+
return incoming !== void 0 && stored !== void 0 && incoming < stored;
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2536
|
+
const fetcher = createFetcher({
|
|
2537
|
+
pb,
|
|
2538
|
+
collectionName,
|
|
2539
|
+
relationTargets,
|
|
2540
|
+
activeExpand,
|
|
2541
|
+
syncedRow: ledger.row,
|
|
2542
|
+
syncedRows: ledger.rows,
|
|
2543
|
+
subsets,
|
|
2544
|
+
filer
|
|
2545
|
+
});
|
|
2546
|
+
const transport = transportFor(pb);
|
|
2547
|
+
const realtime = createRealtimeSubscription({
|
|
2548
|
+
transport,
|
|
2549
|
+
collectionName,
|
|
2550
|
+
handleEvent: (event, topic, expand) => handleRealtimeEvent(event, topic, expand),
|
|
2551
|
+
pendingExpand: pendingSubscribeExpand,
|
|
2552
|
+
subscribeOptions: realtimeSubscribeOptions,
|
|
2553
|
+
onEntryOpened: () => held.sync(targetsAlong(splitPaths(pendingSubscribeExpand()), relationTargets)),
|
|
2554
|
+
onEntryClosed: releaseTopic,
|
|
2555
|
+
onIdle: releaseAll
|
|
2556
|
+
});
|
|
2557
|
+
const adapter = createSyncAdapter({
|
|
2558
|
+
collectionName,
|
|
2559
|
+
syncMode,
|
|
2560
|
+
realtimeMode,
|
|
2561
|
+
subsetGcTime,
|
|
2562
|
+
loadRetryDelays,
|
|
2563
|
+
ledger,
|
|
2564
|
+
store,
|
|
2565
|
+
membership: membership2,
|
|
2566
|
+
fetcher,
|
|
2567
|
+
subsets,
|
|
2568
|
+
realtime,
|
|
2569
|
+
registry: () => registry,
|
|
2570
|
+
onCleanup: () => {
|
|
2571
|
+
held.clearFiled();
|
|
2572
|
+
for (const timer of acceptedTimers) clearTimeout(timer);
|
|
2573
|
+
acceptedTimers.clear();
|
|
2574
|
+
}
|
|
2575
|
+
});
|
|
2576
|
+
function resolveHandler(option, fallback) {
|
|
2577
|
+
return option === false ? void 0 : option ?? fallback;
|
|
2578
|
+
}
|
|
2579
|
+
function landServerRows(rows) {
|
|
2580
|
+
const result = membership2.accept(rows);
|
|
2581
|
+
if (result === false) {
|
|
2582
|
+
logger.debug("Dropping write-back while sync is not running", { collectionName });
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
expireAccepted(rows);
|
|
2586
|
+
}
|
|
2587
|
+
const acceptedTimers = /* @__PURE__ */ new Set();
|
|
2588
|
+
function settleAccepted(ids) {
|
|
2589
|
+
for (const id of ids) {
|
|
2590
|
+
const row = ledger.row(id);
|
|
2591
|
+
if (!row) continue;
|
|
2592
|
+
const holders = adapter.holdersFor(row);
|
|
2593
|
+
if (holders.length > 0) membership2.hold(holders, [id]);
|
|
2594
|
+
}
|
|
2595
|
+
membership2.drop([ACCEPTED], ids);
|
|
2596
|
+
}
|
|
2597
|
+
function expireAccepted(rows) {
|
|
2598
|
+
if (subsetGcTime <= 0) return;
|
|
2599
|
+
const ids = rows.map(idOf).filter((id) => id !== void 0);
|
|
2600
|
+
const timer = setTimeout(() => {
|
|
2601
|
+
acceptedTimers.delete(timer);
|
|
2602
|
+
settleAccepted(ids);
|
|
2603
|
+
}, subsetGcTime);
|
|
2604
|
+
acceptedTimers.add(timer);
|
|
2605
|
+
}
|
|
2606
|
+
async function settleHandler() {
|
|
2607
|
+
if (refetchOnMutation) await adapter.reload();
|
|
2608
|
+
}
|
|
2609
|
+
const defaultInsert = async ({ transaction }) => {
|
|
2610
|
+
const created = await Promise.all(
|
|
2611
|
+
transaction.mutations.map(async (mutation) => {
|
|
2612
|
+
const {
|
|
2613
|
+
created: _created,
|
|
2614
|
+
updated: _updated,
|
|
2615
|
+
collectionId: _collectionId,
|
|
2616
|
+
collectionName: _collectionName,
|
|
2617
|
+
...data
|
|
2618
|
+
} = mutation.modified;
|
|
2619
|
+
return pb.collection(collectionName).create(data);
|
|
2620
|
+
})
|
|
2621
|
+
);
|
|
2622
|
+
landServerRows(created);
|
|
2623
|
+
await settleHandler();
|
|
2624
|
+
};
|
|
2625
|
+
const defaultUpdate = async ({ transaction }) => {
|
|
2626
|
+
const updated = await Promise.all(
|
|
2627
|
+
transaction.mutations.map(
|
|
2628
|
+
(mutation) => pb.collection(collectionName).update(mutation.original.id, mutation.changes)
|
|
2629
|
+
)
|
|
2630
|
+
);
|
|
2631
|
+
landServerRows(updated);
|
|
2632
|
+
await settleHandler();
|
|
2633
|
+
};
|
|
2634
|
+
async function evict(ids) {
|
|
2635
|
+
for (const id of ids) adapter.noteDeleted(id, "*");
|
|
2636
|
+
await whenAccepted(membership2.dropAll(ids));
|
|
2637
|
+
}
|
|
2638
|
+
const defaultDelete = async ({ transaction }) => {
|
|
2639
|
+
const ids = await Promise.all(
|
|
2640
|
+
transaction.mutations.map(async (mutation) => {
|
|
2641
|
+
await pb.collection(collectionName).delete(mutation.original.id);
|
|
2642
|
+
return mutation.original.id;
|
|
2643
|
+
})
|
|
2644
|
+
);
|
|
2645
|
+
await evict(ids);
|
|
2646
|
+
await settleHandler();
|
|
2647
|
+
};
|
|
2648
|
+
const collection = createCollection$1({
|
|
2649
|
+
autoIndex: "eager",
|
|
2650
|
+
defaultIndexType: BTreeIndex,
|
|
2651
|
+
...options?.collectionOptions,
|
|
2652
|
+
id: collectionName,
|
|
2653
|
+
syncMode,
|
|
2654
|
+
getKey: (item) => {
|
|
2655
|
+
const id = idOf(item);
|
|
2656
|
+
if (id === void 0) {
|
|
2657
|
+
throw new Error(
|
|
2658
|
+
`Record in collection '${collectionName}' is missing required 'id' field. Received: ${JSON.stringify(item)}`
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
return id;
|
|
2662
|
+
},
|
|
2663
|
+
sync: { sync: adapter.sync, rowUpdateMode: "full" },
|
|
2664
|
+
onInsert: resolveHandler(options?.onInsert, defaultInsert),
|
|
2665
|
+
onUpdate: resolveHandler(options?.onUpdate, defaultUpdate),
|
|
2666
|
+
onDelete: resolveHandler(options?.onDelete, defaultDelete),
|
|
2667
|
+
utils: { accept, reload: adapter.reload, evict }
|
|
2668
|
+
});
|
|
2669
|
+
function reloadFor(reason) {
|
|
2670
|
+
void adapter.reload().catch(
|
|
2671
|
+
(error) => logger.error(`Failed to reload after ${reason}`, { collectionName, error })
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
let removeTransportListeners;
|
|
2675
|
+
collection.on("status:change", (event) => {
|
|
2676
|
+
if (event.status === "cleaned-up") subsets.clear();
|
|
2677
|
+
if (!removeTransportListeners && (event.status === "loading" || event.status === "ready")) {
|
|
2678
|
+
const removeReconnect = transport.onReconnect((resumed) => {
|
|
2679
|
+
if (resumed || !realtime.isOpen()) return;
|
|
2680
|
+
if (!collection.isReady()) {
|
|
2681
|
+
adapter.wakeRetries();
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
reloadFor("realtime reconnect");
|
|
2685
|
+
});
|
|
2686
|
+
const removeAuth = transport.onAuthChange(() => {
|
|
2687
|
+
adapter.expireParked();
|
|
2688
|
+
for (const timer of acceptedTimers) clearTimeout(timer);
|
|
2689
|
+
acceptedTimers.clear();
|
|
2690
|
+
membership2.drop([ACCEPTED]);
|
|
2691
|
+
if (store.isAttached()) reloadFor("auth change");
|
|
2692
|
+
});
|
|
2693
|
+
removeTransportListeners = () => {
|
|
2694
|
+
removeReconnect();
|
|
2695
|
+
removeAuth();
|
|
2696
|
+
};
|
|
2697
|
+
} else if (removeTransportListeners && event.status === "cleaned-up") {
|
|
2698
|
+
removeTransportListeners();
|
|
2699
|
+
removeTransportListeners = void 0;
|
|
2700
|
+
}
|
|
2701
|
+
});
|
|
2702
|
+
const originalSubscribeChanges = collection.subscribeChanges.bind(collection);
|
|
2703
|
+
const registry = createViewRegistry(originalSubscribeChanges);
|
|
2704
|
+
function subscribeCounted(subscribe) {
|
|
2705
|
+
realtime.addCollectionSubscriber();
|
|
2706
|
+
let subscription;
|
|
2707
|
+
try {
|
|
2708
|
+
subscription = subscribe();
|
|
2709
|
+
} catch (error) {
|
|
2710
|
+
realtime.dropCollectionSubscriber();
|
|
2711
|
+
throw error;
|
|
2712
|
+
}
|
|
2713
|
+
const unsubscribe = subscription.unsubscribe.bind(subscription);
|
|
2714
|
+
let released = false;
|
|
2715
|
+
subscription.unsubscribe = () => {
|
|
2716
|
+
if (!released) {
|
|
2717
|
+
released = true;
|
|
2718
|
+
realtime.dropCollectionSubscriber();
|
|
2719
|
+
realtime.reconcile();
|
|
2720
|
+
}
|
|
2721
|
+
unsubscribe();
|
|
2722
|
+
};
|
|
2723
|
+
return subscription;
|
|
2724
|
+
}
|
|
2725
|
+
function subscribeChangesFor(tag) {
|
|
2726
|
+
return (...args) => {
|
|
2727
|
+
if (tag) noteViewSubscribed(tag.paths);
|
|
2728
|
+
const subscription = (tag?.realtime ?? realtimeMode) === "collection" ? subscribeCounted(() => registry.subscribeTagged(tag, args)) : registry.subscribeTagged(tag, args);
|
|
2729
|
+
realtime.reconcile();
|
|
2730
|
+
return subscription;
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
Object.defineProperty(collection, "subscribeChanges", {
|
|
2734
|
+
value: subscribeChangesFor(void 0)
|
|
2735
|
+
});
|
|
2736
|
+
const views = createViews({
|
|
2737
|
+
collectionName,
|
|
2738
|
+
collection,
|
|
2739
|
+
alwaysFetch,
|
|
2740
|
+
relationTargets,
|
|
2741
|
+
realtimeMode,
|
|
2742
|
+
assertRealtimeMode,
|
|
2743
|
+
subscribeChangesFor
|
|
2744
|
+
});
|
|
2745
|
+
function handleRealtimeEvent(event, topic, expand) {
|
|
2746
|
+
const id = idOf(event.record);
|
|
2747
|
+
if (!id) return;
|
|
2748
|
+
if (event.action === "delete") {
|
|
2749
|
+
adapter.noteDeleted(id, topic);
|
|
2750
|
+
const applied = topic === "*" ? membership2.dropAll([id]) : membership2.drop(
|
|
2751
|
+
[
|
|
2752
|
+
topic,
|
|
2753
|
+
...adapter.subsetsFor(topic),
|
|
2754
|
+
...holdersByFilter.get(topic) ?? [],
|
|
2755
|
+
ACCEPTED
|
|
2756
|
+
],
|
|
2757
|
+
[id]
|
|
2758
|
+
);
|
|
2759
|
+
if (applied === false) {
|
|
2760
|
+
logger.debug("Ignoring delete echo while sync is not running", {
|
|
2761
|
+
collectionName,
|
|
2762
|
+
id
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
const [stored] = stripFetchedRelations([event.record], pendingSubscribeExpand());
|
|
2768
|
+
const land = () => {
|
|
2769
|
+
const applied = membership2.land(topic, [stored]);
|
|
2770
|
+
if (applied !== false) membership2.drop([ACCEPTED], [id]);
|
|
2771
|
+
return applied !== false;
|
|
2772
|
+
};
|
|
2773
|
+
const paths = splitPaths(expand);
|
|
2774
|
+
if (filer.canFileFirst([event.record], relationTargets)) {
|
|
2775
|
+
filer.fileExpanded([event.record], relationTargets, paths).then((change) => {
|
|
2776
|
+
if (land()) {
|
|
2777
|
+
change.commit();
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
change.undo();
|
|
2781
|
+
logger.debug("Ignoring realtime echo while sync is not running", {
|
|
2782
|
+
collectionName,
|
|
2783
|
+
id
|
|
2784
|
+
});
|
|
2785
|
+
}).catch(
|
|
2786
|
+
(error) => logger.error("Failed to file expanded records from realtime echo", {
|
|
2787
|
+
collectionName,
|
|
2788
|
+
error
|
|
2789
|
+
})
|
|
2790
|
+
);
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
if (!land()) {
|
|
2794
|
+
logger.debug("Ignoring realtime echo while sync is not running", { collectionName, id });
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
filer.upsertExpanded([event.record], relationTargets, paths).catch(
|
|
2798
|
+
(error) => logger.error("Failed to upsert expanded records from realtime echo", {
|
|
2799
|
+
collectionName,
|
|
2800
|
+
error
|
|
2801
|
+
})
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2804
|
+
async function ensureSyncing() {
|
|
2805
|
+
if (collection.isReady()) return true;
|
|
2806
|
+
if (collection.status === "error") return false;
|
|
2807
|
+
const idle = collection.status === "idle" || collection.status === "cleaned-up";
|
|
2808
|
+
if (syncMode === "eager") {
|
|
2809
|
+
if (idle) return false;
|
|
2810
|
+
await collection.preload();
|
|
2811
|
+
return true;
|
|
2812
|
+
}
|
|
2813
|
+
if (idle) collection.startSyncImmediate();
|
|
2814
|
+
if (!collection.isReady()) {
|
|
2815
|
+
await new Promise((resolve) => {
|
|
2816
|
+
collection.onFirstReady(resolve);
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
return true;
|
|
2820
|
+
}
|
|
2821
|
+
async function writeFiled(records, holder) {
|
|
2822
|
+
if (!await ensureSyncing()) {
|
|
2823
|
+
logger.warn(
|
|
2824
|
+
`not syncing filed rows into ${collectionName} because store is not yet ready`
|
|
2825
|
+
);
|
|
2826
|
+
return false;
|
|
2827
|
+
}
|
|
2828
|
+
return membership2.land(holder, withoutExpand(records)) !== false;
|
|
2829
|
+
}
|
|
2830
|
+
async function whenAccepted(applied) {
|
|
2831
|
+
if (applied === true || applied === false) return;
|
|
2832
|
+
const accepted = whenSyncAccepted(applied);
|
|
2833
|
+
if (accepted !== true) await accepted;
|
|
2834
|
+
}
|
|
2835
|
+
async function accept(rows) {
|
|
2836
|
+
if (!await ensureSyncing()) {
|
|
2837
|
+
throw new Error(`Collection '${collectionName}' is not syncing; accept() has no store`);
|
|
2838
|
+
}
|
|
2839
|
+
const applied = membership2.accept(rows);
|
|
2840
|
+
if (applied !== false) expireAccepted(rows);
|
|
2841
|
+
await whenAccepted(applied);
|
|
2842
|
+
}
|
|
2843
|
+
const holdersByFilter = /* @__PURE__ */ new Map();
|
|
2844
|
+
function swapHolderFilters(holder, previous, next) {
|
|
2845
|
+
for (const filter of previous) {
|
|
2846
|
+
const holders = holdersByFilter.get(filter);
|
|
2847
|
+
holders?.delete(holder);
|
|
2848
|
+
if (holders?.size === 0) holdersByFilter.delete(filter);
|
|
2849
|
+
}
|
|
2850
|
+
for (const filter of next) {
|
|
2851
|
+
let holders = holdersByFilter.get(filter);
|
|
2852
|
+
if (!holders) {
|
|
2853
|
+
holders = /* @__PURE__ */ new Set();
|
|
2854
|
+
holdersByFilter.set(filter, holders);
|
|
2855
|
+
}
|
|
2856
|
+
holders.add(holder);
|
|
2857
|
+
}
|
|
2858
|
+
realtime.swapHoldFilters(previous, next);
|
|
2859
|
+
}
|
|
2860
|
+
function holdLive(holder) {
|
|
2861
|
+
const subscribe = () => originalSubscribeChanges(() => {
|
|
2862
|
+
}, { includeInitialState: false });
|
|
2863
|
+
const subscription = realtimeMode === "collection" ? subscribeCounted(subscribe) : subscribe();
|
|
2864
|
+
let current = [];
|
|
2865
|
+
let released = false;
|
|
2866
|
+
realtime.reconcile();
|
|
2867
|
+
return {
|
|
2868
|
+
setFilters: (filters) => {
|
|
2869
|
+
if (released) return;
|
|
2870
|
+
const next = [...new Set(filters)];
|
|
2871
|
+
swapHolderFilters(holder, current, next);
|
|
2872
|
+
current = next;
|
|
2873
|
+
},
|
|
2874
|
+
release: () => {
|
|
2875
|
+
if (released) return;
|
|
2876
|
+
released = true;
|
|
2877
|
+
swapHolderFilters(holder, current, []);
|
|
2878
|
+
current = [];
|
|
2879
|
+
membership2.drop([holder]);
|
|
2880
|
+
subscription.unsubscribe();
|
|
2881
|
+
}
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
function releaseFiled(ids, holder) {
|
|
2885
|
+
if (membership2.drop([holder], ids) === false) {
|
|
2886
|
+
logger.debug("Ignoring filed-row release while sync is not running", {
|
|
2887
|
+
collectionName
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
function noteViewSubscribed(paths) {
|
|
2892
|
+
let grew = false;
|
|
2893
|
+
for (const path of paths) {
|
|
2894
|
+
if (requestedExpand.has(path)) continue;
|
|
2895
|
+
requestedExpand.add(path);
|
|
2896
|
+
grew = true;
|
|
2897
|
+
}
|
|
2898
|
+
if (!grew) return;
|
|
2899
|
+
if (syncMode === "eager" && collection.status !== "idle" && collection.status !== "cleaned-up") {
|
|
2900
|
+
void adapter.reload().catch(
|
|
2901
|
+
(error) => logger.error("Failed to reload after widening expand", {
|
|
2902
|
+
collectionName,
|
|
2903
|
+
error
|
|
2904
|
+
})
|
|
2905
|
+
);
|
|
2906
|
+
}
|
|
2907
|
+
realtime.restart();
|
|
2908
|
+
}
|
|
2909
|
+
Object.assign(collection, {
|
|
2910
|
+
collectionName,
|
|
2911
|
+
relationTargets,
|
|
2912
|
+
waitForSubscription: realtime.wait,
|
|
2913
|
+
isSubscribed: realtime.isOpen,
|
|
2914
|
+
heldRelationTargetCount: held.count,
|
|
2915
|
+
markSubsetLoaded: subsets.mark,
|
|
2916
|
+
loadedSubsetCount: subsets.count,
|
|
2917
|
+
holdLive,
|
|
2918
|
+
writeFiled,
|
|
2919
|
+
releaseFiled,
|
|
2920
|
+
expectFiling: fetcher.expectFiling,
|
|
2921
|
+
accept,
|
|
2922
|
+
reload: adapter.reload,
|
|
2923
|
+
evict,
|
|
2924
|
+
fetchRelations: views.fetchRelations,
|
|
2925
|
+
withRealtime: views.withRealtime
|
|
2926
|
+
});
|
|
2927
|
+
return collection;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// src/collection.ts
|
|
2931
|
+
function createCollection(pb, factoryOptions) {
|
|
2932
|
+
return (collectionName, options) => {
|
|
2933
|
+
return buildCollection({
|
|
2934
|
+
pb,
|
|
2935
|
+
factoryOptions,
|
|
2936
|
+
collectionName,
|
|
2937
|
+
options
|
|
2938
|
+
});
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
// src/util.ts
|
|
2943
|
+
function newRecordId() {
|
|
2944
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
2945
|
+
let result = "";
|
|
2946
|
+
for (let i = 0; i < 15; i++) {
|
|
2947
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
2948
|
+
}
|
|
2949
|
+
return result;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
export { convertToPocketBaseFilter, convertToPocketBaseSort, createCollection, disconnectRealtime, newRecordId, resetLogger, resetRealtime, setLogger };
|
|
2953
|
+
//# sourceMappingURL=chunk-4CRCARB3.js.map
|
|
2954
|
+
//# sourceMappingURL=chunk-4CRCARB3.js.map
|