UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

1,038 lines (1,037 loc) • 37.2 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const utils = require("../utils.cjs"); const SortedMap = require("../SortedMap.cjs"); const virtualProps = require("../virtual-props.cjs"); const errors = require("../errors.cjs"); function automaticRowMetadataWrite(operation) { if (operation.metadata !== void 0 && operation.type !== `delete`) { return { type: `set`, value: operation.metadata }; } return operation.type === `update` ? void 0 : { type: `delete` }; } class CollectionStateManager { /** * Creates a new CollectionState manager */ constructor(config) { this.pendingSyncedTransactions = []; this.a7 = { states: /* @__PURE__ */ new Map(), truncated: false }; this.syncedMetadata = /* @__PURE__ */ new Map(); this.syncedCollectionMetadata = /* @__PURE__ */ new Map(); this.hydrationSeedKeys = /* @__PURE__ */ new Set(); this.hydratedKeys = /* @__PURE__ */ new Set(); this.b_ = false; this.optimisticUpserts = /* @__PURE__ */ new Map(); this.optimisticDeletes = /* @__PURE__ */ new Set(); this.heldOptimisticRows = /* @__PURE__ */ new Map(); this.rowOrigins = /* @__PURE__ */ new Map(); this.pendingLocalChanges = /* @__PURE__ */ new Set(); this.pendingLocalOrigins = /* @__PURE__ */ new Set(); this.dE = /* @__PURE__ */ new Map(); this.size = 0; this.preSyncVisibleState = /* @__PURE__ */ new Map(); this.preSyncVirtualState = /* @__PURE__ */ new Map(); this.recentlySyncedKeys = /* @__PURE__ */ new Set(); this.hasReceivedFirstCommit = false; this.isCommittingSyncTransactions = false; this.bU = false; this.syncRunGeneration = 0; this.isLocalOnly = false; this.commitPendingTransactions = () => { if (this.bU) return; this.bU = true; let failed = false; let firstError; try { let result; do { result = this.dG(); if (result.failure && !failed) { failed = true; firstError = result.failure.error; } } while (result.processed); } finally { this.bU = false; } if (failed) throw firstError; }; this.config = config; this.transactions = new SortedMap.SortedMap( (a, b) => a.compareCreatedAt(b) ); this.syncedData = new SortedMap.SortedMap(config.compare); } setDeps(deps) { this.collection = deps.collection; this.lifecycle = deps.lifecycle; this.changes = deps.changes; this.indexes = deps.indexes; this.a3 = deps.events; } /** * Checks whether this row currently has no pending local optimistic writes. * * This is local mutation status, not backend confirmation: `true` means the * row is not currently affected by an optimistic transaction in this * collection's visible state. * * Used to compute the $synced virtual property. */ isRowSynced(key) { if (this.isLocalOnly) { return true; } return !this.optimisticUpserts.has(key) && !this.optimisticDeletes.has(key); } /** * Gets the origin of the last confirmed change to a row. * Returns 'local' if the row has optimistic mutations (optimistic changes are local). * Used to compute the $origin virtual property. */ getRowOrigin(key) { if (this.isLocalOnly) { return "local"; } if (this.optimisticUpserts.has(key) || this.optimisticDeletes.has(key)) { return "local"; } return this.rowOrigins.get(key) ?? "remote"; } bT(key, overrides) { const synced = overrides?.$synced ?? this.isRowSynced(key); return { $hasPendingWrites: !synced, $synced: synced, $origin: overrides?.$origin ?? this.getRowOrigin(key), $key: overrides?.$key ?? key, $collectionId: overrides?.$collectionId ?? this.collection.id }; } ak(key, options) { if (this.isLocalOnly) { return this.bT(key, { $synced: true, $origin: "local" }); } const optimisticUpserts = options?.optimisticUpserts ?? this.optimisticUpserts; const optimisticDeletes = options?.optimisticDeletes ?? this.optimisticDeletes; const hasOptimisticChange = optimisticUpserts.has(key) || optimisticDeletes.has(key); return this.bT(key, { $synced: !hasOptimisticChange, $origin: hasOptimisticChange ? "local" : (options?.rowOrigins ?? this.rowOrigins).get(key) ?? "remote" }); } et(keys) { const rowOrigins = /* @__PURE__ */ new Map(); for (const key of keys) { const origin = this.rowOrigins.get(key); if (origin !== void 0) { rowOrigins.set(key, origin); } } return rowOrigins; } aW(row, virtualProps2) { const existingRow = row; const synced = existingRow.$synced ?? virtualProps2.$synced; const origin = existingRow.$origin ?? virtualProps2.$origin; const resolvedKey = existingRow.$key ?? virtualProps2.$key; const collectionId = existingRow.$collectionId ?? virtualProps2.$collectionId; const cached = this.dE.get(resolvedKey); if (cached && cached.row === row && cached.synced === synced && cached.origin === origin && cached.collectionId === collectionId) { return cached.enriched; } const enriched = { ...row, $hasPendingWrites: !synced, $synced: synced, $origin: origin, $key: resolvedKey, $collectionId: collectionId }; this.dE.set(resolvedKey, { row, synced, origin, collectionId, enriched }); return enriched; } dh() { this.dE.clear(); this.rowOrigins.clear(); this.pendingLocalChanges.clear(); this.pendingLocalOrigins.clear(); } /** * Enriches a row with virtual properties using the "add-if-missing" pattern. * If the row already has virtual properties (from an upstream collection), * they are preserved. Otherwise, new values are computed. */ enrichWithVirtualProps(row, key) { return this.aW( row, this.bT(key) ); } /** * Visible entries whose stored row passes `prefilter`, enriched with virtual * properties. Rows that fail are never enriched. */ *entriesPassing(prefilter) { const rows = this.optimisticUpserts.size === 0 && this.optimisticDeletes.size === 0 ? this.syncedData : this.entries(); for (const [key, row] of rows) { if (prefilter(row)) yield [key, this.enrichWithVirtualProps(row, key)]; } } /** * Creates a change message with virtual properties. * Uses the "add-if-missing" pattern so that pass-through from upstream * collections works correctly. */ enrichChangeMessage(change) { const { __virtualProps } = change; const enrichedPreviousValue = change.previousValue ? __virtualProps?.previousValue ? this.aW( change.previousValue, __virtualProps.previousValue ) : this.enrichWithVirtualProps(change.previousValue, change.key) : void 0; const enrichedValue = __virtualProps?.value ? this.aW(change.value, __virtualProps.value) : this.enrichWithVirtualProps(change.value, change.key); if (change.type === `delete`) this.dE.delete(change.key); return { key: change.key, type: change.type, value: enrichedValue, previousValue: enrichedPreviousValue, metadata: change.metadata }; } /** * Get the current value for a key enriched with virtual properties. */ getWithVirtualProps(key) { const value = this.get(key); if (value === void 0) { return void 0; } return this.enrichWithVirtualProps(value, key); } /** * Get the current value for a key (virtual derived state) */ get(key) { const { optimisticDeletes, optimisticUpserts, syncedData } = this; if (optimisticDeletes.has(key)) { return void 0; } if (optimisticUpserts.has(key)) { return optimisticUpserts.get(key); } return syncedData.get(key); } /** * Check if a key exists in the collection (virtual derived state) */ has(key) { const { optimisticDeletes, optimisticUpserts, syncedData } = this; if (optimisticDeletes.has(key)) { return false; } if (optimisticUpserts.has(key)) { return true; } return syncedData.has(key); } /** * Get all keys (virtual derived state) */ *keys() { const { syncedData, optimisticDeletes, optimisticUpserts } = this; for (const key of syncedData.keys()) { if (!optimisticDeletes.has(key)) { yield key; } } for (const key of optimisticUpserts.keys()) { if (!syncedData.has(key) && !optimisticDeletes.has(key)) { yield key; } } } /** * Get all values (virtual derived state) */ *values() { for (const key of this.keys()) { const value = this.get(key); if (value !== void 0) { yield value; } } } /** * Get all entries (virtual derived state) */ *entries() { for (const key of this.keys()) { const value = this.get(key); if (value !== void 0) { yield [key, value]; } } } /** * Get all entries (virtual derived state) */ *[Symbol.iterator]() { for (const [key, value] of this.entries()) { yield [key, value]; } } /** * Execute a callback for each entry in the collection */ forEach(callbackfn) { let index = 0; for (const [key, value] of this.entries()) { callbackfn(value, key, index++); } } /** * Create a new array with the results of calling a function for each entry in the collection */ map(callbackfn) { const result = []; let index = 0; for (const [key, value] of this.entries()) { result.push(callbackfn(value, key, index++)); } return result; } /** * Check if the given collection is this collection * @param collection The collection to check * @returns True if the given collection is this collection, false otherwise */ aP(collection) { return collection === this.collection; } /** * Recompute optimistic state from active transactions */ recomputeOptimisticState(triggeredByUserAction = false) { if (this.isCommittingSyncTransactions && !triggeredByUserAction) { return; } const previousState = new Map(this.optimisticUpserts); const previousDeletes = new Set(this.optimisticDeletes); const previousRowOrigins = this.rowOrigins; const pendingSyncKeys = /* @__PURE__ */ new Set(); for (const transaction of this.pendingSyncedTransactions) { if (!transaction.committed) continue; for (const operation of transaction.operations) { pendingSyncKeys.add(operation.key); } } for (const transaction of this.transactions.values()) { if (transaction.state !== `completed`) continue; for (const mutation of transaction.mutations) { if (!this.aP(mutation.collection) || !pendingSyncKeys.has(mutation.key)) continue; this.pendingLocalOrigins.add(mutation.key); if (!mutation.optimistic) continue; const held = this.heldOptimisticRows.get(mutation.key); if (held && held.owner.compareCreatedAt(transaction) > 0) continue; this.heldOptimisticRows.set(mutation.key, { owner: transaction, row: mutation.type === `delete` ? void 0 : mutation.modified }); } } this.optimisticUpserts.clear(); this.optimisticDeletes.clear(); this.pendingLocalChanges.clear(); for (const [key, { row }] of this.heldOptimisticRows) { if (!pendingSyncKeys.has(key)) { this.heldOptimisticRows.delete(key); this.pendingLocalOrigins.delete(key); } else if (row === void 0) this.optimisticDeletes.add(key); else this.optimisticUpserts.set(key, row); } this.gl(); this.size = this.e1(); const events = []; this.ey( previousState, previousDeletes, previousRowOrigins, events ); const filteredEvents = triggeredByUserAction ? events : events.filter((event) => !this.recentlySyncedKeys.has(event.key)); if (filteredEvents.length > 0) this.indexes.updateIndexes(filteredEvents); this.changes.emitEvents(filteredEvents, triggeredByUserAction); } /** * Overlay still-active optimistic mutations on the current layers and * record their keys as pending local changes for $origin tracking. */ gl() { for (const transaction of this.transactions.values()) { if (transaction.state === `completed` || transaction.state === `failed`) continue; for (const mutation of transaction.mutations) { if (!this.aP(mutation.collection)) continue; this.pendingLocalChanges.add(mutation.key); if (!mutation.optimistic) continue; if (mutation.type === `delete`) { this.optimisticUpserts.delete(mutation.key); this.optimisticDeletes.add(mutation.key); } else { this.optimisticUpserts.set(mutation.key, mutation.modified); this.optimisticDeletes.delete(mutation.key); } } } } /** * Calculate the current size based on synced data and optimistic changes */ e1() { const syncedSize = this.syncedData.size; const deletesFromSynced = Array.from(this.optimisticDeletes).filter( (key) => this.syncedData.has(key) && !this.optimisticUpserts.has(key) ).length; const upsertsNotInSynced = Array.from(this.optimisticUpserts.keys()).filter( (key) => !this.syncedData.has(key) ).length; return syncedSize - deletesFromSynced + upsertsNotInSynced; } /** * Collect events for optimistic changes */ ey(previousUpserts, previousDeletes, previousRowOrigins, events) { const allKeys = /* @__PURE__ */ new Set([ ...previousUpserts.keys(), ...this.optimisticUpserts.keys(), ...previousDeletes, ...this.optimisticDeletes ]); for (const key of allKeys) { const currentValue = this.get(key); const previousValue = this.fs( key, previousUpserts, previousDeletes ); const previousVirtualProps = this.ak(key, { rowOrigins: previousRowOrigins, optimisticUpserts: previousUpserts, optimisticDeletes: previousDeletes }); const nextVirtualProps = this.ak(key); if (previousValue !== void 0 && currentValue === void 0) { events.push({ type: `delete`, key, value: previousValue, __virtualProps: { value: previousVirtualProps } }); } else if (previousValue === void 0 && currentValue !== void 0) { events.push({ type: `insert`, key, value: currentValue, __virtualProps: { value: nextVirtualProps } }); } else if (previousValue !== void 0 && currentValue !== void 0 && (!utils.deepEquals(previousValue, currentValue) || previousVirtualProps.$origin !== nextVirtualProps.$origin || previousVirtualProps.$synced !== nextVirtualProps.$synced)) { events.push({ type: `update`, key, value: currentValue, previousValue, __virtualProps: { value: nextVirtualProps, previousValue: previousVirtualProps } }); } } } /** Build once per output flush; queued membership excludes optimistic edits. */ createSyncedKeyLookup() { if (this.pendingSyncedTransactions.length === 0) return (key) => this.syncedData.has(key); const queued = /* @__PURE__ */ new Map(); let truncated = false; for (const transaction of this.pendingSyncedTransactions) { if (!transaction.committed) continue; if (transaction.truncate) { queued.clear(); truncated = true; } for (const operation of transaction.operations) { queued.set(operation.key, operation.type !== `delete`); } } return (key) => queued.get(key) ?? (!truncated && this.syncedData.has(key)); } enableHydrationAuthorityTracking() { this.ar ??= /* @__PURE__ */ new Set(); } /** A late hydration seed cannot supersede committed adapter work. */ createAdapterAuthorityLookup() { const queuedKeys = /* @__PURE__ */ new Set(); let queuedTruncate = false; for (const transaction of this.pendingSyncedTransactions) { if (!transaction.committed || transaction.preserveHydrationSeedKeys) continue; if (transaction.truncate) queuedTruncate = true; for (const operation of transaction.operations) { queuedKeys.add(operation.key); } } return (key) => this.b_ || queuedTruncate || queuedKeys.has(key) || this.ar?.has(key) === true || this.syncedData.has(key) && !this.hydrationSeedKeys.has(key); } bj(projection, key) { const state = projection.states.get(key); if (state !== void 0) return state; if (projection.truncated) return { exists: false, value: void 0 }; return { exists: this.syncedData.has(key), value: this.syncedData.get(key) }; } /** * The synced row once every accepted sync transaction applies. A queued * transaction is accepted before it is visible, so a direct write must * read this rather than `syncedData`. */ getAcceptedSyncedRow(key) { return this.bj(this.a7, key).value; } /** * Accept a committed seed transaction, such as a DbClient hydration chunk, * ahead of any still-open source transaction, so that source's `commit()` * still targets its own writes. */ acceptSeedTransaction(transaction) { const open = this.pendingSyncedTransactions.findIndex( (pending) => !pending.committed ); if (open === -1) this.pendingSyncedTransactions.push(transaction); else this.pendingSyncedTransactions.splice(open, 0, transaction); this.refreshPendingSyncedProjection(); this.commitPendingTransactions(); } /** Every synced row once every accepted sync transaction applies. */ *acceptedSyncedEntries() { const { states, truncated } = this.a7; if (!truncated) { for (const entry of this.syncedData.entries()) { if (!states.has(entry[0])) yield entry; } } for (const [key, state] of states) { if (state.exists) yield [key, state.value]; } } dt(projection, key, value) { const current = this.bj(projection, key); if (!current.exists) return `insert`; if (current.value !== void 0 && utils.deepEquals(current.value, value) || this.hydrationSeedKeys.has(key)) { return `update`; } return `duplicate`; } /** Classify an adapter insert against retained and queued source state. */ classifyPendingSyncedInsert(key, value) { return this.dt( this.a7, key, value ); } c1(projection, operation) { const key = operation.key; const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`; if (operation.type === `delete`) { projection.states.set(key, { exists: false, value: void 0 }); } else if (operation.type === `update` && rowUpdateMode === `partial`) { const current = this.bj(projection, key); projection.states.set(key, { exists: true, // Spread defines own fields; assignment would run a `__proto__` setter. value: { ...current.value, ...operation.value } }); } else { projection.states.set(key, { exists: true, value: operation.value }); } } /** Extend the queued projection after admitting one sync operation. */ stagePendingSyncOperation(operation) { this.c1(this.a7, operation); } /** * Get the previous value for a key given previous optimistic state */ fs(key, previousUpserts, previousDeletes) { if (previousDeletes.has(key)) { return void 0; } if (previousUpserts.has(key)) { return previousUpserts.get(key); } return this.syncedData.get(key); } dJ(transaction) { const explicitKeys = transaction.explicitRowMetadataWriteKeys ?? /* @__PURE__ */ new Set(); const operationKeys = new Set( transaction.operations.map((operation) => operation.key) ); for (const key of operationKeys) { if (!explicitKeys.has(key)) transaction.rowMetadataWrites.delete(key); } for (const operation of transaction.operations) { const key = operation.key; if (explicitKeys.has(key)) continue; const metadataWrite = automaticRowMetadataWrite(operation); if (metadataWrite) transaction.rowMetadataWrites.set(key, metadataWrite); } } /** * Rebuild the queued projection after truncate, application, or an * accepted seed changes queue history. An open transaction can become * invalid when a later transaction begun inside it commits first; it then * holds an invalidation error that its commit returns. Only the open last * transaction can be canceled, so a replay that invalidates a committed * transaction is an invariant failure. */ bA() { const projection = { states: /* @__PURE__ */ new Map(), truncated: false }; for (const transaction of this.pendingSyncedTransactions) { if (transaction.invalidationError !== void 0) continue; const statesBeforeTransaction = new Map(projection.states); const truncatedBeforeTransaction = projection.truncated; if (transaction.truncate) { projection.states = /* @__PURE__ */ new Map(); projection.truncated = true; } let invalidKey; for (const operation of transaction.operations) { const key = operation.key; if (operation.originalSyncType === `insert` && operation.type !== `delete`) { const disposition = this.dt( projection, key, operation.value ); if (disposition === `duplicate`) { invalidKey = key; break; } operation.type = disposition; } this.c1(projection, operation); } if (invalidKey !== void 0) { if (transaction.committed) throw new errors.SyncQueueInvariantError( `replay made an accepted insert a duplicate` ); projection.states = statesBeforeTransaction; projection.truncated = truncatedBeforeTransaction; transaction.invalidationError = transaction.duplicateKeyError?.(invalidKey) ?? new errors.SyncTransactionAbortedError(); continue; } this.dJ(transaction); } this.a7 = projection; } /** Rebuild after truncate, application, or an accepted seed. */ refreshPendingSyncedProjection() { this.bA(); } hasPersistingTransaction() { for (const transaction of this.transactions.values()) { if (transaction.state === `persisting`) return true; } return false; } dG() { const syncRunGeneration = this.syncRunGeneration; const hasPersistingTransaction = this.hasPersistingTransaction(); const committedSyncedTransactions = []; const uncommittedSyncedTransactions = []; let hasTruncateSync = false; let layoutChanged = false; for (const t of this.pendingSyncedTransactions) { if (t.committed) { committedSyncedTransactions.push(t); layoutChanged ||= t.layoutChanged; hasTruncateSync ||= t.truncate === true; } else { uncommittedSyncedTransactions.push(t); } } if (committedSyncedTransactions.length === 0) { return { processed: false }; } if (!hasPersistingTransaction || hasTruncateSync) { const previousLayout = layoutChanged ? [...this.keys()] : void 0; this.pendingSyncedTransactions = uncommittedSyncedTransactions; const deferOrder = committedSyncedTransactions.reduce( (count, transaction) => count + transaction.operations.length, 0 ) >= 512; for (const transaction of committedSyncedTransactions) { transaction.applicationStarted = true; } this.isCommittingSyncTransactions = true; let truncatePendingLocalChanges; let truncatePendingLocalOrigins; const changedKeys = /* @__PURE__ */ new Set(); const syncedInsertedOrUpdatedKeys = /* @__PURE__ */ new Set(); const firstSyncOperations = /* @__PURE__ */ new Map(); for (const transaction of committedSyncedTransactions) { for (const operation of transaction.operations) { const key = operation.key; changedKeys.add(key); if (!firstSyncOperations.has(key)) firstSyncOperations.set(key, operation); if (operation.type !== `delete`) syncedInsertedOrUpdatedKeys.add(key); } for (const [key] of transaction.rowMetadataWrites) { changedKeys.add(key); } } const previousRowOrigins = this.et(changedKeys); const previousOptimisticUpserts = new Map(this.optimisticUpserts); const previousOptimisticDeletes = new Set(this.optimisticDeletes); let currentVisibleState = this.preSyncVisibleState; if (currentVisibleState.size === 0) { currentVisibleState = /* @__PURE__ */ new Map(); for (const key of changedKeys) { const currentValue = this.get(key); if (currentValue !== void 0) { currentVisibleState.set(key, currentValue); } } } const events = []; let reappliedKeys; if (hasTruncateSync) { for (const [key, value] of this.entries()) { events.push({ type: `delete`, key, value: this.aW( value, this.ak(key) ) }); } } const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`; for (const transaction of committedSyncedTransactions) { if (transaction.truncate) { truncatePendingLocalChanges = new Set(this.pendingLocalChanges); truncatePendingLocalOrigins = new Set(this.pendingLocalOrigins); this.syncedData.clear(); this.syncedMetadata.clear(); this.hydrationSeedKeys.clear(); this.hydratedKeys.clear(); if (!transaction.preserveHydrationSeedKeys && this.ar) { this.b_ = true; this.ar.clear(); } this.dh(); for (const key of changedKeys) { currentVisibleState.delete(key); } this.a3.emit(`truncate`, { type: `truncate`, collection: this.collection }); } const localKeys = /* @__PURE__ */ new Set(); for (const operation of transaction.operations) { const key = operation.key; const retainedLocalOrigin = truncatePendingLocalChanges?.has(key) === true || truncatePendingLocalOrigins?.has(key) === true; const origin = this.isLocalOnly || this.pendingLocalChanges.has(key) || this.pendingLocalOrigins.has(key) || localKeys.has(key) || retainedLocalOrigin ? "local" : "remote"; if (origin === `local`) localKeys.add(key); this.dE.delete(key); if (operation.type === `delete`) { this.syncedData.delete(key, deferOrder); this.syncedMetadata.delete(key); this.rowOrigins.delete(key); } else { this.syncedData.set( key, operation.type === `update` && rowUpdateMode === `partial` ? { ...this.syncedData.get(key), ...operation.value } : operation.value, deferOrder ); this.rowOrigins.set(key, origin); } this.pendingLocalChanges.delete(key); this.pendingLocalOrigins.delete(key); this.heldOptimisticRows.delete(key); if (!transaction.preserveHydrationSeedKeys) { this.hydrationSeedKeys.delete(key); this.hydratedKeys.delete(key); if (operation.type === `delete`) this.ar?.add(key); else this.ar?.delete(key); } } for (const [key, metadataWrite] of transaction.rowMetadataWrites) { if (metadataWrite.type === `delete`) { this.syncedMetadata.delete(key); continue; } this.syncedMetadata.set(key, metadataWrite.value); } for (const [ key, metadataWrite ] of transaction.collectionMetadataWrites) { if (metadataWrite.type === `delete`) { this.syncedCollectionMetadata.delete(key); continue; } this.syncedCollectionMetadata.set(key, metadataWrite.value); } } this.syncedData.restoreOrder(); this.refreshPendingSyncedProjection(); this.optimisticUpserts.clear(); this.optimisticDeletes.clear(); this.isCommittingSyncTransactions = false; this.gl(); if (hasTruncateSync) { for (const [key, value] of this.optimisticUpserts) { events.push({ type: `insert`, key, value }); } reappliedKeys = new Set(this.optimisticUpserts.keys()); } for (const key of changedKeys) { if (reappliedKeys?.has(key)) continue; const firstSyncOperation = firstSyncOperations.get(key); const syncPreviousValue = firstSyncOperation?.type === `update` && currentVisibleState.get(key) === firstSyncOperation.value ? firstSyncOperation.previousValue : void 0; const previousVisibleValue = !hasTruncateSync && !previousOptimisticUpserts.has(key) && !previousOptimisticDeletes.has(key) && syncPreviousValue !== void 0 ? syncPreviousValue : currentVisibleState.get(key); const newVisibleValue = this.get(key); const previousVirtualProps = this.preSyncVirtualState.get(key) ?? this.ak(key, { rowOrigins: previousRowOrigins, optimisticUpserts: previousOptimisticUpserts, optimisticDeletes: previousOptimisticDeletes }); const nextVirtualProps = this.ak(key); const virtualChanged = previousVirtualProps.$synced !== nextVirtualProps.$synced || previousVirtualProps.$origin !== nextVirtualProps.$origin; const withPreviousVirtualProps = (value) => virtualProps.enrichRowWithVirtualProps( value, key, this.collection.id, () => previousVirtualProps.$synced, () => previousVirtualProps.$origin ); if (previousVisibleValue === void 0) { if (newVisibleValue === void 0) continue; events.push({ type: `insert`, key, value: newVisibleValue }); } else if (newVisibleValue === void 0) { events.push({ type: `delete`, key, value: withPreviousVirtualProps(previousVisibleValue) }); } else if (virtualChanged || !utils.deepEquals(previousVisibleValue, newVisibleValue)) { events.push({ type: `update`, key, value: newVisibleValue, previousValue: withPreviousVirtualProps(previousVisibleValue) }); } } this.size = this.e1(); if (events.length > 0) { this.indexes.updateIndexes(events); } let failure; const capture = (step) => { try { step(); } catch (error) { failure ??= { error }; } }; const visibleLayoutChanged = previousLayout !== void 0 && (previousLayout.length !== this.size || [...this.keys()].some((key, index) => key !== previousLayout[index])); let emitted = false; const emit = () => { emitted = true; capture( () => this.changes.emitEvents(events, true, visibleLayoutChanged) ); }; if (hasTruncateSync && this.lifecycle.status !== `ready`) capture(() => this.lifecycle.markReady(emit)); if (!emitted) emit(); if (this.syncRunGeneration === syncRunGeneration) { this.preSyncVisibleState.clear(); this.preSyncVirtualState.clear(); Promise.resolve().then(() => { if (this.syncRunGeneration === syncRunGeneration) { this.recentlySyncedKeys.clear(); } }); this.hasReceivedFirstCommit = true; } for (const transaction of committedSyncedTransactions) { transaction.applied.resolve(); } return { processed: true, failure }; } return { processed: false }; } /** * Abandons the open last sync transaction before acceptance. Only * `commit(signal)` with an aborted signal reaches this, so no other * transaction can depend on the one canceled. */ cancelPendingSyncedTransaction(transaction, reason = new errors.SyncTransactionAbortedError()) { if (transaction.committed || this.pendingSyncedTransactions.at(-1) !== transaction) throw new errors.SyncQueueInvariantError( `only the open last sync transaction can be canceled` ); this.pendingSyncedTransactions.pop(); const canceledKeys = /* @__PURE__ */ new Set(); for (const operation of transaction.operations) { canceledKeys.add(operation.key); } transaction.applied.reject(reason); this.bA(); const remainingPendingKeys = /* @__PURE__ */ new Set(); for (const pending of this.pendingSyncedTransactions) { if (pending.invalidationError !== void 0) continue; for (const operation of pending.operations) { remainingPendingKeys.add(operation.key); } } for (const key of canceledKeys) { if (!remainingPendingKeys.has(key)) { this.recentlySyncedKeys.delete(key); this.preSyncVisibleState.delete(key); this.preSyncVirtualState.delete(key); } } if (this.pendingSyncedTransactions.length === 0) { this.preSyncVisibleState.clear(); this.preSyncVirtualState.clear(); this.recentlySyncedKeys.clear(); this.changes.emitEvents([], true); } else { this.recomputeOptimisticState(false); } } /** * Schedule cleanup of a transaction when it completes */ scheduleTransactionCleanup(transaction) { if (transaction.state === `completed`) { this.transactions.delete(transaction.id); return; } transaction.isPersisted.promise.then(() => { this.transactions.delete(transaction.id); }).catch(() => { }); } /** * Capture visible state for keys that will be affected by pending sync operations * This must be called BEFORE onTransactionStateChange clears optimistic state */ capturePreSyncVisibleState() { if (this.pendingSyncedTransactions.length === 0) return; const syncedKeys = /* @__PURE__ */ new Set(); for (const transaction of this.pendingSyncedTransactions) { if (!transaction.committed) continue; for (const operation of transaction.operations) { syncedKeys.add(operation.key); } for (const key of transaction.rowMetadataWrites.keys()) { syncedKeys.add(key); } } for (const key of syncedKeys) { this.recentlySyncedKeys.add(key); } for (const key of syncedKeys) { if (!this.preSyncVisibleState.has(key)) { const currentValue = this.get(key); this.preSyncVisibleState.set(key, currentValue); if (currentValue !== void 0) { this.preSyncVirtualState.set( key, this.ak(key) ); } } } } /** * Trigger a recomputation when transactions change * This method should be called by the Transaction class when state changes */ onTransactionStateChange() { const hasPersistingTransaction = this.hasPersistingTransaction(); this.changes.shouldBatchEvents = this.pendingSyncedTransactions.some( (transaction) => transaction.committed && (!hasPersistingTransaction || transaction.truncate) ); if (this.changes.shouldBatchEvents) this.capturePreSyncVisibleState(); this.recomputeOptimisticState(false); } /** * Clean up the collection by stopping sync and clearing data * This can be called manually or automatically by garbage collection */ cleanup() { this.syncRunGeneration++; for (const transaction of this.pendingSyncedTransactions) { transaction.applied.reject(new errors.SyncTransactionAbortedError()); } this.syncedData.clear(); this.syncedMetadata.clear(); this.syncedCollectionMetadata.clear(); this.optimisticUpserts.clear(); this.optimisticDeletes.clear(); this.heldOptimisticRows.clear(); this.hydrationSeedKeys.clear(); this.hydratedKeys.clear(); this.ar?.clear(); this.b_ = false; this.dh(); this.isLocalOnly = false; this.localOnlyDirectWrite = void 0; this.size = 0; this.pendingSyncedTransactions = []; this.a7 = { states: /* @__PURE__ */ new Map(), truncated: false }; this.preSyncVisibleState.clear(); this.preSyncVirtualState.clear(); this.recentlySyncedKeys.clear(); this.hasReceivedFirstCommit = false; } } exports.CollectionStateManager = CollectionStateManager; exports.automaticRowMetadataWrite = automaticRowMetadataWrite; //# sourceMappingURL=state.cjs.map