@tanstack/db
Version:
A reactive client store for building super fast apps on sync
1,038 lines (1,037 loc) • 37 kB
JavaScript
import { deepEquals } from "../utils.js";
import { SortedMap } from "../SortedMap.js";
import { enrichRowWithVirtualProps } from "../virtual-props.js";
import { SyncQueueInvariantError, SyncTransactionAbortedError } from "../errors.js";
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(
(a, b) => a.compareCreatedAt(b)
);
this.syncedData = new 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, virtualProps) {
const existingRow = row;
const synced = existingRow.$synced ?? virtualProps.$synced;
const origin = existingRow.$origin ?? virtualProps.$origin;
const resolvedKey = existingRow.$key ?? virtualProps.$key;
const collectionId = existingRow.$collectionId ?? virtualProps.$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 && (!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 && 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 SyncQueueInvariantError(
`replay made an accepted insert a duplicate`
);
projection.states = statesBeforeTransaction;
projection.truncated = truncatedBeforeTransaction;
transaction.invalidationError = transaction.duplicateKeyError?.(invalidKey) ?? new 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) => 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 || !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 SyncTransactionAbortedError()) {
if (transaction.committed || this.pendingSyncedTransactions.at(-1) !== transaction)
throw new 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 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;
}
}
export {
CollectionStateManager,
automaticRowMetadataWrite
};
//# sourceMappingURL=state.js.map