UNPKG

@tanstack/db-collections

Version:

A collection for (aspirationally) every way of loading your data

1 lines 12 kB
{"version":3,"file":"electric.cjs","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport type {\n CollectionConfig,\n MutationFnParams,\n SyncConfig,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type {\n ControlMessage,\n Message,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\n/**\n * Configuration interface for Electric collection options\n */\nexport interface ElectricCollectionConfig<T extends Row<unknown>> {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions\n\n /**\n * All standard Collection configuration properties\n */\n id?: string\n schema?: CollectionConfig<T>[`schema`]\n getKey: CollectionConfig<T>[`getKey`]\n sync?: CollectionConfig<T>[`sync`]\n\n /**\n * Optional asynchronous handler function called before an insert operation\n * Must return an object containing a txid string\n * @param params Object containing transaction and mutation information\n * @returns Promise resolving to an object with txid\n */\n onInsert?: (\n params: MutationFnParams<T>\n ) => Promise<{ txid: string } | undefined>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * Must return an object containing a txid string\n * @param params Object containing transaction and mutation information\n * @returns Promise resolving to an object with txid\n */\n onUpdate?: (\n params: MutationFnParams<T>\n ) => Promise<{ txid: string } | undefined>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * Must return an object containing a txid string\n * @param params Object containing transaction and mutation information\n * @returns Promise resolving to an object with txid\n */\n onDelete?: (\n params: MutationFnParams<T>\n ) => Promise<{ txid: string } | undefined>\n}\n\nfunction isUpToDateMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n\n// Check if a message contains txids in its headers\nfunction hasTxids<T extends Row<unknown> = Row>(\n message: Message<T>\n): message is Message<T> & { headers: { txids?: Array<number> } } {\n return (\n `headers` in message &&\n `txids` in message.headers &&\n Array.isArray(message.headers.txids)\n )\n}\n\n/**\n * Type for the awaitTxId utility function\n */\nexport type AwaitTxIdFn = (txId: string, timeout?: number) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n}\n\n/**\n * Creates Electric collection options for use with a standard Collection\n *\n * @param config - Configuration options for the Electric collection\n * @returns Collection options with utilities\n */\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T>\n) {\n const seenTxids = new Store<Set<string>>(new Set([`${Math.random()}`]))\n const sync = createElectricSync<T>(config.shapeOptions, {\n seenTxids,\n })\n\n /**\n * Wait for a specific transaction ID to be synced\n * @param txId The transaction ID to wait for as a string\n * @param timeout Optional timeout in milliseconds (defaults to 30000ms)\n * @returns Promise that resolves when the txId is synced\n */\n const awaitTxId: AwaitTxIdFn = async (\n txId: string,\n timeout = 30000\n ): Promise<boolean> => {\n const hasTxid = seenTxids.state.has(txId)\n if (hasTxid) return true\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribe()\n reject(new Error(`Timeout waiting for txId: ${txId}`))\n }, timeout)\n\n const unsubscribe = seenTxids.subscribe(() => {\n if (seenTxids.state.has(txId)) {\n clearTimeout(timeoutId)\n unsubscribe()\n resolve(true)\n }\n })\n })\n }\n\n // Create wrapper handlers for direct persistence operations that handle txid awaiting\n const wrappedOnInsert = config.onInsert\n ? async (params: MutationFnParams<T>) => {\n const handlerResult = (await config.onInsert!(params)) ?? {}\n const txid = (handlerResult as { txid?: string }).txid\n\n if (!txid) {\n throw new Error(\n `Electric collection onInsert handler must return a txid`\n )\n }\n\n await awaitTxId(txid)\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (params: MutationFnParams<T>) => {\n const handlerResult = await config.onUpdate!(params)\n const txid = (handlerResult as { txid?: string }).txid\n\n if (!txid) {\n throw new Error(\n `Electric collection onUpdate handler must return a txid`\n )\n }\n\n await awaitTxId(txid)\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = config.onDelete\n ? async (params: MutationFnParams<T>) => {\n const handlerResult = await config.onDelete!(params)\n const txid = (handlerResult as { txid?: string }).txid\n\n if (!txid) {\n throw new Error(\n `Electric collection onDelete handler must return a txid`\n )\n }\n\n await awaitTxId(txid)\n return handlerResult\n }\n : undefined\n\n // Extract standard Collection config properties\n const {\n shapeOptions: _shapeOptions,\n onInsert: _onInsert,\n onUpdate: _onUpdate,\n onDelete: _onDelete,\n ...restConfig\n } = config\n\n return {\n ...restConfig,\n sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n awaitTxId,\n },\n }\n}\n\n/**\n * Internal function to create ElectricSQL sync configuration\n */\nfunction createElectricSync<T extends Row<unknown>>(\n shapeOptions: ShapeStreamOptions,\n options: {\n seenTxids: Store<Set<string>>\n }\n): SyncConfig<T> {\n const { seenTxids } = options\n\n // Store for the relation schema information\n const relationSchema = new Store<string | undefined>(undefined)\n\n /**\n * Get the sync metadata for insert operations\n * @returns Record containing relation information\n */\n const getSyncMetadata = (): Record<string, unknown> => {\n // Use the stored schema if available, otherwise default to 'public'\n const schema = relationSchema.state || `public`\n\n return {\n relation: shapeOptions.params?.table\n ? [schema, shapeOptions.params.table]\n : undefined,\n }\n }\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit } = params\n const stream = new ShapeStream(shapeOptions)\n let transactionStarted = false\n let newTxids = new Set<string>()\n\n stream.subscribe((messages: Array<Message<Row>>) => {\n let hasUpToDate = false\n\n for (const message of messages) {\n // Check for txids in the message and add them to our store\n if (hasTxids(message) && message.headers.txids) {\n message.headers.txids.forEach((txid) => newTxids.add(String(txid)))\n }\n\n // Check if the message contains schema information\n if (isChangeMessage(message) && message.headers.schema) {\n // Store the schema for future use if it's a valid string\n if (typeof message.headers.schema === `string`) {\n const schema: string = message.headers.schema\n relationSchema.setState(() => schema)\n }\n }\n\n if (isChangeMessage(message)) {\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n const value = message.value as unknown as T\n\n // Include the primary key and relation info in the metadata\n const enhancedMetadata = {\n ...message.headers,\n }\n\n write({\n type: message.headers.operation,\n value,\n metadata: enhancedMetadata,\n })\n } else if (isUpToDateMessage(message)) {\n hasUpToDate = true\n }\n }\n\n if (hasUpToDate && transactionStarted) {\n commit()\n seenTxids.setState((currentTxids) => {\n const clonedSeen = new Set(currentTxids)\n newTxids.forEach((txid) => clonedSeen.add(String(txid)))\n\n newTxids = new Set()\n return clonedSeen\n })\n transactionStarted = false\n }\n })\n },\n // Expose the getSyncMetadata function\n getSyncMetadata,\n }\n}\n"],"names":["isControlMessage","Store","ShapeStream","isChangeMessage"],"mappings":"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"}