@tanstack/trailbase-db-collection 0.1.99 → 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.
@@ -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
- const eventStream = await config.recordApi.subscribe(`*`);
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
- } catch (e) {
181
+ if (!cancelled && collection.status === `loading`) {
182
+ markReady();
183
+ }
184
+ } catch (error) {
175
185
  cancelEventReader();
176
- throw e;
177
- } finally {
178
- if (!cancelled) markReady();
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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"}
@@ -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
- const eventStream = await config.recordApi.subscribe(`*`);
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
- } catch (e) {
179
+ if (!cancelled && collection.status === `loading`) {
180
+ markReady();
181
+ }
182
+ } catch (error) {
173
183
  cancelEventReader();
174
- throw e;
175
- } finally {
176
- if (!cancelled) markReady();
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.99",
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.0"
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
- const eventStream = await config.recordApi.subscribe(`*`)
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
- } catch (e) {
338
+ if (!cancelled && collection.status === `loading`) {
339
+ markReady()
340
+ }
341
+ } catch (error) {
332
342
  cancelEventReader()
333
- throw e
334
- } finally {
335
- // Mark ready both if everything went well or if there's an error to
336
- // avoid blocking apps waiting for `.preload()` to finish.
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`) {