UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

268 lines (267 loc) 8.85 kB
import { D2, output, MultiSet } from "@tanstack/db-ivm"; import { createCollection } from "../collection.js"; import { compileQuery } from "./compiler/index.js"; import { buildQuery, getQueryIR } from "./builder/index.js"; import { convertToBasicExpression } from "./compiler/expressions.js"; let liveQueryCollectionCounter = 0; function liveQueryCollectionOptions(config) { const id = config.id || `live-query-${++liveQueryCollectionCounter}`; const query = typeof config.query === `function` ? buildQuery(config.query) : getQueryIR(config.query); const resultKeys = /* @__PURE__ */ new WeakMap(); const orderByIndices = /* @__PURE__ */ new WeakMap(); const compare = query.orderBy && query.orderBy.length > 0 ? (val1, val2) => { const index1 = orderByIndices.get(val1); const index2 = orderByIndices.get(val2); if (index1 && index2) { if (index1 < index2) { return -1; } else if (index1 > index2) { return 1; } else { return 0; } } return 0; } : void 0; const collections = extractCollectionsFromQuery(query); const allCollectionsReady = () => { return Object.values(collections).every( (collection) => collection.status === `ready` || collection.status === `initialCommit` ); }; let graphCache; let inputsCache; let pipelineCache; let collectionWhereClausesCache; const compileBasePipeline = () => { graphCache = new D2(); inputsCache = Object.fromEntries( Object.entries(collections).map(([key]) => [ key, graphCache.newInput() ]) ); ({ pipeline: pipelineCache, collectionWhereClauses: collectionWhereClausesCache } = compileQuery(query, inputsCache)); }; const maybeCompileBasePipeline = () => { if (!graphCache || !inputsCache || !pipelineCache) { compileBasePipeline(); } return { graph: graphCache, inputs: inputsCache, pipeline: pipelineCache }; }; compileBasePipeline(); const sync = { rowUpdateMode: `full`, sync: ({ begin, write, commit, markReady, collection: theCollection }) => { const { graph, inputs, pipeline } = maybeCompileBasePipeline(); let messagesCount = 0; pipeline.pipe( output((data) => { const messages = data.getInner(); messagesCount += messages.length; begin(); messages.reduce((acc, [[key, tupleData], multiplicity]) => { const [value, orderByIndex] = tupleData; const changes = acc.get(key) || { deletes: 0, inserts: 0, value, orderByIndex }; if (multiplicity < 0) { changes.deletes += Math.abs(multiplicity); } else if (multiplicity > 0) { changes.inserts += multiplicity; changes.value = value; changes.orderByIndex = orderByIndex; } acc.set(key, changes); return acc; }, /* @__PURE__ */ new Map()).forEach((changes, rawKey) => { const { deletes, inserts, value, orderByIndex } = changes; resultKeys.set(value, rawKey); if (orderByIndex !== void 0) { orderByIndices.set(value, orderByIndex); } if (inserts && deletes === 0) { write({ value, type: `insert` }); } else if ( // Insert & update(s) (updates are a delete & insert) inserts > deletes || // Just update(s) but the item is already in the collection (so // was inserted previously). inserts === deletes && theCollection.has(rawKey) ) { write({ value, type: `update` }); } else if (deletes > 0) { write({ value, type: `delete` }); } else { throw new Error( `This should never happen ${JSON.stringify(changes)}` ); } }); commit(); }) ); graph.finalize(); const maybeRunGraph = () => { if (allCollectionsReady()) { graph.run(); if (messagesCount === 0) { begin(); commit(); } markReady(); } }; const unsubscribeCallbacks = /* @__PURE__ */ new Set(); Object.entries(collections).forEach(([collectionId, collection]) => { const input = inputs[collectionId]; const collectionAlias = findCollectionAlias(collectionId, query); const whereClause = collectionAlias && collectionWhereClausesCache ? collectionWhereClausesCache.get(collectionAlias) : void 0; if (whereClause) { const whereExpression = convertToBasicExpression( whereClause, collectionAlias ); if (whereExpression) { const subscription = collection.subscribeChanges( (changes) => { sendChangesToInput(input, changes, collection.config.getKey); maybeRunGraph(); }, { includeInitialState: true, whereExpression } ); unsubscribeCallbacks.add(subscription); } else { throw new Error( `Failed to convert WHERE clause to collection filter for collection '${collectionId}'. This indicates a bug in the query optimization logic.` ); } } else { const subscription = collection.subscribeChanges( (changes) => { sendChangesToInput(input, changes, collection.config.getKey); maybeRunGraph(); }, { includeInitialState: true } ); unsubscribeCallbacks.add(subscription); } }); maybeRunGraph(); return () => { unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe()); }; } }; return { id, getKey: config.getKey || ((item) => resultKeys.get(item)), sync, compare, gcTime: config.gcTime || 5e3, // 5 seconds by default for live queries schema: config.schema, onInsert: config.onInsert, onUpdate: config.onUpdate, onDelete: config.onDelete, startSync: config.startSync }; } function createLiveQueryCollection(configOrQuery) { if (typeof configOrQuery === `function`) { const config = { query: configOrQuery }; const options = liveQueryCollectionOptions(config); return bridgeToCreateCollection(options); } else { const config = configOrQuery; const options = liveQueryCollectionOptions(config); return bridgeToCreateCollection({ ...options, utils: config.utils }); } } function bridgeToCreateCollection(options) { return createCollection(options); } function sendChangesToInput(input, changes, getKey) { const multiSetArray = []; for (const change of changes) { const key = getKey(change.value); if (change.type === `insert`) { multiSetArray.push([[key, change.value], 1]); } else if (change.type === `update`) { multiSetArray.push([[key, change.previousValue], -1]); multiSetArray.push([[key, change.value], 1]); } else { multiSetArray.push([[key, change.value], -1]); } } input.sendData(new MultiSet(multiSetArray)); } function extractCollectionsFromQuery(query) { const collections = {}; function extractFromSource(source) { if (source.type === `collectionRef`) { collections[source.collection.id] = source.collection; } else if (source.type === `queryRef`) { extractFromQuery(source.query); } } function extractFromQuery(q) { if (q.from) { extractFromSource(q.from); } if (q.join && Array.isArray(q.join)) { for (const joinClause of q.join) { if (joinClause.from) { extractFromSource(joinClause.from); } } } } extractFromQuery(query); return collections; } function findCollectionAlias(collectionId, query) { var _a, _b, _c, _d; if (((_a = query.from) == null ? void 0 : _a.type) === `collectionRef` && ((_b = query.from.collection) == null ? void 0 : _b.id) === collectionId) { return query.from.alias; } if (query.join) { for (const joinClause of query.join) { if (((_c = joinClause.from) == null ? void 0 : _c.type) === `collectionRef` && ((_d = joinClause.from.collection) == null ? void 0 : _d.id) === collectionId) { return joinClause.from.alias; } } } return void 0; } export { createLiveQueryCollection, liveQueryCollectionOptions }; //# sourceMappingURL=live-query-collection.js.map