UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

419 lines (418 loc) • 14.3 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const dbIvm = require("@tanstack/db-ivm"); const typeGuards = require("../../utils/type-guards.cjs"); const getOrCreate = require("../../utils/get-or-create.cjs"); const index = require("../../collection/index.cjs"); const routeMetadata = require("../compiler/route-metadata.cjs"); const errorMessage = require("../../error-message.cjs"); const materializedPipeline = require("./materialized-pipeline.cjs"); const PRIVATE_RESULT_KEYS = /* @__PURE__ */ new Set([routeMetadata.INCLUDES_ROUTING]); const NO_FACADE_CHANGES = { prepare: () => { }, publish: () => { }, rollback: () => { } }; class BucketFacadeAdapter { constructor(parentId, compilations, onMessages) { this.parentId = parentId; this.f3 = compilations; 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; 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); } this.beginWrite( entry, sync, snapshot, publications, [...changes.values()].map( (change) => change.value.publicKey ) ); for (const change of changes.values()) { this.fM(entry, sync, change, compilation.hasOrderBy); } sync.commit(); } for (const [bucketKey, multiplicity] of activity ?? []) { if (multiplicity >= 0) continue; active.delete(bucketKey); this.fO( compilation.edgeId, bucketKey, snapshot, publications ); } } } 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); } cleanup() { this.cleanedUp = true; for (const byBucket of this.entries.values()) { for (const entry of byBucket.values()) { void entry.collection.cleanup(); } } this.entries.clear(); this.eR(); 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 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); } copyRows(entry) { return [...entry.collection._state.syncedData].map(([key, value]) => ({ key, value, order: entry.currentOrder.get(key) })); } 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 restoredKeys = new Set(rows.map((row) => row.key)); const synced = entry.collection._state.syncedData; sync.begin(); for (const key of /* @__PURE__ */ new Set([ ...synced.keys(), ...snapshot.written.get(entry) ])) { if (!restoredKeys.has(key)) sync.write({ type: `delete`, key }); } entry.currentOrder.clear(); for (const row of rows) { 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: synced.has(row.key) ? `update` : `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.ds = /* @__PURE__ */ new WeakMap(); for (const entry of currentEntries) { if (!previousEntries.has(entry)) void entry.collection.cleanup(); } } 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()); } /** * Begin a facade write. Copy the facade's rows and defer its events before * its first write in the flush, so a rollback reads only the facades the * flush wrote. A bucket written and then retired in one flush copies once. */ beginWrite(entry, sync, snapshot, publications, keys) { if (!snapshot.rows.has(entry)) { snapshot.rows.set(entry, this.copyRows(entry)); snapshot.written.set(entry, /* @__PURE__ */ new Set()); publications.push(entry.collection._deferPublication()); } for (const key of keys) snapshot.written.get(entry).add(key); sync.begin(); } fO(edgeId, bucketKey, snapshot, publications) { 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) { this.beginWrite(entry, sync, snapshot, publications, keys); for (const key of keys) sync.write({ type: `delete`, key }); sync.commit(); } 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 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( 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 entry = { collection, get sync() { return sync; }, keys, order, currentOrder: /* @__PURE__ */ new Map() }; byBucket.set(bucketKey, entry); return entry; } fM(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 ? { ...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. */ fF(entry, change) { const key = change.value.publicKey; const row = this.resolve(change.value.value); entry.keys.set(row, key); entry.collection.getKeyFromItem(row); } dn(value) { if (value === null || typeof value !== `object`) return value; const cached = this.ds.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.bH.get(edgeId)?.get(bucketKey)?.collection ?? this.fr(edgeId, bucketKey).collection; this.ds.set(value, facade); return facade; } if (Array.isArray(value) || typeGuards.isPlainObject(value)) { const result = routeMetadata.transformPublicContainers( value, (leaf) => isBucketFacadeRef(leaf) ? this.dn(leaf) : leaf, PRIVATE_RESULT_KEYS ); this.ds.set(value, result); return result; } return value; } eR() { for (const byBucket of this.bH.values()) { for (const entry of byBucket.values()) { void entry.collection.cleanup(); } } this.bH.clear(); } } function isBucketFacadeRef(value) { return value !== null && typeof value === `object` && materializedPipeline.BUCKET_FACADE_REF in value; } exports.BucketFacadeAdapter = BucketFacadeAdapter; //# sourceMappingURL=bucket-facade-adapter.cjs.map