UNPKG

@tanstack/db-collections

Version:

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

194 lines (193 loc) 5.98 kB
import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client"; import { Store } from "@tanstack/store"; import DebugModule from "debug"; const debug = DebugModule.debug(`ts/db:electric`); function isUpToDateMessage(message) { return isControlMessage(message) && message.headers.control === `up-to-date`; } function hasTxids(message) { return `txids` in message.headers && Array.isArray(message.headers.txids); } function electricCollectionOptions(config) { const seenTxids = new Store(/* @__PURE__ */ new Set([])); const sync = createElectricSync( config.shapeOptions, { seenTxids } ); const awaitTxId = async (txId, timeout = 3e4) => { debug(`awaitTxId called with txid %d`, txId); if (typeof txId !== `number`) { throw new TypeError( `Expected number in awaitTxId, received ${typeof txId}` ); } const hasTxid = seenTxids.state.has(txId); if (hasTxid) return true; return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { unsubscribe(); reject(new Error(`Timeout waiting for txId: ${txId}`)); }, timeout); const unsubscribe = seenTxids.subscribe(() => { if (seenTxids.state.has(txId)) { debug(`awaitTxId found match for txid %o`, txId); clearTimeout(timeoutId); unsubscribe(); resolve(true); } }); }); }; const wrappedOnInsert = config.onInsert ? async (params) => { const handlerResult = await config.onInsert(params) ?? {}; const txid = handlerResult.txid; if (!txid) { throw new Error( `Electric collection onInsert handler must return a txid or array of txids` ); } if (Array.isArray(txid)) { await Promise.all(txid.map((id) => awaitTxId(id))); } else { await awaitTxId(txid); } return handlerResult; } : void 0; const wrappedOnUpdate = config.onUpdate ? async (params) => { const handlerResult = await config.onUpdate(params) ?? {}; const txid = handlerResult.txid; if (!txid) { throw new Error( `Electric collection onUpdate handler must return a txid or array of txids` ); } if (Array.isArray(txid)) { await Promise.all(txid.map((id) => awaitTxId(id))); } else { await awaitTxId(txid); } return handlerResult; } : void 0; const wrappedOnDelete = config.onDelete ? async (params) => { const handlerResult = await config.onDelete(params); if (!handlerResult.txid) { throw new Error( `Electric collection onDelete handler must return a txid or array of txids` ); } if (Array.isArray(handlerResult.txid)) { await Promise.all(handlerResult.txid.map((id) => awaitTxId(id))); } else { await awaitTxId(handlerResult.txid); } return handlerResult; } : void 0; const { shapeOptions: _shapeOptions, onInsert: _onInsert, onUpdate: _onUpdate, onDelete: _onDelete, ...restConfig } = config; return { ...restConfig, sync, onInsert: wrappedOnInsert, onUpdate: wrappedOnUpdate, onDelete: wrappedOnDelete, utils: { awaitTxId } }; } function createElectricSync(shapeOptions, options) { const { seenTxids } = options; const relationSchema = new Store(void 0); const getSyncMetadata = () => { var _a; const schema = relationSchema.state || `public`; return { relation: ((_a = shapeOptions.params) == null ? void 0 : _a.table) ? [schema, shapeOptions.params.table] : void 0 }; }; const abortController = new AbortController(); if (shapeOptions.signal) { shapeOptions.signal.addEventListener(`abort`, () => { abortController.abort(); }); if (shapeOptions.signal.aborted) { abortController.abort(); } } let unsubscribeStream; return { sync: (params) => { const { begin, write, commit } = params; const stream = new ShapeStream({ ...shapeOptions, signal: abortController.signal }); let transactionStarted = false; const newTxids = /* @__PURE__ */ new Set(); unsubscribeStream = stream.subscribe((messages) => { var _a; let hasUpToDate = false; for (const message of messages) { if (hasTxids(message)) { (_a = message.headers.txids) == null ? void 0 : _a.forEach((txid) => newTxids.add(txid)); } if (isChangeMessage(message)) { const schema = message.headers.schema; if (schema && typeof schema === `string`) { relationSchema.setState(() => schema); } if (!transactionStarted) { begin(); transactionStarted = true; } write({ type: message.headers.operation, value: message.value, // Include the primary key and relation info in the metadata metadata: { ...message.headers } }); } else if (isUpToDateMessage(message)) { hasUpToDate = true; } } if (hasUpToDate) { if (transactionStarted) { commit(); transactionStarted = false; } else { begin(); commit(); } seenTxids.setState((currentTxids) => { const clonedSeen = new Set(currentTxids); if (newTxids.size > 0) { debug(`new txids synced from pg %O`, Array.from(newTxids)); } newTxids.forEach((txid) => clonedSeen.add(txid)); newTxids.clear(); return clonedSeen; }); } }); return () => { unsubscribeStream(); abortController.abort(); }; }, // Expose the getSyncMetadata function getSyncMetadata }; } export { electricCollectionOptions }; //# sourceMappingURL=electric.js.map