@tanstack/db
Version:
A reactive client store for building super fast apps on sync
380 lines (379 loc) • 13.4 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const dbIvm = require("@tanstack/db-ivm");
const getOrCreate = require("../../utils/get-or-create.cjs");
const index = require("../../collection/index.cjs");
const errorMessage = require("../../error-message.cjs");
const materializedPipeline = require("./materialized-pipeline.cjs");
const NO_FACADE_CHANGES = {
prepare: () => {
},
publish: () => {
},
rollback: () => {
}
};
class BucketFacadeAdapter {
constructor(parentId, compilations, onMessages, resolveRoot) {
this.parentId = parentId;
this.f3 = compilations;
this.b2 = resolveRoot;
this.pending = /* @__PURE__ */ new Map();
this.cx = /* @__PURE__ */ new Map();
this.activeBuckets = /* @__PURE__ */ new Map();
this.entries = /* @__PURE__ */ new Map();
this.bH = /* @__PURE__ */ new Map();
this.ds = /* @__PURE__ */ new WeakMap();
this.cleanedUp = false;
this.D = (leaf) => {
if (!isBucketFacadeRef(leaf)) return leaf;
const { edgeId, bucketKey } = leaf[materializedPipeline.BUCKET_FACADE_REF];
return this.entries.get(edgeId)?.get(bucketKey)?.collection ?? this.bH.get(edgeId)?.get(bucketKey)?.collection ?? this.fr(edgeId, bucketKey).collection;
};
for (const compilation of compilations) {
compilation.rows.pipe(
dbIvm.output((data) => {
const messages = data.getInner();
onMessages(messages.length);
for (const [[bucketKey, row], multiplicity] of messages) {
this.fv(compilation.edgeId, bucketKey, row, multiplicity);
}
})
);
compilation.activeBuckets.pipe(
dbIvm.output((data) => {
const messages = data.getInner();
onMessages(messages.length);
for (const [[bucketKey], multiplicity] of messages) {
this.fc(compilation.edgeId, bucketKey, multiplicity);
}
})
);
}
}
hasPendingChanges() {
return this.pending.size > 0 || this.cx.size > 0;
}
flush() {
if (!this.hasPendingChanges()) return NO_FACADE_CHANGES;
const snapshot = this.snapshot();
const publications = [];
const newBaselines = [];
try {
for (const compilation of this.f3) {
const activity = this.cx.get(compilation.edgeId);
const active = this.fq(compilation.edgeId);
for (const [bucketKey, multiplicity] of activity ?? []) {
if (multiplicity > 0 && !active.has(bucketKey)) {
active.add(bucketKey);
newBaselines.push(this.fr(compilation.edgeId, bucketKey));
}
}
const buckets = this.pending.get(compilation.edgeId);
for (const [bucketKey, changes] of buckets ?? []) {
const existing = this.entries.get(compilation.edgeId)?.get(bucketKey);
if (!active.has(bucketKey) && !existing) continue;
const entry = this.fr(compilation.edgeId, bucketKey);
const sync = entry.sync;
if (!sync || changes.size === 0) continue;
for (const change of changes.values()) {
this.fF(entry, change, compilation.resolve);
}
if (!snapshot.rows.has(entry)) {
snapshot.rows.set(
entry,
[...entry.collection._state.acceptedSyncedEntries()].map(
([key, value]) => ({
key,
value,
order: entry.order.get(value)
})
)
);
snapshot.written.set(entry, /* @__PURE__ */ new Set());
publications.push(entry.collection._deferPublication());
}
for (const {
value: [key]
} of changes.values())
snapshot.written.get(entry).add(key);
sync.begin();
for (const change of changes.values()) {
this.fM(
entry,
sync,
change,
compilation.hasOrderBy,
compilation.resolve
);
}
sync.commit();
}
for (const [bucketKey, multiplicity] of activity ?? []) {
if (multiplicity >= 0) continue;
active.delete(bucketKey);
this.fO(compilation.edgeId, bucketKey);
}
}
} catch (error) {
this.abort(snapshot, publications);
throw error;
}
const pending = this.pending;
const pendingActivity = this.cx;
this.pending = /* @__PURE__ */ new Map();
this.cx = /* @__PURE__ */ new Map();
let closed = false;
let prepared = false;
const prepare = () => {
if (closed || prepared) return;
prepared = true;
for (const entry of newBaselines) entry.sync?.markReady();
};
return {
prepare,
publish: () => {
if (closed) return;
prepare();
closed = true;
let publicationError;
for (const publication of publications) {
try {
publication.publish();
} catch (error) {
publicationError ??= { error };
}
}
this.bH.clear();
if (publicationError) throw publicationError.error;
},
rollback: () => {
if (closed || this.cleanedUp) return;
closed = true;
this.abort(snapshot, publications);
if (this.hasPendingChanges()) {
throw new Error(
errorMessage.devBuild() && process.env.NODE_ENV !== `production` ? `Bucket facade received graph output between a flush and its rollback` : errorMessage.codedMessage(235)
);
}
this.pending = pending;
this.cx = pendingActivity;
}
};
}
resolve(value) {
return this.dn(value, this.b2);
}
cleanup() {
this.cleanedUp = true;
for (const entries of [this.entries, this.bH]) {
for (const byBucket of entries.values())
for (const entry of byBucket.values()) void entry.dispose();
entries.clear();
}
this.pending.clear();
this.cx.clear();
this.activeBuckets.clear();
}
fv(edgeId, bucketKey, row, multiplicity) {
const buckets = getOrCreate.getOrCreate(this.pending, edgeId, () => /* @__PURE__ */ new Map());
const rows = getOrCreate.getOrCreate(buckets, bucketKey, () => /* @__PURE__ */ new Map());
const [publicKey] = row;
const key = dbIvm.serializeValue(publicKey);
const change = rows.get(key) ?? {
deletes: 0,
inserts: 0,
value: row
};
if (multiplicity < 0) {
change.deletes += -multiplicity;
} else if (multiplicity > 0) {
change.inserts += multiplicity;
change.value = row;
}
rows.set(key, change);
}
snapshot() {
return {
activeBuckets: new Map(
[...this.activeBuckets].map(([edgeId, buckets]) => [
edgeId,
new Set(buckets)
])
),
entries: new Map(
[...this.entries].map(([edgeId, byBucket]) => [
edgeId,
new Map(byBucket)
])
),
rows: /* @__PURE__ */ new Map(),
written: /* @__PURE__ */ new Map()
};
}
/** Restore the facades, then discard their events even if restore throws. */
abort(snapshot, publications) {
try {
this.restore(snapshot);
} finally {
this.bH.clear();
for (const publication of publications) publication.discard();
}
}
restore(snapshot) {
const previousEntries = new Set(
[...snapshot.entries.values()].flatMap((byBucket) => [
...byBucket.values()
])
);
const currentEntries = new Set(
[...this.entries.values()].flatMap((byBucket) => [...byBucket.values()])
);
for (const [entry, rows] of snapshot.rows) {
if (!previousEntries.has(entry)) continue;
const sync = entry.sync;
if (!sync) continue;
const before = new Map(rows.map((row) => [row.key, row]));
if (!entry.collection._state.hasOpenSyncTransaction()) sync.begin();
for (const key of snapshot.written.get(entry)) {
const row = before.get(key);
if (!row) {
if (entry.collection._state.getAcceptedSyncedRow(key) !== void 0)
sync.write({ type: `delete`, key });
continue;
}
if (row.order !== void 0) entry.order.set(row.value, row.order);
sync.write({
type: entry.collection._state.getAcceptedSyncedRow(key) === void 0 ? `insert` : `update`,
value: row.value
});
}
sync.commit();
}
this.entries.clear();
for (const [edgeId, byBucket] of snapshot.entries)
this.entries.set(edgeId, byBucket);
this.activeBuckets.clear();
for (const [edgeId, buckets] of snapshot.activeBuckets)
this.activeBuckets.set(edgeId, buckets);
this.ds = /* @__PURE__ */ new WeakMap();
for (const entry of currentEntries) {
if (!previousEntries.has(entry)) void entry.dispose();
}
}
fc(edgeId, bucketKey, multiplicity) {
const activity = getOrCreate.getOrCreate(this.cx, edgeId, () => /* @__PURE__ */ new Map());
activity.set(bucketKey, (activity.get(bucketKey) ?? 0) + multiplicity);
}
fq(edgeId) {
return getOrCreate.getOrCreate(this.activeBuckets, edgeId, () => /* @__PURE__ */ new Set());
}
fO(edgeId, bucketKey) {
const byBucket = this.entries.get(edgeId);
const entry = byBucket?.get(bucketKey);
if (!entry) return;
if (!entry.collection._state.acceptedSyncedEntries().next().done) {
throw new Error(
errorMessage.devBuild() && process.env.NODE_ENV !== `production` ? `Bucket facade retired with rows the graph did not retract` : errorMessage.codedMessage(240)
);
}
byBucket.delete(bucketKey);
if (byBucket.size === 0) this.entries.delete(edgeId);
const retired = getOrCreate.getOrCreate(this.bH, edgeId, () => /* @__PURE__ */ new Map());
retired.set(bucketKey, entry);
}
fr(edgeId, bucketKey) {
const byBucket = getOrCreate.getOrCreate(this.entries, edgeId, () => /* @__PURE__ */ new Map());
const existing = byBucket.get(bucketKey);
if (existing) return existing;
const order = /* @__PURE__ */ new WeakMap();
let sync;
const collection = index.createCollection({
id: `__bucket-facade:${this.parentId}:${edgeId}:${bucketKey}`,
getKey: (row) => {
const key = row?.$key;
if (typeof key !== `string` && typeof key !== `number`) {
throw new Error(
errorMessage.devBuild() && process.env.NODE_ENV !== `production` ? `Bucket facade row has no public key` : errorMessage.codedMessage(146)
);
}
return key;
},
compare: (left, right) => {
const leftOrder = order.get(left);
const rightOrder = order.get(right);
if (leftOrder === rightOrder) return 0;
if (leftOrder === void 0) return 1;
if (rightOrder === void 0) return -1;
return leftOrder < rightOrder ? -1 : 1;
},
sync: {
rowUpdateMode: `full`,
sync: (methods) => {
sync = methods;
return () => {
sync = void 0;
};
}
},
startSync: true,
gcTime: 0
});
const dispose = () => collection._lifecycle.cleanup();
collection.cleanup = () => Promise.reject(
new Error(
errorMessage.devBuild() && process.env.NODE_ENV !== `production` ? `A live query's child collection cannot be cleaned up directly. Clean up the live query instead.` : errorMessage.codedMessage(241)
)
);
const entry = {
collection,
dispose,
get sync() {
return sync;
},
order
};
byBucket.set(bucketKey, entry);
return entry;
}
fM(entry, sync, change, hasOrderBy, resolve) {
const [publicKey, value, nextOrder] = change.value;
const key = publicKey;
if (change.deletes > change.inserts) {
sync.write({ type: `delete`, key });
return;
}
const accepted = entry.collection._state.getAcceptedSyncedRow(key);
const present = accepted !== void 0;
if (change.inserts === change.deletes && !present) return;
const orderChanged = present && entry.order.get(accepted) !== nextOrder;
const row = { ...this.dn(value, resolve), $key: key };
if (nextOrder !== void 0) entry.order.set(row, nextOrder);
sync.write({ type: present ? `update` : `insert`, value: row });
if (hasOrderBy && orderChanged) sync.collection._markLayoutChange();
}
/** Resolve and validate every public key before opening a sync transaction. */
fF(entry, change, resolve) {
const [publicKey, value] = change.value;
this.dn(value, resolve);
entry.collection.getKeyFromItem({ $key: publicKey });
}
dn(value, resolve = this.b2) {
if (value === null || typeof value !== `object`)
return resolve(value, this.D);
const cache = getOrCreate.getOrCreate(
this.ds,
resolve,
() => /* @__PURE__ */ new WeakMap()
);
if (cache.has(value)) return cache.get(value);
const result = resolve(value, this.D);
cache.set(value, result);
return result;
}
}
function isBucketFacadeRef(value) {
return value !== null && typeof value === `object` && materializedPipeline.BUCKET_FACADE_REF in value;
}
exports.BucketFacadeAdapter = BucketFacadeAdapter;
//# sourceMappingURL=bucket-facade-adapter.cjs.map