@tanstack/trailbase-db-collection 0.1.100 → 0.1.101
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 +25 -14
- package/dist/cjs/trailbase.cjs.map +1 -1
- package/dist/esm/trailbase.js +25 -14
- package/dist/esm/trailbase.js.map +1 -1
- package/package.json +2 -2
- package/src/trailbase.ts +29 -20
package/dist/cjs/trailbase.cjs
CHANGED
|
@@ -58,7 +58,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
58
58
|
};
|
|
59
59
|
const sync = {
|
|
60
60
|
sync: (params) => {
|
|
61
|
-
const { begin, write, commit, markReady } = params;
|
|
61
|
+
const { begin, write, commit, markReady, markError, collection } = params;
|
|
62
62
|
let cancelled = false;
|
|
63
63
|
let periodicCleanupTask;
|
|
64
64
|
const cleanup = () => {
|
|
@@ -158,26 +158,37 @@ function trailBaseCollectionOptions(config) {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
async function start() {
|
|
161
|
-
|
|
162
|
-
if (cancelled) {
|
|
163
|
-
await eventStream.cancel();
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
const reader = eventReader = eventStream.getReader();
|
|
167
|
-
listen(reader);
|
|
161
|
+
let reader;
|
|
168
162
|
try {
|
|
163
|
+
const eventStream = await config.recordApi.subscribe(`*`);
|
|
164
|
+
if (cancelled) {
|
|
165
|
+
await eventStream.cancel();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
reader = eventReader = eventStream.getReader();
|
|
169
|
+
void listen(reader).catch((error) => {
|
|
170
|
+
if (!cancelled && collection.status === `loading`) {
|
|
171
|
+
markError(error);
|
|
172
|
+
} else if (!cancelled) {
|
|
173
|
+
console.error(`TrailBase subscription failed`, error);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
169
176
|
if (internalSyncMode === `eager`) {
|
|
170
177
|
await load({});
|
|
171
178
|
if (cancelled) return;
|
|
172
179
|
fullSyncCompleted = true;
|
|
173
180
|
}
|
|
174
|
-
|
|
181
|
+
if (!cancelled && collection.status === `loading`) {
|
|
182
|
+
markReady();
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
175
185
|
cancelEventReader();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
if (!cancelled && collection.status === `loading`) {
|
|
187
|
+
markError(error);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
179
190
|
}
|
|
180
|
-
if (cancelled) return;
|
|
191
|
+
if (cancelled || !reader) return;
|
|
181
192
|
periodicCleanupTask = setInterval(() => {
|
|
182
193
|
seenIds.setState((curr) => {
|
|
183
194
|
const now = Date.now();
|
|
@@ -200,7 +211,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
200
211
|
}
|
|
201
212
|
});
|
|
202
213
|
}
|
|
203
|
-
start();
|
|
214
|
+
void start();
|
|
204
215
|
if (internalSyncMode === `eager`) {
|
|
205
216
|
return { cleanup };
|
|
206
217
|
}
|
|
@@ -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 eventReader.cancel()\n eventReader.releaseLock()\n eventReader = undefined\n }\n }\n\n type SyncParams = Parameters<SyncConfig<TItem, TKey>[`sync`]>[0]\n const sync = {\n sync: (params: SyncParams) => {\n const { begin, write, commit, markReady } = params\n 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) 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 (opts.offset ?? 0) > 0 ? opts.offset : undefined\n\n const order: Array<string> | undefined = buildOrder(opts)\n const filters: Array<FilterOrComposite> | undefined = buildFilters(\n opts,\n config,\n )\n\n let remaining: number = opts.limit ?? Number.MAX_VALUE\n if (remaining <= 0) {\n return\n }\n\n while (true) {\n const limit = Math.min(remaining, 256)\n const response = await config.recordApi.list({\n pagination: {\n limit,\n offset,\n cursor,\n },\n order,\n filters,\n })\n if (cancelled) 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 commit()\n\n remaining -= length\n\n // Drained or read enough.\n if (length < limit || remaining <= 0) {\n if (response.cursor) {\n cursors.set(\n getKey(parse(response.records.at(-1)!)),\n response.cursor,\n )\n }\n break\n }\n\n // Update params for next iteration.\n if (offset !== undefined) {\n offset += length\n } else {\n cursor = response.cursor\n }\n }\n }\n\n // Afterwards subscribe.\n async function listen(reader: ReadableStreamDefaultReader<Event>) {\n while (true) {\n const { done, value: event } = await reader.read()\n\n if (done || !event) {\n reader.releaseLock()\n eventReader = undefined\n return\n }\n\n begin()\n let value: TItem | undefined\n if (`Insert` in event) {\n value = parse(event.Insert as TRecord)\n write({ type: `insert`, value })\n } else if (`Delete` in event) {\n value = parse(event.Delete as TRecord)\n write({ type: `delete`, value })\n } else if (`Update` in event) {\n value = parse(event.Update as TRecord)\n write({ type: `update`, value })\n } else {\n console.error(`Error: ${event.Error}`)\n }\n commit()\n\n if (value) {\n seenIds.setState((curr: Map<string, number>) => {\n const newIds = new Map(curr)\n newIds.set(String(getKey(value)), Date.now())\n return newIds\n })\n }\n }\n }\n\n async function start() {\n const eventStream = await config.recordApi.subscribe(`*`)\n if (cancelled) {\n await eventStream.cancel()\n return\n }\n const reader = (eventReader = eventStream.getReader())\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n listen(reader)\n\n try {\n // Eager mode: perform initial fetch to populate everything\n if (internalSyncMode === `eager`) {\n // Load everything on initial load.\n await load({})\n if (cancelled) return\n fullSyncCompleted = true\n }\n } catch (e) {\n cancelEventReader()\n throw e\n } finally {\n // Mark ready both if everything went well or if there's an error to\n // avoid blocking apps waiting for `.preload()` to finish.\n if (!cancelled) markReady()\n }\n\n // Lastly, start a periodic cleanup task that will be removed when the\n // reader closes.\n if (cancelled) 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 reader.closed.finally(() => {\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n })\n }\n\n 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":"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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 { 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 eventReader.cancel()\n eventReader.releaseLock()\n eventReader = undefined\n }\n }\n\n type SyncParams = Parameters<SyncConfig<TItem, TKey>[`sync`]>[0]\n const sync = {\n sync: (params: SyncParams) => {\n const { begin, write, commit, markReady, 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) 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 (opts.offset ?? 0) > 0 ? opts.offset : undefined\n\n const order: Array<string> | undefined = buildOrder(opts)\n const filters: Array<FilterOrComposite> | undefined = buildFilters(\n opts,\n config,\n )\n\n let remaining: number = opts.limit ?? Number.MAX_VALUE\n if (remaining <= 0) {\n return\n }\n\n while (true) {\n const limit = Math.min(remaining, 256)\n const response = await config.recordApi.list({\n pagination: {\n limit,\n offset,\n cursor,\n },\n order,\n filters,\n })\n if (cancelled) 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 commit()\n\n remaining -= length\n\n // Drained or read enough.\n if (length < limit || remaining <= 0) {\n if (response.cursor) {\n cursors.set(\n getKey(parse(response.records.at(-1)!)),\n response.cursor,\n )\n }\n break\n }\n\n // Update params for next iteration.\n if (offset !== undefined) {\n offset += length\n } else {\n cursor = response.cursor\n }\n }\n }\n\n // Afterwards subscribe.\n async function listen(reader: ReadableStreamDefaultReader<Event>) {\n while (true) {\n const { done, value: event } = await reader.read()\n\n if (done || !event) {\n reader.releaseLock()\n eventReader = undefined\n return\n }\n\n begin()\n let value: TItem | undefined\n if (`Insert` in event) {\n value = parse(event.Insert as TRecord)\n write({ type: `insert`, value })\n } else if (`Delete` in event) {\n value = parse(event.Delete as TRecord)\n write({ type: `delete`, value })\n } else if (`Update` in event) {\n value = parse(event.Update as TRecord)\n write({ type: `update`, value })\n } else {\n console.error(`Error: ${event.Error}`)\n }\n commit()\n\n if (value) {\n seenIds.setState((curr: Map<string, number>) => {\n const newIds = new Map(curr)\n newIds.set(String(getKey(value)), Date.now())\n return newIds\n })\n }\n }\n }\n\n async function start() {\n 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 reader = eventReader = eventStream.getReader()\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n void listen(reader).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 cancelEventReader()\n if (!cancelled && 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 reader.closed.finally(() => {\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n })\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":"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"}
|
package/dist/esm/trailbase.js
CHANGED
|
@@ -56,7 +56,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
56
56
|
};
|
|
57
57
|
const sync = {
|
|
58
58
|
sync: (params) => {
|
|
59
|
-
const { begin, write, commit, markReady } = params;
|
|
59
|
+
const { begin, write, commit, markReady, markError, collection } = params;
|
|
60
60
|
let cancelled = false;
|
|
61
61
|
let periodicCleanupTask;
|
|
62
62
|
const cleanup = () => {
|
|
@@ -156,26 +156,37 @@ function trailBaseCollectionOptions(config) {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
async function start() {
|
|
159
|
-
|
|
160
|
-
if (cancelled) {
|
|
161
|
-
await eventStream.cancel();
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const reader = eventReader = eventStream.getReader();
|
|
165
|
-
listen(reader);
|
|
159
|
+
let reader;
|
|
166
160
|
try {
|
|
161
|
+
const eventStream = await config.recordApi.subscribe(`*`);
|
|
162
|
+
if (cancelled) {
|
|
163
|
+
await eventStream.cancel();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
reader = eventReader = eventStream.getReader();
|
|
167
|
+
void listen(reader).catch((error) => {
|
|
168
|
+
if (!cancelled && collection.status === `loading`) {
|
|
169
|
+
markError(error);
|
|
170
|
+
} else if (!cancelled) {
|
|
171
|
+
console.error(`TrailBase subscription failed`, error);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
167
174
|
if (internalSyncMode === `eager`) {
|
|
168
175
|
await load({});
|
|
169
176
|
if (cancelled) return;
|
|
170
177
|
fullSyncCompleted = true;
|
|
171
178
|
}
|
|
172
|
-
|
|
179
|
+
if (!cancelled && collection.status === `loading`) {
|
|
180
|
+
markReady();
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
173
183
|
cancelEventReader();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
if (!cancelled && collection.status === `loading`) {
|
|
185
|
+
markError(error);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
177
188
|
}
|
|
178
|
-
if (cancelled) return;
|
|
189
|
+
if (cancelled || !reader) return;
|
|
179
190
|
periodicCleanupTask = setInterval(() => {
|
|
180
191
|
seenIds.setState((curr) => {
|
|
181
192
|
const now = Date.now();
|
|
@@ -198,7 +209,7 @@ function trailBaseCollectionOptions(config) {
|
|
|
198
209
|
}
|
|
199
210
|
});
|
|
200
211
|
}
|
|
201
|
-
start();
|
|
212
|
+
void start();
|
|
202
213
|
if (internalSyncMode === `eager`) {
|
|
203
214
|
return { cleanup };
|
|
204
215
|
}
|
|
@@ -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 eventReader.cancel()\n eventReader.releaseLock()\n eventReader = undefined\n }\n }\n\n type SyncParams = Parameters<SyncConfig<TItem, TKey>[`sync`]>[0]\n const sync = {\n sync: (params: SyncParams) => {\n const { begin, write, commit, markReady } = params\n 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) 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 (opts.offset ?? 0) > 0 ? opts.offset : undefined\n\n const order: Array<string> | undefined = buildOrder(opts)\n const filters: Array<FilterOrComposite> | undefined = buildFilters(\n opts,\n config,\n )\n\n let remaining: number = opts.limit ?? Number.MAX_VALUE\n if (remaining <= 0) {\n return\n }\n\n while (true) {\n const limit = Math.min(remaining, 256)\n const response = await config.recordApi.list({\n pagination: {\n limit,\n offset,\n cursor,\n },\n order,\n filters,\n })\n if (cancelled) 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 commit()\n\n remaining -= length\n\n // Drained or read enough.\n if (length < limit || remaining <= 0) {\n if (response.cursor) {\n cursors.set(\n getKey(parse(response.records.at(-1)!)),\n response.cursor,\n )\n }\n break\n }\n\n // Update params for next iteration.\n if (offset !== undefined) {\n offset += length\n } else {\n cursor = response.cursor\n }\n }\n }\n\n // Afterwards subscribe.\n async function listen(reader: ReadableStreamDefaultReader<Event>) {\n while (true) {\n const { done, value: event } = await reader.read()\n\n if (done || !event) {\n reader.releaseLock()\n eventReader = undefined\n return\n }\n\n begin()\n let value: TItem | undefined\n if (`Insert` in event) {\n value = parse(event.Insert as TRecord)\n write({ type: `insert`, value })\n } else if (`Delete` in event) {\n value = parse(event.Delete as TRecord)\n write({ type: `delete`, value })\n } else if (`Update` in event) {\n value = parse(event.Update as TRecord)\n write({ type: `update`, value })\n } else {\n console.error(`Error: ${event.Error}`)\n }\n commit()\n\n if (value) {\n seenIds.setState((curr: Map<string, number>) => {\n const newIds = new Map(curr)\n newIds.set(String(getKey(value)), Date.now())\n return newIds\n })\n }\n }\n }\n\n async function start() {\n const eventStream = await config.recordApi.subscribe(`*`)\n if (cancelled) {\n await eventStream.cancel()\n return\n }\n const reader = (eventReader = eventStream.getReader())\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n listen(reader)\n\n try {\n // Eager mode: perform initial fetch to populate everything\n if (internalSyncMode === `eager`) {\n // Load everything on initial load.\n await load({})\n if (cancelled) return\n fullSyncCompleted = true\n }\n } catch (e) {\n cancelEventReader()\n throw e\n } finally {\n // Mark ready both if everything went well or if there's an error to\n // avoid blocking apps waiting for `.preload()` to finish.\n if (!cancelled) markReady()\n }\n\n // Lastly, start a periodic cleanup task that will be removed when the\n // reader closes.\n if (cancelled) 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 reader.closed.finally(() => {\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n })\n }\n\n 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 { 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 eventReader.cancel()\n eventReader.releaseLock()\n eventReader = undefined\n }\n }\n\n type SyncParams = Parameters<SyncConfig<TItem, TKey>[`sync`]>[0]\n const sync = {\n sync: (params: SyncParams) => {\n const { begin, write, commit, markReady, 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) 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 (opts.offset ?? 0) > 0 ? opts.offset : undefined\n\n const order: Array<string> | undefined = buildOrder(opts)\n const filters: Array<FilterOrComposite> | undefined = buildFilters(\n opts,\n config,\n )\n\n let remaining: number = opts.limit ?? Number.MAX_VALUE\n if (remaining <= 0) {\n return\n }\n\n while (true) {\n const limit = Math.min(remaining, 256)\n const response = await config.recordApi.list({\n pagination: {\n limit,\n offset,\n cursor,\n },\n order,\n filters,\n })\n if (cancelled) 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 commit()\n\n remaining -= length\n\n // Drained or read enough.\n if (length < limit || remaining <= 0) {\n if (response.cursor) {\n cursors.set(\n getKey(parse(response.records.at(-1)!)),\n response.cursor,\n )\n }\n break\n }\n\n // Update params for next iteration.\n if (offset !== undefined) {\n offset += length\n } else {\n cursor = response.cursor\n }\n }\n }\n\n // Afterwards subscribe.\n async function listen(reader: ReadableStreamDefaultReader<Event>) {\n while (true) {\n const { done, value: event } = await reader.read()\n\n if (done || !event) {\n reader.releaseLock()\n eventReader = undefined\n return\n }\n\n begin()\n let value: TItem | undefined\n if (`Insert` in event) {\n value = parse(event.Insert as TRecord)\n write({ type: `insert`, value })\n } else if (`Delete` in event) {\n value = parse(event.Delete as TRecord)\n write({ type: `delete`, value })\n } else if (`Update` in event) {\n value = parse(event.Update as TRecord)\n write({ type: `update`, value })\n } else {\n console.error(`Error: ${event.Error}`)\n }\n commit()\n\n if (value) {\n seenIds.setState((curr: Map<string, number>) => {\n const newIds = new Map(curr)\n newIds.set(String(getKey(value)), Date.now())\n return newIds\n })\n }\n }\n }\n\n async function start() {\n 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 reader = eventReader = eventStream.getReader()\n\n // Start listening for subscriptions first. Otherwise, we'd risk a gap\n // between the initial fetch and starting to listen.\n void listen(reader).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 cancelEventReader()\n if (!cancelled && 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 reader.closed.finally(() => {\n if (periodicCleanupTask !== undefined) {\n clearInterval(periodicCleanupTask)\n periodicCleanupTask = undefined\n }\n })\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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"}
|
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.101",
|
|
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.8.
|
|
46
|
+
"@tanstack/db": "0.8.2"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"typescript": ">=4.7"
|
package/src/trailbase.ts
CHANGED
|
@@ -180,7 +180,7 @@ export function trailBaseCollectionOptions<
|
|
|
180
180
|
type SyncParams = Parameters<SyncConfig<TItem, TKey>[`sync`]>[0]
|
|
181
181
|
const sync = {
|
|
182
182
|
sync: (params: SyncParams) => {
|
|
183
|
-
const { begin, write, commit, markReady } = params
|
|
183
|
+
const { begin, write, commit, markReady, markError, collection } = params
|
|
184
184
|
let cancelled = false
|
|
185
185
|
let periodicCleanupTask: ReturnType<typeof setInterval> | undefined
|
|
186
186
|
|
|
@@ -309,18 +309,25 @@ export function trailBaseCollectionOptions<
|
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
async function start() {
|
|
312
|
-
|
|
313
|
-
if (cancelled) {
|
|
314
|
-
await eventStream.cancel()
|
|
315
|
-
return
|
|
316
|
-
}
|
|
317
|
-
const reader = (eventReader = eventStream.getReader())
|
|
318
|
-
|
|
319
|
-
// Start listening for subscriptions first. Otherwise, we'd risk a gap
|
|
320
|
-
// between the initial fetch and starting to listen.
|
|
321
|
-
listen(reader)
|
|
322
|
-
|
|
312
|
+
let reader: ReadableStreamDefaultReader<Event> | undefined
|
|
323
313
|
try {
|
|
314
|
+
const eventStream = await config.recordApi.subscribe(`*`)
|
|
315
|
+
if (cancelled) {
|
|
316
|
+
await eventStream.cancel()
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
reader = eventReader = eventStream.getReader()
|
|
320
|
+
|
|
321
|
+
// Start listening for subscriptions first. Otherwise, we'd risk a gap
|
|
322
|
+
// between the initial fetch and starting to listen.
|
|
323
|
+
void listen(reader).catch((error: unknown) => {
|
|
324
|
+
if (!cancelled && collection.status === `loading`) {
|
|
325
|
+
markError(error)
|
|
326
|
+
} else if (!cancelled) {
|
|
327
|
+
console.error(`TrailBase subscription failed`, error)
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
|
|
324
331
|
// Eager mode: perform initial fetch to populate everything
|
|
325
332
|
if (internalSyncMode === `eager`) {
|
|
326
333
|
// Load everything on initial load.
|
|
@@ -328,18 +335,20 @@ export function trailBaseCollectionOptions<
|
|
|
328
335
|
if (cancelled) return
|
|
329
336
|
fullSyncCompleted = true
|
|
330
337
|
}
|
|
331
|
-
|
|
338
|
+
if (!cancelled && collection.status === `loading`) {
|
|
339
|
+
markReady()
|
|
340
|
+
}
|
|
341
|
+
} catch (error) {
|
|
332
342
|
cancelEventReader()
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (!cancelled) markReady()
|
|
343
|
+
if (!cancelled && collection.status === `loading`) {
|
|
344
|
+
markError(error)
|
|
345
|
+
}
|
|
346
|
+
return
|
|
338
347
|
}
|
|
339
348
|
|
|
340
349
|
// Lastly, start a periodic cleanup task that will be removed when the
|
|
341
350
|
// reader closes.
|
|
342
|
-
if (cancelled) return
|
|
351
|
+
if (cancelled || !reader) return
|
|
343
352
|
|
|
344
353
|
periodicCleanupTask = setInterval(() => {
|
|
345
354
|
seenIds.setState((curr) => {
|
|
@@ -367,7 +376,7 @@ export function trailBaseCollectionOptions<
|
|
|
367
376
|
})
|
|
368
377
|
}
|
|
369
378
|
|
|
370
|
-
start()
|
|
379
|
+
void start()
|
|
371
380
|
|
|
372
381
|
// Eager mode doesn't need subset loading
|
|
373
382
|
if (internalSyncMode === `eager`) {
|