@tanstack/optimistic
Version:
Core optimistic updates library
163 lines (162 loc) • 4.84 kB
JavaScript
import { D2, MultiSet, output, MessageType } from "@electric-sql/d2ts";
import { batch, Effect } from "@tanstack/store";
import { Collection } from "../collection.js";
import { compileQueryPipeline } from "./pipeline-compiler.js";
function compileQuery(queryBuilder) {
return new CompiledQuery(queryBuilder);
}
class CompiledQuery {
constructor(queryBuilder) {
this.state = `compiled`;
this.version = 0;
const query = queryBuilder._query;
const collections = query.collections;
if (!collections) {
throw new Error(`No collections provided`);
}
this.inputCollections = collections;
const graph = new D2({ initialFrontier: this.version });
const inputs = Object.fromEntries(
Object.entries(collections).map(([key]) => [key, graph.newInput()])
);
const sync = ({ begin, write, commit }) => {
compileQueryPipeline(
query,
inputs
).pipe(
output(({ type, data }) => {
if (type === MessageType.DATA) {
begin();
data.collection.getInner().reduce((acc, [[key, value], multiplicity]) => {
const changes = acc.get(key) || {
deletes: 0,
inserts: 0,
value
};
if (multiplicity < 0) {
changes.deletes += Math.abs(multiplicity);
} else if (multiplicity > 0) {
changes.inserts += multiplicity;
changes.value = value;
}
acc.set(key, changes);
return acc;
}, /* @__PURE__ */ new Map()).forEach((changes, rawKey) => {
const key = rawKey.toString();
const { deletes, inserts, value } = changes;
if (inserts && !deletes) {
write({
key,
value,
type: `insert`
});
} else if (inserts >= deletes) {
write({
key,
value,
type: `update`
});
} else if (deletes > 0) {
write({
key,
value,
type: `delete`
});
}
});
commit();
}
})
);
graph.finalize();
};
this.graph = graph;
this.inputs = inputs;
this.resultCollection = new Collection({
id: crypto.randomUUID(),
// TODO: remove when we don't require any more
sync: {
sync
}
});
}
get results() {
return this.resultCollection;
}
sendChangesToInput(inputKey, changes) {
const input = this.inputs[inputKey];
const multiSetArray = [];
for (const change of changes) {
if (change.type === `insert`) {
multiSetArray.push([change.value, 1]);
} else if (change.type === `update`) {
multiSetArray.push([change.previousValue, -1]);
multiSetArray.push([change.value, 1]);
} else {
multiSetArray.push([change.value, -1]);
}
}
input.sendData(this.version, new MultiSet(multiSetArray));
}
sendFrontierToInput(inputKey) {
const input = this.inputs[inputKey];
input.sendFrontier(this.version);
}
sendFrontierToAllInputs() {
Object.entries(this.inputs).forEach(([key]) => {
this.sendFrontierToInput(key);
});
}
incrementVersion() {
this.version++;
}
runGraph() {
this.graph.run();
}
start() {
if (this.state === `running`) {
throw new Error(`Query is already running`);
} else if (this.state === `stopped`) {
throw new Error(`Query is stopped`);
}
batch(() => {
Object.entries(this.inputCollections).forEach(([key, collection]) => {
this.sendChangesToInput(key, collection.currentStateAsChanges());
});
this.incrementVersion();
this.sendFrontierToAllInputs();
this.runGraph();
});
const changeEffect = new Effect({
fn: () => {
batch(() => {
Object.entries(this.inputCollections).forEach(([key, collection]) => {
this.sendChangesToInput(key, collection.derivedChanges.state);
});
this.incrementVersion();
this.sendFrontierToAllInputs();
this.runGraph();
});
},
deps: Object.values(this.inputCollections).map(
(collection) => collection.derivedChanges
)
});
this.unsubscribeEffect = changeEffect.mount();
this.state = `running`;
return () => {
this.stop();
};
}
stop() {
var _a;
(_a = this.unsubscribeEffect) == null ? void 0 : _a.call(this);
this.unsubscribeEffect = void 0;
this.state = `stopped`;
}
}
export {
CompiledQuery,
compileQuery
};
//# sourceMappingURL=compiled-query.js.map