@tanstack/trailbase-db-collection 0.1.119 → 0.1.120
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 +90 -48
- package/dist/cjs/trailbase.cjs.map +1 -1
- package/dist/esm/trailbase.js +90 -48
- package/dist/esm/trailbase.js.map +1 -1
- package/package.json +2 -2
- package/src/trailbase.ts +123 -59
package/dist/cjs/trailbase.cjs
CHANGED
|
@@ -63,6 +63,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
63
63
|
let periodicCleanupTask;
|
|
64
64
|
const cleanup = () => {
|
|
65
65
|
cancelled = true;
|
|
66
|
+
settleListener();
|
|
66
67
|
cancelEventReader();
|
|
67
68
|
if (periodicCleanupTask !== void 0) {
|
|
68
69
|
clearInterval(periodicCleanupTask);
|
|
@@ -70,9 +71,38 @@ function trailBaseCollectionOptions(config) {
|
|
|
70
71
|
}
|
|
71
72
|
};
|
|
72
73
|
const cursors = /* @__PURE__ */ new Map();
|
|
74
|
+
let listening = false;
|
|
75
|
+
let eventSequence = 0;
|
|
76
|
+
const eventSequenceByKey = /* @__PURE__ */ new Map();
|
|
77
|
+
let loadsInFlight = 0;
|
|
78
|
+
let settleListener;
|
|
79
|
+
const listenerSettled = new Promise(
|
|
80
|
+
(resolve) => {
|
|
81
|
+
settleListener = resolve;
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
async function waitForListener(signal) {
|
|
85
|
+
if (!signal) return listenerSettled;
|
|
86
|
+
let onAbort;
|
|
87
|
+
const aborted = new Promise((resolve) => {
|
|
88
|
+
onAbort = () => resolve(void 0);
|
|
89
|
+
signal.addEventListener(`abort`, onAbort, { once: true });
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
return await Promise.race([listenerSettled, aborted]);
|
|
93
|
+
} finally {
|
|
94
|
+
signal.removeEventListener(`abort`, onAbort);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
73
97
|
async function load(opts) {
|
|
74
98
|
if (cancelled) return;
|
|
75
99
|
if (opts.signal?.aborted) throw new db.LoadSubsetOperationAbortedError();
|
|
100
|
+
if (!listening) {
|
|
101
|
+
const failure = await waitForListener(opts.signal);
|
|
102
|
+
if (cancelled) return;
|
|
103
|
+
if (opts.signal?.aborted) throw new db.LoadSubsetOperationAbortedError();
|
|
104
|
+
if (failure) throw failure.error;
|
|
105
|
+
}
|
|
76
106
|
const lastKey = opts.cursor?.lastKey;
|
|
77
107
|
let cursor = lastKey !== void 0 ? cursors.get(lastKey) : void 0;
|
|
78
108
|
let offset = cursor === void 0 && (opts.offset ?? 0) > 0 ? opts.offset : void 0;
|
|
@@ -86,57 +116,63 @@ function trailBaseCollectionOptions(config) {
|
|
|
86
116
|
return;
|
|
87
117
|
}
|
|
88
118
|
const appliedPages = [];
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
response
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
119
|
+
loadsInFlight++;
|
|
120
|
+
try {
|
|
121
|
+
while (true) {
|
|
122
|
+
const limit = Math.min(remaining, 256);
|
|
123
|
+
let response;
|
|
124
|
+
const listedAfter = eventSequence;
|
|
125
|
+
try {
|
|
126
|
+
response = await config.recordApi.list({
|
|
127
|
+
pagination: {
|
|
128
|
+
limit,
|
|
129
|
+
offset,
|
|
130
|
+
cursor
|
|
131
|
+
},
|
|
132
|
+
order,
|
|
133
|
+
filters
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (cancelled) return;
|
|
137
|
+
if (opts.signal?.aborted) break;
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
103
140
|
if (cancelled) return;
|
|
104
141
|
if (opts.signal?.aborted) break;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
const length = response.records.length;
|
|
143
|
+
if (length === 0) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
begin();
|
|
147
|
+
for (let i = 0; i < Math.min(length, remaining); ++i) {
|
|
148
|
+
const value = parse(response.records[i]);
|
|
149
|
+
const key = getKey(value);
|
|
150
|
+
if ((eventSequenceByKey.get(key) ?? 0) > listedAfter) continue;
|
|
151
|
+
write({ type: `update`, value });
|
|
152
|
+
}
|
|
153
|
+
const applied = commit(opts.signal);
|
|
154
|
+
if (applied !== true) {
|
|
155
|
+
appliedPages.push(applied);
|
|
156
|
+
}
|
|
157
|
+
if (cancelled || opts.signal?.aborted) break;
|
|
158
|
+
remaining -= length;
|
|
159
|
+
if (length < limit || remaining <= 0) {
|
|
160
|
+
if (response.cursor && (opts.subscription === void 0 || lastKey !== void 0)) {
|
|
161
|
+
cursors.set(
|
|
162
|
+
getKey(parse(response.records.at(-1))),
|
|
163
|
+
response.cursor
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (offset !== void 0) {
|
|
169
|
+
offset += length;
|
|
170
|
+
} else {
|
|
171
|
+
cursor = response.cursor;
|
|
132
172
|
}
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
if (offset !== void 0) {
|
|
136
|
-
offset += length;
|
|
137
|
-
} else {
|
|
138
|
-
cursor = response.cursor;
|
|
139
173
|
}
|
|
174
|
+
} finally {
|
|
175
|
+
if (--loadsInFlight === 0) eventSequenceByKey.clear();
|
|
140
176
|
}
|
|
141
177
|
await Promise.all(appliedPages);
|
|
142
178
|
if (opts.signal?.aborted) throw new db.LoadSubsetOperationAbortedError();
|
|
@@ -151,7 +187,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
151
187
|
let value;
|
|
152
188
|
if (`Insert` in event) {
|
|
153
189
|
value = parse(event.Insert);
|
|
154
|
-
write({ type: `
|
|
190
|
+
write({ type: `update`, value });
|
|
155
191
|
} else if (`Delete` in event) {
|
|
156
192
|
value = parse(event.Delete);
|
|
157
193
|
write({ type: `delete`, value });
|
|
@@ -163,6 +199,9 @@ function trailBaseCollectionOptions(config) {
|
|
|
163
199
|
}
|
|
164
200
|
void commit();
|
|
165
201
|
if (value) {
|
|
202
|
+
++eventSequence;
|
|
203
|
+
if (loadsInFlight > 0)
|
|
204
|
+
eventSequenceByKey.set(getKey(value), eventSequence);
|
|
166
205
|
seenIds.setState((curr) => {
|
|
167
206
|
const newIds = new Map(curr);
|
|
168
207
|
newIds.set(String(getKey(value)), Date.now());
|
|
@@ -192,6 +231,8 @@ function trailBaseCollectionOptions(config) {
|
|
|
192
231
|
console.error(`TrailBase subscription failed`, error);
|
|
193
232
|
}
|
|
194
233
|
});
|
|
234
|
+
listening = true;
|
|
235
|
+
settleListener();
|
|
195
236
|
if (internalSyncMode === `eager`) {
|
|
196
237
|
await load({});
|
|
197
238
|
if (cancelled) return;
|
|
@@ -202,6 +243,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
202
243
|
}
|
|
203
244
|
} catch (error) {
|
|
204
245
|
if (cancelled) return;
|
|
246
|
+
settleListener({ error });
|
|
205
247
|
cancelEventReader();
|
|
206
248
|
if (collection.status === `loading`) {
|
|
207
249
|
markError(error);
|
|
@@ -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 LoadSubsetOperationAbortedError,\n withCollectionConfigFactory,\n} from '@tanstack/db'\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 [\n K in keyof InputType as InputType[K] extends OutputType[K] ? K : never\n ]?: 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 [\n K in keyof InputType as InputType[K] extends OutputType[K] ? never : K\n ]: 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 // An already-errored stream rejects cancellation too. Cleanup still\n // retires its reader; that rejection must not escape as detached work.\n void eventReader.cancel().catch(() => undefined)\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, markError, collection } = params\n let cancelled = false\n let periodicCleanupTask: ReturnType<typeof setInterval> | undefined\n\n const cleanup = () => {\n cancelled = true\n cancelEventReader()\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n }\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 // A caller that aborts sees `AbortError`. Pages a commit accepted still\n // apply, and the load waits until they are visible; cleanup is quiet.\n async function load(opts: LoadSubsetOptions) {\n if (cancelled) return\n if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()\n\n const lastKey = opts.cursor?.lastKey\n let cursor: string | undefined =\n lastKey !== undefined ? cursors.get(lastKey) : undefined\n let offset: number | undefined =\n cursor === undefined && (opts.offset ?? 0) > 0\n ? opts.offset\n : 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 const appliedPages: Array<Promise<void>> = []\n\n while (true) {\n const limit = Math.min(remaining, 256)\n let response\n try {\n response = await config.recordApi.list({\n pagination: {\n limit,\n offset,\n cursor,\n },\n order,\n filters,\n })\n } catch (error) {\n if (cancelled) return\n if (opts.signal?.aborted) break\n throw error\n }\n if (cancelled) return\n // Check the signal before commit to discard a stale page.\n if (opts.signal?.aborted) break\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 // An accepted page always applies.\n const applied = commit(opts.signal)\n if (applied !== true) {\n appliedPages.push(applied)\n }\n if (cancelled || opts.signal?.aborted) break\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 await Promise.all(appliedPages)\n // An accepted page applies, but a caller that aborted still sees\n // `AbortError`.\n if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()\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 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 void 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 let reader: ReadableStreamDefaultReader<Event> | undefined\n try {\n const eventStream = await config.recordApi.subscribe(`*`)\n if (cancelled) {\n await eventStream.cancel()\n return\n }\n const subscribedReader = eventStream.getReader()\n reader = eventReader = subscribedReader\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n void listen(subscribedReader)\n .finally(() => {\n // A closed stream can still have a final event being processed.\n // Release only after the listener has finished draining it.\n // A processing failure can leave the stream open; cancel it too.\n // Preserve the original failure if the stream already errored.\n // Error settlement must not wait for transport cleanup.\n void subscribedReader.cancel().catch(() => undefined)\n subscribedReader.releaseLock()\n if (eventReader === subscribedReader) eventReader = undefined\n })\n .catch((error: unknown) => {\n if (!cancelled && collection.status === `loading`) {\n markError(error)\n } else if (!cancelled) {\n console.error(`TrailBase subscription failed`, error)\n }\n })\n\n // Eager mode: perform initial fetch to populate everything\n if (internalSyncMode === `eager`) {\n // Load everything on initial load.\n await load({})\n if (cancelled) return\n fullSyncCompleted = true\n }\n if (!cancelled && collection.status === `loading`) {\n markReady()\n }\n } catch (error) {\n // An abandoned startup must not cancel a replacement session's reader.\n if (cancelled) return\n cancelEventReader()\n if (collection.status === `loading`) {\n markError(error)\n }\n return\n }\n\n // Lastly, start a periodic cleanup task that will be removed when the\n // reader closes.\n if (cancelled || !reader) return\n\n 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 const clearCleanupTask = () => {\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n }\n // listen() reports read errors. Observe this separate promise too.\n void reader.closed.then(clearCleanupTask, clearCleanupTask)\n }\n\n void start()\n\n // Eager mode doesn't need subset loading\n if (internalSyncMode === `eager`) {\n return { cleanup }\n }\n\n return {\n cleanup,\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 const {\n recordApi: _recordApi,\n parse: _parse,\n serialize: _serialize,\n ...collectionConfig\n } = config\n\n const options = {\n ...collectionConfig,\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 return withCollectionConfigFactory(\n options,\n () => trailBaseCollectionOptions(config) as typeof options,\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","LoadSubsetOperationAbortedError","ExpectedInsertTypeError","ExpectedUpdateTypeError","ExpectedDeleteTypeError","withCollectionConfigFactory"],"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 LoadSubsetOperationAbortedError,\n withCollectionConfigFactory,\n} from '@tanstack/db'\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 [\n K in keyof InputType as InputType[K] extends OutputType[K] ? K : never\n ]?: 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 [\n K in keyof InputType as InputType[K] extends OutputType[K] ? never : K\n ]: 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 // An already-errored stream rejects cancellation too. Cleanup still\n // retires its reader; that rejection must not escape as detached work.\n void eventReader.cancel().catch(() => undefined)\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, markError, collection } = params\n let cancelled = false\n let periodicCleanupTask: ReturnType<typeof setInterval> | undefined\n const cleanup = () => {\n cancelled = true\n settleListener()\n cancelEventReader()\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n }\n\n // TrailBase cursors are opaque. Cache a cursor by its last fetched key\n // for callers that supply an established lastKey on a later request.\n // CollectionSubscription does not derive lastKey from local snapshots,\n // so its ordered requests use the offset fallback.\n const cursors = new Map<string | number, string>()\n\n let listening = false\n // Orders this run's change events against its list calls. Only loads\n // in flight compare against it, so it is kept only while one runs.\n let eventSequence = 0\n const eventSequenceByKey = new Map<TKey, number>()\n let loadsInFlight = 0\n // Settles when the listener runs, cannot start (with the failure), or\n // the session cleans up, so a waiting load never depends on the order\n // of their continuations.\n let settleListener!: (failure?: { error: unknown }) => void\n const listenerSettled = new Promise<{ error: unknown } | undefined>(\n (resolve) => {\n settleListener = resolve\n },\n )\n\n // Wait for the listener, but let the caller's abort end the wait even\n // when the subscription never resolves.\n async function waitForListener(signal: AbortSignal | undefined) {\n if (!signal) return listenerSettled\n let onAbort!: () => void\n const aborted = new Promise<undefined>((resolve) => {\n onAbort = () => resolve(undefined)\n signal.addEventListener(`abort`, onAbort, { once: true })\n })\n try {\n return await Promise.race([listenerSettled, aborted])\n } finally {\n // The request signal can outlive this wait; do not keep its listener.\n signal.removeEventListener(`abort`, onAbort)\n }\n }\n\n // Load (more) data.\n // A caller that aborts sees `AbortError`. Pages a commit accepted still\n // apply, and the load waits until they are visible; cleanup is quiet.\n async function load(opts: LoadSubsetOptions) {\n if (cancelled) return\n if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()\n if (!listening) {\n const failure = await waitForListener(opts.signal)\n if (cancelled) return\n if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()\n if (failure) throw failure.error\n }\n\n const lastKey = opts.cursor?.lastKey\n let cursor: string | undefined =\n lastKey !== undefined ? cursors.get(lastKey) : undefined\n let offset: number | undefined =\n cursor === undefined && (opts.offset ?? 0) > 0\n ? opts.offset\n : 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 const appliedPages: Array<Promise<void>> = []\n\n loadsInFlight++\n try {\n while (true) {\n const limit = Math.min(remaining, 256)\n let response\n // A change event after this point is newer than the page.\n const listedAfter = eventSequence\n try {\n response = await config.recordApi.list({\n pagination: {\n limit,\n offset,\n cursor,\n },\n order,\n filters,\n })\n } catch (error) {\n if (cancelled) return\n if (opts.signal?.aborted) break\n throw error\n }\n if (cancelled) return\n // Check the signal before commit to discard a stale page.\n if (opts.signal?.aborted) break\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 const value = parse(response.records[i]!)\n const key = getKey(value)\n // The listener already applied a newer change for this row.\n if ((eventSequenceByKey.get(key) ?? 0) > listedAfter) continue\n // A page row is a full row. An update creates a missing row and\n // replaces a held one, including one an accepted event queued.\n write({ type: `update`, value })\n }\n\n // An accepted page always applies.\n const applied = commit(opts.signal)\n if (applied !== true) {\n appliedPages.push(applied)\n }\n if (cancelled || opts.signal?.aborted) break\n\n remaining -= length\n\n // Drained or read enough.\n if (length < limit || remaining <= 0) {\n // Core subscriptions cannot send lastKey, so retaining their\n // cursors only grows this cache. Direct callers can use the key\n // from a completed page as their next opaque boundary.\n if (\n response.cursor &&\n (opts.subscription === undefined || lastKey !== undefined)\n ) {\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 } finally {\n if (--loadsInFlight === 0) eventSequenceByKey.clear()\n }\n\n await Promise.all(appliedPages)\n // An accepted page applies, but a caller that aborted still sees\n // `AbortError`.\n if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()\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 return\n }\n\n begin()\n let value: TItem | undefined\n if (`Insert` in event) {\n value = parse(event.Insert as TRecord)\n // A page may already hold a newer value for this row, so an\n // insert is written as a full-row update; it creates a missing row.\n write({ type: `update`, 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 void commit()\n\n if (value) {\n ++eventSequence\n if (loadsInFlight > 0)\n eventSequenceByKey.set(getKey(value), eventSequence)\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 let reader: ReadableStreamDefaultReader<Event> | undefined\n try {\n const eventStream = await config.recordApi.subscribe(`*`)\n if (cancelled) {\n await eventStream.cancel()\n return\n }\n const subscribedReader = eventStream.getReader()\n reader = eventReader = subscribedReader\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n void listen(subscribedReader)\n .finally(() => {\n // A closed stream can still have a final event being processed.\n // Release only after the listener has finished draining it.\n // A processing failure can leave the stream open; cancel it too.\n // Preserve the original failure if the stream already errored.\n // Error settlement must not wait for transport cleanup.\n void subscribedReader.cancel().catch(() => undefined)\n subscribedReader.releaseLock()\n if (eventReader === subscribedReader) eventReader = undefined\n })\n .catch((error: unknown) => {\n if (!cancelled && collection.status === `loading`) {\n markError(error)\n } else if (!cancelled) {\n console.error(`TrailBase subscription failed`, error)\n }\n })\n listening = true\n settleListener()\n\n // Eager mode: perform initial fetch to populate everything\n if (internalSyncMode === `eager`) {\n // Load everything on initial load.\n await load({})\n if (cancelled) return\n fullSyncCompleted = true\n }\n if (!cancelled && collection.status === `loading`) {\n markReady()\n }\n } catch (error) {\n // An abandoned startup must not cancel a replacement session's reader.\n if (cancelled) return\n settleListener({ error })\n cancelEventReader()\n if (collection.status === `loading`) {\n markError(error)\n }\n return\n }\n\n // Lastly, start a periodic cleanup task that will be removed when the\n // reader closes.\n if (cancelled || !reader) return\n\n 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 const clearCleanupTask = () => {\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n }\n // listen() reports read errors. Observe this separate promise too.\n void reader.closed.then(clearCleanupTask, clearCleanupTask)\n }\n\n void start()\n\n // Eager mode doesn't need subset loading\n if (internalSyncMode === `eager`) {\n return { cleanup }\n }\n\n return {\n cleanup,\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 const {\n recordApi: _recordApi,\n parse: _parse,\n serialize: _serialize,\n ...collectionConfig\n } = config\n\n const options = {\n ...collectionConfig,\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 return withCollectionConfigFactory(\n options,\n () => trailBaseCollectionOptions(config) as typeof options,\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","LoadSubsetOperationAbortedError","ExpectedInsertTypeError","ExpectedUpdateTypeError","ExpectedDeleteTypeError","withCollectionConfigFactory"],"mappings":"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"}
|
package/dist/esm/trailbase.js
CHANGED
|
@@ -61,6 +61,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
61
61
|
let periodicCleanupTask;
|
|
62
62
|
const cleanup = () => {
|
|
63
63
|
cancelled = true;
|
|
64
|
+
settleListener();
|
|
64
65
|
cancelEventReader();
|
|
65
66
|
if (periodicCleanupTask !== void 0) {
|
|
66
67
|
clearInterval(periodicCleanupTask);
|
|
@@ -68,9 +69,38 @@ function trailBaseCollectionOptions(config) {
|
|
|
68
69
|
}
|
|
69
70
|
};
|
|
70
71
|
const cursors = /* @__PURE__ */ new Map();
|
|
72
|
+
let listening = false;
|
|
73
|
+
let eventSequence = 0;
|
|
74
|
+
const eventSequenceByKey = /* @__PURE__ */ new Map();
|
|
75
|
+
let loadsInFlight = 0;
|
|
76
|
+
let settleListener;
|
|
77
|
+
const listenerSettled = new Promise(
|
|
78
|
+
(resolve) => {
|
|
79
|
+
settleListener = resolve;
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
async function waitForListener(signal) {
|
|
83
|
+
if (!signal) return listenerSettled;
|
|
84
|
+
let onAbort;
|
|
85
|
+
const aborted = new Promise((resolve) => {
|
|
86
|
+
onAbort = () => resolve(void 0);
|
|
87
|
+
signal.addEventListener(`abort`, onAbort, { once: true });
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
return await Promise.race([listenerSettled, aborted]);
|
|
91
|
+
} finally {
|
|
92
|
+
signal.removeEventListener(`abort`, onAbort);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
71
95
|
async function load(opts) {
|
|
72
96
|
if (cancelled) return;
|
|
73
97
|
if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError();
|
|
98
|
+
if (!listening) {
|
|
99
|
+
const failure = await waitForListener(opts.signal);
|
|
100
|
+
if (cancelled) return;
|
|
101
|
+
if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError();
|
|
102
|
+
if (failure) throw failure.error;
|
|
103
|
+
}
|
|
74
104
|
const lastKey = opts.cursor?.lastKey;
|
|
75
105
|
let cursor = lastKey !== void 0 ? cursors.get(lastKey) : void 0;
|
|
76
106
|
let offset = cursor === void 0 && (opts.offset ?? 0) > 0 ? opts.offset : void 0;
|
|
@@ -84,57 +114,63 @@ function trailBaseCollectionOptions(config) {
|
|
|
84
114
|
return;
|
|
85
115
|
}
|
|
86
116
|
const appliedPages = [];
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
response
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
loadsInFlight++;
|
|
118
|
+
try {
|
|
119
|
+
while (true) {
|
|
120
|
+
const limit = Math.min(remaining, 256);
|
|
121
|
+
let response;
|
|
122
|
+
const listedAfter = eventSequence;
|
|
123
|
+
try {
|
|
124
|
+
response = await config.recordApi.list({
|
|
125
|
+
pagination: {
|
|
126
|
+
limit,
|
|
127
|
+
offset,
|
|
128
|
+
cursor
|
|
129
|
+
},
|
|
130
|
+
order,
|
|
131
|
+
filters
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (cancelled) return;
|
|
135
|
+
if (opts.signal?.aborted) break;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
101
138
|
if (cancelled) return;
|
|
102
139
|
if (opts.signal?.aborted) break;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
const length = response.records.length;
|
|
141
|
+
if (length === 0) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
begin();
|
|
145
|
+
for (let i = 0; i < Math.min(length, remaining); ++i) {
|
|
146
|
+
const value = parse(response.records[i]);
|
|
147
|
+
const key = getKey(value);
|
|
148
|
+
if ((eventSequenceByKey.get(key) ?? 0) > listedAfter) continue;
|
|
149
|
+
write({ type: `update`, value });
|
|
150
|
+
}
|
|
151
|
+
const applied = commit(opts.signal);
|
|
152
|
+
if (applied !== true) {
|
|
153
|
+
appliedPages.push(applied);
|
|
154
|
+
}
|
|
155
|
+
if (cancelled || opts.signal?.aborted) break;
|
|
156
|
+
remaining -= length;
|
|
157
|
+
if (length < limit || remaining <= 0) {
|
|
158
|
+
if (response.cursor && (opts.subscription === void 0 || lastKey !== void 0)) {
|
|
159
|
+
cursors.set(
|
|
160
|
+
getKey(parse(response.records.at(-1))),
|
|
161
|
+
response.cursor
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (offset !== void 0) {
|
|
167
|
+
offset += length;
|
|
168
|
+
} else {
|
|
169
|
+
cursor = response.cursor;
|
|
130
170
|
}
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
if (offset !== void 0) {
|
|
134
|
-
offset += length;
|
|
135
|
-
} else {
|
|
136
|
-
cursor = response.cursor;
|
|
137
171
|
}
|
|
172
|
+
} finally {
|
|
173
|
+
if (--loadsInFlight === 0) eventSequenceByKey.clear();
|
|
138
174
|
}
|
|
139
175
|
await Promise.all(appliedPages);
|
|
140
176
|
if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError();
|
|
@@ -149,7 +185,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
149
185
|
let value;
|
|
150
186
|
if (`Insert` in event) {
|
|
151
187
|
value = parse(event.Insert);
|
|
152
|
-
write({ type: `
|
|
188
|
+
write({ type: `update`, value });
|
|
153
189
|
} else if (`Delete` in event) {
|
|
154
190
|
value = parse(event.Delete);
|
|
155
191
|
write({ type: `delete`, value });
|
|
@@ -161,6 +197,9 @@ function trailBaseCollectionOptions(config) {
|
|
|
161
197
|
}
|
|
162
198
|
void commit();
|
|
163
199
|
if (value) {
|
|
200
|
+
++eventSequence;
|
|
201
|
+
if (loadsInFlight > 0)
|
|
202
|
+
eventSequenceByKey.set(getKey(value), eventSequence);
|
|
164
203
|
seenIds.setState((curr) => {
|
|
165
204
|
const newIds = new Map(curr);
|
|
166
205
|
newIds.set(String(getKey(value)), Date.now());
|
|
@@ -190,6 +229,8 @@ function trailBaseCollectionOptions(config) {
|
|
|
190
229
|
console.error(`TrailBase subscription failed`, error);
|
|
191
230
|
}
|
|
192
231
|
});
|
|
232
|
+
listening = true;
|
|
233
|
+
settleListener();
|
|
193
234
|
if (internalSyncMode === `eager`) {
|
|
194
235
|
await load({});
|
|
195
236
|
if (cancelled) return;
|
|
@@ -200,6 +241,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
200
241
|
}
|
|
201
242
|
} catch (error) {
|
|
202
243
|
if (cancelled) return;
|
|
244
|
+
settleListener({ error });
|
|
203
245
|
cancelEventReader();
|
|
204
246
|
if (collection.status === `loading`) {
|
|
205
247
|
markError(error);
|
|
@@ -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 LoadSubsetOperationAbortedError,\n withCollectionConfigFactory,\n} from '@tanstack/db'\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 [\n K in keyof InputType as InputType[K] extends OutputType[K] ? K : never\n ]?: 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 [\n K in keyof InputType as InputType[K] extends OutputType[K] ? never : K\n ]: 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 // An already-errored stream rejects cancellation too. Cleanup still\n // retires its reader; that rejection must not escape as detached work.\n void eventReader.cancel().catch(() => undefined)\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, markError, collection } = params\n let cancelled = false\n let periodicCleanupTask: ReturnType<typeof setInterval> | undefined\n\n const cleanup = () => {\n cancelled = true\n cancelEventReader()\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n }\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 // A caller that aborts sees `AbortError`. Pages a commit accepted still\n // apply, and the load waits until they are visible; cleanup is quiet.\n async function load(opts: LoadSubsetOptions) {\n if (cancelled) return\n if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()\n\n const lastKey = opts.cursor?.lastKey\n let cursor: string | undefined =\n lastKey !== undefined ? cursors.get(lastKey) : undefined\n let offset: number | undefined =\n cursor === undefined && (opts.offset ?? 0) > 0\n ? opts.offset\n : 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 const appliedPages: Array<Promise<void>> = []\n\n while (true) {\n const limit = Math.min(remaining, 256)\n let response\n try {\n response = await config.recordApi.list({\n pagination: {\n limit,\n offset,\n cursor,\n },\n order,\n filters,\n })\n } catch (error) {\n if (cancelled) return\n if (opts.signal?.aborted) break\n throw error\n }\n if (cancelled) return\n // Check the signal before commit to discard a stale page.\n if (opts.signal?.aborted) break\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 // An accepted page always applies.\n const applied = commit(opts.signal)\n if (applied !== true) {\n appliedPages.push(applied)\n }\n if (cancelled || opts.signal?.aborted) break\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 await Promise.all(appliedPages)\n // An accepted page applies, but a caller that aborted still sees\n // `AbortError`.\n if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()\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 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 void 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 let reader: ReadableStreamDefaultReader<Event> | undefined\n try {\n const eventStream = await config.recordApi.subscribe(`*`)\n if (cancelled) {\n await eventStream.cancel()\n return\n }\n const subscribedReader = eventStream.getReader()\n reader = eventReader = subscribedReader\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n void listen(subscribedReader)\n .finally(() => {\n // A closed stream can still have a final event being processed.\n // Release only after the listener has finished draining it.\n // A processing failure can leave the stream open; cancel it too.\n // Preserve the original failure if the stream already errored.\n // Error settlement must not wait for transport cleanup.\n void subscribedReader.cancel().catch(() => undefined)\n subscribedReader.releaseLock()\n if (eventReader === subscribedReader) eventReader = undefined\n })\n .catch((error: unknown) => {\n if (!cancelled && collection.status === `loading`) {\n markError(error)\n } else if (!cancelled) {\n console.error(`TrailBase subscription failed`, error)\n }\n })\n\n // Eager mode: perform initial fetch to populate everything\n if (internalSyncMode === `eager`) {\n // Load everything on initial load.\n await load({})\n if (cancelled) return\n fullSyncCompleted = true\n }\n if (!cancelled && collection.status === `loading`) {\n markReady()\n }\n } catch (error) {\n // An abandoned startup must not cancel a replacement session's reader.\n if (cancelled) return\n cancelEventReader()\n if (collection.status === `loading`) {\n markError(error)\n }\n return\n }\n\n // Lastly, start a periodic cleanup task that will be removed when the\n // reader closes.\n if (cancelled || !reader) return\n\n 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 const clearCleanupTask = () => {\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n }\n // listen() reports read errors. Observe this separate promise too.\n void reader.closed.then(clearCleanupTask, clearCleanupTask)\n }\n\n void start()\n\n // Eager mode doesn't need subset loading\n if (internalSyncMode === `eager`) {\n return { cleanup }\n }\n\n return {\n cleanup,\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 const {\n recordApi: _recordApi,\n parse: _parse,\n serialize: _serialize,\n ...collectionConfig\n } = config\n\n const options = {\n ...collectionConfig,\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 return withCollectionConfigFactory(\n options,\n () => trailBaseCollectionOptions(config) as typeof options,\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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"}
|
|
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 LoadSubsetOperationAbortedError,\n withCollectionConfigFactory,\n} from '@tanstack/db'\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 [\n K in keyof InputType as InputType[K] extends OutputType[K] ? K : never\n ]?: 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 [\n K in keyof InputType as InputType[K] extends OutputType[K] ? never : K\n ]: 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 // An already-errored stream rejects cancellation too. Cleanup still\n // retires its reader; that rejection must not escape as detached work.\n void eventReader.cancel().catch(() => undefined)\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, markError, collection } = params\n let cancelled = false\n let periodicCleanupTask: ReturnType<typeof setInterval> | undefined\n const cleanup = () => {\n cancelled = true\n settleListener()\n cancelEventReader()\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n }\n\n // TrailBase cursors are opaque. Cache a cursor by its last fetched key\n // for callers that supply an established lastKey on a later request.\n // CollectionSubscription does not derive lastKey from local snapshots,\n // so its ordered requests use the offset fallback.\n const cursors = new Map<string | number, string>()\n\n let listening = false\n // Orders this run's change events against its list calls. Only loads\n // in flight compare against it, so it is kept only while one runs.\n let eventSequence = 0\n const eventSequenceByKey = new Map<TKey, number>()\n let loadsInFlight = 0\n // Settles when the listener runs, cannot start (with the failure), or\n // the session cleans up, so a waiting load never depends on the order\n // of their continuations.\n let settleListener!: (failure?: { error: unknown }) => void\n const listenerSettled = new Promise<{ error: unknown } | undefined>(\n (resolve) => {\n settleListener = resolve\n },\n )\n\n // Wait for the listener, but let the caller's abort end the wait even\n // when the subscription never resolves.\n async function waitForListener(signal: AbortSignal | undefined) {\n if (!signal) return listenerSettled\n let onAbort!: () => void\n const aborted = new Promise<undefined>((resolve) => {\n onAbort = () => resolve(undefined)\n signal.addEventListener(`abort`, onAbort, { once: true })\n })\n try {\n return await Promise.race([listenerSettled, aborted])\n } finally {\n // The request signal can outlive this wait; do not keep its listener.\n signal.removeEventListener(`abort`, onAbort)\n }\n }\n\n // Load (more) data.\n // A caller that aborts sees `AbortError`. Pages a commit accepted still\n // apply, and the load waits until they are visible; cleanup is quiet.\n async function load(opts: LoadSubsetOptions) {\n if (cancelled) return\n if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()\n if (!listening) {\n const failure = await waitForListener(opts.signal)\n if (cancelled) return\n if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()\n if (failure) throw failure.error\n }\n\n const lastKey = opts.cursor?.lastKey\n let cursor: string | undefined =\n lastKey !== undefined ? cursors.get(lastKey) : undefined\n let offset: number | undefined =\n cursor === undefined && (opts.offset ?? 0) > 0\n ? opts.offset\n : 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 const appliedPages: Array<Promise<void>> = []\n\n loadsInFlight++\n try {\n while (true) {\n const limit = Math.min(remaining, 256)\n let response\n // A change event after this point is newer than the page.\n const listedAfter = eventSequence\n try {\n response = await config.recordApi.list({\n pagination: {\n limit,\n offset,\n cursor,\n },\n order,\n filters,\n })\n } catch (error) {\n if (cancelled) return\n if (opts.signal?.aborted) break\n throw error\n }\n if (cancelled) return\n // Check the signal before commit to discard a stale page.\n if (opts.signal?.aborted) break\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 const value = parse(response.records[i]!)\n const key = getKey(value)\n // The listener already applied a newer change for this row.\n if ((eventSequenceByKey.get(key) ?? 0) > listedAfter) continue\n // A page row is a full row. An update creates a missing row and\n // replaces a held one, including one an accepted event queued.\n write({ type: `update`, value })\n }\n\n // An accepted page always applies.\n const applied = commit(opts.signal)\n if (applied !== true) {\n appliedPages.push(applied)\n }\n if (cancelled || opts.signal?.aborted) break\n\n remaining -= length\n\n // Drained or read enough.\n if (length < limit || remaining <= 0) {\n // Core subscriptions cannot send lastKey, so retaining their\n // cursors only grows this cache. Direct callers can use the key\n // from a completed page as their next opaque boundary.\n if (\n response.cursor &&\n (opts.subscription === undefined || lastKey !== undefined)\n ) {\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 } finally {\n if (--loadsInFlight === 0) eventSequenceByKey.clear()\n }\n\n await Promise.all(appliedPages)\n // An accepted page applies, but a caller that aborted still sees\n // `AbortError`.\n if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()\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 return\n }\n\n begin()\n let value: TItem | undefined\n if (`Insert` in event) {\n value = parse(event.Insert as TRecord)\n // A page may already hold a newer value for this row, so an\n // insert is written as a full-row update; it creates a missing row.\n write({ type: `update`, 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 void commit()\n\n if (value) {\n ++eventSequence\n if (loadsInFlight > 0)\n eventSequenceByKey.set(getKey(value), eventSequence)\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 let reader: ReadableStreamDefaultReader<Event> | undefined\n try {\n const eventStream = await config.recordApi.subscribe(`*`)\n if (cancelled) {\n await eventStream.cancel()\n return\n }\n const subscribedReader = eventStream.getReader()\n reader = eventReader = subscribedReader\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n void listen(subscribedReader)\n .finally(() => {\n // A closed stream can still have a final event being processed.\n // Release only after the listener has finished draining it.\n // A processing failure can leave the stream open; cancel it too.\n // Preserve the original failure if the stream already errored.\n // Error settlement must not wait for transport cleanup.\n void subscribedReader.cancel().catch(() => undefined)\n subscribedReader.releaseLock()\n if (eventReader === subscribedReader) eventReader = undefined\n })\n .catch((error: unknown) => {\n if (!cancelled && collection.status === `loading`) {\n markError(error)\n } else if (!cancelled) {\n console.error(`TrailBase subscription failed`, error)\n }\n })\n listening = true\n settleListener()\n\n // Eager mode: perform initial fetch to populate everything\n if (internalSyncMode === `eager`) {\n // Load everything on initial load.\n await load({})\n if (cancelled) return\n fullSyncCompleted = true\n }\n if (!cancelled && collection.status === `loading`) {\n markReady()\n }\n } catch (error) {\n // An abandoned startup must not cancel a replacement session's reader.\n if (cancelled) return\n settleListener({ error })\n cancelEventReader()\n if (collection.status === `loading`) {\n markError(error)\n }\n return\n }\n\n // Lastly, start a periodic cleanup task that will be removed when the\n // reader closes.\n if (cancelled || !reader) return\n\n 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 const clearCleanupTask = () => {\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n }\n // listen() reports read errors. Observe this separate promise too.\n void reader.closed.then(clearCleanupTask, clearCleanupTask)\n }\n\n void start()\n\n // Eager mode doesn't need subset loading\n if (internalSyncMode === `eager`) {\n return { cleanup }\n }\n\n return {\n cleanup,\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 const {\n recordApi: _recordApi,\n parse: _parse,\n serialize: _serialize,\n ...collectionConfig\n } = config\n\n const options = {\n ...collectionConfig,\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 return withCollectionConfigFactory(\n options,\n () => trailBaseCollectionOptions(config) as typeof options,\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.120",
|
|
4
4
|
"description": "TrailBase collection for TanStack DB",
|
|
5
5
|
"author": "Sebastian Jeltsch",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@tanstack/store": "^0.9.2",
|
|
44
44
|
"debug": "^4.4.3",
|
|
45
45
|
"trailbase": "^0.10.0",
|
|
46
|
-
"@tanstack/db": "0.
|
|
46
|
+
"@tanstack/db": "0.14.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"typescript": ">=4.7"
|
package/src/trailbase.ts
CHANGED
|
@@ -188,9 +188,9 @@ export function trailBaseCollectionOptions<
|
|
|
188
188
|
const { begin, write, commit, markReady, markError, collection } = params
|
|
189
189
|
let cancelled = false
|
|
190
190
|
let periodicCleanupTask: ReturnType<typeof setInterval> | undefined
|
|
191
|
-
|
|
192
191
|
const cleanup = () => {
|
|
193
192
|
cancelled = true
|
|
193
|
+
settleListener()
|
|
194
194
|
cancelEventReader()
|
|
195
195
|
if (periodicCleanupTask !== undefined) {
|
|
196
196
|
clearInterval(periodicCleanupTask)
|
|
@@ -198,17 +198,57 @@ export function trailBaseCollectionOptions<
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
201
|
+
// TrailBase cursors are opaque. Cache a cursor by its last fetched key
|
|
202
|
+
// for callers that supply an established lastKey on a later request.
|
|
203
|
+
// CollectionSubscription does not derive lastKey from local snapshots,
|
|
204
|
+
// so its ordered requests use the offset fallback.
|
|
204
205
|
const cursors = new Map<string | number, string>()
|
|
205
206
|
|
|
207
|
+
let listening = false
|
|
208
|
+
// Orders this run's change events against its list calls. Only loads
|
|
209
|
+
// in flight compare against it, so it is kept only while one runs.
|
|
210
|
+
let eventSequence = 0
|
|
211
|
+
const eventSequenceByKey = new Map<TKey, number>()
|
|
212
|
+
let loadsInFlight = 0
|
|
213
|
+
// Settles when the listener runs, cannot start (with the failure), or
|
|
214
|
+
// the session cleans up, so a waiting load never depends on the order
|
|
215
|
+
// of their continuations.
|
|
216
|
+
let settleListener!: (failure?: { error: unknown }) => void
|
|
217
|
+
const listenerSettled = new Promise<{ error: unknown } | undefined>(
|
|
218
|
+
(resolve) => {
|
|
219
|
+
settleListener = resolve
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
// Wait for the listener, but let the caller's abort end the wait even
|
|
224
|
+
// when the subscription never resolves.
|
|
225
|
+
async function waitForListener(signal: AbortSignal | undefined) {
|
|
226
|
+
if (!signal) return listenerSettled
|
|
227
|
+
let onAbort!: () => void
|
|
228
|
+
const aborted = new Promise<undefined>((resolve) => {
|
|
229
|
+
onAbort = () => resolve(undefined)
|
|
230
|
+
signal.addEventListener(`abort`, onAbort, { once: true })
|
|
231
|
+
})
|
|
232
|
+
try {
|
|
233
|
+
return await Promise.race([listenerSettled, aborted])
|
|
234
|
+
} finally {
|
|
235
|
+
// The request signal can outlive this wait; do not keep its listener.
|
|
236
|
+
signal.removeEventListener(`abort`, onAbort)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
206
240
|
// Load (more) data.
|
|
207
241
|
// A caller that aborts sees `AbortError`. Pages a commit accepted still
|
|
208
242
|
// apply, and the load waits until they are visible; cleanup is quiet.
|
|
209
243
|
async function load(opts: LoadSubsetOptions) {
|
|
210
244
|
if (cancelled) return
|
|
211
245
|
if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()
|
|
246
|
+
if (!listening) {
|
|
247
|
+
const failure = await waitForListener(opts.signal)
|
|
248
|
+
if (cancelled) return
|
|
249
|
+
if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()
|
|
250
|
+
if (failure) throw failure.error
|
|
251
|
+
}
|
|
212
252
|
|
|
213
253
|
const lastKey = opts.cursor?.lastKey
|
|
214
254
|
let cursor: string | undefined =
|
|
@@ -230,69 +270,85 @@ export function trailBaseCollectionOptions<
|
|
|
230
270
|
}
|
|
231
271
|
const appliedPages: Array<Promise<void>> = []
|
|
232
272
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
response
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
273
|
+
loadsInFlight++
|
|
274
|
+
try {
|
|
275
|
+
while (true) {
|
|
276
|
+
const limit = Math.min(remaining, 256)
|
|
277
|
+
let response
|
|
278
|
+
// A change event after this point is newer than the page.
|
|
279
|
+
const listedAfter = eventSequence
|
|
280
|
+
try {
|
|
281
|
+
response = await config.recordApi.list({
|
|
282
|
+
pagination: {
|
|
283
|
+
limit,
|
|
284
|
+
offset,
|
|
285
|
+
cursor,
|
|
286
|
+
},
|
|
287
|
+
order,
|
|
288
|
+
filters,
|
|
289
|
+
})
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (cancelled) return
|
|
292
|
+
if (opts.signal?.aborted) break
|
|
293
|
+
throw error
|
|
294
|
+
}
|
|
247
295
|
if (cancelled) return
|
|
296
|
+
// Check the signal before commit to discard a stale page.
|
|
248
297
|
if (opts.signal?.aborted) break
|
|
249
|
-
throw error
|
|
250
|
-
}
|
|
251
|
-
if (cancelled) return
|
|
252
|
-
// Check the signal before commit to discard a stale page.
|
|
253
|
-
if (opts.signal?.aborted) break
|
|
254
298
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
begin()
|
|
262
|
-
|
|
263
|
-
for (let i = 0; i < Math.min(length, remaining); ++i) {
|
|
264
|
-
write({
|
|
265
|
-
type: `insert`,
|
|
266
|
-
value: parse(response.records[i]!),
|
|
267
|
-
})
|
|
268
|
-
}
|
|
299
|
+
const length = response.records.length
|
|
300
|
+
if (length === 0) {
|
|
301
|
+
// Drained - read everything.
|
|
302
|
+
break
|
|
303
|
+
}
|
|
269
304
|
|
|
270
|
-
|
|
271
|
-
const applied = commit(opts.signal)
|
|
272
|
-
if (applied !== true) {
|
|
273
|
-
appliedPages.push(applied)
|
|
274
|
-
}
|
|
275
|
-
if (cancelled || opts.signal?.aborted) break
|
|
305
|
+
begin()
|
|
276
306
|
|
|
277
|
-
|
|
307
|
+
for (let i = 0; i < Math.min(length, remaining); ++i) {
|
|
308
|
+
const value = parse(response.records[i]!)
|
|
309
|
+
const key = getKey(value)
|
|
310
|
+
// The listener already applied a newer change for this row.
|
|
311
|
+
if ((eventSequenceByKey.get(key) ?? 0) > listedAfter) continue
|
|
312
|
+
// A page row is a full row. An update creates a missing row and
|
|
313
|
+
// replaces a held one, including one an accepted event queued.
|
|
314
|
+
write({ type: `update`, value })
|
|
315
|
+
}
|
|
278
316
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
317
|
+
// An accepted page always applies.
|
|
318
|
+
const applied = commit(opts.signal)
|
|
319
|
+
if (applied !== true) {
|
|
320
|
+
appliedPages.push(applied)
|
|
321
|
+
}
|
|
322
|
+
if (cancelled || opts.signal?.aborted) break
|
|
323
|
+
|
|
324
|
+
remaining -= length
|
|
325
|
+
|
|
326
|
+
// Drained or read enough.
|
|
327
|
+
if (length < limit || remaining <= 0) {
|
|
328
|
+
// Core subscriptions cannot send lastKey, so retaining their
|
|
329
|
+
// cursors only grows this cache. Direct callers can use the key
|
|
330
|
+
// from a completed page as their next opaque boundary.
|
|
331
|
+
if (
|
|
332
|
+
response.cursor &&
|
|
333
|
+
(opts.subscription === undefined || lastKey !== undefined)
|
|
334
|
+
) {
|
|
335
|
+
cursors.set(
|
|
336
|
+
getKey(parse(response.records.at(-1)!)),
|
|
337
|
+
response.cursor,
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
break
|
|
286
341
|
}
|
|
287
|
-
break
|
|
288
|
-
}
|
|
289
342
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
343
|
+
// Update params for next iteration.
|
|
344
|
+
if (offset !== undefined) {
|
|
345
|
+
offset += length
|
|
346
|
+
} else {
|
|
347
|
+
cursor = response.cursor
|
|
348
|
+
}
|
|
295
349
|
}
|
|
350
|
+
} finally {
|
|
351
|
+
if (--loadsInFlight === 0) eventSequenceByKey.clear()
|
|
296
352
|
}
|
|
297
353
|
|
|
298
354
|
await Promise.all(appliedPages)
|
|
@@ -314,7 +370,9 @@ export function trailBaseCollectionOptions<
|
|
|
314
370
|
let value: TItem | undefined
|
|
315
371
|
if (`Insert` in event) {
|
|
316
372
|
value = parse(event.Insert as TRecord)
|
|
317
|
-
|
|
373
|
+
// A page may already hold a newer value for this row, so an
|
|
374
|
+
// insert is written as a full-row update; it creates a missing row.
|
|
375
|
+
write({ type: `update`, value })
|
|
318
376
|
} else if (`Delete` in event) {
|
|
319
377
|
value = parse(event.Delete as TRecord)
|
|
320
378
|
write({ type: `delete`, value })
|
|
@@ -327,6 +385,9 @@ export function trailBaseCollectionOptions<
|
|
|
327
385
|
void commit()
|
|
328
386
|
|
|
329
387
|
if (value) {
|
|
388
|
+
++eventSequence
|
|
389
|
+
if (loadsInFlight > 0)
|
|
390
|
+
eventSequenceByKey.set(getKey(value), eventSequence)
|
|
330
391
|
seenIds.setState((curr: Map<string, number>) => {
|
|
331
392
|
const newIds = new Map(curr)
|
|
332
393
|
newIds.set(String(getKey(value)), Date.now())
|
|
@@ -367,6 +428,8 @@ export function trailBaseCollectionOptions<
|
|
|
367
428
|
console.error(`TrailBase subscription failed`, error)
|
|
368
429
|
}
|
|
369
430
|
})
|
|
431
|
+
listening = true
|
|
432
|
+
settleListener()
|
|
370
433
|
|
|
371
434
|
// Eager mode: perform initial fetch to populate everything
|
|
372
435
|
if (internalSyncMode === `eager`) {
|
|
@@ -381,6 +444,7 @@ export function trailBaseCollectionOptions<
|
|
|
381
444
|
} catch (error) {
|
|
382
445
|
// An abandoned startup must not cancel a replacement session's reader.
|
|
383
446
|
if (cancelled) return
|
|
447
|
+
settleListener({ error })
|
|
384
448
|
cancelEventReader()
|
|
385
449
|
if (collection.status === `loading`) {
|
|
386
450
|
markError(error)
|