@michaelstewart/convex-tanstack-db-collection 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +385 -0
- package/dist/cjs/index.cjs +679 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/esm/index.d.ts +461 -0
- package/dist/esm/index.js +679 -0
- package/dist/esm/index.js.map +1 -0
- package/package.json +63 -0
- package/src/ConvexSyncManager.ts +611 -0
- package/src/expression-parser.ts +255 -0
- package/src/index.ts +247 -0
- package/src/serialization.ts +125 -0
- package/src/types.ts +260 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
function isTypeMarker(value) {
|
|
4
|
+
return typeof value === `object` && value !== null && `__type` in value && typeof value.__type === `string`;
|
|
5
|
+
}
|
|
6
|
+
function serializeValue(value) {
|
|
7
|
+
if (value === void 0) {
|
|
8
|
+
return { __type: `undefined` };
|
|
9
|
+
}
|
|
10
|
+
if (typeof value === `number`) {
|
|
11
|
+
if (Number.isNaN(value)) {
|
|
12
|
+
return { __type: `nan` };
|
|
13
|
+
}
|
|
14
|
+
if (value === Number.POSITIVE_INFINITY) {
|
|
15
|
+
return { __type: `infinity`, sign: 1 };
|
|
16
|
+
}
|
|
17
|
+
if (value === Number.NEGATIVE_INFINITY) {
|
|
18
|
+
return { __type: `infinity`, sign: -1 };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (value === null || typeof value === `string` || typeof value === `number` || typeof value === `boolean`) {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
if (value instanceof Date) {
|
|
25
|
+
return { __type: `date`, value: value.toJSON() };
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((item) => serializeValue(item));
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === `object`) {
|
|
31
|
+
return Object.fromEntries(
|
|
32
|
+
Object.entries(value).map(([key, val]) => [
|
|
33
|
+
key,
|
|
34
|
+
serializeValue(val)
|
|
35
|
+
])
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function deserializeValue(value) {
|
|
41
|
+
if (isTypeMarker(value)) {
|
|
42
|
+
switch (value.__type) {
|
|
43
|
+
case `undefined`:
|
|
44
|
+
return void 0;
|
|
45
|
+
case `nan`:
|
|
46
|
+
return NaN;
|
|
47
|
+
case `infinity`:
|
|
48
|
+
return value.sign === 1 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
|
49
|
+
case `date`:
|
|
50
|
+
return new Date(value.value);
|
|
51
|
+
default:
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (value === null || typeof value === `string` || typeof value === `number` || typeof value === `boolean`) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return value.map((item) => deserializeValue(item));
|
|
60
|
+
}
|
|
61
|
+
if (typeof value === `object`) {
|
|
62
|
+
return Object.fromEntries(
|
|
63
|
+
Object.entries(value).map(([key, val]) => [
|
|
64
|
+
key,
|
|
65
|
+
deserializeValue(val)
|
|
66
|
+
])
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function toKey(value) {
|
|
72
|
+
return JSON.stringify(serializeValue(value));
|
|
73
|
+
}
|
|
74
|
+
function fromKey(key) {
|
|
75
|
+
return deserializeValue(JSON.parse(key));
|
|
76
|
+
}
|
|
77
|
+
class ConvexSyncManager {
|
|
78
|
+
constructor(options) {
|
|
79
|
+
this.activeDimensions = /* @__PURE__ */ new Map();
|
|
80
|
+
this.refCounts = /* @__PURE__ */ new Map();
|
|
81
|
+
this.pendingFilters = {};
|
|
82
|
+
this.globalCursor = 0;
|
|
83
|
+
this.currentSubscription = null;
|
|
84
|
+
this.debounceTimer = null;
|
|
85
|
+
this.isProcessing = false;
|
|
86
|
+
this.markedReady = false;
|
|
87
|
+
this.hasRequestedGlobal = false;
|
|
88
|
+
this.globalRefCount = 0;
|
|
89
|
+
this.messagesSinceSubscription = 0;
|
|
90
|
+
this.callbacks = null;
|
|
91
|
+
this.client = options.client;
|
|
92
|
+
this.query = options.query;
|
|
93
|
+
this.filterDimensions = options.filterDimensions;
|
|
94
|
+
this.updatedAtFieldName = options.updatedAtFieldName;
|
|
95
|
+
this.debounceMs = options.debounceMs;
|
|
96
|
+
this.tailOverlapMs = options.tailOverlapMs;
|
|
97
|
+
this.resubscribeThreshold = options.resubscribeThreshold;
|
|
98
|
+
this.getKey = options.getKey;
|
|
99
|
+
for (const dim of this.filterDimensions) {
|
|
100
|
+
this.activeDimensions.set(dim.convexArg, /* @__PURE__ */ new Set());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Initialize the sync manager with callbacks from TanStack DB
|
|
105
|
+
*/
|
|
106
|
+
setCallbacks(callbacks) {
|
|
107
|
+
this.callbacks = callbacks;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Create a composite key for ref counting multi-filter combinations.
|
|
111
|
+
* Uses serialized values for deterministic keys.
|
|
112
|
+
*/
|
|
113
|
+
createCompositeKey(filters) {
|
|
114
|
+
const sorted = Object.keys(filters).sort().reduce(
|
|
115
|
+
(acc, key) => {
|
|
116
|
+
const values = filters[key];
|
|
117
|
+
if (values) {
|
|
118
|
+
acc[key] = values.map((v) => toKey(v)).sort();
|
|
119
|
+
}
|
|
120
|
+
return acc;
|
|
121
|
+
},
|
|
122
|
+
{}
|
|
123
|
+
);
|
|
124
|
+
return JSON.stringify(sorted);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Request filters to be synced (called by loadSubset)
|
|
128
|
+
* Filters are batched via debouncing for efficiency
|
|
129
|
+
*/
|
|
130
|
+
requestFilters(filters) {
|
|
131
|
+
var _a;
|
|
132
|
+
if (this.filterDimensions.length === 0) {
|
|
133
|
+
this.globalRefCount++;
|
|
134
|
+
if (!this.hasRequestedGlobal) {
|
|
135
|
+
this.hasRequestedGlobal = true;
|
|
136
|
+
return this.scheduleProcessing();
|
|
137
|
+
}
|
|
138
|
+
return Promise.resolve();
|
|
139
|
+
}
|
|
140
|
+
const compositeKey = this.createCompositeKey(filters);
|
|
141
|
+
const count = this.refCounts.get(compositeKey) || 0;
|
|
142
|
+
this.refCounts.set(compositeKey, count + 1);
|
|
143
|
+
let hasNewValues = false;
|
|
144
|
+
for (const [convexArg, values] of Object.entries(filters)) {
|
|
145
|
+
const activeSet = this.activeDimensions.get(convexArg);
|
|
146
|
+
if (!activeSet) continue;
|
|
147
|
+
const dim = this.filterDimensions.find((d) => d.convexArg === convexArg);
|
|
148
|
+
if (dim == null ? void 0 : dim.single) {
|
|
149
|
+
const existingCount = activeSet.size;
|
|
150
|
+
const pendingCount = ((_a = this.pendingFilters[convexArg]) == null ? void 0 : _a.length) ?? 0;
|
|
151
|
+
const newValues = values.filter((v) => {
|
|
152
|
+
var _a2;
|
|
153
|
+
const serialized = toKey(v);
|
|
154
|
+
const alreadyActive = activeSet.has(serialized);
|
|
155
|
+
const alreadyPending = (_a2 = this.pendingFilters[convexArg]) == null ? void 0 : _a2.some(
|
|
156
|
+
(pv) => toKey(pv) === serialized
|
|
157
|
+
);
|
|
158
|
+
return !alreadyActive && !alreadyPending;
|
|
159
|
+
});
|
|
160
|
+
if (existingCount + pendingCount + newValues.length > 1) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Filter '${dim.filterField}' is configured as single but multiple values were requested. Active: ${existingCount}, Pending: ${pendingCount}, New: ${newValues.length}. Use single: false if you need to sync multiple values.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const value of values) {
|
|
167
|
+
const serialized = toKey(value);
|
|
168
|
+
if (!activeSet.has(serialized)) {
|
|
169
|
+
if (!this.pendingFilters[convexArg]) {
|
|
170
|
+
this.pendingFilters[convexArg] = [];
|
|
171
|
+
}
|
|
172
|
+
const alreadyPending = this.pendingFilters[convexArg].some(
|
|
173
|
+
(v) => toKey(v) === serialized
|
|
174
|
+
);
|
|
175
|
+
if (!alreadyPending) {
|
|
176
|
+
this.pendingFilters[convexArg].push(value);
|
|
177
|
+
hasNewValues = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (hasNewValues) {
|
|
183
|
+
return this.scheduleProcessing();
|
|
184
|
+
}
|
|
185
|
+
return Promise.resolve();
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Release filters when no longer needed (called by unloadSubset)
|
|
189
|
+
*/
|
|
190
|
+
releaseFilters(filters) {
|
|
191
|
+
if (this.filterDimensions.length === 0) {
|
|
192
|
+
this.globalRefCount = Math.max(0, this.globalRefCount - 1);
|
|
193
|
+
if (this.globalRefCount === 0 && this.hasRequestedGlobal) {
|
|
194
|
+
this.hasRequestedGlobal = false;
|
|
195
|
+
this.updateSubscription();
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const compositeKey = this.createCompositeKey(filters);
|
|
200
|
+
const count = (this.refCounts.get(compositeKey) || 0) - 1;
|
|
201
|
+
if (count <= 0) {
|
|
202
|
+
this.refCounts.delete(compositeKey);
|
|
203
|
+
} else {
|
|
204
|
+
this.refCounts.set(compositeKey, count);
|
|
205
|
+
}
|
|
206
|
+
this.cleanupUnreferencedValues();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Remove values from activeDimensions that are no longer referenced
|
|
210
|
+
* by any composite key in refCounts
|
|
211
|
+
*/
|
|
212
|
+
cleanupUnreferencedValues() {
|
|
213
|
+
const referencedValues = /* @__PURE__ */ new Map();
|
|
214
|
+
for (const dim of this.filterDimensions) {
|
|
215
|
+
referencedValues.set(dim.convexArg, /* @__PURE__ */ new Set());
|
|
216
|
+
}
|
|
217
|
+
for (const compositeKey of this.refCounts.keys()) {
|
|
218
|
+
try {
|
|
219
|
+
const filters = JSON.parse(compositeKey);
|
|
220
|
+
for (const [convexArg, serializedValues] of Object.entries(filters)) {
|
|
221
|
+
const refSet = referencedValues.get(convexArg);
|
|
222
|
+
if (refSet) {
|
|
223
|
+
for (const serialized of serializedValues) {
|
|
224
|
+
refSet.add(serialized);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
let needsSubscriptionUpdate = false;
|
|
232
|
+
for (const [convexArg, activeSet] of this.activeDimensions) {
|
|
233
|
+
const refSet = referencedValues.get(convexArg);
|
|
234
|
+
for (const serialized of activeSet) {
|
|
235
|
+
if (!refSet.has(serialized)) {
|
|
236
|
+
activeSet.delete(serialized);
|
|
237
|
+
needsSubscriptionUpdate = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (needsSubscriptionUpdate) {
|
|
242
|
+
this.updateSubscription();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Schedule debounced processing of pending filters
|
|
247
|
+
*/
|
|
248
|
+
scheduleProcessing() {
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
if (this.debounceTimer) {
|
|
251
|
+
clearTimeout(this.debounceTimer);
|
|
252
|
+
}
|
|
253
|
+
this.debounceTimer = setTimeout(async () => {
|
|
254
|
+
try {
|
|
255
|
+
await this.processFilterBatch();
|
|
256
|
+
resolve();
|
|
257
|
+
} catch (error) {
|
|
258
|
+
reject(error);
|
|
259
|
+
}
|
|
260
|
+
}, this.debounceMs);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Process the current batch of pending filters
|
|
265
|
+
*/
|
|
266
|
+
async processFilterBatch() {
|
|
267
|
+
if (this.isProcessing) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const hasPendingFilters = Object.keys(this.pendingFilters).length > 0;
|
|
271
|
+
const needsGlobalSync = this.filterDimensions.length === 0 && this.hasRequestedGlobal;
|
|
272
|
+
if (!hasPendingFilters && !needsGlobalSync) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.isProcessing = true;
|
|
276
|
+
try {
|
|
277
|
+
if (this.filterDimensions.length === 0) {
|
|
278
|
+
await this.runGlobalBackfill();
|
|
279
|
+
} else {
|
|
280
|
+
const newFilters = { ...this.pendingFilters };
|
|
281
|
+
this.pendingFilters = {};
|
|
282
|
+
for (const [convexArg, values] of Object.entries(newFilters)) {
|
|
283
|
+
const activeSet = this.activeDimensions.get(convexArg);
|
|
284
|
+
if (activeSet) {
|
|
285
|
+
for (const value of values) {
|
|
286
|
+
activeSet.add(toKey(value));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
await this.runBackfill(newFilters);
|
|
291
|
+
}
|
|
292
|
+
this.updateSubscription();
|
|
293
|
+
if (!this.markedReady && this.callbacks) {
|
|
294
|
+
this.callbacks.markReady();
|
|
295
|
+
this.markedReady = true;
|
|
296
|
+
}
|
|
297
|
+
} finally {
|
|
298
|
+
this.isProcessing = false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Run global backfill for 0-filter case
|
|
303
|
+
*/
|
|
304
|
+
async runGlobalBackfill() {
|
|
305
|
+
try {
|
|
306
|
+
const args = { after: 0 };
|
|
307
|
+
const items = await this.client.query(this.query, args);
|
|
308
|
+
if (Array.isArray(items)) {
|
|
309
|
+
this.handleIncomingData(items);
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error("[ConvexSyncManager] Global backfill error:", error);
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Run backfill query for new filter values to get their full history
|
|
318
|
+
*/
|
|
319
|
+
async runBackfill(newFilters) {
|
|
320
|
+
if (Object.keys(newFilters).length === 0) return;
|
|
321
|
+
try {
|
|
322
|
+
const args = {
|
|
323
|
+
...newFilters,
|
|
324
|
+
after: 0
|
|
325
|
+
};
|
|
326
|
+
const items = await this.client.query(this.query, args);
|
|
327
|
+
if (Array.isArray(items)) {
|
|
328
|
+
this.handleIncomingData(items);
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error("[ConvexSyncManager] Backfill error:", error);
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Build query args from all active dimensions
|
|
337
|
+
*/
|
|
338
|
+
buildQueryArgs(after) {
|
|
339
|
+
const args = { after };
|
|
340
|
+
for (const [convexArg, serializedValues] of this.activeDimensions) {
|
|
341
|
+
const values = [...serializedValues].map((s) => fromKey(s));
|
|
342
|
+
const dim = this.filterDimensions.find((d) => d.convexArg === convexArg);
|
|
343
|
+
args[convexArg] = (dim == null ? void 0 : dim.single) ? values[0] : values;
|
|
344
|
+
}
|
|
345
|
+
return args;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Update the live subscription to cover all active filter values
|
|
349
|
+
*/
|
|
350
|
+
updateSubscription() {
|
|
351
|
+
if (this.currentSubscription) {
|
|
352
|
+
this.currentSubscription();
|
|
353
|
+
this.currentSubscription = null;
|
|
354
|
+
}
|
|
355
|
+
this.messagesSinceSubscription = 0;
|
|
356
|
+
if (this.filterDimensions.length === 0) {
|
|
357
|
+
if (!this.hasRequestedGlobal) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
let hasActiveValues = false;
|
|
362
|
+
for (const activeSet of this.activeDimensions.values()) {
|
|
363
|
+
if (activeSet.size > 0) {
|
|
364
|
+
hasActiveValues = true;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (!hasActiveValues) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const cursor = Math.max(0, this.globalCursor - this.tailOverlapMs);
|
|
373
|
+
const args = this.buildQueryArgs(cursor);
|
|
374
|
+
if ("onUpdate" in this.client) {
|
|
375
|
+
const subscription = this.client.onUpdate(
|
|
376
|
+
this.query,
|
|
377
|
+
args,
|
|
378
|
+
(result) => {
|
|
379
|
+
if (result !== void 0) {
|
|
380
|
+
const items = result;
|
|
381
|
+
if (Array.isArray(items)) {
|
|
382
|
+
this.handleIncomingData(items);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
(error) => {
|
|
387
|
+
console.error(`[ConvexSyncManager] Subscription error:`, error);
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
this.currentSubscription = () => subscription.unsubscribe();
|
|
391
|
+
} else {
|
|
392
|
+
const watch = this.client.watchQuery(this.query, args);
|
|
393
|
+
this.currentSubscription = watch.onUpdate(() => {
|
|
394
|
+
const result = watch.localQueryResult();
|
|
395
|
+
if (result !== void 0) {
|
|
396
|
+
const items = result;
|
|
397
|
+
if (Array.isArray(items)) {
|
|
398
|
+
this.handleIncomingData(items);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Handle incoming data from backfill or subscription
|
|
406
|
+
* Uses LWW (Last-Write-Wins) to resolve conflicts
|
|
407
|
+
*/
|
|
408
|
+
handleIncomingData(items) {
|
|
409
|
+
if (!this.callbacks || items.length === 0) return;
|
|
410
|
+
const { collection, begin, write, commit } = this.callbacks;
|
|
411
|
+
const previousCursor = this.globalCursor;
|
|
412
|
+
let newItemCount = 0;
|
|
413
|
+
begin();
|
|
414
|
+
for (const item of items) {
|
|
415
|
+
const key = this.getKey(item);
|
|
416
|
+
const incomingTs = item[this.updatedAtFieldName];
|
|
417
|
+
if (incomingTs !== void 0 && incomingTs > this.globalCursor) {
|
|
418
|
+
this.globalCursor = incomingTs;
|
|
419
|
+
}
|
|
420
|
+
const existing = collection.get(key);
|
|
421
|
+
if (!existing) {
|
|
422
|
+
write({ type: `insert`, value: item });
|
|
423
|
+
if (incomingTs !== void 0 && incomingTs > previousCursor) {
|
|
424
|
+
newItemCount++;
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
const existingTs = existing[this.updatedAtFieldName];
|
|
428
|
+
if (incomingTs !== void 0 && existingTs !== void 0) {
|
|
429
|
+
if (incomingTs > existingTs) {
|
|
430
|
+
write({ type: `update`, value: item });
|
|
431
|
+
if (incomingTs > previousCursor) {
|
|
432
|
+
newItemCount++;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} else if (incomingTs !== void 0) {
|
|
436
|
+
write({ type: `update`, value: item });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
commit();
|
|
441
|
+
this.messagesSinceSubscription += newItemCount;
|
|
442
|
+
if (this.resubscribeThreshold > 0 && this.messagesSinceSubscription >= this.resubscribeThreshold && this.currentSubscription !== null) {
|
|
443
|
+
this.updateSubscription();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Clean up all resources
|
|
448
|
+
*/
|
|
449
|
+
cleanup() {
|
|
450
|
+
if (this.debounceTimer) {
|
|
451
|
+
clearTimeout(this.debounceTimer);
|
|
452
|
+
this.debounceTimer = null;
|
|
453
|
+
}
|
|
454
|
+
if (this.currentSubscription) {
|
|
455
|
+
this.currentSubscription();
|
|
456
|
+
this.currentSubscription = null;
|
|
457
|
+
}
|
|
458
|
+
for (const activeSet of this.activeDimensions.values()) {
|
|
459
|
+
activeSet.clear();
|
|
460
|
+
}
|
|
461
|
+
this.refCounts.clear();
|
|
462
|
+
this.pendingFilters = {};
|
|
463
|
+
this.globalCursor = 0;
|
|
464
|
+
this.markedReady = false;
|
|
465
|
+
this.hasRequestedGlobal = false;
|
|
466
|
+
this.globalRefCount = 0;
|
|
467
|
+
this.messagesSinceSubscription = 0;
|
|
468
|
+
this.callbacks = null;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Get debug info about current state
|
|
472
|
+
*/
|
|
473
|
+
getDebugInfo() {
|
|
474
|
+
const activeDimensions = {};
|
|
475
|
+
for (const [convexArg, serializedValues] of this.activeDimensions) {
|
|
476
|
+
activeDimensions[convexArg] = [...serializedValues].map((s) => fromKey(s));
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
activeDimensions,
|
|
480
|
+
globalCursor: this.globalCursor,
|
|
481
|
+
pendingFilters: { ...this.pendingFilters },
|
|
482
|
+
hasSubscription: this.currentSubscription !== null,
|
|
483
|
+
markedReady: this.markedReady,
|
|
484
|
+
hasRequestedGlobal: this.hasRequestedGlobal,
|
|
485
|
+
messagesSinceSubscription: this.messagesSinceSubscription
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function isPropRef(expr) {
|
|
490
|
+
return typeof expr === `object` && expr !== null && `type` in expr && expr.type === `ref` && `path` in expr && Array.isArray(expr.path);
|
|
491
|
+
}
|
|
492
|
+
function isValue(expr) {
|
|
493
|
+
return typeof expr === `object` && expr !== null && `type` in expr && expr.type === `val` && `value` in expr;
|
|
494
|
+
}
|
|
495
|
+
function isFunc(expr) {
|
|
496
|
+
return typeof expr === `object` && expr !== null && `type` in expr && expr.type === `func` && `name` in expr && `args` in expr && Array.isArray(expr.args);
|
|
497
|
+
}
|
|
498
|
+
function propRefMatchesField(propRef, fieldName) {
|
|
499
|
+
const { path } = propRef;
|
|
500
|
+
if (path.length === 1 && path[0] === fieldName) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
if (path.length === 2 && path[1] === fieldName) {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
function extractFromEq(func, filterField) {
|
|
509
|
+
if (func.args.length !== 2) return [];
|
|
510
|
+
const [left, right] = func.args;
|
|
511
|
+
if (isPropRef(left) && propRefMatchesField(left, filterField) && isValue(right)) {
|
|
512
|
+
return [right.value];
|
|
513
|
+
}
|
|
514
|
+
if (isValue(left) && isPropRef(right) && propRefMatchesField(right, filterField)) {
|
|
515
|
+
return [left.value];
|
|
516
|
+
}
|
|
517
|
+
return [];
|
|
518
|
+
}
|
|
519
|
+
function extractFromIn(func, filterField) {
|
|
520
|
+
if (func.args.length !== 2) return [];
|
|
521
|
+
const [left, right] = func.args;
|
|
522
|
+
if (isPropRef(left) && propRefMatchesField(left, filterField) && isValue(right)) {
|
|
523
|
+
const val = right.value;
|
|
524
|
+
if (Array.isArray(val)) {
|
|
525
|
+
return val;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return [];
|
|
529
|
+
}
|
|
530
|
+
function walkExpression(expr, filterField) {
|
|
531
|
+
if (!isFunc(expr)) return [];
|
|
532
|
+
const { name, args } = expr;
|
|
533
|
+
const results = [];
|
|
534
|
+
switch (name) {
|
|
535
|
+
case `eq`:
|
|
536
|
+
results.push(...extractFromEq(expr, filterField));
|
|
537
|
+
break;
|
|
538
|
+
case `in`:
|
|
539
|
+
results.push(...extractFromIn(expr, filterField));
|
|
540
|
+
break;
|
|
541
|
+
case `and`:
|
|
542
|
+
case `or`:
|
|
543
|
+
for (const arg of args) {
|
|
544
|
+
results.push(...walkExpression(arg, filterField));
|
|
545
|
+
}
|
|
546
|
+
break;
|
|
547
|
+
// For other functions, recursively check their arguments
|
|
548
|
+
// (in case of nested expressions)
|
|
549
|
+
default:
|
|
550
|
+
for (const arg of args) {
|
|
551
|
+
results.push(...walkExpression(arg, filterField));
|
|
552
|
+
}
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
return results;
|
|
556
|
+
}
|
|
557
|
+
function extractFilterValues(options, filterField) {
|
|
558
|
+
const { where } = options;
|
|
559
|
+
if (!where) {
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
const values = walkExpression(where, filterField);
|
|
563
|
+
const seen = /* @__PURE__ */ new Set();
|
|
564
|
+
const unique = [];
|
|
565
|
+
for (const value of values) {
|
|
566
|
+
const key = toKey(value);
|
|
567
|
+
if (!seen.has(key)) {
|
|
568
|
+
seen.add(key);
|
|
569
|
+
unique.push(value);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return unique;
|
|
573
|
+
}
|
|
574
|
+
function hasFilterField(options, filterField) {
|
|
575
|
+
return extractFilterValues(options, filterField).length > 0;
|
|
576
|
+
}
|
|
577
|
+
function extractMultipleFilterValues(options, filterDimensions) {
|
|
578
|
+
const result = {};
|
|
579
|
+
for (const dim of filterDimensions) {
|
|
580
|
+
const values = extractFilterValues(options, dim.filterField);
|
|
581
|
+
if (values.length > 0) {
|
|
582
|
+
result[dim.convexArg] = values;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
const DEFAULT_UPDATED_AT_FIELD = `updatedAt`;
|
|
588
|
+
const DEFAULT_DEBOUNCE_MS = 50;
|
|
589
|
+
const DEFAULT_TAIL_OVERLAP_MS = 1e4;
|
|
590
|
+
const DEFAULT_RESUBSCRIBE_THRESHOLD = 10;
|
|
591
|
+
function normalizeFilterConfig(filters) {
|
|
592
|
+
if (filters === void 0) return [];
|
|
593
|
+
return Array.isArray(filters) ? filters : [filters];
|
|
594
|
+
}
|
|
595
|
+
function convexCollectionOptions(config) {
|
|
596
|
+
const {
|
|
597
|
+
client,
|
|
598
|
+
query,
|
|
599
|
+
filters,
|
|
600
|
+
updatedAtFieldName = DEFAULT_UPDATED_AT_FIELD,
|
|
601
|
+
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
602
|
+
tailOverlapMs = DEFAULT_TAIL_OVERLAP_MS,
|
|
603
|
+
resubscribeThreshold = DEFAULT_RESUBSCRIBE_THRESHOLD,
|
|
604
|
+
getKey,
|
|
605
|
+
onInsert,
|
|
606
|
+
onUpdate,
|
|
607
|
+
...baseConfig
|
|
608
|
+
} = config;
|
|
609
|
+
const filterDimensions = normalizeFilterConfig(filters);
|
|
610
|
+
const syncManager = new ConvexSyncManager({
|
|
611
|
+
client,
|
|
612
|
+
query,
|
|
613
|
+
filterDimensions,
|
|
614
|
+
updatedAtFieldName,
|
|
615
|
+
debounceMs,
|
|
616
|
+
tailOverlapMs,
|
|
617
|
+
resubscribeThreshold,
|
|
618
|
+
getKey
|
|
619
|
+
});
|
|
620
|
+
const syncConfig = {
|
|
621
|
+
sync: (params) => {
|
|
622
|
+
const { collection, begin, write, commit, markReady } = params;
|
|
623
|
+
syncManager.setCallbacks({
|
|
624
|
+
collection: {
|
|
625
|
+
get: (key) => collection.get(key),
|
|
626
|
+
has: (key) => collection.has(key)
|
|
627
|
+
},
|
|
628
|
+
begin,
|
|
629
|
+
write,
|
|
630
|
+
commit,
|
|
631
|
+
markReady
|
|
632
|
+
});
|
|
633
|
+
return {
|
|
634
|
+
loadSubset: (options) => {
|
|
635
|
+
if (filterDimensions.length === 0) {
|
|
636
|
+
return syncManager.requestFilters({});
|
|
637
|
+
}
|
|
638
|
+
const extracted = extractMultipleFilterValues(options, filterDimensions);
|
|
639
|
+
if (Object.keys(extracted).length === 0) {
|
|
640
|
+
return Promise.resolve();
|
|
641
|
+
}
|
|
642
|
+
return syncManager.requestFilters(extracted);
|
|
643
|
+
},
|
|
644
|
+
unloadSubset: (options) => {
|
|
645
|
+
if (filterDimensions.length === 0) {
|
|
646
|
+
syncManager.releaseFilters({});
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const extracted = extractMultipleFilterValues(options, filterDimensions);
|
|
650
|
+
if (Object.keys(extracted).length > 0) {
|
|
651
|
+
syncManager.releaseFilters(extracted);
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
cleanup: () => {
|
|
655
|
+
syncManager.cleanup();
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
return {
|
|
661
|
+
...baseConfig,
|
|
662
|
+
getKey,
|
|
663
|
+
syncMode: `on-demand`,
|
|
664
|
+
// Always on-demand since we sync based on query predicates
|
|
665
|
+
sync: syncConfig,
|
|
666
|
+
onInsert,
|
|
667
|
+
onUpdate
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
exports.ConvexSyncManager = ConvexSyncManager;
|
|
671
|
+
exports.convexCollectionOptions = convexCollectionOptions;
|
|
672
|
+
exports.deserializeValue = deserializeValue;
|
|
673
|
+
exports.extractFilterValues = extractFilterValues;
|
|
674
|
+
exports.extractMultipleFilterValues = extractMultipleFilterValues;
|
|
675
|
+
exports.fromKey = fromKey;
|
|
676
|
+
exports.hasFilterField = hasFilterField;
|
|
677
|
+
exports.serializeValue = serializeValue;
|
|
678
|
+
exports.toKey = toKey;
|
|
679
|
+
//# sourceMappingURL=index.cjs.map
|