@tanstack/trailbase-db-collection 0.1.76 → 0.1.78
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/dist/cjs/trailbase.cjs +148 -29
- package/dist/cjs/trailbase.cjs.map +1 -1
- package/dist/cjs/trailbase.d.cts +8 -2
- package/dist/esm/trailbase.d.ts +8 -2
- package/dist/esm/trailbase.js +148 -29
- package/dist/esm/trailbase.js.map +1 -1
- package/package.json +8 -6
- package/src/trailbase.ts +210 -35
package/dist/cjs/trailbase.cjs
CHANGED
|
@@ -26,6 +26,8 @@ function trailBaseCollectionOptions(config) {
|
|
|
26
26
|
const serialUpd = (item) => convertPartial(config.serialize, item);
|
|
27
27
|
const serialIns = (item) => convert(config.serialize, item);
|
|
28
28
|
const seenIds = new store.Store(/* @__PURE__ */ new Map());
|
|
29
|
+
const internalSyncMode = config.syncMode ?? `eager`;
|
|
30
|
+
let fullSyncCompleted = false;
|
|
29
31
|
const awaitIds = (ids, timeout = 120 * 1e3) => {
|
|
30
32
|
const completed = (value) => ids.every((id) => value.has(id));
|
|
31
33
|
if (completed(seenIds.state)) {
|
|
@@ -33,13 +35,13 @@ function trailBaseCollectionOptions(config) {
|
|
|
33
35
|
}
|
|
34
36
|
return new Promise((resolve, reject) => {
|
|
35
37
|
const timeoutId = setTimeout(() => {
|
|
36
|
-
unsubscribe();
|
|
38
|
+
sub.unsubscribe();
|
|
37
39
|
reject(new errors.TimeoutWaitingForIdsError(ids.toString()));
|
|
38
40
|
}, timeout);
|
|
39
|
-
const
|
|
40
|
-
if (completed(value
|
|
41
|
+
const sub = seenIds.subscribe((value) => {
|
|
42
|
+
if (completed(value)) {
|
|
41
43
|
clearTimeout(timeoutId);
|
|
42
|
-
unsubscribe();
|
|
44
|
+
sub.unsubscribe();
|
|
43
45
|
resolve();
|
|
44
46
|
}
|
|
45
47
|
});
|
|
@@ -56,37 +58,59 @@ function trailBaseCollectionOptions(config) {
|
|
|
56
58
|
const sync = {
|
|
57
59
|
sync: (params) => {
|
|
58
60
|
const { begin, write, commit, markReady } = params;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
62
|
+
async function load(opts) {
|
|
63
|
+
const lastKey = opts.cursor?.lastKey;
|
|
64
|
+
let cursor = lastKey !== void 0 ? cursors.get(lastKey) : void 0;
|
|
65
|
+
let offset = (opts.offset ?? 0) > 0 ? opts.offset : void 0;
|
|
66
|
+
const order = buildOrder(opts);
|
|
67
|
+
const filters = buildFilters(
|
|
68
|
+
opts,
|
|
69
|
+
config
|
|
70
|
+
);
|
|
71
|
+
let remaining = opts.limit ?? Number.MAX_VALUE;
|
|
72
|
+
if (remaining <= 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
69
75
|
while (true) {
|
|
76
|
+
const limit = Math.min(remaining, 256);
|
|
77
|
+
const response = await config.recordApi.list({
|
|
78
|
+
pagination: {
|
|
79
|
+
limit,
|
|
80
|
+
offset,
|
|
81
|
+
cursor
|
|
82
|
+
},
|
|
83
|
+
order,
|
|
84
|
+
filters
|
|
85
|
+
});
|
|
70
86
|
const length = response.records.length;
|
|
71
|
-
if (length === 0)
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
if (length === 0) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
begin();
|
|
91
|
+
for (let i = 0; i < Math.min(length, remaining); ++i) {
|
|
74
92
|
write({
|
|
75
93
|
type: `insert`,
|
|
76
|
-
value: parse(
|
|
94
|
+
value: parse(response.records[i])
|
|
77
95
|
});
|
|
78
96
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
commit();
|
|
98
|
+
remaining -= length;
|
|
99
|
+
if (length < limit || remaining <= 0) {
|
|
100
|
+
if (response.cursor) {
|
|
101
|
+
cursors.set(
|
|
102
|
+
getKey(parse(response.records.at(-1))),
|
|
103
|
+
response.cursor
|
|
104
|
+
);
|
|
85
105
|
}
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (offset !== void 0) {
|
|
109
|
+
offset += length;
|
|
110
|
+
} else {
|
|
111
|
+
cursor = response.cursor;
|
|
112
|
+
}
|
|
88
113
|
}
|
|
89
|
-
commit();
|
|
90
114
|
}
|
|
91
115
|
async function listen(reader) {
|
|
92
116
|
while (true) {
|
|
@@ -125,7 +149,10 @@ function trailBaseCollectionOptions(config) {
|
|
|
125
149
|
const reader = eventReader = eventStream.getReader();
|
|
126
150
|
listen(reader);
|
|
127
151
|
try {
|
|
128
|
-
|
|
152
|
+
if (internalSyncMode === `eager`) {
|
|
153
|
+
await load({});
|
|
154
|
+
fullSyncCompleted = true;
|
|
155
|
+
}
|
|
129
156
|
} catch (e) {
|
|
130
157
|
cancelEventReader();
|
|
131
158
|
throw e;
|
|
@@ -150,9 +177,21 @@ function trailBaseCollectionOptions(config) {
|
|
|
150
177
|
reader.closed.finally(() => clearInterval(periodicCleanupTask));
|
|
151
178
|
}
|
|
152
179
|
start();
|
|
180
|
+
if (internalSyncMode === `eager`) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
loadSubset: load,
|
|
185
|
+
getSyncMetadata: () => ({
|
|
186
|
+
syncMode: internalSyncMode
|
|
187
|
+
})
|
|
188
|
+
};
|
|
153
189
|
},
|
|
154
190
|
// Expose the getSyncMetadata function
|
|
155
|
-
getSyncMetadata:
|
|
191
|
+
getSyncMetadata: () => ({
|
|
192
|
+
syncMode: internalSyncMode,
|
|
193
|
+
fullSyncComplete: fullSyncCompleted
|
|
194
|
+
})
|
|
156
195
|
};
|
|
157
196
|
return {
|
|
158
197
|
...config,
|
|
@@ -202,5 +241,85 @@ function trailBaseCollectionOptions(config) {
|
|
|
202
241
|
}
|
|
203
242
|
};
|
|
204
243
|
}
|
|
244
|
+
function buildOrder(opts) {
|
|
245
|
+
return opts.orderBy?.map((o) => {
|
|
246
|
+
switch (o.expression.type) {
|
|
247
|
+
case "ref": {
|
|
248
|
+
const field = o.expression.path[0];
|
|
249
|
+
if (o.compareOptions.direction == "asc") {
|
|
250
|
+
return `+${field}`;
|
|
251
|
+
}
|
|
252
|
+
return `-${field}`;
|
|
253
|
+
}
|
|
254
|
+
default: {
|
|
255
|
+
console.warn(
|
|
256
|
+
"Skipping unsupported order clause:",
|
|
257
|
+
JSON.stringify(o.expression)
|
|
258
|
+
);
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}).filter((f) => f !== void 0);
|
|
263
|
+
}
|
|
264
|
+
function buildCompareOp(name) {
|
|
265
|
+
switch (name) {
|
|
266
|
+
case "eq":
|
|
267
|
+
return "equal";
|
|
268
|
+
case "ne":
|
|
269
|
+
return "notEqual";
|
|
270
|
+
case "gt":
|
|
271
|
+
return "greaterThan";
|
|
272
|
+
case "gte":
|
|
273
|
+
return "greaterThanEqual";
|
|
274
|
+
case "lt":
|
|
275
|
+
return "lessThan";
|
|
276
|
+
case "lte":
|
|
277
|
+
return "lessThanEqual";
|
|
278
|
+
default:
|
|
279
|
+
return void 0;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function buildFilters(opts, config) {
|
|
283
|
+
const where = opts.where;
|
|
284
|
+
if (where === void 0) {
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
function serializeValue(column, value) {
|
|
288
|
+
const conv = config.serialize[column];
|
|
289
|
+
if (conv) {
|
|
290
|
+
return `${conv(value)}`;
|
|
291
|
+
}
|
|
292
|
+
if (typeof value === "boolean") {
|
|
293
|
+
return value ? "1" : "0";
|
|
294
|
+
}
|
|
295
|
+
return `${value}`;
|
|
296
|
+
}
|
|
297
|
+
switch (where.type) {
|
|
298
|
+
case "func": {
|
|
299
|
+
const field = where.args[0];
|
|
300
|
+
const val = where.args[1];
|
|
301
|
+
const op = buildCompareOp(where.name);
|
|
302
|
+
if (op === void 0) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
if (field?.type === "ref" && val?.type === "val") {
|
|
306
|
+
const column = field.path.at(0);
|
|
307
|
+
if (column) {
|
|
308
|
+
const f = [
|
|
309
|
+
{
|
|
310
|
+
column: field.path.at(0) ?? "",
|
|
311
|
+
op,
|
|
312
|
+
value: serializeValue(column, val.value)
|
|
313
|
+
}
|
|
314
|
+
];
|
|
315
|
+
return f;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
console.warn("where clause which is not (yet) supported", opts.where);
|
|
322
|
+
return void 0;
|
|
323
|
+
}
|
|
205
324
|
exports.trailBaseCollectionOptions = trailBaseCollectionOptions;
|
|
206
325
|
//# sourceMappingURL=trailbase.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trailbase.cjs","sources":["../../src/trailbase.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unnecessary-condition */\nimport { Store } from '@tanstack/store'\nimport {\n ExpectedDeleteTypeError,\n ExpectedInsertTypeError,\n ExpectedUpdateTypeError,\n TimeoutWaitingForIdsError,\n} from './errors'\nimport type { Event, RecordApi } from 'trailbase'\n\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from '@tanstack/db'\n\ntype ShapeOf<T> = Record<keyof T, unknown>\ntype Conversion<I, O> = (value: I) => O\n\ntype OptionalConversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = {\n // Excludes all keys that require a conversation.\n [K in keyof InputType as InputType[K] extends OutputType[K]\n ? K\n : never]?: Conversion<InputType[K], OutputType[K]>\n}\n\ntype RequiredConversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = {\n // Excludes all keys that do not strictly require a conversation.\n [K in keyof InputType as InputType[K] extends OutputType[K]\n ? never\n : K]: Conversion<InputType[K], OutputType[K]>\n}\n\ntype Conversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = OptionalConversions<InputType, OutputType> &\n RequiredConversions<InputType, OutputType>\n\nfunction convert<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: InputType,\n): OutputType {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\nfunction convertPartial<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: Partial<InputType>,\n): Partial<OutputType> {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\n/**\n * Configuration interface for Trailbase Collection\n */\nexport interface TrailBaseCollectionConfig<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n> extends Omit<\n BaseCollectionConfig<TItem, TKey>,\n `onInsert` | `onUpdate` | `onDelete`\n> {\n /**\n * Record API name\n */\n recordApi: RecordApi<TRecord>\n\n parse: Conversions<TRecord, TItem>\n serialize: Conversions<TItem, TRecord>\n}\n\nexport type AwaitTxIdFn = (txId: string, timeout?: number) => Promise<boolean>\n\nexport interface TrailBaseCollectionUtils extends UtilsRecord {\n cancel: () => void\n}\n\nexport function trailBaseCollectionOptions<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n>(\n config: TrailBaseCollectionConfig<TItem, TRecord, TKey>,\n): CollectionConfig<TItem, TKey, never, TrailBaseCollectionUtils> & {\n utils: TrailBaseCollectionUtils\n} {\n const getKey = config.getKey\n\n const parse = (record: TRecord) =>\n convert<TRecord, TItem>(config.parse, record)\n const serialUpd = (item: Partial<TItem>) =>\n convertPartial<TItem, TRecord>(config.serialize, item)\n const serialIns = (item: TItem) =>\n convert<TItem, TRecord>(config.serialize, item)\n\n const seenIds = new Store(new Map<string, number>())\n\n const awaitIds = (\n ids: Array<string>,\n timeout: number = 120 * 1000,\n ): Promise<void> => {\n const completed = (value: Map<string, number>) =>\n ids.every((id) => value.has(id))\n if (completed(seenIds.state)) {\n return Promise.resolve()\n }\n\n return new Promise<void>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribe()\n reject(new TimeoutWaitingForIdsError(ids.toString()))\n }, timeout)\n\n const unsubscribe = seenIds.subscribe((value) => {\n if (completed(value.currentVal)) {\n clearTimeout(timeoutId)\n unsubscribe()\n resolve()\n }\n })\n })\n }\n\n let eventReader: ReadableStreamDefaultReader<Event> | undefined\n const cancelEventReader = () => {\n if (eventReader) {\n eventReader.cancel()\n eventReader.releaseLock()\n eventReader = undefined\n }\n }\n\n type SyncParams = Parameters<SyncConfig<TItem, TKey>[`sync`]>[0]\n const sync = {\n sync: (params: SyncParams) => {\n const { begin, write, commit, markReady } = params\n\n // Initial fetch.\n async function initialFetch() {\n const limit = 256\n let response = await config.recordApi.list({\n pagination: {\n limit,\n },\n })\n let cursor = response.cursor\n let got = 0\n\n begin()\n\n while (true) {\n const length = response.records.length\n if (length === 0) break\n\n got = got + length\n for (const item of response.records) {\n write({\n type: `insert`,\n value: parse(item),\n })\n }\n\n if (length < limit) break\n\n response = await config.recordApi.list({\n pagination: {\n limit,\n cursor,\n offset: cursor === undefined ? got : undefined,\n },\n })\n cursor = response.cursor\n }\n\n commit()\n }\n\n // Afterwards subscribe.\n async function listen(reader: ReadableStreamDefaultReader<Event>) {\n while (true) {\n const { done, value: event } = await reader.read()\n\n if (done || !event) {\n reader.releaseLock()\n eventReader = undefined\n return\n }\n\n begin()\n let value: TItem | undefined\n if (`Insert` in event) {\n value = parse(event.Insert as TRecord)\n write({ type: `insert`, value })\n } else if (`Delete` in event) {\n value = parse(event.Delete as TRecord)\n write({ type: `delete`, value })\n } else if (`Update` in event) {\n value = parse(event.Update as TRecord)\n write({ type: `update`, value })\n } else {\n console.error(`Error: ${event.Error}`)\n }\n commit()\n\n if (value) {\n seenIds.setState((curr: Map<string, number>) => {\n const newIds = new Map(curr)\n newIds.set(String(getKey(value)), Date.now())\n return newIds\n })\n }\n }\n }\n\n async function start() {\n const eventStream = await config.recordApi.subscribe(`*`)\n const reader = (eventReader = eventStream.getReader())\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n listen(reader)\n\n try {\n await initialFetch()\n } catch (e) {\n cancelEventReader()\n throw e\n } finally {\n // Mark ready both if everything went well or if there's an error to\n // avoid blocking apps waiting for `.preload()` to finish.\n markReady()\n }\n\n // Lastly, start a periodic cleanup task that will be removed when the\n // reader closes.\n const periodicCleanupTask = setInterval(() => {\n seenIds.setState((curr) => {\n const now = Date.now()\n let anyExpired = false\n\n const notExpired = Array.from(curr.entries()).filter(([_, v]) => {\n const expired = now - v > 300 * 1000\n anyExpired = anyExpired || expired\n return !expired\n })\n\n if (anyExpired) {\n return new Map(notExpired)\n }\n return curr\n })\n }, 120 * 1000)\n\n reader.closed.finally(() => clearInterval(periodicCleanupTask))\n }\n\n start()\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }\n\n return {\n ...config,\n sync,\n getKey,\n onInsert: async (\n params: InsertMutationFnParams<TItem, TKey>,\n ): Promise<Array<number | string>> => {\n const ids = await config.recordApi.createBulk(\n params.transaction.mutations.map((tx) => {\n const { type, modified } = tx\n if (type !== `insert`) {\n throw new ExpectedInsertTypeError(type)\n }\n return serialIns(modified)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly added to the local\n // DB by the subscription.\n await awaitIds(ids.map((id) => String(id)))\n\n return ids\n },\n onUpdate: async (params: UpdateMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, changes, key } = tx\n if (type !== `update`) {\n throw new ExpectedUpdateTypeError(type)\n }\n\n await config.recordApi.update(key, serialUpd(changes))\n\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n onDelete: async (params: DeleteMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, key } = tx\n if (type !== `delete`) {\n throw new ExpectedDeleteTypeError(type)\n }\n\n await config.recordApi.delete(key)\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n utils: {\n cancel: cancelEventReader,\n },\n }\n}\n"],"names":["Store","TimeoutWaitingForIdsError","ExpectedInsertTypeError","ExpectedUpdateTypeError","ExpectedDeleteTypeError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"trailbase.cjs","sources":["../../src/trailbase.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unnecessary-condition */\nimport { Store } from '@tanstack/store'\nimport {\n ExpectedDeleteTypeError,\n ExpectedInsertTypeError,\n ExpectedUpdateTypeError,\n TimeoutWaitingForIdsError,\n} from './errors'\nimport type { OrderByClause } from '../../db/dist/esm/query/ir'\nimport type { CompareOp, Event, FilterOrComposite, RecordApi } from 'trailbase'\n\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n LoadSubsetOptions,\n SyncConfig,\n SyncMode,\n UpdateMutationFnParams,\n UtilsRecord,\n} from '@tanstack/db'\n\ntype ShapeOf<T> = Record<keyof T, unknown>\ntype Conversion<I, O> = (value: I) => O\n\ntype OptionalConversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = {\n // Excludes all keys that require a conversation.\n [K in keyof InputType as InputType[K] extends OutputType[K]\n ? K\n : never]?: Conversion<InputType[K], OutputType[K]>\n}\n\ntype RequiredConversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = {\n // Excludes all keys that do not strictly require a conversation.\n [K in keyof InputType as InputType[K] extends OutputType[K]\n ? never\n : K]: Conversion<InputType[K], OutputType[K]>\n}\n\ntype Conversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = OptionalConversions<InputType, OutputType> &\n RequiredConversions<InputType, OutputType>\n\nfunction convert<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: InputType,\n): OutputType {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\nfunction convertPartial<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: Partial<InputType>,\n): Partial<OutputType> {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\nexport type TrailBaseSyncMode = SyncMode\n\n/**\n * Configuration interface for Trailbase Collection\n */\nexport interface TrailBaseCollectionConfig<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n> extends Omit<\n BaseCollectionConfig<TItem, TKey>,\n `onInsert` | `onUpdate` | `onDelete` | `syncMode`\n> {\n /**\n * Record API name\n */\n recordApi: RecordApi<TRecord>\n\n /**\n * The mode of sync to use for the collection.\n * @default `eager`\n */\n syncMode?: TrailBaseSyncMode\n\n parse: Conversions<TRecord, TItem>\n serialize: Conversions<TItem, TRecord>\n}\n\nexport type AwaitTxIdFn = (txId: string, timeout?: number) => Promise<boolean>\n\nexport interface TrailBaseCollectionUtils extends UtilsRecord {\n cancel: () => void\n}\n\nexport function trailBaseCollectionOptions<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n>(\n config: TrailBaseCollectionConfig<TItem, TRecord, TKey>,\n): CollectionConfig<TItem, TKey, never, TrailBaseCollectionUtils> & {\n utils: TrailBaseCollectionUtils\n} {\n const getKey = config.getKey\n\n const parse = (record: TRecord) =>\n convert<TRecord, TItem>(config.parse, record)\n const serialUpd = (item: Partial<TItem>) =>\n convertPartial<TItem, TRecord>(config.serialize, item)\n const serialIns = (item: TItem) =>\n convert<TItem, TRecord>(config.serialize, item)\n\n const seenIds = new Store(new Map<string, number>())\n\n const internalSyncMode = config.syncMode ?? `eager`\n let fullSyncCompleted = false\n\n const awaitIds = (\n ids: Array<string>,\n timeout: number = 120 * 1000,\n ): Promise<void> => {\n const completed = (value: Map<string, number>) =>\n ids.every((id) => value.has(id))\n if (completed(seenIds.state)) {\n return Promise.resolve()\n }\n\n return new Promise<void>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n sub.unsubscribe()\n reject(new TimeoutWaitingForIdsError(ids.toString()))\n }, timeout)\n\n const sub = seenIds.subscribe((value) => {\n if (completed(value)) {\n clearTimeout(timeoutId)\n sub.unsubscribe()\n resolve()\n }\n })\n })\n }\n\n let eventReader: ReadableStreamDefaultReader<Event> | undefined\n const cancelEventReader = () => {\n if (eventReader) {\n eventReader.cancel()\n eventReader.releaseLock()\n eventReader = undefined\n }\n }\n\n type SyncParams = Parameters<SyncConfig<TItem, TKey>[`sync`]>[0]\n const sync = {\n sync: (params: SyncParams) => {\n const { begin, write, commit, markReady } = params\n\n // NOTE: We cache cursors from prior fetches. TanStack/db expects that\n // cursors can be derived from a key, which is not true for TB, since\n // cursors are encrypted. This is leaky and therefore not ideal.\n const cursors = new Map<string | number, string>()\n\n // Load (more) data.\n async function load(opts: LoadSubsetOptions) {\n const lastKey = opts.cursor?.lastKey\n let cursor: string | undefined =\n lastKey !== undefined ? cursors.get(lastKey) : undefined\n let offset: number | undefined =\n (opts.offset ?? 0) > 0 ? opts.offset : undefined\n\n const order: Array<string> | undefined = buildOrder(opts)\n const filters: Array<FilterOrComposite> | undefined = buildFilters(\n opts,\n config,\n )\n\n let remaining: number = opts.limit ?? Number.MAX_VALUE\n if (remaining <= 0) {\n return\n }\n\n while (true) {\n const limit = Math.min(remaining, 256)\n const response = await config.recordApi.list({\n pagination: {\n limit,\n offset,\n cursor,\n },\n order,\n filters,\n })\n\n const length = response.records.length\n if (length === 0) {\n // Drained - read everything.\n break\n }\n\n begin()\n\n for (let i = 0; i < Math.min(length, remaining); ++i) {\n write({\n type: `insert`,\n value: parse(response.records[i]!),\n })\n }\n\n commit()\n\n remaining -= length\n\n // Drained or read enough.\n if (length < limit || remaining <= 0) {\n if (response.cursor) {\n cursors.set(\n getKey(parse(response.records.at(-1)!)),\n response.cursor,\n )\n }\n break\n }\n\n // Update params for next iteration.\n if (offset !== undefined) {\n offset += length\n } else {\n cursor = response.cursor\n }\n }\n }\n\n // Afterwards subscribe.\n async function listen(reader: ReadableStreamDefaultReader<Event>) {\n while (true) {\n const { done, value: event } = await reader.read()\n\n if (done || !event) {\n reader.releaseLock()\n eventReader = undefined\n return\n }\n\n begin()\n let value: TItem | undefined\n if (`Insert` in event) {\n value = parse(event.Insert as TRecord)\n write({ type: `insert`, value })\n } else if (`Delete` in event) {\n value = parse(event.Delete as TRecord)\n write({ type: `delete`, value })\n } else if (`Update` in event) {\n value = parse(event.Update as TRecord)\n write({ type: `update`, value })\n } else {\n console.error(`Error: ${event.Error}`)\n }\n commit()\n\n if (value) {\n seenIds.setState((curr: Map<string, number>) => {\n const newIds = new Map(curr)\n newIds.set(String(getKey(value)), Date.now())\n return newIds\n })\n }\n }\n }\n\n async function start() {\n const eventStream = await config.recordApi.subscribe(`*`)\n const reader = (eventReader = eventStream.getReader())\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n listen(reader)\n\n try {\n // Eager mode: perform initial fetch to populate everything\n if (internalSyncMode === `eager`) {\n // Load everything on initial load.\n await load({})\n fullSyncCompleted = true\n }\n } catch (e) {\n cancelEventReader()\n throw e\n } finally {\n // Mark ready both if everything went well or if there's an error to\n // avoid blocking apps waiting for `.preload()` to finish.\n markReady()\n }\n\n // Lastly, start a periodic cleanup task that will be removed when the\n // reader closes.\n const periodicCleanupTask = setInterval(() => {\n seenIds.setState((curr) => {\n const now = Date.now()\n let anyExpired = false\n\n const notExpired = Array.from(curr.entries()).filter(([_, v]) => {\n const expired = now - v > 300 * 1000\n anyExpired = anyExpired || expired\n return !expired\n })\n\n if (anyExpired) {\n return new Map(notExpired)\n }\n return curr\n })\n }, 120 * 1000)\n\n reader.closed.finally(() => clearInterval(periodicCleanupTask))\n }\n\n start()\n\n // Eager mode doesn't need subset loading\n if (internalSyncMode === `eager`) {\n return\n }\n\n return {\n loadSubset: load,\n getSyncMetadata: () =>\n ({\n syncMode: internalSyncMode,\n }) as const,\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: () =>\n ({\n syncMode: internalSyncMode,\n fullSyncComplete: fullSyncCompleted,\n }) as const,\n }\n\n return {\n ...config,\n sync,\n getKey,\n onInsert: async (\n params: InsertMutationFnParams<TItem, TKey>,\n ): Promise<Array<number | string>> => {\n const ids = await config.recordApi.createBulk(\n params.transaction.mutations.map((tx) => {\n const { type, modified } = tx\n if (type !== `insert`) {\n throw new ExpectedInsertTypeError(type)\n }\n return serialIns(modified)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly added to the local\n // DB by the subscription.\n await awaitIds(ids.map((id) => String(id)))\n\n return ids\n },\n onUpdate: async (params: UpdateMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, changes, key } = tx\n if (type !== `update`) {\n throw new ExpectedUpdateTypeError(type)\n }\n\n await config.recordApi.update(key, serialUpd(changes))\n\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n onDelete: async (params: DeleteMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, key } = tx\n if (type !== `delete`) {\n throw new ExpectedDeleteTypeError(type)\n }\n\n await config.recordApi.delete(key)\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n utils: {\n cancel: cancelEventReader,\n },\n }\n}\n\nfunction buildOrder(opts: LoadSubsetOptions): undefined | Array<string> {\n return opts.orderBy\n ?.map((o: OrderByClause) => {\n switch (o.expression.type) {\n case 'ref': {\n const field = o.expression.path[0]\n if (o.compareOptions.direction == 'asc') {\n return `+${field}`\n }\n return `-${field}`\n }\n default: {\n console.warn(\n 'Skipping unsupported order clause:',\n JSON.stringify(o.expression),\n )\n return undefined\n }\n }\n })\n .filter((f: string | undefined) => f !== undefined)\n}\n\nfunction buildCompareOp(name: string): CompareOp | undefined {\n switch (name) {\n case 'eq':\n return 'equal'\n case 'ne':\n return 'notEqual'\n case 'gt':\n return 'greaterThan'\n case 'gte':\n return 'greaterThanEqual'\n case 'lt':\n return 'lessThan'\n case 'lte':\n return 'lessThanEqual'\n default:\n return undefined\n }\n}\n\nfunction buildFilters<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n>(\n opts: LoadSubsetOptions,\n config: TrailBaseCollectionConfig<TItem, TRecord, TKey>,\n): undefined | Array<FilterOrComposite> {\n const where = opts.where\n if (where === undefined) {\n return undefined\n }\n\n function serializeValue<T = any>(column: string, value: T): string {\n const conv = (config.serialize as any)[column]\n if (conv) {\n return `${conv(value)}`\n }\n\n if (typeof value === 'boolean') {\n return value ? '1' : '0'\n }\n\n return `${value}`\n }\n\n switch (where.type) {\n case 'func': {\n const field = where.args[0]\n const val = where.args[1]\n\n const op = buildCompareOp(where.name)\n if (op === undefined) {\n break\n }\n\n if (field?.type === 'ref' && val?.type === 'val') {\n const column = field.path.at(0)\n if (column) {\n const f = [\n {\n column: field.path.at(0) ?? '',\n op,\n value: serializeValue(column, val.value),\n },\n ]\n\n return f\n }\n }\n break\n }\n case 'ref':\n case 'val':\n break\n }\n\n console.warn('where clause which is not (yet) supported', opts.where)\n\n return undefined\n}\n"],"names":["Store","TimeoutWaitingForIdsError","ExpectedInsertTypeError","ExpectedUpdateTypeError","ExpectedDeleteTypeError"],"mappings":"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"}
|
package/dist/cjs/trailbase.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RecordApi } from 'trailbase';
|
|
2
|
-
import { BaseCollectionConfig, CollectionConfig, UtilsRecord } from '@tanstack/db';
|
|
2
|
+
import { BaseCollectionConfig, CollectionConfig, SyncMode, UtilsRecord } from '@tanstack/db';
|
|
3
3
|
type ShapeOf<T> = Record<keyof T, unknown>;
|
|
4
4
|
type Conversion<I, O> = (value: I) => O;
|
|
5
5
|
type OptionalConversions<InputType extends ShapeOf<OutputType>, OutputType extends ShapeOf<InputType>> = {
|
|
@@ -9,14 +9,20 @@ type RequiredConversions<InputType extends ShapeOf<OutputType>, OutputType exten
|
|
|
9
9
|
[K in keyof InputType as InputType[K] extends OutputType[K] ? never : K]: Conversion<InputType[K], OutputType[K]>;
|
|
10
10
|
};
|
|
11
11
|
type Conversions<InputType extends ShapeOf<OutputType>, OutputType extends ShapeOf<InputType>> = OptionalConversions<InputType, OutputType> & RequiredConversions<InputType, OutputType>;
|
|
12
|
+
export type TrailBaseSyncMode = SyncMode;
|
|
12
13
|
/**
|
|
13
14
|
* Configuration interface for Trailbase Collection
|
|
14
15
|
*/
|
|
15
|
-
export interface TrailBaseCollectionConfig<TItem extends ShapeOf<TRecord>, TRecord extends ShapeOf<TItem> = TItem, TKey extends string | number = string | number> extends Omit<BaseCollectionConfig<TItem, TKey>, `onInsert` | `onUpdate` | `onDelete`> {
|
|
16
|
+
export interface TrailBaseCollectionConfig<TItem extends ShapeOf<TRecord>, TRecord extends ShapeOf<TItem> = TItem, TKey extends string | number = string | number> extends Omit<BaseCollectionConfig<TItem, TKey>, `onInsert` | `onUpdate` | `onDelete` | `syncMode`> {
|
|
16
17
|
/**
|
|
17
18
|
* Record API name
|
|
18
19
|
*/
|
|
19
20
|
recordApi: RecordApi<TRecord>;
|
|
21
|
+
/**
|
|
22
|
+
* The mode of sync to use for the collection.
|
|
23
|
+
* @default `eager`
|
|
24
|
+
*/
|
|
25
|
+
syncMode?: TrailBaseSyncMode;
|
|
20
26
|
parse: Conversions<TRecord, TItem>;
|
|
21
27
|
serialize: Conversions<TItem, TRecord>;
|
|
22
28
|
}
|
package/dist/esm/trailbase.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RecordApi } from 'trailbase';
|
|
2
|
-
import { BaseCollectionConfig, CollectionConfig, UtilsRecord } from '@tanstack/db';
|
|
2
|
+
import { BaseCollectionConfig, CollectionConfig, SyncMode, UtilsRecord } from '@tanstack/db';
|
|
3
3
|
type ShapeOf<T> = Record<keyof T, unknown>;
|
|
4
4
|
type Conversion<I, O> = (value: I) => O;
|
|
5
5
|
type OptionalConversions<InputType extends ShapeOf<OutputType>, OutputType extends ShapeOf<InputType>> = {
|
|
@@ -9,14 +9,20 @@ type RequiredConversions<InputType extends ShapeOf<OutputType>, OutputType exten
|
|
|
9
9
|
[K in keyof InputType as InputType[K] extends OutputType[K] ? never : K]: Conversion<InputType[K], OutputType[K]>;
|
|
10
10
|
};
|
|
11
11
|
type Conversions<InputType extends ShapeOf<OutputType>, OutputType extends ShapeOf<InputType>> = OptionalConversions<InputType, OutputType> & RequiredConversions<InputType, OutputType>;
|
|
12
|
+
export type TrailBaseSyncMode = SyncMode;
|
|
12
13
|
/**
|
|
13
14
|
* Configuration interface for Trailbase Collection
|
|
14
15
|
*/
|
|
15
|
-
export interface TrailBaseCollectionConfig<TItem extends ShapeOf<TRecord>, TRecord extends ShapeOf<TItem> = TItem, TKey extends string | number = string | number> extends Omit<BaseCollectionConfig<TItem, TKey>, `onInsert` | `onUpdate` | `onDelete`> {
|
|
16
|
+
export interface TrailBaseCollectionConfig<TItem extends ShapeOf<TRecord>, TRecord extends ShapeOf<TItem> = TItem, TKey extends string | number = string | number> extends Omit<BaseCollectionConfig<TItem, TKey>, `onInsert` | `onUpdate` | `onDelete` | `syncMode`> {
|
|
16
17
|
/**
|
|
17
18
|
* Record API name
|
|
18
19
|
*/
|
|
19
20
|
recordApi: RecordApi<TRecord>;
|
|
21
|
+
/**
|
|
22
|
+
* The mode of sync to use for the collection.
|
|
23
|
+
* @default `eager`
|
|
24
|
+
*/
|
|
25
|
+
syncMode?: TrailBaseSyncMode;
|
|
20
26
|
parse: Conversions<TRecord, TItem>;
|
|
21
27
|
serialize: Conversions<TItem, TRecord>;
|
|
22
28
|
}
|
package/dist/esm/trailbase.js
CHANGED
|
@@ -24,6 +24,8 @@ function trailBaseCollectionOptions(config) {
|
|
|
24
24
|
const serialUpd = (item) => convertPartial(config.serialize, item);
|
|
25
25
|
const serialIns = (item) => convert(config.serialize, item);
|
|
26
26
|
const seenIds = new Store(/* @__PURE__ */ new Map());
|
|
27
|
+
const internalSyncMode = config.syncMode ?? `eager`;
|
|
28
|
+
let fullSyncCompleted = false;
|
|
27
29
|
const awaitIds = (ids, timeout = 120 * 1e3) => {
|
|
28
30
|
const completed = (value) => ids.every((id) => value.has(id));
|
|
29
31
|
if (completed(seenIds.state)) {
|
|
@@ -31,13 +33,13 @@ function trailBaseCollectionOptions(config) {
|
|
|
31
33
|
}
|
|
32
34
|
return new Promise((resolve, reject) => {
|
|
33
35
|
const timeoutId = setTimeout(() => {
|
|
34
|
-
unsubscribe();
|
|
36
|
+
sub.unsubscribe();
|
|
35
37
|
reject(new TimeoutWaitingForIdsError(ids.toString()));
|
|
36
38
|
}, timeout);
|
|
37
|
-
const
|
|
38
|
-
if (completed(value
|
|
39
|
+
const sub = seenIds.subscribe((value) => {
|
|
40
|
+
if (completed(value)) {
|
|
39
41
|
clearTimeout(timeoutId);
|
|
40
|
-
unsubscribe();
|
|
42
|
+
sub.unsubscribe();
|
|
41
43
|
resolve();
|
|
42
44
|
}
|
|
43
45
|
});
|
|
@@ -54,37 +56,59 @@ function trailBaseCollectionOptions(config) {
|
|
|
54
56
|
const sync = {
|
|
55
57
|
sync: (params) => {
|
|
56
58
|
const { begin, write, commit, markReady } = params;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
60
|
+
async function load(opts) {
|
|
61
|
+
const lastKey = opts.cursor?.lastKey;
|
|
62
|
+
let cursor = lastKey !== void 0 ? cursors.get(lastKey) : void 0;
|
|
63
|
+
let offset = (opts.offset ?? 0) > 0 ? opts.offset : void 0;
|
|
64
|
+
const order = buildOrder(opts);
|
|
65
|
+
const filters = buildFilters(
|
|
66
|
+
opts,
|
|
67
|
+
config
|
|
68
|
+
);
|
|
69
|
+
let remaining = opts.limit ?? Number.MAX_VALUE;
|
|
70
|
+
if (remaining <= 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
67
73
|
while (true) {
|
|
74
|
+
const limit = Math.min(remaining, 256);
|
|
75
|
+
const response = await config.recordApi.list({
|
|
76
|
+
pagination: {
|
|
77
|
+
limit,
|
|
78
|
+
offset,
|
|
79
|
+
cursor
|
|
80
|
+
},
|
|
81
|
+
order,
|
|
82
|
+
filters
|
|
83
|
+
});
|
|
68
84
|
const length = response.records.length;
|
|
69
|
-
if (length === 0)
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
if (length === 0) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
begin();
|
|
89
|
+
for (let i = 0; i < Math.min(length, remaining); ++i) {
|
|
72
90
|
write({
|
|
73
91
|
type: `insert`,
|
|
74
|
-
value: parse(
|
|
92
|
+
value: parse(response.records[i])
|
|
75
93
|
});
|
|
76
94
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
commit();
|
|
96
|
+
remaining -= length;
|
|
97
|
+
if (length < limit || remaining <= 0) {
|
|
98
|
+
if (response.cursor) {
|
|
99
|
+
cursors.set(
|
|
100
|
+
getKey(parse(response.records.at(-1))),
|
|
101
|
+
response.cursor
|
|
102
|
+
);
|
|
83
103
|
}
|
|
84
|
-
|
|
85
|
-
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (offset !== void 0) {
|
|
107
|
+
offset += length;
|
|
108
|
+
} else {
|
|
109
|
+
cursor = response.cursor;
|
|
110
|
+
}
|
|
86
111
|
}
|
|
87
|
-
commit();
|
|
88
112
|
}
|
|
89
113
|
async function listen(reader) {
|
|
90
114
|
while (true) {
|
|
@@ -123,7 +147,10 @@ function trailBaseCollectionOptions(config) {
|
|
|
123
147
|
const reader = eventReader = eventStream.getReader();
|
|
124
148
|
listen(reader);
|
|
125
149
|
try {
|
|
126
|
-
|
|
150
|
+
if (internalSyncMode === `eager`) {
|
|
151
|
+
await load({});
|
|
152
|
+
fullSyncCompleted = true;
|
|
153
|
+
}
|
|
127
154
|
} catch (e) {
|
|
128
155
|
cancelEventReader();
|
|
129
156
|
throw e;
|
|
@@ -148,9 +175,21 @@ function trailBaseCollectionOptions(config) {
|
|
|
148
175
|
reader.closed.finally(() => clearInterval(periodicCleanupTask));
|
|
149
176
|
}
|
|
150
177
|
start();
|
|
178
|
+
if (internalSyncMode === `eager`) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
loadSubset: load,
|
|
183
|
+
getSyncMetadata: () => ({
|
|
184
|
+
syncMode: internalSyncMode
|
|
185
|
+
})
|
|
186
|
+
};
|
|
151
187
|
},
|
|
152
188
|
// Expose the getSyncMetadata function
|
|
153
|
-
getSyncMetadata:
|
|
189
|
+
getSyncMetadata: () => ({
|
|
190
|
+
syncMode: internalSyncMode,
|
|
191
|
+
fullSyncComplete: fullSyncCompleted
|
|
192
|
+
})
|
|
154
193
|
};
|
|
155
194
|
return {
|
|
156
195
|
...config,
|
|
@@ -200,6 +239,86 @@ function trailBaseCollectionOptions(config) {
|
|
|
200
239
|
}
|
|
201
240
|
};
|
|
202
241
|
}
|
|
242
|
+
function buildOrder(opts) {
|
|
243
|
+
return opts.orderBy?.map((o) => {
|
|
244
|
+
switch (o.expression.type) {
|
|
245
|
+
case "ref": {
|
|
246
|
+
const field = o.expression.path[0];
|
|
247
|
+
if (o.compareOptions.direction == "asc") {
|
|
248
|
+
return `+${field}`;
|
|
249
|
+
}
|
|
250
|
+
return `-${field}`;
|
|
251
|
+
}
|
|
252
|
+
default: {
|
|
253
|
+
console.warn(
|
|
254
|
+
"Skipping unsupported order clause:",
|
|
255
|
+
JSON.stringify(o.expression)
|
|
256
|
+
);
|
|
257
|
+
return void 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}).filter((f) => f !== void 0);
|
|
261
|
+
}
|
|
262
|
+
function buildCompareOp(name) {
|
|
263
|
+
switch (name) {
|
|
264
|
+
case "eq":
|
|
265
|
+
return "equal";
|
|
266
|
+
case "ne":
|
|
267
|
+
return "notEqual";
|
|
268
|
+
case "gt":
|
|
269
|
+
return "greaterThan";
|
|
270
|
+
case "gte":
|
|
271
|
+
return "greaterThanEqual";
|
|
272
|
+
case "lt":
|
|
273
|
+
return "lessThan";
|
|
274
|
+
case "lte":
|
|
275
|
+
return "lessThanEqual";
|
|
276
|
+
default:
|
|
277
|
+
return void 0;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function buildFilters(opts, config) {
|
|
281
|
+
const where = opts.where;
|
|
282
|
+
if (where === void 0) {
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
function serializeValue(column, value) {
|
|
286
|
+
const conv = config.serialize[column];
|
|
287
|
+
if (conv) {
|
|
288
|
+
return `${conv(value)}`;
|
|
289
|
+
}
|
|
290
|
+
if (typeof value === "boolean") {
|
|
291
|
+
return value ? "1" : "0";
|
|
292
|
+
}
|
|
293
|
+
return `${value}`;
|
|
294
|
+
}
|
|
295
|
+
switch (where.type) {
|
|
296
|
+
case "func": {
|
|
297
|
+
const field = where.args[0];
|
|
298
|
+
const val = where.args[1];
|
|
299
|
+
const op = buildCompareOp(where.name);
|
|
300
|
+
if (op === void 0) {
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
if (field?.type === "ref" && val?.type === "val") {
|
|
304
|
+
const column = field.path.at(0);
|
|
305
|
+
if (column) {
|
|
306
|
+
const f = [
|
|
307
|
+
{
|
|
308
|
+
column: field.path.at(0) ?? "",
|
|
309
|
+
op,
|
|
310
|
+
value: serializeValue(column, val.value)
|
|
311
|
+
}
|
|
312
|
+
];
|
|
313
|
+
return f;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
console.warn("where clause which is not (yet) supported", opts.where);
|
|
320
|
+
return void 0;
|
|
321
|
+
}
|
|
203
322
|
export {
|
|
204
323
|
trailBaseCollectionOptions
|
|
205
324
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trailbase.js","sources":["../../src/trailbase.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unnecessary-condition */\nimport { Store } from '@tanstack/store'\nimport {\n ExpectedDeleteTypeError,\n ExpectedInsertTypeError,\n ExpectedUpdateTypeError,\n TimeoutWaitingForIdsError,\n} from './errors'\nimport type { Event, RecordApi } from 'trailbase'\n\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from '@tanstack/db'\n\ntype ShapeOf<T> = Record<keyof T, unknown>\ntype Conversion<I, O> = (value: I) => O\n\ntype OptionalConversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = {\n // Excludes all keys that require a conversation.\n [K in keyof InputType as InputType[K] extends OutputType[K]\n ? K\n : never]?: Conversion<InputType[K], OutputType[K]>\n}\n\ntype RequiredConversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = {\n // Excludes all keys that do not strictly require a conversation.\n [K in keyof InputType as InputType[K] extends OutputType[K]\n ? never\n : K]: Conversion<InputType[K], OutputType[K]>\n}\n\ntype Conversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = OptionalConversions<InputType, OutputType> &\n RequiredConversions<InputType, OutputType>\n\nfunction convert<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: InputType,\n): OutputType {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\nfunction convertPartial<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: Partial<InputType>,\n): Partial<OutputType> {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\n/**\n * Configuration interface for Trailbase Collection\n */\nexport interface TrailBaseCollectionConfig<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n> extends Omit<\n BaseCollectionConfig<TItem, TKey>,\n `onInsert` | `onUpdate` | `onDelete`\n> {\n /**\n * Record API name\n */\n recordApi: RecordApi<TRecord>\n\n parse: Conversions<TRecord, TItem>\n serialize: Conversions<TItem, TRecord>\n}\n\nexport type AwaitTxIdFn = (txId: string, timeout?: number) => Promise<boolean>\n\nexport interface TrailBaseCollectionUtils extends UtilsRecord {\n cancel: () => void\n}\n\nexport function trailBaseCollectionOptions<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n>(\n config: TrailBaseCollectionConfig<TItem, TRecord, TKey>,\n): CollectionConfig<TItem, TKey, never, TrailBaseCollectionUtils> & {\n utils: TrailBaseCollectionUtils\n} {\n const getKey = config.getKey\n\n const parse = (record: TRecord) =>\n convert<TRecord, TItem>(config.parse, record)\n const serialUpd = (item: Partial<TItem>) =>\n convertPartial<TItem, TRecord>(config.serialize, item)\n const serialIns = (item: TItem) =>\n convert<TItem, TRecord>(config.serialize, item)\n\n const seenIds = new Store(new Map<string, number>())\n\n const awaitIds = (\n ids: Array<string>,\n timeout: number = 120 * 1000,\n ): Promise<void> => {\n const completed = (value: Map<string, number>) =>\n ids.every((id) => value.has(id))\n if (completed(seenIds.state)) {\n return Promise.resolve()\n }\n\n return new Promise<void>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribe()\n reject(new TimeoutWaitingForIdsError(ids.toString()))\n }, timeout)\n\n const unsubscribe = seenIds.subscribe((value) => {\n if (completed(value.currentVal)) {\n clearTimeout(timeoutId)\n unsubscribe()\n resolve()\n }\n })\n })\n }\n\n let eventReader: ReadableStreamDefaultReader<Event> | undefined\n const cancelEventReader = () => {\n if (eventReader) {\n eventReader.cancel()\n eventReader.releaseLock()\n eventReader = undefined\n }\n }\n\n type SyncParams = Parameters<SyncConfig<TItem, TKey>[`sync`]>[0]\n const sync = {\n sync: (params: SyncParams) => {\n const { begin, write, commit, markReady } = params\n\n // Initial fetch.\n async function initialFetch() {\n const limit = 256\n let response = await config.recordApi.list({\n pagination: {\n limit,\n },\n })\n let cursor = response.cursor\n let got = 0\n\n begin()\n\n while (true) {\n const length = response.records.length\n if (length === 0) break\n\n got = got + length\n for (const item of response.records) {\n write({\n type: `insert`,\n value: parse(item),\n })\n }\n\n if (length < limit) break\n\n response = await config.recordApi.list({\n pagination: {\n limit,\n cursor,\n offset: cursor === undefined ? got : undefined,\n },\n })\n cursor = response.cursor\n }\n\n commit()\n }\n\n // Afterwards subscribe.\n async function listen(reader: ReadableStreamDefaultReader<Event>) {\n while (true) {\n const { done, value: event } = await reader.read()\n\n if (done || !event) {\n reader.releaseLock()\n eventReader = undefined\n return\n }\n\n begin()\n let value: TItem | undefined\n if (`Insert` in event) {\n value = parse(event.Insert as TRecord)\n write({ type: `insert`, value })\n } else if (`Delete` in event) {\n value = parse(event.Delete as TRecord)\n write({ type: `delete`, value })\n } else if (`Update` in event) {\n value = parse(event.Update as TRecord)\n write({ type: `update`, value })\n } else {\n console.error(`Error: ${event.Error}`)\n }\n commit()\n\n if (value) {\n seenIds.setState((curr: Map<string, number>) => {\n const newIds = new Map(curr)\n newIds.set(String(getKey(value)), Date.now())\n return newIds\n })\n }\n }\n }\n\n async function start() {\n const eventStream = await config.recordApi.subscribe(`*`)\n const reader = (eventReader = eventStream.getReader())\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n listen(reader)\n\n try {\n await initialFetch()\n } catch (e) {\n cancelEventReader()\n throw e\n } finally {\n // Mark ready both if everything went well or if there's an error to\n // avoid blocking apps waiting for `.preload()` to finish.\n markReady()\n }\n\n // Lastly, start a periodic cleanup task that will be removed when the\n // reader closes.\n const periodicCleanupTask = setInterval(() => {\n seenIds.setState((curr) => {\n const now = Date.now()\n let anyExpired = false\n\n const notExpired = Array.from(curr.entries()).filter(([_, v]) => {\n const expired = now - v > 300 * 1000\n anyExpired = anyExpired || expired\n return !expired\n })\n\n if (anyExpired) {\n return new Map(notExpired)\n }\n return curr\n })\n }, 120 * 1000)\n\n reader.closed.finally(() => clearInterval(periodicCleanupTask))\n }\n\n start()\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }\n\n return {\n ...config,\n sync,\n getKey,\n onInsert: async (\n params: InsertMutationFnParams<TItem, TKey>,\n ): Promise<Array<number | string>> => {\n const ids = await config.recordApi.createBulk(\n params.transaction.mutations.map((tx) => {\n const { type, modified } = tx\n if (type !== `insert`) {\n throw new ExpectedInsertTypeError(type)\n }\n return serialIns(modified)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly added to the local\n // DB by the subscription.\n await awaitIds(ids.map((id) => String(id)))\n\n return ids\n },\n onUpdate: async (params: UpdateMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, changes, key } = tx\n if (type !== `update`) {\n throw new ExpectedUpdateTypeError(type)\n }\n\n await config.recordApi.update(key, serialUpd(changes))\n\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n onDelete: async (params: DeleteMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, key } = tx\n if (type !== `delete`) {\n throw new ExpectedDeleteTypeError(type)\n }\n\n await config.recordApi.delete(key)\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n utils: {\n cancel: cancelEventReader,\n },\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"trailbase.js","sources":["../../src/trailbase.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unnecessary-condition */\nimport { Store } from '@tanstack/store'\nimport {\n ExpectedDeleteTypeError,\n ExpectedInsertTypeError,\n ExpectedUpdateTypeError,\n TimeoutWaitingForIdsError,\n} from './errors'\nimport type { OrderByClause } from '../../db/dist/esm/query/ir'\nimport type { CompareOp, Event, FilterOrComposite, RecordApi } from 'trailbase'\n\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n LoadSubsetOptions,\n SyncConfig,\n SyncMode,\n UpdateMutationFnParams,\n UtilsRecord,\n} from '@tanstack/db'\n\ntype ShapeOf<T> = Record<keyof T, unknown>\ntype Conversion<I, O> = (value: I) => O\n\ntype OptionalConversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = {\n // Excludes all keys that require a conversation.\n [K in keyof InputType as InputType[K] extends OutputType[K]\n ? K\n : never]?: Conversion<InputType[K], OutputType[K]>\n}\n\ntype RequiredConversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = {\n // Excludes all keys that do not strictly require a conversation.\n [K in keyof InputType as InputType[K] extends OutputType[K]\n ? never\n : K]: Conversion<InputType[K], OutputType[K]>\n}\n\ntype Conversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = OptionalConversions<InputType, OutputType> &\n RequiredConversions<InputType, OutputType>\n\nfunction convert<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: InputType,\n): OutputType {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\nfunction convertPartial<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: Partial<InputType>,\n): Partial<OutputType> {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\nexport type TrailBaseSyncMode = SyncMode\n\n/**\n * Configuration interface for Trailbase Collection\n */\nexport interface TrailBaseCollectionConfig<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n> extends Omit<\n BaseCollectionConfig<TItem, TKey>,\n `onInsert` | `onUpdate` | `onDelete` | `syncMode`\n> {\n /**\n * Record API name\n */\n recordApi: RecordApi<TRecord>\n\n /**\n * The mode of sync to use for the collection.\n * @default `eager`\n */\n syncMode?: TrailBaseSyncMode\n\n parse: Conversions<TRecord, TItem>\n serialize: Conversions<TItem, TRecord>\n}\n\nexport type AwaitTxIdFn = (txId: string, timeout?: number) => Promise<boolean>\n\nexport interface TrailBaseCollectionUtils extends UtilsRecord {\n cancel: () => void\n}\n\nexport function trailBaseCollectionOptions<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n>(\n config: TrailBaseCollectionConfig<TItem, TRecord, TKey>,\n): CollectionConfig<TItem, TKey, never, TrailBaseCollectionUtils> & {\n utils: TrailBaseCollectionUtils\n} {\n const getKey = config.getKey\n\n const parse = (record: TRecord) =>\n convert<TRecord, TItem>(config.parse, record)\n const serialUpd = (item: Partial<TItem>) =>\n convertPartial<TItem, TRecord>(config.serialize, item)\n const serialIns = (item: TItem) =>\n convert<TItem, TRecord>(config.serialize, item)\n\n const seenIds = new Store(new Map<string, number>())\n\n const internalSyncMode = config.syncMode ?? `eager`\n let fullSyncCompleted = false\n\n const awaitIds = (\n ids: Array<string>,\n timeout: number = 120 * 1000,\n ): Promise<void> => {\n const completed = (value: Map<string, number>) =>\n ids.every((id) => value.has(id))\n if (completed(seenIds.state)) {\n return Promise.resolve()\n }\n\n return new Promise<void>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n sub.unsubscribe()\n reject(new TimeoutWaitingForIdsError(ids.toString()))\n }, timeout)\n\n const sub = seenIds.subscribe((value) => {\n if (completed(value)) {\n clearTimeout(timeoutId)\n sub.unsubscribe()\n resolve()\n }\n })\n })\n }\n\n let eventReader: ReadableStreamDefaultReader<Event> | undefined\n const cancelEventReader = () => {\n if (eventReader) {\n eventReader.cancel()\n eventReader.releaseLock()\n eventReader = undefined\n }\n }\n\n type SyncParams = Parameters<SyncConfig<TItem, TKey>[`sync`]>[0]\n const sync = {\n sync: (params: SyncParams) => {\n const { begin, write, commit, markReady } = params\n\n // NOTE: We cache cursors from prior fetches. TanStack/db expects that\n // cursors can be derived from a key, which is not true for TB, since\n // cursors are encrypted. This is leaky and therefore not ideal.\n const cursors = new Map<string | number, string>()\n\n // Load (more) data.\n async function load(opts: LoadSubsetOptions) {\n const lastKey = opts.cursor?.lastKey\n let cursor: string | undefined =\n lastKey !== undefined ? cursors.get(lastKey) : undefined\n let offset: number | undefined =\n (opts.offset ?? 0) > 0 ? opts.offset : undefined\n\n const order: Array<string> | undefined = buildOrder(opts)\n const filters: Array<FilterOrComposite> | undefined = buildFilters(\n opts,\n config,\n )\n\n let remaining: number = opts.limit ?? Number.MAX_VALUE\n if (remaining <= 0) {\n return\n }\n\n while (true) {\n const limit = Math.min(remaining, 256)\n const response = await config.recordApi.list({\n pagination: {\n limit,\n offset,\n cursor,\n },\n order,\n filters,\n })\n\n const length = response.records.length\n if (length === 0) {\n // Drained - read everything.\n break\n }\n\n begin()\n\n for (let i = 0; i < Math.min(length, remaining); ++i) {\n write({\n type: `insert`,\n value: parse(response.records[i]!),\n })\n }\n\n commit()\n\n remaining -= length\n\n // Drained or read enough.\n if (length < limit || remaining <= 0) {\n if (response.cursor) {\n cursors.set(\n getKey(parse(response.records.at(-1)!)),\n response.cursor,\n )\n }\n break\n }\n\n // Update params for next iteration.\n if (offset !== undefined) {\n offset += length\n } else {\n cursor = response.cursor\n }\n }\n }\n\n // Afterwards subscribe.\n async function listen(reader: ReadableStreamDefaultReader<Event>) {\n while (true) {\n const { done, value: event } = await reader.read()\n\n if (done || !event) {\n reader.releaseLock()\n eventReader = undefined\n return\n }\n\n begin()\n let value: TItem | undefined\n if (`Insert` in event) {\n value = parse(event.Insert as TRecord)\n write({ type: `insert`, value })\n } else if (`Delete` in event) {\n value = parse(event.Delete as TRecord)\n write({ type: `delete`, value })\n } else if (`Update` in event) {\n value = parse(event.Update as TRecord)\n write({ type: `update`, value })\n } else {\n console.error(`Error: ${event.Error}`)\n }\n commit()\n\n if (value) {\n seenIds.setState((curr: Map<string, number>) => {\n const newIds = new Map(curr)\n newIds.set(String(getKey(value)), Date.now())\n return newIds\n })\n }\n }\n }\n\n async function start() {\n const eventStream = await config.recordApi.subscribe(`*`)\n const reader = (eventReader = eventStream.getReader())\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n listen(reader)\n\n try {\n // Eager mode: perform initial fetch to populate everything\n if (internalSyncMode === `eager`) {\n // Load everything on initial load.\n await load({})\n fullSyncCompleted = true\n }\n } catch (e) {\n cancelEventReader()\n throw e\n } finally {\n // Mark ready both if everything went well or if there's an error to\n // avoid blocking apps waiting for `.preload()` to finish.\n markReady()\n }\n\n // Lastly, start a periodic cleanup task that will be removed when the\n // reader closes.\n const periodicCleanupTask = setInterval(() => {\n seenIds.setState((curr) => {\n const now = Date.now()\n let anyExpired = false\n\n const notExpired = Array.from(curr.entries()).filter(([_, v]) => {\n const expired = now - v > 300 * 1000\n anyExpired = anyExpired || expired\n return !expired\n })\n\n if (anyExpired) {\n return new Map(notExpired)\n }\n return curr\n })\n }, 120 * 1000)\n\n reader.closed.finally(() => clearInterval(periodicCleanupTask))\n }\n\n start()\n\n // Eager mode doesn't need subset loading\n if (internalSyncMode === `eager`) {\n return\n }\n\n return {\n loadSubset: load,\n getSyncMetadata: () =>\n ({\n syncMode: internalSyncMode,\n }) as const,\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: () =>\n ({\n syncMode: internalSyncMode,\n fullSyncComplete: fullSyncCompleted,\n }) as const,\n }\n\n return {\n ...config,\n sync,\n getKey,\n onInsert: async (\n params: InsertMutationFnParams<TItem, TKey>,\n ): Promise<Array<number | string>> => {\n const ids = await config.recordApi.createBulk(\n params.transaction.mutations.map((tx) => {\n const { type, modified } = tx\n if (type !== `insert`) {\n throw new ExpectedInsertTypeError(type)\n }\n return serialIns(modified)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly added to the local\n // DB by the subscription.\n await awaitIds(ids.map((id) => String(id)))\n\n return ids\n },\n onUpdate: async (params: UpdateMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, changes, key } = tx\n if (type !== `update`) {\n throw new ExpectedUpdateTypeError(type)\n }\n\n await config.recordApi.update(key, serialUpd(changes))\n\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n onDelete: async (params: DeleteMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, key } = tx\n if (type !== `delete`) {\n throw new ExpectedDeleteTypeError(type)\n }\n\n await config.recordApi.delete(key)\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n utils: {\n cancel: cancelEventReader,\n },\n }\n}\n\nfunction buildOrder(opts: LoadSubsetOptions): undefined | Array<string> {\n return opts.orderBy\n ?.map((o: OrderByClause) => {\n switch (o.expression.type) {\n case 'ref': {\n const field = o.expression.path[0]\n if (o.compareOptions.direction == 'asc') {\n return `+${field}`\n }\n return `-${field}`\n }\n default: {\n console.warn(\n 'Skipping unsupported order clause:',\n JSON.stringify(o.expression),\n )\n return undefined\n }\n }\n })\n .filter((f: string | undefined) => f !== undefined)\n}\n\nfunction buildCompareOp(name: string): CompareOp | undefined {\n switch (name) {\n case 'eq':\n return 'equal'\n case 'ne':\n return 'notEqual'\n case 'gt':\n return 'greaterThan'\n case 'gte':\n return 'greaterThanEqual'\n case 'lt':\n return 'lessThan'\n case 'lte':\n return 'lessThanEqual'\n default:\n return undefined\n }\n}\n\nfunction buildFilters<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n>(\n opts: LoadSubsetOptions,\n config: TrailBaseCollectionConfig<TItem, TRecord, TKey>,\n): undefined | Array<FilterOrComposite> {\n const where = opts.where\n if (where === undefined) {\n return undefined\n }\n\n function serializeValue<T = any>(column: string, value: T): string {\n const conv = (config.serialize as any)[column]\n if (conv) {\n return `${conv(value)}`\n }\n\n if (typeof value === 'boolean') {\n return value ? '1' : '0'\n }\n\n return `${value}`\n }\n\n switch (where.type) {\n case 'func': {\n const field = where.args[0]\n const val = where.args[1]\n\n const op = buildCompareOp(where.name)\n if (op === undefined) {\n break\n }\n\n if (field?.type === 'ref' && val?.type === 'val') {\n const column = field.path.at(0)\n if (column) {\n const f = [\n {\n column: field.path.at(0) ?? '',\n op,\n value: serializeValue(column, val.value),\n },\n ]\n\n return f\n }\n }\n break\n }\n case 'ref':\n case 'val':\n break\n }\n\n console.warn('where clause which is not (yet) supported', opts.where)\n\n return undefined\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/trailbase-db-collection",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.78",
|
|
4
4
|
"description": "TrailBase collection for TanStack DB",
|
|
5
5
|
"author": "Sebastian Jeltsch",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,22 +40,24 @@
|
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@standard-schema/spec": "^1.1.0",
|
|
43
|
-
"@tanstack/store": "^0.
|
|
43
|
+
"@tanstack/store": "^0.9.2",
|
|
44
44
|
"debug": "^4.4.3",
|
|
45
|
-
"trailbase": "^0.
|
|
46
|
-
"@tanstack/db": "0.
|
|
45
|
+
"trailbase": "^0.10.0",
|
|
46
|
+
"@tanstack/db": "0.6.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"typescript": ">=4.7"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/debug": "^4.1.12",
|
|
53
|
-
"@vitest/coverage-istanbul": "^3.2.4"
|
|
53
|
+
"@vitest/coverage-istanbul": "^3.2.4",
|
|
54
|
+
"uuid": "^13.0.0"
|
|
54
55
|
},
|
|
55
56
|
"scripts": {
|
|
56
57
|
"build": "vite build",
|
|
57
58
|
"dev": "vite build --watch",
|
|
58
59
|
"lint": "eslint . --fix",
|
|
59
|
-
"test": "vitest --run"
|
|
60
|
+
"test": "vitest --run",
|
|
61
|
+
"test:e2e": "vitest --run --config vitest.e2e.config.ts"
|
|
60
62
|
}
|
|
61
63
|
}
|
package/src/trailbase.ts
CHANGED
|
@@ -6,14 +6,17 @@ import {
|
|
|
6
6
|
ExpectedUpdateTypeError,
|
|
7
7
|
TimeoutWaitingForIdsError,
|
|
8
8
|
} from './errors'
|
|
9
|
-
import type {
|
|
9
|
+
import type { OrderByClause } from '../../db/dist/esm/query/ir'
|
|
10
|
+
import type { CompareOp, Event, FilterOrComposite, RecordApi } from 'trailbase'
|
|
10
11
|
|
|
11
12
|
import type {
|
|
12
13
|
BaseCollectionConfig,
|
|
13
14
|
CollectionConfig,
|
|
14
15
|
DeleteMutationFnParams,
|
|
15
16
|
InsertMutationFnParams,
|
|
17
|
+
LoadSubsetOptions,
|
|
16
18
|
SyncConfig,
|
|
19
|
+
SyncMode,
|
|
17
20
|
UpdateMutationFnParams,
|
|
18
21
|
UtilsRecord,
|
|
19
22
|
} from '@tanstack/db'
|
|
@@ -81,6 +84,8 @@ function convertPartial<
|
|
|
81
84
|
) as OutputType
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
export type TrailBaseSyncMode = SyncMode
|
|
88
|
+
|
|
84
89
|
/**
|
|
85
90
|
* Configuration interface for Trailbase Collection
|
|
86
91
|
*/
|
|
@@ -90,13 +95,19 @@ export interface TrailBaseCollectionConfig<
|
|
|
90
95
|
TKey extends string | number = string | number,
|
|
91
96
|
> extends Omit<
|
|
92
97
|
BaseCollectionConfig<TItem, TKey>,
|
|
93
|
-
`onInsert` | `onUpdate` | `onDelete`
|
|
98
|
+
`onInsert` | `onUpdate` | `onDelete` | `syncMode`
|
|
94
99
|
> {
|
|
95
100
|
/**
|
|
96
101
|
* Record API name
|
|
97
102
|
*/
|
|
98
103
|
recordApi: RecordApi<TRecord>
|
|
99
104
|
|
|
105
|
+
/**
|
|
106
|
+
* The mode of sync to use for the collection.
|
|
107
|
+
* @default `eager`
|
|
108
|
+
*/
|
|
109
|
+
syncMode?: TrailBaseSyncMode
|
|
110
|
+
|
|
100
111
|
parse: Conversions<TRecord, TItem>
|
|
101
112
|
serialize: Conversions<TItem, TRecord>
|
|
102
113
|
}
|
|
@@ -127,6 +138,9 @@ export function trailBaseCollectionOptions<
|
|
|
127
138
|
|
|
128
139
|
const seenIds = new Store(new Map<string, number>())
|
|
129
140
|
|
|
141
|
+
const internalSyncMode = config.syncMode ?? `eager`
|
|
142
|
+
let fullSyncCompleted = false
|
|
143
|
+
|
|
130
144
|
const awaitIds = (
|
|
131
145
|
ids: Array<string>,
|
|
132
146
|
timeout: number = 120 * 1000,
|
|
@@ -139,14 +153,14 @@ export function trailBaseCollectionOptions<
|
|
|
139
153
|
|
|
140
154
|
return new Promise<void>((resolve, reject) => {
|
|
141
155
|
const timeoutId = setTimeout(() => {
|
|
142
|
-
unsubscribe()
|
|
156
|
+
sub.unsubscribe()
|
|
143
157
|
reject(new TimeoutWaitingForIdsError(ids.toString()))
|
|
144
158
|
}, timeout)
|
|
145
159
|
|
|
146
|
-
const
|
|
147
|
-
if (completed(value
|
|
160
|
+
const sub = seenIds.subscribe((value) => {
|
|
161
|
+
if (completed(value)) {
|
|
148
162
|
clearTimeout(timeoutId)
|
|
149
|
-
unsubscribe()
|
|
163
|
+
sub.unsubscribe()
|
|
150
164
|
resolve()
|
|
151
165
|
}
|
|
152
166
|
})
|
|
@@ -167,44 +181,79 @@ export function trailBaseCollectionOptions<
|
|
|
167
181
|
sync: (params: SyncParams) => {
|
|
168
182
|
const { begin, write, commit, markReady } = params
|
|
169
183
|
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
let cursor =
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
// NOTE: We cache cursors from prior fetches. TanStack/db expects that
|
|
185
|
+
// cursors can be derived from a key, which is not true for TB, since
|
|
186
|
+
// cursors are encrypted. This is leaky and therefore not ideal.
|
|
187
|
+
const cursors = new Map<string | number, string>()
|
|
188
|
+
|
|
189
|
+
// Load (more) data.
|
|
190
|
+
async function load(opts: LoadSubsetOptions) {
|
|
191
|
+
const lastKey = opts.cursor?.lastKey
|
|
192
|
+
let cursor: string | undefined =
|
|
193
|
+
lastKey !== undefined ? cursors.get(lastKey) : undefined
|
|
194
|
+
let offset: number | undefined =
|
|
195
|
+
(opts.offset ?? 0) > 0 ? opts.offset : undefined
|
|
196
|
+
|
|
197
|
+
const order: Array<string> | undefined = buildOrder(opts)
|
|
198
|
+
const filters: Array<FilterOrComposite> | undefined = buildFilters(
|
|
199
|
+
opts,
|
|
200
|
+
config,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
let remaining: number = opts.limit ?? Number.MAX_VALUE
|
|
204
|
+
if (remaining <= 0) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
182
207
|
|
|
183
208
|
while (true) {
|
|
209
|
+
const limit = Math.min(remaining, 256)
|
|
210
|
+
const response = await config.recordApi.list({
|
|
211
|
+
pagination: {
|
|
212
|
+
limit,
|
|
213
|
+
offset,
|
|
214
|
+
cursor,
|
|
215
|
+
},
|
|
216
|
+
order,
|
|
217
|
+
filters,
|
|
218
|
+
})
|
|
219
|
+
|
|
184
220
|
const length = response.records.length
|
|
185
|
-
if (length === 0)
|
|
221
|
+
if (length === 0) {
|
|
222
|
+
// Drained - read everything.
|
|
223
|
+
break
|
|
224
|
+
}
|
|
186
225
|
|
|
187
|
-
|
|
188
|
-
|
|
226
|
+
begin()
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < Math.min(length, remaining); ++i) {
|
|
189
229
|
write({
|
|
190
230
|
type: `insert`,
|
|
191
|
-
value: parse(
|
|
231
|
+
value: parse(response.records[i]!),
|
|
192
232
|
})
|
|
193
233
|
}
|
|
194
234
|
|
|
195
|
-
|
|
235
|
+
commit()
|
|
196
236
|
|
|
197
|
-
|
|
198
|
-
pagination: {
|
|
199
|
-
limit,
|
|
200
|
-
cursor,
|
|
201
|
-
offset: cursor === undefined ? got : undefined,
|
|
202
|
-
},
|
|
203
|
-
})
|
|
204
|
-
cursor = response.cursor
|
|
205
|
-
}
|
|
237
|
+
remaining -= length
|
|
206
238
|
|
|
207
|
-
|
|
239
|
+
// Drained or read enough.
|
|
240
|
+
if (length < limit || remaining <= 0) {
|
|
241
|
+
if (response.cursor) {
|
|
242
|
+
cursors.set(
|
|
243
|
+
getKey(parse(response.records.at(-1)!)),
|
|
244
|
+
response.cursor,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Update params for next iteration.
|
|
251
|
+
if (offset !== undefined) {
|
|
252
|
+
offset += length
|
|
253
|
+
} else {
|
|
254
|
+
cursor = response.cursor
|
|
255
|
+
}
|
|
256
|
+
}
|
|
208
257
|
}
|
|
209
258
|
|
|
210
259
|
// Afterwards subscribe.
|
|
@@ -253,7 +302,12 @@ export function trailBaseCollectionOptions<
|
|
|
253
302
|
listen(reader)
|
|
254
303
|
|
|
255
304
|
try {
|
|
256
|
-
|
|
305
|
+
// Eager mode: perform initial fetch to populate everything
|
|
306
|
+
if (internalSyncMode === `eager`) {
|
|
307
|
+
// Load everything on initial load.
|
|
308
|
+
await load({})
|
|
309
|
+
fullSyncCompleted = true
|
|
310
|
+
}
|
|
257
311
|
} catch (e) {
|
|
258
312
|
cancelEventReader()
|
|
259
313
|
throw e
|
|
@@ -287,9 +341,26 @@ export function trailBaseCollectionOptions<
|
|
|
287
341
|
}
|
|
288
342
|
|
|
289
343
|
start()
|
|
344
|
+
|
|
345
|
+
// Eager mode doesn't need subset loading
|
|
346
|
+
if (internalSyncMode === `eager`) {
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
loadSubset: load,
|
|
352
|
+
getSyncMetadata: () =>
|
|
353
|
+
({
|
|
354
|
+
syncMode: internalSyncMode,
|
|
355
|
+
}) as const,
|
|
356
|
+
}
|
|
290
357
|
},
|
|
291
358
|
// Expose the getSyncMetadata function
|
|
292
|
-
getSyncMetadata:
|
|
359
|
+
getSyncMetadata: () =>
|
|
360
|
+
({
|
|
361
|
+
syncMode: internalSyncMode,
|
|
362
|
+
fullSyncComplete: fullSyncCompleted,
|
|
363
|
+
}) as const,
|
|
293
364
|
}
|
|
294
365
|
|
|
295
366
|
return {
|
|
@@ -358,3 +429,107 @@ export function trailBaseCollectionOptions<
|
|
|
358
429
|
},
|
|
359
430
|
}
|
|
360
431
|
}
|
|
432
|
+
|
|
433
|
+
function buildOrder(opts: LoadSubsetOptions): undefined | Array<string> {
|
|
434
|
+
return opts.orderBy
|
|
435
|
+
?.map((o: OrderByClause) => {
|
|
436
|
+
switch (o.expression.type) {
|
|
437
|
+
case 'ref': {
|
|
438
|
+
const field = o.expression.path[0]
|
|
439
|
+
if (o.compareOptions.direction == 'asc') {
|
|
440
|
+
return `+${field}`
|
|
441
|
+
}
|
|
442
|
+
return `-${field}`
|
|
443
|
+
}
|
|
444
|
+
default: {
|
|
445
|
+
console.warn(
|
|
446
|
+
'Skipping unsupported order clause:',
|
|
447
|
+
JSON.stringify(o.expression),
|
|
448
|
+
)
|
|
449
|
+
return undefined
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
.filter((f: string | undefined) => f !== undefined)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function buildCompareOp(name: string): CompareOp | undefined {
|
|
457
|
+
switch (name) {
|
|
458
|
+
case 'eq':
|
|
459
|
+
return 'equal'
|
|
460
|
+
case 'ne':
|
|
461
|
+
return 'notEqual'
|
|
462
|
+
case 'gt':
|
|
463
|
+
return 'greaterThan'
|
|
464
|
+
case 'gte':
|
|
465
|
+
return 'greaterThanEqual'
|
|
466
|
+
case 'lt':
|
|
467
|
+
return 'lessThan'
|
|
468
|
+
case 'lte':
|
|
469
|
+
return 'lessThanEqual'
|
|
470
|
+
default:
|
|
471
|
+
return undefined
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function buildFilters<
|
|
476
|
+
TItem extends ShapeOf<TRecord>,
|
|
477
|
+
TRecord extends ShapeOf<TItem> = TItem,
|
|
478
|
+
TKey extends string | number = string | number,
|
|
479
|
+
>(
|
|
480
|
+
opts: LoadSubsetOptions,
|
|
481
|
+
config: TrailBaseCollectionConfig<TItem, TRecord, TKey>,
|
|
482
|
+
): undefined | Array<FilterOrComposite> {
|
|
483
|
+
const where = opts.where
|
|
484
|
+
if (where === undefined) {
|
|
485
|
+
return undefined
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function serializeValue<T = any>(column: string, value: T): string {
|
|
489
|
+
const conv = (config.serialize as any)[column]
|
|
490
|
+
if (conv) {
|
|
491
|
+
return `${conv(value)}`
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (typeof value === 'boolean') {
|
|
495
|
+
return value ? '1' : '0'
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return `${value}`
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
switch (where.type) {
|
|
502
|
+
case 'func': {
|
|
503
|
+
const field = where.args[0]
|
|
504
|
+
const val = where.args[1]
|
|
505
|
+
|
|
506
|
+
const op = buildCompareOp(where.name)
|
|
507
|
+
if (op === undefined) {
|
|
508
|
+
break
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (field?.type === 'ref' && val?.type === 'val') {
|
|
512
|
+
const column = field.path.at(0)
|
|
513
|
+
if (column) {
|
|
514
|
+
const f = [
|
|
515
|
+
{
|
|
516
|
+
column: field.path.at(0) ?? '',
|
|
517
|
+
op,
|
|
518
|
+
value: serializeValue(column, val.value),
|
|
519
|
+
},
|
|
520
|
+
]
|
|
521
|
+
|
|
522
|
+
return f
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
break
|
|
526
|
+
}
|
|
527
|
+
case 'ref':
|
|
528
|
+
case 'val':
|
|
529
|
+
break
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
console.warn('where clause which is not (yet) supported', opts.where)
|
|
533
|
+
|
|
534
|
+
return undefined
|
|
535
|
+
}
|