UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

382 lines (381 loc) • 14.8 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const proxy = require("../proxy.cjs"); const uuid = require("../utils/uuid.cjs"); const transactions = require("../transactions.cjs"); const errors = require("../errors.cjs"); const transactionMetadata = require("./transaction-metadata.cjs"); let mutationIdPrefix; let mutationCount = 0; function createMutationId() { mutationIdPrefix ??= uuid.safeRandomUUID(); return `${mutationIdPrefix}-${++mutationCount}`; } class CollectionMutationsManager { constructor(config, id) { this.insert = (data, config2) => { this.lifecycle.validateCollectionUsable(`insert`); const state = this.state; const ambientTransaction = this.getActiveTransaction(); if (!ambientTransaction && !this.config.onInsert) { throw new errors.MissingInsertHandlerError(); } const items = Array.isArray(data) ? data : [data]; const now = /* @__PURE__ */ new Date(); const mutations = []; const keysInCurrentBatch = /* @__PURE__ */ new Set(); items.forEach((item) => { const validatedData = this.validateData(item, `insert`); const key = this.config.getKey(validatedData); if (keysInCurrentBatch.has(key)) { throw new errors.DuplicateKeyError(key); } keysInCurrentBatch.add(key); const globalKey = this.generateGlobalKey(key, item); const mutation = { mutationId: createMutationId(), original: {}, modified: validatedData, // Pick the values from validatedData based on what's passed in - this is for cases // where a schema has default values. The validated data has the extra default // values but for changes, we just want to show the data that was actually passed in. changes: Object.fromEntries( Object.keys(item).map((k) => [ k, validatedData[k] ]) ), globalKey, key, metadata: config2?.metadata, syncMetadata: this.config.sync.getSyncMetadata?.() || {}, optimistic: config2?.optimistic ?? true, type: `insert`, createdAt: now, updatedAt: now, collection: this.collection }; mutations.push(mutation); }); let duplicate = mutations.find(({ key }) => state.has(key)); if (duplicate) throw new errors.DuplicateKeyError(duplicate.key); this.collection._sync.startSync(); duplicate = mutations.find(({ key }) => state.has(key)); if (duplicate) throw new errors.DuplicateKeyError(duplicate.key); if (ambientTransaction) { ambientTransaction.applyMutations(mutations); state.transactions.set(ambientTransaction.id, ambientTransaction); state.scheduleTransactionCleanup(ambientTransaction); state.recomputeOptimisticState(true); return ambientTransaction; } else { const localOnly = this.f7(mutations, `insert`); if (localOnly) return localOnly; const directOpTransaction = this.createTransaction({ metadata: { [transactionMetadata.DIRECT_TRANSACTION_METADATA_KEY]: true }, mutationFn: async (params) => { return await this.config.onInsert({ transaction: params.transaction, collection: this.collection }); } }); directOpTransaction.applyMutations(mutations); this.cn(mutations); state.transactions.set(directOpTransaction.id, directOpTransaction); state.scheduleTransactionCleanup(directOpTransaction); state.recomputeOptimisticState(true); directOpTransaction.commit().catch(() => void 0); return directOpTransaction; } }; this.delete = (keys, config2) => { const state = this.state; this.lifecycle.validateCollectionUsable(`delete`); const ambientTransaction = this.getActiveTransaction(); if (!ambientTransaction && !this.config.onDelete) { throw new errors.MissingDeleteHandlerError(); } if (Array.isArray(keys) && keys.length === 0) { throw new errors.NoKeysPassedToDeleteError(); } const keysArray = Array.isArray(keys) ? keys : [keys]; this.collection._sync.startSync(); const now = /* @__PURE__ */ new Date(); const mutations = []; for (const key of keysArray) { if (!this.state.has(key)) { throw new errors.DeleteKeyNotFoundError(key); } const globalKey = this.generateGlobalKey(key, this.state.get(key)); const mutation = { mutationId: createMutationId(), original: this.state.get(key), modified: this.state.get(key), changes: this.state.get(key), globalKey, key, metadata: config2?.metadata, syncMetadata: state.syncedMetadata.get(key) || {}, optimistic: config2?.optimistic ?? true, type: `delete`, createdAt: now, updatedAt: now, collection: this.collection }; mutations.push(mutation); } if (ambientTransaction) { ambientTransaction.applyMutations(mutations); state.transactions.set(ambientTransaction.id, ambientTransaction); state.scheduleTransactionCleanup(ambientTransaction); state.recomputeOptimisticState(true); return ambientTransaction; } const localOnly = this.f7(mutations, `delete`); if (localOnly) return localOnly; const directOpTransaction = this.createTransaction({ autoCommit: true, metadata: { [transactionMetadata.DIRECT_TRANSACTION_METADATA_KEY]: true }, mutationFn: async (params) => { return this.config.onDelete({ transaction: params.transaction, collection: this.collection }); } }); directOpTransaction.applyMutations(mutations); this.cn(mutations); state.transactions.set(directOpTransaction.id, directOpTransaction); state.scheduleTransactionCleanup(directOpTransaction); state.recomputeOptimisticState(true); directOpTransaction.commit().catch(() => void 0); return directOpTransaction; }; this.id = id; this.config = config; } setDeps(deps) { this.lifecycle = deps.lifecycle; this.state = deps.state; this.collection = deps.collection; } setTransactionScope(transactionScope) { this.fR = transactionScope; } getActiveTransaction() { return this.fR ? this.fR.getActiveTransactionForCollection() : transactions.getActiveTransaction(); } createTransaction(config) { return this.fR ? this.fR.createTransaction(config) : transactions.createTransaction(config); } eX(schema) { if (schema && `~standard` in schema) { return schema; } throw new errors.InvalidSchemaError(); } validateData(data, type, key) { if (!this.config.schema) return data; const standardSchema = this.eX(this.config.schema); if (type === `update` && key) { const existingData = this.state.get(key); if (existingData && data && typeof data === `object` && typeof existingData === `object`) { const mergedData = { ...existingData, ...data }; const result2 = standardSchema[`~standard`].validate(mergedData); if (result2 instanceof Promise) { throw new errors.SchemaMustBeSynchronousError(); } if (`issues` in result2 && result2.issues) { const typedIssues = result2.issues.map((issue) => ({ message: issue.message, path: issue.path?.map((p) => String(p)) })); throw new errors.SchemaValidationError(type, typedIssues); } const validatedMergedData = result2.value; const modifiedKeys = Object.keys(data); const extractedChanges = Object.fromEntries( modifiedKeys.map((k) => [k, validatedMergedData[k]]) ); return extractedChanges; } } const result = standardSchema[`~standard`].validate(data); if (result instanceof Promise) { throw new errors.SchemaMustBeSynchronousError(); } if (`issues` in result && result.issues) { const typedIssues = result.issues.map((issue) => ({ message: issue.message, path: issue.path?.map((p) => String(p)) })); throw new errors.SchemaValidationError(type, typedIssues); } return result.value; } generateGlobalKey(key, item) { if (typeof key !== `string` && typeof key !== `number`) { if (typeof key === `undefined`) { throw new errors.UndefinedKeyError(item); } throw new errors.InvalidKeyError(key, item); } return `KEY::${this.id}/${key}`; } cn(mutations) { for (const mutation of mutations) { this.state.pendingLocalChanges.add(mutation.key); } } /** * A local-only Collection confirms its own writes. Without a user handler * for this operation type, and with no other transaction unsettled, write * the mutations as synced rows and return a completed transaction instead * of publishing an optimistic overlay and confirming it a tick later. */ f7(mutations, type) { const direct = this.state.localOnlyDirectWrite; if (!direct?.types.has(type)) return void 0; for (const transaction2 of this.state.transactions.values()) { if (transaction2.state === `pending` || transaction2.state === `persisting`) { return void 0; } } const transaction = this.createTransaction({ autoCommit: false, metadata: { [transactionMetadata.DIRECT_TRANSACTION_METADATA_KEY]: true }, mutationFn: () => Promise.resolve() }); transaction.applyMutations(mutations); direct.write(mutations); transaction.setState(`completed`); transaction.isPersisted.resolve(transaction); return transaction; } /** * Updates one or more items in the collection using a callback function */ update(keys, configOrCallback, maybeCallback) { if (typeof keys === `undefined`) { throw new errors.MissingUpdateArgumentError(); } const state = this.state; this.lifecycle.validateCollectionUsable(`update`); const ambientTransaction = this.getActiveTransaction(); if (!ambientTransaction && !this.config.onUpdate) { throw new errors.MissingUpdateHandlerError(); } const isArray = Array.isArray(keys); const keysArray = isArray ? keys : [keys]; if (isArray && keysArray.length === 0) { throw new errors.NoKeysPassedToUpdateError(); } const callback = typeof configOrCallback === `function` ? configOrCallback : maybeCallback; if (typeof callback !== `function`) throw new TypeError(); const config = typeof configOrCallback === `function` ? {} : configOrCallback; this.collection._sync.startSync(); const currentObjects = keysArray.map((key) => { const item = this.state.get(key); if (!item) { throw new errors.UpdateKeyNotFoundError(key); } return item; }); const changesArray = proxy.withFlatChangeTracking( currentObjects, callback, isArray ) ?? (isArray ? proxy.withArrayChangeTracking( currentObjects, callback ) : [ proxy.withChangeTracking( currentObjects[0], callback ) ]); const now = /* @__PURE__ */ new Date(); const mutations = keysArray.map((key, index) => { const itemChanges = changesArray[index]; if (!itemChanges || Object.keys(itemChanges).length === 0) { return null; } const originalItem = currentObjects[index]; const validatedUpdatePayload = this.validateData( itemChanges, `update`, key ); const modifiedItem = { ...originalItem, ...validatedUpdatePayload }; const originalItemId = this.config.getKey(originalItem); const modifiedItemId = this.config.getKey(modifiedItem); if (originalItemId !== modifiedItemId) { throw new errors.KeyUpdateNotAllowedError(originalItemId, modifiedItemId); } const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem); return { mutationId: createMutationId(), original: originalItem, modified: modifiedItem, // Pick the values from modifiedItem based on what's passed in - this is for cases // where a schema has default values or transforms. The modified data has the extra // default or transformed values but for changes, we just want to show the data that // was actually passed in. // Without a schema, validation returns the tracker's fresh change // object, which already holds exactly these values. changes: validatedUpdatePayload === itemChanges ? itemChanges : Object.fromEntries( Object.keys(itemChanges).map((k) => [ k, modifiedItem[k] ]) ), globalKey, key, metadata: config.metadata, syncMetadata: state.syncedMetadata.get(key) || {}, optimistic: config.optimistic ?? true, type: `update`, createdAt: now, updatedAt: now, collection: this.collection }; }).filter(Boolean); if (mutations.length === 0) { const emptyTransaction = this.createTransaction({ mutationFn: async () => { } }); emptyTransaction.commit().catch(() => void 0); state.scheduleTransactionCleanup(emptyTransaction); return emptyTransaction; } if (ambientTransaction) { ambientTransaction.applyMutations(mutations); state.transactions.set(ambientTransaction.id, ambientTransaction); state.scheduleTransactionCleanup(ambientTransaction); state.recomputeOptimisticState(true); return ambientTransaction; } const localOnly = this.f7(mutations, `update`); if (localOnly) return localOnly; const directOpTransaction = this.createTransaction({ metadata: { [transactionMetadata.DIRECT_TRANSACTION_METADATA_KEY]: true }, mutationFn: async (params) => { return this.config.onUpdate({ transaction: params.transaction, collection: this.collection }); } }); directOpTransaction.applyMutations(mutations); this.cn(mutations); state.transactions.set(directOpTransaction.id, directOpTransaction); state.scheduleTransactionCleanup(directOpTransaction); state.recomputeOptimisticState(true); directOpTransaction.commit().catch(() => void 0); return directOpTransaction; } } exports.CollectionMutationsManager = CollectionMutationsManager; //# sourceMappingURL=mutations.cjs.map