@tanstack/db
Version:
A reactive client store for building super fast apps on sync
268 lines (267 loc) • 8.85 kB
JavaScript
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