@tanstack/db 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/{change-events.cjs → collection/change-events.cjs} +13 -42
- package/dist/cjs/collection/change-events.cjs.map +1 -0
- package/dist/{esm/change-events.d.ts → cjs/collection/change-events.d.cts} +6 -6
- package/dist/cjs/collection/changes.cjs +108 -0
- package/dist/cjs/collection/changes.cjs.map +1 -0
- package/dist/cjs/collection/changes.d.cts +53 -0
- package/dist/cjs/{collection-events.cjs → collection/events.cjs} +7 -5
- package/dist/cjs/collection/events.cjs.map +1 -0
- package/dist/cjs/{collection-events.d.cts → collection/events.d.cts} +7 -4
- package/dist/cjs/collection/index.cjs +417 -0
- package/dist/cjs/collection/index.cjs.map +1 -0
- package/dist/{esm/collection.d.ts → cjs/collection/index.d.cts} +46 -184
- package/dist/cjs/collection/indexes.cjs +124 -0
- package/dist/cjs/collection/indexes.cjs.map +1 -0
- package/dist/cjs/collection/indexes.d.cts +47 -0
- package/dist/cjs/collection/lifecycle.cjs +150 -0
- package/dist/cjs/collection/lifecycle.cjs.map +1 -0
- package/dist/cjs/collection/lifecycle.d.cts +70 -0
- package/dist/cjs/collection/mutations.cjs +315 -0
- package/dist/cjs/collection/mutations.cjs.map +1 -0
- package/dist/cjs/collection/mutations.d.cts +33 -0
- package/dist/cjs/collection/state.cjs +597 -0
- package/dist/cjs/collection/state.cjs.map +1 -0
- package/dist/cjs/collection/state.d.cts +122 -0
- package/dist/cjs/collection/subscription.cjs +160 -0
- package/dist/cjs/collection/subscription.cjs.map +1 -0
- package/dist/cjs/collection/subscription.d.cts +57 -0
- package/dist/cjs/collection/sync.cjs +154 -0
- package/dist/cjs/collection/sync.cjs.map +1 -0
- package/dist/cjs/collection/sync.d.cts +34 -0
- package/dist/cjs/index.cjs +8 -8
- package/dist/cjs/index.d.cts +2 -2
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/auto-index.d.cts +1 -1
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +2 -2
- package/dist/cjs/indexes/btree-index.cjs +2 -2
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +1 -1
- package/dist/cjs/query/builder/index.cjs +2 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +1 -1
- package/dist/cjs/query/compiler/index.cjs +5 -2
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +3 -2
- package/dist/cjs/query/compiler/joins.cjs +22 -24
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +3 -2
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -1
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +29 -12
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +3 -0
- package/dist/cjs/query/live/collection-subscriber.cjs +43 -104
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
- package/dist/cjs/query/live-query-collection.cjs +2 -2
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +1 -1
- package/dist/cjs/transactions.cjs +3 -3
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +12 -10
- package/dist/{cjs/change-events.d.cts → esm/collection/change-events.d.ts} +6 -6
- package/dist/esm/{change-events.js → collection/change-events.js} +13 -42
- package/dist/esm/collection/change-events.js.map +1 -0
- package/dist/esm/collection/changes.d.ts +53 -0
- package/dist/esm/collection/changes.js +108 -0
- package/dist/esm/collection/changes.js.map +1 -0
- package/dist/esm/{collection-events.d.ts → collection/events.d.ts} +7 -4
- package/dist/esm/{collection-events.js → collection/events.js} +7 -5
- package/dist/esm/collection/events.js.map +1 -0
- package/dist/{cjs/collection.d.cts → esm/collection/index.d.ts} +46 -184
- package/dist/esm/collection/index.js +417 -0
- package/dist/esm/collection/index.js.map +1 -0
- package/dist/esm/collection/indexes.d.ts +47 -0
- package/dist/esm/collection/indexes.js +124 -0
- package/dist/esm/collection/indexes.js.map +1 -0
- package/dist/esm/collection/lifecycle.d.ts +70 -0
- package/dist/esm/collection/lifecycle.js +150 -0
- package/dist/esm/collection/lifecycle.js.map +1 -0
- package/dist/esm/collection/mutations.d.ts +33 -0
- package/dist/esm/collection/mutations.js +315 -0
- package/dist/esm/collection/mutations.js.map +1 -0
- package/dist/esm/collection/state.d.ts +122 -0
- package/dist/esm/collection/state.js +597 -0
- package/dist/esm/collection/state.js.map +1 -0
- package/dist/esm/collection/subscription.d.ts +57 -0
- package/dist/esm/collection/subscription.js +160 -0
- package/dist/esm/collection/subscription.js.map +1 -0
- package/dist/esm/collection/sync.d.ts +34 -0
- package/dist/esm/collection/sync.js +154 -0
- package/dist/esm/collection/sync.js.map +1 -0
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/indexes/auto-index.d.ts +1 -1
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +2 -2
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/btree-index.d.ts +1 -1
- package/dist/esm/indexes/btree-index.js +2 -2
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/proxy.js +1 -1
- package/dist/esm/query/builder/index.js +1 -1
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +1 -1
- package/dist/esm/query/compiler/index.d.ts +3 -2
- package/dist/esm/query/compiler/index.js +5 -2
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +3 -2
- package/dist/esm/query/compiler/joins.js +22 -24
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -1
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/ir.d.ts +1 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +3 -0
- package/dist/esm/query/live/collection-config-builder.js +29 -12
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
- package/dist/esm/query/live/collection-subscriber.js +43 -104
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +1 -1
- package/dist/esm/query/live-query-collection.js +1 -1
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/transactions.js +3 -3
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +12 -10
- package/package.json +2 -2
- package/src/{change-events.ts → collection/change-events.ts} +25 -39
- package/src/collection/changes.ts +163 -0
- package/src/{collection-events.ts → collection/events.ts} +8 -6
- package/src/collection/index.ts +808 -0
- package/src/collection/indexes.ts +172 -0
- package/src/collection/lifecycle.ts +221 -0
- package/src/collection/mutations.ts +535 -0
- package/src/collection/state.ts +866 -0
- package/src/collection/subscription.ts +239 -0
- package/src/collection/sync.ts +235 -0
- package/src/index.ts +2 -2
- package/src/indexes/auto-index.ts +1 -1
- package/src/indexes/base-index.ts +3 -3
- package/src/indexes/btree-index.ts +2 -2
- package/src/query/builder/index.ts +1 -1
- package/src/query/builder/types.ts +1 -1
- package/src/query/compiler/index.ts +7 -1
- package/src/query/compiler/joins.ts +28 -41
- package/src/query/compiler/order-by.ts +1 -1
- package/src/query/ir.ts +1 -1
- package/src/query/live/collection-config-builder.ts +48 -22
- package/src/query/live/collection-subscriber.ts +63 -168
- package/src/query/live-query-collection.ts +2 -2
- package/src/transactions.ts +3 -3
- package/src/types.ts +14 -15
- package/dist/cjs/change-events.cjs.map +0 -1
- package/dist/cjs/collection-events.cjs.map +0 -1
- package/dist/cjs/collection.cjs +0 -1625
- package/dist/cjs/collection.cjs.map +0 -1
- package/dist/esm/change-events.js.map +0 -1
- package/dist/esm/collection-events.js.map +0 -1
- package/dist/esm/collection.js +0 -1625
- package/dist/esm/collection.js.map +0 -1
- package/src/collection.ts +0 -2564
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transactions.cjs","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport {\n MissingMutationFunctionError,\n TransactionAlreadyCompletedRollbackError,\n TransactionNotPendingCommitError,\n TransactionNotPendingMutateError,\n} from \"./errors\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Merges two pending mutations for the same item within a transaction\n *\n * Merge behavior truth table:\n * - (insert, update) → insert (merge changes, keep empty original)\n * - (insert, delete) → null (cancel both mutations)\n * - (update, delete) → delete (delete dominates)\n * - (update, update) → update (replace with latest, union changes)\n * - (delete, delete) → delete (replace with latest)\n * - (insert, insert) → insert (replace with latest)\n *\n * Note: (delete, update) and (delete, insert) should never occur as the collection\n * layer prevents operations on deleted items within the same transaction.\n *\n * @param existing - The existing mutation in the transaction\n * @param incoming - The new mutation being applied\n * @returns The merged mutation, or null if both should be removed\n */\nfunction mergePendingMutations<T extends object>(\n existing: PendingMutation<T>,\n incoming: PendingMutation<T>\n): PendingMutation<T> | null {\n // Truth table implementation\n switch (`${existing.type}-${incoming.type}` as const) {\n case `insert-update`: {\n // Update after insert: keep as insert but merge changes\n // For insert-update, the key should remain the same since collections don't allow key changes\n return {\n ...existing,\n type: `insert` as const,\n original: {},\n modified: incoming.modified,\n changes: { ...existing.changes, ...incoming.changes },\n // Keep existing keys (key changes not allowed in updates)\n key: existing.key,\n globalKey: existing.globalKey,\n // Merge metadata (last-write-wins)\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n // Update tracking info\n mutationId: incoming.mutationId,\n updatedAt: incoming.updatedAt,\n }\n }\n\n case `insert-delete`:\n // Delete after insert: cancel both mutations\n return null\n\n case `update-delete`:\n // Delete after update: delete dominates\n return incoming\n\n case `update-update`: {\n // Update after update: replace with latest, union changes\n return {\n ...incoming,\n // Keep original from first update\n original: existing.original,\n // Union the changes from both updates\n changes: { ...existing.changes, ...incoming.changes },\n // Merge metadata\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n }\n }\n\n case `delete-delete`:\n case `insert-insert`:\n // Same type: replace with latest\n return incoming\n\n default: {\n // Exhaustiveness check\n const _exhaustive: never = `${existing.type}-${incoming.type}` as never\n throw new Error(`Unhandled mutation combination: ${_exhaustive}`)\n }\n }\n}\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<T extends object = Record<string, unknown>>(\n config: TransactionConfig<T>\n): Transaction<T> {\n const newTransaction = new Transaction<T>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\n * }\n */\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<T extends object = Record<string, unknown>> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T>>\n public isPersisted: Deferred<Transaction<T>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw new MissingMutationFunctionError()\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingMutateError()\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit().catch(() => {\n // Errors from autoCommit are handled via isPersisted.promise\n // This catch prevents unhandled promise rejections\n })\n }\n\n return this\n }\n\n /**\n * Apply new mutations to this transaction, intelligently merging with existing mutations\n *\n * When mutations operate on the same item (same globalKey), they are merged according to\n * the following rules:\n *\n * - **insert + update** → insert (merge changes, keep empty original)\n * - **insert + delete** → removed (mutations cancel each other out)\n * - **update + delete** → delete (delete dominates)\n * - **update + update** → update (union changes, keep first original)\n * - **same type** → replace with latest\n *\n * This merging reduces over-the-wire churn and keeps the optimistic local view\n * aligned with user intent.\n *\n * @param mutations - Array of new mutations to apply\n */\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n const existingMutation = this.mutations[existingIndex]!\n const mergeResult = mergePendingMutations(existingMutation, newMutation)\n\n if (mergeResult === null) {\n // Remove the mutation (e.g., delete after insert cancels both)\n this.mutations.splice(existingIndex, 1)\n } else {\n // Replace with merged mutation\n this.mutations[existingIndex] = mergeResult\n }\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw new TransactionAlreadyCompletedRollbackError()\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingCommitError()\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n this.isPersisted.resolve(this)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Preserve the original error for rethrowing\n const originalError =\n error instanceof Error ? error : new Error(String(error))\n\n // Update transaction with error information\n this.error = {\n message: originalError.message,\n error: originalError,\n }\n\n // rollback the transaction\n this.rollback()\n\n // Re-throw the original error to preserve identity and stack\n throw originalError\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":["MissingMutationFunctionError","createDeferred","TransactionNotPendingMutateError","TransactionAlreadyCompletedRollbackError","TransactionNotPendingCommitError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transactions.cjs","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport {\n MissingMutationFunctionError,\n TransactionAlreadyCompletedRollbackError,\n TransactionNotPendingCommitError,\n TransactionNotPendingMutateError,\n} from \"./errors\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Merges two pending mutations for the same item within a transaction\n *\n * Merge behavior truth table:\n * - (insert, update) → insert (merge changes, keep empty original)\n * - (insert, delete) → null (cancel both mutations)\n * - (update, delete) → delete (delete dominates)\n * - (update, update) → update (replace with latest, union changes)\n * - (delete, delete) → delete (replace with latest)\n * - (insert, insert) → insert (replace with latest)\n *\n * Note: (delete, update) and (delete, insert) should never occur as the collection\n * layer prevents operations on deleted items within the same transaction.\n *\n * @param existing - The existing mutation in the transaction\n * @param incoming - The new mutation being applied\n * @returns The merged mutation, or null if both should be removed\n */\nfunction mergePendingMutations<T extends object>(\n existing: PendingMutation<T>,\n incoming: PendingMutation<T>\n): PendingMutation<T> | null {\n // Truth table implementation\n switch (`${existing.type}-${incoming.type}` as const) {\n case `insert-update`: {\n // Update after insert: keep as insert but merge changes\n // For insert-update, the key should remain the same since collections don't allow key changes\n return {\n ...existing,\n type: `insert` as const,\n original: {},\n modified: incoming.modified,\n changes: { ...existing.changes, ...incoming.changes },\n // Keep existing keys (key changes not allowed in updates)\n key: existing.key,\n globalKey: existing.globalKey,\n // Merge metadata (last-write-wins)\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n // Update tracking info\n mutationId: incoming.mutationId,\n updatedAt: incoming.updatedAt,\n }\n }\n\n case `insert-delete`:\n // Delete after insert: cancel both mutations\n return null\n\n case `update-delete`:\n // Delete after update: delete dominates\n return incoming\n\n case `update-update`: {\n // Update after update: replace with latest, union changes\n return {\n ...incoming,\n // Keep original from first update\n original: existing.original,\n // Union the changes from both updates\n changes: { ...existing.changes, ...incoming.changes },\n // Merge metadata\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n }\n }\n\n case `delete-delete`:\n case `insert-insert`:\n // Same type: replace with latest\n return incoming\n\n default: {\n // Exhaustiveness check\n const _exhaustive: never = `${existing.type}-${incoming.type}` as never\n throw new Error(`Unhandled mutation combination: ${_exhaustive}`)\n }\n }\n}\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<T extends object = Record<string, unknown>>(\n config: TransactionConfig<T>\n): Transaction<T> {\n const newTransaction = new Transaction<T>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\n * }\n */\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<T extends object = Record<string, unknown>> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T>>\n public isPersisted: Deferred<Transaction<T>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw new MissingMutationFunctionError()\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingMutateError()\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit().catch(() => {\n // Errors from autoCommit are handled via isPersisted.promise\n // This catch prevents unhandled promise rejections\n })\n }\n\n return this\n }\n\n /**\n * Apply new mutations to this transaction, intelligently merging with existing mutations\n *\n * When mutations operate on the same item (same globalKey), they are merged according to\n * the following rules:\n *\n * - **insert + update** → insert (merge changes, keep empty original)\n * - **insert + delete** → removed (mutations cancel each other out)\n * - **update + delete** → delete (delete dominates)\n * - **update + update** → update (union changes, keep first original)\n * - **same type** → replace with latest\n *\n * This merging reduces over-the-wire churn and keeps the optimistic local view\n * aligned with user intent.\n *\n * @param mutations - Array of new mutations to apply\n */\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n const existingMutation = this.mutations[existingIndex]!\n const mergeResult = mergePendingMutations(existingMutation, newMutation)\n\n if (mergeResult === null) {\n // Remove the mutation (e.g., delete after insert cancels both)\n this.mutations.splice(existingIndex, 1)\n } else {\n // Replace with merged mutation\n this.mutations[existingIndex] = mergeResult\n }\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw new TransactionAlreadyCompletedRollbackError()\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection._state.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection._state.pendingSyncedTransactions.length > 0) {\n mutation.collection._state.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingCommitError()\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n this.isPersisted.resolve(this)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Preserve the original error for rethrowing\n const originalError =\n error instanceof Error ? error : new Error(String(error))\n\n // Update transaction with error information\n this.error = {\n message: originalError.message,\n error: originalError,\n }\n\n // rollback the transaction\n this.rollback()\n\n // Re-throw the original error to preserve identity and stack\n throw originalError\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":["MissingMutationFunctionError","createDeferred","TransactionNotPendingMutateError","TransactionAlreadyCompletedRollbackError","TransactionNotPendingCommitError"],"mappings":"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"}
|
package/dist/cjs/types.d.cts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { IStreamBuilder } from '@tanstack/db-ivm';
|
|
2
|
-
import { Collection } from './collection.
|
|
2
|
+
import { Collection } from './collection/index.js';
|
|
3
3
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
4
4
|
import { Transaction } from './transactions.cjs';
|
|
5
|
-
import {
|
|
6
|
-
import { BasicExpression } from './query/ir.js';
|
|
5
|
+
import { BasicExpression, OrderBy } from './query/ir.js';
|
|
7
6
|
/**
|
|
8
7
|
* Helper type to extract the output type from a standard schema
|
|
9
8
|
*
|
|
@@ -403,22 +402,25 @@ export type NamespacedAndKeyedStream = IStreamBuilder<KeyedNamespacedRow>;
|
|
|
403
402
|
/**
|
|
404
403
|
* Options for subscribing to collection changes
|
|
405
404
|
*/
|
|
406
|
-
export interface SubscribeChangesOptions
|
|
405
|
+
export interface SubscribeChangesOptions {
|
|
407
406
|
/** Whether to include the current state as initial changes */
|
|
408
407
|
includeInitialState?: boolean;
|
|
409
|
-
/** Filter changes using a where expression */
|
|
410
|
-
where?: (row: SingleRowRefProxy<T>) => any;
|
|
411
408
|
/** Pre-compiled expression for filtering changes */
|
|
412
409
|
whereExpression?: BasicExpression<boolean>;
|
|
413
410
|
}
|
|
411
|
+
export interface SubscribeChangesSnapshotOptions extends Omit<SubscribeChangesOptions, `includeInitialState`> {
|
|
412
|
+
orderBy?: OrderBy;
|
|
413
|
+
limit?: number;
|
|
414
|
+
}
|
|
414
415
|
/**
|
|
415
416
|
* Options for getting current state as changes
|
|
416
417
|
*/
|
|
417
|
-
export interface CurrentStateAsChangesOptions
|
|
418
|
-
/** Filter the current state using a where expression */
|
|
419
|
-
where?: (row: SingleRowRefProxy<T>) => any;
|
|
418
|
+
export interface CurrentStateAsChangesOptions {
|
|
420
419
|
/** Pre-compiled expression for filtering the current state */
|
|
421
|
-
|
|
420
|
+
where?: BasicExpression<boolean>;
|
|
421
|
+
orderBy?: OrderBy;
|
|
422
|
+
limit?: number;
|
|
423
|
+
optimizedOnly?: boolean;
|
|
422
424
|
}
|
|
423
425
|
/**
|
|
424
426
|
* Function type for listening to collection changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ChangeMessage, CurrentStateAsChangesOptions, SubscribeChangesOptions } from '
|
|
2
|
-
import { Collection } from './
|
|
3
|
-
import { SingleRowRefProxy } from '
|
|
4
|
-
import { BasicExpression } from '
|
|
1
|
+
import { ChangeMessage, CurrentStateAsChangesOptions, SubscribeChangesOptions } from '../types.js';
|
|
2
|
+
import { Collection } from './index.js';
|
|
3
|
+
import { SingleRowRefProxy } from '../query/builder/ref-proxy.js';
|
|
4
|
+
import { BasicExpression } from '../query/ir.js';
|
|
5
5
|
/**
|
|
6
6
|
* Interface for a collection-like object that provides the necessary methods
|
|
7
7
|
* for the change events system to work
|
|
@@ -27,7 +27,7 @@ export interface CollectionLike<T extends object = Record<string, unknown>, TKey
|
|
|
27
27
|
* whereExpression: eq(row.status, 'active')
|
|
28
28
|
* })
|
|
29
29
|
*/
|
|
30
|
-
export declare function currentStateAsChanges<T extends object, TKey extends string | number>(collection: CollectionLike<T, TKey>, options?: CurrentStateAsChangesOptions
|
|
30
|
+
export declare function currentStateAsChanges<T extends object, TKey extends string | number>(collection: CollectionLike<T, TKey>, options?: CurrentStateAsChangesOptions): Array<ChangeMessage<T>> | void;
|
|
31
31
|
/**
|
|
32
32
|
* Creates a filter function from a where callback
|
|
33
33
|
* @param whereCallback - The callback function that defines the filter condition
|
|
@@ -46,4 +46,4 @@ export declare function createFilterFunctionFromExpression<T extends object>(exp
|
|
|
46
46
|
* @param options - The subscription options containing the where clause
|
|
47
47
|
* @returns A filtered callback function
|
|
48
48
|
*/
|
|
49
|
-
export declare function createFilteredCallback<T extends object>(originalCallback: (changes: Array<ChangeMessage<T>>) => void, options: SubscribeChangesOptions
|
|
49
|
+
export declare function createFilteredCallback<T extends object>(originalCallback: (changes: Array<ChangeMessage<T>>) => void, options: SubscribeChangesOptions): (changes: Array<ChangeMessage<T>>) => void;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { optimizeExpressionWithIndexes } from "./utils/index-optimization.js";
|
|
1
|
+
import { compileSingleRowExpression } from "../query/compiler/evaluators.js";
|
|
2
|
+
import { optimizeExpressionWithIndexes } from "../utils/index-optimization.js";
|
|
4
3
|
function currentStateAsChanges(collection, options = {}) {
|
|
5
4
|
const collectFilteredResults = (filterFn) => {
|
|
6
5
|
const result = [];
|
|
@@ -15,20 +14,11 @@ function currentStateAsChanges(collection, options = {}) {
|
|
|
15
14
|
}
|
|
16
15
|
return result;
|
|
17
16
|
};
|
|
18
|
-
if (!options.where
|
|
17
|
+
if (!options.where) {
|
|
19
18
|
return collectFilteredResults();
|
|
20
19
|
}
|
|
21
20
|
try {
|
|
22
|
-
|
|
23
|
-
if (options.whereExpression) {
|
|
24
|
-
expression = options.whereExpression;
|
|
25
|
-
} else if (options.where) {
|
|
26
|
-
const singleRowRefProxy = createSingleRowRefProxy();
|
|
27
|
-
const whereExpression = options.where(singleRowRefProxy);
|
|
28
|
-
expression = toExpression(whereExpression);
|
|
29
|
-
} else {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
21
|
+
const expression = options.where;
|
|
32
22
|
const optimizationResult = optimizeExpressionWithIndexes(
|
|
33
23
|
expression,
|
|
34
24
|
collection.indexes
|
|
@@ -47,7 +37,10 @@ function currentStateAsChanges(collection, options = {}) {
|
|
|
47
37
|
}
|
|
48
38
|
return result;
|
|
49
39
|
} else {
|
|
50
|
-
|
|
40
|
+
if (options.optimizedOnly) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const filterFn = createFilterFunctionFromExpression(expression);
|
|
51
44
|
return collectFilteredResults(filterFn);
|
|
52
45
|
}
|
|
53
46
|
} catch (error) {
|
|
@@ -55,34 +48,13 @@ function currentStateAsChanges(collection, options = {}) {
|
|
|
55
48
|
`Error processing where clause, falling back to full scan:`,
|
|
56
49
|
error
|
|
57
50
|
);
|
|
58
|
-
const filterFn =
|
|
51
|
+
const filterFn = createFilterFunctionFromExpression(options.where);
|
|
52
|
+
if (options.optimizedOnly) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
59
55
|
return collectFilteredResults(filterFn);
|
|
60
56
|
}
|
|
61
57
|
}
|
|
62
|
-
function createFilterFunction(whereCallback) {
|
|
63
|
-
return (item) => {
|
|
64
|
-
try {
|
|
65
|
-
const singleRowRefProxy = createSingleRowRefProxy();
|
|
66
|
-
const whereExpression = whereCallback(singleRowRefProxy);
|
|
67
|
-
const expression = toExpression(whereExpression);
|
|
68
|
-
const evaluator = compileSingleRowExpression(expression);
|
|
69
|
-
const result = evaluator(item);
|
|
70
|
-
return result;
|
|
71
|
-
} catch {
|
|
72
|
-
try {
|
|
73
|
-
const simpleProxy = new Proxy(item, {
|
|
74
|
-
get(target, prop) {
|
|
75
|
-
return target[prop];
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
const result = whereCallback(simpleProxy);
|
|
79
|
-
return result;
|
|
80
|
-
} catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
58
|
function createFilterFunctionFromExpression(expression) {
|
|
87
59
|
return (item) => {
|
|
88
60
|
try {
|
|
@@ -95,7 +67,7 @@ function createFilterFunctionFromExpression(expression) {
|
|
|
95
67
|
};
|
|
96
68
|
}
|
|
97
69
|
function createFilteredCallback(originalCallback, options) {
|
|
98
|
-
const filterFn =
|
|
70
|
+
const filterFn = createFilterFunctionFromExpression(options.whereExpression);
|
|
99
71
|
return (changes) => {
|
|
100
72
|
const filteredChanges = [];
|
|
101
73
|
for (const change of changes) {
|
|
@@ -133,7 +105,6 @@ function createFilteredCallback(originalCallback, options) {
|
|
|
133
105
|
};
|
|
134
106
|
}
|
|
135
107
|
export {
|
|
136
|
-
createFilterFunction,
|
|
137
108
|
createFilterFunctionFromExpression,
|
|
138
109
|
createFilteredCallback,
|
|
139
110
|
currentStateAsChanges
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"change-events.js","sources":["../../../src/collection/change-events.ts"],"sourcesContent":["import {\n createSingleRowRefProxy,\n toExpression,\n} from \"../query/builder/ref-proxy\"\nimport { compileSingleRowExpression } from \"../query/compiler/evaluators.js\"\nimport { optimizeExpressionWithIndexes } from \"../utils/index-optimization.js\"\nimport type {\n ChangeMessage,\n CurrentStateAsChangesOptions,\n SubscribeChangesOptions,\n} from \"../types\"\nimport type { Collection } from \"./index.js\"\nimport type { SingleRowRefProxy } from \"../query/builder/ref-proxy\"\nimport type { BasicExpression } from \"../query/ir.js\"\n\n/**\n * Interface for a collection-like object that provides the necessary methods\n * for the change events system to work\n */\nexport interface CollectionLike<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> extends Pick<Collection<T, TKey>, `get` | `has` | `entries` | `indexes`> {}\n\n/**\n * Returns the current state of the collection as an array of changes\n * @param collection - The collection to get changes from\n * @param options - Options including optional where filter\n * @returns An array of changes\n * @example\n * // Get all items as changes\n * const allChanges = currentStateAsChanges(collection)\n *\n * // Get only items matching a condition\n * const activeChanges = currentStateAsChanges(collection, {\n * where: (row) => row.status === 'active'\n * })\n *\n * // Get only items using a pre-compiled expression\n * const activeChanges = currentStateAsChanges(collection, {\n * whereExpression: eq(row.status, 'active')\n * })\n */\nexport function currentStateAsChanges<\n T extends object,\n TKey extends string | number,\n>(\n collection: CollectionLike<T, TKey>,\n options: CurrentStateAsChangesOptions = {}\n): Array<ChangeMessage<T>> | void {\n // Helper function to collect filtered results\n const collectFilteredResults = (\n filterFn?: (value: T) => boolean\n ): Array<ChangeMessage<T>> => {\n const result: Array<ChangeMessage<T>> = []\n for (const [key, value] of collection.entries()) {\n // If no filter function is provided, include all items\n if (filterFn?.(value) ?? true) {\n result.push({\n type: `insert`,\n key,\n value,\n })\n }\n }\n return result\n }\n\n // TODO: handle orderBy and limit options\n // by calling optimizeOrderedLimit\n\n if (!options.where) {\n // No filtering, return all items\n return collectFilteredResults()\n }\n\n // There's a where clause, let's see if we can use an index\n try {\n const expression: BasicExpression<boolean> = options.where\n\n // Try to optimize the query using indexes\n const optimizationResult = optimizeExpressionWithIndexes(\n expression,\n collection.indexes\n )\n\n if (optimizationResult.canOptimize) {\n // Use index optimization\n const result: Array<ChangeMessage<T>> = []\n for (const key of optimizationResult.matchingKeys) {\n const value = collection.get(key)\n if (value !== undefined) {\n result.push({\n type: `insert`,\n key,\n value,\n })\n }\n }\n return result\n } else {\n if (options.optimizedOnly) {\n return\n }\n\n const filterFn = createFilterFunctionFromExpression(expression)\n return collectFilteredResults(filterFn)\n }\n } catch (error) {\n // If anything goes wrong with the where clause, fall back to full scan\n console.warn(\n `Error processing where clause, falling back to full scan:`,\n error\n )\n\n const filterFn = createFilterFunctionFromExpression(options.where)\n\n if (options.optimizedOnly) {\n return\n }\n\n return collectFilteredResults(filterFn)\n }\n}\n\n/**\n * Creates a filter function from a where callback\n * @param whereCallback - The callback function that defines the filter condition\n * @returns A function that takes an item and returns true if it matches the filter\n */\nexport function createFilterFunction<T extends object>(\n whereCallback: (row: SingleRowRefProxy<T>) => any\n): (item: T) => boolean {\n return (item: T): boolean => {\n try {\n // First try the RefProxy approach for query builder functions\n const singleRowRefProxy = createSingleRowRefProxy<T>()\n const whereExpression = whereCallback(singleRowRefProxy)\n const expression = toExpression(whereExpression)\n const evaluator = compileSingleRowExpression(expression)\n const result = evaluator(item as Record<string, unknown>)\n // WHERE clauses should always evaluate to boolean predicates (Kevin's feedback)\n return result\n } catch {\n // If RefProxy approach fails (e.g., arithmetic operations), fall back to direct evaluation\n try {\n // Create a simple proxy that returns actual values for arithmetic operations\n const simpleProxy = new Proxy(item as any, {\n get(target, prop) {\n return target[prop]\n },\n }) as SingleRowRefProxy<T>\n\n const result = whereCallback(simpleProxy)\n return result\n } catch {\n // If both approaches fail, exclude the item\n return false\n }\n }\n }\n}\n\n/**\n * Creates a filter function from a pre-compiled expression\n * @param expression - The pre-compiled expression to evaluate\n * @returns A function that takes an item and returns true if it matches the filter\n */\nexport function createFilterFunctionFromExpression<T extends object>(\n expression: BasicExpression<boolean>\n): (item: T) => boolean {\n return (item: T): boolean => {\n try {\n const evaluator = compileSingleRowExpression(expression)\n const result = evaluator(item as Record<string, unknown>)\n return Boolean(result)\n } catch {\n // If evaluation fails, exclude the item\n return false\n }\n }\n}\n\n/**\n * Creates a filtered callback that only calls the original callback with changes that match the where clause\n * @param originalCallback - The original callback to filter\n * @param options - The subscription options containing the where clause\n * @returns A filtered callback function\n */\nexport function createFilteredCallback<T extends object>(\n originalCallback: (changes: Array<ChangeMessage<T>>) => void,\n options: SubscribeChangesOptions\n): (changes: Array<ChangeMessage<T>>) => void {\n const filterFn = createFilterFunctionFromExpression(options.whereExpression!)\n\n return (changes: Array<ChangeMessage<T>>) => {\n const filteredChanges: Array<ChangeMessage<T>> = []\n\n for (const change of changes) {\n if (change.type === `insert`) {\n // For inserts, check if the new value matches the filter\n if (filterFn(change.value)) {\n filteredChanges.push(change)\n }\n } else if (change.type === `update`) {\n // For updates, we need to check both old and new values\n const newValueMatches = filterFn(change.value)\n const oldValueMatches = change.previousValue\n ? filterFn(change.previousValue)\n : false\n\n if (newValueMatches && oldValueMatches) {\n // Both old and new match: emit update\n filteredChanges.push(change)\n } else if (newValueMatches && !oldValueMatches) {\n // New matches but old didn't: emit insert\n filteredChanges.push({\n ...change,\n type: `insert`,\n })\n } else if (!newValueMatches && oldValueMatches) {\n // Old matched but new doesn't: emit delete\n filteredChanges.push({\n ...change,\n type: `delete`,\n value: change.previousValue!, // Use the previous value for the delete\n })\n }\n // If neither matches, don't emit anything\n } else {\n // For deletes, include if the previous value would have matched\n // (so subscribers know something they were tracking was deleted)\n if (filterFn(change.value)) {\n filteredChanges.push(change)\n }\n }\n }\n\n // Always call the original callback if we have filtered changes OR\n // if the original changes array was empty (which indicates a ready signal)\n if (filteredChanges.length > 0 || changes.length === 0) {\n originalCallback(filteredChanges)\n }\n }\n}\n"],"names":[],"mappings":";;AA2CO,SAAS,sBAId,YACA,UAAwC,IACR;AAEhC,QAAM,yBAAyB,CAC7B,aAC4B;AAC5B,UAAM,SAAkC,CAAA;AACxC,eAAW,CAAC,KAAK,KAAK,KAAK,WAAW,WAAW;AAE/C,WAAI,qCAAW,WAAU,MAAM;AAC7B,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN;AAAA,UACA;AAAA,QAAA,CACD;AAAA,MACH;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAKA,MAAI,CAAC,QAAQ,OAAO;AAElB,WAAO,uBAAA;AAAA,EACT;AAGA,MAAI;AACF,UAAM,aAAuC,QAAQ;AAGrD,UAAM,qBAAqB;AAAA,MACzB;AAAA,MACA,WAAW;AAAA,IAAA;AAGb,QAAI,mBAAmB,aAAa;AAElC,YAAM,SAAkC,CAAA;AACxC,iBAAW,OAAO,mBAAmB,cAAc;AACjD,cAAM,QAAQ,WAAW,IAAI,GAAG;AAChC,YAAI,UAAU,QAAW;AACvB,iBAAO,KAAK;AAAA,YACV,MAAM;AAAA,YACN;AAAA,YACA;AAAA,UAAA,CACD;AAAA,QACH;AAAA,MACF;AACA,aAAO;AAAA,IACT,OAAO;AACL,UAAI,QAAQ,eAAe;AACzB;AAAA,MACF;AAEA,YAAM,WAAW,mCAAmC,UAAU;AAC9D,aAAO,uBAAuB,QAAQ;AAAA,IACxC;AAAA,EACF,SAAS,OAAO;AAEd,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,IAAA;AAGF,UAAM,WAAW,mCAAmC,QAAQ,KAAK;AAEjE,QAAI,QAAQ,eAAe;AACzB;AAAA,IACF;AAEA,WAAO,uBAAuB,QAAQ;AAAA,EACxC;AACF;AA6CO,SAAS,mCACd,YACsB;AACtB,SAAO,CAAC,SAAqB;AAC3B,QAAI;AACF,YAAM,YAAY,2BAA2B,UAAU;AACvD,YAAM,SAAS,UAAU,IAA+B;AACxD,aAAO,QAAQ,MAAM;AAAA,IACvB,QAAQ;AAEN,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAQO,SAAS,uBACd,kBACA,SAC4C;AAC5C,QAAM,WAAW,mCAAmC,QAAQ,eAAgB;AAE5E,SAAO,CAAC,YAAqC;AAC3C,UAAM,kBAA2C,CAAA;AAEjD,eAAW,UAAU,SAAS;AAC5B,UAAI,OAAO,SAAS,UAAU;AAE5B,YAAI,SAAS,OAAO,KAAK,GAAG;AAC1B,0BAAgB,KAAK,MAAM;AAAA,QAC7B;AAAA,MACF,WAAW,OAAO,SAAS,UAAU;AAEnC,cAAM,kBAAkB,SAAS,OAAO,KAAK;AAC7C,cAAM,kBAAkB,OAAO,gBAC3B,SAAS,OAAO,aAAa,IAC7B;AAEJ,YAAI,mBAAmB,iBAAiB;AAEtC,0BAAgB,KAAK,MAAM;AAAA,QAC7B,WAAW,mBAAmB,CAAC,iBAAiB;AAE9C,0BAAgB,KAAK;AAAA,YACnB,GAAG;AAAA,YACH,MAAM;AAAA,UAAA,CACP;AAAA,QACH,WAAW,CAAC,mBAAmB,iBAAiB;AAE9C,0BAAgB,KAAK;AAAA,YACnB,GAAG;AAAA,YACH,MAAM;AAAA,YACN,OAAO,OAAO;AAAA;AAAA,UAAA,CACf;AAAA,QACH;AAAA,MAEF,OAAO;AAGL,YAAI,SAAS,OAAO,KAAK,GAAG;AAC1B,0BAAgB,KAAK,MAAM;AAAA,QAC7B;AAAA,MACF;AAAA,IACF;AAIA,QAAI,gBAAgB,SAAS,KAAK,QAAQ,WAAW,GAAG;AACtD,uBAAiB,eAAe;AAAA,IAClC;AAAA,EACF;AACF;"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CollectionSubscription } from './subscription.js';
|
|
2
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3
|
+
import { ChangeMessage, SubscribeChangesOptions } from '../types.js';
|
|
4
|
+
import { CollectionLifecycleManager } from './lifecycle.js';
|
|
5
|
+
import { CollectionSyncManager } from './sync.js';
|
|
6
|
+
import { CollectionEventsManager } from './events.js';
|
|
7
|
+
import { CollectionImpl } from './index.js';
|
|
8
|
+
export declare class CollectionChangesManager<TOutput extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = StandardSchemaV1, TInput extends object = TOutput> {
|
|
9
|
+
private lifecycle;
|
|
10
|
+
private sync;
|
|
11
|
+
private events;
|
|
12
|
+
private collection;
|
|
13
|
+
activeSubscribersCount: number;
|
|
14
|
+
changeSubscriptions: Set<CollectionSubscription>;
|
|
15
|
+
batchedEvents: Array<ChangeMessage<TOutput, TKey>>;
|
|
16
|
+
shouldBatchEvents: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new CollectionChangesManager instance
|
|
19
|
+
*/
|
|
20
|
+
constructor();
|
|
21
|
+
setDeps(deps: {
|
|
22
|
+
lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>;
|
|
23
|
+
sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>;
|
|
24
|
+
events: CollectionEventsManager;
|
|
25
|
+
collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>;
|
|
26
|
+
}): void;
|
|
27
|
+
/**
|
|
28
|
+
* Emit an empty ready event to notify subscribers that the collection is ready
|
|
29
|
+
* This bypasses the normal empty array check in emitEvents
|
|
30
|
+
*/
|
|
31
|
+
emitEmptyReadyEvent(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Emit events either immediately or batch them for later emission
|
|
34
|
+
*/
|
|
35
|
+
emitEvents(changes: Array<ChangeMessage<TOutput, TKey>>, forceEmit?: boolean): void;
|
|
36
|
+
/**
|
|
37
|
+
* Subscribe to changes in the collection
|
|
38
|
+
*/
|
|
39
|
+
subscribeChanges(callback: (changes: Array<ChangeMessage<TOutput>>) => void, options?: SubscribeChangesOptions): CollectionSubscription;
|
|
40
|
+
/**
|
|
41
|
+
* Increment the active subscribers count and start sync if needed
|
|
42
|
+
*/
|
|
43
|
+
private addSubscriber;
|
|
44
|
+
/**
|
|
45
|
+
* Decrement the active subscribers count and start GC timer if needed
|
|
46
|
+
*/
|
|
47
|
+
private removeSubscriber;
|
|
48
|
+
/**
|
|
49
|
+
* Clean up the collection by stopping sync and clearing data
|
|
50
|
+
* This can be called manually or automatically by garbage collection
|
|
51
|
+
*/
|
|
52
|
+
cleanup(): void;
|
|
53
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { NegativeActiveSubscribersError } from "../errors.js";
|
|
2
|
+
import { CollectionSubscription } from "./subscription.js";
|
|
3
|
+
class CollectionChangesManager {
|
|
4
|
+
/**
|
|
5
|
+
* Creates a new CollectionChangesManager instance
|
|
6
|
+
*/
|
|
7
|
+
constructor() {
|
|
8
|
+
this.activeSubscribersCount = 0;
|
|
9
|
+
this.changeSubscriptions = /* @__PURE__ */ new Set();
|
|
10
|
+
this.batchedEvents = [];
|
|
11
|
+
this.shouldBatchEvents = false;
|
|
12
|
+
}
|
|
13
|
+
setDeps(deps) {
|
|
14
|
+
this.lifecycle = deps.lifecycle;
|
|
15
|
+
this.sync = deps.sync;
|
|
16
|
+
this.events = deps.events;
|
|
17
|
+
this.collection = deps.collection;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Emit an empty ready event to notify subscribers that the collection is ready
|
|
21
|
+
* This bypasses the normal empty array check in emitEvents
|
|
22
|
+
*/
|
|
23
|
+
emitEmptyReadyEvent() {
|
|
24
|
+
for (const subscription of this.changeSubscriptions) {
|
|
25
|
+
subscription.emitEvents([]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Emit events either immediately or batch them for later emission
|
|
30
|
+
*/
|
|
31
|
+
emitEvents(changes, forceEmit = false) {
|
|
32
|
+
if (this.shouldBatchEvents && !forceEmit) {
|
|
33
|
+
this.batchedEvents.push(...changes);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let eventsToEmit = changes;
|
|
37
|
+
if (this.batchedEvents.length > 0 && forceEmit) {
|
|
38
|
+
eventsToEmit = [...this.batchedEvents, ...changes];
|
|
39
|
+
this.batchedEvents = [];
|
|
40
|
+
this.shouldBatchEvents = false;
|
|
41
|
+
}
|
|
42
|
+
if (eventsToEmit.length === 0) return;
|
|
43
|
+
for (const subscription of this.changeSubscriptions) {
|
|
44
|
+
subscription.emitEvents(eventsToEmit);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Subscribe to changes in the collection
|
|
49
|
+
*/
|
|
50
|
+
subscribeChanges(callback, options = {}) {
|
|
51
|
+
this.addSubscriber();
|
|
52
|
+
const subscription = new CollectionSubscription(this.collection, callback, {
|
|
53
|
+
...options,
|
|
54
|
+
onUnsubscribe: () => {
|
|
55
|
+
this.removeSubscriber();
|
|
56
|
+
this.changeSubscriptions.delete(subscription);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (options.includeInitialState) {
|
|
60
|
+
subscription.requestSnapshot();
|
|
61
|
+
}
|
|
62
|
+
this.changeSubscriptions.add(subscription);
|
|
63
|
+
return subscription;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Increment the active subscribers count and start sync if needed
|
|
67
|
+
*/
|
|
68
|
+
addSubscriber() {
|
|
69
|
+
const previousSubscriberCount = this.activeSubscribersCount;
|
|
70
|
+
this.activeSubscribersCount++;
|
|
71
|
+
this.lifecycle.cancelGCTimer();
|
|
72
|
+
if (this.lifecycle.status === `cleaned-up` || this.lifecycle.status === `idle`) {
|
|
73
|
+
this.sync.startSync();
|
|
74
|
+
}
|
|
75
|
+
this.events.emitSubscribersChange(
|
|
76
|
+
this.activeSubscribersCount,
|
|
77
|
+
previousSubscriberCount
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Decrement the active subscribers count and start GC timer if needed
|
|
82
|
+
*/
|
|
83
|
+
removeSubscriber() {
|
|
84
|
+
const previousSubscriberCount = this.activeSubscribersCount;
|
|
85
|
+
this.activeSubscribersCount--;
|
|
86
|
+
if (this.activeSubscribersCount === 0) {
|
|
87
|
+
this.lifecycle.startGCTimer();
|
|
88
|
+
} else if (this.activeSubscribersCount < 0) {
|
|
89
|
+
throw new NegativeActiveSubscribersError();
|
|
90
|
+
}
|
|
91
|
+
this.events.emitSubscribersChange(
|
|
92
|
+
this.activeSubscribersCount,
|
|
93
|
+
previousSubscriberCount
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Clean up the collection by stopping sync and clearing data
|
|
98
|
+
* This can be called manually or automatically by garbage collection
|
|
99
|
+
*/
|
|
100
|
+
cleanup() {
|
|
101
|
+
this.batchedEvents = [];
|
|
102
|
+
this.shouldBatchEvents = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
CollectionChangesManager
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=changes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changes.js","sources":["../../../src/collection/changes.ts"],"sourcesContent":["import { NegativeActiveSubscribersError } from \"../errors\"\nimport { CollectionSubscription } from \"./subscription.js\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type { ChangeMessage, SubscribeChangesOptions } from \"../types\"\nimport type { CollectionLifecycleManager } from \"./lifecycle.js\"\nimport type { CollectionSyncManager } from \"./sync.js\"\nimport type { CollectionEventsManager } from \"./events.js\"\nimport type { CollectionImpl } from \"./index.js\"\n\nexport class CollectionChangesManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n\n public activeSubscribersCount = 0\n public changeSubscriptions = new Set<CollectionSubscription>()\n public batchedEvents: Array<ChangeMessage<TOutput, TKey>> = []\n public shouldBatchEvents = false\n\n /**\n * Creates a new CollectionChangesManager instance\n */\n constructor() {}\n\n public setDeps(deps: {\n lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n }) {\n this.lifecycle = deps.lifecycle\n this.sync = deps.sync\n this.events = deps.events\n this.collection = deps.collection\n }\n\n /**\n * Emit an empty ready event to notify subscribers that the collection is ready\n * This bypasses the normal empty array check in emitEvents\n */\n public emitEmptyReadyEvent(): void {\n // Emit empty array directly to all subscribers\n for (const subscription of this.changeSubscriptions) {\n subscription.emitEvents([])\n }\n }\n\n /**\n * Emit events either immediately or batch them for later emission\n */\n public emitEvents(\n changes: Array<ChangeMessage<TOutput, TKey>>,\n forceEmit = false\n ): void {\n // Skip batching for user actions (forceEmit=true) to keep UI responsive\n if (this.shouldBatchEvents && !forceEmit) {\n // Add events to the batch\n this.batchedEvents.push(...changes)\n return\n }\n\n // Either we're not batching, or we're forcing emission (user action or ending batch cycle)\n let eventsToEmit = changes\n\n // If we have batched events and this is a forced emit, combine them\n if (this.batchedEvents.length > 0 && forceEmit) {\n eventsToEmit = [...this.batchedEvents, ...changes]\n this.batchedEvents = []\n this.shouldBatchEvents = false\n }\n\n if (eventsToEmit.length === 0) return\n\n // Emit to all listeners\n for (const subscription of this.changeSubscriptions) {\n subscription.emitEvents(eventsToEmit)\n }\n }\n\n /**\n * Subscribe to changes in the collection\n */\n public subscribeChanges(\n callback: (changes: Array<ChangeMessage<TOutput>>) => void,\n options: SubscribeChangesOptions = {}\n ): CollectionSubscription {\n // Start sync and track subscriber\n this.addSubscriber()\n\n const subscription = new CollectionSubscription(this.collection, callback, {\n ...options,\n onUnsubscribe: () => {\n this.removeSubscriber()\n this.changeSubscriptions.delete(subscription)\n },\n })\n\n if (options.includeInitialState) {\n subscription.requestSnapshot()\n }\n\n // Add to batched listeners\n this.changeSubscriptions.add(subscription)\n\n return subscription\n }\n\n /**\n * Increment the active subscribers count and start sync if needed\n */\n private addSubscriber(): void {\n const previousSubscriberCount = this.activeSubscribersCount\n this.activeSubscribersCount++\n this.lifecycle.cancelGCTimer()\n\n // Start sync if collection was cleaned up\n if (\n this.lifecycle.status === `cleaned-up` ||\n this.lifecycle.status === `idle`\n ) {\n this.sync.startSync()\n }\n\n this.events.emitSubscribersChange(\n this.activeSubscribersCount,\n previousSubscriberCount\n )\n }\n\n /**\n * Decrement the active subscribers count and start GC timer if needed\n */\n private removeSubscriber(): void {\n const previousSubscriberCount = this.activeSubscribersCount\n this.activeSubscribersCount--\n\n if (this.activeSubscribersCount === 0) {\n this.lifecycle.startGCTimer()\n } else if (this.activeSubscribersCount < 0) {\n throw new NegativeActiveSubscribersError()\n }\n\n this.events.emitSubscribersChange(\n this.activeSubscribersCount,\n previousSubscriberCount\n )\n }\n\n /**\n * Clean up the collection by stopping sync and clearing data\n * This can be called manually or automatically by garbage collection\n */\n public cleanup(): void {\n this.batchedEvents = []\n this.shouldBatchEvents = false\n }\n}\n"],"names":[],"mappings":";;AASO,MAAM,yBAKX;AAAA;AAAA;AAAA;AAAA,EAcA,cAAc;AARd,SAAO,yBAAyB;AAChC,SAAO,0CAA0B,IAAA;AACjC,SAAO,gBAAqD,CAAA;AAC5D,SAAO,oBAAoB;AAAA,EAKZ;AAAA,EAER,QAAQ,MAKZ;AACD,SAAK,YAAY,KAAK;AACtB,SAAK,OAAO,KAAK;AACjB,SAAK,SAAS,KAAK;AACnB,SAAK,aAAa,KAAK;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,sBAA4B;AAEjC,eAAW,gBAAgB,KAAK,qBAAqB;AACnD,mBAAa,WAAW,EAAE;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,WACL,SACA,YAAY,OACN;AAEN,QAAI,KAAK,qBAAqB,CAAC,WAAW;AAExC,WAAK,cAAc,KAAK,GAAG,OAAO;AAClC;AAAA,IACF;AAGA,QAAI,eAAe;AAGnB,QAAI,KAAK,cAAc,SAAS,KAAK,WAAW;AAC9C,qBAAe,CAAC,GAAG,KAAK,eAAe,GAAG,OAAO;AACjD,WAAK,gBAAgB,CAAA;AACrB,WAAK,oBAAoB;AAAA,IAC3B;AAEA,QAAI,aAAa,WAAW,EAAG;AAG/B,eAAW,gBAAgB,KAAK,qBAAqB;AACnD,mBAAa,WAAW,YAAY;AAAA,IACtC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,iBACL,UACA,UAAmC,IACX;AAExB,SAAK,cAAA;AAEL,UAAM,eAAe,IAAI,uBAAuB,KAAK,YAAY,UAAU;AAAA,MACzE,GAAG;AAAA,MACH,eAAe,MAAM;AACnB,aAAK,iBAAA;AACL,aAAK,oBAAoB,OAAO,YAAY;AAAA,MAC9C;AAAA,IAAA,CACD;AAED,QAAI,QAAQ,qBAAqB;AAC/B,mBAAa,gBAAA;AAAA,IACf;AAGA,SAAK,oBAAoB,IAAI,YAAY;AAEzC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,gBAAsB;AAC5B,UAAM,0BAA0B,KAAK;AACrC,SAAK;AACL,SAAK,UAAU,cAAA;AAGf,QACE,KAAK,UAAU,WAAW,gBAC1B,KAAK,UAAU,WAAW,QAC1B;AACA,WAAK,KAAK,UAAA;AAAA,IACZ;AAEA,SAAK,OAAO;AAAA,MACV,KAAK;AAAA,MACL;AAAA,IAAA;AAAA,EAEJ;AAAA;AAAA;AAAA;AAAA,EAKQ,mBAAyB;AAC/B,UAAM,0BAA0B,KAAK;AACrC,SAAK;AAEL,QAAI,KAAK,2BAA2B,GAAG;AACrC,WAAK,UAAU,aAAA;AAAA,IACjB,WAAW,KAAK,yBAAyB,GAAG;AAC1C,YAAM,IAAI,+BAAA;AAAA,IACZ;AAEA,SAAK,OAAO;AAAA,MACV,KAAK;AAAA,MACL;AAAA,IAAA;AAAA,EAEJ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,UAAgB;AACrB,SAAK,gBAAgB,CAAA;AACrB,SAAK,oBAAoB;AAAA,EAC3B;AACF;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Collection } from './
|
|
2
|
-
import { CollectionStatus } from '
|
|
1
|
+
import { Collection } from './index.js';
|
|
2
|
+
import { CollectionStatus } from '../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Event emitted when the collection status changes
|
|
5
5
|
*/
|
|
@@ -35,10 +35,13 @@ export type AllCollectionEvents = {
|
|
|
35
35
|
};
|
|
36
36
|
export type CollectionEvent = AllCollectionEvents[keyof AllCollectionEvents] | CollectionStatusChangeEvent | CollectionSubscribersChangeEvent;
|
|
37
37
|
export type CollectionEventHandler<T extends keyof AllCollectionEvents> = (event: AllCollectionEvents[T]) => void;
|
|
38
|
-
export declare class
|
|
38
|
+
export declare class CollectionEventsManager {
|
|
39
39
|
private collection;
|
|
40
40
|
private listeners;
|
|
41
|
-
constructor(
|
|
41
|
+
constructor();
|
|
42
|
+
setDeps(deps: {
|
|
43
|
+
collection: Collection<any, any, any, any, any>;
|
|
44
|
+
}): void;
|
|
42
45
|
on<T extends keyof AllCollectionEvents>(event: T, callback: CollectionEventHandler<T>): () => void;
|
|
43
46
|
once<T extends keyof AllCollectionEvents>(event: T, callback: CollectionEventHandler<T>): () => void;
|
|
44
47
|
off<T extends keyof AllCollectionEvents>(event: T, callback: CollectionEventHandler<T>): void;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
class
|
|
2
|
-
constructor(
|
|
1
|
+
class CollectionEventsManager {
|
|
2
|
+
constructor() {
|
|
3
3
|
this.listeners = /* @__PURE__ */ new Map();
|
|
4
|
-
|
|
4
|
+
}
|
|
5
|
+
setDeps(deps) {
|
|
6
|
+
this.collection = deps.collection;
|
|
5
7
|
}
|
|
6
8
|
on(event, callback) {
|
|
7
9
|
if (!this.listeners.has(event)) {
|
|
@@ -83,6 +85,6 @@ class CollectionEvents {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
export {
|
|
86
|
-
|
|
88
|
+
CollectionEventsManager
|
|
87
89
|
};
|
|
88
|
-
//# sourceMappingURL=
|
|
90
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sources":["../../../src/collection/events.ts"],"sourcesContent":["import type { Collection } from \"./index.js\"\nimport type { CollectionStatus } from \"../types.js\"\n\n/**\n * Event emitted when the collection status changes\n */\nexport interface CollectionStatusChangeEvent {\n type: `status:change`\n collection: Collection\n previousStatus: CollectionStatus\n status: CollectionStatus\n}\n\n/**\n * Event emitted when the collection status changes to a specific status\n */\nexport interface CollectionStatusEvent<T extends CollectionStatus> {\n type: `status:${T}`\n collection: Collection\n previousStatus: CollectionStatus\n status: T\n}\n\n/**\n * Event emitted when the number of subscribers to the collection changes\n */\nexport interface CollectionSubscribersChangeEvent {\n type: `subscribers:change`\n collection: Collection\n previousSubscriberCount: number\n subscriberCount: number\n}\n\nexport type AllCollectionEvents = {\n \"status:change\": CollectionStatusChangeEvent\n \"subscribers:change\": CollectionSubscribersChangeEvent\n} & {\n [K in CollectionStatus as `status:${K}`]: CollectionStatusEvent<K>\n}\n\nexport type CollectionEvent =\n | AllCollectionEvents[keyof AllCollectionEvents]\n | CollectionStatusChangeEvent\n | CollectionSubscribersChangeEvent\n\nexport type CollectionEventHandler<T extends keyof AllCollectionEvents> = (\n event: AllCollectionEvents[T]\n) => void\n\nexport class CollectionEventsManager {\n private collection!: Collection<any, any, any, any, any>\n private listeners = new Map<\n keyof AllCollectionEvents,\n Set<CollectionEventHandler<any>>\n >()\n\n constructor() {}\n\n setDeps(deps: { collection: Collection<any, any, any, any, any> }) {\n this.collection = deps.collection\n }\n\n on<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>\n ) {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set())\n }\n this.listeners.get(event)!.add(callback)\n\n return () => {\n this.listeners.get(event)!.delete(callback)\n }\n }\n\n once<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>\n ) {\n const unsubscribe = this.on(event, (eventPayload) => {\n callback(eventPayload)\n unsubscribe()\n })\n return unsubscribe\n }\n\n off<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>\n ) {\n this.listeners.get(event)?.delete(callback)\n }\n\n waitFor<T extends keyof AllCollectionEvents>(\n event: T,\n timeout?: number\n ): Promise<AllCollectionEvents[T]> {\n return new Promise((resolve, reject) => {\n let timeoutId: NodeJS.Timeout | undefined\n const unsubscribe = this.on(event, (eventPayload) => {\n if (timeoutId) {\n clearTimeout(timeoutId)\n timeoutId = undefined\n }\n resolve(eventPayload)\n unsubscribe()\n })\n if (timeout) {\n timeoutId = setTimeout(() => {\n timeoutId = undefined\n unsubscribe()\n reject(new Error(`Timeout waiting for event ${event}`))\n }, timeout)\n }\n })\n }\n\n emit<T extends keyof AllCollectionEvents>(\n event: T,\n eventPayload: AllCollectionEvents[T]\n ) {\n this.listeners.get(event)?.forEach((listener) => {\n try {\n listener(eventPayload)\n } catch (error) {\n // Re-throw in a microtask to surface the error\n queueMicrotask(() => {\n throw error\n })\n }\n })\n }\n\n emitStatusChange<T extends CollectionStatus>(\n status: T,\n previousStatus: CollectionStatus\n ) {\n this.emit(`status:change`, {\n type: `status:change`,\n collection: this.collection,\n previousStatus,\n status,\n })\n\n // Emit specific status event using type assertion\n const eventKey: `status:${T}` = `status:${status}`\n this.emit(eventKey, {\n type: eventKey,\n collection: this.collection,\n previousStatus,\n status,\n } as AllCollectionEvents[`status:${T}`])\n }\n\n emitSubscribersChange(\n subscriberCount: number,\n previousSubscriberCount: number\n ) {\n this.emit(`subscribers:change`, {\n type: `subscribers:change`,\n collection: this.collection,\n previousSubscriberCount,\n subscriberCount,\n })\n }\n\n cleanup() {\n this.listeners.clear()\n }\n}\n"],"names":[],"mappings":"AAiDO,MAAM,wBAAwB;AAAA,EAOnC,cAAc;AALd,SAAQ,gCAAgB,IAAA;AAAA,EAKT;AAAA,EAEf,QAAQ,MAA2D;AACjE,SAAK,aAAa,KAAK;AAAA,EACzB;AAAA,EAEA,GACE,OACA,UACA;AACA,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,oBAAI,KAAK;AAAA,IACrC;AACA,SAAK,UAAU,IAAI,KAAK,EAAG,IAAI,QAAQ;AAEvC,WAAO,MAAM;AACX,WAAK,UAAU,IAAI,KAAK,EAAG,OAAO,QAAQ;AAAA,IAC5C;AAAA,EACF;AAAA,EAEA,KACE,OACA,UACA;AACA,UAAM,cAAc,KAAK,GAAG,OAAO,CAAC,iBAAiB;AACnD,eAAS,YAAY;AACrB,kBAAA;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEA,IACE,OACA,UACA;AAzCG;AA0CH,eAAK,UAAU,IAAI,KAAK,MAAxB,mBAA2B,OAAO;AAAA,EACpC;AAAA,EAEA,QACE,OACA,SACiC;AACjC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAI;AACJ,YAAM,cAAc,KAAK,GAAG,OAAO,CAAC,iBAAiB;AACnD,YAAI,WAAW;AACb,uBAAa,SAAS;AACtB,sBAAY;AAAA,QACd;AACA,gBAAQ,YAAY;AACpB,oBAAA;AAAA,MACF,CAAC;AACD,UAAI,SAAS;AACX,oBAAY,WAAW,MAAM;AAC3B,sBAAY;AACZ,sBAAA;AACA,iBAAO,IAAI,MAAM,6BAA6B,KAAK,EAAE,CAAC;AAAA,QACxD,GAAG,OAAO;AAAA,MACZ;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,KACE,OACA,cACA;AAxEG;AAyEH,eAAK,UAAU,IAAI,KAAK,MAAxB,mBAA2B,QAAQ,CAAC,aAAa;AAC/C,UAAI;AACF,iBAAS,YAAY;AAAA,MACvB,SAAS,OAAO;AAEd,uBAAe,MAAM;AACnB,gBAAM;AAAA,QACR,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAEA,iBACE,QACA,gBACA;AACA,SAAK,KAAK,iBAAiB;AAAA,MACzB,MAAM;AAAA,MACN,YAAY,KAAK;AAAA,MACjB;AAAA,MACA;AAAA,IAAA,CACD;AAGD,UAAM,WAA0B,UAAU,MAAM;AAChD,SAAK,KAAK,UAAU;AAAA,MAClB,MAAM;AAAA,MACN,YAAY,KAAK;AAAA,MACjB;AAAA,MACA;AAAA,IAAA,CACqC;AAAA,EACzC;AAAA,EAEA,sBACE,iBACA,yBACA;AACA,SAAK,KAAK,sBAAsB;AAAA,MAC9B,MAAM;AAAA,MACN,YAAY,KAAK;AAAA,MACjB;AAAA,MACA;AAAA,IAAA,CACD;AAAA,EACH;AAAA,EAEA,UAAU;AACR,SAAK,UAAU,MAAA;AAAA,EACjB;AACF;"}
|