@tanstack/trailbase-db-collection 0.1.114 → 0.1.115
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 +15 -5
- package/dist/cjs/trailbase.cjs.map +1 -1
- package/dist/esm/trailbase.js +16 -6
- package/dist/esm/trailbase.js.map +1 -1
- package/package.json +2 -2
- package/src/trailbase.ts +32 -12
package/dist/cjs/trailbase.cjs
CHANGED
|
@@ -71,7 +71,8 @@ function trailBaseCollectionOptions(config) {
|
|
|
71
71
|
};
|
|
72
72
|
const cursors = /* @__PURE__ */ new Map();
|
|
73
73
|
async function load(opts) {
|
|
74
|
-
if (cancelled
|
|
74
|
+
if (cancelled) return;
|
|
75
|
+
if (opts.signal?.aborted) throw new db.LoadSubsetOperationAbortedError();
|
|
75
76
|
const lastKey = opts.cursor?.lastKey;
|
|
76
77
|
let cursor = lastKey !== void 0 ? cursors.get(lastKey) : void 0;
|
|
77
78
|
let offset = cursor === void 0 && (opts.offset ?? 0) > 0 ? opts.offset : void 0;
|
|
@@ -99,10 +100,12 @@ function trailBaseCollectionOptions(config) {
|
|
|
99
100
|
filters
|
|
100
101
|
});
|
|
101
102
|
} catch (error) {
|
|
102
|
-
if (cancelled
|
|
103
|
+
if (cancelled) return;
|
|
104
|
+
if (opts.signal?.aborted) break;
|
|
103
105
|
throw error;
|
|
104
106
|
}
|
|
105
|
-
if (cancelled
|
|
107
|
+
if (cancelled) return;
|
|
108
|
+
if (opts.signal?.aborted) break;
|
|
106
109
|
const length = response.records.length;
|
|
107
110
|
if (length === 0) {
|
|
108
111
|
break;
|
|
@@ -118,7 +121,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
118
121
|
if (applied !== true) {
|
|
119
122
|
appliedPages.push(applied);
|
|
120
123
|
}
|
|
121
|
-
if (cancelled || opts.signal?.aborted)
|
|
124
|
+
if (cancelled || opts.signal?.aborted) break;
|
|
122
125
|
remaining -= length;
|
|
123
126
|
if (length < limit || remaining <= 0) {
|
|
124
127
|
if (response.cursor) {
|
|
@@ -136,6 +139,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
await Promise.all(appliedPages);
|
|
142
|
+
if (opts.signal?.aborted) throw new db.LoadSubsetOperationAbortedError();
|
|
139
143
|
}
|
|
140
144
|
async function listen(reader) {
|
|
141
145
|
while (true) {
|
|
@@ -246,8 +250,14 @@ function trailBaseCollectionOptions(config) {
|
|
|
246
250
|
fullSyncComplete: fullSyncCompleted
|
|
247
251
|
})
|
|
248
252
|
};
|
|
253
|
+
const {
|
|
254
|
+
recordApi: _recordApi,
|
|
255
|
+
parse: _parse,
|
|
256
|
+
serialize: _serialize,
|
|
257
|
+
...collectionConfig
|
|
258
|
+
} = config;
|
|
249
259
|
const options = {
|
|
250
|
-
...
|
|
260
|
+
...collectionConfig,
|
|
251
261
|
sync,
|
|
252
262
|
getKey,
|
|
253
263
|
onInsert: async (params) => {
|
|
@@ -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 { withCollectionConfigFactory } 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 [K in keyof InputType as InputType[K] extends OutputType[K]\n ? K\n : never]?: Conversion<InputType[K], OutputType[K]>\n}\n\ntype RequiredConversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = {\n // Excludes all keys that do not strictly require a conversation.\n [K in keyof InputType as InputType[K] extends OutputType[K]\n ? never\n : K]: Conversion<InputType[K], OutputType[K]>\n}\n\ntype Conversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = OptionalConversions<InputType, OutputType> &\n RequiredConversions<InputType, OutputType>\n\nfunction convert<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: InputType,\n): OutputType {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\nfunction convertPartial<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: Partial<InputType>,\n): Partial<OutputType> {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\nexport type TrailBaseSyncMode = SyncMode\n\n/**\n * Configuration interface for Trailbase Collection\n */\nexport interface TrailBaseCollectionConfig<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n> extends Omit<\n BaseCollectionConfig<TItem, TKey>,\n `onInsert` | `onUpdate` | `onDelete` | `syncMode`\n> {\n /**\n * Record API name\n */\n recordApi: RecordApi<TRecord>\n\n /**\n * The mode of sync to use for the collection.\n * @default `eager`\n */\n syncMode?: TrailBaseSyncMode\n\n parse: Conversions<TRecord, TItem>\n serialize: Conversions<TItem, TRecord>\n}\n\nexport type AwaitTxIdFn = (txId: string, timeout?: number) => Promise<boolean>\n\nexport interface TrailBaseCollectionUtils extends UtilsRecord {\n cancel: () => void\n}\n\nexport function trailBaseCollectionOptions<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n>(\n config: TrailBaseCollectionConfig<TItem, TRecord, TKey>,\n): CollectionConfig<TItem, TKey, never, TrailBaseCollectionUtils> & {\n utils: TrailBaseCollectionUtils\n} {\n const getKey = config.getKey\n\n const parse = (record: TRecord) =>\n convert<TRecord, TItem>(config.parse, record)\n const serialUpd = (item: Partial<TItem>) =>\n convertPartial<TItem, TRecord>(config.serialize, item)\n const serialIns = (item: TItem) =>\n convert<TItem, TRecord>(config.serialize, item)\n\n const seenIds = new Store(new Map<string, number>())\n\n const internalSyncMode = config.syncMode ?? `eager`\n let fullSyncCompleted = false\n\n const awaitIds = (\n ids: Array<string>,\n timeout: number = 120 * 1000,\n ): Promise<void> => {\n const completed = (value: Map<string, number>) =>\n ids.every((id) => value.has(id))\n if (completed(seenIds.state)) {\n return Promise.resolve()\n }\n\n return new Promise<void>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n sub.unsubscribe()\n reject(new TimeoutWaitingForIdsError(ids.toString()))\n }, timeout)\n\n const sub = seenIds.subscribe((value) => {\n if (completed(value)) {\n clearTimeout(timeoutId)\n sub.unsubscribe()\n resolve()\n }\n })\n })\n }\n\n let eventReader: ReadableStreamDefaultReader<Event> | undefined\n const cancelEventReader = () => {\n if (eventReader) {\n // 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 async function load(opts: LoadSubsetOptions) {\n if (cancelled || opts.signal?.aborted) return\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 || opts.signal?.aborted) return\n throw error\n }\n if (cancelled || opts.signal?.aborted) return\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 const applied = commit(opts.signal)\n if (applied !== true) {\n appliedPages.push(applied)\n }\n if (cancelled || opts.signal?.aborted) return\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 }\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 options = {\n ...config,\n sync,\n getKey,\n onInsert: async (\n params: InsertMutationFnParams<TItem, TKey>,\n ): Promise<Array<number | string>> => {\n const ids = await config.recordApi.createBulk(\n params.transaction.mutations.map((tx) => {\n const { type, modified } = tx\n if (type !== `insert`) {\n throw new ExpectedInsertTypeError(type)\n }\n return serialIns(modified)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly added to the local\n // DB by the subscription.\n await awaitIds(ids.map((id) => String(id)))\n\n return ids\n },\n onUpdate: async (params: UpdateMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, changes, key } = tx\n if (type !== `update`) {\n throw new ExpectedUpdateTypeError(type)\n }\n\n await config.recordApi.update(key, serialUpd(changes))\n\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n onDelete: async (params: DeleteMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, key } = tx\n if (type !== `delete`) {\n throw new ExpectedDeleteTypeError(type)\n }\n\n await config.recordApi.delete(key)\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n utils: {\n cancel: cancelEventReader,\n },\n }\n\n 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","ExpectedInsertTypeError","ExpectedUpdateTypeError","ExpectedDeleteTypeError","withCollectionConfigFactory"],"mappings":";;;;;AAqDA,SAAS,QAIP,aACA,OACY;AACZ,QAAM,IAAI;AAEV,SAAO,OAAO;AAAA,IACZ,OAAO,KAAK,KAAK,EAAE,IAAI,CAAC,MAAc;AACpC,YAAM,QAAQ,MAAM,CAAC;AACrB,aAAO,CAAC,GAAG,EAAE,CAAC,IAAI,KAAY,KAAK,KAAK;AAAA,IAC1C,CAAC;AAAA,EAAA;AAEL;AAEA,SAAS,eAIP,aACA,OACqB;AACrB,QAAM,IAAI;AAEV,SAAO,OAAO;AAAA,IACZ,OAAO,KAAK,KAAK,EAAE,IAAI,CAAC,MAAc;AACpC,YAAM,QAAQ,MAAM,CAAC;AACrB,aAAO,CAAC,GAAG,EAAE,CAAC,IAAI,KAAY,KAAK,KAAK;AAAA,IAC1C,CAAC;AAAA,EAAA;AAEL;AAoCO,SAAS,2BAKd,QAGA;AACA,QAAM,SAAS,OAAO;AAEtB,QAAM,QAAQ,CAAC,WACb,QAAwB,OAAO,OAAO,MAAM;AAC9C,QAAM,YAAY,CAAC,SACjB,eAA+B,OAAO,WAAW,IAAI;AACvD,QAAM,YAAY,CAAC,SACjB,QAAwB,OAAO,WAAW,IAAI;AAEhD,QAAM,UAAU,IAAIA,YAAM,oBAAI,KAAqB;AAEnD,QAAM,mBAAmB,OAAO,YAAY;AAC5C,MAAI,oBAAoB;AAExB,QAAM,WAAW,CACf,KACA,UAAkB,MAAM,QACN;AAClB,UAAM,YAAY,CAAC,UACjB,IAAI,MAAM,CAAC,OAAO,MAAM,IAAI,EAAE,CAAC;AACjC,QAAI,UAAU,QAAQ,KAAK,GAAG;AAC5B,aAAO,QAAQ,QAAA;AAAA,IACjB;AAEA,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,YAAM,YAAY,WAAW,MAAM;AACjC,YAAI,YAAA;AACJ,eAAO,IAAIC,OAAAA,0BAA0B,IAAI,SAAA,CAAU,CAAC;AAAA,MACtD,GAAG,OAAO;AAEV,YAAM,MAAM,QAAQ,UAAU,CAAC,UAAU;AACvC,YAAI,UAAU,KAAK,GAAG;AACpB,uBAAa,SAAS;AACtB,cAAI,YAAA;AACJ,kBAAA;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAEA,MAAI;AACJ,QAAM,oBAAoB,MAAM;AAC9B,QAAI,aAAa;AAGf,WAAK,YAAY,OAAA,EAAS,MAAM,MAAM,MAAS;AAC/C,kBAAY,YAAA;AACZ,oBAAc;AAAA,IAChB;AAAA,EACF;AAGA,QAAM,OAAO;AAAA,IACX,MAAM,CAAC,WAAuB;AAC5B,YAAM,EAAE,OAAO,OAAO,QAAQ,WAAW,WAAW,eAAe;AACnE,UAAI,YAAY;AAChB,UAAI;AAEJ,YAAM,UAAU,MAAM;AACpB,oBAAY;AACZ,0BAAA;AACA,YAAI,wBAAwB,QAAW;AACrC,wBAAc,mBAAmB;AACjC,gCAAsB;AAAA,QACxB;AAAA,MACF;AAKA,YAAM,8BAAc,IAAA;AAGpB,qBAAe,KAAK,MAAyB;AAC3C,YAAI,aAAa,KAAK,QAAQ,QAAS;AAEvC,cAAM,UAAU,KAAK,QAAQ;AAC7B,YAAI,SACF,YAAY,SAAY,QAAQ,IAAI,OAAO,IAAI;AACjD,YAAI,SACF,WAAW,WAAc,KAAK,UAAU,KAAK,IACzC,KAAK,SACL;AAEN,cAAM,QAAmC,WAAW,IAAI;AACxD,cAAM,UAAgD;AAAA,UACpD;AAAA,UACA;AAAA,QAAA;AAGF,YAAI,YAAoB,KAAK,SAAS,OAAO;AAC7C,YAAI,aAAa,GAAG;AAClB;AAAA,QACF;AACA,cAAM,eAAqC,CAAA;AAE3C,eAAO,MAAM;AACX,gBAAM,QAAQ,KAAK,IAAI,WAAW,GAAG;AACrC,cAAI;AACJ,cAAI;AACF,uBAAW,MAAM,OAAO,UAAU,KAAK;AAAA,cACrC,YAAY;AAAA,gBACV;AAAA,gBACA;AAAA,gBACA;AAAA,cAAA;AAAA,cAEF;AAAA,cACA;AAAA,YAAA,CACD;AAAA,UACH,SAAS,OAAO;AACd,gBAAI,aAAa,KAAK,QAAQ,QAAS;AACvC,kBAAM;AAAA,UACR;AACA,cAAI,aAAa,KAAK,QAAQ,QAAS;AAEvC,gBAAM,SAAS,SAAS,QAAQ;AAChC,cAAI,WAAW,GAAG;AAEhB;AAAA,UACF;AAEA,gBAAA;AAEA,mBAAS,IAAI,GAAG,IAAI,KAAK,IAAI,QAAQ,SAAS,GAAG,EAAE,GAAG;AACpD,kBAAM;AAAA,cACJ,MAAM;AAAA,cACN,OAAO,MAAM,SAAS,QAAQ,CAAC,CAAE;AAAA,YAAA,CAClC;AAAA,UACH;AAEA,gBAAM,UAAU,OAAO,KAAK,MAAM;AAClC,cAAI,YAAY,MAAM;AACpB,yBAAa,KAAK,OAAO;AAAA,UAC3B;AACA,cAAI,aAAa,KAAK,QAAQ,QAAS;AAEvC,uBAAa;AAGb,cAAI,SAAS,SAAS,aAAa,GAAG;AACpC,gBAAI,SAAS,QAAQ;AACnB,sBAAQ;AAAA,gBACN,OAAO,MAAM,SAAS,QAAQ,GAAG,EAAE,CAAE,CAAC;AAAA,gBACtC,SAAS;AAAA,cAAA;AAAA,YAEb;AACA;AAAA,UACF;AAGA,cAAI,WAAW,QAAW;AACxB,sBAAU;AAAA,UACZ,OAAO;AACL,qBAAS,SAAS;AAAA,UACpB;AAAA,QACF;AAEA,cAAM,QAAQ,IAAI,YAAY;AAAA,MAChC;AAGA,qBAAe,OAAO,QAA4C;AAChE,eAAO,MAAM;AACX,gBAAM,EAAE,MAAM,OAAO,UAAU,MAAM,OAAO,KAAA;AAE5C,cAAI,QAAQ,CAAC,OAAO;AAClB;AAAA,UACF;AAEA,gBAAA;AACA,cAAI;AACJ,cAAI,YAAY,OAAO;AACrB,oBAAQ,MAAM,MAAM,MAAiB;AACrC,kBAAM,EAAE,MAAM,UAAU,MAAA,CAAO;AAAA,UACjC,WAAW,YAAY,OAAO;AAC5B,oBAAQ,MAAM,MAAM,MAAiB;AACrC,kBAAM,EAAE,MAAM,UAAU,MAAA,CAAO;AAAA,UACjC,WAAW,YAAY,OAAO;AAC5B,oBAAQ,MAAM,MAAM,MAAiB;AACrC,kBAAM,EAAE,MAAM,UAAU,MAAA,CAAO;AAAA,UACjC,OAAO;AACL,oBAAQ,MAAM,UAAU,MAAM,KAAK,EAAE;AAAA,UACvC;AACA,eAAK,OAAA;AAEL,cAAI,OAAO;AACT,oBAAQ,SAAS,CAAC,SAA8B;AAC9C,oBAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,qBAAO,IAAI,OAAO,OAAO,KAAK,CAAC,GAAG,KAAK,KAAK;AAC5C,qBAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAEA,qBAAe,QAAQ;AACrB,YAAI;AACJ,YAAI;AACF,gBAAM,cAAc,MAAM,OAAO,UAAU,UAAU,GAAG;AACxD,cAAI,WAAW;AACb,kBAAM,YAAY,OAAA;AAClB;AAAA,UACF;AACA,gBAAM,mBAAmB,YAAY,UAAA;AACrC,mBAAS,cAAc;AAIvB,eAAK,OAAO,gBAAgB,EACzB,QAAQ,MAAM;AAMb,iBAAK,iBAAiB,OAAA,EAAS,MAAM,MAAM,MAAS;AACpD,6BAAiB,YAAA;AACjB,gBAAI,gBAAgB,iBAAkB,eAAc;AAAA,UACtD,CAAC,EACA,MAAM,CAAC,UAAmB;AACzB,gBAAI,CAAC,aAAa,WAAW,WAAW,WAAW;AACjD,wBAAU,KAAK;AAAA,YACjB,WAAW,CAAC,WAAW;AACrB,sBAAQ,MAAM,iCAAiC,KAAK;AAAA,YACtD;AAAA,UACF,CAAC;AAGH,cAAI,qBAAqB,SAAS;AAEhC,kBAAM,KAAK,CAAA,CAAE;AACb,gBAAI,UAAW;AACf,gCAAoB;AAAA,UACtB;AACA,cAAI,CAAC,aAAa,WAAW,WAAW,WAAW;AACjD,sBAAA;AAAA,UACF;AAAA,QACF,SAAS,OAAO;AAEd,cAAI,UAAW;AACf,4BAAA;AACA,cAAI,WAAW,WAAW,WAAW;AACnC,sBAAU,KAAK;AAAA,UACjB;AACA;AAAA,QACF;AAIA,YAAI,aAAa,CAAC,OAAQ;AAE1B,8BAAsB,YAAY,MAAM;AACtC,kBAAQ,SAAS,CAAC,SAAS;AACzB,kBAAM,MAAM,KAAK,IAAA;AACjB,gBAAI,aAAa;AAEjB,kBAAM,aAAa,MAAM,KAAK,KAAK,SAAS,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM;AAC/D,oBAAM,UAAU,MAAM,IAAI,MAAM;AAChC,2BAAa,cAAc;AAC3B,qBAAO,CAAC;AAAA,YACV,CAAC;AAED,gBAAI,YAAY;AACd,qBAAO,IAAI,IAAI,UAAU;AAAA,YAC3B;AACA,mBAAO;AAAA,UACT,CAAC;AAAA,QACH,GAAG,MAAM,GAAI;AAEb,cAAM,mBAAmB,MAAM;AAC7B,cAAI,wBAAwB,QAAW;AACrC,0BAAc,mBAAmB;AACjC,kCAAsB;AAAA,UACxB;AAAA,QACF;AAEA,aAAK,OAAO,OAAO,KAAK,kBAAkB,gBAAgB;AAAA,MAC5D;AAEA,WAAK,MAAA;AAGL,UAAI,qBAAqB,SAAS;AAChC,eAAO,EAAE,QAAA;AAAA,MACX;AAEA,aAAO;AAAA,QACL;AAAA,QACA,YAAY;AAAA,QACZ,iBAAiB,OACd;AAAA,UACC,UAAU;AAAA,QAAA;AAAA,MACZ;AAAA,IAEN;AAAA;AAAA,IAEA,iBAAiB,OACd;AAAA,MACC,UAAU;AAAA,MACV,kBAAkB;AAAA,IAAA;AAAA,EACpB;AAGJ,QAAM,UAAU;AAAA,IACd,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA,UAAU,OACR,WACoC;AACpC,YAAM,MAAM,MAAM,OAAO,UAAU;AAAA,QACjC,OAAO,YAAY,UAAU,IAAI,CAAC,OAAO;AACvC,gBAAM,EAAE,MAAM,SAAA,IAAa;AAC3B,cAAI,SAAS,UAAU;AACrB,kBAAM,IAAIC,OAAAA,wBAAwB,IAAI;AAAA,UACxC;AACA,iBAAO,UAAU,QAAQ;AAAA,QAC3B,CAAC;AAAA,MAAA;AAMH,YAAM,SAAS,IAAI,IAAI,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC;AAE1C,aAAO;AAAA,IACT;AAAA,IACA,UAAU,OAAO,WAAgD;AAC/D,YAAM,MAAqB,MAAM,QAAQ;AAAA,QACvC,OAAO,YAAY,UAAU,IAAI,OAAO,OAAO;AAC7C,gBAAM,EAAE,MAAM,SAAS,IAAA,IAAQ;AAC/B,cAAI,SAAS,UAAU;AACrB,kBAAM,IAAIC,OAAAA,wBAAwB,IAAI;AAAA,UACxC;AAEA,gBAAM,OAAO,UAAU,OAAO,KAAK,UAAU,OAAO,CAAC;AAErD,iBAAO,OAAO,GAAG;AAAA,QACnB,CAAC;AAAA,MAAA;AAMH,YAAM,SAAS,GAAG;AAAA,IACpB;AAAA,IACA,UAAU,OAAO,WAAgD;AAC/D,YAAM,MAAqB,MAAM,QAAQ;AAAA,QACvC,OAAO,YAAY,UAAU,IAAI,OAAO,OAAO;AAC7C,gBAAM,EAAE,MAAM,IAAA,IAAQ;AACtB,cAAI,SAAS,UAAU;AACrB,kBAAM,IAAIC,OAAAA,wBAAwB,IAAI;AAAA,UACxC;AAEA,gBAAM,OAAO,UAAU,OAAO,GAAG;AACjC,iBAAO,OAAO,GAAG;AAAA,QACnB,CAAC;AAAA,MAAA;AAMH,YAAM,SAAS,GAAG;AAAA,IACpB;AAAA,IACA,OAAO;AAAA,MACL,QAAQ;AAAA,IAAA;AAAA,EACV;AAGF,SAAOC,GAAAA;AAAAA,IACL;AAAA,IACA,MAAM,2BAA2B,MAAM;AAAA,EAAA;AAE3C;AAEA,SAAS,WAAW,MAAoD;AACtE,SAAO,KAAK,SACR,IAAI,CAAC,MAAqB;AAC1B,YAAQ,EAAE,WAAW,MAAA;AAAA,MACnB,KAAK,OAAO;AACV,cAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;AACjC,YAAI,EAAE,eAAe,aAAa,OAAO;AACvC,iBAAO,IAAI,KAAK;AAAA,QAClB;AACA,eAAO,IAAI,KAAK;AAAA,MAClB;AAAA,MACA,SAAS;AACP,gBAAQ;AAAA,UACN;AAAA,UACA,KAAK,UAAU,EAAE,UAAU;AAAA,QAAA;AAE7B,eAAO;AAAA,MACT;AAAA,IAAA;AAAA,EAEJ,CAAC,EACA,OAAO,CAAC,MAA0B,MAAM,MAAS;AACtD;AAEA,SAAS,eAAe,MAAqC;AAC3D,UAAQ,MAAA;AAAA,IACN,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EAAA;AAEb;AAEA,SAAS,aAKP,MACA,QACsC;AACtC,QAAM,QAAQ,KAAK;AACnB,MAAI,UAAU,QAAW;AACvB,WAAO;AAAA,EACT;AAEA,WAAS,eAAwB,QAAgB,OAAkB;AACjE,UAAM,OAAQ,OAAO,UAAkB,MAAM;AAC7C,QAAI,MAAM;AACR,aAAO,GAAG,KAAK,KAAK,CAAC;AAAA,IACvB;AAEA,QAAI,OAAO,UAAU,WAAW;AAC9B,aAAO,QAAQ,MAAM;AAAA,IACvB;AAEA,WAAO,GAAG,KAAK;AAAA,EACjB;AAEA,UAAQ,MAAM,MAAA;AAAA,IACZ,KAAK,QAAQ;AACX,YAAM,QAAQ,MAAM,KAAK,CAAC;AAC1B,YAAM,MAAM,MAAM,KAAK,CAAC;AAExB,YAAM,KAAK,eAAe,MAAM,IAAI;AACpC,UAAI,OAAO,QAAW;AACpB;AAAA,MACF;AAEA,UAAI,OAAO,SAAS,SAAS,KAAK,SAAS,OAAO;AAChD,cAAM,SAAS,MAAM,KAAK,GAAG,CAAC;AAC9B,YAAI,QAAQ;AACV,gBAAM,IAAI;AAAA,YACR;AAAA,cACE,QAAQ,MAAM,KAAK,GAAG,CAAC,KAAK;AAAA,cAC5B;AAAA,cACA,OAAO,eAAe,QAAQ,IAAI,KAAK;AAAA,YAAA;AAAA,UACzC;AAGF,iBAAO;AAAA,QACT;AAAA,MACF;AACA;AAAA,IACF;AAAA,EAGE;AAGJ,UAAQ,KAAK,6CAA6C,KAAK,KAAK;AAEpE,SAAO;AACT;;"}
|
|
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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"}
|
package/dist/esm/trailbase.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Store } from "@tanstack/store";
|
|
2
|
-
import { withCollectionConfigFactory } from "@tanstack/db";
|
|
2
|
+
import { withCollectionConfigFactory, LoadSubsetOperationAbortedError } from "@tanstack/db";
|
|
3
3
|
import { ExpectedDeleteTypeError, ExpectedUpdateTypeError, ExpectedInsertTypeError, TimeoutWaitingForIdsError } from "./errors.js";
|
|
4
4
|
function convert(conversions, input) {
|
|
5
5
|
const c = conversions;
|
|
@@ -69,7 +69,8 @@ function trailBaseCollectionOptions(config) {
|
|
|
69
69
|
};
|
|
70
70
|
const cursors = /* @__PURE__ */ new Map();
|
|
71
71
|
async function load(opts) {
|
|
72
|
-
if (cancelled
|
|
72
|
+
if (cancelled) return;
|
|
73
|
+
if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError();
|
|
73
74
|
const lastKey = opts.cursor?.lastKey;
|
|
74
75
|
let cursor = lastKey !== void 0 ? cursors.get(lastKey) : void 0;
|
|
75
76
|
let offset = cursor === void 0 && (opts.offset ?? 0) > 0 ? opts.offset : void 0;
|
|
@@ -97,10 +98,12 @@ function trailBaseCollectionOptions(config) {
|
|
|
97
98
|
filters
|
|
98
99
|
});
|
|
99
100
|
} catch (error) {
|
|
100
|
-
if (cancelled
|
|
101
|
+
if (cancelled) return;
|
|
102
|
+
if (opts.signal?.aborted) break;
|
|
101
103
|
throw error;
|
|
102
104
|
}
|
|
103
|
-
if (cancelled
|
|
105
|
+
if (cancelled) return;
|
|
106
|
+
if (opts.signal?.aborted) break;
|
|
104
107
|
const length = response.records.length;
|
|
105
108
|
if (length === 0) {
|
|
106
109
|
break;
|
|
@@ -116,7 +119,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
116
119
|
if (applied !== true) {
|
|
117
120
|
appliedPages.push(applied);
|
|
118
121
|
}
|
|
119
|
-
if (cancelled || opts.signal?.aborted)
|
|
122
|
+
if (cancelled || opts.signal?.aborted) break;
|
|
120
123
|
remaining -= length;
|
|
121
124
|
if (length < limit || remaining <= 0) {
|
|
122
125
|
if (response.cursor) {
|
|
@@ -134,6 +137,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
134
137
|
}
|
|
135
138
|
}
|
|
136
139
|
await Promise.all(appliedPages);
|
|
140
|
+
if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError();
|
|
137
141
|
}
|
|
138
142
|
async function listen(reader) {
|
|
139
143
|
while (true) {
|
|
@@ -244,8 +248,14 @@ function trailBaseCollectionOptions(config) {
|
|
|
244
248
|
fullSyncComplete: fullSyncCompleted
|
|
245
249
|
})
|
|
246
250
|
};
|
|
251
|
+
const {
|
|
252
|
+
recordApi: _recordApi,
|
|
253
|
+
parse: _parse,
|
|
254
|
+
serialize: _serialize,
|
|
255
|
+
...collectionConfig
|
|
256
|
+
} = config;
|
|
247
257
|
const options = {
|
|
248
|
-
...
|
|
258
|
+
...collectionConfig,
|
|
249
259
|
sync,
|
|
250
260
|
getKey,
|
|
251
261
|
onInsert: async (params) => {
|
|
@@ -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 { withCollectionConfigFactory } 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 [K in keyof InputType as InputType[K] extends OutputType[K]\n ? K\n : never]?: Conversion<InputType[K], OutputType[K]>\n}\n\ntype RequiredConversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = {\n // Excludes all keys that do not strictly require a conversation.\n [K in keyof InputType as InputType[K] extends OutputType[K]\n ? never\n : K]: Conversion<InputType[K], OutputType[K]>\n}\n\ntype Conversions<\n InputType extends ShapeOf<OutputType>,\n OutputType extends ShapeOf<InputType>,\n> = OptionalConversions<InputType, OutputType> &\n RequiredConversions<InputType, OutputType>\n\nfunction convert<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: InputType,\n): OutputType {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\nfunction convertPartial<\n InputType extends ShapeOf<OutputType> & Record<string, unknown>,\n OutputType extends ShapeOf<InputType>,\n>(\n conversions: Conversions<InputType, OutputType>,\n input: Partial<InputType>,\n): Partial<OutputType> {\n const c = conversions as Record<string, Conversion<InputType, OutputType>>\n\n return Object.fromEntries(\n Object.keys(input).map((k: string) => {\n const value = input[k]\n return [k, c[k]?.(value as any) ?? value]\n }),\n ) as OutputType\n}\n\nexport type TrailBaseSyncMode = SyncMode\n\n/**\n * Configuration interface for Trailbase Collection\n */\nexport interface TrailBaseCollectionConfig<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n> extends Omit<\n BaseCollectionConfig<TItem, TKey>,\n `onInsert` | `onUpdate` | `onDelete` | `syncMode`\n> {\n /**\n * Record API name\n */\n recordApi: RecordApi<TRecord>\n\n /**\n * The mode of sync to use for the collection.\n * @default `eager`\n */\n syncMode?: TrailBaseSyncMode\n\n parse: Conversions<TRecord, TItem>\n serialize: Conversions<TItem, TRecord>\n}\n\nexport type AwaitTxIdFn = (txId: string, timeout?: number) => Promise<boolean>\n\nexport interface TrailBaseCollectionUtils extends UtilsRecord {\n cancel: () => void\n}\n\nexport function trailBaseCollectionOptions<\n TItem extends ShapeOf<TRecord>,\n TRecord extends ShapeOf<TItem> = TItem,\n TKey extends string | number = string | number,\n>(\n config: TrailBaseCollectionConfig<TItem, TRecord, TKey>,\n): CollectionConfig<TItem, TKey, never, TrailBaseCollectionUtils> & {\n utils: TrailBaseCollectionUtils\n} {\n const getKey = config.getKey\n\n const parse = (record: TRecord) =>\n convert<TRecord, TItem>(config.parse, record)\n const serialUpd = (item: Partial<TItem>) =>\n convertPartial<TItem, TRecord>(config.serialize, item)\n const serialIns = (item: TItem) =>\n convert<TItem, TRecord>(config.serialize, item)\n\n const seenIds = new Store(new Map<string, number>())\n\n const internalSyncMode = config.syncMode ?? `eager`\n let fullSyncCompleted = false\n\n const awaitIds = (\n ids: Array<string>,\n timeout: number = 120 * 1000,\n ): Promise<void> => {\n const completed = (value: Map<string, number>) =>\n ids.every((id) => value.has(id))\n if (completed(seenIds.state)) {\n return Promise.resolve()\n }\n\n return new Promise<void>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n sub.unsubscribe()\n reject(new TimeoutWaitingForIdsError(ids.toString()))\n }, timeout)\n\n const sub = seenIds.subscribe((value) => {\n if (completed(value)) {\n clearTimeout(timeoutId)\n sub.unsubscribe()\n resolve()\n }\n })\n })\n }\n\n let eventReader: ReadableStreamDefaultReader<Event> | undefined\n const cancelEventReader = () => {\n if (eventReader) {\n // 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 async function load(opts: LoadSubsetOptions) {\n if (cancelled || opts.signal?.aborted) return\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 || opts.signal?.aborted) return\n throw error\n }\n if (cancelled || opts.signal?.aborted) return\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 const applied = commit(opts.signal)\n if (applied !== true) {\n appliedPages.push(applied)\n }\n if (cancelled || opts.signal?.aborted) return\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 }\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 options = {\n ...config,\n sync,\n getKey,\n onInsert: async (\n params: InsertMutationFnParams<TItem, TKey>,\n ): Promise<Array<number | string>> => {\n const ids = await config.recordApi.createBulk(\n params.transaction.mutations.map((tx) => {\n const { type, modified } = tx\n if (type !== `insert`) {\n throw new ExpectedInsertTypeError(type)\n }\n return serialIns(modified)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly added to the local\n // DB by the subscription.\n await awaitIds(ids.map((id) => String(id)))\n\n return ids\n },\n onUpdate: async (params: UpdateMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, changes, key } = tx\n if (type !== `update`) {\n throw new ExpectedUpdateTypeError(type)\n }\n\n await config.recordApi.update(key, serialUpd(changes))\n\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n onDelete: async (params: DeleteMutationFnParams<TItem, TKey>) => {\n const ids: Array<string> = await Promise.all(\n params.transaction.mutations.map(async (tx) => {\n const { type, key } = tx\n if (type !== `delete`) {\n throw new ExpectedDeleteTypeError(type)\n }\n\n await config.recordApi.delete(key)\n return String(key)\n }),\n )\n\n // The optimistic mutation overlay is removed on return, so at this point\n // we have to ensure that the new record was properly updated in the local\n // DB by the subscription.\n await awaitIds(ids)\n },\n utils: {\n cancel: cancelEventReader,\n },\n }\n\n 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\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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"}
|
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.115",
|
|
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.12.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"typescript": ">=4.7"
|
package/src/trailbase.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
2
2
|
import { Store } from '@tanstack/store'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
LoadSubsetOperationAbortedError,
|
|
5
|
+
withCollectionConfigFactory,
|
|
6
|
+
} from '@tanstack/db'
|
|
4
7
|
import {
|
|
5
8
|
ExpectedDeleteTypeError,
|
|
6
9
|
ExpectedInsertTypeError,
|
|
@@ -30,9 +33,9 @@ type OptionalConversions<
|
|
|
30
33
|
OutputType extends ShapeOf<InputType>,
|
|
31
34
|
> = {
|
|
32
35
|
// Excludes all keys that require a conversation.
|
|
33
|
-
[
|
|
34
|
-
? K
|
|
35
|
-
|
|
36
|
+
[
|
|
37
|
+
K in keyof InputType as InputType[K] extends OutputType[K] ? K : never
|
|
38
|
+
]?: Conversion<InputType[K], OutputType[K]>
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
type RequiredConversions<
|
|
@@ -40,9 +43,9 @@ type RequiredConversions<
|
|
|
40
43
|
OutputType extends ShapeOf<InputType>,
|
|
41
44
|
> = {
|
|
42
45
|
// Excludes all keys that do not strictly require a conversation.
|
|
43
|
-
[
|
|
44
|
-
? never
|
|
45
|
-
|
|
46
|
+
[
|
|
47
|
+
K in keyof InputType as InputType[K] extends OutputType[K] ? never : K
|
|
48
|
+
]: Conversion<InputType[K], OutputType[K]>
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
type Conversions<
|
|
@@ -201,8 +204,11 @@ export function trailBaseCollectionOptions<
|
|
|
201
204
|
const cursors = new Map<string | number, string>()
|
|
202
205
|
|
|
203
206
|
// Load (more) data.
|
|
207
|
+
// A caller that aborts sees `AbortError`. Pages a commit accepted still
|
|
208
|
+
// apply, and the load waits until they are visible; cleanup is quiet.
|
|
204
209
|
async function load(opts: LoadSubsetOptions) {
|
|
205
|
-
if (cancelled
|
|
210
|
+
if (cancelled) return
|
|
211
|
+
if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()
|
|
206
212
|
|
|
207
213
|
const lastKey = opts.cursor?.lastKey
|
|
208
214
|
let cursor: string | undefined =
|
|
@@ -238,10 +244,13 @@ export function trailBaseCollectionOptions<
|
|
|
238
244
|
filters,
|
|
239
245
|
})
|
|
240
246
|
} catch (error) {
|
|
241
|
-
if (cancelled
|
|
247
|
+
if (cancelled) return
|
|
248
|
+
if (opts.signal?.aborted) break
|
|
242
249
|
throw error
|
|
243
250
|
}
|
|
244
|
-
if (cancelled
|
|
251
|
+
if (cancelled) return
|
|
252
|
+
// Check the signal before commit to discard a stale page.
|
|
253
|
+
if (opts.signal?.aborted) break
|
|
245
254
|
|
|
246
255
|
const length = response.records.length
|
|
247
256
|
if (length === 0) {
|
|
@@ -258,11 +267,12 @@ export function trailBaseCollectionOptions<
|
|
|
258
267
|
})
|
|
259
268
|
}
|
|
260
269
|
|
|
270
|
+
// An accepted page always applies.
|
|
261
271
|
const applied = commit(opts.signal)
|
|
262
272
|
if (applied !== true) {
|
|
263
273
|
appliedPages.push(applied)
|
|
264
274
|
}
|
|
265
|
-
if (cancelled || opts.signal?.aborted)
|
|
275
|
+
if (cancelled || opts.signal?.aborted) break
|
|
266
276
|
|
|
267
277
|
remaining -= length
|
|
268
278
|
|
|
@@ -286,6 +296,9 @@ export function trailBaseCollectionOptions<
|
|
|
286
296
|
}
|
|
287
297
|
|
|
288
298
|
await Promise.all(appliedPages)
|
|
299
|
+
// An accepted page applies, but a caller that aborted still sees
|
|
300
|
+
// `AbortError`.
|
|
301
|
+
if (opts.signal?.aborted) throw new LoadSubsetOperationAbortedError()
|
|
289
302
|
}
|
|
290
303
|
|
|
291
304
|
// Afterwards subscribe.
|
|
@@ -431,8 +444,15 @@ export function trailBaseCollectionOptions<
|
|
|
431
444
|
}) as const,
|
|
432
445
|
}
|
|
433
446
|
|
|
447
|
+
const {
|
|
448
|
+
recordApi: _recordApi,
|
|
449
|
+
parse: _parse,
|
|
450
|
+
serialize: _serialize,
|
|
451
|
+
...collectionConfig
|
|
452
|
+
} = config
|
|
453
|
+
|
|
434
454
|
const options = {
|
|
435
|
-
...
|
|
455
|
+
...collectionConfig,
|
|
436
456
|
sync,
|
|
437
457
|
getKey,
|
|
438
458
|
onInsert: async (
|