@tanstack/db
Version:
A reactive client store for building super fast apps on sync
384 lines (383 loc) • 13.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const dbIvm = require("@tanstack/db-ivm");
const index = require("../../collection/index.cjs");
const index$1 = require("../compiler/index.cjs");
const materializedPipeline = require("./materialized-pipeline.cjs");
class BucketFacadeAdapter {
constructor(parentId, compilations, onMessages) {
this.parentId = parentId;
this.compilations = compilations;
this.pending = /* @__PURE__ */ new Map();
this.pendingActivity = /* @__PURE__ */ new Map();
this.activeBuckets = /* @__PURE__ */ new Map();
this.entries = /* @__PURE__ */ new Map();
this.retiredEntries = /* @__PURE__ */ new Map();
this.resolvedValues = /* @__PURE__ */ new WeakMap();
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.accumulate(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.accumulateActivity(compilation.edgeId, bucketKey, multiplicity);
}
})
);
}
}
hasPendingChanges() {
return this.pending.size > 0 || this.pendingActivity.size > 0;
}
flush() {
const snapshot = this.snapshot();
const deferredEntries = /* @__PURE__ */ new Set();
const publications = [];
const deferPublication = (entry) => {
if (deferredEntries.has(entry)) return;
deferredEntries.add(entry);
publications.push(entry.collection._deferPublication());
};
try {
for (const compilation of this.compilations) {
const activity = this.pendingActivity.get(compilation.edgeId);
const active = this.getActiveBuckets(compilation.edgeId);
const newBaselines = [];
for (const [bucketKey, multiplicity] of activity ?? []) {
if (multiplicity > 0 && !active.has(bucketKey)) {
active.add(bucketKey);
newBaselines.push(this.getEntry(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.getEntry(compilation.edgeId, bucketKey);
const sync = entry.sync;
if (!sync || changes.size === 0) continue;
for (const change of changes.values()) {
this.prepareChange(entry, change);
}
deferPublication(entry);
sync.begin();
for (const change of changes.values()) {
this.applyChange(entry, sync, change, compilation.hasOrderBy);
}
sync.commit();
}
for (const entry of newBaselines) entry.sync?.markReady();
for (const [bucketKey, multiplicity] of activity ?? []) {
if (multiplicity >= 0) continue;
active.delete(bucketKey);
this.retireEntry(compilation.edgeId, bucketKey, deferPublication);
}
}
} catch (error) {
this.restore(snapshot, deferredEntries);
this.retiredEntries.clear();
for (const publication of publications) publication.discard();
throw error;
}
this.pending.clear();
this.pendingActivity.clear();
let closed = false;
return {
publish: () => {
if (closed) return;
closed = true;
for (const publication of publications) publication.publish();
this.retiredEntries.clear();
},
rollback: () => {
if (closed) return;
closed = true;
this.restore(snapshot, deferredEntries);
this.retiredEntries.clear();
for (const publication of publications) publication.discard();
}
};
}
resolve(value) {
return this.resolveValue(value);
}
cleanup() {
for (const byBucket of this.entries.values()) {
for (const entry of byBucket.values()) {
void entry.collection.cleanup();
}
}
this.entries.clear();
this.cleanupRetiredEntries();
this.pending.clear();
this.pendingActivity.clear();
this.activeBuckets.clear();
}
accumulate(edgeId, bucketKey, row, multiplicity) {
let buckets = this.pending.get(edgeId);
if (!buckets) {
buckets = /* @__PURE__ */ new Map();
this.pending.set(edgeId, buckets);
}
let rows = buckets.get(bucketKey);
if (!rows) {
rows = /* @__PURE__ */ new Map();
buckets.set(bucketKey, rows);
}
const key = dbIvm.serializeValue(row.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() {
const rows = /* @__PURE__ */ new Map();
for (const byBucket of this.entries.values()) {
for (const entry of byBucket.values()) {
rows.set(
entry,
[...entry.collection._state.syncedData].map(([key, value]) => ({
key,
value,
order: entry.currentOrder.get(key)
}))
);
}
}
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
};
}
restore(snapshot, changedEntries) {
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 of changedEntries) {
if (!previousEntries.has(entry)) continue;
const sync = entry.sync;
if (!sync) continue;
sync.begin();
sync.truncate();
entry.currentOrder.clear();
for (const row of snapshot.rows.get(entry) ?? []) {
entry.keys.set(row.value, row.key);
if (row.order !== void 0) entry.order.set(row.value, row.order);
entry.currentOrder.set(row.key, row.order);
sync.write({ type: `insert`, value: row.value });
}
sync.commit();
}
this.entries.clear();
for (const [edgeId, byBucket] of snapshot.entries) {
this.entries.set(edgeId, new Map(byBucket));
}
this.activeBuckets.clear();
for (const [edgeId, buckets] of snapshot.activeBuckets) {
this.activeBuckets.set(edgeId, new Set(buckets));
}
this.resolvedValues = /* @__PURE__ */ new WeakMap();
for (const entry of currentEntries) {
if (!previousEntries.has(entry)) void entry.collection.cleanup();
}
}
accumulateActivity(edgeId, bucketKey, multiplicity) {
let activity = this.pendingActivity.get(edgeId);
if (!activity) {
activity = /* @__PURE__ */ new Map();
this.pendingActivity.set(edgeId, activity);
}
activity.set(bucketKey, (activity.get(bucketKey) ?? 0) + multiplicity);
}
getActiveBuckets(edgeId) {
let active = this.activeBuckets.get(edgeId);
if (!active) {
active = /* @__PURE__ */ new Set();
this.activeBuckets.set(edgeId, active);
}
return active;
}
retireEntry(edgeId, bucketKey, deferPublication) {
const byBucket = this.entries.get(edgeId);
const entry = byBucket?.get(bucketKey);
if (!entry) return;
const sync = entry.sync;
const keys = [...entry.collection.keys()];
if (sync && keys.length > 0) {
deferPublication(entry);
sync.begin();
for (const key of keys) sync.write({ type: `delete`, key });
sync.commit();
}
byBucket.delete(bucketKey);
if (byBucket.size === 0) this.entries.delete(edgeId);
let retired = this.retiredEntries.get(edgeId);
if (!retired) {
retired = /* @__PURE__ */ new Map();
this.retiredEntries.set(edgeId, retired);
}
retired.set(bucketKey, entry);
}
getEntry(edgeId, bucketKey) {
let byBucket = this.entries.get(edgeId);
if (!byBucket) {
byBucket = /* @__PURE__ */ new Map();
this.entries.set(edgeId, byBucket);
}
const existing = byBucket.get(bucketKey);
if (existing) return existing;
const keys = /* @__PURE__ */ new WeakMap();
const order = /* @__PURE__ */ new WeakMap();
let sync;
const collection = index.createCollection({
id: `__bucket-facade:${this.parentId}:${edgeId}:${bucketKey}`,
getKey: (row) => {
const key = keys.get(row) ?? row?.$key;
if (typeof key !== `string` && typeof key !== `number`) {
throw new Error(`Bucket facade row has no public key`);
}
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 entry = {
collection,
get sync() {
return sync;
},
keys,
order,
currentOrder: /* @__PURE__ */ new Map()
};
byBucket.set(bucketKey, entry);
return entry;
}
applyChange(entry, sync, change, hasOrderBy) {
const key = change.value.publicKey;
const previousOrder = entry.currentOrder.get(key);
const nextOrder = change.value.order;
const orderChanged = sync.collection.has(key) && previousOrder !== nextOrder;
const resolvedRow = this.resolve(change.value.value);
const row = orderChanged && sync.collection.get(key) === resolvedRow ? { ...resolvedRow } : resolvedRow;
entry.keys.set(row, key);
if (nextOrder !== void 0) {
entry.order.set(row, nextOrder);
}
if (change.inserts > change.deletes) {
sync.write({
type: sync.collection.has(key) ? `update` : `insert`,
value: row
});
} else if (change.inserts === change.deletes && sync.collection.has(key)) {
sync.write({ type: `update`, value: row });
} else if (change.deletes > 0) {
sync.write({ type: `delete`, key });
entry.currentOrder.delete(key);
return;
}
entry.currentOrder.set(key, nextOrder);
if (hasOrderBy && orderChanged) sync.collection._markLayoutChange();
}
/** Resolve and validate every public key before opening a sync transaction. */
prepareChange(entry, change) {
const key = change.value.publicKey;
const row = this.resolve(change.value.value);
entry.keys.set(row, key);
entry.collection.getKeyFromItem(row);
}
resolveValue(value) {
if (value !== null && typeof value === `object`) {
const cached = this.resolvedValues.get(value);
if (cached !== void 0) return cached;
}
if (isBucketFacadeRef(value)) {
const { edgeId, bucketKey } = value[materializedPipeline.BUCKET_FACADE_REF];
const facade = this.entries.get(edgeId)?.get(bucketKey)?.collection ?? this.retiredEntries.get(edgeId)?.get(bucketKey)?.collection ?? this.getEntry(edgeId, bucketKey).collection;
this.resolvedValues.set(value, facade);
return facade;
}
if (Array.isArray(value)) {
const result2 = [];
this.resolvedValues.set(value, result2);
result2.push(...value.map((item) => this.resolveValue(item)));
return result2;
}
if (!isPlainObject(value)) return value;
const result = {};
this.resolvedValues.set(value, result);
for (const key of Reflect.ownKeys(value)) {
if (key === index$1.INCLUDES_ROUTING || key === index$1.FN_SELECT_STATE) continue;
result[key] = this.resolveValue(value[key]);
}
return result;
}
cleanupRetiredEntries() {
for (const byBucket of this.retiredEntries.values()) {
for (const entry of byBucket.values()) {
void entry.collection.cleanup();
}
}
this.retiredEntries.clear();
}
}
function isBucketFacadeRef(value) {
return value !== null && typeof value === `object` && materializedPipeline.BUCKET_FACADE_REF in value;
}
function isPlainObject(value) {
if (value === null || typeof value !== `object`) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
exports.BucketFacadeAdapter = BucketFacadeAdapter;
//# sourceMappingURL=bucket-facade-adapter.cjs.map