From bfc1e10b44eb112486d230a41023b94ca0c0a8e5 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 15 Oct 2026 08:23:36 -0700 Subject: [PATCH 01/13] feat: prepare first 1.0 release candidate --- .changeset/first-rc.md | 26 ++ docs/collections/electric-collection.md | 2 +- docs/collections/indexed-db-collection.md | 10 +- docs/collections/local-storage-collection.md | 2 +- docs/collections/powersync-collection.md | 8 +- docs/collections/query-collection.md | 25 +- docs/contributing/oracle-coverage.md | 16 +- docs/framework/react/overview.md | 14 +- .../functions/useLiveInfiniteQuery.md | 15 +- .../react/reference/functions/useLiveQuery.md | 432 +----------------- .../functions/useLiveSuspenseQuery.md | 167 +------ .../reference/functions/usePacedMutations.md | 2 +- .../ConditionalUseLiveQueryConfig.md | 2 +- .../reference/type-aliases/LiveQueryKey.md | 2 +- .../UseLiveInfiniteQueryConfig.md | 10 +- .../UseLiveInfiniteQueryReturn.md | 2 +- ...seLiveInfiniteQueryReturnWithCollection.md | 40 +- .../type-aliases/UseLiveQueryConfig.md | 2 +- .../type-aliases/UseLiveQueryStatus.md | 2 +- docs/framework/solid/overview.md | 16 +- .../solid/reference/functions/useLiveQuery.md | 18 +- docs/guides/collection-options-creator.md | 6 +- docs/guides/live-queries.md | 13 +- docs/guides/mutations.md | 7 +- docs/guides/ssr.md | 40 +- docs/overview.md | 4 +- .../AggregateFunctionNotInSelectError.md | 4 +- .../CannotCombineEmptyExpressionListError.md | 4 +- .../classes/CollectionPreloadAbortedError.md | 4 +- .../classes/DuplicateAliasInSubqueryError.md | 229 ---------- .../classes/EmptyReferencePathError.md | 4 +- docs/reference/classes/GroupByError.md | 4 +- .../reference/classes/InvalidJoinCondition.md | 4 +- .../InvalidJoinConditionLeftSourceError.md | 4 +- .../InvalidJoinConditionRightSourceError.md | 4 +- .../InvalidJoinConditionSameSourceError.md | 4 +- ...InvalidJoinConditionSourceMismatchError.md | 4 +- .../classes/InvalidStorageDataFormatError.md | 4 +- .../InvalidStorageObjectFormatError.md | 4 +- .../classes/JoinCollectionNotFoundError.md | 4 +- docs/reference/classes/JoinError.md | 4 +- .../LoadSubsetOperationAbortedError.md | 4 +- .../classes/LocalStorageCollectionError.md | 4 +- .../classes/MissingAliasInputsError.md | 4 +- ...NonAggregateExpressionNotInGroupByError.md | 4 +- .../classes/QueryCompilationError.md | 1 - docs/reference/classes/QueryOptimizerError.md | 4 +- docs/reference/classes/SerializationError.md | 4 +- .../classes/SetWindowFindOneLimitError.md | 4 +- .../classes/SetWindowReentrancyError.md | 4 +- .../classes/SetWindowRequiresOrderByError.md | 4 +- docs/reference/classes/StorageError.md | 4 +- .../classes/StorageKeyRequiredError.md | 4 +- docs/reference/classes/SyncCleanupError.md | 4 +- .../classes/SyncTransactionAbortedError.md | 4 +- .../classes/UnhashableQueryIRError.md | 8 +- .../classes/UnknownExpressionTypeError.md | 4 +- .../reference/classes/UnknownFunctionError.md | 4 +- .../UnknownHavingExpressionTypeError.md | 4 +- .../UnsupportedAggregateFunctionError.md | 4 +- .../classes/UnsupportedFromTypeError.md | 4 +- .../classes/UnsupportedJoinSourceTypeError.md | 4 +- .../classes/UnsupportedJoinTypeError.md | 4 +- .../functions/electricCollectionOptions.md | 4 +- .../interfaces/ElectricCollectionConfig.md | 35 +- .../interfaces/ElectricCollectionUtils.md | 6 +- .../type-aliases/AwaitTxIdFn.md | 2 +- .../type-aliases/Txid.md | 2 +- docs/reference/functions/hasVirtualProps.md | 8 +- docs/reference/index.md | 1 - .../interfaces/BaseCollectionConfig.md | 57 +-- docs/reference/interfaces/CollectionConfig.md | 55 +-- docs/reference/interfaces/Context.md | 24 +- .../CurrentStateAsChangesOptions.md | 10 +- .../interfaces/IndexedDBCollectionConfig.md | 51 +-- .../interfaces/LiveQueryCollectionConfig.md | 12 +- .../interfaces/LocalOnlyCollectionConfig.md | 47 +- .../LocalStorageCollectionConfig.md | 51 +-- .../interfaces/SubscribeChangesOptions.md | 22 +- .../SubscribeChangesSnapshotOptions.md | 20 +- docs/reference/interfaces/Transaction.md | 69 ++- docs/reference/interfaces/VirtualRowProps.md | 61 +-- .../functions/queryCollectionOptions.md | 8 +- .../interfaces/QueryCollectionConfig.md | 102 ++++- .../interfaces/QueryCollectionUtils.md | 32 +- .../functions/rxdbCollectionOptions.md | 2 +- .../ApplyJoinOptionalityToMergedSchema.md | 2 +- docs/reference/type-aliases/ChangeListener.md | 2 +- docs/reference/type-aliases/ChangesPayload.md | 2 +- .../CollectionConfigSingleRowOption.md | 2 +- .../type-aliases/CollectionStatus.md | 2 +- .../type-aliases/ContextFromSource.md | 10 +- .../type-aliases/ContextFromUnionBranches.md | 18 +- .../type-aliases/ContextFromUnionSource.md | 2 +- docs/reference/type-aliases/ContextSchema.md | 2 +- .../type-aliases/DeleteMutationFn.md | 12 +- .../type-aliases/FunctionalHavingRow.md | 2 +- docs/reference/type-aliases/GetResult.md | 2 +- .../reference/type-aliases/GroupByCallback.md | 2 +- .../type-aliases/InferCollectionType.md | 2 +- .../reference/type-aliases/InferResultType.md | 2 +- docs/reference/type-aliases/InputRow.md | 2 +- .../type-aliases/InsertMutationFn.md | 12 +- docs/reference/type-aliases/JoinOnCallback.md | 2 +- .../type-aliases/KeyedNamespacedRow.md | 2 +- docs/reference/type-aliases/KeyedStream.md | 2 +- docs/reference/type-aliases/MakeOptional.md | 2 +- .../type-aliases/MaybeSingleResult.md | 4 +- .../MergeContextForJoinCallback.md | 2 +- .../type-aliases/MergeContextWithJoinType.md | 2 +- .../type-aliases/NamespacedAndKeyedStream.md | 2 +- docs/reference/type-aliases/NamespacedRow.md | 2 +- .../reference/type-aliases/NonSingleResult.md | 4 +- .../reference/type-aliases/OrderByCallback.md | 2 +- docs/reference/type-aliases/Prettify.md | 2 +- docs/reference/type-aliases/Ref.md | 2 +- docs/reference/type-aliases/RefsForContext.md | 2 +- docs/reference/type-aliases/ResultStream.md | 2 +- .../type-aliases/ResultTypeFromSelect.md | 2 +- .../type-aliases/SchemaFromSource.md | 2 +- docs/reference/type-aliases/SelectObject.md | 2 +- docs/reference/type-aliases/SingleResult.md | 4 +- docs/reference/type-aliases/SingleSource.md | 2 +- docs/reference/type-aliases/Source.md | 2 +- docs/reference/type-aliases/SyncMode.md | 2 +- .../type-aliases/UpdateMutationFn.md | 12 +- docs/reference/type-aliases/WhereCallback.md | 2 +- docs/reference/type-aliases/WithResult.md | 2 +- .../type-aliases/WithVirtualProps.md | 7 +- .../type-aliases/WithoutVirtualProps.md | 2 +- docs/reference/type-aliases/WritableDeep.md | 2 +- .../src/components/ListDetail.tsx | 6 +- .../src/components/ListsScreen.tsx | 10 +- .../react/paced-mutations-demo/src/App.tsx | 2 +- .../src/routes/_authenticated/index.tsx | 8 +- package.json | 2 +- ...ingle-tab-persisted-collection.e2e.test.ts | 6 +- .../e2e/electric-immediate-reload.opfs.ts | 2 +- .../capacitor-persisted-collection-harness.ts | 6 +- .../tests/capacitor-persistence.test.ts | 4 +- .../e2e/fixtures/worker.mjs | 2 +- .../src/persisted.ts | 30 +- .../contracts/runtime-persistence-contract.ts | 2 +- .../tests/persisted-oracle.test.ts | 88 ++-- .../persisted-real-adapter-lifecycle.test.ts | 4 +- .../sqlite-resume-snapshot-oracle.test.ts | 2 +- packages/db/e2e/indexed-db/browser.ts | 19 +- .../db/e2e/indexed-db/coverage-browser.ts | 36 +- packages/db/e2e/indexed-db/host-driver.ts | 11 +- .../indexed-db/packed-consumer.ts | 4 +- .../db/skills/db-core/live-queries/SKILL.md | 2 - .../db-core/mutations-optimistic/SKILL.md | 2 +- .../references/transaction-api.md | 10 - packages/db/src/collection/changes.ts | 2 +- packages/db/src/collection/mutations.ts | 7 +- packages/db/src/collection/state.ts | 44 +- packages/db/src/errors.ts | 19 - packages/db/src/indexed-db.ts | 9 +- packages/db/src/local-only.ts | 24 +- packages/db/src/local-storage.ts | 11 +- packages/db/src/paced-mutations.ts | 2 +- packages/db/src/query/builder/ref-proxy.ts | 6 +- packages/db/src/query/builder/types.ts | 9 +- packages/db/src/query/compiler/group-by.ts | 29 +- packages/db/src/query/compiler/index.ts | 55 ++- .../query/live/collection-config-builder.ts | 4 +- .../db/src/strategies/commit-completion.ts | 2 +- packages/db/src/strategies/queueStrategy.ts | 2 +- packages/db/src/transactions.ts | 48 +- packages/db/src/types.ts | 45 +- packages/db/src/virtual-props.ts | 71 +-- .../tests/change-event-history-oracle.test.ts | 6 +- packages/db/tests/collection-indexes.test.ts | 32 +- ...tadata-publication-oracle.property.test.ts | 23 +- ...collection-mutation-startup-oracle.test.ts | 8 +- ...ction-query-publication-boundaries.test.ts | 4 +- ...on-row-metadata-composition-oracle.test.ts | 4 +- ...on-state-retention-oracle.property.test.ts | 25 +- .../collection-subscribe-changes.test.ts | 96 ++-- ...ubscription-replay-oracle.property.test.ts | 4 +- .../collection-sync-reentrancy-oracle.test.ts | 18 +- packages/db/tests/collection-truncate.test.ts | 22 +- packages/db/tests/collection.test.ts | 96 ++-- .../db/tests/conformance-result-laws.test.ts | 1 - packages/db/tests/conformance/contract.ts | 2 +- packages/db/tests/conformance/result-laws.ts | 8 +- packages/db/tests/conformance/suite.ts | 8 +- ...-client-hydration-authority-oracle.test.ts | 2 +- packages/db/tests/db-client.test.ts | 10 +- packages/db/tests/error-sample-arguments.ts | 2 +- packages/db/tests/indexed-db/api.test.ts | 11 +- .../indexed-db/compatibility-oracle.test.ts | 30 +- .../cross-tab-boundary-oracle.test.ts | 26 +- .../db/tests/indexed-db/cross-tab-driver.ts | 16 +- .../db/tests/indexed-db/cross-tab-oracle.ts | 1 - packages/db/tests/indexed-db/harness.ts | 1 - .../local-write-order-oracle.test.ts | 31 +- .../indexed-db/pending-history-oracle.test.ts | 21 +- .../indexed-db/persistence-oracle.test.ts | 18 +- .../persistence-values-oracle.test.ts | 45 +- .../indexed-db/retirement-oracle.test.ts | 26 +- .../indexed-db/settlement-oracle.test.ts | 39 +- .../tests/indexed-db/transport-oracle.test.ts | 64 +-- .../tests/live-query-order-only-move.test.ts | 2 +- .../local-only-direct-write-oracle.test.ts | 26 +- packages/db/tests/local-only.test-d.ts | 6 +- packages/db/tests/local-only.test.ts | 4 +- .../tests/local-storage-order-oracle.test.ts | 63 +-- .../tests/local-storage-peer-oracle.test.ts | 142 +++--- .../local-storage-persistence-failure.test.ts | 6 +- packages/db/tests/local-storage.test-d.ts | 6 +- packages/db/tests/local-storage.test.ts | 114 +++-- .../mutation-handler-compatibility.test-d.ts | 35 +- .../mutation-handler-type-oracle.test-d.ts | 4 +- packages/db/tests/optimistic-action.test.ts | 6 +- .../optimistic-composition-oracle.test.ts | 22 +- .../db/tests/optimistic-history-oracle.ts | 17 +- ...mistic-transaction-oracle.property.test.ts | 40 +- .../db/tests/paced-mutations-oracle.test.ts | 8 +- packages/db/tests/paced-mutations.test.ts | 28 +- .../proxy-detachment-contract-oracle.test.ts | 22 +- .../proxy-iteration-contract-oracle.test.ts | 4 +- packages/db/tests/proxy-oracle.test.ts | 2 +- .../query/compiler/group-by-pipeline.test.ts | 39 +- ...rived-delete-reconciliation-oracle.test.ts | 6 +- .../includes-alias-shadowing-oracle.test.ts | 1 - ...ncludes-collection-oracle.property.test.ts | 12 +- .../includes-context-transport-oracle.test.ts | 1 - ...-cross-formulation-oracle.property.test.ts | 2 - ...ludes-functional-projection-oracle.test.ts | 2 +- ...ncludes-optimistic-oracle.property.test.ts | 8 +- .../query/includes-oracle.property.test.ts | 8 +- .../query/includes-publication-oracle.test.ts | 4 +- .../query/includes-temporal-oracle.test.ts | 4 +- .../includes-work-counter-oracle.test.ts | 22 +- packages/db/tests/query/includes.test-d.ts | 14 +- .../tests/query/live-query-collection.test.ts | 8 +- .../query/load-subset-oracle.property.test.ts | 36 +- ...bset-transaction-refinement-oracle.test.ts | 4 +- .../ordered-work-oracle.property.test.ts | 47 +- .../query/pagination-oracle.property.test.ts | 15 +- .../pooled-live-query-oracle.property.test.ts | 4 +- packages/db/tests/query/scheduler.test.ts | 3 - .../query/virtual-row-fields-oracle.test-d.ts | 26 +- .../query/virtual-row-fields-oracle.test.ts | 69 ++- ...dicate-publication-oracle.property.test.ts | 42 +- ...nsaction-ownership-oracle.property.test.ts | 14 +- packages/db/tests/transaction-types.test.ts | 2 +- packages/db/tests/transactions.test.ts | 51 +-- .../tests/truncate-readiness-oracle.test.ts | 2 +- packages/db/tests/utils.ts | 7 +- packages/db/tests/virtual-props-cache.test.ts | 4 +- ....test-d.ts => virtual-row-guard.test-d.ts} | 10 +- .../electric-db-collection/src/electric.ts | 135 +----- ...ectric-descriptor-isolation-oracle.test.ts | 10 +- .../tests/electric-oracle.property.test.ts | 18 +- .../tests/electric-recovery-oracle.test.ts | 1 - .../tests/electric.test-d.ts | 24 +- .../tests/electric.test.ts | 322 +++++-------- .../electron-persisted-collection.e2e.test.ts | 6 +- .../e2e/expo-runtime-app/App.tsx | 2 +- ...-persisted-collection-conformance-suite.ts | 6 +- .../tests/expo-persistence.test.ts | 4 +- .../e2e/node-persisted-collection.e2e.test.ts | 6 +- .../tests/node-persistence.test.ts | 2 +- packages/offline-transactions/README.md | 10 +- .../src/OfflineExecutor.ts | 3 +- .../leadership-replay-oracle.property.test.ts | 8 +- .../tests/restoration-ownership.test.ts | 6 +- ...saction-serializer-oracle.property.test.ts | 2 +- ...saction-settlement-oracle.property.test.ts | 28 +- .../tests/collection-schema.test.ts | 11 +- .../tests/correctness-oracle.test.ts | 35 +- .../tests/on-demand-sync.test.ts | 30 +- .../tests/powersync.test.ts | 69 +-- packages/query-db-collection/src/query.ts | 59 ++- .../tests/optimistic-writeback.test.ts | 8 +- .../tests/ownership-lifecycle.oracle.test.ts | 66 +-- .../query-db-collection/tests/query.test.ts | 58 +-- packages/react-db/skills/react-db/SKILL.md | 6 +- packages/react-db/src/live-query-internals.ts | 7 +- packages/react-db/src/useLiveInfiniteQuery.ts | 46 +- packages/react-db/src/useLiveQuery.ts | 217 +-------- packages/react-db/src/useLiveSuspenseQuery.ts | 28 +- packages/react-db/src/usePacedMutations.ts | 2 +- packages/react-db/tests/conformance.test.tsx | 18 +- .../descriptor-query-binding-oracle.test.tsx | 9 +- .../tests/infinite-query-conformance.test.tsx | 19 +- ...infinite-query-render-cuts-oracle.test.tsx | 8 +- .../tests/useLiveInfiniteQuery.test.tsx | 15 +- .../react-db/tests/useLiveQuery.test-d.tsx | 21 +- packages/react-db/tests/useLiveQuery.test.tsx | 327 ++++--------- .../tests/useLiveSuspenseQuery.test.tsx | 15 +- .../react-db/tests/usePacedMutations.test.tsx | 2 +- ...-persisted-collection-conformance-suite.ts | 6 +- .../tests/react-native-persistence.test.ts | 8 +- packages/rxdb-db-collection/src/rxdb.ts | 20 +- .../rxdb-db-collection/tests/rxdb.test.ts | 12 +- packages/solid-db/skills/solid-db/SKILL.md | 5 +- packages/solid-db/src/useLiveQuery.ts | 30 -- packages/solid-db/tests/conformance.test.tsx | 18 +- packages/solid-db/tests/useLiveQuery.test.tsx | 22 +- .../tauri-persisted-collection-harness.ts | 6 +- .../tests/tauri-persistence.test.ts | 4 +- .../trailbase-db-collection/src/trailbase.ts | 6 +- .../tests/lifecycle-oracle.property.test.ts | 8 +- .../tests/trailbase.test.ts | 43 +- packages/vue-db/tests/useLiveQuery.test.ts | 2 +- scripts/version-packages.mjs | 128 ++++++ 309 files changed, 2526 insertions(+), 3925 deletions(-) create mode 100644 .changeset/first-rc.md delete mode 100644 docs/reference/classes/DuplicateAliasInSubqueryError.md rename packages/db/tests/{virtual-row-legacy-guard.test-d.ts => virtual-row-guard.test-d.ts} (61%) create mode 100644 scripts/version-packages.mjs diff --git a/.changeset/first-rc.md b/.changeset/first-rc.md new file mode 100644 index 0000000000..059843eda9 --- /dev/null +++ b/.changeset/first-rc.md @@ -0,0 +1,26 @@ +--- +'@tanstack/angular-db': major +'@tanstack/browser-db-sqlite-persistence': major +'@tanstack/capacitor-db-sqlite-persistence': major +'@tanstack/cloudflare-durable-objects-db-sqlite-persistence': major +'@tanstack/db': major +'@tanstack/db-ivm': major +'@tanstack/db-sqlite-persistence-core': major +'@tanstack/electric-db-collection': major +'@tanstack/electron-db-sqlite-persistence': major +'@tanstack/expo-db-sqlite-persistence': major +'@tanstack/node-db-sqlite-persistence': major +'@tanstack/powersync-db-collection': major +'@tanstack/react-db': major +'@tanstack/react-native-db-sqlite-persistence': major +'@tanstack/react-router-with-db': major +'@tanstack/rxdb-db-collection': major +'@tanstack/solid-db': major +'@tanstack/svelte-db': major +'@tanstack/tauri-db-sqlite-persistence': major +'@tanstack/trailbase-db-collection': major +'@tanstack/vue-db': major +--- + +Release the first 1.0 candidate. Remove deprecated pre-1.0 APIs and give +source metadata precedence over colliding selected fields in live queries. diff --git a/docs/collections/electric-collection.md b/docs/collections/electric-collection.md index 0b56f39712..74c3b07a92 100644 --- a/docs/collections/electric-collection.md +++ b/docs/collections/electric-collection.md @@ -445,7 +445,7 @@ const matchFn = (message) => { ### Common Issue: awaitTxId Stalls or Times Out -A frequent issue developers encounter is that `awaitTxId` (or the transaction's `isPersisted.promise`) stalls indefinitely, eventually timing out with no error messages. The data persists correctly to the database, but the optimistic mutation never resolves. +A frequent issue developers encounter is that `awaitTxId` (or the transaction's `when('settled')`) stalls indefinitely, eventually timing out with no error messages. The data persists correctly to the database, but the optimistic mutation never resolves. **Root Cause:** This happens when the transaction ID (txid) returned from your API doesn't match the actual transaction ID of the mutation in Postgres. This mismatch occurs when you query `pg_current_xact_id()` **outside** the same transaction that performs the mutation. diff --git a/docs/collections/indexed-db-collection.md b/docs/collections/indexed-db-collection.md index 33f1956993..3a3ec239d5 100644 --- a/docs/collections/indexed-db-collection.md +++ b/docs/collections/indexed-db-collection.md @@ -86,18 +86,18 @@ const insert = todos.insert({ text: 'Write a draft', completed: false, }) -await insert.isPersisted.promise +await insert.when('settled') const update = todos.update(id, (draft) => { draft.completed = true }) -await update.isPersisted.promise +await update.when('settled') const remove = todos.delete(id) -await remove.isPersisted.promise +await remove.when('settled') ``` -Await `isPersisted.promise` to observe persistence success or failure. The returned transaction itself is not a Promise. +Await `when('settled')` to observe persistence success or failure. The returned transaction itself is not a Promise. If you provide `onInsert`, `onUpdate`, or `onDelete`, the handler runs before persistence. A rejected handler leaves durable rows unchanged and rolls back its optimistic changes. Each accepted Collection batch writes rows and metadata in one IndexedDB transaction. A failed write aborts the batch. @@ -210,7 +210,7 @@ For a transaction involving several Collections, call each Collection's acceptan Automatic writes in one Collection persist in mutation order, even when their handlers finish out of order. Later writes wait for earlier handlers and -persistence to settle before reporting `isPersisted`. Rejection contributes no +persistence to settle before reporting `when('settled')`. Rejection contributes no durable write and allows the next mutation to proceed. Do not await a later automatic write from an earlier handler in the same Collection: each would wait for the other. This rule does not order separate Collections, manual acceptance, diff --git a/docs/collections/local-storage-collection.md b/docs/collections/local-storage-collection.md index e61f9e7dd2..152e9903b8 100644 --- a/docs/collections/local-storage-collection.md +++ b/docs/collections/local-storage-collection.md @@ -111,7 +111,7 @@ localStorage before the mutation method returns. A later handler failure rejects its transaction promptly, even if an earlier handler is still pending. A handler can start another mutation on the same Collection, then return without -awaiting that mutation's `isPersisted` promise. Do not await that promise or a +awaiting that mutation's `when('settled')` promise. Do not await that promise or a later `acceptMutations()` call inside the earlier handler. Those writes wait for the earlier handler to return, so awaiting them would leave both pending. diff --git a/docs/collections/powersync-collection.md b/docs/collections/powersync-collection.md index d2147241ce..f93c76b91c 100644 --- a/docs/collections/powersync-collection.md +++ b/docs/collections/powersync-collection.md @@ -452,7 +452,7 @@ await documents.insert( timestamp: Date.now(), }, } -).isPersisted.promise +).when('settled') // Update with metadata await documents.update( @@ -461,12 +461,12 @@ await documents.update( (doc) => { doc.name = "Report Q4 (Updated)" } -).isPersisted.promise +).when('settled') // Delete with metadata await documents.delete(docId, { metadata: { deletedBy: "user-789", reason: "duplicate" }, -}).isPersisted.promise +}).when('settled') ``` #### Accessing Metadata During Upload @@ -568,7 +568,7 @@ batchTx.mutate(() => { await batchTx.commit() // Wait for the changes to be persisted -await batchTx.isPersisted.promise +await batchTx.when('settled') ``` This approach allows you to: diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md index ef022a5e5d..3731c3766d 100644 --- a/docs/collections/query-collection.md +++ b/docs/collections/query-collection.md @@ -569,7 +569,7 @@ const todosCollection = createCollection( onInsert: async ({ transaction }) => { const newItems = transaction.mutations.map((m) => m.modified) await api.createTodos(newItems) - // Auto-refetch happens after handler completes (pre-1.0 behavior) + // Auto-refetch happens after the handler completes }, onUpdate: async ({ transaction }) => { @@ -588,13 +588,13 @@ const todosCollection = createCollection( ) ``` -> **Note**: QueryCollection currently auto-refetches after handlers complete. See [Controlling Refetch Behavior](#controlling-refetch-behavior) for details on this transitional behavior. +> **Note**: QueryCollection auto-refetches after handlers complete. See [Controlling Refetch Behavior](#controlling-refetch-behavior) for details. ### Controlling Refetch Behavior -> **⚠️ Transitional API**: QueryCollection currently auto-refetches after handlers complete. This behavior is deprecated and will be removed in v1.0. See the migration notes below. +> **Note:** QueryCollection 1.x retains auto-refetch after handlers complete. See the options below to control this behavior. -#### Current Behavior (Pre-1.0) +#### Current Behavior By default, QueryCollection automatically refetches after each handler completes. To **skip** auto-refetch, return `{ refetch: false }`: @@ -609,9 +609,9 @@ onInsert: async ({ transaction }) => { If you don't return `{ refetch: false }`, auto-refetch happens automatically. -#### v1.0 Behavior (Future) +#### Explicit Refetch Inside a Handler -In v1.0, auto-refetch will be **removed**. Handlers will need to explicitly call `collection.utils.refetch()` when refetching is needed: +Handlers can explicitly call `collection.utils.refetch()` when they need its result before returning. Return `{ refetch: false }` to prevent a second automatic refetch: ```typescript onInsert: async ({ transaction, collection }) => { @@ -619,6 +619,7 @@ onInsert: async ({ transaction, collection }) => { // Explicitly trigger refetch when you need server state await collection.utils.refetch() + return { refetch: false } } ``` @@ -636,7 +637,7 @@ the Query fetch boundary. This rule applies whether the mutation or the refetch starts first. It includes calls through the handler parameter, captured or external Collection references, manual transaction mutation functions, and `createOptimisticAction` mutation functions. Code can await the mutation's -`isPersisted.promise` when it needs the complete transaction and its queued +`when('settled')` when it needs the complete transaction and its queued Collection application. Utilities that refetch, including `clearError()`, inherit the same phase boundary. @@ -655,13 +656,13 @@ boundary, `throwOnError` applies only to the Query fetch because application happens after that call settles. A later application failure is recorded by the Collection's error utilities. -To skip refetch in v1.0, simply don't call `refetch()`: +To skip refetch after a handler, return `{ refetch: false }`: ```typescript onInsert: async ({ transaction }) => { await api.createTodos(transaction.mutations.map((m) => m.modified)) - // No refetch call = no refetch (v1.0 behavior) + return { refetch: false } } ``` @@ -861,7 +862,7 @@ const todosCollection = createCollection( // No need to refetch - we've already synced the server response via direct writes // (optimistic state is automatically replaced when handler completes) - return { refetch: false } // Transitional pre-1.0 opt-out + return { refetch: false } }, onUpdate: async ({ transaction, collection }) => { @@ -879,7 +880,7 @@ const todosCollection = createCollection( }) // No refetch needed since we used direct writes - return { refetch: false } // Transitional pre-1.0 opt-out + return { refetch: false } }, }) ) @@ -1279,7 +1280,7 @@ has a subset owner or not. It refetches that Query when the subset is acquired, then applies the new server result. The earlier direct write still starts no request by itself. -To use direct writes with persistence handlers, return `{ refetch: false }` to avoid the handler's separate automatic refetch during the pre-1.0 transition. An on-demand `queryFn` still needs to return the complete current result for its pushed-down predicate, order, limit, and offset when it does run. For robust real-time synchronization, use a collection backed by a sync engine that owns snapshot and change-stream ordering. +To use direct writes with persistence handlers, return `{ refetch: false }` to avoid the handler's separate automatic refetch. An on-demand `queryFn` still needs to return the complete current result for its pushed-down predicate, order, limit, and offset when it does run. For robust real-time synchronization, use a collection backed by a sync engine that owns snapshot and change-stream ordering. ## Complete Direct Write API Reference diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 8e7456cd43..6137050a23 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -80,7 +80,7 @@ that test identifiers must copy production's private data structures. | Electric and TrailBase | Complete | Electric replica and recovery models, installed-SDK HTTP delivery, PostgreSQL serialization, and TrailBase's controlled RecordApi/native-stream lifecycle are literate with their real-provider limits intact. | | PowerSync | Complete | Patch conservation, effective-update receipts, metadata and falsey changes, declared-view keys, transformed schema output, logging, cleanup, and native SQLite reach are literate. | | SQLite persistence and native hosts | Complete | Persisted hydration/replay and ownership, shared-handle driver transaction laws, OPFS page and diagnostic state machines, and the 113-law native conformance manifest are literate. Native execution remains distinct from registration and shim evidence. | -| Offline execution | Complete | FIFO retry, scheduler eligibility, leadership replay, transaction settlement, and typed wire serialization are literate. The [interruption settlement oracle](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/interruption-settlement-oracle.property.test.ts) moves one leadership loss, offline change, or disposal across individual microtask turns after one to three commits on disjoint keys, then recovers by regaining leadership, going online, or restarting over the same storage. It requires that no provider call or new outbox admission starts while fenced, that each admitted ID reaches the provider once and in commit order, that a fulfilled commit's row is already gone from storage, that commits settle with their provider outcome after a regain or reconnect, and that the outbox ends empty. A reach check requires interruptions before admission, between admission and the provider call, and after it, for each interruption. Mutants that drop the execution loop's fence, resolve before deletion, drop the admission check, or check admission before awaiting initialization fail it. Open there: shared keys, providers that answer later, and overlapping old and new executors. The settlement oracle's generated FIFO histories draw each transaction as one record (outcome; manual commit, auto-commit with the option set or omitted, or an `OfflineAction`; receipt read or not), use `isPersisted` as an auto-commit's receipt, and require that no failure escapes as an unhandled rejection, also when nobody reads the receipt, and that settlement logs no failure again and writes no debug output. An empty transaction on any commit path may precede each one; its receipt and completion waiter must fulfil without reaching the provider or the outbox. A manual transaction that writes a row and rolls back before it commits may also precede each one; its waiter must reject with an Error that names the rollback. A focused witness rolls a transaction back while it persists, for both provider outcomes: its waiter must wait for the executor and report the executor's outcome. After every transaction settles, a new waiter for each one must reject within a turn rather than hang. An unread receipt still settles with its own outcome when read afterwards, and a caller may register its completion waiter before admission on every path but `OfflineAction`. A focused witness fails the admission outbox write on every commit path and requires the receipt to reject with the storage error, without an unhandled rejection or a repeated log; a failed outbox write stops the executor, so generated histories do not continue past one. Open: the generated failure source is the mutation function only; manual commits always follow their own `mutate()` at once, so a pending manual transaction behind a failing shared-key sibling is not generated; a throwing `mutate()` callback is not generated, because core keeps its transaction pending; a rolled-back-before-commit transaction is generated only on the manual path, with its own key and its waiter registered after `mutate()`, so a secondary rollback of a pending transaction by a failing sibling, whose waiter gets the generic rollback Error, is not generated. | +| Offline execution | Complete | FIFO retry, scheduler eligibility, leadership replay, transaction settlement, and typed wire serialization are literate. The [interruption settlement oracle](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/interruption-settlement-oracle.property.test.ts) moves one leadership loss, offline change, or disposal across individual microtask turns after one to three commits on disjoint keys, then recovers by regaining leadership, going online, or restarting over the same storage. It requires that no provider call or new outbox admission starts while fenced, that each admitted ID reaches the provider once and in commit order, that a fulfilled commit's row is already gone from storage, that commits settle with their provider outcome after a regain or reconnect, and that the outbox ends empty. A reach check requires interruptions before admission, between admission and the provider call, and after it, for each interruption. Mutants that drop the execution loop's fence, resolve before deletion, drop the admission check, or check admission before awaiting initialization fail it. Open there: shared keys, providers that answer later, and overlapping old and new executors. The settlement oracle's generated FIFO histories draw each transaction as one record (outcome; manual commit, auto-commit with the option set or omitted, or an `OfflineAction`; receipt read or not), use `when('settled')` as an auto-commit's receipt, and require that no failure escapes as an unhandled rejection, also when nobody reads the receipt, and that settlement logs no failure again and writes no debug output. An empty transaction on any commit path may precede each one; its receipt and completion waiter must fulfil without reaching the provider or the outbox. A manual transaction that writes a row and rolls back before it commits may also precede each one; its waiter must reject with an Error that names the rollback. A focused witness rolls a transaction back while it persists, for both provider outcomes: its waiter must wait for the executor and report the executor's outcome. After every transaction settles, a new waiter for each one must reject within a turn rather than hang. An unread receipt still settles with its own outcome when read afterwards, and a caller may register its completion waiter before admission on every path but `OfflineAction`. A focused witness fails the admission outbox write on every commit path and requires the receipt to reject with the storage error, without an unhandled rejection or a repeated log; a failed outbox write stops the executor, so generated histories do not continue past one. Open: the generated failure source is the mutation function only; manual commits always follow their own `mutate()` at once, so a pending manual transaction behind a failing shared-key sibling is not generated; a throwing `mutate()` callback is not generated, because core keeps its transaction pending; a rolled-back-before-commit transaction is generated only on the manual path, with its own key and its waiter registered after `mutate()`, so a secondary rollback of a pending transaction by a failing sibling, whose waiter gets the generic rollback Error, is not generated. | | Frameworks | Complete | Shared live-query and infinite-query models are literate. Each framework keeps its own realm, ownership, and scheduling driver. | | Structural values and ordered primitives | Complete | Structural hashing, deep equality, comparison, cursor denotation, index refinement, and query-identity output equivalence are literate. | | Mixed-kind ordered prefixes | Bounded | Eager BasicIndex and BTreeIndex paths compare ascending and descending direct and live-query windows with an unindexed scan across mixed value kinds and paired source changes. One retained on-demand BTree history checks settled windows after a number/bigint transition, a finite request for a missing row, and a cursor request after index eligibility changes. Other provider paging histories remain open. | @@ -99,8 +99,14 @@ owners. The virtual-row-fields owner (`packages/db/tests/query/virtual-row-fields-oracle.test.ts`) checks presence of all four public virtual fields directly after implicit, expression, and -functional root publication. Its fixed controlled-Collection cases do not -establish every nested projection shape or framework receiving boundary. +functional root publication. It also holds an optimistic source update and +checks that a root projection cannot replace the source's pending-write, +origin, key, or Collection ID metadata with explicitly selected fields. +Whole-row roots retain source metadata, while ordinary projections receive +metadata from the live-query Collection. Inline Effects and nested subquery +results retain their selected shapes; they are not Collection row publication +boundaries. These fixed controlled-Collection cases do not establish every +nested projection shape or framework receiving boundary. Three type-only owners check compile-time boundaries: `packages/db-sqlite-persistence-core/tests/persisted-options-type-oracle.test-d.ts` @@ -482,7 +488,7 @@ generic visitor behavior outside the named collectors. | Includes and publication | [cross-formulation](https://github.com/TanStack/db/blob/main/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts), [temporal](https://github.com/TanStack/db/blob/main/packages/db/tests/query/includes-temporal-oracle.test.ts), [Collection includes](https://github.com/TanStack/db/blob/main/packages/db/tests/query/includes-collection-oracle.property.test.ts), [architecture and complete suite map](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/ARCHITECTURE.md#executable-contracts) | Per-parent/flat-join/partition relations, callback-time rows, nested values, and route histories. Fragmented lazy-demand consolidation covers delta growth and successful, rejected/retried, and obsolete replacements at the Collection request boundary; it observes exact key unions and abort checkpoints. A compiled-includes fixture makes adapter unload evict owned child rows and proves a rejected union cannot remove established public child rows. Retry after that terminal live-query failure remains covered at the acquisition boundary; no automatic in-place recovery is claimed. Observe raw promised order; fresh queries do not establish continuous publication safety. The temporal owner's source-readiness lane enumerates every legal order of delivery, settlement, source readiness, and one later change for a source whose Collection readiness lags its settled subsets. It checks live-query readiness, preload settlement, and visible rows for direct, single-result, ordered-window, include-child, lazy-join, and eager-root sources; `queryOnce` results for the direct and eager-root shapes; `toArrayWhenReady()` and `stateWhenReady()` results for a read started after rows are visible but before readiness, over the direct, single-result, and eager-root shapes (and the lazy join for `toArrayWhenReady()`); and Effect `skipInitial` deltas for the direct, lazy-join, and eager-root shapes, and every on-demand cell of every consumer is crossed with a truncate or (join and include) a parent rekey before settlement. Each on-demand cell also compares its consumer with a metamorphic twin over a source that is ready from its start, which does not rely on the model's eager/on-demand rule. A live-query observer test checks that a hydration seed hands off at subset settlement, and an Electric test pins that a ready progressive query's rows update only at the full sync's swap. Effects over an ordered window or an include have no lane. It rejects both a design that waits for every source's Collection readiness and one that ignores source readiness. Electric live-query tests are the receiving witnesses for a real progressive source and an on-demand source that is ready before its subset loads; a React Suspense test checks that Suspense and `isReady` release together. Other framework adapters have no Suspense witness. | | Collection lifecycle | [mutation startup](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-mutation-startup-oracle.test.ts), [change-event history](https://github.com/TanStack/db/blob/main/packages/db/tests/change-event-history-oracle.test.ts), [sync publication reentrancy](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-sync-reentrancy-oracle.test.ts), [history](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-subscription-lifecycle-history-oracle.property.test.ts), [publication](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-subscription-lifecycle-publication-oracle.property.test.ts), [replay](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-subscription-replay-oracle.property.test.ts), [effect disposal](https://github.com/TanStack/db/blob/main/packages/db/tests/effect-disposal-oracle.test.ts), [PR #1902 review](oracle-reviews/pr-1902-change-event-history.md), [batch-order decision review](oracle-reviews/change-event-batch-order.md), [absent-key insert review](oracle-reviews/2026-10-02-insert-for-absent-key.md) | Core Collection `insert`/`update`/`delete` admission while `startSync:false` is idle; complete public-row/change-message agreement across bounded one- and two-key histories plus replayable four-key campaigns, where every message must be valid for a strict consumer (insert only for an absent key, update or delete only for a present one; [absent-key insert review](oracle-reviews/2026-10-02-insert-for-absent-key.md)); a two-key deferred sync history checks each key's causal insert/update/delete trace while allowing any cross-key interleaving. Reversing all deferred messages fails that check; reversing each sync transaction's distinct-key messages passes. Queued duplicate admission and cancellation, ownership and phase histories, exact caller/error/publication evidence, and late completion and restart have separate witnesses. Generated deferred histories with cancellation or reentrant callbacks need a witness in the change-event history owner before claiming general batch-shape coverage. Query write utilities and effect self-dependent disposal remain separate contracts. During a truncate, a ready callback can write to any key after a prior optimistic request. Each subscriber, with or without initial state or a filter, must receive valid messages after the Collection is ready and end at the Collection's rows. A throwing ready callback during sync entry surfaces once the sync function returns, whether it calls markReady or commits a truncate ([round 3 follow-ups](oracle-reviews/2026-10-05-round-3-followups.md)). Callback rollbacks, non-optimistic callback writes, and several writing callbacks remain open ([ready-callback truncate review](oracle-reviews/2026-10-05-truncate-ready-callback-delete.md)). | | Paced mutations | [virtual-clock oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/paced-mutations-oracle.test.ts) | `createPacedMutations` with queue front/back options, bounded waiting capacity at zero and one, cleanup draining in FIFO/LIFO order after a held write, cleanup timing at the configured wait for immediate writes, admitted writes after separate Collection cleanup, debounce and throttle schedules with explicit and omitted options, and first-leading throttle execution at epoch zero. Finite histories compare optimistic rows, returned transaction identity and receipt outcome, execution order and virtual-clock time. Held writes verify queue serialization. Leading-only throttle and debounce witnesses reject skipped calls immediately with their named dropped-call errors, including omitted edges, both edges disabled, and same-row rollback while a prior write is held. Capacity witnesses cover overflow rejection, distinct-key optimistic rollback, same-key admitted-write survival, and in-flight versus waiting admission. Cleanup witnesses check repeated queue cleanup, eventual admitted receipt settlement, direct queue strategy rejection of new callbacks, public post-cleanup rollback with `QueueDisposedError`, pending debounce transactions that wait until the last call's quiet edge after separate Collection cleanup, and a trailing throttle timer that runs at its regular edge after separate Collection cleanup. Deferred custom queue and batch callbacks check compatibility with `void` and `false` execute results, respectively. Frozen options verify factory non-mutation; a mutable option witness checks that debounce uses its construction-time trailing setting. Throwing `onMutate` histories check leading-edge reuse before any row change, failed-row restoration when a pending transaction already has admitted work, eligible leading writes behind a held handler, and throttle pending merges at the throw, start, receipt, and later-timer cuts. New debounce/throttle admission after cleanup, manual commit of a strategy-owned transaction, longer throwing/reentrant bursts, failed persistence, broader capacities and schedules remain outside this owner. | -| Optimistic state | [same-key composition](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-composition-oracle.test.ts), [history model](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-oracle.ts), [generated histories](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-transaction-oracle.property.test.ts), [outcomes](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-outcomes-oracle.test.ts), [publication](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-publication-oracle.test.ts) | Optimistic state drops at settlement and held sync transactions publish with it ([settlement-drop review](oracle-reviews/2026-10-03-settlement-drop.md)); whole-row active overlays, metadata and prior-value events. A raw subscriber without initial state checks that each truncate batch inserts a key once ([2026-10-02 review](oracle-reviews/2026-10-02-truncate-raw-subscriber.md)). Never rebase a pending snapshot merely to simplify the model. Generated histories also write source batches inside mutation handlers, before they return; the Collection must own the request by then ([handler source batch review](oracle-reviews/2026-10-01-handler-source-batch.md)). The same-key transaction lane also authors a delete followed by a reinsert, which must be visible optimistically and net to the truth-table request ([state mutation round 2](oracle-reviews/2026-10-02-state-mutation-round-2.md)). Source rows are written as `insert` for a key the source lacks, and batches may delete keys the source holds ([state-stack mutation gaps](oracle-reviews/2026-10-02-state-stack-mutant-gaps.md)). Batches may also delete a key the source never held. That delete leaves an active request in place ([absent-key source delete review](oracle-reviews/2026-10-02-absent-key-source-delete.md)). A separate partial campaign keeps the default partial row update mode, where source updates may omit a field and merge into the source row, not into an optimistic row ([round 3 review](oracle-reviews/2026-10-05-state-mutation-round-3.md)). An aborted open batch restores the driver's own source rows as well as its keys, so a later partial update still omits the field ([aborted-batch review](oracle-reviews/2026-10-08-partial-update-aborted-batch.md)). A failed synchronous manual `mutate` callback is now checked after same-key update, distinct insert, and same-key delete, with and without earlier intent, plus two two-Collection cases, one with a restoration listener failure. The throw cut compares public rows and pending payload; the later commit checks only surviving intent. A restoration listener error retains the callback error as the aggregate cause and does not stop other Collection restoration. These witnesses allow transient notifications during the callback and do not cover nested throwing callbacks or async work after the synchronous scope. After every step the driver also checks, by transaction identity, that the Collection tracks exactly its unsettled transactions, so a failed or rolled-back transaction leaves the map ([failed transaction retention review](oracle-reviews/failed-transaction-retention.md)). The [transaction ownership oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/transaction-ownership-oracle.property.test.ts) owns release across two Collections: merged-away pairs, conflicting rollbacks with a throwing subscriber, asynchronous success and failure settlements with a throwing subscriber, rollbacks from a `truncate` listener during a sync commit, a reused transaction id, and a `mutate()` callback that throws, checked in the same call stack and after promises flush. It also checks the settlement error shape and that a settled transaction's set of tracking Collections is empty; its mutations still name their Collection, so neither owner claims the Collection is collectable. Focused [restoration ownership](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/restoration-ownership.test.ts) witnesses check that repeated offline `mutate()` calls add to one transaction, that a completed restoration settles `isPersisted`, and that a restoration one Collection cannot track leaves no Collection tracking it. Its grammar also has direct writes (`insert`, `update` and `delete` outside a transaction), optionally with a throwing subscriber that may have been sent the rows first; it captures each direct write's own transaction and requires it to fail when the admission throws and to complete otherwise, counts handler calls (none for a rejected write), and keeps a mirror subscriber on each Collection that must match the rows after every step. A write whose admission throws rolls back only its own transaction and rethrows. The [change-event history oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/change-event-history-oracle.test.ts) generates local-only direct writes that meet a throwing subscriber, registered after the mirror, and requires the call to rethrow with its transaction completed and the rows following the model. Focused witnesses cover a local-only subscriber that writes the key it was told about, and a subscriber that throws a value that is not an Error; whether a throwing subscriber may stop a later subscriber is not decided. Open (approved limits): a direct write made inside another write's change subscriber reports its subscriber failure to the outer write; on a local-only Collection a direct write whose subscriber throws completes on the fast path but rolls back on the transaction path, and the change-event oracle generates only the fast path; a direct write admitted from inside a sync-commit listener, where the rollback's recompute is deferred, is not generated; the generated ownership histories read every receipt themselves, so only a focused witness (rolling back an empty manual transaction) checks that an unread receipt reaches no `unhandledrejection`; two Collection instances with one id, a Collection cleaned up before its transaction settles, and offline restoration have focused witnesses only; no generated history combines a held completed row with a second Collection. | +| Optimistic state | [same-key composition](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-composition-oracle.test.ts), [history model](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-oracle.ts), [generated histories](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-transaction-oracle.property.test.ts), [outcomes](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-outcomes-oracle.test.ts), [publication](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-publication-oracle.test.ts) | Optimistic state drops at settlement and held sync transactions publish with it ([settlement-drop review](oracle-reviews/2026-10-03-settlement-drop.md)); whole-row active overlays, metadata and prior-value events. A raw subscriber without initial state checks that each truncate batch inserts a key once ([2026-10-02 review](oracle-reviews/2026-10-02-truncate-raw-subscriber.md)). Never rebase a pending snapshot merely to simplify the model. Generated histories also write source batches inside mutation handlers, before they return; the Collection must own the request by then ([handler source batch review](oracle-reviews/2026-10-01-handler-source-batch.md)). The same-key transaction lane also authors a delete followed by a reinsert, which must be visible optimistically and net to the truth-table request ([state mutation round 2](oracle-reviews/2026-10-02-state-mutation-round-2.md)). Source rows are written as `insert` for a key the source lacks, and batches may delete keys the source holds ([state-stack mutation gaps](oracle-reviews/2026-10-02-state-stack-mutant-gaps.md)). Batches may also delete a key the source never held. That delete leaves an active request in place ([absent-key source delete review](oracle-reviews/2026-10-02-absent-key-source-delete.md)). A separate partial campaign keeps the default partial row update mode, where source updates may omit a field and merge into the source row, not into an optimistic row ([round 3 review](oracle-reviews/2026-10-05-state-mutation-round-3.md)). An aborted open batch restores the driver's own source rows as well as its keys, so a later partial update still omits the field ([aborted-batch review](oracle-reviews/2026-10-08-partial-update-aborted-batch.md)). A failed synchronous manual `mutate` callback is now checked after same-key update, distinct insert, and same-key delete, with and without earlier intent, plus two two-Collection cases, one with a restoration listener failure. The throw cut compares public rows and pending payload; the later commit checks only surviving intent. A restoration listener error retains the callback error as the aggregate cause and does not stop other Collection restoration. These witnesses allow transient notifications during the callback and do not cover nested throwing callbacks or async work after the synchronous scope. After every step the driver also checks, by transaction identity, that the Collection tracks exactly its unsettled transactions, so a failed or rolled-back transaction leaves the map ([failed transaction retention review](oracle-reviews/failed-transaction-retention.md)). The [transaction ownership oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/transaction-ownership-oracle.property.test.ts) owns release across two Collections: merged-away pairs, conflicting rollbacks with a throwing subscriber, asynchronous success and failure settlements with a throwing subscriber, rollbacks from a `truncate` listener during a sync commit, a reused transaction id, and a `mutate()` callback that throws, checked in the same call stack and after promises flush. It also checks the settlement error shape and that a settled transaction's set of tracking Collections is empty; its mutations still name their Collection, so neither owner claims the Collection is collectable. Focused [restoration ownership](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/restoration-ownership.test.ts) witnesses check that repeated offline `mutate()` calls add to one transaction, that a completed restoration settles `when('settled')`, and that a restoration one Collection cannot track leaves no Collection tracking it. Its grammar also has direct writes (`insert`, `update` and `delete` outside a transaction), optionally with a throwing subscriber that may have been sent the rows first; it captures each direct write's own transaction and requires it to fail when the admission throws and to complete otherwise, counts handler calls (none for a rejected write), and keeps a mirror subscriber on each Collection that must match the rows after every step. A write whose admission throws rolls back only its own transaction and rethrows. The [change-event history oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/change-event-history-oracle.test.ts) generates local-only direct writes that meet a throwing subscriber, registered after the mirror, and requires the call to rethrow with its transaction completed and the rows following the model. Focused witnesses cover a local-only subscriber that writes the key it was told about, and a subscriber that throws a value that is not an Error; whether a throwing subscriber may stop a later subscriber is not decided. Open (approved limits): a direct write made inside another write's change subscriber reports its subscriber failure to the outer write; on a local-only Collection a direct write whose subscriber throws completes on the fast path but rolls back on the transaction path, and the change-event oracle generates only the fast path; a direct write admitted from inside a sync-commit listener, where the rollback's recompute is deferred, is not generated; the generated ownership histories read every receipt themselves, so only a focused witness (rolling back an empty manual transaction) checks that an unread receipt reaches no `unhandledrejection`; two Collection instances with one id, a Collection cleaned up before its transaction settles, and offline restoration have focused witnesses only; no generated history combines a held completed row with a second Collection. | | Drafts and native values | [proxy](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-oracle.test.ts), [detachment](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-detachment-contract-oracle.test.ts), [iteration](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-iteration-contract-oracle.test.ts), [revert](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-revert-oracle.property.test.ts), [native methods](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-native-methods-oracle.property.test.ts) | Native-operation controls, exact patches and actual stored rows; alias/cycle/adversarial-key histories. Set and Map reorder histories check draft patches and stored iteration order after replacement and clear-and-readd; RegExp replacement/reversion histories check `lastIndex`. General native-mutator and symbol-write support is not established by a plain-object oracle. The revert owner generates assignment, revert, delete, nested, symbol-key, and `for...of` histories and checks `getChanges()` and the draft against an independent draft-equality model: ordered Map and Set contents (including Sets inside Map values), RegExp `lastIndex`, array holes, and symbol keys inside nested values. Map and Set mutators and top-level symbol writes are outside its grammar; array mutators appear only in the mixed block below. Its round-trip grammar also replaces a field with a new object and restores the original value through a nested write. The law requires every changed key, and permits a written key whose final value equals its original to be reported with that value: a false positive repeats the stored value, and a false negative loses a write ([review](oracle-reviews/proxy-revert-replaced-object.md)). A further revert block mixes array `push`, `pop`, and `reverse` with assignments, a nested object replacement, and a retained handle, and checks that a mutator through a replaced handle changes nothing; pinned witnesses require a native write through a Map or Set value, a typed-array view, or a frozen draft to be reported, and a nested revert inside a Map or Set to keep the container's other write; Map and Set mutators are not mixed with assignments, and an equal-valued assignment keeps a retained handle attached (declared). The proxy owner's stored-function law checks, against a native row, that a function stored as data reads back as stored by every read path and that a stored method sees the draft as `this`, so its writes are tracked. The detachment owner pins which key classes a draft copies: enumerable string keys and every symbol key. The revert owner also generates typed arrays (including a subclass that does not forward its constructor argument), URLs, and a class with only private state as written values, with a property that writes two of them over one field. The native-methods owner generates rows (holes, `undefined`, duplicates, nested arrays, empty rows, typed `-0` and `NaN`) and calls every method of `Array.prototype` and `TypedArray.prototype`, in fixed and random campaigns with replay; it compares the result, whether the method returned the array itself, the identity of each original object, and the published row with native values, and a new built-in method fails until it has arguments. The proxy owner's `defineProperty`, stored-function, and frozen-draft laws are bounded tables against native rows. Its `defineProperty` law compares result, value, descriptor, and changes, for values and accessors. The iteration owner checks that array iterators see a push, removal, index write, or length cut made while they are open. Open: a draft reads a class instance of the original row as a plain object, so private-field getters read `undefined`, and a built-in method called through `this` on a nested Map or Set draft rejects the Proxy receiver; a mutator that changes nothing publishes an equal value; writing back a keyless class instance of the original row is a change, and `DataView` setters are untracked ([draft proxy review](oracle-reviews/code-weight-draft-proxy.md)). | | Query DB and observer | [ownership](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts), [persisted direct-write window](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts) ([review](oracle-reviews/issue-2029-direct-write-lock-window.md)), [load lifecycle](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts), [observer histories](https://github.com/TanStack/db/blob/main/packages/db/tests/live-query-observer-history-oracle.property.test.ts), [retained snapshots](https://github.com/TanStack/db/blob/main/packages/db/tests/live-query-observer-snapshot-oracle.property.test.ts) ([review](oracle-reviews/pr-2043-retained-observer-snapshots.md)) | Real QueryClient boundary, applied-settlement barriers for existing and cached demand, mutation-handler Collection identity, terminal fetch-record lifecycle, and a per-listener eligibility ledger, not a duplicate dispatch queue. The bounded handler grammar crosses insert, update, delete, parameter and mutation-alias access, refetch, and clearError. A held clearError retry retains the prior public error through initial, background, and Collection application failures; success clears it, failure advances the count, including a repeated error at clock time zero. An intermediate Query retry attempt does not recount the previous background error; a held final attempt can succeed or fail. A held application after successful Query fetch keeps the error visible until the applied result. A mocked persisted-baseline scan holds an older success while a newer terminal Query failure arrives; the newer public error and count survive application, including when the Error object and clock tick repeat. Native SQLite persistence, concurrent retries across multiple tracked Queries, mutation-handler fetch-boundary settlement, and deferred result application remain outside this witness. The mutation overlap grammar crosses both start orders. Check reentry, peer survival, FIFO and disposal independently of final rows. A retained observer snapshot shows its capture-time rows on a later first read of `data` or `state`. Direct-write witnesses now distinguish an idle pending eager cache seed (success with no new freshness timestamp) from an in-flight pending request, and require update, delete, and upsert to reach both eager Collections through their shared physical Query, in both acquisition orders. They check both public Collections and the Query cache immediately after each write. A held older shared fetch also crosses B's release and reacquisition, then compares both public rows at refetch and load settlement; mixed select wrappers and three or more co-owners remain outside this witness. The load-lifecycle owner also restores an intermediate checkpoint that keeps an explicit subset refetch pending until a post-write request applies, and rejects a no-cancel mutant at the replacement-request assertion. | | Query DB SSR dehydration | `packages/query-db-collection/tests/ssr-dehydration-oracle.test.ts`, [review record](oracle-reviews/issue-1950-ssr-dehydration.md) | A plain Query cache control, a live on-demand compound predicate, a signal-only request, and an ordered cursor with a custom comparator all retain their row through QueryClient dehydration, reconstruction of Seroval's emitted stream payload, and Query Core hydration from that payload. The on-demand query functions still receive the original IR, comparator, and signal; serialized metadata retains enumerable user metadata but excludes request options. The original implementation fails the compound and signal cases at the stream checkpoint. A serializable stream-only request-options leak passes the former chunk-count check but fails the current payload assertion. This owner covers successful Query cache entries at the installed Query Core 5.90.20 and Seroval 1.5.0 versions. A TanStack Start integration owner still needs the reporter's Router 1.171.33 and Seroval 1.6.8 path, browser Collection resume/refetch, and request cancellation across SSR. Query subset and pagination oracles remain owners of predicate/order/cursor meaning and supported value types. The issue's `shouldDehydrateQuery` workaround replaces Query Core's success-only default and admits pending/error queries; any documented workaround must preserve that default filter. | @@ -1855,7 +1861,7 @@ The post-merge review added three missing domains to existing owners: and [state retention](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-state-retention-oracle.property.test.ts). A transaction's optimistic state drops when its mutation function settles. Sync transactions committed while it persisted are held and publish with - that drop; `isPersisted` settles after that publication. Handler sync writes + that drop; `when('settled')` settles after that publication. Handler sync writes are generated at every cut, with and without awaiting their acceptance. Receipts stay pending until visible. With one persisting local mutation and no truncate, the first queued same-key source transaction consumes local diff --git a/docs/framework/react/overview.md b/docs/framework/react/overview.md index 484dd6b56d..7a9e25fecb 100644 --- a/docs/framework/react/overview.md +++ b/docs/framework/react/overview.md @@ -112,12 +112,10 @@ An explicit `queryKey` is the caller's identity contract. If its source object changes, change the key too, even when the source ID stays the same. Use a primitive scope token in the key; Collection objects cannot be hashed there. -Before 1.0, an unhashable query warns in development and keeps its legacy -mount-stable identity. The query still runs, but captured values inside opaque -logic only become reactive when they are represented in `queryKey`. In 1.0, an -unhashable query without `queryKey` will throw. If deriving identity becomes -expensive across renders, the hook warns once and suggests adding a `queryKey` -as a performance escape hatch. +An unhashable query without `queryKey` throws. Include captured values from +opaque logic in the key so changes to those values recreate the query. If +deriving identity becomes expensive across renders, the hook warns once and +suggests adding a `queryKey` as a performance escape hatch. #### What Happens When Identity Changes @@ -160,8 +158,6 @@ const { data } = useLiveQuery({ }) ``` -Dependency arrays are still accepted for backwards compatibility, but they warn in development and will be removed in 1.0. - For SSR setup, collection hydration, and migration details, see the [SSR and Hydration guide](../../guides/ssr.md). ### useLiveInfiniteQuery @@ -189,7 +185,7 @@ or skip remote rows. For server pagination, use an on-demand Query Collection whose `queryFn` fulfills `meta.loadSubsetOptions`. See the [server pagination guide](../../collections/query-collection.md#server-pagination-with-live-queries). -The deprecated dependency array is only available when using the query function variant, not when passing a pre-created collection. +React hook dependency arrays were removed in the first 1.0 RC. Use derived query identity or an explicit `queryKey`. ### useLiveSuspenseQuery diff --git a/docs/framework/react/reference/functions/useLiveInfiniteQuery.md b/docs/framework/react/reference/functions/useLiveInfiniteQuery.md index 29a55ad3f8..c53f25b283 100644 --- a/docs/framework/react/reference/functions/useLiveInfiniteQuery.md +++ b/docs/framework/react/reference/functions/useLiveInfiniteQuery.md @@ -9,7 +9,7 @@ title: useLiveInfiniteQuery function useLiveInfiniteQuery(liveQueryCollection, config): UseLiveInfiniteQueryReturnWithCollection; ``` -Defined in: [useLiveInfiniteQuery.ts:166](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L166) +Defined in: [useLiveInfiniteQuery.ts:153](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L153) Create an infinite query using a query function with live updates. @@ -51,13 +51,10 @@ Object with pages, data, and pagination controls ## Call Signature ```ts -function useLiveInfiniteQuery( - queryFn, - config, -deps?): UseLiveInfiniteQueryReturn; +function useLiveInfiniteQuery(queryFn, config): UseLiveInfiniteQueryReturn; ``` -Defined in: [useLiveInfiniteQuery.ts:176](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L176) +Defined in: [useLiveInfiniteQuery.ts:163](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L163) Create an infinite query using a query function with live updates. @@ -84,12 +81,6 @@ Query function that defines what data to fetch. Must include `.orderBy()` for se Configuration including pageSize and an optional initial page label -#### deps? - -`unknown`[] - -Deprecated array of dependencies that trigger query re-execution when changed - ### Returns [`UseLiveInfiniteQueryReturn`](../type-aliases/UseLiveInfiniteQueryReturn.md)\<`TContext`\> diff --git a/docs/framework/react/reference/functions/useLiveQuery.md b/docs/framework/react/reference/functions/useLiveQuery.md index 7e8833b9be..d1e0be9d04 100644 --- a/docs/framework/react/reference/functions/useLiveQuery.md +++ b/docs/framework/react/reference/functions/useLiveQuery.md @@ -6,10 +6,10 @@ title: useLiveQuery ## Call Signature ```ts -function useLiveQuery(queryFn, deps?): object; +function useLiveQuery(queryFn): object; ``` -Defined in: [useLiveQuery.ts:387](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L387) +Defined in: [useLiveQuery.ts:333](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L333) Create a live query using a query function. @@ -27,12 +27,6 @@ Create a live query using a query function. Query function that defines what data to fetch -#### deps? - -`unknown`[] - -Deprecated array of dependencies that trigger query re-execution when changed - ### Returns `object` @@ -193,10 +187,10 @@ return ( ## Call Signature ```ts -function useLiveQuery(queryFn, deps?): object; +function useLiveQuery(queryFn): object; ``` -Defined in: [useLiveQuery.ts:407](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L407) +Defined in: [useLiveQuery.ts:352](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L352) Create a live query using a query function. @@ -214,12 +208,6 @@ Create a live query using a query function. Query function that defines what data to fetch -#### deps? - -`unknown`[] - -Deprecated array of dependencies that trigger query re-execution when changed - ### Returns `object` @@ -384,10 +372,10 @@ return ( ## Call Signature ```ts -function useLiveQuery(queryFn, deps?): object; +function useLiveQuery(queryFn): object; ``` -Defined in: [useLiveQuery.ts:429](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L429) +Defined in: [useLiveQuery.ts:373](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L373) Create a live query using a query function. @@ -408,12 +396,6 @@ Create a live query using a query function. Query function that defines what data to fetch -#### deps? - -`unknown`[] - -Deprecated array of dependencies that trigger query re-execution when changed - ### Returns `object` @@ -578,10 +560,10 @@ return ( ## Call Signature ```ts -function useLiveQuery(queryFn, deps?): object; +function useLiveQuery(queryFn): object; ``` -Defined in: [useLiveQuery.ts:451](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L451) +Defined in: [useLiveQuery.ts:394](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L394) Create a live query using a query function. @@ -610,12 +592,6 @@ Create a live query using a query function. Query function that defines what data to fetch -#### deps? - -`unknown`[] - -Deprecated array of dependencies that trigger query re-execution when changed - ### Returns `object` @@ -777,10 +753,10 @@ return ( ## Call Signature ```ts -function useLiveQuery(queryFn, deps?): object; +function useLiveQuery(queryFn): object; ``` -Defined in: [useLiveQuery.ts:477](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L477) +Defined in: [useLiveQuery.ts:419](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L419) Create a live query using a query function. @@ -815,12 +791,6 @@ Create a live query using a query function. Query function that defines what data to fetch -#### deps? - -`unknown`[] - -Deprecated array of dependencies that trigger query re-execution when changed - ### Returns `object` @@ -990,7 +960,7 @@ return ( function useLiveQuery(config): object; ``` -Defined in: [useLiveQuery.ts:548](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L548) +Defined in: [useLiveQuery.ts:488](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L488) Create a live query using configuration object @@ -1134,375 +1104,7 @@ return
{data.length} items loaded
function useLiveQuery(config): object; ``` -Defined in: [useLiveQuery.ts:567](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L567) - -Create a live query using a query function. - -### Type Parameters - -#### TContext - -`TContext` *extends* `Context` - -### Parameters - -#### config - -[`ConditionalUseLiveQueryConfig`](../type-aliases/ConditionalUseLiveQueryConfig.md)\<`TContext`\> - -### Returns - -`object` - -Object with reactive data, state, and status information - -#### collection - -```ts -collection: - | Collection<{ [K in string | number | symbol]: ResultValue[K] }, string | number, { -}, StandardSchemaV1, { [K in string | number | symbol]: ResultValue[K] }> - | undefined; -``` - -#### data - -```ts -data: InferResultType | undefined; -``` - -#### isCleanedUp - -```ts -isCleanedUp: boolean; -``` - -#### isEnabled - -```ts -isEnabled: boolean; -``` - -#### isError - -```ts -isError: boolean; -``` - -#### isIdle - -```ts -isIdle: boolean; -``` - -#### isLoading - -```ts -isLoading: boolean; -``` - -#### isPersistedReady - -```ts -isPersistedReady: boolean; -``` - -#### isReady - -```ts -isReady: boolean; -``` - -#### persistedError - -```ts -persistedError: unknown; -``` - -#### persistedStatus - -```ts -persistedStatus: LiveQueryPersistedStatus; -``` - -#### state - -```ts -state: - | Map[K] }> - | undefined; -``` - -#### status - -```ts -status: UseLiveQueryStatus; -``` - -### Examples - -```ts -// Prefer config object syntax -const { data, isLoading } = useLiveQuery({ - query: (q) => - q.from({ todos: todosCollection }) - .where(({ todos }) => eq(todos.completed, false)) - .select(({ todos }) => ({ id: todos.id, text: todos.text })) -}) -``` - -```ts -// Single result query -const { data } = useLiveQuery({ - query: (q) => q.from({ todos: todosCollection }) - .where(({ todos }) => eq(todos.id, 1)) - .findOne() -}) -``` - -```ts -// Structured captured values are included in derived query identity -const { data, state } = useLiveQuery({ - query: (q) => q.from({ todos: todosCollection }) - .where(({ todos }) => gt(todos.priority, minPriority)), -}) -``` - -```ts -// Return undefined or null to disable a query -const { data, isEnabled } = useLiveQuery({ - query: (q) => { - if (!userId) return undefined - return q.from({ todos: todosCollection }) - .where(({ todos }) => eq(todos.userId, userId)) - }, -}) -``` - -```ts -// Join pattern -const { data } = useLiveQuery({ - query: (q) => - q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id) - ) - .select(({ issues, persons }) => ({ - id: issues.id, - title: issues.title, - userName: persons.name - })) -}) -``` - -```ts -// Handle loading and error states -const { data, isLoading, isError, status } = useLiveQuery({ - query: (q) => q.from({ todos: todoCollection }) -}) - -if (isLoading) return
Loading...
-if (isError) return
Error: {status}
- -return ( -
    - {data.map(todo =>
  • {todo.text}
  • )} -
-) -``` - -## Call Signature - -```ts -function useLiveQuery(config, deps?): object; -``` - -Defined in: [useLiveQuery.ts:586](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L586) - -Create a live query using a query function. - -### Type Parameters - -#### TContext - -`TContext` *extends* `Context` - -### Parameters - -#### config - -`LiveQueryCollectionConfig`\<`TContext`\> - -#### deps? - -`unknown`[] - -Deprecated array of dependencies that trigger query re-execution when changed - -### Returns - -`object` - -Object with reactive data, state, and status information - -#### collection - -```ts -collection: Collection<{ [K in string | number | symbol]: ResultValue[K] }, string | number, { -}>; -``` - -#### data - -```ts -data: InferResultType; -``` - -#### isCleanedUp - -```ts -isCleanedUp: boolean; -``` - -#### isEnabled - -```ts -isEnabled: true; -``` - -#### isError - -```ts -isError: boolean; -``` - -#### isIdle - -```ts -isIdle: boolean; -``` - -#### isLoading - -```ts -isLoading: boolean; -``` - -#### isPersistedReady - -```ts -isPersistedReady: boolean; -``` - -#### isReady - -```ts -isReady: boolean; -``` - -#### persistedError - -```ts -persistedError: unknown; -``` - -#### persistedStatus - -```ts -persistedStatus: LiveQueryPersistedStatus; -``` - -#### state - -```ts -state: Map[K] }>; -``` - -#### status - -```ts -status: CollectionStatus; -``` - -### Examples - -```ts -// Prefer config object syntax -const { data, isLoading } = useLiveQuery({ - query: (q) => - q.from({ todos: todosCollection }) - .where(({ todos }) => eq(todos.completed, false)) - .select(({ todos }) => ({ id: todos.id, text: todos.text })) -}) -``` - -```ts -// Single result query -const { data } = useLiveQuery({ - query: (q) => q.from({ todos: todosCollection }) - .where(({ todos }) => eq(todos.id, 1)) - .findOne() -}) -``` - -```ts -// Structured captured values are included in derived query identity -const { data, state } = useLiveQuery({ - query: (q) => q.from({ todos: todosCollection }) - .where(({ todos }) => gt(todos.priority, minPriority)), -}) -``` - -```ts -// Return undefined or null to disable a query -const { data, isEnabled } = useLiveQuery({ - query: (q) => { - if (!userId) return undefined - return q.from({ todos: todosCollection }) - .where(({ todos }) => eq(todos.userId, userId)) - }, -}) -``` - -```ts -// Join pattern -const { data } = useLiveQuery({ - query: (q) => - q.from({ issues: issueCollection }) - .join({ persons: personCollection }, ({ issues, persons }) => - eq(issues.userId, persons.id) - ) - .select(({ issues, persons }) => ({ - id: issues.id, - title: issues.title, - userName: persons.name - })) -}) -``` - -```ts -// Handle loading and error states -const { data, isLoading, isError, status } = useLiveQuery({ - query: (q) => q.from({ todos: todoCollection }) -}) - -if (isLoading) return
Loading...
-if (isError) return
Error: {status}
- -return ( -
    - {data.map(todo =>
  • {todo.text}
  • )} -
-) -``` - -## Call Signature - -```ts -function useLiveQuery(config, deps): object; -``` - -Defined in: [useLiveQuery.ts:606](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L606) +Defined in: [useLiveQuery.ts:507](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L507) Create a live query using a query function. @@ -1518,12 +1120,6 @@ Create a live query using a query function. [`ConditionalUseLiveQueryConfig`](../type-aliases/ConditionalUseLiveQueryConfig.md)\<`TContext`\> -#### deps - -`unknown`[] - -Deprecated array of dependencies that trigger query re-execution when changed - ### Returns `object` @@ -1691,7 +1287,7 @@ return ( function useLiveQuery(liveQueryCollection): object; ``` -Defined in: [useLiveQuery.ts:655](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L655) +Defined in: [useLiveQuery.ts:555](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L555) Subscribe to an existing live query collection @@ -1837,7 +1433,7 @@ return
{data.map(item => )}
function useLiveQuery(liveQueryCollection): object; ``` -Defined in: [useLiveQuery.ts:678](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L678) +Defined in: [useLiveQuery.ts:578](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L578) Create a live query using a query function. diff --git a/docs/framework/react/reference/functions/useLiveSuspenseQuery.md b/docs/framework/react/reference/functions/useLiveSuspenseQuery.md index c07e520d8e..e04cfc8bc9 100644 --- a/docs/framework/react/reference/functions/useLiveSuspenseQuery.md +++ b/docs/framework/react/reference/functions/useLiveSuspenseQuery.md @@ -6,10 +6,10 @@ title: useLiveSuspenseQuery ## Call Signature ```ts -function useLiveSuspenseQuery(queryFn, deps?): object; +function useLiveSuspenseQuery(queryFn): object; ``` -Defined in: [useLiveSuspenseQuery.ts:151](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L151) +Defined in: [useLiveSuspenseQuery.ts:149](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L149) Create a live query with React Suspense support @@ -27,12 +27,6 @@ Create a live query with React Suspense support Query function that defines what data to fetch -#### deps? - -`unknown`[] - -Deprecated array of dependencies that trigger query re-execution when changed - ### Returns `object` @@ -162,7 +156,7 @@ function Profile({ userId }: { userId: string }) { function useLiveSuspenseQuery(config): object; ``` -Defined in: [useLiveSuspenseQuery.ts:161](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L161) +Defined in: [useLiveSuspenseQuery.ts:158](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L158) Create a live query with React Suspense support @@ -303,162 +297,11 @@ function Profile({ userId }: { userId: string }) { ## Call Signature -```ts -function useLiveSuspenseQuery(config, deps?): object; -``` - -Defined in: [useLiveSuspenseQuery.ts:170](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L170) - -Create a live query with React Suspense support - -### Type Parameters - -#### TContext - -`TContext` *extends* `Context` - -### Parameters - -#### config - -`LiveQueryCollectionConfig`\<`TContext`\> - -#### deps? - -`unknown`[] - -Deprecated array of dependencies that trigger query re-execution when changed - -### Returns - -`object` - -Object with reactive data and state - data is guaranteed to be defined - -#### collection - -```ts -collection: Collection<{ [K in string | number | symbol]: ResultValue[K] }, string | number, { -}>; -``` - -#### data - -```ts -data: InferResultType; -``` - -#### state - -```ts -state: Map[K] }>; -``` - -### Throws - -Promise when data is loading (caught by Suspense boundary) - -### Throws - -Error when collection fails (caught by Error boundary) - -### Examples - -```ts -// Basic usage with Suspense -function TodoList() { - const { data } = useLiveSuspenseQuery({ - query: (q) => - q.from({ todos: todosCollection }) - .where(({ todos }) => eq(todos.completed, false)) - .select(({ todos }) => ({ id: todos.id, text: todos.text })) - }) - - return ( -
    - {data.map(todo =>
  • {todo.text}
  • )} -
- ) -} - -function App() { - return ( - Loading...}> - - - ) -} -``` - -```ts -// Single result query -const { data } = useLiveSuspenseQuery( - (q) => q.from({ todos: todosCollection }) - .where(({ todos }) => eq(todos.id, 1)) - .findOne() -) -// data is guaranteed to be the single item (or undefined if not found) -``` - -```ts -// Structured captured values are included in derived query identity and trigger re-suspension -const { data } = useLiveSuspenseQuery({ - query: (q) => q.from({ todos: todosCollection }) - .where(({ todos }) => gt(todos.priority, minPriority)), -}) -``` - -```ts -// With Error boundary -function App() { - return ( - Error loading data}> - Loading...}> - - - - ) -} -``` - -### Remarks - -**Important:** This hook does NOT support disabled queries (returning undefined/null). -Following TanStack Query's useSuspenseQuery design, the query callback must always -return a valid query, collection, or config object. - -❌ **This will cause a type error:** -```ts -useLiveSuspenseQuery( - (q) => userId ? q.from({ users }) : undefined // ❌ Error! -) -``` - -✅ **Use conditional rendering instead:** -```ts -function Profile({ userId }: { userId: string }) { - const { data } = useLiveSuspenseQuery({ - query: (q) => q.from({ users }).where(({ users }) => eq(users.id, userId)), - }) - return
{data.name}
-} - -// In parent component: -{userId ? :
No user
} -``` - -✅ **For optional inputs, conditionally render a component with complete query inputs:** -```ts -{userId ? :
No user
} -``` - -## Call Signature - ```ts function useLiveSuspenseQuery(liveQueryCollection): object; ``` -Defined in: [useLiveSuspenseQuery.ts:180](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L180) +Defined in: [useLiveSuspenseQuery.ts:167](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L167) Create a live query with React Suspense support @@ -610,7 +453,7 @@ function Profile({ userId }: { userId: string }) { function useLiveSuspenseQuery(liveQueryCollection): object; ``` -Defined in: [useLiveSuspenseQuery.ts:193](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L193) +Defined in: [useLiveSuspenseQuery.ts:180](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveSuspenseQuery.ts#L180) Create a live query with React Suspense support diff --git a/docs/framework/react/reference/functions/usePacedMutations.md b/docs/framework/react/reference/functions/usePacedMutations.md index 00affeb216..c2a049b88b 100644 --- a/docs/framework/react/reference/functions/usePacedMutations.md +++ b/docs/framework/react/reference/functions/usePacedMutations.md @@ -74,7 +74,7 @@ function AutoSaveForm({ formId }: { formId: string }) { // Optional: await persistence or handle errors try { - await tx.isPersisted.promise + await tx.when('settled') console.log('Saved!') } catch (error) { console.error('Save failed:', error) diff --git a/docs/framework/react/reference/type-aliases/ConditionalUseLiveQueryConfig.md b/docs/framework/react/reference/type-aliases/ConditionalUseLiveQueryConfig.md index 1926ba2bb6..56f350b1b1 100644 --- a/docs/framework/react/reference/type-aliases/ConditionalUseLiveQueryConfig.md +++ b/docs/framework/react/reference/type-aliases/ConditionalUseLiveQueryConfig.md @@ -7,7 +7,7 @@ title: ConditionalUseLiveQueryConfig type ConditionalUseLiveQueryConfig = UseLiveQueryConfigOptions & object; ``` -Defined in: [useLiveQuery.ts:133](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L133) +Defined in: [useLiveQuery.ts:131](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L131) ## Type Declaration diff --git a/docs/framework/react/reference/type-aliases/LiveQueryKey.md b/docs/framework/react/reference/type-aliases/LiveQueryKey.md index bfce3663b8..d24f140947 100644 --- a/docs/framework/react/reference/type-aliases/LiveQueryKey.md +++ b/docs/framework/react/reference/type-aliases/LiveQueryKey.md @@ -7,4 +7,4 @@ title: LiveQueryKey type LiveQueryKey = ReadonlyArray; ``` -Defined in: [useLiveQuery.ts:109](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L109) +Defined in: [useLiveQuery.ts:107](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L107) diff --git a/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryConfig.md b/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryConfig.md index 72f939f102..da95cac024 100644 --- a/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryConfig.md +++ b/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryConfig.md @@ -7,7 +7,7 @@ title: UseLiveInfiniteQueryConfig type UseLiveInfiniteQueryConfig<_TContext> = object; ``` -Defined in: [useLiveInfiniteQuery.ts:54](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L54) +Defined in: [useLiveInfiniteQuery.ts:49](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L49) ## Type Parameters @@ -23,7 +23,7 @@ Defined in: [useLiveInfiniteQuery.ts:54](https://github.com/TanStack/db/blob/mai optional client: DbClient; ``` -Defined in: [useLiveInfiniteQuery.ts:62](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L62) +Defined in: [useLiveInfiniteQuery.ts:57](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L57) Override the nearest DbProvider for this query. @@ -35,7 +35,7 @@ Override the nearest DbProvider for this query. optional initialPageParam: number; ``` -Defined in: [useLiveInfiniteQuery.ts:65](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L65) +Defined in: [useLiveInfiniteQuery.ts:60](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L60) First result-page label, not a server cursor or remote offset. @@ -47,7 +47,7 @@ First result-page label, not a server cursor or remote offset. optional pageSize: number; ``` -Defined in: [useLiveInfiniteQuery.ts:63](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L63) +Defined in: [useLiveInfiniteQuery.ts:58](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L58) *** @@ -57,7 +57,7 @@ Defined in: [useLiveInfiniteQuery.ts:63](https://github.com/TanStack/db/blob/mai optional queryKey: LiveQueryKey; ``` -Defined in: [useLiveInfiniteQuery.ts:60](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L60) +Defined in: [useLiveInfiniteQuery.ts:55](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L55) Explicit identity for queries that contain opaque functional variants or are hot enough that deriving identity from structured IR is too expensive. diff --git a/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryReturn.md b/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryReturn.md index a4501cd291..7b4b9efbd2 100644 --- a/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryReturn.md +++ b/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryReturn.md @@ -7,7 +7,7 @@ title: UseLiveInfiniteQueryReturn type UseLiveInfiniteQueryReturn = Omit, "data"> & object; ``` -Defined in: [useLiveInfiniteQuery.ts:68](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L68) +Defined in: [useLiveInfiniteQuery.ts:63](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L63) ## Type Declaration diff --git a/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryReturnWithCollection.md b/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryReturnWithCollection.md index ffe9403754..e3d02f3919 100644 --- a/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryReturnWithCollection.md +++ b/docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryReturnWithCollection.md @@ -7,7 +7,7 @@ title: UseLiveInfiniteQueryReturnWithCollection type UseLiveInfiniteQueryReturnWithCollection = object; ``` -Defined in: [useLiveInfiniteQuery.ts:81](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L81) +Defined in: [useLiveInfiniteQuery.ts:76](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L76) ## Type Parameters @@ -31,7 +31,7 @@ Defined in: [useLiveInfiniteQuery.ts:81](https://github.com/TanStack/db/blob/mai collection: Collection & NonSingleResult; ``` -Defined in: [useLiveInfiniteQuery.ts:88](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L88) +Defined in: [useLiveInfiniteQuery.ts:83](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L83) *** @@ -41,7 +41,7 @@ Defined in: [useLiveInfiniteQuery.ts:88](https://github.com/TanStack/db/blob/mai data: TResult[]; ``` -Defined in: [useLiveInfiniteQuery.ts:86](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L86) +Defined in: [useLiveInfiniteQuery.ts:81](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L81) *** @@ -51,7 +51,7 @@ Defined in: [useLiveInfiniteQuery.ts:86](https://github.com/TanStack/db/blob/mai error: unknown; ``` -Defined in: [useLiveInfiniteQuery.ts:104](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L104) +Defined in: [useLiveInfiniteQuery.ts:99](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L99) *** @@ -61,7 +61,7 @@ Defined in: [useLiveInfiniteQuery.ts:104](https://github.com/TanStack/db/blob/ma fetchNextPage: () => Promise; ``` -Defined in: [useLiveInfiniteQuery.ts:101](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L101) +Defined in: [useLiveInfiniteQuery.ts:96](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L96) #### Returns @@ -75,7 +75,7 @@ Defined in: [useLiveInfiniteQuery.ts:101](https://github.com/TanStack/db/blob/ma hasNextPage: boolean; ``` -Defined in: [useLiveInfiniteQuery.ts:102](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L102) +Defined in: [useLiveInfiniteQuery.ts:97](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L97) *** @@ -85,7 +85,7 @@ Defined in: [useLiveInfiniteQuery.ts:102](https://github.com/TanStack/db/blob/ma isCleanedUp: boolean; ``` -Defined in: [useLiveInfiniteQuery.ts:97](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L97) +Defined in: [useLiveInfiniteQuery.ts:92](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L92) *** @@ -95,7 +95,7 @@ Defined in: [useLiveInfiniteQuery.ts:97](https://github.com/TanStack/db/blob/mai isEnabled: true; ``` -Defined in: [useLiveInfiniteQuery.ts:98](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L98) +Defined in: [useLiveInfiniteQuery.ts:93](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L93) *** @@ -105,7 +105,7 @@ Defined in: [useLiveInfiniteQuery.ts:98](https://github.com/TanStack/db/blob/mai isError: boolean; ``` -Defined in: [useLiveInfiniteQuery.ts:96](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L96) +Defined in: [useLiveInfiniteQuery.ts:91](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L91) *** @@ -115,7 +115,7 @@ Defined in: [useLiveInfiniteQuery.ts:96](https://github.com/TanStack/db/blob/mai isFetchingNextPage: boolean; ``` -Defined in: [useLiveInfiniteQuery.ts:103](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L103) +Defined in: [useLiveInfiniteQuery.ts:98](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L98) *** @@ -125,7 +125,7 @@ Defined in: [useLiveInfiniteQuery.ts:103](https://github.com/TanStack/db/blob/ma isIdle: boolean; ``` -Defined in: [useLiveInfiniteQuery.ts:95](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L95) +Defined in: [useLiveInfiniteQuery.ts:90](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L90) *** @@ -135,7 +135,7 @@ Defined in: [useLiveInfiniteQuery.ts:95](https://github.com/TanStack/db/blob/mai isLoading: boolean; ``` -Defined in: [useLiveInfiniteQuery.ts:90](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L90) +Defined in: [useLiveInfiniteQuery.ts:85](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L85) *** @@ -145,7 +145,7 @@ Defined in: [useLiveInfiniteQuery.ts:90](https://github.com/TanStack/db/blob/mai isPersistedReady: boolean; ``` -Defined in: [useLiveInfiniteQuery.ts:93](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L93) +Defined in: [useLiveInfiniteQuery.ts:88](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L88) *** @@ -155,7 +155,7 @@ Defined in: [useLiveInfiniteQuery.ts:93](https://github.com/TanStack/db/blob/mai isReady: boolean; ``` -Defined in: [useLiveInfiniteQuery.ts:91](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L91) +Defined in: [useLiveInfiniteQuery.ts:86](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L86) *** @@ -165,7 +165,7 @@ Defined in: [useLiveInfiniteQuery.ts:91](https://github.com/TanStack/db/blob/mai pageParams: number[]; ``` -Defined in: [useLiveInfiniteQuery.ts:100](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L100) +Defined in: [useLiveInfiniteQuery.ts:95](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L95) *** @@ -175,7 +175,7 @@ Defined in: [useLiveInfiniteQuery.ts:100](https://github.com/TanStack/db/blob/ma pages: TResult[][]; ``` -Defined in: [useLiveInfiniteQuery.ts:99](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L99) +Defined in: [useLiveInfiniteQuery.ts:94](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L94) *** @@ -185,7 +185,7 @@ Defined in: [useLiveInfiniteQuery.ts:99](https://github.com/TanStack/db/blob/mai persistedError: unknown | undefined; ``` -Defined in: [useLiveInfiniteQuery.ts:94](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L94) +Defined in: [useLiveInfiniteQuery.ts:89](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L89) *** @@ -195,7 +195,7 @@ Defined in: [useLiveInfiniteQuery.ts:94](https://github.com/TanStack/db/blob/mai persistedStatus: LiveQueryPersistedStatus; ``` -Defined in: [useLiveInfiniteQuery.ts:92](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L92) +Defined in: [useLiveInfiniteQuery.ts:87](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L87) *** @@ -205,7 +205,7 @@ Defined in: [useLiveInfiniteQuery.ts:92](https://github.com/TanStack/db/blob/mai state: Map; ``` -Defined in: [useLiveInfiniteQuery.ts:87](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L87) +Defined in: [useLiveInfiniteQuery.ts:82](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L82) *** @@ -215,4 +215,4 @@ Defined in: [useLiveInfiniteQuery.ts:87](https://github.com/TanStack/db/blob/mai status: CollectionStatus; ``` -Defined in: [useLiveInfiniteQuery.ts:89](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L89) +Defined in: [useLiveInfiniteQuery.ts:84](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L84) diff --git a/docs/framework/react/reference/type-aliases/UseLiveQueryConfig.md b/docs/framework/react/reference/type-aliases/UseLiveQueryConfig.md index f42f993e61..e70c28db31 100644 --- a/docs/framework/react/reference/type-aliases/UseLiveQueryConfig.md +++ b/docs/framework/react/reference/type-aliases/UseLiveQueryConfig.md @@ -7,7 +7,7 @@ title: UseLiveQueryConfig type UseLiveQueryConfig = UseLiveQueryConfigOptions & Pick, "query">; ``` -Defined in: [useLiveQuery.ts:129](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L129) +Defined in: [useLiveQuery.ts:127](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L127) ## Type Parameters diff --git a/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md b/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md index 4219dcacaa..27a6afba13 100644 --- a/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md +++ b/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md @@ -7,4 +7,4 @@ title: UseLiveQueryStatus type UseLiveQueryStatus = CollectionStatus | "disabled"; ``` -Defined in: [useLiveQuery.ts:108](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L108) +Defined in: [useLiveQuery.ts:106](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L106) diff --git a/docs/framework/solid/overview.md b/docs/framework/solid/overview.md index d69a75b750..df184cb89a 100644 --- a/docs/framework/solid/overview.md +++ b/docs/framework/solid/overview.md @@ -36,7 +36,7 @@ function TodoList() { return ( Loading...}>
    - + {(todo) =>
  • {todo.text}
  • }
@@ -45,7 +45,7 @@ function TodoList() { } ``` -**Note:** `query.data` returns an array directly (not a function), but status fields like `isLoading()`, `status()`, etc. are accessor functions. +**Note:** `query()` reads the array, while status fields such as `isLoading` and `status` are properties. ### Reactive Queries with Signals @@ -62,7 +62,7 @@ function FilteredTodos(props: { minPriority: number }) { .where(({ todos }) => gt(todos.priority, props.minPriority)) ) - return
{query.data.length} high-priority todos
+ return
{query().length} high-priority todos
} ``` @@ -98,7 +98,7 @@ function TodoList() { -
{query.data.length} todos
+
{query().length} todos
) } @@ -125,7 +125,7 @@ function TodoList() { ) // Solid automatically tracks minPriority() and recomputes when it changes - return
{query.data.length} todos
+ return
{query().length} todos
} ``` @@ -147,7 +147,7 @@ function TodoList() { ) // Won't update when minPriority changes! - return
{query.data.length} todos
+ return
{query().length} todos
} ``` @@ -162,7 +162,7 @@ function AllTodos() { q.from({ todos: todosCollection }) ) - return
{query.data.length} todos
+ return
{query().length} todos
} ``` @@ -184,6 +184,6 @@ function TodoList() { // Pass existing collection const query = useLiveQuery(() => todosQuery) - return
{query.data.length} todos
+ return
{query().length} todos
} ``` diff --git a/docs/framework/solid/reference/functions/useLiveQuery.md b/docs/framework/solid/reference/functions/useLiveQuery.md index 4f05f23ecc..61c35e14fb 100644 --- a/docs/framework/solid/reference/functions/useLiveQuery.md +++ b/docs/framework/solid/reference/functions/useLiveQuery.md @@ -29,6 +29,8 @@ Query function that defines what data to fetch ### Returns +`Accessor`\<`InferResultType`\<`TContext`\>\> & `object` + Accessor that returns data with Suspense support, with state and status information as properties ### Examples @@ -109,7 +111,7 @@ return ( function useLiveQuery(queryFn): Accessor> & object; ``` -Defined in: [useLiveQuery.ts:134](https://github.com/TanStack/db/blob/main/packages/solid-db/src/useLiveQuery.ts#L134) +Defined in: [useLiveQuery.ts:129](https://github.com/TanStack/db/blob/main/packages/solid-db/src/useLiveQuery.ts#L129) Create a live query using a query function @@ -129,6 +131,8 @@ Query function that defines what data to fetch ### Returns +`Accessor`\<`InferConditionalResultType`\<`TContext`\>\> & `object` + Accessor that returns data with Suspense support, with state and status information as properties ### Examples @@ -209,7 +213,7 @@ return ( function useLiveQuery(config): Accessor> & object; ``` -Defined in: [useLiveQuery.ts:198](https://github.com/TanStack/db/blob/main/packages/solid-db/src/useLiveQuery.ts#L198) +Defined in: [useLiveQuery.ts:188](https://github.com/TanStack/db/blob/main/packages/solid-db/src/useLiveQuery.ts#L188) Create a live query using configuration object @@ -229,6 +233,8 @@ Configuration object with query and options ### Returns +`Accessor`\<`InferResultType`\<`TContext`\>\> & `object` + Accessor that returns data with Suspense support, with state and status information as properties ### Examples @@ -278,7 +284,7 @@ return ( function useLiveQuery(liveQueryCollection): Accessor & object; ``` -Defined in: [useLiveQuery.ts:255](https://github.com/TanStack/db/blob/main/packages/solid-db/src/useLiveQuery.ts#L255) +Defined in: [useLiveQuery.ts:240](https://github.com/TanStack/db/blob/main/packages/solid-db/src/useLiveQuery.ts#L240) Subscribe to an existing live query collection @@ -306,6 +312,8 @@ Pre-created live query collection to subscribe to ### Returns +`Accessor`\<`TResult`[]\> & `object` + Accessor that returns data with Suspense support, with state and status information as properties ### Examples @@ -350,7 +358,7 @@ return ( function useLiveQuery(liveQueryCollection): Accessor & object; ``` -Defined in: [useLiveQuery.ts:283](https://github.com/TanStack/db/blob/main/packages/solid-db/src/useLiveQuery.ts#L283) +Defined in: [useLiveQuery.ts:263](https://github.com/TanStack/db/blob/main/packages/solid-db/src/useLiveQuery.ts#L263) Create a live query using a query function @@ -376,6 +384,8 @@ Create a live query using a query function ### Returns +`Accessor`\<`TResult` \| `undefined`\> & `object` + Accessor that returns data with Suspense support, with state and status information as properties ### Examples diff --git a/docs/guides/collection-options-creator.md b/docs/guides/collection-options-creator.md index 575523630f..e4bbb9326a 100644 --- a/docs/guides/collection-options-creator.md +++ b/docs/guides/collection-options-creator.md @@ -397,7 +397,7 @@ There are two distinct patterns for handling mutations in collection options cre The user provides mutation handlers in the config. Your collection creator passes them through. -**Note:** Handler return values are deprecated. Users should trigger refetch/sync within their handlers. +Mutation handlers return `Promise`. Trigger refetch or sync within the handler and await it before the handler completes. ```typescript interface MyCollectionConfig { @@ -959,7 +959,7 @@ const collection = createCollection( await api.createTodos(transaction.mutations.map(m => m.modified)) // User explicitly triggers refetch await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. + // Prevent QueryCollection from refetching again. return { refetch: false } } }) @@ -990,7 +990,7 @@ const collection = createCollection( 5. **Race Conditions** - Start listeners before initial fetch and buffer events 6. **Type safety** - Use TypeScript generics to maintain type safety throughout 7. **Provide utilities** - Export sync-engine-specific utilities for advanced use cases -8. **Handler return values are deprecated** - Mutation handlers should coordinate sync internally (via `await`) rather than returning values. For Electric collections, use `await collection.utils.awaitTxId(txid)` instead of returning the txid +8. **Coordinate sync in handlers** - Mutation handlers should await sync internally. For Electric collections, use `await collection.utils.awaitTxId(txid)`. ## Testing Your Collection diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index 8b1ea145c8..bcb4a3aa2c 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -32,7 +32,6 @@ The result types are automatically inferred from your query structure, providing Live query results include computed, read-only virtual properties on every row: - `$hasPendingWrites`: `true` while a pending local optimistic mutation affects the row; otherwise `false`. It is always `false` for local-only collections. It does not indicate server acknowledgement. -- `$synced` (deprecated): the inverse of `$hasPendingWrites`. It will be removed in the 1.0 RC. Replace `row.$synced` with `!row.$hasPendingWrites`, and `eq(row.$synced, true)` with `eq(row.$hasPendingWrites, false)`. - `$origin`: Collection attribution for the current row. Optimistic rows are `"local"`; source rows use key and timing, not a source client ID. A peer write can be `"local"`. A truncate does not consume an active mutation's attribution for an untouched key. See [VirtualOrigin](../reference/type-aliases/VirtualOrigin.md) for settlement, truncate, and overlap rules. Local-only Collections always use `"local"`. - `$key`: the row key for the result. - `$collectionId`: the source collection ID. @@ -207,10 +206,8 @@ export class UserListComponent { ``` > **Note:** React hooks derive query identity from structured query IR by -> default. Dependency arrays are still accepted for backwards compatibility, -> but warn in development and will be removed in 1.0. Unhashable queries also -> warn and keep legacy mount-stable identity until 1.0; add `queryKey` to make -> captured opaque values reactive. See the [React Adapter +> default. An unhashable query requires `queryKey`; include captured opaque +> values in the key to make them reactive. See the [React Adapter > documentation](../framework/react/overview#query-identity) for details. For server rendering and hydration, live query preloading transports the ordered @@ -240,10 +237,8 @@ function UserSearch({ search }: { search: string }) { You can also provide a `queryKey` as a performance escape hatch for a very hot render path, but normal structured queries should omit it. -React development builds detect both cases. Before 1.0, opaque, unhashable IR -warns and keeps legacy mount-stable identity; repeated expensive identity -derivation also warns once. Both warnings point to the same `queryKey` escape -hatch. +An opaque, unhashable query without `queryKey` throws. In development, repeated +expensive identity derivation warns once and suggests an explicit `queryKey`. For more details on framework integration, see the [React](../framework/react/overview), [Vue](../framework/vue/overview), and [Angular](../framework/angular/overview) adapter documentation. diff --git a/docs/guides/mutations.md b/docs/guides/mutations.md index 5b1160fe8b..502e6ed90e 100644 --- a/docs/guides/mutations.md +++ b/docs/guides/mutations.md @@ -271,9 +271,7 @@ with the transaction on success and rejects with the original error on failure (or `undefined` for a rollback without an error). It does not by itself prove that a server uploaded, confirmed, or returned the write. It proves those stronger guarantees only when the handler waits for that backend observation -before returning. The old `tx.isPersisted.promise` remains available for the -pre-RC releases but is deprecated and will be removed in the 1.0 RC; replace -each use with `tx.when('settled')`. +before returning. ### Concurrent Optimistic Transactions @@ -528,8 +526,7 @@ onUpdate: async ({ transaction, collection }) => { ) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the current pre-1.0 wrapper from refetching a second time. - // Remove this return in v1.0. + // Prevent QueryCollection from refetching a second time. return { refetch: false } } ``` diff --git a/docs/guides/ssr.md b/docs/guides/ssr.md index 8fa0c17011..4cc066938b 100644 --- a/docs/guides/ssr.md +++ b/docs/guides/ssr.md @@ -37,13 +37,13 @@ instances still exist. The migration is required when you want SSR-safe request isolation, hydration, incremental chunks, Suspense streaming, or the 1.0-ready React hook shape. -The old dependency-array form now warns: +The dependency-array form was removed in the first 1.0 RC: ```tsx useLiveQuery((q) => q.from({ todos }).where(...), [status]) ``` -It still works, but warns in development and will be removed in 1.0. Prefer: +Use the query config form instead: ```tsx useLiveQuery({ @@ -579,22 +579,13 @@ Common reasons to add `queryKey`: the structured query - a render path where derived identity becomes measurably expensive -Before 1.0, DB warns when structured IR cannot be hashed and preserves the -legacy mount-stable identity. The query still works, but captured values inside -opaque logic are not reactive unless they are represented in `queryKey`. In 1.0, -an unhashable query without `queryKey` will throw. +An unhashable query without `queryKey` throws. Include captured values from +opaque logic in `queryKey` so changes to those values recreate the query. DB also warns once in development if deriving identity becomes expensive enough that an explicit `queryKey` would be better. -Dependency arrays are accepted for backwards compatibility: - -```tsx -useLiveQuery((q) => q.from({ todo: todoCollection }), [status]) -``` - -They warn in development and will be removed in 1.0. Migrate to the config -object form: +Use the config object form: ```tsx useLiveQuery({ @@ -757,24 +748,22 @@ rendering instead. ## Compatibility -No existing public API is removed by this change. +The SSR APIs remain available in the first 1.0 RC. React dependency-array hook forms have been removed. Still supported: - `createCollection(...)` - passing collection instances to `useLiveQuery(...)` -- `useLiveQuery(queryFn, deps)` -- `useLiveSuspenseQuery(queryFn, deps)` - mutation APIs such as `insert`, `update`, `delete`, `subscribe`, and optimistic mutation helpers Warnings: -- React dependency arrays warn in development and will be removed in 1.0. -- Opaque query IR without `queryKey` warns in development and keeps legacy - mount-stable identity until 1.0. In 1.0 it will throw. - Expensive derived identity warns in development and suggests `queryKey`. +Opaque query IR without a serializable `queryKey` throws when React cannot +derive a stable identity. + Required for SSR: - stable explicit collection ids @@ -789,7 +778,10 @@ Required for render-time Suspense streaming: - `useLiveSuspenseQuery(...)` inside a Suspense boundary - a stable derived query identity or explicit serializable `queryKey` -## Detailed Changelog +## SSR Introduction Changelog + +This section records the original SSR feature release. Its React dependency +array deprecation was completed in the first 1.0 RC. ### Added @@ -829,7 +821,7 @@ Required for render-time Suspense streaming: `from`, `join`, `leftJoin`, and `unionAll` sources when a `DbProvider` is present. - React live query identity is derived from normalized structured IR when no - explicit `queryKey` or legacy dependency array is supplied. + explicit `queryKey` is supplied. - Explicit collection preloading serializes normalized collection rows. - Live-query preloading and render-time discovery serialize ordered result snapshots without implicitly serializing source collections. @@ -844,10 +836,10 @@ Required for render-time Suspense streaming: - Incremental chunks use the same collection payload shape as full dehydration. - Streamed live-query promises resolve to live-query result snapshots. -### Deprecated +### Since Removed - React dependency arrays for `useLiveQuery` and wrappers that delegate to it. - They still work and warn in development. They are planned for removal in 1.0. + They were removed in the first 1.0 RC. ### Not Changed diff --git a/docs/overview.md b/docs/overview.md index 454afb1877..2e1009d3c8 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -462,7 +462,7 @@ const todoCollection = collectionOptions("todos", (client) => // Handle the local write by sending it to your API. await api.todos.create(newTodo) await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. + // Prevent QueryCollection from refetching again. return { refetch: false } }, // also add onUpdate, onDelete as needed. @@ -483,7 +483,7 @@ const listCollection = collectionOptions("todo-lists", (client) => // Handle the local write by sending it to your API. await api.todoLists.create(newTodo) await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. + // Prevent QueryCollection from refetching again. return { refetch: false } }, // also add onUpdate, onDelete as needed. diff --git a/docs/reference/classes/AggregateFunctionNotInSelectError.md b/docs/reference/classes/AggregateFunctionNotInSelectError.md index 54dbc5a00f..d4a9ae2666 100644 --- a/docs/reference/classes/AggregateFunctionNotInSelectError.md +++ b/docs/reference/classes/AggregateFunctionNotInSelectError.md @@ -3,7 +3,7 @@ id: AggregateFunctionNotInSelectError title: AggregateFunctionNotInSelectError --- -Defined in: [packages/db/src/errors.ts:967](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L967) +Defined in: [packages/db/src/errors.ts:948](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L948) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:967](https://github.com/TanStack/db/blob/ new AggregateFunctionNotInSelectError(functionName): AggregateFunctionNotInSelectError; ``` -Defined in: [packages/db/src/errors.ts:968](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L968) +Defined in: [packages/db/src/errors.ts:949](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L949) #### Parameters diff --git a/docs/reference/classes/CannotCombineEmptyExpressionListError.md b/docs/reference/classes/CannotCombineEmptyExpressionListError.md index 1507d2eac6..664067c1df 100644 --- a/docs/reference/classes/CannotCombineEmptyExpressionListError.md +++ b/docs/reference/classes/CannotCombineEmptyExpressionListError.md @@ -3,7 +3,7 @@ id: CannotCombineEmptyExpressionListError title: CannotCombineEmptyExpressionListError --- -Defined in: [packages/db/src/errors.ts:1099](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1099) +Defined in: [packages/db/src/errors.ts:1080](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1080) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1099](https://github.com/TanStack/db/blob new CannotCombineEmptyExpressionListError(): CannotCombineEmptyExpressionListError; ``` -Defined in: [packages/db/src/errors.ts:1100](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1100) +Defined in: [packages/db/src/errors.ts:1081](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1081) #### Returns diff --git a/docs/reference/classes/CollectionPreloadAbortedError.md b/docs/reference/classes/CollectionPreloadAbortedError.md index 5578be5873..1311d80ad5 100644 --- a/docs/reference/classes/CollectionPreloadAbortedError.md +++ b/docs/reference/classes/CollectionPreloadAbortedError.md @@ -3,7 +3,7 @@ id: CollectionPreloadAbortedError title: CollectionPreloadAbortedError --- -Defined in: [packages/db/src/errors.ts:1068](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1068) +Defined in: [packages/db/src/errors.ts:1049](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1049) A collection was cleaned up before its initial preload became ready. @@ -19,7 +19,7 @@ A collection was cleaned up before its initial preload became ready. new CollectionPreloadAbortedError(): CollectionPreloadAbortedError; ``` -Defined in: [packages/db/src/errors.ts:1069](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1069) +Defined in: [packages/db/src/errors.ts:1050](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1050) #### Returns diff --git a/docs/reference/classes/DuplicateAliasInSubqueryError.md b/docs/reference/classes/DuplicateAliasInSubqueryError.md deleted file mode 100644 index e9dd8e6dc2..0000000000 --- a/docs/reference/classes/DuplicateAliasInSubqueryError.md +++ /dev/null @@ -1,229 +0,0 @@ ---- -id: DuplicateAliasInSubqueryError -title: DuplicateAliasInSubqueryError ---- - -Defined in: [packages/db/src/errors.ts:798](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L798) - -Retained for callers that imported this formerly thrown error. - -## Deprecated - -Nested queries may now shadow ancestor aliases. Duplicate aliases -are rejected only within one lexical scope or union branch namespace. - -## Extends - -- [`QueryCompilationError`](QueryCompilationError.md) - -## Constructors - -### Constructor - -```ts -new DuplicateAliasInSubqueryError(alias, parentAliases): DuplicateAliasInSubqueryError; -``` - -Defined in: [packages/db/src/errors.ts:799](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L799) - -#### Parameters - -##### alias - -`string` - -##### parentAliases - -`string`[] - -#### Returns - -`DuplicateAliasInSubqueryError` - -#### Overrides - -[`QueryCompilationError`](QueryCompilationError.md).[`constructor`](QueryCompilationError.md#constructor) - -## Properties - -### ~~cause?~~ - -```ts -optional cause: unknown; -``` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:26 - -#### Inherited from - -[`QueryCompilationError`](QueryCompilationError.md).[`cause`](QueryCompilationError.md#cause) - -*** - -### ~~message~~ - -```ts -message: string; -``` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1077 - -#### Inherited from - -[`QueryCompilationError`](QueryCompilationError.md).[`message`](QueryCompilationError.md#message) - -*** - -### ~~name~~ - -```ts -name: string; -``` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 - -#### Inherited from - -[`QueryCompilationError`](QueryCompilationError.md).[`name`](QueryCompilationError.md#name) - -*** - -### ~~stack?~~ - -```ts -optional stack: string; -``` - -Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1078 - -#### Inherited from - -[`QueryCompilationError`](QueryCompilationError.md).[`stack`](QueryCompilationError.md#stack) - -*** - -### ~~stackTraceLimit~~ - -```ts -static stackTraceLimit: number; -``` - -Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:67 - -The `Error.stackTraceLimit` property specifies the number of stack frames -collected by a stack trace (whether generated by `new Error().stack` or -`Error.captureStackTrace(obj)`). - -The default value is `10` but may be set to any valid JavaScript number. Changes -will affect any stack trace captured _after_ the value has been changed. - -If set to a non-number value, or set to a negative number, stack traces will -not capture any frames. - -#### Inherited from - -[`QueryCompilationError`](QueryCompilationError.md).[`stackTraceLimit`](QueryCompilationError.md#stacktracelimit) - -## Methods - -### ~~captureStackTrace()~~ - -```ts -static captureStackTrace(targetObject, constructorOpt?): void; -``` - -Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:51 - -Creates a `.stack` property on `targetObject`, which when accessed returns -a string representing the location in the code at which -`Error.captureStackTrace()` was called. - -```js -const myObject = {}; -Error.captureStackTrace(myObject); -myObject.stack; // Similar to `new Error().stack` -``` - -The first line of the trace will be prefixed with -`${myObject.name}: ${myObject.message}`. - -The optional `constructorOpt` argument accepts a function. If given, all frames -above `constructorOpt`, including `constructorOpt`, will be omitted from the -generated stack trace. - -The `constructorOpt` argument is useful for hiding implementation -details of error generation from the user. For instance: - -```js -function a() { - b(); -} - -function b() { - c(); -} - -function c() { - // Create an error without stack trace to avoid calculating the stack trace twice. - const { stackTraceLimit } = Error; - Error.stackTraceLimit = 0; - const error = new Error(); - Error.stackTraceLimit = stackTraceLimit; - - // Capture the stack trace above function b - Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace - throw error; -} - -a(); -``` - -#### Parameters - -##### targetObject - -`object` - -##### constructorOpt? - -`Function` - -#### Returns - -`void` - -#### Inherited from - -[`QueryCompilationError`](QueryCompilationError.md).[`captureStackTrace`](QueryCompilationError.md#capturestacktrace) - -*** - -### ~~prepareStackTrace()~~ - -```ts -static prepareStackTrace(err, stackTraces): any; -``` - -Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:55 - -#### Parameters - -##### err - -`Error` - -##### stackTraces - -`CallSite`[] - -#### Returns - -`any` - -#### See - -https://v8.dev/docs/stack-trace-api#customizing-stack-traces - -#### Inherited from - -[`QueryCompilationError`](QueryCompilationError.md).[`prepareStackTrace`](QueryCompilationError.md#preparestacktrace) diff --git a/docs/reference/classes/EmptyReferencePathError.md b/docs/reference/classes/EmptyReferencePathError.md index f6a0a9ba0c..a158b9d79e 100644 --- a/docs/reference/classes/EmptyReferencePathError.md +++ b/docs/reference/classes/EmptyReferencePathError.md @@ -3,7 +3,7 @@ id: EmptyReferencePathError title: EmptyReferencePathError --- -Defined in: [packages/db/src/errors.ts:831](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L831) +Defined in: [packages/db/src/errors.ts:812](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L812) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:831](https://github.com/TanStack/db/blob/ new EmptyReferencePathError(): EmptyReferencePathError; ``` -Defined in: [packages/db/src/errors.ts:832](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L832) +Defined in: [packages/db/src/errors.ts:813](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L813) #### Returns diff --git a/docs/reference/classes/GroupByError.md b/docs/reference/classes/GroupByError.md index dda9264335..1d60036781 100644 --- a/docs/reference/classes/GroupByError.md +++ b/docs/reference/classes/GroupByError.md @@ -3,7 +3,7 @@ id: GroupByError title: GroupByError --- -Defined in: [packages/db/src/errors.ts:940](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L940) +Defined in: [packages/db/src/errors.ts:921](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L921) ## Extends @@ -24,7 +24,7 @@ Defined in: [packages/db/src/errors.ts:940](https://github.com/TanStack/db/blob/ new GroupByError(message): GroupByError; ``` -Defined in: [packages/db/src/errors.ts:941](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L941) +Defined in: [packages/db/src/errors.ts:922](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L922) #### Parameters diff --git a/docs/reference/classes/InvalidJoinCondition.md b/docs/reference/classes/InvalidJoinCondition.md index b04f1d4d35..3ba58e4cce 100644 --- a/docs/reference/classes/InvalidJoinCondition.md +++ b/docs/reference/classes/InvalidJoinCondition.md @@ -3,7 +3,7 @@ id: InvalidJoinCondition title: InvalidJoinCondition --- -Defined in: [packages/db/src/errors.ts:919](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L919) +Defined in: [packages/db/src/errors.ts:900](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L900) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:919](https://github.com/TanStack/db/blob/ new InvalidJoinCondition(): InvalidJoinCondition; ``` -Defined in: [packages/db/src/errors.ts:920](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L920) +Defined in: [packages/db/src/errors.ts:901](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L901) #### Returns diff --git a/docs/reference/classes/InvalidJoinConditionLeftSourceError.md b/docs/reference/classes/InvalidJoinConditionLeftSourceError.md index 262f678750..89e00e1ee0 100644 --- a/docs/reference/classes/InvalidJoinConditionLeftSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionLeftSourceError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionLeftSourceError title: InvalidJoinConditionLeftSourceError --- -Defined in: [packages/db/src/errors.ts:899](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L899) +Defined in: [packages/db/src/errors.ts:880](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L880) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:899](https://github.com/TanStack/db/blob/ new InvalidJoinConditionLeftSourceError(sourceAlias): InvalidJoinConditionLeftSourceError; ``` -Defined in: [packages/db/src/errors.ts:900](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L900) +Defined in: [packages/db/src/errors.ts:881](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L881) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionRightSourceError.md b/docs/reference/classes/InvalidJoinConditionRightSourceError.md index 5e5006c01c..fd9c43c3b5 100644 --- a/docs/reference/classes/InvalidJoinConditionRightSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionRightSourceError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionRightSourceError title: InvalidJoinConditionRightSourceError --- -Defined in: [packages/db/src/errors.ts:909](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L909) +Defined in: [packages/db/src/errors.ts:890](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L890) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:909](https://github.com/TanStack/db/blob/ new InvalidJoinConditionRightSourceError(sourceAlias): InvalidJoinConditionRightSourceError; ``` -Defined in: [packages/db/src/errors.ts:910](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L910) +Defined in: [packages/db/src/errors.ts:891](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L891) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionSameSourceError.md b/docs/reference/classes/InvalidJoinConditionSameSourceError.md index 4c04251592..b84e9630a9 100644 --- a/docs/reference/classes/InvalidJoinConditionSameSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionSameSourceError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionSameSourceError title: InvalidJoinConditionSameSourceError --- -Defined in: [packages/db/src/errors.ts:879](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L879) +Defined in: [packages/db/src/errors.ts:860](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L860) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:879](https://github.com/TanStack/db/blob/ new InvalidJoinConditionSameSourceError(sourceAlias): InvalidJoinConditionSameSourceError; ``` -Defined in: [packages/db/src/errors.ts:880](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L880) +Defined in: [packages/db/src/errors.ts:861](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L861) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md b/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md index 0465d5a644..13ff131282 100644 --- a/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md +++ b/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md @@ -3,7 +3,7 @@ id: InvalidJoinConditionSourceMismatchError title: InvalidJoinConditionSourceMismatchError --- -Defined in: [packages/db/src/errors.ts:889](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L889) +Defined in: [packages/db/src/errors.ts:870](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L870) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:889](https://github.com/TanStack/db/blob/ new InvalidJoinConditionSourceMismatchError(): InvalidJoinConditionSourceMismatchError; ``` -Defined in: [packages/db/src/errors.ts:890](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L890) +Defined in: [packages/db/src/errors.ts:871](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L871) #### Returns diff --git a/docs/reference/classes/InvalidStorageDataFormatError.md b/docs/reference/classes/InvalidStorageDataFormatError.md index 3c15655e8e..3f2f4e7504 100644 --- a/docs/reference/classes/InvalidStorageDataFormatError.md +++ b/docs/reference/classes/InvalidStorageDataFormatError.md @@ -3,7 +3,7 @@ id: InvalidStorageDataFormatError title: InvalidStorageDataFormatError --- -Defined in: [packages/db/src/errors.ts:1023](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1023) +Defined in: [packages/db/src/errors.ts:1004](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1004) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1023](https://github.com/TanStack/db/blob new InvalidStorageDataFormatError(storageKey, key): InvalidStorageDataFormatError; ``` -Defined in: [packages/db/src/errors.ts:1024](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1024) +Defined in: [packages/db/src/errors.ts:1005](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1005) #### Parameters diff --git a/docs/reference/classes/InvalidStorageObjectFormatError.md b/docs/reference/classes/InvalidStorageObjectFormatError.md index adb1573d08..779a25b53f 100644 --- a/docs/reference/classes/InvalidStorageObjectFormatError.md +++ b/docs/reference/classes/InvalidStorageObjectFormatError.md @@ -3,7 +3,7 @@ id: InvalidStorageObjectFormatError title: InvalidStorageObjectFormatError --- -Defined in: [packages/db/src/errors.ts:1033](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1033) +Defined in: [packages/db/src/errors.ts:1014](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1014) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1033](https://github.com/TanStack/db/blob new InvalidStorageObjectFormatError(storageKey): InvalidStorageObjectFormatError; ``` -Defined in: [packages/db/src/errors.ts:1034](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1034) +Defined in: [packages/db/src/errors.ts:1015](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1015) #### Parameters diff --git a/docs/reference/classes/JoinCollectionNotFoundError.md b/docs/reference/classes/JoinCollectionNotFoundError.md index 413f597c7a..fb2bcf274b 100644 --- a/docs/reference/classes/JoinCollectionNotFoundError.md +++ b/docs/reference/classes/JoinCollectionNotFoundError.md @@ -3,7 +3,7 @@ id: JoinCollectionNotFoundError title: JoinCollectionNotFoundError --- -Defined in: [packages/db/src/errors.ts:851](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L851) +Defined in: [packages/db/src/errors.ts:832](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L832) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:851](https://github.com/TanStack/db/blob/ new JoinCollectionNotFoundError(collectionId): JoinCollectionNotFoundError; ``` -Defined in: [packages/db/src/errors.ts:852](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L852) +Defined in: [packages/db/src/errors.ts:833](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L833) #### Parameters diff --git a/docs/reference/classes/JoinError.md b/docs/reference/classes/JoinError.md index 930a82c829..1ea3c86f7a 100644 --- a/docs/reference/classes/JoinError.md +++ b/docs/reference/classes/JoinError.md @@ -3,7 +3,7 @@ id: JoinError title: JoinError --- -Defined in: [packages/db/src/errors.ts:862](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L862) +Defined in: [packages/db/src/errors.ts:843](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L843) ## Extends @@ -27,7 +27,7 @@ Defined in: [packages/db/src/errors.ts:862](https://github.com/TanStack/db/blob/ new JoinError(message): JoinError; ``` -Defined in: [packages/db/src/errors.ts:863](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L863) +Defined in: [packages/db/src/errors.ts:844](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L844) #### Parameters diff --git a/docs/reference/classes/LoadSubsetOperationAbortedError.md b/docs/reference/classes/LoadSubsetOperationAbortedError.md index 40258450dc..f6131f4e28 100644 --- a/docs/reference/classes/LoadSubsetOperationAbortedError.md +++ b/docs/reference/classes/LoadSubsetOperationAbortedError.md @@ -3,7 +3,7 @@ id: LoadSubsetOperationAbortedError title: LoadSubsetOperationAbortedError --- -Defined in: [packages/db/src/errors.ts:1080](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1080) +Defined in: [packages/db/src/errors.ts:1061](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1061) A subset operation was canceled before its result became visible. @@ -19,7 +19,7 @@ A subset operation was canceled before its result became visible. new LoadSubsetOperationAbortedError(): LoadSubsetOperationAbortedError; ``` -Defined in: [packages/db/src/errors.ts:1081](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1081) +Defined in: [packages/db/src/errors.ts:1062](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1062) #### Returns diff --git a/docs/reference/classes/LocalStorageCollectionError.md b/docs/reference/classes/LocalStorageCollectionError.md index 6555c32205..d2bf39439d 100644 --- a/docs/reference/classes/LocalStorageCollectionError.md +++ b/docs/reference/classes/LocalStorageCollectionError.md @@ -3,7 +3,7 @@ id: LocalStorageCollectionError title: LocalStorageCollectionError --- -Defined in: [packages/db/src/errors.ts:1006](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1006) +Defined in: [packages/db/src/errors.ts:987](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L987) ## Extends @@ -23,7 +23,7 @@ Defined in: [packages/db/src/errors.ts:1006](https://github.com/TanStack/db/blob new LocalStorageCollectionError(message): LocalStorageCollectionError; ``` -Defined in: [packages/db/src/errors.ts:1007](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1007) +Defined in: [packages/db/src/errors.ts:988](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L988) #### Parameters diff --git a/docs/reference/classes/MissingAliasInputsError.md b/docs/reference/classes/MissingAliasInputsError.md index 97a76aa771..4650b83e8b 100644 --- a/docs/reference/classes/MissingAliasInputsError.md +++ b/docs/reference/classes/MissingAliasInputsError.md @@ -3,7 +3,7 @@ id: MissingAliasInputsError title: MissingAliasInputsError --- -Defined in: [packages/db/src/errors.ts:1113](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1113) +Defined in: [packages/db/src/errors.ts:1094](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1094) Internal error when the compiler returns aliases that don't have corresponding input streams. This should never happen since all aliases come from user declarations. @@ -20,7 +20,7 @@ This should never happen since all aliases come from user declarations. new MissingAliasInputsError(missingAliases): MissingAliasInputsError; ``` -Defined in: [packages/db/src/errors.ts:1114](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1114) +Defined in: [packages/db/src/errors.ts:1095](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1095) #### Parameters diff --git a/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md b/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md index 64c0f0494a..2bec18ad4c 100644 --- a/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md +++ b/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md @@ -3,7 +3,7 @@ id: NonAggregateExpressionNotInGroupByError title: NonAggregateExpressionNotInGroupByError --- -Defined in: [packages/db/src/errors.ts:947](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L947) +Defined in: [packages/db/src/errors.ts:928](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L928) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:947](https://github.com/TanStack/db/blob/ new NonAggregateExpressionNotInGroupByError(alias): NonAggregateExpressionNotInGroupByError; ``` -Defined in: [packages/db/src/errors.ts:948](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L948) +Defined in: [packages/db/src/errors.ts:929](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L929) #### Parameters diff --git a/docs/reference/classes/QueryCompilationError.md b/docs/reference/classes/QueryCompilationError.md index 081cd0086b..df516443d2 100644 --- a/docs/reference/classes/QueryCompilationError.md +++ b/docs/reference/classes/QueryCompilationError.md @@ -19,7 +19,6 @@ Defined in: [packages/db/src/errors.ts:677](https://github.com/TanStack/db/blob/ - [`HavingRequiresGroupByError`](HavingRequiresGroupByError.md) - [`LimitOffsetRequireOrderByError`](LimitOffsetRequireOrderByError.md) - [`CollectionInputNotFoundError`](CollectionInputNotFoundError.md) -- [`DuplicateAliasInSubqueryError`](DuplicateAliasInSubqueryError.md) - [`UnsupportedFromTypeError`](UnsupportedFromTypeError.md) - [`UnknownExpressionTypeError`](UnknownExpressionTypeError.md) - [`EmptyReferencePathError`](EmptyReferencePathError.md) diff --git a/docs/reference/classes/QueryOptimizerError.md b/docs/reference/classes/QueryOptimizerError.md index 9951d5cb43..f8a8bb9f42 100644 --- a/docs/reference/classes/QueryOptimizerError.md +++ b/docs/reference/classes/QueryOptimizerError.md @@ -3,7 +3,7 @@ id: QueryOptimizerError title: QueryOptimizerError --- -Defined in: [packages/db/src/errors.ts:1092](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1092) +Defined in: [packages/db/src/errors.ts:1073](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1073) ## Extends @@ -21,7 +21,7 @@ Defined in: [packages/db/src/errors.ts:1092](https://github.com/TanStack/db/blob new QueryOptimizerError(message): QueryOptimizerError; ``` -Defined in: [packages/db/src/errors.ts:1093](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1093) +Defined in: [packages/db/src/errors.ts:1074](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1074) #### Parameters diff --git a/docs/reference/classes/SerializationError.md b/docs/reference/classes/SerializationError.md index 062bcd34b1..a6486ea1d5 100644 --- a/docs/reference/classes/SerializationError.md +++ b/docs/reference/classes/SerializationError.md @@ -3,7 +3,7 @@ id: SerializationError title: SerializationError --- -Defined in: [packages/db/src/errors.ts:995](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L995) +Defined in: [packages/db/src/errors.ts:976](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L976) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:995](https://github.com/TanStack/db/blob/ new SerializationError(operation, originalError): SerializationError; ``` -Defined in: [packages/db/src/errors.ts:996](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L996) +Defined in: [packages/db/src/errors.ts:977](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L977) #### Parameters diff --git a/docs/reference/classes/SetWindowFindOneLimitError.md b/docs/reference/classes/SetWindowFindOneLimitError.md index a060541d64..fe2244dfe8 100644 --- a/docs/reference/classes/SetWindowFindOneLimitError.md +++ b/docs/reference/classes/SetWindowFindOneLimitError.md @@ -3,7 +3,7 @@ id: SetWindowFindOneLimitError title: SetWindowFindOneLimitError --- -Defined in: [packages/db/src/errors.ts:1141](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1141) +Defined in: [packages/db/src/errors.ts:1122](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1122) Error thrown when setWindow would change the limit of a findOne() query. @@ -19,7 +19,7 @@ Error thrown when setWindow would change the limit of a findOne() query. new SetWindowFindOneLimitError(): SetWindowFindOneLimitError; ``` -Defined in: [packages/db/src/errors.ts:1142](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1142) +Defined in: [packages/db/src/errors.ts:1123](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1123) #### Returns diff --git a/docs/reference/classes/SetWindowReentrancyError.md b/docs/reference/classes/SetWindowReentrancyError.md index f8a7b0285a..94984bcb61 100644 --- a/docs/reference/classes/SetWindowReentrancyError.md +++ b/docs/reference/classes/SetWindowReentrancyError.md @@ -3,7 +3,7 @@ id: SetWindowReentrancyError title: SetWindowReentrancyError --- -Defined in: [packages/db/src/errors.ts:1152](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1152) +Defined in: [packages/db/src/errors.ts:1133](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1133) Error thrown when setWindow is called from inside another setWindow call. @@ -19,7 +19,7 @@ Error thrown when setWindow is called from inside another setWindow call. new SetWindowReentrancyError(): SetWindowReentrancyError; ``` -Defined in: [packages/db/src/errors.ts:1153](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1153) +Defined in: [packages/db/src/errors.ts:1134](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1134) #### Returns diff --git a/docs/reference/classes/SetWindowRequiresOrderByError.md b/docs/reference/classes/SetWindowRequiresOrderByError.md index 9ce0624498..91809a38df 100644 --- a/docs/reference/classes/SetWindowRequiresOrderByError.md +++ b/docs/reference/classes/SetWindowRequiresOrderByError.md @@ -3,7 +3,7 @@ id: SetWindowRequiresOrderByError title: SetWindowRequiresOrderByError --- -Defined in: [packages/db/src/errors.ts:1127](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1127) +Defined in: [packages/db/src/errors.ts:1108](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1108) Error thrown when setWindow is called on a collection without an ORDER BY clause. @@ -19,7 +19,7 @@ Error thrown when setWindow is called on a collection without an ORDER BY clause new SetWindowRequiresOrderByError(): SetWindowRequiresOrderByError; ``` -Defined in: [packages/db/src/errors.ts:1128](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1128) +Defined in: [packages/db/src/errors.ts:1109](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1109) #### Returns diff --git a/docs/reference/classes/StorageError.md b/docs/reference/classes/StorageError.md index 57ea54b824..908c491bfc 100644 --- a/docs/reference/classes/StorageError.md +++ b/docs/reference/classes/StorageError.md @@ -3,7 +3,7 @@ id: StorageError title: StorageError --- -Defined in: [packages/db/src/errors.ts:988](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L988) +Defined in: [packages/db/src/errors.ts:969](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L969) ## Extends @@ -22,7 +22,7 @@ Defined in: [packages/db/src/errors.ts:988](https://github.com/TanStack/db/blob/ new StorageError(message): StorageError; ``` -Defined in: [packages/db/src/errors.ts:989](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L989) +Defined in: [packages/db/src/errors.ts:970](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L970) #### Parameters diff --git a/docs/reference/classes/StorageKeyRequiredError.md b/docs/reference/classes/StorageKeyRequiredError.md index d5409d93f0..13de34a14c 100644 --- a/docs/reference/classes/StorageKeyRequiredError.md +++ b/docs/reference/classes/StorageKeyRequiredError.md @@ -3,7 +3,7 @@ id: StorageKeyRequiredError title: StorageKeyRequiredError --- -Defined in: [packages/db/src/errors.ts:1013](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1013) +Defined in: [packages/db/src/errors.ts:994](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L994) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1013](https://github.com/TanStack/db/blob new StorageKeyRequiredError(): StorageKeyRequiredError; ``` -Defined in: [packages/db/src/errors.ts:1014](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1014) +Defined in: [packages/db/src/errors.ts:995](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L995) #### Returns diff --git a/docs/reference/classes/SyncCleanupError.md b/docs/reference/classes/SyncCleanupError.md index 7cb34e4aef..14bb6ad24e 100644 --- a/docs/reference/classes/SyncCleanupError.md +++ b/docs/reference/classes/SyncCleanupError.md @@ -3,7 +3,7 @@ id: SyncCleanupError title: SyncCleanupError --- -Defined in: [packages/db/src/errors.ts:1044](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1044) +Defined in: [packages/db/src/errors.ts:1025](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1025) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1044](https://github.com/TanStack/db/blob new SyncCleanupError(collectionId, error): SyncCleanupError; ``` -Defined in: [packages/db/src/errors.ts:1045](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1045) +Defined in: [packages/db/src/errors.ts:1026](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1026) #### Parameters diff --git a/docs/reference/classes/SyncTransactionAbortedError.md b/docs/reference/classes/SyncTransactionAbortedError.md index 6a05e786f9..fb16c78103 100644 --- a/docs/reference/classes/SyncTransactionAbortedError.md +++ b/docs/reference/classes/SyncTransactionAbortedError.md @@ -3,7 +3,7 @@ id: SyncTransactionAbortedError title: SyncTransactionAbortedError --- -Defined in: [packages/db/src/errors.ts:1056](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1056) +Defined in: [packages/db/src/errors.ts:1037](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1037) A sync transaction was canceled before its writes became visible. @@ -19,7 +19,7 @@ A sync transaction was canceled before its writes became visible. new SyncTransactionAbortedError(): SyncTransactionAbortedError; ``` -Defined in: [packages/db/src/errors.ts:1057](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1057) +Defined in: [packages/db/src/errors.ts:1038](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1038) #### Returns diff --git a/docs/reference/classes/UnhashableQueryIRError.md b/docs/reference/classes/UnhashableQueryIRError.md index e6ab2621e5..bc54051dc7 100644 --- a/docs/reference/classes/UnhashableQueryIRError.md +++ b/docs/reference/classes/UnhashableQueryIRError.md @@ -3,7 +3,7 @@ id: UnhashableQueryIRError title: UnhashableQueryIRError --- -Defined in: [packages/db/src/errors.ts:1163](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1163) +Defined in: [packages/db/src/errors.ts:1144](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1144) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:1163](https://github.com/TanStack/db/blob new UnhashableQueryIRError(path, reason): UnhashableQueryIRError; ``` -Defined in: [packages/db/src/errors.ts:1164](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1164) +Defined in: [packages/db/src/errors.ts:1145](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1145) #### Parameters @@ -95,7 +95,7 @@ Error.name readonly path: string; ``` -Defined in: [packages/db/src/errors.ts:1165](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1165) +Defined in: [packages/db/src/errors.ts:1146](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1146) *** @@ -105,7 +105,7 @@ Defined in: [packages/db/src/errors.ts:1165](https://github.com/TanStack/db/blob readonly reason: string; ``` -Defined in: [packages/db/src/errors.ts:1166](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1166) +Defined in: [packages/db/src/errors.ts:1147](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L1147) *** diff --git a/docs/reference/classes/UnknownExpressionTypeError.md b/docs/reference/classes/UnknownExpressionTypeError.md index 5ec84e8123..bbaed7ac7d 100644 --- a/docs/reference/classes/UnknownExpressionTypeError.md +++ b/docs/reference/classes/UnknownExpressionTypeError.md @@ -3,7 +3,7 @@ id: UnknownExpressionTypeError title: UnknownExpressionTypeError --- -Defined in: [packages/db/src/errors.ts:821](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L821) +Defined in: [packages/db/src/errors.ts:802](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L802) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:821](https://github.com/TanStack/db/blob/ new UnknownExpressionTypeError(type): UnknownExpressionTypeError; ``` -Defined in: [packages/db/src/errors.ts:822](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L822) +Defined in: [packages/db/src/errors.ts:803](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L803) #### Parameters diff --git a/docs/reference/classes/UnknownFunctionError.md b/docs/reference/classes/UnknownFunctionError.md index eb91c7b91a..87a4beea80 100644 --- a/docs/reference/classes/UnknownFunctionError.md +++ b/docs/reference/classes/UnknownFunctionError.md @@ -3,7 +3,7 @@ id: UnknownFunctionError title: UnknownFunctionError --- -Defined in: [packages/db/src/errors.ts:841](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L841) +Defined in: [packages/db/src/errors.ts:822](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L822) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:841](https://github.com/TanStack/db/blob/ new UnknownFunctionError(functionName): UnknownFunctionError; ``` -Defined in: [packages/db/src/errors.ts:842](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L842) +Defined in: [packages/db/src/errors.ts:823](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L823) #### Parameters diff --git a/docs/reference/classes/UnknownHavingExpressionTypeError.md b/docs/reference/classes/UnknownHavingExpressionTypeError.md index d8ab35a3e9..b095bde232 100644 --- a/docs/reference/classes/UnknownHavingExpressionTypeError.md +++ b/docs/reference/classes/UnknownHavingExpressionTypeError.md @@ -3,7 +3,7 @@ id: UnknownHavingExpressionTypeError title: UnknownHavingExpressionTypeError --- -Defined in: [packages/db/src/errors.ts:977](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L977) +Defined in: [packages/db/src/errors.ts:958](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L958) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:977](https://github.com/TanStack/db/blob/ new UnknownHavingExpressionTypeError(type): UnknownHavingExpressionTypeError; ``` -Defined in: [packages/db/src/errors.ts:978](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L978) +Defined in: [packages/db/src/errors.ts:959](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L959) #### Parameters diff --git a/docs/reference/classes/UnsupportedAggregateFunctionError.md b/docs/reference/classes/UnsupportedAggregateFunctionError.md index 8173b0e2e5..f334008dc2 100644 --- a/docs/reference/classes/UnsupportedAggregateFunctionError.md +++ b/docs/reference/classes/UnsupportedAggregateFunctionError.md @@ -3,7 +3,7 @@ id: UnsupportedAggregateFunctionError title: UnsupportedAggregateFunctionError --- -Defined in: [packages/db/src/errors.ts:957](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L957) +Defined in: [packages/db/src/errors.ts:938](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L938) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:957](https://github.com/TanStack/db/blob/ new UnsupportedAggregateFunctionError(functionName): UnsupportedAggregateFunctionError; ``` -Defined in: [packages/db/src/errors.ts:958](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L958) +Defined in: [packages/db/src/errors.ts:939](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L939) #### Parameters diff --git a/docs/reference/classes/UnsupportedFromTypeError.md b/docs/reference/classes/UnsupportedFromTypeError.md index 2bb3716416..fee0158d8e 100644 --- a/docs/reference/classes/UnsupportedFromTypeError.md +++ b/docs/reference/classes/UnsupportedFromTypeError.md @@ -3,7 +3,7 @@ id: UnsupportedFromTypeError title: UnsupportedFromTypeError --- -Defined in: [packages/db/src/errors.ts:811](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L811) +Defined in: [packages/db/src/errors.ts:792](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L792) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:811](https://github.com/TanStack/db/blob/ new UnsupportedFromTypeError(type): UnsupportedFromTypeError; ``` -Defined in: [packages/db/src/errors.ts:812](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L812) +Defined in: [packages/db/src/errors.ts:793](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L793) #### Parameters diff --git a/docs/reference/classes/UnsupportedJoinSourceTypeError.md b/docs/reference/classes/UnsupportedJoinSourceTypeError.md index f2352e4f8b..d8a90c1214 100644 --- a/docs/reference/classes/UnsupportedJoinSourceTypeError.md +++ b/docs/reference/classes/UnsupportedJoinSourceTypeError.md @@ -3,7 +3,7 @@ id: UnsupportedJoinSourceTypeError title: UnsupportedJoinSourceTypeError --- -Defined in: [packages/db/src/errors.ts:929](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L929) +Defined in: [packages/db/src/errors.ts:910](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L910) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:929](https://github.com/TanStack/db/blob/ new UnsupportedJoinSourceTypeError(type): UnsupportedJoinSourceTypeError; ``` -Defined in: [packages/db/src/errors.ts:930](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L930) +Defined in: [packages/db/src/errors.ts:911](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L911) #### Parameters diff --git a/docs/reference/classes/UnsupportedJoinTypeError.md b/docs/reference/classes/UnsupportedJoinTypeError.md index fd08b7ef5d..3bc5e06575 100644 --- a/docs/reference/classes/UnsupportedJoinTypeError.md +++ b/docs/reference/classes/UnsupportedJoinTypeError.md @@ -3,7 +3,7 @@ id: UnsupportedJoinTypeError title: UnsupportedJoinTypeError --- -Defined in: [packages/db/src/errors.ts:869](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L869) +Defined in: [packages/db/src/errors.ts:850](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L850) ## Extends @@ -17,7 +17,7 @@ Defined in: [packages/db/src/errors.ts:869](https://github.com/TanStack/db/blob/ new UnsupportedJoinTypeError(joinType): UnsupportedJoinTypeError; ``` -Defined in: [packages/db/src/errors.ts:870](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L870) +Defined in: [packages/db/src/errors.ts:851](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L851) #### Parameters diff --git a/docs/reference/electric-db-collection/functions/electricCollectionOptions.md b/docs/reference/electric-db-collection/functions/electricCollectionOptions.md index 669820845f..090205f334 100644 --- a/docs/reference/electric-db-collection/functions/electricCollectionOptions.md +++ b/docs/reference/electric-db-collection/functions/electricCollectionOptions.md @@ -9,7 +9,7 @@ title: electricCollectionOptions function electricCollectionOptions(config): Omit, string | number, T, UtilsRecord>, "utils" | "onInsert" | "onUpdate" | "onDelete"> & Pick, T>, "onInsert" | "onUpdate" | "onDelete"> & object; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:1170](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L1170) +Defined in: [packages/electric-db-collection/src/electric.ts:1146](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L1146) Creates Electric collection options for use with a standard Collection @@ -41,7 +41,7 @@ Collection options with utilities function electricCollectionOptions(config): Omit, "utils" | "onInsert" | "onUpdate" | "onDelete"> & Pick, "onInsert" | "onUpdate" | "onDelete"> & object; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:1188](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L1188) +Defined in: [packages/electric-db-collection/src/electric.ts:1164](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L1164) Creates Electric collection options for use with a standard Collection diff --git a/docs/reference/electric-db-collection/interfaces/ElectricCollectionConfig.md b/docs/reference/electric-db-collection/interfaces/ElectricCollectionConfig.md index 36ce4d0ae1..6769427ce7 100644 --- a/docs/reference/electric-db-collection/interfaces/ElectricCollectionConfig.md +++ b/docs/reference/electric-db-collection/interfaces/ElectricCollectionConfig.md @@ -3,7 +3,7 @@ id: ElectricCollectionConfig title: ElectricCollectionConfig --- -Defined in: [packages/electric-db-collection/src/electric.ts:345](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L345) +Defined in: [packages/electric-db-collection/src/electric.ts:330](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L330) Configuration interface for Electric collection options @@ -33,7 +33,7 @@ The schema type for validation optional [ELECTRIC_TEST_HOOKS]: ElectricTestHooks; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:362](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L362) +Defined in: [packages/electric-db-collection/src/electric.ts:347](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L347) Internal test hooks (for testing only) Hidden via Symbol to prevent accidental usage in production @@ -43,10 +43,10 @@ Hidden via Symbol to prevent accidental usage in production ### onDelete()? ```ts -optional onDelete: (params) => Promise; +optional onDelete: (params) => Promise; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:548](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L548) +Defined in: [packages/electric-db-collection/src/electric.ts:524](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L524) Optional asynchronous handler function called before a delete operation @@ -68,13 +68,10 @@ Object containing transaction and collection information #### Returns -`Promise`\<`MatchingStrategy`\> +`Promise`\<`void`\> Promise that should resolve after synchronization is complete -**Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0. -Use `await collection.utils.awaitTxId(txid)` within the handler instead. - #### Examples ```ts @@ -108,10 +105,10 @@ onDelete: async ({ transaction, collection }) => { ### onInsert()? ```ts -optional onInsert: (params) => Promise; +optional onInsert: (params) => Promise; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:449](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L449) +Defined in: [packages/electric-db-collection/src/electric.ts:431](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L431) Optional asynchronous handler function called before an insert operation @@ -133,13 +130,10 @@ Object containing transaction and collection information #### Returns -`Promise`\<`MatchingStrategy`\> +`Promise`\<`void`\> Promise that should resolve after synchronization is complete -**Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0. -Use `await collection.utils.awaitTxId(txid)` within the handler instead. - #### Examples ```ts @@ -220,10 +214,10 @@ onInsert: async ({ transaction, collection }) => { ### onUpdate()? ```ts -optional onUpdate: (params) => Promise; +optional onUpdate: (params) => Promise; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:499](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L499) +Defined in: [packages/electric-db-collection/src/electric.ts:478](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L478) Optional asynchronous handler function called before an update operation @@ -245,13 +239,10 @@ Object containing transaction and collection information #### Returns -`Promise`\<`MatchingStrategy`\> +`Promise`\<`void`\> Promise that should resolve after synchronization is complete -**Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0. -Use `await collection.utils.awaitTxId(txid)` within the handler instead. - #### Examples ```ts @@ -289,7 +280,7 @@ onUpdate: async ({ transaction, collection }) => { shapeOptions: ShapeStreamOptions>; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:355](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L355) +Defined in: [packages/electric-db-collection/src/electric.ts:340](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L340) Configuration options for the ElectricSQL ShapeStream @@ -301,4 +292,4 @@ Configuration options for the ElectricSQL ShapeStream optional syncMode: ElectricSyncMode; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:356](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L356) +Defined in: [packages/electric-db-collection/src/electric.ts:341](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L341) diff --git a/docs/reference/electric-db-collection/interfaces/ElectricCollectionUtils.md b/docs/reference/electric-db-collection/interfaces/ElectricCollectionUtils.md index 8e72864d77..2038a262d0 100644 --- a/docs/reference/electric-db-collection/interfaces/ElectricCollectionUtils.md +++ b/docs/reference/electric-db-collection/interfaces/ElectricCollectionUtils.md @@ -3,7 +3,7 @@ id: ElectricCollectionUtils title: ElectricCollectionUtils --- -Defined in: [packages/electric-db-collection/src/electric.ts:906](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L906) +Defined in: [packages/electric-db-collection/src/electric.ts:882](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L882) Electric collection utilities type @@ -21,7 +21,7 @@ Electric collection utilities type awaitMatch: AwaitMatchFn; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:912](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L912) +Defined in: [packages/electric-db-collection/src/electric.ts:888](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L888) *** @@ -31,4 +31,4 @@ Defined in: [packages/electric-db-collection/src/electric.ts:912](https://github awaitTxId: AwaitTxIdFn; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:911](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L911) +Defined in: [packages/electric-db-collection/src/electric.ts:887](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L887) diff --git a/docs/reference/electric-db-collection/type-aliases/AwaitTxIdFn.md b/docs/reference/electric-db-collection/type-aliases/AwaitTxIdFn.md index 6688f5e914..54aa781a51 100644 --- a/docs/reference/electric-db-collection/type-aliases/AwaitTxIdFn.md +++ b/docs/reference/electric-db-collection/type-aliases/AwaitTxIdFn.md @@ -7,7 +7,7 @@ title: AwaitTxIdFn type AwaitTxIdFn = (txId, timeout?) => Promise; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:893](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L893) +Defined in: [packages/electric-db-collection/src/electric.ts:869](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L869) Type for the awaitTxId utility function diff --git a/docs/reference/electric-db-collection/type-aliases/Txid.md b/docs/reference/electric-db-collection/type-aliases/Txid.md index d2370c415e..c6f093ade9 100644 --- a/docs/reference/electric-db-collection/type-aliases/Txid.md +++ b/docs/reference/electric-db-collection/type-aliases/Txid.md @@ -7,6 +7,6 @@ title: Txid type Txid = number; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:96](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L96) +Defined in: [packages/electric-db-collection/src/electric.ts:95](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L95) Type representing a transaction ID in ElectricSQL diff --git a/docs/reference/functions/hasVirtualProps.md b/docs/reference/functions/hasVirtualProps.md index 71b94b4d7a..6c1565fd93 100644 --- a/docs/reference/functions/hasVirtualProps.md +++ b/docs/reference/functions/hasVirtualProps.md @@ -7,11 +7,9 @@ title: hasVirtualProps function hasVirtualProps(value): value is VirtualRowProps; ``` -Defined in: [packages/db/src/virtual-props.ts:194](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L194) +Defined in: [packages/db/src/virtual-props.ts:166](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L166) -Checks if a value has virtual properties attached. Legacy rows with the -original four properties still match; only rows published by this version -are guaranteed to carry `$hasPendingWrites`. +Checks if a value has all virtual properties attached. ## Parameters @@ -30,7 +28,7 @@ true if the value has virtual properties ## Example ```typescript -if (hasVirtualProps(row) && row.$hasPendingWrites !== undefined) { +if (hasVirtualProps(row)) { console.log('Pending local writes:', row.$hasPendingWrites) } ``` diff --git a/docs/reference/index.md b/docs/reference/index.md index 7d43a88931..b1f3428ac2 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -31,7 +31,6 @@ title: "@tanstack/db" - [DeduplicatedLoadSubset](classes/DeduplicatedLoadSubset.md) - [DeleteKeyNotFoundError](classes/DeleteKeyNotFoundError.md) - [DistinctRequiresSelectError](classes/DistinctRequiresSelectError.md) -- [~~DuplicateAliasInSubqueryError~~](classes/DuplicateAliasInSubqueryError.md) - [DuplicateDbInstanceError](classes/DuplicateDbInstanceError.md) - [DuplicateKeyError](classes/DuplicateKeyError.md) - [DuplicateKeySyncError](classes/DuplicateKeySyncError.md) diff --git a/docs/reference/interfaces/BaseCollectionConfig.md b/docs/reference/interfaces/BaseCollectionConfig.md index e6992c10a4..4b870d93dd 100644 --- a/docs/reference/interfaces/BaseCollectionConfig.md +++ b/docs/reference/interfaces/BaseCollectionConfig.md @@ -3,7 +3,7 @@ id: BaseCollectionConfig title: BaseCollectionConfig --- -Defined in: [packages/db/src/types.ts:740](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L740) +Defined in: [packages/db/src/types.ts:728](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L728) ## Extended by @@ -29,10 +29,6 @@ Defined in: [packages/db/src/types.ts:740](https://github.com/TanStack/db/blob/m `TUtils` *extends* [`UtilsRecord`](../type-aliases/UtilsRecord.md) = [`UtilsRecord`](../type-aliases/UtilsRecord.md) -### TReturn - -`TReturn` = `any` - ## Properties ### autoIndex? @@ -41,7 +37,7 @@ Defined in: [packages/db/src/types.ts:740](https://github.com/TanStack/db/blob/m optional autoIndex: "eager" | "off"; ``` -Defined in: [packages/db/src/types.ts:794](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L794) +Defined in: [packages/db/src/types.ts:781](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L781) Auto-indexing mode for the collection. When enabled, indexes will be automatically created for simple where expressions. @@ -66,7 +62,7 @@ When enabled, indexes will be automatically created for simple where expressions optional compare: (x, y) => number; ``` -Defined in: [packages/db/src/types.ts:819](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L819) +Defined in: [packages/db/src/types.ts:806](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L806) Optional function to compare two items. This is used to order the items in the collection. @@ -106,7 +102,7 @@ compare: (x, y) => x.createdAt.getTime() - y.createdAt.getTime() optional defaultIndexType: IndexConstructor; ``` -Defined in: [packages/db/src/types.ts:808](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L808) +Defined in: [packages/db/src/types.ts:795](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L795) Default index type to use when creating indexes without an explicit type. Required for auto-indexing. Import from '@tanstack/db'. @@ -130,7 +126,7 @@ const collection = createCollection({ optional defaultStringCollation: StringCollationConfig; ``` -Defined in: [packages/db/src/types.ts:1014](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1014) +Defined in: [packages/db/src/types.ts:981](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L981) Specifies how to compare data in the collection. This should be configured to match data ordering on the backend. @@ -145,7 +141,7 @@ E.g., when using the Electric DB collection these options optional gcTime: number; ``` -Defined in: [packages/db/src/types.ts:773](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L773) +Defined in: [packages/db/src/types.ts:760](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L760) Time in milliseconds after which the collection will be garbage collected when it has no active subscribers. Defaults to 5 minutes (300000ms). @@ -162,7 +158,7 @@ disables automatic garbage collection. getKey: (item) => TKey; ``` -Defined in: [packages/db/src/types.ts:764](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L764) +Defined in: [packages/db/src/types.ts:751](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L751) Function to extract the ID from an object This is required for update/delete operations which now only accept IDs @@ -196,22 +192,19 @@ getKey: (item) => item.uuid optional id: string; ``` -Defined in: [packages/db/src/types.ts:753](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L753) +Defined in: [packages/db/src/types.ts:740](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L740) *** ### onDelete? ```ts -optional onDelete: - | DeleteMutationFn -| DeleteMutationFn; +optional onDelete: DeleteMutationFn; ``` -Defined in: [packages/db/src/types.ts:1003](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1003) +Defined in: [packages/db/src/types.ts:973](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L973) Optional asynchronous handler function called before a delete operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -230,8 +223,6 @@ onDelete: async ({ transaction, collection }) => { await api.deleteTodos(keysToDelete) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -276,15 +267,12 @@ onDelete: async ({ transaction, collection }) => { ### onInsert? ```ts -optional onInsert: - | InsertMutationFn -| InsertMutationFn; +optional onInsert: InsertMutationFn; ``` -Defined in: [packages/db/src/types.ts:882](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L882) +Defined in: [packages/db/src/types.ts:865](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L865) Optional asynchronous handler function called before an insert operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -303,8 +291,6 @@ onInsert: async ({ transaction, collection }) => { await api.createTodo(newItem) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -325,7 +311,6 @@ onInsert: async ({ transaction, collection }) => { await api.createTodos(items) // Refetch to get updated data from server await collection.utils.refetch() - return { refetch: false } } ``` @@ -347,15 +332,12 @@ onInsert: async ({ transaction, collection }) => { ### onUpdate? ```ts -optional onUpdate: - | UpdateMutationFn -| UpdateMutationFn; +optional onUpdate: UpdateMutationFn; ``` -Defined in: [packages/db/src/types.ts:944](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L944) +Defined in: [packages/db/src/types.ts:920](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L920) Optional asynchronous handler function called before an update operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -375,8 +357,6 @@ onUpdate: async ({ transaction, collection }) => { await api.updateTodo(mutation.original.id, changes) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -399,7 +379,6 @@ onUpdate: async ({ transaction, collection }) => { })) await api.updateTodos(updates) await collection.utils.refetch() - return { refetch: false } } ``` @@ -425,7 +404,7 @@ onUpdate: async ({ transaction, collection }) => { optional schema: TSchema; ``` -Defined in: [packages/db/src/types.ts:754](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L754) +Defined in: [packages/db/src/types.ts:741](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L741) *** @@ -435,7 +414,7 @@ Defined in: [packages/db/src/types.ts:754](https://github.com/TanStack/db/blob/m optional startSync: boolean; ``` -Defined in: [packages/db/src/types.ts:784](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L784) +Defined in: [packages/db/src/types.ts:771](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L771) Whether to eagerly start syncing on collection creation. When true, syncing begins immediately. When false, syncing starts when the first subscriber attaches. @@ -458,7 +437,7 @@ false optional syncMode: SyncMode; ``` -Defined in: [packages/db/src/types.ts:828](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L828) +Defined in: [packages/db/src/types.ts:815](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L815) The mode of sync to use for the collection. @@ -480,4 +459,4 @@ The exact implementation of the sync mode is up to the sync implementation. optional utils: TUtils; ``` -Defined in: [packages/db/src/types.ts:1016](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1016) +Defined in: [packages/db/src/types.ts:983](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L983) diff --git a/docs/reference/interfaces/CollectionConfig.md b/docs/reference/interfaces/CollectionConfig.md index b6052ab32e..9275d0aacc 100644 --- a/docs/reference/interfaces/CollectionConfig.md +++ b/docs/reference/interfaces/CollectionConfig.md @@ -3,7 +3,7 @@ id: CollectionConfig title: CollectionConfig --- -Defined in: [packages/db/src/types.ts:1019](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1019) +Defined in: [packages/db/src/types.ts:986](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L986) ## Extends @@ -35,7 +35,7 @@ Defined in: [packages/db/src/types.ts:1019](https://github.com/TanStack/db/blob/ optional autoIndex: "eager" | "off"; ``` -Defined in: [packages/db/src/types.ts:794](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L794) +Defined in: [packages/db/src/types.ts:781](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L781) Auto-indexing mode for the collection. When enabled, indexes will be automatically created for simple where expressions. @@ -64,7 +64,7 @@ When enabled, indexes will be automatically created for simple where expressions optional compare: (x, y) => number; ``` -Defined in: [packages/db/src/types.ts:819](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L819) +Defined in: [packages/db/src/types.ts:806](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L806) Optional function to compare two items. This is used to order the items in the collection. @@ -108,7 +108,7 @@ compare: (x, y) => x.createdAt.getTime() - y.createdAt.getTime() optional defaultIndexType: IndexConstructor; ``` -Defined in: [packages/db/src/types.ts:808](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L808) +Defined in: [packages/db/src/types.ts:795](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L795) Default index type to use when creating indexes without an explicit type. Required for auto-indexing. Import from '@tanstack/db'. @@ -136,7 +136,7 @@ const collection = createCollection({ optional defaultStringCollation: StringCollationConfig; ``` -Defined in: [packages/db/src/types.ts:1014](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1014) +Defined in: [packages/db/src/types.ts:981](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L981) Specifies how to compare data in the collection. This should be configured to match data ordering on the backend. @@ -155,7 +155,7 @@ E.g., when using the Electric DB collection these options optional gcTime: number; ``` -Defined in: [packages/db/src/types.ts:773](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L773) +Defined in: [packages/db/src/types.ts:760](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L760) Time in milliseconds after which the collection will be garbage collected when it has no active subscribers. Defaults to 5 minutes (300000ms). @@ -176,7 +176,7 @@ disables automatic garbage collection. getKey: (item) => TKey; ``` -Defined in: [packages/db/src/types.ts:764](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L764) +Defined in: [packages/db/src/types.ts:751](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L751) Function to extract the ID from an object This is required for update/delete operations which now only accept IDs @@ -214,7 +214,7 @@ getKey: (item) => item.uuid optional id: string; ``` -Defined in: [packages/db/src/types.ts:753](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L753) +Defined in: [packages/db/src/types.ts:740](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L740) #### Inherited from @@ -225,15 +225,12 @@ Defined in: [packages/db/src/types.ts:753](https://github.com/TanStack/db/blob/m ### onDelete? ```ts -optional onDelete: - | DeleteMutationFn -| DeleteMutationFn; +optional onDelete: DeleteMutationFn; ``` -Defined in: [packages/db/src/types.ts:1003](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1003) +Defined in: [packages/db/src/types.ts:973](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L973) Optional asynchronous handler function called before a delete operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -252,8 +249,6 @@ onDelete: async ({ transaction, collection }) => { await api.deleteTodos(keysToDelete) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -302,15 +297,12 @@ onDelete: async ({ transaction, collection }) => { ### onInsert? ```ts -optional onInsert: - | InsertMutationFn -| InsertMutationFn; +optional onInsert: InsertMutationFn; ``` -Defined in: [packages/db/src/types.ts:882](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L882) +Defined in: [packages/db/src/types.ts:865](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L865) Optional asynchronous handler function called before an insert operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -329,8 +321,6 @@ onInsert: async ({ transaction, collection }) => { await api.createTodo(newItem) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -351,7 +341,6 @@ onInsert: async ({ transaction, collection }) => { await api.createTodos(items) // Refetch to get updated data from server await collection.utils.refetch() - return { refetch: false } } ``` @@ -377,15 +366,12 @@ onInsert: async ({ transaction, collection }) => { ### onUpdate? ```ts -optional onUpdate: - | UpdateMutationFn -| UpdateMutationFn; +optional onUpdate: UpdateMutationFn; ``` -Defined in: [packages/db/src/types.ts:944](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L944) +Defined in: [packages/db/src/types.ts:920](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L920) Optional asynchronous handler function called before an update operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -405,8 +391,6 @@ onUpdate: async ({ transaction, collection }) => { await api.updateTodo(mutation.original.id, changes) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -429,7 +413,6 @@ onUpdate: async ({ transaction, collection }) => { })) await api.updateTodos(updates) await collection.utils.refetch() - return { refetch: false } } ``` @@ -459,7 +442,7 @@ onUpdate: async ({ transaction, collection }) => { optional schema: TSchema; ``` -Defined in: [packages/db/src/types.ts:754](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L754) +Defined in: [packages/db/src/types.ts:741](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L741) #### Inherited from @@ -473,7 +456,7 @@ Defined in: [packages/db/src/types.ts:754](https://github.com/TanStack/db/blob/m optional startSync: boolean; ``` -Defined in: [packages/db/src/types.ts:784](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L784) +Defined in: [packages/db/src/types.ts:771](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L771) Whether to eagerly start syncing on collection creation. When true, syncing begins immediately. When false, syncing starts when the first subscriber attaches. @@ -500,7 +483,7 @@ false sync: SyncConfig; ``` -Defined in: [packages/db/src/types.ts:1025](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1025) +Defined in: [packages/db/src/types.ts:992](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L992) *** @@ -510,7 +493,7 @@ Defined in: [packages/db/src/types.ts:1025](https://github.com/TanStack/db/blob/ optional syncMode: SyncMode; ``` -Defined in: [packages/db/src/types.ts:828](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L828) +Defined in: [packages/db/src/types.ts:815](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L815) The mode of sync to use for the collection. @@ -536,7 +519,7 @@ The exact implementation of the sync mode is up to the sync implementation. optional utils: TUtils; ``` -Defined in: [packages/db/src/types.ts:1016](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1016) +Defined in: [packages/db/src/types.ts:983](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L983) #### Inherited from diff --git a/docs/reference/interfaces/Context.md b/docs/reference/interfaces/Context.md index 124e3187ef..319432be37 100644 --- a/docs/reference/interfaces/Context.md +++ b/docs/reference/interfaces/Context.md @@ -3,7 +3,7 @@ id: Context title: Context --- -Defined in: [packages/db/src/query/builder/types.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L47) +Defined in: [packages/db/src/query/builder/types.ts:44](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L44) Context - The central state container for query builder operations @@ -35,7 +35,7 @@ The context evolves through the query builder chain: baseSchema: ContextSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:49](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L49) +Defined in: [packages/db/src/query/builder/types.ts:46](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L46) *** @@ -45,7 +45,7 @@ Defined in: [packages/db/src/query/builder/types.ts:49](https://github.com/TanSt fromSourceName: string; ``` -Defined in: [packages/db/src/query/builder/types.ts:55](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L55) +Defined in: [packages/db/src/query/builder/types.ts:52](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L52) *** @@ -55,7 +55,7 @@ Defined in: [packages/db/src/query/builder/types.ts:55](https://github.com/TanSt optional fromSourceNames: readonly string[]; ``` -Defined in: [packages/db/src/query/builder/types.ts:57](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L57) +Defined in: [packages/db/src/query/builder/types.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L54) *** @@ -65,7 +65,7 @@ Defined in: [packages/db/src/query/builder/types.ts:57](https://github.com/TanSt optional hasJoins: boolean; ``` -Defined in: [packages/db/src/query/builder/types.ts:61](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L61) +Defined in: [packages/db/src/query/builder/types.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L58) *** @@ -75,7 +75,7 @@ Defined in: [packages/db/src/query/builder/types.ts:61](https://github.com/TanSt optional hasResult: true; ``` -Defined in: [packages/db/src/query/builder/types.ts:70](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L70) +Defined in: [packages/db/src/query/builder/types.ts:67](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L67) *** @@ -85,7 +85,7 @@ Defined in: [packages/db/src/query/builder/types.ts:70](https://github.com/TanSt optional hasUnionFrom: true; ``` -Defined in: [packages/db/src/query/builder/types.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L59) +Defined in: [packages/db/src/query/builder/types.ts:56](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L56) *** @@ -95,7 +95,7 @@ Defined in: [packages/db/src/query/builder/types.ts:59](https://github.com/TanSt optional joinTypes: Record; ``` -Defined in: [packages/db/src/query/builder/types.ts:63](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L63) +Defined in: [packages/db/src/query/builder/types.ts:60](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L60) *** @@ -105,7 +105,7 @@ Defined in: [packages/db/src/query/builder/types.ts:63](https://github.com/TanSt optional refsSchema: ContextSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:53](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L53) +Defined in: [packages/db/src/query/builder/types.ts:50](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L50) *** @@ -115,7 +115,7 @@ Defined in: [packages/db/src/query/builder/types.ts:53](https://github.com/TanSt optional result: any; ``` -Defined in: [packages/db/src/query/builder/types.ts:68](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L68) +Defined in: [packages/db/src/query/builder/types.ts:65](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L65) *** @@ -125,7 +125,7 @@ Defined in: [packages/db/src/query/builder/types.ts:68](https://github.com/TanSt schema: ContextSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:51](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L51) +Defined in: [packages/db/src/query/builder/types.ts:48](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L48) *** @@ -135,4 +135,4 @@ Defined in: [packages/db/src/query/builder/types.ts:51](https://github.com/TanSt optional singleResult: boolean; ``` -Defined in: [packages/db/src/query/builder/types.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L72) +Defined in: [packages/db/src/query/builder/types.ts:69](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L69) diff --git a/docs/reference/interfaces/CurrentStateAsChangesOptions.md b/docs/reference/interfaces/CurrentStateAsChangesOptions.md index 57662284a4..f4328c9804 100644 --- a/docs/reference/interfaces/CurrentStateAsChangesOptions.md +++ b/docs/reference/interfaces/CurrentStateAsChangesOptions.md @@ -3,7 +3,7 @@ id: CurrentStateAsChangesOptions title: CurrentStateAsChangesOptions --- -Defined in: [packages/db/src/types.ts:1168](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1168) +Defined in: [packages/db/src/types.ts:1135](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1135) Options for getting current state as changes @@ -15,7 +15,7 @@ Options for getting current state as changes optional limit: number; ``` -Defined in: [packages/db/src/types.ts:1172](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1172) +Defined in: [packages/db/src/types.ts:1139](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1139) *** @@ -25,7 +25,7 @@ Defined in: [packages/db/src/types.ts:1172](https://github.com/TanStack/db/blob/ optional optimizedOnly: boolean; ``` -Defined in: [packages/db/src/types.ts:1173](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1173) +Defined in: [packages/db/src/types.ts:1140](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1140) *** @@ -35,7 +35,7 @@ Defined in: [packages/db/src/types.ts:1173](https://github.com/TanStack/db/blob/ optional orderBy: OrderBy; ``` -Defined in: [packages/db/src/types.ts:1171](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1171) +Defined in: [packages/db/src/types.ts:1138](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1138) *** @@ -45,6 +45,6 @@ Defined in: [packages/db/src/types.ts:1171](https://github.com/TanStack/db/blob/ optional where: BasicExpression; ``` -Defined in: [packages/db/src/types.ts:1170](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1170) +Defined in: [packages/db/src/types.ts:1137](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1137) Pre-compiled expression for filtering the current state diff --git a/docs/reference/interfaces/IndexedDBCollectionConfig.md b/docs/reference/interfaces/IndexedDBCollectionConfig.md index a5e0fd0dec..8c22f8e7f0 100644 --- a/docs/reference/interfaces/IndexedDBCollectionConfig.md +++ b/docs/reference/interfaces/IndexedDBCollectionConfig.md @@ -33,7 +33,7 @@ Configuration options for creating an IndexedDB Collection optional autoIndex: "eager" | "off"; ``` -Defined in: [packages/db/src/types.ts:794](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L794) +Defined in: [packages/db/src/types.ts:781](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L781) Auto-indexing mode for the collection. When enabled, indexes will be automatically created for simple where expressions. @@ -62,7 +62,7 @@ When enabled, indexes will be automatically created for simple where expressions optional compare: (x, y) => number; ``` -Defined in: [packages/db/src/types.ts:819](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L819) +Defined in: [packages/db/src/types.ts:806](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L806) Optional function to compare two items. This is used to order the items in the collection. @@ -119,7 +119,7 @@ REQUIRED - must create database before collections optional defaultIndexType: IndexConstructor; ``` -Defined in: [packages/db/src/types.ts:808](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L808) +Defined in: [packages/db/src/types.ts:795](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L795) Default index type to use when creating indexes without an explicit type. Required for auto-indexing. Import from '@tanstack/db'. @@ -147,7 +147,7 @@ const collection = createCollection({ optional defaultStringCollation: StringCollationConfig; ``` -Defined in: [packages/db/src/types.ts:1014](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1014) +Defined in: [packages/db/src/types.ts:981](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L981) Specifies how to compare data in the collection. This should be configured to match data ordering on the backend. @@ -166,7 +166,7 @@ E.g., when using the Electric DB collection these options optional gcTime: number; ``` -Defined in: [packages/db/src/types.ts:773](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L773) +Defined in: [packages/db/src/types.ts:760](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L760) Time in milliseconds after which the collection will be garbage collected when it has no active subscribers. Defaults to 5 minutes (300000ms). @@ -187,7 +187,7 @@ disables automatic garbage collection. getKey: (item) => TKey; ``` -Defined in: [packages/db/src/types.ts:764](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L764) +Defined in: [packages/db/src/types.ts:751](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L751) Function to extract the ID from an object This is required for update/delete operations which now only accept IDs @@ -225,7 +225,7 @@ getKey: (item) => item.uuid optional id: string; ``` -Defined in: [packages/db/src/types.ts:753](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L753) +Defined in: [packages/db/src/types.ts:740](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L740) #### Inherited from @@ -249,15 +249,12 @@ Must exist in the underlying database ### onDelete? ```ts -optional onDelete: - | DeleteMutationFn -| DeleteMutationFn; +optional onDelete: DeleteMutationFn; ``` -Defined in: [packages/db/src/types.ts:1003](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1003) +Defined in: [packages/db/src/types.ts:973](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L973) Optional asynchronous handler function called before a delete operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -276,8 +273,6 @@ onDelete: async ({ transaction, collection }) => { await api.deleteTodos(keysToDelete) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -326,15 +321,12 @@ onDelete: async ({ transaction, collection }) => { ### onInsert? ```ts -optional onInsert: - | InsertMutationFn -| InsertMutationFn; +optional onInsert: InsertMutationFn; ``` -Defined in: [packages/db/src/types.ts:882](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L882) +Defined in: [packages/db/src/types.ts:865](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L865) Optional asynchronous handler function called before an insert operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -353,8 +345,6 @@ onInsert: async ({ transaction, collection }) => { await api.createTodo(newItem) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -375,7 +365,6 @@ onInsert: async ({ transaction, collection }) => { await api.createTodos(items) // Refetch to get updated data from server await collection.utils.refetch() - return { refetch: false } } ``` @@ -401,15 +390,12 @@ onInsert: async ({ transaction, collection }) => { ### onUpdate? ```ts -optional onUpdate: - | UpdateMutationFn -| UpdateMutationFn; +optional onUpdate: UpdateMutationFn; ``` -Defined in: [packages/db/src/types.ts:944](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L944) +Defined in: [packages/db/src/types.ts:920](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L920) Optional asynchronous handler function called before an update operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -429,8 +415,6 @@ onUpdate: async ({ transaction, collection }) => { await api.updateTodo(mutation.original.id, changes) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -453,7 +437,6 @@ onUpdate: async ({ transaction, collection }) => { })) await api.updateTodos(updates) await collection.utils.refetch() - return { refetch: false } } ``` @@ -483,7 +466,7 @@ onUpdate: async ({ transaction, collection }) => { optional schema: TSchema; ``` -Defined in: [packages/db/src/types.ts:754](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L754) +Defined in: [packages/db/src/types.ts:741](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L741) #### Inherited from @@ -497,7 +480,7 @@ Defined in: [packages/db/src/types.ts:754](https://github.com/TanStack/db/blob/m optional startSync: boolean; ``` -Defined in: [packages/db/src/types.ts:784](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L784) +Defined in: [packages/db/src/types.ts:771](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L771) Whether to eagerly start syncing on collection creation. When true, syncing begins immediately. When false, syncing starts when the first subscriber attaches. @@ -524,7 +507,7 @@ false optional syncMode: SyncMode; ``` -Defined in: [packages/db/src/types.ts:828](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L828) +Defined in: [packages/db/src/types.ts:815](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L815) The mode of sync to use for the collection. @@ -550,7 +533,7 @@ The exact implementation of the sync mode is up to the sync implementation. optional utils: UtilsRecord; ``` -Defined in: [packages/db/src/types.ts:1016](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1016) +Defined in: [packages/db/src/types.ts:983](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L983) #### Inherited from diff --git a/docs/reference/interfaces/LiveQueryCollectionConfig.md b/docs/reference/interfaces/LiveQueryCollectionConfig.md index 039753fffe..bf52d573cb 100644 --- a/docs/reference/interfaces/LiveQueryCollectionConfig.md +++ b/docs/reference/interfaces/LiveQueryCollectionConfig.md @@ -105,9 +105,7 @@ If not provided, defaults to `live-query-${number}` with auto-incrementing numbe ### onDelete? ```ts -optional onDelete: - | DeleteMutationFn -| DeleteMutationFn; +optional onDelete: DeleteMutationFn; ``` Defined in: [packages/db/src/query/live/types.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L100) @@ -117,9 +115,7 @@ Defined in: [packages/db/src/query/live/types.ts:100](https://github.com/TanStac ### onInsert? ```ts -optional onInsert: - | InsertMutationFn -| InsertMutationFn; +optional onInsert: InsertMutationFn; ``` Defined in: [packages/db/src/query/live/types.ts:98](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L98) @@ -131,9 +127,7 @@ Optional mutation handlers ### onUpdate? ```ts -optional onUpdate: - | UpdateMutationFn -| UpdateMutationFn; +optional onUpdate: UpdateMutationFn; ``` Defined in: [packages/db/src/query/live/types.ts:99](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/types.ts#L99) diff --git a/docs/reference/interfaces/LocalOnlyCollectionConfig.md b/docs/reference/interfaces/LocalOnlyCollectionConfig.md index e19c72208f..385f514007 100644 --- a/docs/reference/interfaces/LocalOnlyCollectionConfig.md +++ b/docs/reference/interfaces/LocalOnlyCollectionConfig.md @@ -39,7 +39,7 @@ The type of the key returned by `getKey` optional autoIndex: "eager" | "off"; ``` -Defined in: [packages/db/src/types.ts:794](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L794) +Defined in: [packages/db/src/types.ts:781](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L781) Auto-indexing mode for the collection. When enabled, indexes will be automatically created for simple where expressions. @@ -68,7 +68,7 @@ When enabled, indexes will be automatically created for simple where expressions optional compare: (x, y) => number; ``` -Defined in: [packages/db/src/types.ts:819](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L819) +Defined in: [packages/db/src/types.ts:806](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L806) Optional function to compare two items. This is used to order the items in the collection. @@ -114,7 +114,7 @@ Omit.compare optional defaultIndexType: IndexConstructor; ``` -Defined in: [packages/db/src/types.ts:808](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L808) +Defined in: [packages/db/src/types.ts:795](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L795) Default index type to use when creating indexes without an explicit type. Required for auto-indexing. Import from '@tanstack/db'. @@ -144,7 +144,7 @@ Omit.defaultIndexType optional defaultStringCollation: StringCollationConfig; ``` -Defined in: [packages/db/src/types.ts:1014](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1014) +Defined in: [packages/db/src/types.ts:981](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L981) Specifies how to compare data in the collection. This should be configured to match data ordering on the backend. @@ -165,7 +165,7 @@ Omit.defaultStringCollation getKey: (item) => TKey; ``` -Defined in: [packages/db/src/types.ts:764](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L764) +Defined in: [packages/db/src/types.ts:751](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L751) Function to extract the ID from an object This is required for update/delete operations which now only accept IDs @@ -205,7 +205,7 @@ Omit.getKey optional id: string; ``` -Defined in: [packages/db/src/types.ts:753](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L753) +Defined in: [packages/db/src/types.ts:740](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L740) #### Inherited from @@ -229,15 +229,12 @@ This data will be applied during the initial sync process ### onDelete? ```ts -optional onDelete: - | DeleteMutationFn -| DeleteMutationFn; +optional onDelete: DeleteMutationFn; ``` -Defined in: [packages/db/src/types.ts:1003](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1003) +Defined in: [packages/db/src/types.ts:973](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L973) Optional asynchronous handler function called before a delete operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -256,8 +253,6 @@ onDelete: async ({ transaction, collection }) => { await api.deleteTodos(keysToDelete) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -308,15 +303,12 @@ Omit.onDelete ### onInsert? ```ts -optional onInsert: - | InsertMutationFn -| InsertMutationFn; +optional onInsert: InsertMutationFn; ``` -Defined in: [packages/db/src/types.ts:882](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L882) +Defined in: [packages/db/src/types.ts:865](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L865) Optional asynchronous handler function called before an insert operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -335,8 +327,6 @@ onInsert: async ({ transaction, collection }) => { await api.createTodo(newItem) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -357,7 +347,6 @@ onInsert: async ({ transaction, collection }) => { await api.createTodos(items) // Refetch to get updated data from server await collection.utils.refetch() - return { refetch: false } } ``` @@ -385,15 +374,12 @@ Omit.onInsert ### onUpdate? ```ts -optional onUpdate: - | UpdateMutationFn -| UpdateMutationFn; +optional onUpdate: UpdateMutationFn; ``` -Defined in: [packages/db/src/types.ts:944](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L944) +Defined in: [packages/db/src/types.ts:920](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L920) Optional asynchronous handler function called before an update operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -413,8 +399,6 @@ onUpdate: async ({ transaction, collection }) => { await api.updateTodo(mutation.original.id, changes) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -437,7 +421,6 @@ onUpdate: async ({ transaction, collection }) => { })) await api.updateTodos(updates) await collection.utils.refetch() - return { refetch: false } } ``` @@ -469,7 +452,7 @@ Omit.onUpdate optional schema: TSchema; ``` -Defined in: [packages/db/src/types.ts:754](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L754) +Defined in: [packages/db/src/types.ts:741](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L741) #### Inherited from @@ -485,7 +468,7 @@ Omit.schema optional syncMode: SyncMode; ``` -Defined in: [packages/db/src/types.ts:828](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L828) +Defined in: [packages/db/src/types.ts:815](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L815) The mode of sync to use for the collection. @@ -511,7 +494,7 @@ The exact implementation of the sync mode is up to the sync implementation. optional utils: LocalOnlyCollectionUtils; ``` -Defined in: [packages/db/src/types.ts:1016](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1016) +Defined in: [packages/db/src/types.ts:983](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L983) #### Inherited from diff --git a/docs/reference/interfaces/LocalStorageCollectionConfig.md b/docs/reference/interfaces/LocalStorageCollectionConfig.md index 95f8f31bae..ba1f40e2bf 100644 --- a/docs/reference/interfaces/LocalStorageCollectionConfig.md +++ b/docs/reference/interfaces/LocalStorageCollectionConfig.md @@ -39,7 +39,7 @@ The type of the key returned by `getKey` optional autoIndex: "eager" | "off"; ``` -Defined in: [packages/db/src/types.ts:794](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L794) +Defined in: [packages/db/src/types.ts:781](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L781) Auto-indexing mode for the collection. When enabled, indexes will be automatically created for simple where expressions. @@ -68,7 +68,7 @@ When enabled, indexes will be automatically created for simple where expressions optional compare: (x, y) => number; ``` -Defined in: [packages/db/src/types.ts:819](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L819) +Defined in: [packages/db/src/types.ts:806](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L806) Optional function to compare two items. This is used to order the items in the collection. @@ -112,7 +112,7 @@ compare: (x, y) => x.createdAt.getTime() - y.createdAt.getTime() optional defaultIndexType: IndexConstructor; ``` -Defined in: [packages/db/src/types.ts:808](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L808) +Defined in: [packages/db/src/types.ts:795](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L795) Default index type to use when creating indexes without an explicit type. Required for auto-indexing. Import from '@tanstack/db'. @@ -140,7 +140,7 @@ const collection = createCollection({ optional defaultStringCollation: StringCollationConfig; ``` -Defined in: [packages/db/src/types.ts:1014](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1014) +Defined in: [packages/db/src/types.ts:981](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L981) Specifies how to compare data in the collection. This should be configured to match data ordering on the backend. @@ -159,7 +159,7 @@ E.g., when using the Electric DB collection these options optional gcTime: number; ``` -Defined in: [packages/db/src/types.ts:773](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L773) +Defined in: [packages/db/src/types.ts:760](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L760) Time in milliseconds after which the collection will be garbage collected when it has no active subscribers. Defaults to 5 minutes (300000ms). @@ -180,7 +180,7 @@ disables automatic garbage collection. getKey: (item) => TKey; ``` -Defined in: [packages/db/src/types.ts:764](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L764) +Defined in: [packages/db/src/types.ts:751](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L751) Function to extract the ID from an object This is required for update/delete operations which now only accept IDs @@ -218,7 +218,7 @@ getKey: (item) => item.uuid optional id: string; ``` -Defined in: [packages/db/src/types.ts:753](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L753) +Defined in: [packages/db/src/types.ts:740](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L740) #### Inherited from @@ -229,15 +229,12 @@ Defined in: [packages/db/src/types.ts:753](https://github.com/TanStack/db/blob/m ### onDelete? ```ts -optional onDelete: - | DeleteMutationFn -| DeleteMutationFn; +optional onDelete: DeleteMutationFn; ``` -Defined in: [packages/db/src/types.ts:1003](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1003) +Defined in: [packages/db/src/types.ts:973](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L973) Optional asynchronous handler function called before a delete operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -256,8 +253,6 @@ onDelete: async ({ transaction, collection }) => { await api.deleteTodos(keysToDelete) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -306,15 +301,12 @@ onDelete: async ({ transaction, collection }) => { ### onInsert? ```ts -optional onInsert: - | InsertMutationFn -| InsertMutationFn; +optional onInsert: InsertMutationFn; ``` -Defined in: [packages/db/src/types.ts:882](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L882) +Defined in: [packages/db/src/types.ts:865](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L865) Optional asynchronous handler function called before an insert operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -333,8 +325,6 @@ onInsert: async ({ transaction, collection }) => { await api.createTodo(newItem) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -355,7 +345,6 @@ onInsert: async ({ transaction, collection }) => { await api.createTodos(items) // Refetch to get updated data from server await collection.utils.refetch() - return { refetch: false } } ``` @@ -381,15 +370,12 @@ onInsert: async ({ transaction, collection }) => { ### onUpdate? ```ts -optional onUpdate: - | UpdateMutationFn -| UpdateMutationFn; +optional onUpdate: UpdateMutationFn; ``` -Defined in: [packages/db/src/types.ts:944](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L944) +Defined in: [packages/db/src/types.ts:920](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L920) Optional asynchronous handler function called before an update operation -Returning a value is deprecated; coordinate synchronization through collection utilities instead. #### Examples @@ -409,8 +395,6 @@ onUpdate: async ({ transaction, collection }) => { await api.updateTodo(mutation.original.id, changes) // Trigger refetch to sync server state await collection.utils.refetch() - // Prevent the pre-1.0 compatibility wrapper from refetching again. - return { refetch: false } } ``` @@ -433,7 +417,6 @@ onUpdate: async ({ transaction, collection }) => { })) await api.updateTodos(updates) await collection.utils.refetch() - return { refetch: false } } ``` @@ -476,7 +459,7 @@ Defaults to JSON optional schema: TSchema; ``` -Defined in: [packages/db/src/types.ts:754](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L754) +Defined in: [packages/db/src/types.ts:741](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L741) #### Inherited from @@ -490,7 +473,7 @@ Defined in: [packages/db/src/types.ts:754](https://github.com/TanStack/db/blob/m optional startSync: boolean; ``` -Defined in: [packages/db/src/types.ts:784](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L784) +Defined in: [packages/db/src/types.ts:771](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L771) Whether to eagerly start syncing on collection creation. When true, syncing begins immediately. When false, syncing starts when the first subscriber attaches. @@ -555,7 +538,7 @@ The key to use for storing the collection data in localStorage/sessionStorage optional syncMode: SyncMode; ``` -Defined in: [packages/db/src/types.ts:828](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L828) +Defined in: [packages/db/src/types.ts:815](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L815) The mode of sync to use for the collection. @@ -581,7 +564,7 @@ The exact implementation of the sync mode is up to the sync implementation. optional utils: UtilsRecord; ``` -Defined in: [packages/db/src/types.ts:1016](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1016) +Defined in: [packages/db/src/types.ts:983](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L983) #### Inherited from diff --git a/docs/reference/interfaces/SubscribeChangesOptions.md b/docs/reference/interfaces/SubscribeChangesOptions.md index 6f37dcf9fb..ea8d3f47af 100644 --- a/docs/reference/interfaces/SubscribeChangesOptions.md +++ b/docs/reference/interfaces/SubscribeChangesOptions.md @@ -3,7 +3,7 @@ id: SubscribeChangesOptions title: SubscribeChangesOptions --- -Defined in: [packages/db/src/types.ts:1094](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1094) +Defined in: [packages/db/src/types.ts:1061](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1061) Options for subscribing to collection changes @@ -25,7 +25,7 @@ Options for subscribing to collection changes optional deferAcquisition: boolean; ``` -Defined in: [packages/db/src/types.ts:1125](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1125) +Defined in: [packages/db/src/types.ts:1092](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1092) **`Internal`** @@ -43,7 +43,7 @@ subscriber or a preload in its current sync run. optional includeInitialState: boolean; ``` -Defined in: [packages/db/src/types.ts:1099](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1099) +Defined in: [packages/db/src/types.ts:1066](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1066) Whether to include the current state as initial changes @@ -55,7 +55,7 @@ Whether to include the current state as initial changes optional limit: number; ``` -Defined in: [packages/db/src/types.ts:1141](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1141) +Defined in: [packages/db/src/types.ts:1108](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1108) **`Internal`** @@ -69,7 +69,7 @@ Optional limit to include in loadSubset for query-specific cache keys. optional onLoadSubsetError: (event) => void; ``` -Defined in: [packages/db/src/types.ts:1149](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1149) +Defined in: [packages/db/src/types.ts:1116](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1116) **`Internal`** @@ -93,7 +93,7 @@ Receives subset-load failures scoped to this subscription. optional onLoadSubsetResult: (result) => void; ``` -Defined in: [packages/db/src/types.ts:1147](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1147) +Defined in: [packages/db/src/types.ts:1114](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1114) **`Internal`** @@ -118,7 +118,7 @@ Allows the caller to directly track the loading promise for isReady status. optional onStatusChange: (event) => void; ``` -Defined in: [packages/db/src/types.ts:1131](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1131) +Defined in: [packages/db/src/types.ts:1098](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1098) **`Internal`** @@ -143,7 +143,7 @@ Registered BEFORE any snapshot is requested, ensuring no status transitions are optional orderBy: OrderBy; ``` -Defined in: [packages/db/src/types.ts:1136](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1136) +Defined in: [packages/db/src/types.ts:1103](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1103) **`Internal`** @@ -157,7 +157,7 @@ Optional orderBy to include in loadSubset for query-specific cache keys. optional truncateReplayPublication: object; ``` -Defined in: [packages/db/src/types.ts:1151](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1151) +Defined in: [packages/db/src/types.ts:1118](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1118) **`Internal`** @@ -191,7 +191,7 @@ readonly succeed: () => void; optional where: (row) => any; ``` -Defined in: [packages/db/src/types.ts:1114](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1114) +Defined in: [packages/db/src/types.ts:1081](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1081) Callback function for filtering changes using a row proxy. The callback receives a proxy object that records property access, @@ -225,6 +225,6 @@ collection.subscribeChanges(callback, { optional whereExpression: BasicExpression; ``` -Defined in: [packages/db/src/types.ts:1116](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1116) +Defined in: [packages/db/src/types.ts:1083](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1083) Pre-compiled expression for filtering changes diff --git a/docs/reference/interfaces/SubscribeChangesSnapshotOptions.md b/docs/reference/interfaces/SubscribeChangesSnapshotOptions.md index fac0045734..7a01dbef59 100644 --- a/docs/reference/interfaces/SubscribeChangesSnapshotOptions.md +++ b/docs/reference/interfaces/SubscribeChangesSnapshotOptions.md @@ -3,7 +3,7 @@ id: SubscribeChangesSnapshotOptions title: SubscribeChangesSnapshotOptions --- -Defined in: [packages/db/src/types.ts:1157](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1157) +Defined in: [packages/db/src/types.ts:1124](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1124) ## Extends @@ -27,7 +27,7 @@ Defined in: [packages/db/src/types.ts:1157](https://github.com/TanStack/db/blob/ optional deferAcquisition: boolean; ``` -Defined in: [packages/db/src/types.ts:1125](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1125) +Defined in: [packages/db/src/types.ts:1092](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1092) **`Internal`** @@ -49,7 +49,7 @@ subscriber or a preload in its current sync run. optional limit: number; ``` -Defined in: [packages/db/src/types.ts:1162](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1162) +Defined in: [packages/db/src/types.ts:1129](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1129) **`Internal`** @@ -67,7 +67,7 @@ Optional limit to include in loadSubset for query-specific cache keys. optional onLoadSubsetError: (event) => void; ``` -Defined in: [packages/db/src/types.ts:1149](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1149) +Defined in: [packages/db/src/types.ts:1116](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1116) **`Internal`** @@ -97,7 +97,7 @@ Omit.onLoadSubsetError optional onLoadSubsetResult: (result) => void; ``` -Defined in: [packages/db/src/types.ts:1147](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1147) +Defined in: [packages/db/src/types.ts:1114](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1114) **`Internal`** @@ -128,7 +128,7 @@ Omit.onLoadSubsetResult optional onStatusChange: (event) => void; ``` -Defined in: [packages/db/src/types.ts:1131](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1131) +Defined in: [packages/db/src/types.ts:1098](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1098) **`Internal`** @@ -159,7 +159,7 @@ Omit.onStatusChange optional orderBy: OrderBy; ``` -Defined in: [packages/db/src/types.ts:1161](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1161) +Defined in: [packages/db/src/types.ts:1128](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1128) **`Internal`** @@ -177,7 +177,7 @@ Optional orderBy to include in loadSubset for query-specific cache keys. optional truncateReplayPublication: object; ``` -Defined in: [packages/db/src/types.ts:1151](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1151) +Defined in: [packages/db/src/types.ts:1118](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1118) **`Internal`** @@ -217,7 +217,7 @@ Omit.truncateReplayPublication optional where: (row) => any; ``` -Defined in: [packages/db/src/types.ts:1114](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1114) +Defined in: [packages/db/src/types.ts:1081](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1081) Callback function for filtering changes using a row proxy. The callback receives a proxy object that records property access, @@ -257,7 +257,7 @@ Omit.where optional whereExpression: BasicExpression; ``` -Defined in: [packages/db/src/types.ts:1116](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1116) +Defined in: [packages/db/src/types.ts:1083](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1083) Pre-compiled expression for filtering changes diff --git a/docs/reference/interfaces/Transaction.md b/docs/reference/interfaces/Transaction.md index f5b8f65cf6..ac0f7999db 100644 --- a/docs/reference/interfaces/Transaction.md +++ b/docs/reference/interfaces/Transaction.md @@ -3,7 +3,7 @@ id: Transaction title: Transaction --- -Defined in: [packages/db/src/transactions.ts:365](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L365) +Defined in: [packages/db/src/transactions.ts:375](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L375) ## Type Parameters @@ -19,7 +19,7 @@ Defined in: [packages/db/src/transactions.ts:365](https://github.com/TanStack/db autoCommit: boolean; ``` -Defined in: [packages/db/src/transactions.ts:394](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L394) +Defined in: [packages/db/src/transactions.ts:387](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L387) *** @@ -29,7 +29,7 @@ Defined in: [packages/db/src/transactions.ts:394](https://github.com/TanStack/db readonly collections: Set>; ``` -Defined in: [packages/db/src/transactions.ts:374](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L374) +Defined in: [packages/db/src/transactions.ts:384](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L384) Every Collection that has tracked this transaction. Settlement recomputes each of them, including one whose mutations merged away. @@ -42,7 +42,7 @@ each of them, including one whose mutations merged away. createdAt: Date; ``` -Defined in: [packages/db/src/transactions.ts:395](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L395) +Defined in: [packages/db/src/transactions.ts:388](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L388) *** @@ -52,7 +52,7 @@ Defined in: [packages/db/src/transactions.ts:395](https://github.com/TanStack/db optional error: object; ``` -Defined in: [packages/db/src/transactions.ts:398](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L398) +Defined in: [packages/db/src/transactions.ts:391](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L391) #### error @@ -74,34 +74,7 @@ message: string; id: string; ``` -Defined in: [packages/db/src/transactions.ts:366](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L366) - -*** - -### ~~isPersisted~~ - -```ts -isPersisted: Deferred>; -``` - -Defined in: [packages/db/src/transactions.ts:393](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L393) - -Deferred that settles when this transaction settles. - -Await `when('settled')` instead. This legacy promise resolves -when the transaction completes successfully and rejects if the transaction -fails or is rolled back. - -For non-empty commits, the mutation function is the normal settlement -boundary. This does not inherently prove that a backend has uploaded, -confirmed, or read back the write unless the mutation function waits for -that backend observation before returning. An adapter can also register -commit work during the mutation function; settlement waits for that work. - -#### Deprecated - -Use `when('settled')` instead. This alias will be removed in -the 1.0 RC. +Defined in: [packages/db/src/transactions.ts:376](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L376) *** @@ -111,7 +84,7 @@ the 1.0 RC. metadata: Record; ``` -Defined in: [packages/db/src/transactions.ts:397](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L397) +Defined in: [packages/db/src/transactions.ts:390](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L390) *** @@ -121,7 +94,7 @@ Defined in: [packages/db/src/transactions.ts:397](https://github.com/TanStack/db mutationFn: MutationFn; ``` -Defined in: [packages/db/src/transactions.ts:368](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L368) +Defined in: [packages/db/src/transactions.ts:378](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L378) *** @@ -131,7 +104,7 @@ Defined in: [packages/db/src/transactions.ts:368](https://github.com/TanStack/db mutations: PendingMutation>[]; ``` -Defined in: [packages/db/src/transactions.ts:369](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L369) +Defined in: [packages/db/src/transactions.ts:379](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L379) *** @@ -141,7 +114,7 @@ Defined in: [packages/db/src/transactions.ts:369](https://github.com/TanStack/db sequenceNumber: number; ``` -Defined in: [packages/db/src/transactions.ts:396](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L396) +Defined in: [packages/db/src/transactions.ts:389](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L389) *** @@ -151,7 +124,7 @@ Defined in: [packages/db/src/transactions.ts:396](https://github.com/TanStack/db state: TransactionState; ``` -Defined in: [packages/db/src/transactions.ts:367](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L367) +Defined in: [packages/db/src/transactions.ts:377](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L377) ## Methods @@ -278,6 +251,24 @@ The other transaction to compare to *** +### completeWithoutCommit() + +```ts +completeWithoutCommit(): void; +``` + +Defined in: [packages/db/src/transactions.ts:436](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L436) + +**`Internal`** + +Complete a transaction whose work was already applied outside commit(). + +#### Returns + +`void` + +*** + ### mutate() ```ts @@ -472,7 +463,7 @@ tracking Collections. Its mutations still name their Collection. when(_state): Promise>; ``` -Defined in: [packages/db/src/transactions.ts:437](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L437) +Defined in: [packages/db/src/transactions.ts:431](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L431) Wait for this transaction to complete successfully or fail. diff --git a/docs/reference/interfaces/VirtualRowProps.md b/docs/reference/interfaces/VirtualRowProps.md index 39084c557a..61942edeaa 100644 --- a/docs/reference/interfaces/VirtualRowProps.md +++ b/docs/reference/interfaces/VirtualRowProps.md @@ -3,12 +3,9 @@ id: VirtualRowProps title: VirtualRowProps --- -Defined in: [packages/db/src/virtual-props.ts:76](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L76) +Defined in: [packages/db/src/virtual-props.ts:77](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L77) -Virtual properties recognized on TanStack DB rows. The new -`$hasPendingWrites` field is optional here so legacy four-field rows accepted -by `hasVirtualProps` remain assignable. Rows returned by collections use -`WithVirtualProps`, which requires it. +Virtual properties recognized on TanStack DB rows. These properties are: - Computed (not stored in the data model) @@ -16,6 +13,10 @@ These properties are: - Available in queries (WHERE, ORDER BY, SELECT) - Included when spreading rows (`...user`) +Whole-row live-query outputs retain source metadata. Ordinary projections +receive metadata from the live-query Collection. An explicitly selected +virtual field name cannot replace source metadata at root publication. + ## Examples ```typescript @@ -54,29 +55,28 @@ The type of the row's key (string or number) readonly $collectionId: string; ``` -Defined in: [packages/db/src/virtual-props.ts:133](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L133) +Defined in: [packages/db/src/virtual-props.ts:115](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L115) The ID of the source collection this row originated from. In joins, this can help identify which collection each row came from. -For live query collections, this is the ID of the upstream collection. +Whole-row live queries preserve the upstream Collection's ID. Projected +row roots otherwise use the live-query Collection's ID. *** -### $hasPendingWrites? +### $hasPendingWrites ```ts -readonly optional $hasPendingWrites: boolean; +readonly $hasPendingWrites: boolean; ``` -Defined in: [packages/db/src/virtual-props.ts:87](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L87) +Defined in: [packages/db/src/virtual-props.ts:86](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L86) Whether this row currently has pending local optimistic writes. This describes the row's local optimistic state, not backend upload or -acknowledgement. It is always `false` for local-only collections. It is -optional only for compatibility with legacy rows; collection-published -rows always provide it. +acknowledgement. It is always `false` for local-only collections. *** @@ -86,7 +86,7 @@ rows always provide it. readonly $key: TKey; ``` -Defined in: [packages/db/src/virtual-props.ts:125](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L125) +Defined in: [packages/db/src/virtual-props.ts:106](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L106) The row's key (primary identifier). @@ -101,7 +101,7 @@ Useful when you need the key in projections or computations. readonly $origin: VirtualOrigin; ``` -Defined in: [packages/db/src/virtual-props.ts:117](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L117) +Defined in: [packages/db/src/virtual-props.ts:98](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L98) Collection attribution for this row's current value. `'local'` covers optimistic rows and attributed source writes; `'remote'` covers source @@ -109,32 +109,5 @@ writes without local attribution. See [VirtualOrigin](../type-aliases/VirtualOri key-and-timing rules, including truncate and overlapping mutations. For local-only collections, this is always `'local'`. -For live query collections, this is passed through from the source collection. - -*** - -### ~~$synced~~ - -```ts -readonly $synced: boolean; -``` - -Defined in: [packages/db/src/virtual-props.ts:106](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L106) - -Whether this row currently has no pending local optimistic writes. - -- `true`: No pending local optimistic mutation currently affects this row -- `false`: One or more pending local optimistic mutations currently affect this row - -This is local mutation status. It does not prove that a backend has uploaded, -confirmed, or read back the row. If you need backend-confirmed status, keep -your mutation function pending until that backend observation has happened, -or expose adapter-specific status. - -For local-only collections (no sync), this is always `true`. -For live query collections, this is passed through from the source collection. - -#### Deprecated - -Use `!row.$hasPendingWrites` instead. This alias will be -removed in the 1.0 RC. +Whole-row live queries preserve source attribution. Projected row roots +otherwise use the live-query Collection's attribution. diff --git a/docs/reference/query-db-collection/functions/queryCollectionOptions.md b/docs/reference/query-db-collection/functions/queryCollectionOptions.md index adbb3b3f2d..5a9ce90822 100644 --- a/docs/reference/query-db-collection/functions/queryCollectionOptions.md +++ b/docs/reference/query-db-collection/functions/queryCollectionOptions.md @@ -9,7 +9,7 @@ title: queryCollectionOptions function queryCollectionOptions(config): CollectionConfig, TKey, T, QueryCollectionUtils, TKey, InferSchemaInput, TError>> & object; ``` -Defined in: [packages/query-db-collection/src/query.ts:575](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L575) +Defined in: [packages/query-db-collection/src/query.ts:618](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L618) Creates query collection options for use with a standard Collection. This integrates TanStack Query with TanStack DB for automatic synchronization. @@ -149,7 +149,7 @@ const todosCollection = createCollection( function queryCollectionOptions(config): CollectionConfig> & object; ``` -Defined in: [packages/query-db-collection/src/query.ts:614](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L614) +Defined in: [packages/query-db-collection/src/query.ts:657](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L657) Creates query collection options for use with a standard Collection. This integrates TanStack Query with TanStack DB for automatic synchronization. @@ -289,7 +289,7 @@ const todosCollection = createCollection( function queryCollectionOptions(config): CollectionConfig, TKey, T, QueryCollectionUtils, TKey, InferSchemaInput, TError>> & object; ``` -Defined in: [packages/query-db-collection/src/query.ts:647](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L647) +Defined in: [packages/query-db-collection/src/query.ts:690](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L690) Creates query collection options for use with a standard Collection. This integrates TanStack Query with TanStack DB for automatic synchronization. @@ -421,7 +421,7 @@ const todosCollection = createCollection( function queryCollectionOptions(config): CollectionConfig> & object; ``` -Defined in: [packages/query-db-collection/src/query.ts:681](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L681) +Defined in: [packages/query-db-collection/src/query.ts:724](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L724) Creates query collection options for use with a standard Collection. This integrates TanStack Query with TanStack DB for automatic synchronization. diff --git a/docs/reference/query-db-collection/interfaces/QueryCollectionConfig.md b/docs/reference/query-db-collection/interfaces/QueryCollectionConfig.md index 2ff36c5f33..a730c722ab 100644 --- a/docs/reference/query-db-collection/interfaces/QueryCollectionConfig.md +++ b/docs/reference/query-db-collection/interfaces/QueryCollectionConfig.md @@ -3,13 +3,13 @@ id: QueryCollectionConfig title: QueryCollectionConfig --- -Defined in: [packages/query-db-collection/src/query.ts:116](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L116) +Defined in: [packages/query-db-collection/src/query.ts:119](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L119) Configuration options for creating a Query Collection ## Extends -- `BaseCollectionConfig`\<`T`, `TKey`, `TSchema`, [`QueryCollectionUtils`](QueryCollectionUtils.md)\<`T`, `TKey`, \[`TSchema`\] *extends* \[`never`\] ? `T` : `InferSchemaInput`\<`TSchema`\>, `TError`\>\> +- `Omit`\<`BaseCollectionConfig`\<`T`, `TKey`, `TSchema`, [`QueryCollectionUtils`](QueryCollectionUtils.md)\<`T`, `TKey`, \[`TSchema`\] *extends* \[`never`\] ? `T` : `InferSchemaInput`\<`TSchema`\>, `TError`\>\>, `"onInsert"` \| `"onUpdate"` \| `"onDelete"`\> ## Type Parameters @@ -61,7 +61,7 @@ The schema type for validation optional enabled: Enabled; ``` -Defined in: [packages/query-db-collection/src/query.ts:155](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L155) +Defined in: [packages/query-db-collection/src/query.ts:198](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L198) Whether the query should automatically run (default: true) @@ -73,7 +73,7 @@ Whether the query should automatically run (default: true) optional gcTime: number; ``` -Defined in: [packages/query-db-collection/src/query.ts:190](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L190) +Defined in: [packages/query-db-collection/src/query.ts:233](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L233) Time in milliseconds after which the collection will be garbage collected when it has no active subscribers. Defaults to 5 minutes (300000ms). @@ -85,7 +85,7 @@ disables automatic garbage collection. #### Overrides ```ts -BaseCollectionConfig.gcTime +Omit.gcTime ``` *** @@ -96,7 +96,7 @@ BaseCollectionConfig.gcTime optional initialData: TQueryData | InitialDataFunction; ``` -Defined in: [packages/query-db-collection/src/query.ts:230](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L230) +Defined in: [packages/query-db-collection/src/query.ts:273](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L273) Data used to initialize the TanStack Query cache for an eager collection. The value has the original Query response shape and is projected through @@ -110,7 +110,7 @@ the collection's select option before rows are materialized. optional initialDataUpdatedAt: number | () => number | undefined; ``` -Defined in: [packages/query-db-collection/src/query.ts:238](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L238) +Defined in: [packages/query-db-collection/src/query.ts:281](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L281) The timestamp TanStack Query uses to determine initialData freshness. @@ -122,7 +122,7 @@ The timestamp TanStack Query uses to determine initialData freshness. optional meta: Record; ``` -Defined in: [packages/query-db-collection/src/query.ts:267](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L267) +Defined in: [packages/query-db-collection/src/query.ts:310](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L310) Metadata to pass to the query. Available in queryFn via context.meta @@ -154,7 +154,67 @@ meta: { optional networkMode: NetworkMode; ``` -Defined in: [packages/query-db-collection/src/query.ts:218](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L218) +Defined in: [packages/query-db-collection/src/query.ts:261](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L261) + +*** + +### onDelete()? + +```ts +optional onDelete: (params) => Promise; +``` + +Defined in: [packages/query-db-collection/src/query.ts:168](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L168) + +#### Parameters + +##### params + +`DeleteMutationFnParams`\<`T`, `TKey`, [`QueryCollectionUtils`](QueryCollectionUtils.md)\<`T`, `TKey`, \[`TSchema`\] *extends* \[`never`\] ? `T` : `InferSchemaInput`\<`TSchema`\>, `TError`\>\> + +#### Returns + +`Promise`\<`unknown`\> + +*** + +### onInsert()? + +```ts +optional onInsert: (params) => Promise; +``` + +Defined in: [packages/query-db-collection/src/query.ts:144](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L144) + +#### Parameters + +##### params + +`InsertMutationFnParams`\<`T`, `TKey`, [`QueryCollectionUtils`](QueryCollectionUtils.md)\<`T`, `TKey`, \[`TSchema`\] *extends* \[`never`\] ? `T` : `InferSchemaInput`\<`TSchema`\>, `TError`\>\> + +#### Returns + +`Promise`\<`unknown`\> + +*** + +### onUpdate()? + +```ts +optional onUpdate: (params) => Promise; +``` + +Defined in: [packages/query-db-collection/src/query.ts:156](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L156) + +#### Parameters + +##### params + +`UpdateMutationFnParams`\<`T`, `TKey`, [`QueryCollectionUtils`](QueryCollectionUtils.md)\<`T`, `TKey`, \[`TSchema`\] *extends* \[`never`\] ? `T` : `InferSchemaInput`\<`TSchema`\>, `TError`\>\> + +#### Returns + +`Promise`\<`unknown`\> *** @@ -164,7 +224,7 @@ Defined in: [packages/query-db-collection/src/query.ts:218](https://github.com/T optional persistedGcTime: number; ``` -Defined in: [packages/query-db-collection/src/query.ts:245](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L245) +Defined in: [packages/query-db-collection/src/query.ts:288](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L288) *** @@ -174,7 +234,7 @@ Defined in: [packages/query-db-collection/src/query.ts:245](https://github.com/T queryClient: QueryClient; ``` -Defined in: [packages/query-db-collection/src/query.ts:151](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L151) +Defined in: [packages/query-db-collection/src/query.ts:194](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L194) The TanStack Query client instance @@ -186,7 +246,7 @@ The TanStack Query client instance queryFn: TQueryFn extends (context) => any[] | Promise ? (context) => T[] | Promise : TQueryFn; ``` -Defined in: [packages/query-db-collection/src/query.ts:140](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L140) +Defined in: [packages/query-db-collection/src/query.ts:183](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L183) Function that fetches data from the server. Must return the complete collection state @@ -198,7 +258,7 @@ Function that fetches data from the server. Must return the complete collection queryKey: TQueryKey | TQueryKeyBuilder; ``` -Defined in: [packages/query-db-collection/src/query.ts:138](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L138) +Defined in: [packages/query-db-collection/src/query.ts:181](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L181) The query key used by TanStack Query to identify this query @@ -210,7 +270,7 @@ The query key used by TanStack Query to identify this query optional refetchInterval: number | false | (query) => number | false | undefined; ``` -Defined in: [packages/query-db-collection/src/query.ts:162](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L162) +Defined in: [packages/query-db-collection/src/query.ts:205](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L205) *** @@ -220,7 +280,7 @@ Defined in: [packages/query-db-collection/src/query.ts:162](https://github.com/T optional refetchOnMount: boolean | "always" | (query) => boolean | "always"; ``` -Defined in: [packages/query-db-collection/src/query.ts:211](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L211) +Defined in: [packages/query-db-collection/src/query.ts:254](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L254) *** @@ -230,7 +290,7 @@ Defined in: [packages/query-db-collection/src/query.ts:211](https://github.com/T optional refetchOnReconnect: boolean | "always" | (query) => boolean | "always"; ``` -Defined in: [packages/query-db-collection/src/query.ts:204](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L204) +Defined in: [packages/query-db-collection/src/query.ts:247](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L247) *** @@ -240,7 +300,7 @@ Defined in: [packages/query-db-collection/src/query.ts:204](https://github.com/T optional refetchOnWindowFocus: boolean | "always" | (query) => boolean | "always"; ``` -Defined in: [packages/query-db-collection/src/query.ts:197](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L197) +Defined in: [packages/query-db-collection/src/query.ts:240](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L240) *** @@ -250,7 +310,7 @@ Defined in: [packages/query-db-collection/src/query.ts:197](https://github.com/T optional retry: RetryValue; ``` -Defined in: [packages/query-db-collection/src/query.ts:169](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L169) +Defined in: [packages/query-db-collection/src/query.ts:212](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L212) *** @@ -260,7 +320,7 @@ Defined in: [packages/query-db-collection/src/query.ts:169](https://github.com/T optional retryDelay: RetryDelayValue; ``` -Defined in: [packages/query-db-collection/src/query.ts:176](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L176) +Defined in: [packages/query-db-collection/src/query.ts:219](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L219) *** @@ -270,7 +330,7 @@ Defined in: [packages/query-db-collection/src/query.ts:176](https://github.com/T optional select: (data) => T[]; ``` -Defined in: [packages/query-db-collection/src/query.ts:149](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L149) +Defined in: [packages/query-db-collection/src/query.ts:192](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L192) Extracts the row array TanStack DB materializes from the Query response. The Query cache keeps the original response shape. @@ -293,4 +353,4 @@ The Query cache keeps the original response shape. optional staleTime: StaleTimeFunction; ``` -Defined in: [packages/query-db-collection/src/query.ts:183](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L183) +Defined in: [packages/query-db-collection/src/query.ts:226](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L226) diff --git a/docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md b/docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md index 07616fb317..e346298922 100644 --- a/docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md +++ b/docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md @@ -3,7 +3,7 @@ id: QueryCollectionUtils title: QueryCollectionUtils --- -Defined in: [packages/query-db-collection/src/query.ts:300](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L300) +Defined in: [packages/query-db-collection/src/query.ts:343](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L343) Utility methods available on Query Collections for direct writes and manual operations. Direct writes bypass optimistic mutations and write to the synced data store. @@ -44,7 +44,7 @@ The type of errors that can occur during queries clearError: () => Promise; ``` -Defined in: [packages/query-db-collection/src/query.ts:356](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L356) +Defined in: [packages/query-db-collection/src/query.ts:399](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L399) Refetch, retaining errors until a successful result applies. While a user mutation is persisting or its handler is active, this retains the Query @@ -69,7 +69,7 @@ Error if the refetch fails dataUpdatedAt: number; ``` -Defined in: [packages/query-db-collection/src/query.ts:340](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L340) +Defined in: [packages/query-db-collection/src/query.ts:383](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L383) Get timestamp of last successful data update (in milliseconds) @@ -81,7 +81,7 @@ Get timestamp of last successful data update (in milliseconds) errorCount: number; ``` -Defined in: [packages/query-db-collection/src/query.ts:332](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L332) +Defined in: [packages/query-db-collection/src/query.ts:375](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L375) Get the number of consecutive sync failures. Incremented only when query fails completely (not per retry attempt); reset after a successful result applies. @@ -94,7 +94,7 @@ Incremented only when query fails completely (not per retry attempt); reset afte fetchStatus: "idle" | "fetching" | "paused"; ``` -Defined in: [packages/query-db-collection/src/query.ts:346](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L346) +Defined in: [packages/query-db-collection/src/query.ts:389](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L389) Get the aggregate observer fetch status. Returns `fetching` if any observer is fetching, otherwise `paused` if any observer is paused, and @@ -108,7 +108,7 @@ observer is fetching, otherwise `paused` if any observer is paused, and isError: boolean; ``` -Defined in: [packages/query-db-collection/src/query.ts:327](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L327) +Defined in: [packages/query-db-collection/src/query.ts:370](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L370) Check if the collection is in an error state @@ -120,7 +120,7 @@ Check if the collection is in an error state isFetching: boolean; ``` -Defined in: [packages/query-db-collection/src/query.ts:334](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L334) +Defined in: [packages/query-db-collection/src/query.ts:377](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L377) Check if query is currently fetching (initial or background) @@ -132,7 +132,7 @@ Check if query is currently fetching (initial or background) isLoading: boolean; ``` -Defined in: [packages/query-db-collection/src/query.ts:338](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L338) +Defined in: [packages/query-db-collection/src/query.ts:381](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L381) Check if query is loading for the first time (no data yet) @@ -144,7 +144,7 @@ Check if query is loading for the first time (no data yet) isRefetching: boolean; ``` -Defined in: [packages/query-db-collection/src/query.ts:336](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L336) +Defined in: [packages/query-db-collection/src/query.ts:379](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L379) Check if query is refetching in background (not initial fetch) @@ -156,7 +156,7 @@ Check if query is refetching in background (not initial fetch) lastError: TError | undefined; ``` -Defined in: [packages/query-db-collection/src/query.ts:325](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L325) +Defined in: [packages/query-db-collection/src/query.ts:368](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L368) Get the last error encountered by the query (if any); reset after a successful result applies @@ -168,7 +168,7 @@ Get the last error encountered by the query (if any); reset after a successful r refetch: RefetchFn; ``` -Defined in: [packages/query-db-collection/src/query.ts:309](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L309) +Defined in: [packages/query-db-collection/src/query.ts:352](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L352) Manually refetch and await the applicable fetch or application boundary. @@ -180,7 +180,7 @@ Manually refetch and await the applicable fetch or application boundary. writeBatch: (callback) => Promise; ``` -Defined in: [packages/query-db-collection/src/query.ts:321](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L321) +Defined in: [packages/query-db-collection/src/query.ts:364](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L364) Execute direct writes as one atomic batch. Resolves when the sync commit applies. @@ -202,7 +202,7 @@ Execute direct writes as one atomic batch. Resolves when the sync commit applies writeDelete: (keys) => Promise; ``` -Defined in: [packages/query-db-collection/src/query.ts:317](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L317) +Defined in: [packages/query-db-collection/src/query.ts:360](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L360) Delete items without an optimistic update. Resolves when the sync commit applies. @@ -224,7 +224,7 @@ Delete items without an optimistic update. Resolves when the sync commit applies writeInsert: (data) => Promise; ``` -Defined in: [packages/query-db-collection/src/query.ts:311](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L311) +Defined in: [packages/query-db-collection/src/query.ts:354](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L354) Insert items without an optimistic update. Resolves when the sync commit applies. @@ -246,7 +246,7 @@ Insert items without an optimistic update. Resolves when the sync commit applies writeUpdate: (updates) => Promise; ``` -Defined in: [packages/query-db-collection/src/query.ts:313](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L313) +Defined in: [packages/query-db-collection/src/query.ts:356](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L356) Update items without an optimistic update. Resolves when the sync commit applies. @@ -268,7 +268,7 @@ Update items without an optimistic update. Resolves when the sync commit applies writeUpsert: (data) => Promise; ``` -Defined in: [packages/query-db-collection/src/query.ts:319](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L319) +Defined in: [packages/query-db-collection/src/query.ts:362](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L362) Insert or update items without an optimistic update. Resolves when the sync commit applies. diff --git a/docs/reference/rxdb-db-collection/functions/rxdbCollectionOptions.md b/docs/reference/rxdb-db-collection/functions/rxdbCollectionOptions.md index 4b0834eda3..bec830b62c 100644 --- a/docs/reference/rxdb-db-collection/functions/rxdbCollectionOptions.md +++ b/docs/reference/rxdb-db-collection/functions/rxdbCollectionOptions.md @@ -53,7 +53,7 @@ Creates RxDB collection options for use with a standard Collection #### config -`Omit`\<`BaseCollectionConfig`\<`T`, `string`, `never`, `UtilsRecord`, `any`\>, `"getKey"` \| `"onInsert"` \| `"onUpdate"` \| `"onDelete"`\> & `object` & `object` +`Omit`\<`BaseCollectionConfig`\<`T`, `string`, `never`, `UtilsRecord`\>, `"getKey"` \| `"onInsert"` \| `"onUpdate"` \| `"onDelete"`\> & `object` & `object` Configuration options for the RxDB collection diff --git a/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md b/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md index 694de11907..97b4375a0b 100644 --- a/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md +++ b/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md @@ -7,7 +7,7 @@ title: ApplyJoinOptionalityToMergedSchema type ApplyJoinOptionalityToMergedSchema = { [K in keyof TExistingSchema]: K extends TFromSourceNames ? TJoinType extends "right" | "full" ? TExistingSchema[K] | undefined : TExistingSchema[K] : TExistingSchema[K] } & { [K in keyof TNewSchema]: TJoinType extends "left" | "full" ? TNewSchema[K] | undefined : TNewSchema[K] }; ``` -Defined in: [packages/db/src/query/builder/types.ts:1037](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1037) +Defined in: [packages/db/src/query/builder/types.ts:1032](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1032) ApplyJoinOptionalityToMergedSchema - Applies optionality rules when merging schemas diff --git a/docs/reference/type-aliases/ChangeListener.md b/docs/reference/type-aliases/ChangeListener.md index 07d6ee33c1..aa111e30e9 100644 --- a/docs/reference/type-aliases/ChangeListener.md +++ b/docs/reference/type-aliases/ChangeListener.md @@ -7,7 +7,7 @@ title: ChangeListener type ChangeListener = (changes) => void; ``` -Defined in: [packages/db/src/types.ts:1209](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1209) +Defined in: [packages/db/src/types.ts:1176](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1176) Function type for listening to collection changes Changes to the same key retain their causal order within a callback. diff --git a/docs/reference/type-aliases/ChangesPayload.md b/docs/reference/type-aliases/ChangesPayload.md index 39e62f18e0..1ad316d1b9 100644 --- a/docs/reference/type-aliases/ChangesPayload.md +++ b/docs/reference/type-aliases/ChangesPayload.md @@ -7,7 +7,7 @@ title: ChangesPayload type ChangesPayload = ChangeMessage, TKey>[]; ``` -Defined in: [packages/db/src/types.ts:1051](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1051) +Defined in: [packages/db/src/types.ts:1018](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1018) ## Type Parameters diff --git a/docs/reference/type-aliases/CollectionConfigSingleRowOption.md b/docs/reference/type-aliases/CollectionConfigSingleRowOption.md index 6ae70160c4..105b03540e 100644 --- a/docs/reference/type-aliases/CollectionConfigSingleRowOption.md +++ b/docs/reference/type-aliases/CollectionConfigSingleRowOption.md @@ -7,7 +7,7 @@ title: CollectionConfigSingleRowOption type CollectionConfigSingleRowOption = CollectionConfig & MaybeSingleResult; ``` -Defined in: [packages/db/src/types.ts:1044](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1044) +Defined in: [packages/db/src/types.ts:1011](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1011) ## Type Parameters diff --git a/docs/reference/type-aliases/CollectionStatus.md b/docs/reference/type-aliases/CollectionStatus.md index e97597f3df..38eb773bda 100644 --- a/docs/reference/type-aliases/CollectionStatus.md +++ b/docs/reference/type-aliases/CollectionStatus.md @@ -7,7 +7,7 @@ title: CollectionStatus type CollectionStatus = "idle" | "loading" | "ready" | "error" | "cleaned-up"; ``` -Defined in: [packages/db/src/types.ts:726](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L726) +Defined in: [packages/db/src/types.ts:714](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L714) Collection status values for lifecycle management diff --git a/docs/reference/type-aliases/ContextFromSource.md b/docs/reference/type-aliases/ContextFromSource.md index 7331163899..44e1da2b08 100644 --- a/docs/reference/type-aliases/ContextFromSource.md +++ b/docs/reference/type-aliases/ContextFromSource.md @@ -7,7 +7,7 @@ title: ContextFromSource type ContextFromSource = object; ``` -Defined in: [packages/db/src/query/builder/types.ts:154](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L154) +Defined in: [packages/db/src/query/builder/types.ts:151](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L151) ## Type Parameters @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/builder/types.ts:154](https://github.com/TanS baseSchema: SchemaFromSource; ``` -Defined in: [packages/db/src/query/builder/types.ts:155](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L155) +Defined in: [packages/db/src/query/builder/types.ts:152](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L152) *** @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/types.ts:155](https://github.com/TanS fromSourceName: keyof TSource & string; ``` -Defined in: [packages/db/src/query/builder/types.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L157) +Defined in: [packages/db/src/query/builder/types.ts:154](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L154) *** @@ -43,7 +43,7 @@ Defined in: [packages/db/src/query/builder/types.ts:157](https://github.com/TanS hasJoins: false; ``` -Defined in: [packages/db/src/query/builder/types.ts:158](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L158) +Defined in: [packages/db/src/query/builder/types.ts:155](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L155) *** @@ -53,4 +53,4 @@ Defined in: [packages/db/src/query/builder/types.ts:158](https://github.com/TanS schema: SchemaFromSource; ``` -Defined in: [packages/db/src/query/builder/types.ts:156](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L156) +Defined in: [packages/db/src/query/builder/types.ts:153](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L153) diff --git a/docs/reference/type-aliases/ContextFromUnionBranches.md b/docs/reference/type-aliases/ContextFromUnionBranches.md index 84c7931b1c..3112c63465 100644 --- a/docs/reference/type-aliases/ContextFromUnionBranches.md +++ b/docs/reference/type-aliases/ContextFromUnionBranches.md @@ -7,7 +7,7 @@ title: ContextFromUnionBranches type ContextFromUnionBranches = object; ``` -Defined in: [packages/db/src/query/builder/types.ts:188](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L188) +Defined in: [packages/db/src/query/builder/types.ts:185](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L185) ## Type Parameters @@ -23,7 +23,7 @@ Defined in: [packages/db/src/query/builder/types.ts:188](https://github.com/TanS [BranchUnionRefs]: UnionBranchResult; ``` -Defined in: [packages/db/src/query/builder/types.ts:198](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L198) +Defined in: [packages/db/src/query/builder/types.ts:195](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L195) *** @@ -33,7 +33,7 @@ Defined in: [packages/db/src/query/builder/types.ts:198](https://github.com/TanS baseSchema: UnionBranchSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:191](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L191) +Defined in: [packages/db/src/query/builder/types.ts:188](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L188) *** @@ -43,7 +43,7 @@ Defined in: [packages/db/src/query/builder/types.ts:191](https://github.com/TanS fromSourceName: keyof UnionBranchSchema & string; ``` -Defined in: [packages/db/src/query/builder/types.ts:194](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L194) +Defined in: [packages/db/src/query/builder/types.ts:191](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L191) *** @@ -53,7 +53,7 @@ Defined in: [packages/db/src/query/builder/types.ts:194](https://github.com/TanS hasJoins: false; ``` -Defined in: [packages/db/src/query/builder/types.ts:195](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L195) +Defined in: [packages/db/src/query/builder/types.ts:192](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L192) *** @@ -63,7 +63,7 @@ Defined in: [packages/db/src/query/builder/types.ts:195](https://github.com/TanS hasResult: true; ``` -Defined in: [packages/db/src/query/builder/types.ts:197](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L197) +Defined in: [packages/db/src/query/builder/types.ts:194](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L194) *** @@ -73,7 +73,7 @@ Defined in: [packages/db/src/query/builder/types.ts:197](https://github.com/TanS refsSchema: UnionBranchSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L193) +Defined in: [packages/db/src/query/builder/types.ts:190](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L190) *** @@ -83,7 +83,7 @@ Defined in: [packages/db/src/query/builder/types.ts:193](https://github.com/TanS result: PrettifyIfPlainObject>; ``` -Defined in: [packages/db/src/query/builder/types.ts:196](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L196) +Defined in: [packages/db/src/query/builder/types.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L193) *** @@ -93,4 +93,4 @@ Defined in: [packages/db/src/query/builder/types.ts:196](https://github.com/TanS schema: UnionBranchSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:192](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L192) +Defined in: [packages/db/src/query/builder/types.ts:189](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L189) diff --git a/docs/reference/type-aliases/ContextFromUnionSource.md b/docs/reference/type-aliases/ContextFromUnionSource.md index 34c1c1f710..29ae676ab6 100644 --- a/docs/reference/type-aliases/ContextFromUnionSource.md +++ b/docs/reference/type-aliases/ContextFromUnionSource.md @@ -7,7 +7,7 @@ title: ContextFromUnionSource type ContextFromUnionSource = IsUnion extends true ? object : ContextFromSource; ``` -Defined in: [packages/db/src/query/builder/types.ts:161](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L161) +Defined in: [packages/db/src/query/builder/types.ts:158](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L158) ## Type Parameters diff --git a/docs/reference/type-aliases/ContextSchema.md b/docs/reference/type-aliases/ContextSchema.md index 7e0592623c..1379b230af 100644 --- a/docs/reference/type-aliases/ContextSchema.md +++ b/docs/reference/type-aliases/ContextSchema.md @@ -7,7 +7,7 @@ title: ContextSchema type ContextSchema = Record; ``` -Defined in: [packages/db/src/query/builder/types.ts:84](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L84) +Defined in: [packages/db/src/query/builder/types.ts:81](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L81) ContextSchema - The shape of available tables/collections in a query context diff --git a/docs/reference/type-aliases/DeleteMutationFn.md b/docs/reference/type-aliases/DeleteMutationFn.md index 46bbed4f46..307c28f162 100644 --- a/docs/reference/type-aliases/DeleteMutationFn.md +++ b/docs/reference/type-aliases/DeleteMutationFn.md @@ -4,10 +4,10 @@ title: DeleteMutationFn --- ```ts -type DeleteMutationFn = (params) => Promise; +type DeleteMutationFn = (params) => Promise; ``` -Defined in: [packages/db/src/types.ts:703](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L703) +Defined in: [packages/db/src/types.ts:692](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L692) ## Type Parameters @@ -23,12 +23,6 @@ Defined in: [packages/db/src/types.ts:703](https://github.com/TanStack/db/blob/m `TUtils` *extends* [`UtilsRecord`](UtilsRecord.md) = [`UtilsRecord`](UtilsRecord.md) -### TReturn - -`TReturn` = `any` - -DEPRECATED: Return values are kept for backward compatibility and will be removed in v1.0. - ## Parameters ### params @@ -37,4 +31,4 @@ DEPRECATED: Return values are kept for backward compatibility and will be remove ## Returns -`Promise`\<`TReturn`\> +`Promise`\<`void`\> diff --git a/docs/reference/type-aliases/FunctionalHavingRow.md b/docs/reference/type-aliases/FunctionalHavingRow.md index 08594c36ad..419c3e0658 100644 --- a/docs/reference/type-aliases/FunctionalHavingRow.md +++ b/docs/reference/type-aliases/FunctionalHavingRow.md @@ -7,7 +7,7 @@ title: FunctionalHavingRow type FunctionalHavingRow = TContext["schema"] & TContext["hasResult"] extends true ? object : object; ``` -Defined in: [packages/db/src/query/builder/types.ts:662](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L662) +Defined in: [packages/db/src/query/builder/types.ts:659](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L659) FunctionalHavingRow - Type for the row parameter in functional having callbacks diff --git a/docs/reference/type-aliases/GetResult.md b/docs/reference/type-aliases/GetResult.md index 0e82d3f63b..3eae2b35fa 100644 --- a/docs/reference/type-aliases/GetResult.md +++ b/docs/reference/type-aliases/GetResult.md @@ -7,7 +7,7 @@ title: GetResult type GetResult = Prettify>; ``` -Defined in: [packages/db/src/query/builder/types.ts:1171](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1171) +Defined in: [packages/db/src/query/builder/types.ts:1166](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1166) ## Type Parameters diff --git a/docs/reference/type-aliases/GroupByCallback.md b/docs/reference/type-aliases/GroupByCallback.md index fd4a00f164..c21fae0fe0 100644 --- a/docs/reference/type-aliases/GroupByCallback.md +++ b/docs/reference/type-aliases/GroupByCallback.md @@ -7,7 +7,7 @@ title: GroupByCallback type GroupByCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:625](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L625) +Defined in: [packages/db/src/query/builder/types.ts:622](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L622) GroupByCallback - Type for groupBy clause callback functions diff --git a/docs/reference/type-aliases/InferCollectionType.md b/docs/reference/type-aliases/InferCollectionType.md index 118b077e7b..8cfd71117b 100644 --- a/docs/reference/type-aliases/InferCollectionType.md +++ b/docs/reference/type-aliases/InferCollectionType.md @@ -7,7 +7,7 @@ title: InferCollectionType type InferCollectionType = T extends CollectionImpl ? WithVirtualProps : T extends CollectionOptionsIdentity ? WithVirtualProps : never; ``` -Defined in: [packages/db/src/query/builder/types.ts:108](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L108) +Defined in: [packages/db/src/query/builder/types.ts:105](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L105) InferCollectionType - Extracts the TypeScript type from a CollectionImpl diff --git a/docs/reference/type-aliases/InferResultType.md b/docs/reference/type-aliases/InferResultType.md index 60c2fcb1be..bcb948841d 100644 --- a/docs/reference/type-aliases/InferResultType.md +++ b/docs/reference/type-aliases/InferResultType.md @@ -7,7 +7,7 @@ title: InferResultType type InferResultType = TContext extends SingleResult ? GetResult | undefined : GetResult[]; ``` -Defined in: [packages/db/src/query/builder/types.ts:1063](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1063) +Defined in: [packages/db/src/query/builder/types.ts:1058](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1058) Utility type to infer the query result size (single row or an array) diff --git a/docs/reference/type-aliases/InputRow.md b/docs/reference/type-aliases/InputRow.md index 718a34d094..9c9eb7f0f3 100644 --- a/docs/reference/type-aliases/InputRow.md +++ b/docs/reference/type-aliases/InputRow.md @@ -7,6 +7,6 @@ title: InputRow type InputRow = [unknown, Record]; ``` -Defined in: [packages/db/src/types.ts:1059](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1059) +Defined in: [packages/db/src/types.ts:1026](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1026) An input row from a collection diff --git a/docs/reference/type-aliases/InsertMutationFn.md b/docs/reference/type-aliases/InsertMutationFn.md index e3aed64b20..bdaee5a990 100644 --- a/docs/reference/type-aliases/InsertMutationFn.md +++ b/docs/reference/type-aliases/InsertMutationFn.md @@ -4,10 +4,10 @@ title: InsertMutationFn --- ```ts -type InsertMutationFn = (params) => Promise; +type InsertMutationFn = (params) => Promise; ``` -Defined in: [packages/db/src/types.ts:683](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L683) +Defined in: [packages/db/src/types.ts:680](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L680) ## Type Parameters @@ -23,12 +23,6 @@ Defined in: [packages/db/src/types.ts:683](https://github.com/TanStack/db/blob/m `TUtils` *extends* [`UtilsRecord`](UtilsRecord.md) = [`UtilsRecord`](UtilsRecord.md) -### TReturn - -`TReturn` = `any` - -DEPRECATED: Return values are kept for backward compatibility and will be removed in v1.0. - ## Parameters ### params @@ -37,4 +31,4 @@ DEPRECATED: Return values are kept for backward compatibility and will be remove ## Returns -`Promise`\<`TReturn`\> +`Promise`\<`void`\> diff --git a/docs/reference/type-aliases/JoinOnCallback.md b/docs/reference/type-aliases/JoinOnCallback.md index 5aa8093c06..412d4609b0 100644 --- a/docs/reference/type-aliases/JoinOnCallback.md +++ b/docs/reference/type-aliases/JoinOnCallback.md @@ -7,7 +7,7 @@ title: JoinOnCallback type JoinOnCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:641](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L641) +Defined in: [packages/db/src/query/builder/types.ts:638](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L638) JoinOnCallback - Type for join condition callback functions diff --git a/docs/reference/type-aliases/KeyedNamespacedRow.md b/docs/reference/type-aliases/KeyedNamespacedRow.md index 2a3f49d2ec..5fe5b33920 100644 --- a/docs/reference/type-aliases/KeyedNamespacedRow.md +++ b/docs/reference/type-aliases/KeyedNamespacedRow.md @@ -7,7 +7,7 @@ title: KeyedNamespacedRow type KeyedNamespacedRow = [unknown, NamespacedRow]; ``` -Defined in: [packages/db/src/types.ts:1082](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1082) +Defined in: [packages/db/src/types.ts:1049](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1049) A keyed namespaced row is a row with a key and a namespaced row This is the main representation of a row in a query pipeline diff --git a/docs/reference/type-aliases/KeyedStream.md b/docs/reference/type-aliases/KeyedStream.md index e4bb4a672a..3a1c0b11cb 100644 --- a/docs/reference/type-aliases/KeyedStream.md +++ b/docs/reference/type-aliases/KeyedStream.md @@ -7,7 +7,7 @@ title: KeyedStream type KeyedStream = IStreamBuilder; ``` -Defined in: [packages/db/src/types.ts:1065](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1065) +Defined in: [packages/db/src/types.ts:1032](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1032) A keyed stream is a stream of rows This is used as the inputs from a collection to a query diff --git a/docs/reference/type-aliases/MakeOptional.md b/docs/reference/type-aliases/MakeOptional.md index ee3d26a620..4b996ee4d5 100644 --- a/docs/reference/type-aliases/MakeOptional.md +++ b/docs/reference/type-aliases/MakeOptional.md @@ -7,7 +7,7 @@ title: MakeOptional type MakeOptional = Omit & Partial>; ``` -Defined in: [packages/db/src/types.ts:1288](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1288) +Defined in: [packages/db/src/types.ts:1255](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1255) ## Type Parameters diff --git a/docs/reference/type-aliases/MaybeSingleResult.md b/docs/reference/type-aliases/MaybeSingleResult.md index d1170110ec..e526b3c48e 100644 --- a/docs/reference/type-aliases/MaybeSingleResult.md +++ b/docs/reference/type-aliases/MaybeSingleResult.md @@ -7,7 +7,7 @@ title: MaybeSingleResult type MaybeSingleResult = object; ``` -Defined in: [packages/db/src/types.ts:1036](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1036) +Defined in: [packages/db/src/types.ts:1003](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1003) ## Properties @@ -17,6 +17,6 @@ Defined in: [packages/db/src/types.ts:1036](https://github.com/TanStack/db/blob/ optional singleResult: true; ``` -Defined in: [packages/db/src/types.ts:1040](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1040) +Defined in: [packages/db/src/types.ts:1007](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1007) If enabled the collection will return a single object instead of an array diff --git a/docs/reference/type-aliases/MergeContextForJoinCallback.md b/docs/reference/type-aliases/MergeContextForJoinCallback.md index 35a01bf8f7..2219b02877 100644 --- a/docs/reference/type-aliases/MergeContextForJoinCallback.md +++ b/docs/reference/type-aliases/MergeContextForJoinCallback.md @@ -7,7 +7,7 @@ title: MergeContextForJoinCallback type MergeContextForJoinCallback = Omit & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:1328](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1328) +Defined in: [packages/db/src/query/builder/types.ts:1323](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1323) MergeContextForJoinCallback - Special context for join condition callbacks diff --git a/docs/reference/type-aliases/MergeContextWithJoinType.md b/docs/reference/type-aliases/MergeContextWithJoinType.md index 77505e1816..d5cca28ad0 100644 --- a/docs/reference/type-aliases/MergeContextWithJoinType.md +++ b/docs/reference/type-aliases/MergeContextWithJoinType.md @@ -7,7 +7,7 @@ title: MergeContextWithJoinType type MergeContextWithJoinType = Omit & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:983](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L983) +Defined in: [packages/db/src/query/builder/types.ts:978](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L978) MergeContextWithJoinType - Creates a new context after a join operation diff --git a/docs/reference/type-aliases/NamespacedAndKeyedStream.md b/docs/reference/type-aliases/NamespacedAndKeyedStream.md index 5095a65ed8..b03b985213 100644 --- a/docs/reference/type-aliases/NamespacedAndKeyedStream.md +++ b/docs/reference/type-aliases/NamespacedAndKeyedStream.md @@ -7,7 +7,7 @@ title: NamespacedAndKeyedStream type NamespacedAndKeyedStream = IStreamBuilder; ``` -Defined in: [packages/db/src/types.ts:1089](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1089) +Defined in: [packages/db/src/types.ts:1056](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1056) A namespaced and keyed stream is a stream of rows This is used throughout a query pipeline and as the output from a query without diff --git a/docs/reference/type-aliases/NamespacedRow.md b/docs/reference/type-aliases/NamespacedRow.md index f04f691dd5..7435ff95e7 100644 --- a/docs/reference/type-aliases/NamespacedRow.md +++ b/docs/reference/type-aliases/NamespacedRow.md @@ -7,6 +7,6 @@ title: NamespacedRow type NamespacedRow = Record>; ``` -Defined in: [packages/db/src/types.ts:1076](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1076) +Defined in: [packages/db/src/types.ts:1043](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1043) A namespaced row is a row withing a pipeline that had each table wrapped in its alias diff --git a/docs/reference/type-aliases/NonSingleResult.md b/docs/reference/type-aliases/NonSingleResult.md index 96e98a2a34..544fdade5f 100644 --- a/docs/reference/type-aliases/NonSingleResult.md +++ b/docs/reference/type-aliases/NonSingleResult.md @@ -7,7 +7,7 @@ title: NonSingleResult type NonSingleResult = object; ``` -Defined in: [packages/db/src/types.ts:1032](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1032) +Defined in: [packages/db/src/types.ts:999](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L999) ## Properties @@ -17,4 +17,4 @@ Defined in: [packages/db/src/types.ts:1032](https://github.com/TanStack/db/blob/ optional singleResult: never; ``` -Defined in: [packages/db/src/types.ts:1033](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1033) +Defined in: [packages/db/src/types.ts:1000](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1000) diff --git a/docs/reference/type-aliases/OrderByCallback.md b/docs/reference/type-aliases/OrderByCallback.md index 0210ce829c..3edcd5a357 100644 --- a/docs/reference/type-aliases/OrderByCallback.md +++ b/docs/reference/type-aliases/OrderByCallback.md @@ -7,7 +7,7 @@ title: OrderByCallback type OrderByCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:589](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L589) +Defined in: [packages/db/src/query/builder/types.ts:586](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L586) OrderByCallback - Type for orderBy clause callback functions diff --git a/docs/reference/type-aliases/Prettify.md b/docs/reference/type-aliases/Prettify.md index 9999dd635b..6a8bd97259 100644 --- a/docs/reference/type-aliases/Prettify.md +++ b/docs/reference/type-aliases/Prettify.md @@ -7,7 +7,7 @@ title: Prettify type Prettify = { [K in keyof T]: T[K] } & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:1370](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1370) +Defined in: [packages/db/src/query/builder/types.ts:1365](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1365) Prettify - Utility type for clean IDE display diff --git a/docs/reference/type-aliases/Ref.md b/docs/reference/type-aliases/Ref.md index 77e88812f2..5f31d93a1f 100644 --- a/docs/reference/type-aliases/Ref.md +++ b/docs/reference/type-aliases/Ref.md @@ -7,7 +7,7 @@ title: Ref type Ref = T extends unknown ? RefBranch : never; ``` -Defined in: [packages/db/src/query/builder/types.ts:899](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L899) +Defined in: [packages/db/src/query/builder/types.ts:894](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L894) Ref - The user-facing ref interface for the query builder diff --git a/docs/reference/type-aliases/RefsForContext.md b/docs/reference/type-aliases/RefsForContext.md index b3c59189e0..8e18e503d5 100644 --- a/docs/reference/type-aliases/RefsForContext.md +++ b/docs/reference/type-aliases/RefsForContext.md @@ -7,7 +7,7 @@ title: RefsForContext type RefsForContext = { [K in Exclude>, keyof JoinedRefsForContext | keyof BranchUnionResultRefs>]: RefForContextSchemaValue, K>, IsNullableContextKey> } & TContext["hasResult"] extends true ? object : object & BranchUnionResultRefs & JoinedRefsForContext; ``` -Defined in: [packages/db/src/query/builder/types.ts:761](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L761) +Defined in: [packages/db/src/query/builder/types.ts:758](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L758) ## Type Parameters diff --git a/docs/reference/type-aliases/ResultStream.md b/docs/reference/type-aliases/ResultStream.md index 3dec6bd5dc..9e86a8cc25 100644 --- a/docs/reference/type-aliases/ResultStream.md +++ b/docs/reference/type-aliases/ResultStream.md @@ -7,7 +7,7 @@ title: ResultStream type ResultStream = IStreamBuilder<[unknown, [any, string | undefined]]>; ``` -Defined in: [packages/db/src/types.ts:1071](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1071) +Defined in: [packages/db/src/types.ts:1038](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1038) Result stream type representing the output of compiled queries Always returns [key, [result, orderByIndex]] where orderByIndex is undefined for unordered queries diff --git a/docs/reference/type-aliases/ResultTypeFromSelect.md b/docs/reference/type-aliases/ResultTypeFromSelect.md index 00f2132fc4..9209506e7c 100644 --- a/docs/reference/type-aliases/ResultTypeFromSelect.md +++ b/docs/reference/type-aliases/ResultTypeFromSelect.md @@ -7,7 +7,7 @@ title: ResultTypeFromSelect type ResultTypeFromSelect = IsAny extends true ? any : IsTrueRef extends true ? ExtractDirectSelectRef : WithoutRefBrand extends true ? ExtractExpressionType : TSelectObject[K] extends ToArrayWrapper ? T[] : TSelectObject[K] extends ConcatToArrayWrapper ? string : TSelectObject[K] extends MaterializeWrapper ? IsSingle extends true ? (...) | (...) : (...)[] : (...)[(...)] extends { __brand: ...; _result?: ... } ? ResultTypeFromCaseWhen<(...)> : (...) extends (...) ? (...) : (...) }>>; ``` -Defined in: [packages/db/src/query/builder/types.ts:413](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L413) +Defined in: [packages/db/src/query/builder/types.ts:410](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L410) ResultTypeFromSelect - Infers the result type from a select object diff --git a/docs/reference/type-aliases/SchemaFromSource.md b/docs/reference/type-aliases/SchemaFromSource.md index 64f52563e8..35e2164826 100644 --- a/docs/reference/type-aliases/SchemaFromSource.md +++ b/docs/reference/type-aliases/SchemaFromSource.md @@ -7,7 +7,7 @@ title: SchemaFromSource type SchemaFromSource = Prettify<{ [K in keyof T]: T[K] extends CollectionImpl ? InferCollectionType : T[K] extends CollectionOptionsIdentity ? InferCollectionType : T[K] extends QueryBuilder ? GetRawResult : never }>; ``` -Defined in: [packages/db/src/query/builder/types.ts:131](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L131) +Defined in: [packages/db/src/query/builder/types.ts:128](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L128) SchemaFromSource - Converts a Source definition into a ContextSchema diff --git a/docs/reference/type-aliases/SelectObject.md b/docs/reference/type-aliases/SelectObject.md index 1cd3cfc774..d957d2ec44 100644 --- a/docs/reference/type-aliases/SelectObject.md +++ b/docs/reference/type-aliases/SelectObject.md @@ -7,7 +7,7 @@ title: SelectObject type SelectObject = T; ``` -Defined in: [packages/db/src/query/builder/types.ts:303](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L303) +Defined in: [packages/db/src/query/builder/types.ts:300](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L300) SelectObject - Wrapper type for select clause objects diff --git a/docs/reference/type-aliases/SingleResult.md b/docs/reference/type-aliases/SingleResult.md index 0c9ea92e08..7fca2ce762 100644 --- a/docs/reference/type-aliases/SingleResult.md +++ b/docs/reference/type-aliases/SingleResult.md @@ -7,7 +7,7 @@ title: SingleResult type SingleResult = object; ``` -Defined in: [packages/db/src/types.ts:1028](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1028) +Defined in: [packages/db/src/types.ts:995](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L995) ## Properties @@ -17,4 +17,4 @@ Defined in: [packages/db/src/types.ts:1028](https://github.com/TanStack/db/blob/ singleResult: true; ``` -Defined in: [packages/db/src/types.ts:1029](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1029) +Defined in: [packages/db/src/types.ts:996](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L996) diff --git a/docs/reference/type-aliases/SingleSource.md b/docs/reference/type-aliases/SingleSource.md index 846101c262..0a102b3143 100644 --- a/docs/reference/type-aliases/SingleSource.md +++ b/docs/reference/type-aliases/SingleSource.md @@ -7,7 +7,7 @@ title: SingleSource type SingleSource = IsUnion extends true ? never : TSource; ``` -Defined in: [packages/db/src/query/builder/types.ts:151](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L151) +Defined in: [packages/db/src/query/builder/types.ts:148](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L148) ## Type Parameters diff --git a/docs/reference/type-aliases/Source.md b/docs/reference/type-aliases/Source.md index 5b3d8d1594..c6f57d80b6 100644 --- a/docs/reference/type-aliases/Source.md +++ b/docs/reference/type-aliases/Source.md @@ -7,7 +7,7 @@ title: Source type Source = object; ``` -Defined in: [packages/db/src/query/builder/types.ts:95](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L95) +Defined in: [packages/db/src/query/builder/types.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L92) Source - Input definition for query builder `from()` and `unionAll()` clauses diff --git a/docs/reference/type-aliases/SyncMode.md b/docs/reference/type-aliases/SyncMode.md index eb6581d046..1fb57845e4 100644 --- a/docs/reference/type-aliases/SyncMode.md +++ b/docs/reference/type-aliases/SyncMode.md @@ -7,4 +7,4 @@ title: SyncMode type SyncMode = "eager" | "on-demand"; ``` -Defined in: [packages/db/src/types.ts:738](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L738) +Defined in: [packages/db/src/types.ts:726](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L726) diff --git a/docs/reference/type-aliases/UpdateMutationFn.md b/docs/reference/type-aliases/UpdateMutationFn.md index 745eac2e87..87e8ca806f 100644 --- a/docs/reference/type-aliases/UpdateMutationFn.md +++ b/docs/reference/type-aliases/UpdateMutationFn.md @@ -4,10 +4,10 @@ title: UpdateMutationFn --- ```ts -type UpdateMutationFn = (params) => Promise; +type UpdateMutationFn = (params) => Promise; ``` -Defined in: [packages/db/src/types.ts:693](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L693) +Defined in: [packages/db/src/types.ts:686](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L686) ## Type Parameters @@ -23,12 +23,6 @@ Defined in: [packages/db/src/types.ts:693](https://github.com/TanStack/db/blob/m `TUtils` *extends* [`UtilsRecord`](UtilsRecord.md) = [`UtilsRecord`](UtilsRecord.md) -### TReturn - -`TReturn` = `any` - -DEPRECATED: Return values are kept for backward compatibility and will be removed in v1.0. - ## Parameters ### params @@ -37,4 +31,4 @@ DEPRECATED: Return values are kept for backward compatibility and will be remove ## Returns -`Promise`\<`TReturn`\> +`Promise`\<`void`\> diff --git a/docs/reference/type-aliases/WhereCallback.md b/docs/reference/type-aliases/WhereCallback.md index 0f5a10e44f..2658bd594e 100644 --- a/docs/reference/type-aliases/WhereCallback.md +++ b/docs/reference/type-aliases/WhereCallback.md @@ -7,7 +7,7 @@ title: WhereCallback type WhereCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:218](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L218) +Defined in: [packages/db/src/query/builder/types.ts:215](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L215) WhereCallback - Type for where/having clause callback functions diff --git a/docs/reference/type-aliases/WithResult.md b/docs/reference/type-aliases/WithResult.md index 687f0b2fcc..15fed9b47e 100644 --- a/docs/reference/type-aliases/WithResult.md +++ b/docs/reference/type-aliases/WithResult.md @@ -7,7 +7,7 @@ title: WithResult type WithResult = Prettify & Pick & object>; ``` -Defined in: [packages/db/src/query/builder/types.ts:1359](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1359) +Defined in: [packages/db/src/query/builder/types.ts:1354](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1354) WithResult - Updates a context with a new result type after select() diff --git a/docs/reference/type-aliases/WithVirtualProps.md b/docs/reference/type-aliases/WithVirtualProps.md index 3ae1e8ba71..d9da7f4b71 100644 --- a/docs/reference/type-aliases/WithVirtualProps.md +++ b/docs/reference/type-aliases/WithVirtualProps.md @@ -4,10 +4,10 @@ title: WithVirtualProps --- ```ts -type WithVirtualProps = T & PublishedVirtualRowProps; +type WithVirtualProps = T & VirtualRowProps; ``` -Defined in: [packages/db/src/virtual-props.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L157) +Defined in: [packages/db/src/virtual-props.ts:131](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L131) Adds virtual properties to a row type. @@ -30,6 +30,5 @@ The type of the row's key ```typescript type User = { id: string; name: string } type UserWithVirtual = WithVirtualProps -// { id: string; name: string; $hasPendingWrites: boolean; $synced: boolean; $origin: 'local' | 'remote'; $key: string; $collectionId: string } -// $synced is deprecated; use !$hasPendingWrites instead. +// { id: string; name: string; $hasPendingWrites: boolean; $origin: 'local' | 'remote'; $key: string; $collectionId: string } ``` diff --git a/docs/reference/type-aliases/WithoutVirtualProps.md b/docs/reference/type-aliases/WithoutVirtualProps.md index 3d07741cbd..368c80371e 100644 --- a/docs/reference/type-aliases/WithoutVirtualProps.md +++ b/docs/reference/type-aliases/WithoutVirtualProps.md @@ -7,7 +7,7 @@ title: WithoutVirtualProps type WithoutVirtualProps = T extends unknown ? Omit : never; ``` -Defined in: [packages/db/src/virtual-props.ts:175](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L175) +Defined in: [packages/db/src/virtual-props.ts:149](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L149) Extracts the base type from a type that may have virtual properties. Useful when you need to work with the raw data without virtual properties. diff --git a/docs/reference/type-aliases/WritableDeep.md b/docs/reference/type-aliases/WritableDeep.md index aa9493afb6..7b2776040a 100644 --- a/docs/reference/type-aliases/WritableDeep.md +++ b/docs/reference/type-aliases/WritableDeep.md @@ -7,7 +7,7 @@ title: WritableDeep type WritableDeep = T extends BuiltIns ? T : T extends (...arguments_) => unknown ? object extends WritableObjectDeep ? T : HasMultipleCallSignatures extends true ? T : (...arguments_) => ReturnType & WritableObjectDeep : T extends ReadonlyMap ? WritableMapDeep : T extends ReadonlySet ? WritableSetDeep : T extends ReadonlyArray ? WritableArrayDeep : T extends object ? WritableObjectDeep : unknown; ``` -Defined in: [packages/db/src/types.ts:1269](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1269) +Defined in: [packages/db/src/types.ts:1236](https://github.com/TanStack/db/blob/main/packages/db/src/types.ts#L1236) ## Type Parameters diff --git a/examples/react-native/shopping-list/src/components/ListDetail.tsx b/examples/react-native/shopping-list/src/components/ListDetail.tsx index 2b3e300e3a..fd15f2a593 100644 --- a/examples/react-native/shopping-list/src/components/ListDetail.tsx +++ b/examples/react-native/shopping-list/src/components/ListDetail.tsx @@ -21,7 +21,7 @@ type ListItemRow = { text: string checked: boolean createdAt: string - $synced?: boolean + $hasPendingWrites?: boolean } function ItemRow({ @@ -36,7 +36,7 @@ function ItemRow({ const [showSavingBadge, setShowSavingBadge] = useState(false) useEffect(() => { - if (item.$synced !== false) { + if (!item.$hasPendingWrites) { setShowSavingBadge(false) return } @@ -48,7 +48,7 @@ function ItemRow({ return () => { clearTimeout(timer) } - }, [item.id, item.$synced]) + }, [item.id, item.$hasPendingWrites]) return ( diff --git a/examples/react-native/shopping-list/src/components/ListsScreen.tsx b/examples/react-native/shopping-list/src/components/ListsScreen.tsx index d46b277644..56283ac3a3 100644 --- a/examples/react-native/shopping-list/src/components/ListsScreen.tsx +++ b/examples/react-native/shopping-list/src/components/ListsScreen.tsx @@ -26,7 +26,7 @@ function ListCard({ totalItems: any checkedItems: any uncheckedPreview: any - $synced?: boolean + $hasPendingWrites?: boolean } onPress: () => void onDelete: () => void @@ -48,7 +48,7 @@ function ListCard({ const [showSavingBadge, setShowSavingBadge] = useState(false) useEffect(() => { - if (list.$synced !== false) { + if (!list.$hasPendingWrites) { setShowSavingBadge(false) return } @@ -60,7 +60,7 @@ function ListCard({ return () => { clearTimeout(timer) } - }, [list.id, list.$synced]) + }, [list.id, list.$hasPendingWrites]) return ( eq(item.listId, list.id)) @@ -143,7 +143,7 @@ export function ListsScreen() { id: string name: string createdAt: string - $synced?: boolean + $hasPendingWrites?: boolean totalItems: any uncheckedPreview: any checkedItems: any diff --git a/examples/react/paced-mutations-demo/src/App.tsx b/examples/react/paced-mutations-demo/src/App.tsx index 26781446c2..4d09704e2c 100644 --- a/examples/react/paced-mutations-demo/src/App.tsx +++ b/examples/react/paced-mutations-demo/src/App.tsx @@ -188,7 +188,7 @@ export function App() { }) // Listen for completion - tx.isPersisted.promise + tx.when(`settled`) .then(() => { setTransactions((prev) => prev.map((t) => { diff --git a/examples/react/projects/src/routes/_authenticated/index.tsx b/examples/react/projects/src/routes/_authenticated/index.tsx index 603a1c5f6b..cfca6623e4 100644 --- a/examples/react/projects/src/routes/_authenticated/index.tsx +++ b/examples/react/projects/src/routes/_authenticated/index.tsx @@ -1,6 +1,6 @@ -import { createFileRoute, redirect } from '@tanstack/react-router' -import { projectCollection, todoCollection } from '@/lib/collections' -import { authClient } from '@/lib/auth-client' +import { createFileRoute, redirect } from "@tanstack/react-router" +import { projectCollection, todoCollection } from "@/lib/collections" +import { authClient } from "@/lib/auth-client" export const Route = createFileRoute(`/_authenticated/`)({ component: () => null, @@ -31,7 +31,7 @@ export const Route = createFileRoute(`/_authenticated/`)({ created_at: new Date(), updated_at: new Date(), }) - await tx.isPersisted.promise + await tx.when(`settled`) const serverProjectId = projectCollection.toArray[0].id throw redirect({ to: `/project/$projectId`, diff --git a/package.json b/package.json index f71c3e5082..8c52401ca3 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "build:minified": "pnpm --filter \"./packages/**\" build:minified", "changeset": "changeset", "changeset:publish": "changeset publish", - "changeset:version": "changeset version && tsx scripts/update-example-deps.ts && pnpm install --no-frozen-lockfile", + "changeset:version": "node scripts/version-packages.mjs", "format": "prettier --experimental-cli --ignore-unknown '**/*' --write", "lint": "pnpm --filter \"./packages/**\" lint", "lint:check": "pnpm --filter \"./packages/**\" lint --fix=false", diff --git a/packages/browser-db-sqlite-persistence/e2e/browser-single-tab-persisted-collection.e2e.test.ts b/packages/browser-db-sqlite-persistence/e2e/browser-single-tab-persisted-collection.e2e.test.ts index f1afe48116..898efb3cfd 100644 --- a/packages/browser-db-sqlite-persistence/e2e/browser-single-tab-persisted-collection.e2e.test.ts +++ b/packages/browser-db-sqlite-persistence/e2e/browser-single-tab-persisted-collection.e2e.test.ts @@ -82,15 +82,13 @@ function createPersistedCollection( } type PersistedTransactionHandle = { - isPersisted: { - promise: Promise - } + when: (state: 'settled') => Promise } async function waitForPersisted( transaction: PersistedTransactionHandle, ): Promise { - await transaction.isPersisted.promise + await transaction.when(`settled`) } async function seedCollection( diff --git a/packages/browser-db-sqlite-persistence/e2e/electric-immediate-reload.opfs.ts b/packages/browser-db-sqlite-persistence/e2e/electric-immediate-reload.opfs.ts index 460ecefc14..8d9d8cc9b0 100644 --- a/packages/browser-db-sqlite-persistence/e2e/electric-immediate-reload.opfs.ts +++ b/packages/browser-db-sqlite-persistence/e2e/electric-immediate-reload.opfs.ts @@ -208,7 +208,7 @@ try { insert = async (row) => { try { const transaction = collection.insert(row) - await transaction.isPersisted.promise + await transaction.when(`settled`) insertAcknowledged = true } catch (error) { failure = failureMessage(error) diff --git a/packages/capacitor-db-sqlite-persistence/e2e/shared/capacitor-persisted-collection-harness.ts b/packages/capacitor-db-sqlite-persistence/e2e/shared/capacitor-persisted-collection-harness.ts index a036efdd3b..6a83269346 100644 --- a/packages/capacitor-db-sqlite-persistence/e2e/shared/capacitor-persisted-collection-harness.ts +++ b/packages/capacitor-db-sqlite-persistence/e2e/shared/capacitor-persisted-collection-harness.ts @@ -23,9 +23,7 @@ type PersistedCollectionHarness = { } type PersistedTransactionHandle = { - isPersisted: { - promise: Promise - } + when: (state: 'settled') => Promise } type PersistenceFactory = ( @@ -89,7 +87,7 @@ function createPersistedCollection( async function waitForPersisted( transaction: PersistedTransactionHandle, ): Promise { - await transaction.isPersisted.promise + await transaction.when(`settled`) } async function seedCollection( diff --git a/packages/capacitor-db-sqlite-persistence/tests/capacitor-persistence.test.ts b/packages/capacitor-db-sqlite-persistence/tests/capacitor-persistence.test.ts index ebe2e711ee..4f8fc197b4 100644 --- a/packages/capacitor-db-sqlite-persistence/tests/capacitor-persistence.test.ts +++ b/packages/capacitor-db-sqlite-persistence/tests/capacitor-persistence.test.ts @@ -250,7 +250,7 @@ it(`resumes persisted sync after simulated app lifecycle transitions`, async () title: `Before background`, score: 1, }) - await initialInsert.isPersisted.promise + await initialInsert.when(`settled`) await collection.cleanup() collection.startSyncImmediate() @@ -261,7 +261,7 @@ it(`resumes persisted sync after simulated app lifecycle transitions`, async () title: `Post resume write`, score: 2, }) - await postResumeInsert.isPersisted.promise + await postResumeInsert.when(`settled`) const persistedRows = await persistence.adapter.loadSubset( { kind: `eager`, collectionId: collectionId }, diff --git a/packages/cloudflare-durable-objects-db-sqlite-persistence/e2e/fixtures/worker.mjs b/packages/cloudflare-durable-objects-db-sqlite-persistence/e2e/fixtures/worker.mjs index b401358b43..71badb0be9 100644 --- a/packages/cloudflare-durable-objects-db-sqlite-persistence/e2e/fixtures/worker.mjs +++ b/packages/cloudflare-durable-objects-db-sqlite-persistence/e2e/fixtures/worker.mjs @@ -208,7 +208,7 @@ export class PersistenceObject extends DurableObject { } await this.collectionReady const tx = this.collection.insert(requestBody.todo) - await tx.isPersisted.promise + await tx.when(`settled`) return jsonResponse(200, { ok: true, diff --git a/packages/db-sqlite-persistence-core/src/persisted.ts b/packages/db-sqlite-persistence-core/src/persisted.ts index 30d9aa8861..b60b7b45cf 100644 --- a/packages/db-sqlite-persistence-core/src/persisted.ts +++ b/packages/db-sqlite-persistence-core/src/persisted.ts @@ -6940,54 +6940,42 @@ export function persistedCollectionOptions< params: InsertMutationFnParams, ) => { const lifecycleGeneration = runtime.getLifecycleGeneration() - const handlerResult = localOnlyOptions.onInsert - ? await localOnlyOptions.onInsert( - params as unknown as InsertMutationFnParams, - ) - : undefined + await localOnlyOptions.onInsert?.( + params as unknown as InsertMutationFnParams, + ) await runtime.persistAndConfirmCollectionMutations( params.transaction.mutations as Array>, lifecycleGeneration, ) - - return handlerResult ?? {} } const wrappedOnUpdate = async ( params: UpdateMutationFnParams, ) => { const lifecycleGeneration = runtime.getLifecycleGeneration() - const handlerResult = localOnlyOptions.onUpdate - ? await localOnlyOptions.onUpdate( - params as unknown as UpdateMutationFnParams, - ) - : undefined + await localOnlyOptions.onUpdate?.( + params as unknown as UpdateMutationFnParams, + ) await runtime.persistAndConfirmCollectionMutations( params.transaction.mutations as Array>, lifecycleGeneration, ) - - return handlerResult ?? {} } const wrappedOnDelete = async ( params: DeleteMutationFnParams, ) => { const lifecycleGeneration = runtime.getLifecycleGeneration() - const handlerResult = localOnlyOptions.onDelete - ? await localOnlyOptions.onDelete( - params as unknown as DeleteMutationFnParams, - ) - : undefined + await localOnlyOptions.onDelete?.( + params as unknown as DeleteMutationFnParams, + ) await runtime.persistAndConfirmCollectionMutations( params.transaction.mutations as Array>, lifecycleGeneration, ) - - return handlerResult ?? {} } const acceptMutations = async (transaction: { diff --git a/packages/db-sqlite-persistence-core/tests/contracts/runtime-persistence-contract.ts b/packages/db-sqlite-persistence-core/tests/contracts/runtime-persistence-contract.ts index ae8af28237..3eedd2fb51 100644 --- a/packages/db-sqlite-persistence-core/tests/contracts/runtime-persistence-contract.ts +++ b/packages/db-sqlite-persistence-core/tests/contracts/runtime-persistence-contract.ts @@ -120,7 +120,7 @@ export function runRuntimePersistenceContractSuite( title: `Persisted from collection`, score: 42, }) - await insertTx.isPersisted.promise + await insertTx.when(`settled`) const readerAdapter = factory.createAdapter(readerDriver) const loadedRows = await readerAdapter.loadSubset( diff --git a/packages/db-sqlite-persistence-core/tests/persisted-oracle.test.ts b/packages/db-sqlite-persistence-core/tests/persisted-oracle.test.ts index bd5fd592e6..def23c5a66 100644 --- a/packages/db-sqlite-persistence-core/tests/persisted-oracle.test.ts +++ b/packages/db-sqlite-persistence-core/tests/persisted-oracle.test.ts @@ -665,7 +665,6 @@ const stripVirtualProps = | undefined>( if (!value || typeof value !== `object`) return value const { $hasPendingWrites: _hasPendingWrites, - $synced: _synced, $origin: _origin, $key: _key, $collectionId: _collectionId, @@ -2937,7 +2936,7 @@ async function runRejectedHydrationBufferWitness( const freshTransaction = reopened.insert(freshRow) await atPersistedOracleCheckpoint( (async () => { - await freshTransaction.isPersisted.promise + await freshTransaction.when(`settled`) })(), `${id} fresh transaction persisted`, ) @@ -4466,7 +4465,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { unexpectedThird = createCollection(options) }).toThrow(InvalidPersistedCollectionConfigError) const receipt = collection.insert({ id: `owned`, title: `Retried owner` }) - await receipt.isPersisted.promise + await receipt.when(`settled`) expect(stripVirtualProps(collection.get(`owned`))).toEqual({ id: `owned`, title: `Retried owner`, @@ -4568,7 +4567,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { source.write({ type: `insert`, value: row }) await source.commit() } else { - await collection.insert(row).isPersisted.promise + await collection.insert(row).when(`settled`) } expect(stripVirtualProps(collection.get(`owned`))).toEqual({ id: `owned`, @@ -4624,7 +4623,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { }).toThrow(InvalidPersistedCollectionConfigError) const receipt = first.insert({ id: `owned`, title: `First owner` }) - await receipt.isPersisted.promise + await receipt.when(`settled`) expect(stripVirtualProps(first.get(`owned`))).toEqual({ id: `owned`, title: `First owner`, @@ -4751,8 +4750,9 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { if (mode === `sync-present`) { await Promise.resolve(source.commit()) } else { - await first.insert({ id: `owned`, title: `First owner` }).isPersisted - .promise + await first + .insert({ id: `owned`, title: `First owner` }) + .when(`settled`) } expect(first.has(`owned`)).toBe(true) expect(second.has(`owned`)).toBe(false) @@ -4905,8 +4905,9 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { }) await Promise.resolve(source.commit()) } else { - await first.insert({ id: `after-restart`, title: `First owner` }) - .isPersisted.promise + await first + .insert({ id: `after-restart`, title: `First owner` }) + .when(`settled`) } expect(first.has(`after-restart`)).toBe(true) expect(second.has(`after-restart`)).toBe(false) @@ -5004,7 +5005,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { title: `Phase 0`, }) - await insertTx.isPersisted.promise + await insertTx.when(`settled`) expect(stripVirtualProps(collection.get(`1`))).toEqual({ id: `1`, @@ -5060,7 +5061,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { title: `Managed during startup`, }) releaseInitialSnapshot() - await insert.isPersisted.promise + await insert.when(`settled`) await collection.stateWhenReady() await vi.waitFor(() => expect(snapshotCalls).toBeGreaterThanOrEqual(2)) @@ -6754,7 +6755,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { expect(receipt).toBeInstanceOf(Promise) releaseMutation() - await transaction.isPersisted.promise + await transaction.when(`settled`) await receipt subscription.unsubscribe() @@ -6767,7 +6768,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { expect(adapter.applyCommittedTxCalls).toHaveLength(1) } finally { releaseMutation() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await collection.cleanup() } }) @@ -6840,7 +6841,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { aborted.abort() expect(collection.get(`dependent`)).toBeUndefined() localPersistence.resolve() - await localTransaction.isPersisted.promise + await localTransaction.when(`settled`) const abortedOutcome = await atPersistedOracleCheckpoint( abortedReceipt.then( () => ({ status: `fulfilled` as const }), @@ -6877,7 +6878,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { localPersistence.resolve() await cleanupPersistedOracle( [ - () => localTransaction.isPersisted.promise.catch(() => undefined), + () => localTransaction.when(`settled`).catch(() => undefined), () => abortedReceipt?.catch(() => undefined), () => dependentReceipt?.catch(() => undefined), () => collection.cleanup(), @@ -8973,7 +8974,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { releaseReplacementHydration.resolve() await cleanupPersistedOracle( [ - () => localTransaction?.isPersisted.promise.catch(() => undefined), + () => localTransaction?.when(`settled`).catch(() => undefined), () => replacementReady, () => (cleanedUp ? undefined : collection.cleanup()), ], @@ -9011,7 +9012,6 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { onInsert: async () => { hookEntered.resolve() await releaseHook.promise - return {} }, persistence: { adapter, coordinator }, }), @@ -9039,7 +9039,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { id: `old-hook`, title: `must not enter replacement coordinator`, }) - transactionOutcome = transaction.isPersisted.promise.then( + transactionOutcome = transaction.when(`settled`).then( () => ({ status: `fulfilled` as const }), (reason: unknown) => ({ status: `rejected` as const, reason }), ) @@ -9996,7 +9996,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { id: `local`, title: `durable before confirmation settles`, }) - const transactionOutcome = transaction.isPersisted.promise.then( + const transactionOutcome = transaction.when(`settled`).then( () => ({ status: `fulfilled` as const }), (reason: unknown) => ({ status: `rejected` as const, reason }), ) @@ -10822,7 +10822,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { title: `must roll back`, }) const firstOutcome = await atPersistedOracleCheckpoint( - firstTransaction.isPersisted.promise.then( + firstTransaction.when(`settled`).then( () => ({ status: `fulfilled` as const }), (reason: unknown) => ({ status: `rejected` as const, reason }), ), @@ -10860,7 +10860,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { id: `late-local`, title: `must not be admitted`, }) - await lateTransaction.isPersisted.promise + await lateTransaction.when(`settled`) }) .then( () => ({ status: `fulfilled` as const }), @@ -12835,7 +12835,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { } finally { hydration.resolve() localPersistence.reject(appliedError) - await localTransaction.isPersisted.promise.catch(() => undefined) + await localTransaction.when(`settled`).catch(() => undefined) await cleanupPersistedOracle( [() => collection.cleanup()], hasPrimaryFailure, @@ -12952,7 +12952,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { } outbound.reject(refused) - await expect(transaction.isPersisted.promise).rejects.toBe(refused) + await expect(transaction.when(`settled`)).rejects.toBe(refused) expect(transaction.state).toBe(`failed`) checkSource(`after refused mutation settled`) if (firstReceipt !== undefined) { @@ -12985,7 +12985,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { if (transaction) outbound.reject(refused) await cleanupPersistedOracle( [ - () => transaction?.isPersisted.promise.catch(() => undefined), + () => transaction?.when(`settled`).catch(() => undefined), () => firstReceipt === undefined ? undefined @@ -13062,7 +13062,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { releaseHandler.resolve() await atPersistedOracleCheckpoint( - transaction.isPersisted.promise.then(() => undefined), + transaction.when(`settled`).then(() => undefined), `successful persisted origin mutation settled`, ) await Promise.all(receipts.map((receipt) => Promise.resolve(receipt))) @@ -13080,7 +13080,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { releaseHandler.resolve() await cleanupPersistedOracle( [ - () => transaction?.isPersisted.promise.catch(() => undefined), + () => transaction?.when(`settled`).catch(() => undefined), ...receipts.map( (receipt) => () => Promise.resolve(receipt).catch(() => undefined), @@ -13171,7 +13171,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { localPersistence.resolve() await atPersistedOracleCheckpoint( - localTransaction.isPersisted.promise, + localTransaction.when(`settled`), `local settlement publishes the source commit`, ) expect(stripVirtualProps(collection.get(`remote`))).toEqual({ @@ -13270,7 +13270,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { `handler dependency submitted`, ) await atPersistedOracleCheckpoint( - localTransaction.isPersisted.promise, + localTransaction.when(`settled`), `user persistence completes after its awaited source commit`, ) await atPersistedOracleCheckpoint( @@ -13314,7 +13314,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { normalAbort.abort() await cleanupPersistedOracle( [ - () => localTransaction.isPersisted.promise.catch(() => undefined), + () => localTransaction.when(`settled`).catch(() => undefined), () => normalReceipt?.catch(() => undefined), () => dependencyReceipt === true @@ -13710,7 +13710,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { local.mutate(() => { collection.insert({ id: `local`, title: `optimistic local` }) }) - localReceipt = local.isPersisted.promise + localReceipt = local.when(`settled`) void localReceipt.catch(() => undefined) await mutationEntered.promise @@ -13816,7 +13816,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { local.mutate(() => { collection.insert({ id: `local`, title: `optimistic local` }) }) - localReceipt = local.isPersisted.promise + localReceipt = local.when(`settled`) void localReceipt.catch(() => undefined) await mutationEntered.promise @@ -22642,7 +22642,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { title: `Ack mismatch`, }) - await expect(tx.isPersisted.promise).rejects.toThrow( + await expect(tx.when(`settled`)).rejects.toThrow( /partial acceptance is not supported/, ) expect(collection.get(`ack-mismatch`)).toBeUndefined() @@ -22720,7 +22720,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { id: `local-failure`, title: `Must not persist`, }) - const rejection = await tx.isPersisted.promise.then( + const rejection = await tx.when(`settled`).then( () => undefined, (error: unknown) => error, ) @@ -22847,7 +22847,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { id: `primary-local`, title: `must install terminal state`, }) - const primaryOutcome = await primary.isPersisted.promise.then( + const primaryOutcome = await primary.when(`settled`).then( () => ({ status: `fulfilled` as const }), (reason: unknown) => ({ status: `rejected` as const, reason }), ) @@ -22912,7 +22912,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { id: `old-local`, title: `durable only in the replaced lifecycle`, }) - const settlement = transaction.isPersisted.promise.then( + const settlement = transaction.when(`settled`).then( () => ({ status: `fulfilled` as const }), (reason: unknown) => ({ status: `rejected` as const, reason }), ) @@ -25451,7 +25451,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { expect(publishedFreshTitles).toEqual([]) releaseMutation.resolve() await atPersistedOracleCheckpoint( - mutation.isPersisted.promise, + mutation.when(`settled`), `optimistic publication released`, ) await atPersistedOracleCheckpoint( @@ -25501,7 +25501,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { await cleanupPersistedOracle( [ () => recovery, - () => mutation?.isPersisted.promise.catch(() => undefined), + () => mutation?.when(`settled`).catch(() => undefined), () => demand?.catch(() => undefined), () => collection.cleanup(), ], @@ -28082,7 +28082,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { expect(collection.get(`local`)).toMatchObject({ id: `local` }) expect(collection.get(`old`)).toBeUndefined() release.resolve() - await mutation.isPersisted.promise + await mutation.when(`settled`) source.begin() source.write({ type: `insert`, @@ -28099,7 +28099,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { ).toEqual([`optimistic-expiry-new`]) } finally { release.resolve() - await mutation?.isPersisted.promise.catch(() => undefined) + await mutation?.when(`settled`).catch(() => undefined) await collection.cleanup() } }) @@ -28535,10 +28535,10 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { expect(recovery.read()).toEqual({ status: `fulfilled` }) expect(collection.get(`local`)).toBeDefined() release.resolve() - await mutation.isPersisted.promise + await mutation.when(`settled`) } finally { release.resolve() - await mutation?.isPersisted.promise.catch(() => undefined) + await mutation?.when(`settled`).catch(() => undefined) await collection.cleanup() } }) @@ -29824,13 +29824,13 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { // so the load settles once that mutation does. It must not also wait // for the unrelated receipt. releaseMutation.resolve() - await mutation.isPersisted.promise + await mutation.when(`settled`) await expect(load).resolves.toBeUndefined() await unrelated.receipt.catch(() => undefined) } finally { unrelated?.abortController.abort() releaseMutation.resolve() - await mutation?.isPersisted.promise.catch(() => undefined) + await mutation?.when(`settled`).catch(() => undefined) await load?.catch(() => undefined) await collection.cleanup() } @@ -30558,7 +30558,7 @@ describeUnlessOracleReplay(`persistedCollectionOptions`, () => { ) const mutation = collection.insert({ id: `local`, title: `Pending` }) - mutationPersisted = mutation.isPersisted.promise + mutationPersisted = mutation.when(`settled`) void mutationPersisted.catch(() => undefined) const bufferedCommit = await atPersistedOracleCheckpoint( bufferedCommitReturned.promise, diff --git a/packages/db-sqlite-persistence-core/tests/persisted-real-adapter-lifecycle.test.ts b/packages/db-sqlite-persistence-core/tests/persisted-real-adapter-lifecycle.test.ts index 13c4e5f202..c8f3fb4636 100644 --- a/packages/db-sqlite-persistence-core/tests/persisted-real-adapter-lifecycle.test.ts +++ b/packages/db-sqlite-persistence-core/tests/persisted-real-adapter-lifecycle.test.ts @@ -383,7 +383,7 @@ it.each([ ).toEqual([]) rejectOutbound(refused) - await expect(transaction.isPersisted.promise).rejects.toBe(refused) + await expect(transaction.when(`settled`)).rejects.toBe(refused) await Promise.resolve(heldDelete) expect(transaction.state).toBe(`failed`) expect(collection.has(`local`)).toBe(false) @@ -488,7 +488,7 @@ it.each([ if (transaction) rejectOutbound(refused) await cleanupTestActions( [ - () => transaction?.isPersisted.promise.catch(() => undefined), + () => transaction?.when(`settled`).catch(() => undefined), () => heldDelete === undefined ? undefined diff --git a/packages/db-sqlite-persistence-core/tests/sqlite-resume-snapshot-oracle.test.ts b/packages/db-sqlite-persistence-core/tests/sqlite-resume-snapshot-oracle.test.ts index 7e4891f442..c45e6352a8 100644 --- a/packages/db-sqlite-persistence-core/tests/sqlite-resume-snapshot-oracle.test.ts +++ b/packages/db-sqlite-persistence-core/tests/sqlite-resume-snapshot-oracle.test.ts @@ -3837,7 +3837,7 @@ describe(`SQLite resume snapshots`, () => { let managedPersistence: Promise | undefined let managedPersistenceSettled = false if (history.transition === `managed-insert`) { - managedPersistence = collection.insert(managedRow).isPersisted.promise + managedPersistence = collection.insert(managedRow).when(`settled`) void managedPersistence.then( () => { managedPersistenceSettled = true diff --git a/packages/db/e2e/indexed-db/browser.ts b/packages/db/e2e/indexed-db/browser.ts index e5e1d07c7d..1332011192 100644 --- a/packages/db/e2e/indexed-db/browser.ts +++ b/packages/db/e2e/indexed-db/browser.ts @@ -296,7 +296,7 @@ async function setup() { else draft.optional = row.optional }) : collection.insert(structuredClone(row)) - await transaction.isPersisted.promise + await transaction.when(`settled`) }, // Low-level writers own invalidation: wrapper writes do not broadcast for // them. This native provider fixture writes without version metadata and @@ -326,7 +326,7 @@ async function setup() { } }), remove: async (key: string | number) => { - await collection.delete(key).isPersisted.promise + await collection.delete(key).when(`settled`) }, omit: (row: OracleRow) => { // Manual acceptance admits both native transactions before the first @@ -356,13 +356,14 @@ async function setup() { : kind === 'import' ? collection.utils.importData([{ id: 2, name: 'replacement' }]) : kind === 'insert' - ? collection.insert({ id: 2, name: 'inserted' }).isPersisted - .promise + ? collection.insert({ id: 2, name: 'inserted' }).when(`settled`) : kind === 'delete' - ? collection.delete(1).isPersisted.promise - : collection.update(1, (row) => { - row.name = 'updated' - }).isPersisted.promise + ? collection.delete(1).when(`settled`) + : collection + .update(1, (row) => { + row.name = 'updated' + }) + .when(`settled`) retirementDone = operation.then( () => { retirement.outcome = 'fulfilled' @@ -496,7 +497,7 @@ async function setup() { id: 'held', name: 'unaccepted local intent', }) - void transaction.isPersisted.promise.then( + void transaction.when(`settled`).then( () => { handlerStatus = 'fulfilled' }, diff --git a/packages/db/e2e/indexed-db/coverage-browser.ts b/packages/db/e2e/indexed-db/coverage-browser.ts index 5387fb5580..8e12501186 100644 --- a/packages/db/e2e/indexed-db/coverage-browser.ts +++ b/packages/db/e2e/indexed-db/coverage-browser.ts @@ -81,10 +81,12 @@ async function valuePeer(name: string) { const pending = entry === 'import' ? c.utils.importData([row]) - : c.update(1, (draft) => { - draft.name = row.name - draft.nested = row.nested - }).isPersisted.promise + : c + .update(1, (draft) => { + draft.name = row.name + draft.nested = row.nested + }) + .when(`settled`) // Attach rejection immediately so failure cannot become an unhandled event // while the oracle records the deliberately held native boundary. const done = pending.then( @@ -139,7 +141,7 @@ async function valuePeer(name: string) { }) const immediate = c.get(1)! bytes.fill(99) - await tx.isPersisted.promise + await tx.when(`settled`) function observe(row: ValueRow) { const copy = row.nested.value as typeof value return { @@ -167,16 +169,20 @@ async function valuePeer(name: string) { exported: await observeValueRows(await c.utils.exportData()), }), insert: async (kind: ValueKind) => { - await c.insert({ - id: 1, - name: 'initial', - nested: { value: createValue(kind) }, - }).isPersisted.promise + await c + .insert({ + id: 1, + name: 'initial', + nested: { value: createValue(kind) }, + }) + .when(`settled`) }, update: async () => { - await c.update(1, (draft) => { - draft.name = 'updated' - }).isPersisted.promise + await c + .update(1, (draft) => { + draft.name = 'updated' + }) + .when(`settled`) }, replace: async () => { const rows = await c.utils.exportData() @@ -196,7 +202,7 @@ async function valuePeer(name: string) { }, })) try { - if (entry === 'insert') await c.insert(rows).isPersisted.promise + if (entry === 'insert') await c.insert(rows).when(`settled`) else await c.utils.importData(rows) return 'fulfilled' } catch { @@ -264,7 +270,7 @@ async function initialize(name: string, shared: boolean, store: string) { nested: { value: id }, })) await Promise.all( - collections.map((c, index) => c.insert(rows[index]!).isPersisted.promise), + collections.map((c, index) => c.insert(rows[index]!).when(`settled`)), ) return { evidence: { admission, schema, initialVersions, held, restored }, diff --git a/packages/db/e2e/indexed-db/host-driver.ts b/packages/db/e2e/indexed-db/host-driver.ts index 45424e9dd5..4b84b17c09 100644 --- a/packages/db/e2e/indexed-db/host-driver.ts +++ b/packages/db/e2e/indexed-db/host-driver.ts @@ -107,14 +107,15 @@ export async function createHost({ database, stores }: HostSetup) { })), ) const c = collections.get(command.store)! - if (command.type === 'insert') - await c.insert(command.rows).isPersisted.promise + if (command.type === 'insert') await c.insert(command.rows).when(`settled`) else if (command.type === 'import') await c.utils.importData(command.rows) else if (command.type === 'clear') await c.utils.clearObjectStore() else - await c.update(command.key, (draft) => { - draft.name = command.name - }).isPersisted.promise + await c + .update(command.key, (draft) => { + draft.name = command.name + }) + .when(`settled`) return undefined } } diff --git a/packages/db/package-tests/indexed-db/packed-consumer.ts b/packages/db/package-tests/indexed-db/packed-consumer.ts index e811885d10..c5da2955fd 100644 --- a/packages/db/package-tests/indexed-db/packed-consumer.ts +++ b/packages/db/package-tests/indexed-db/packed-consumer.ts @@ -42,8 +42,8 @@ export async function run(factory) { const collection = make(db) try { await collection.preload() - await collection.insert({ id: 1, name: 'inserted' }).isPersisted.promise - await collection.update(1, draft => { draft.name = 'updated' }).isPersisted.promise + await collection.insert({ id: 1, name: 'inserted' }).when('settled') + await collection.update(1, draft => { draft.name = 'updated' }).when('settled') } finally { await collection.cleanup(); db.close() } const reopened = await open() const restored = make(reopened) diff --git a/packages/db/skills/db-core/live-queries/SKILL.md b/packages/db/skills/db-core/live-queries/SKILL.md index 78b2a9395c..01451ca7aa 100644 --- a/packages/db/skills/db-core/live-queries/SKILL.md +++ b/packages/db/skills/db-core/live-queries/SKILL.md @@ -226,8 +226,6 @@ Live query results include computed, read-only virtual properties on every row: - `$collectionId`: the source collection ID. These props are added automatically and can be used in `where`, `select`, and `orderBy` clauses. Do not persist them back to storage. -The deprecated `$synced` alias is the inverse of `$hasPendingWrites` and will -be removed in the 1.0 RC. ## Includes (Subqueries in Select) diff --git a/packages/db/skills/db-core/mutations-optimistic/SKILL.md b/packages/db/skills/db-core/mutations-optimistic/SKILL.md index 048259135b..0c56c693c9 100644 --- a/packages/db/skills/db-core/mutations-optimistic/SKILL.md +++ b/packages/db/skills/db-core/mutations-optimistic/SKILL.md @@ -6,7 +6,7 @@ description: > createPacedMutations with debounceStrategy, throttleStrategy, queueStrategy. createTransaction, getActiveTransaction, ambient transaction context. Transaction lifecycle (pending/persisting/completed/failed). Mutation merging. - onInsert/onUpdate/onDelete handlers. PendingMutation type. Transaction.isPersisted. + onInsert/onUpdate/onDelete handlers. PendingMutation type. Transaction.when('settled'). type: sub-skill library: db library_version: '0.6.17' diff --git a/packages/db/skills/db-core/mutations-optimistic/references/transaction-api.md b/packages/db/skills/db-core/mutations-optimistic/references/transaction-api.md index 66eb96bfd4..be40e8f1d6 100644 --- a/packages/db/skills/db-core/mutations-optimistic/references/transaction-api.md +++ b/packages/db/skills/db-core/mutations-optimistic/references/transaction-api.md @@ -26,13 +26,6 @@ interface Transaction { metadata: Record error?: { message: string; error: Error } - // Deprecated alias for the settlement promise; retained until the 1.0 RC - isPersisted: { - promise: Promise> - resolve: (value: Transaction) => void - reject: (reason?: any) => void - } - // Resolves when the transaction settles; rejects on failure or rollback when(state: 'settled'): Promise> @@ -228,6 +221,3 @@ For a non-empty commit, `mutationFn` is the normal success boundary. `when('settled')` does not by itself prove that a backend uploaded, confirmed, or read back the write. It proves those stronger guarantees only when `mutationFn` waits for them before returning. - -The old `isPersisted.promise` remains available until the 1.0 RC but is -deprecated. Replace it with `when('settled')`. diff --git a/packages/db/src/collection/changes.ts b/packages/db/src/collection/changes.ts index bed65f1ae5..e9c4cfc36c 100644 --- a/packages/db/src/collection/changes.ts +++ b/packages/db/src/collection/changes.ts @@ -114,7 +114,7 @@ export class CollectionChangesManager< } /** - * Enriches a change message with virtual properties ($hasPendingWrites, $synced, $origin, $key, $collectionId). + * Enriches a change message with virtual properties ($hasPendingWrites, $origin, $key, $collectionId). * Uses the "add-if-missing" pattern to preserve virtual properties from upstream collections. */ private enrichChangeWithVirtualProps( diff --git a/packages/db/src/collection/mutations.ts b/packages/db/src/collection/mutations.ts index 4f87f8810e..f0f7106e99 100644 --- a/packages/db/src/collection/mutations.ts +++ b/packages/db/src/collection/mutations.ts @@ -225,8 +225,7 @@ export class CollectionMutationsManager< try { direct.write(mutations) } finally { - transaction.setState(`completed`) - transaction.isPersisted.resolve(transaction) + transaction.completeWithoutCommit() } return transaction } @@ -290,7 +289,7 @@ export class CollectionMutationsManager< } throw error } - // Errors still reject `isPersisted.promise`. This catch only prevents an + // Errors still reject `when('settled')`. This catch only prevents an // unhandled rejection from the fire-and-forget commit. transaction.commit().catch(() => undefined) return transaction @@ -532,7 +531,7 @@ export class CollectionMutationsManager< const emptyTransaction = this.createTransaction({ mutationFn: async () => {}, }) - // Errors still propagate through tx.isPersisted.promise; suppress the background commit from warning + // Errors still propagate through tx.when(`settled`); suppress the background commit from warning emptyTransaction.commit().catch(() => undefined) return emptyTransaction } diff --git a/packages/db/src/collection/state.ts b/packages/db/src/collection/state.ts index 9f47f0261a..e0e0998b03 100644 --- a/packages/db/src/collection/state.ts +++ b/packages/db/src/collection/state.ts @@ -171,7 +171,7 @@ export class CollectionStateManager< TKey, { row: TOutput - synced: boolean + hasPendingWrites: boolean origin: VirtualOrigin collectionId: string enriched: WithVirtualProps @@ -232,19 +232,15 @@ export class CollectionStateManager< } /** - * Checks whether this row currently has no pending local optimistic writes. + * Checks whether this row currently has 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. + * This is local mutation status, not backend confirmation. */ - public isRowSynced(key: TKey): boolean { + private hasPendingWrites(key: TKey): boolean { if (this.isLocalOnly) { - return true + return false } - return !this.optimisticUpserts.has(key) && !this.optimisticDeletes.has(key) + return this.optimisticUpserts.has(key) || this.optimisticDeletes.has(key) } /** @@ -268,10 +264,10 @@ export class CollectionStateManager< key: TKey, overrides?: Partial>, ): VirtualRowProps { - const synced = overrides?.$synced ?? this.isRowSynced(key) + const hasPendingWrites = + overrides?.$hasPendingWrites ?? this.hasPendingWrites(key) return { - $hasPendingWrites: !synced, - $synced: synced, + $hasPendingWrites: hasPendingWrites, $origin: overrides?.$origin ?? this.getRowOrigin(key), $key: overrides?.$key ?? key, $collectionId: overrides?.$collectionId ?? this.collection.id, @@ -288,7 +284,7 @@ export class CollectionStateManager< ): VirtualRowProps { if (this.isLocalOnly) { return this.createVirtualPropsSnapshot(key, { - $synced: true, + $hasPendingWrites: false, $origin: 'local', }) } @@ -301,7 +297,7 @@ export class CollectionStateManager< optimisticUpserts.has(key) || optimisticDeletes.has(key) return this.createVirtualPropsSnapshot(key, { - $synced: !hasOptimisticChange, + $hasPendingWrites: hasOptimisticChange, $origin: hasOptimisticChange ? 'local' : ((options?.rowOrigins ?? this.rowOrigins).get(key) ?? 'remote'), @@ -328,7 +324,8 @@ export class CollectionStateManager< virtualProps: VirtualRowProps, ): WithVirtualProps { const existingRow = row as Partial> - const synced = existingRow.$synced ?? virtualProps.$synced + const hasPendingWrites = + existingRow.$hasPendingWrites ?? virtualProps.$hasPendingWrites const origin = existingRow.$origin ?? virtualProps.$origin const resolvedKey = existingRow.$key ?? virtualProps.$key const collectionId = existingRow.$collectionId ?? virtualProps.$collectionId @@ -337,7 +334,7 @@ export class CollectionStateManager< if ( cached && cached.row === row && - cached.synced === synced && + cached.hasPendingWrites === hasPendingWrites && cached.origin === origin && cached.collectionId === collectionId ) { @@ -346,8 +343,7 @@ export class CollectionStateManager< const enriched = { ...row, - $hasPendingWrites: !synced, - $synced: synced, + $hasPendingWrites: hasPendingWrites, $origin: origin, $key: resolvedKey, $collectionId: collectionId, @@ -355,7 +351,7 @@ export class CollectionStateManager< this.virtualPropsCache.set(resolvedKey, { row, - synced, + hasPendingWrites, origin, collectionId, enriched, @@ -780,7 +776,8 @@ export class CollectionStateManager< currentValue !== undefined && (!deepEquals(previousValue, currentValue) || previousVirtualProps.$origin !== nextVirtualProps.$origin || - previousVirtualProps.$synced !== nextVirtualProps.$synced) + previousVirtualProps.$hasPendingWrites !== + nextVirtualProps.$hasPendingWrites) ) { events.push({ type: `update`, @@ -1366,14 +1363,15 @@ export class CollectionStateManager< }) const nextVirtualProps = this.getVirtualPropsSnapshotForState(key) const virtualChanged = - previousVirtualProps.$synced !== nextVirtualProps.$synced || + previousVirtualProps.$hasPendingWrites !== + nextVirtualProps.$hasPendingWrites || previousVirtualProps.$origin !== nextVirtualProps.$origin const withPreviousVirtualProps = (value: TOutput) => enrichRowWithVirtualProps( value, key, this.collection.id, - () => previousVirtualProps.$synced, + () => previousVirtualProps.$hasPendingWrites, () => previousVirtualProps.$origin, ) diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index f45910b122..75f426b4dd 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -789,25 +789,6 @@ export class CollectionInputNotFoundError extends QueryCompilationError { } } -/** - * Retained for callers that imported this formerly thrown error. - * - * @deprecated Nested queries may now shadow ancestor aliases. Duplicate aliases - * are rejected only within one lexical scope or union branch namespace. - */ -export class DuplicateAliasInSubqueryError extends QueryCompilationError { - constructor(alias: string, parentAliases: Array) { - super( - devBuild() && process.env.NODE_ENV !== `production` - ? `Subquery uses alias "${alias}" which is already used in the parent query. ` + - `Each alias must be unique across parent and subquery contexts. ` + - `Parent query aliases: ${parentAliases.join(`, `)}. ` + - `Please rename "${alias}" in either the parent query or subquery to avoid conflicts.` - : codedMessage(57, { alias, parentAliases }), - ) - } -} - export class UnsupportedFromTypeError extends QueryCompilationError { constructor(type: string) { super( diff --git a/packages/db/src/indexed-db.ts b/packages/db/src/indexed-db.ts index bad7e37ddf..a05a9208dd 100644 --- a/packages/db/src/indexed-db.ts +++ b/packages/db/src/indexed-db.ts @@ -800,21 +800,20 @@ export function indexedDBCollectionOptions( // is held. Each rejected decision releases its place without persisting. function persistInOrder( transaction: { mutations: Array> }, - handler: () => unknown, - ): Promise { + handler: () => void | Promise, + ): Promise { const result = acceptance.then(async () => { const outcome = await decision if (!outcome.accepted) throw outcome.error await acceptMutations(transaction) - return outcome.value }) acceptance = result.then( () => undefined, () => undefined, ) // Reserve the position before application code can author another write. - const decision = new Promise((resolve) => resolve(handler())).then( - (value) => ({ accepted: true as const, value }), + const decision = new Promise((resolve) => resolve(handler())).then( + () => ({ accepted: true as const }), (error: unknown) => ({ accepted: false as const, error }), ) return result diff --git a/packages/db/src/local-only.ts b/packages/db/src/local-only.ts index 5aece5cb77..2fc6d1835a 100644 --- a/packages/db/src/local-only.ts +++ b/packages/db/src/local-only.ts @@ -200,16 +200,10 @@ export function localOnlyCollectionOptions< const wrappedOnInsert = async ( params: InsertMutationFnParams, ) => { - // Call user handler first if provided - let handlerResult - if (onInsert) { - handlerResult = (await onInsert(params)) ?? {} - } + if (onInsert) await onInsert(params) // Then synchronously confirm the transaction by looping through mutations syncResult.confirmOperationsSync(params.transaction.mutations) - - return handlerResult } /** @@ -218,16 +212,10 @@ export function localOnlyCollectionOptions< const wrappedOnUpdate = async ( params: UpdateMutationFnParams, ) => { - // Call user handler first if provided - let handlerResult - if (onUpdate) { - handlerResult = (await onUpdate(params)) ?? {} - } + if (onUpdate) await onUpdate(params) // Then synchronously confirm the transaction by looping through mutations syncResult.confirmOperationsSync(params.transaction.mutations) - - return handlerResult } /** @@ -236,16 +224,10 @@ export function localOnlyCollectionOptions< const wrappedOnDelete = async ( params: DeleteMutationFnParams, ) => { - // Call user handler first if provided - let handlerResult - if (onDelete) { - handlerResult = (await onDelete(params)) ?? {} - } + if (onDelete) await onDelete(params) // Then synchronously confirm the transaction by looping through mutations syncResult.confirmOperationsSync(params.transaction.mutations) - - return handlerResult } /** diff --git a/packages/db/src/local-storage.ts b/packages/db/src/local-storage.ts index ea853ba3b7..3595bc8555 100644 --- a/packages/db/src/local-storage.ts +++ b/packages/db/src/local-storage.ts @@ -624,8 +624,8 @@ export function localStorageCollectionOptions( const persistAutomatic = ( mutations: Array>>, - handler?: () => unknown | Promise, - ): Promise => { + handler?: () => void | Promise, + ): Promise => { const { previous, retire } = reserveWrite() // Native Storage writes synchronously. With no handler or earlier write, @@ -633,7 +633,7 @@ export function localStorageCollectionOptions( if (!handler && previous === undefined) { try { persistMutations(mutations) - return Promise.resolve({}) + return Promise.resolve() } catch (error) { return Promise.reject(error) } finally { @@ -641,7 +641,7 @@ export function localStorageCollectionOptions( } } - let result: unknown + let result: void | Promise try { result = handler?.() } catch (error) { @@ -650,11 +650,10 @@ export function localStorageCollectionOptions( return Promise.reject(error) } return Promise.resolve(result).then( - async (value) => { + async () => { await previous try { persistMutations(mutations) - return value ?? {} } finally { retire() } diff --git a/packages/db/src/paced-mutations.ts b/packages/db/src/paced-mutations.ts index b634d4c717..bef70628b1 100644 --- a/packages/db/src/paced-mutations.ts +++ b/packages/db/src/paced-mutations.ts @@ -153,7 +153,7 @@ export function createPacedMutations< const completion = strategyCommit() onStarted?.(completion) completion.catch(() => { - // Persistence failures are reported by transaction.isPersisted.promise. + // Persistence failures are reported by transaction.when('settled'). }) return transaction } diff --git a/packages/db/src/query/builder/ref-proxy.ts b/packages/db/src/query/builder/ref-proxy.ts index 6d4738dd84..660078f7e6 100644 --- a/packages/db/src/query/builder/ref-proxy.ts +++ b/packages/db/src/query/builder/ref-proxy.ts @@ -4,7 +4,7 @@ import { REF_PROXY_BRAND, readRefProxyBrand } from './ref-proxy-identity.js' import { getWrapperExpressionName } from './wrapper-identity.js' import type { BasicExpression } from '../ir.js' import type { IsPlainObject, RefLeaf } from './types.js' -import type { PublishedVirtualRowProps } from '../../virtual-props.js' +import type { VirtualRowProps } from '../../virtual-props.js' export { isRefProxy } from './ref-proxy-identity.js' @@ -33,9 +33,7 @@ export interface RefProxy { export type VirtualPropsRefProxy< TKey extends string | number = string | number, > = { - readonly [K in keyof PublishedVirtualRowProps]: RefLeaf< - PublishedVirtualRowProps[K] - > + readonly [K in keyof VirtualRowProps]: RefLeaf[K]> } // Strip nullish members before deciding whether a schema field is traversable, diff --git a/packages/db/src/query/builder/types.ts b/packages/db/src/query/builder/types.ts index 41a89c1179..0c7996f908 100644 --- a/packages/db/src/query/builder/types.ts +++ b/packages/db/src/query/builder/types.ts @@ -10,10 +10,7 @@ import type { Value, } from '../ir.js' import type { InitialQueryBuilder, QueryBuilder } from './index.js' -import type { - PublishedVirtualRowProps, - WithVirtualProps, -} from '../../virtual-props.js' +import type { VirtualRowProps, WithVirtualProps } from '../../virtual-props.js' import type { CaseWhenWrapper, ConcatToArrayWrapper, @@ -856,9 +853,7 @@ type NonNull = T extends null ? never : T * ``` */ type VirtualPropsRef = { - readonly [K in keyof PublishedVirtualRowProps]: RefLeaf< - PublishedVirtualRowProps[K] - > + readonly [K in keyof VirtualRowProps]: RefLeaf[K]> } /** diff --git a/packages/db/src/query/compiler/group-by.ts b/packages/db/src/query/compiler/group-by.ts index 528dd30d17..fcedb38dd3 100644 --- a/packages/db/src/query/compiler/group-by.ts +++ b/packages/db/src/query/compiler/group-by.ts @@ -82,7 +82,7 @@ function createInternalGroupFields(groupCount: number, selectClause?: Select) { } type RowVirtualMetadata = { - synced: boolean + hasPendingWrites: boolean hasLocal: boolean } @@ -254,27 +254,20 @@ function getGroupEvaluationRow( } function getRowVirtualMetadata(row: NamespacedRow): RowVirtualMetadata { - let found = false - let allSynced = true + let hasPendingWrites = false let hasLocal = false for (const [alias, value] of Object.entries(row as Record)) { if (alias === `$selected`) continue if (value === null || typeof value !== `object`) continue const asRecord = value as Record - const hasSyncedProp = `$synced` in asRecord const hasPendingWritesProp = `$hasPendingWrites` in asRecord const hasOriginProp = `$origin` in asRecord - if (!hasSyncedProp && !hasPendingWritesProp && !hasOriginProp) { + if (!hasPendingWritesProp && !hasOriginProp) { continue } - found = true - // Preserve the existing alias while present; accept new-field-only rows too. - if ( - asRecord.$synced === false || - (!hasSyncedProp && asRecord.$hasPendingWrites === true) - ) { - allSynced = false + if (asRecord.$hasPendingWrites === true) { + hasPendingWrites = true } if (asRecord.$origin === `local`) { hasLocal = true @@ -282,7 +275,7 @@ function getRowVirtualMetadata(row: NamespacedRow): RowVirtualMetadata { } return { - synced: found ? allSynced : true, + hasPendingWrites, hasLocal, } } @@ -311,10 +304,13 @@ export function processGroupBy( [fields.virtual]: { preMap: ([, row]: [string, NamespacedRow]) => getRowVirtualMetadata(row), reduce: (values: Array<[RowVirtualMetadata, number]>) => { - const group: RowVirtualMetadata = { synced: true, hasLocal: false } + const group: RowVirtualMetadata = { + hasPendingWrites: false, + hasLocal: false, + } for (const [metadata, multiplicity] of values) { if (multiplicity <= 0) continue - if (!metadata.synced) group.synced = false + if (metadata.hasPendingWrites) group.hasPendingWrites = true if (metadata.hasLocal) group.hasLocal = true } return group @@ -527,8 +523,7 @@ export function processGroupBy( } const virtual = (aggregatedRow as Record)[fields.virtual] as RowVirtualMetadata | undefined - resultRow.$synced = virtual?.synced ?? true - resultRow.$hasPendingWrites = !resultRow.$synced + resultRow.$hasPendingWrites = virtual?.hasPendingWrites ?? false resultRow.$origin = ( virtual?.hasLocal ? `local` : `remote` ) satisfies VirtualOrigin diff --git a/packages/db/src/query/compiler/index.ts b/packages/db/src/query/compiler/index.ts index 7fa3d8deac..88a59d8a44 100644 --- a/packages/db/src/query/compiler/index.ts +++ b/packages/db/src/query/compiler/index.ts @@ -406,6 +406,12 @@ export function compileQuery(...args: CompileQueryArgs): CompilationResult { return compileQueryInternal(args, false) } +export function compileCollectionQuery( + ...args: CompileQueryArgs +): CompilationResult { + return compileQueryInternal(args, false, true) +} + function compileQueryInternal( [ rawQuery, @@ -423,6 +429,7 @@ function compileQueryInternal( ancestorHasTopK = false, ]: CompileQueryArgs, scopeValidated: boolean, + publishCollectionRoot = false, ): CompilationResult { // Check if the original raw query has already been compiled const cachedResult = @@ -1196,6 +1203,8 @@ function compileQueryInternal( const finalResults = attachVirtualPropsToSelected( unwrapValue(row.$selected), row, + publishCollectionRoot && parentKeyStream === undefined, + mainSource, ) // When in includes mode, embed the correlation key and parentContext if (parentKeyStream) { @@ -2038,6 +2047,8 @@ function unwrapValue(value: any): any { function attachVirtualPropsToSelected( selected: any, row: Record, + isCollectionRoot: boolean, + mainSource: string, ): any { if ( !selected || @@ -2048,23 +2059,39 @@ function attachVirtualPropsToSelected( } const selectedRecord = selected as Record - let needsMerge = false - for (const prop of VIRTUAL_PROP_NAMES) { - if (selectedRecord[prop] == null && prop in row) { - needsMerge = true - break + const source = row[mainSource] + const sourceRecord = + isCollectionRoot && source !== null && typeof source === `object` + ? (source as Record) + : undefined + let result = selectedRecord + const copy = () => { + if (result === selectedRecord) { + result = Array.isArray(selected) ? [...selected] : { ...selected } } } - - if (!needsMerge) { - return selected - } - - const result = ( - Array.isArray(selected) ? [...selected] : { ...selected } - ) as Record for (const prop of VIRTUAL_PROP_NAMES) { - if (selectedRecord[prop] == null && prop in row) { + if (isCollectionRoot && prop in selectedRecord) { + // An explicit virtual name in a root projection cannot replace source + // metadata. Ordinary projections acquire their own Collection metadata. + const owner = + prop in row + ? row + : sourceRecord && prop in sourceRecord + ? sourceRecord + : undefined + if (owner) { + const value = owner[prop] + if (selectedRecord[prop] !== value) { + copy() + result[prop] = value + } + } else { + copy() + delete result[prop] + } + } else if (selectedRecord[prop] == null && prop in row) { + copy() result[prop] = row[prop] } } diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index 74758f24a2..47379ce8c6 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -1,5 +1,5 @@ import { D2, output } from '@tanstack/db-ivm' -import { compileQuery } from '../compiler/index.js' +import { compileCollectionQuery } from '../compiler/index.js' import { LoadSubsetOperationAbortedError, MissingAliasInputsError, @@ -870,7 +870,7 @@ export class CollectionConfigBuilder< } this.inputsCache = inputs - const compilation = compileQuery( + const compilation = compileCollectionQuery( this.query, this.inputsCache as Record, this.collections, diff --git a/packages/db/src/strategies/commit-completion.ts b/packages/db/src/strategies/commit-completion.ts index aeb06c1a2f..f8ce904c53 100644 --- a/packages/db/src/strategies/commit-completion.ts +++ b/packages/db/src/strategies/commit-completion.ts @@ -6,5 +6,5 @@ export function runWithCommitCompletion( onCommit?: () => Promise | undefined, ): Promise { const transaction = run() - return onCommit?.() ?? transaction.isPersisted.promise + return onCommit?.() ?? transaction.when(`settled`) } diff --git a/packages/db/src/strategies/queueStrategy.ts b/packages/db/src/strategies/queueStrategy.ts index 9e3bc52913..842a91fa2c 100644 --- a/packages/db/src/strategies/queueStrategy.ts +++ b/packages/db/src/strategies/queueStrategy.ts @@ -74,7 +74,7 @@ export function queueStrategy(options?: QueueStrategyOptions): QueueStrategy { await runWithCommitCompletion(run, onCommit) }) .catch(() => { - // Errors are handled via transaction.isPersisted.promise and surfaced there. + // Errors are handled via transaction.when(`settled`) and surfaced there. // This catch prevents unhandled promise rejections from breaking the chain, // ensuring subsequent transactions can still execute even if one fails. }) diff --git a/packages/db/src/transactions.ts b/packages/db/src/transactions.ts index 0b8764dc01..bdb35666bf 100644 --- a/packages/db/src/transactions.ts +++ b/packages/db/src/transactions.ts @@ -362,6 +362,16 @@ export function getActiveTransaction(): Transaction | undefined { return defaultTransactionScope.getActiveTransaction() } +// TransactionWithMutations is an Omit-based structural view of Transaction, +// so the settlement cannot be a private class field. +const transactionSettlements = new WeakMap() + +function settlementFor( + transaction: Transaction, +): Deferred> { + return transactionSettlements.get(transaction) as Deferred> +} + class Transaction> { public id: string public state: TransactionState @@ -374,23 +384,6 @@ class Transaction> { public readonly collections: Set> = new Set() private captureMutations?: () => void - /** - * Deferred that settles when this transaction settles. - * - * Await `when('settled')` instead. This legacy promise resolves - * when the transaction completes successfully and rejects if the transaction - * fails or is rolled back. - * - * For non-empty commits, the mutation function is the normal settlement - * boundary. This does not inherently prove that a backend has uploaded, - * confirmed, or read back the write unless the mutation function waits for - * that backend observation before returning. An adapter can also register - * commit work during the mutation function; settlement waits for that work. - * - * @deprecated Use `when('settled')` instead. This alias will be removed in - * the 1.0 RC. - */ - public isPersisted: Deferred> public autoCommit: boolean public createdAt: Date public sequenceNumber: number @@ -412,10 +405,11 @@ class Transaction> { this.mutationFn = config.mutationFn this.state = `pending` this.mutations = [] - this.isPersisted = createDeferred>() + const settlement = createDeferred>() + transactionSettlements.set(this, settlement) // A rejection nobody reads is not an unhandled rejection; a caller reads // the outcome from `when('settled')`. - this.isPersisted.promise.catch(() => undefined) + settlement.promise.catch(() => undefined) this.autoCommit = config.autoCommit ?? true this.createdAt = new Date() this.sequenceNumber = sequenceNumber @@ -435,7 +429,13 @@ class Transaction> { * or an adapter-registered commit work item waits for it. */ when(_state: 'settled'): Promise> { - return this.isPersisted.promise + return settlementFor(this).promise + } + + /** Complete a transaction whose work was already applied outside commit(). @internal */ + completeWithoutCommit(): void { + this.setState(`completed`) + settlementFor(this).resolve(this) } setState(newState: TransactionState) { @@ -561,7 +561,7 @@ class Transaction> { if (this.autoCommit) { this.commit().catch(() => { - // Errors from autoCommit are handled via isPersisted.promise + // Errors from autoCommit are handled via when('settled'). // This catch prevents unhandled promise rejections }) } @@ -693,7 +693,7 @@ class Transaction> { } } finally { // Reject the promise - this.isPersisted.reject(this.error?.error) + settlementFor(this).reject(this.error?.error) this.touchCollection() } @@ -780,7 +780,7 @@ class Transaction> { try { this.touchCollection() } finally { - this.isPersisted.resolve(this) + settlementFor(this).resolve(this) } return this @@ -835,7 +835,7 @@ class Transaction> { try { this.touchCollection() } finally { - this.isPersisted.resolve(this) + settlementFor(this).resolve(this) } return this diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts index 39bd8aeb33..30a1d22e89 100644 --- a/packages/db/src/types.ts +++ b/packages/db/src/types.ts @@ -677,35 +677,23 @@ export type DeleteMutationFnParams< collection: Collection } -/** - * @typeParam TReturn - DEPRECATED: Return values are kept for backward compatibility and will be removed in v1.0. - */ export type InsertMutationFn< T extends object = Record, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, - TReturn = any, -> = (params: InsertMutationFnParams) => Promise +> = (params: InsertMutationFnParams) => Promise -/** - * @typeParam TReturn - DEPRECATED: Return values are kept for backward compatibility and will be removed in v1.0. - */ export type UpdateMutationFn< T extends object = Record, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, - TReturn = any, -> = (params: UpdateMutationFnParams) => Promise +> = (params: UpdateMutationFnParams) => Promise -/** - * @typeParam TReturn - DEPRECATED: Return values are kept for backward compatibility and will be removed in v1.0. - */ export type DeleteMutationFn< T extends object = Record, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, - TReturn = any, -> = (params: DeleteMutationFnParams) => Promise +> = (params: DeleteMutationFnParams) => Promise /** * Collection status values for lifecycle management @@ -746,7 +734,6 @@ export interface BaseCollectionConfig< // requires either T to be provided or a schema to be provided but not both! TSchema extends StandardSchemaV1 = never, TUtils extends UtilsRecord = UtilsRecord, - TReturn = any, > { // If an id isn't passed in, a UUID will be // generated for it. @@ -828,7 +815,6 @@ export interface BaseCollectionConfig< syncMode?: SyncMode /** * Optional asynchronous handler function called before an insert operation - * Returning a value is deprecated; coordinate synchronization through collection utilities instead. * * @example * // Basic insert handler @@ -844,8 +830,6 @@ export interface BaseCollectionConfig< * await api.createTodo(newItem) * // Trigger refetch to sync server state * await collection.utils.refetch() - * // Prevent the pre-1.0 compatibility wrapper from refetching again. - * return { refetch: false } * } * * @example @@ -864,7 +848,6 @@ export interface BaseCollectionConfig< * await api.createTodos(items) * // Refetch to get updated data from server * await collection.utils.refetch() - * return { refetch: false } * } * * @example @@ -879,14 +862,10 @@ export interface BaseCollectionConfig< * } * } */ - onInsert?: - | InsertMutationFn - // Return-value compatibility will be removed in v1.0. - | InsertMutationFn + onInsert?: InsertMutationFn /** * Optional asynchronous handler function called before an update operation - * Returning a value is deprecated; coordinate synchronization through collection utilities instead. * * @example * // Basic update handler @@ -903,8 +882,6 @@ export interface BaseCollectionConfig< * await api.updateTodo(mutation.original.id, changes) * // Trigger refetch to sync server state * await collection.utils.refetch() - * // Prevent the pre-1.0 compatibility wrapper from refetching again. - * return { refetch: false } * } * * @example @@ -925,7 +902,6 @@ export interface BaseCollectionConfig< * })) * await api.updateTodos(updates) * await collection.utils.refetch() - * return { refetch: false } * } * * @example @@ -941,13 +917,9 @@ export interface BaseCollectionConfig< * } * } */ - onUpdate?: - | UpdateMutationFn - // Return-value compatibility will be removed in v1.0. - | UpdateMutationFn + onUpdate?: UpdateMutationFn /** * Optional asynchronous handler function called before a delete operation - * Returning a value is deprecated; coordinate synchronization through collection utilities instead. * * @example * // Basic delete handler @@ -963,8 +935,6 @@ export interface BaseCollectionConfig< * await api.deleteTodos(keysToDelete) * // Trigger refetch to sync server state * await collection.utils.refetch() - * // Prevent the pre-1.0 compatibility wrapper from refetching again. - * return { refetch: false } * } * * @example @@ -1000,10 +970,7 @@ export interface BaseCollectionConfig< * } * } */ - onDelete?: - | DeleteMutationFn - // Return-value compatibility will be removed in v1.0. - | DeleteMutationFn + onDelete?: DeleteMutationFn /** * Specifies how to compare data in the collection. diff --git a/packages/db/src/virtual-props.ts b/packages/db/src/virtual-props.ts index c11a95c83b..78c074e5f0 100644 --- a/packages/db/src/virtual-props.ts +++ b/packages/db/src/virtual-props.ts @@ -38,10 +38,7 @@ export type VirtualOrigin = 'local' | 'remote' /** - * Virtual properties recognized on TanStack DB rows. The new - * `$hasPendingWrites` field is optional here so legacy four-field rows accepted - * by `hasVirtualProps` remain assignable. Rows returned by collections use - * `WithVirtualProps`, which requires it. + * Virtual properties recognized on TanStack DB rows. * * These properties are: * - Computed (not stored in the data model) @@ -49,6 +46,10 @@ export type VirtualOrigin = 'local' | 'remote' * - Available in queries (WHERE, ORDER BY, SELECT) * - Included when spreading rows (`...user`) * + * Whole-row live-query outputs retain source metadata. Ordinary projections + * receive metadata from the live-query Collection. An explicitly selected + * virtual field name cannot replace source metadata at root publication. + * * @template TKey - The type of the row's key (string or number) * * @example @@ -80,30 +81,9 @@ export interface VirtualRowProps< * Whether this row currently has pending local optimistic writes. * * This describes the row's local optimistic state, not backend upload or - * acknowledgement. It is always `false` for local-only collections. It is - * optional only for compatibility with legacy rows; collection-published - * rows always provide it. + * acknowledgement. It is always `false` for local-only collections. */ - readonly $hasPendingWrites?: boolean - - /** - * Whether this row currently has no pending local optimistic writes. - * - * - `true`: No pending local optimistic mutation currently affects this row - * - `false`: One or more pending local optimistic mutations currently affect this row - * - * This is local mutation status. It does not prove that a backend has uploaded, - * confirmed, or read back the row. If you need backend-confirmed status, keep - * your mutation function pending until that backend observation has happened, - * or expose adapter-specific status. - * - * For local-only collections (no sync), this is always `true`. - * For live query collections, this is passed through from the source collection. - * - * @deprecated Use `!row.$hasPendingWrites` instead. This alias will be - * removed in the 1.0 RC. - */ - readonly $synced: boolean + readonly $hasPendingWrites: boolean /** * Collection attribution for this row's current value. `'local'` covers @@ -112,7 +92,8 @@ export interface VirtualRowProps< * key-and-timing rules, including truncate and overlapping mutations. * * For local-only collections, this is always `'local'`. - * For live query collections, this is passed through from the source collection. + * Whole-row live queries preserve source attribution. Projected row roots + * otherwise use the live-query Collection's attribution. */ readonly $origin: VirtualOrigin @@ -128,18 +109,12 @@ export interface VirtualRowProps< * The ID of the source collection this row originated from. * * In joins, this can help identify which collection each row came from. - * For live query collections, this is the ID of the upstream collection. + * Whole-row live queries preserve the upstream Collection's ID. Projected + * row roots otherwise use the live-query Collection's ID. */ readonly $collectionId: string } -/** Virtual properties guaranteed on rows published by this version. @internal */ -export interface PublishedVirtualRowProps< - TKey extends string | number = string | number, -> extends VirtualRowProps { - readonly $hasPendingWrites: boolean -} - /** * Adds virtual properties to a row type. * @@ -150,14 +125,13 @@ export interface PublishedVirtualRowProps< * ```typescript * type User = { id: string; name: string } * type UserWithVirtual = WithVirtualProps - * // { id: string; name: string; $hasPendingWrites: boolean; $synced: boolean; $origin: 'local' | 'remote'; $key: string; $collectionId: string } - * // $synced is deprecated; use !$hasPendingWrites instead. + * // { id: string; name: string; $hasPendingWrites: boolean; $origin: 'local' | 'remote'; $key: string; $collectionId: string } * ``` */ export type WithVirtualProps< T extends object, TKey extends string | number = string | number, -> = T & PublishedVirtualRowProps +> = T & VirtualRowProps /** * Extracts the base type from a type that may have virtual properties. @@ -177,16 +151,14 @@ export type WithoutVirtualProps = T extends unknown : never /** - * Checks if a value has virtual properties attached. Legacy rows with the - * original four properties still match; only rows published by this version - * are guaranteed to carry `$hasPendingWrites`. + * Checks if a value has all virtual properties attached. * * @param value - The value to check * @returns true if the value has virtual properties * * @example * ```typescript - * if (hasVirtualProps(row) && row.$hasPendingWrites !== undefined) { + * if (hasVirtualProps(row)) { * console.log('Pending local writes:', row.$hasPendingWrites) * } * ``` @@ -197,7 +169,7 @@ export function hasVirtualProps( return ( typeof value === 'object' && value !== null && - ['$synced', '$origin', '$key', '$collectionId'].every( + ['$hasPendingWrites', '$origin', '$key', '$collectionId'].every( (name) => name in value, ) ) @@ -215,7 +187,7 @@ export function hasVirtualProps( * @param row - The row to enrich * @param key - The row's key * @param collectionId - The collection's ID - * @param computeSynced - Function to compute $synced if missing + * @param computePendingWrites - Function to compute $hasPendingWrites if missing * @param computeOrigin - Function to compute $origin if missing * @returns The row with virtual properties (possibly the same object if already present) * @@ -228,18 +200,18 @@ export function enrichRowWithVirtualProps< row: T, key: TKey, collectionId: string, - computeSynced: () => boolean, + computePendingWrites: () => boolean, computeOrigin: () => VirtualOrigin, ): WithVirtualProps { // Use nullish coalescing to preserve existing virtual properties (pass-through) // This is the "add-if-missing" pattern described in the RFC const existingRow = row as Partial> - const synced = existingRow.$synced ?? computeSynced() + const hasPendingWrites = + existingRow.$hasPendingWrites ?? computePendingWrites() return { ...row, - $hasPendingWrites: !synced, - $synced: synced, + $hasPendingWrites: hasPendingWrites, $origin: existingRow.$origin ?? computeOrigin(), $key: existingRow.$key ?? key, $collectionId: existingRow.$collectionId ?? collectionId, @@ -252,7 +224,6 @@ export function enrichRowWithVirtualProps< */ export const VIRTUAL_PROP_NAMES = [ '$hasPendingWrites', - '$synced', '$origin', '$key', '$collectionId', diff --git a/packages/db/tests/change-event-history-oracle.test.ts b/packages/db/tests/change-event-history-oracle.test.ts index 719cdb404b..01cd73b01b 100644 --- a/packages/db/tests/change-event-history-oracle.test.ts +++ b/packages/db/tests/change-event-history-oracle.test.ts @@ -230,7 +230,7 @@ function expectedEventTrace( function applyOp( collection: Collection, op: Op, -): { isPersisted: { promise: Promise } } { +): { when: (state: 'settled') => Promise } { switch (op.kind) { case `insert`: return collection.insert({ @@ -247,8 +247,8 @@ function applyOp( } } -const settle = (tx: { isPersisted: { promise: Promise } }) => - tx.isPersisted.promise +const settle = (tx: { when: (state: 'settled') => Promise }) => + tx.when(`settled`) function publicRows( collection: Collection, diff --git a/packages/db/tests/collection-indexes.test.ts b/packages/db/tests/collection-indexes.test.ts index d08920b07d..72d8fcf83e 100644 --- a/packages/db/tests/collection-indexes.test.ts +++ b/packages/db/tests/collection-indexes.test.ts @@ -451,7 +451,7 @@ describe(`Collection Indexes`, () => { const tx = createTransaction({ mutationFn }) tx.mutate(() => collection.insert(newItem)) - await tx.isPersisted.promise + await tx.when(`settled`) // Item should be in collection state expect(collection.size).toBe(6) @@ -488,7 +488,7 @@ describe(`Collection Indexes`, () => { draft.age = 26 }), ) - await tx.isPersisted.promise + await tx.when(`settled`) // Updated item should be in collection state const updatedItem = collection.get(`1`) @@ -518,7 +518,7 @@ describe(`Collection Indexes`, () => { draft.age = 26 }), ) - await tx.isPersisted.promise + await tx.when(`settled`) // Updated item should be in collection state const updatedItem = collection.get(`1`) @@ -551,7 +551,7 @@ describe(`Collection Indexes`, () => { const tx = createTransaction({ mutationFn }) tx.mutate(() => collection.delete(`1`)) - await tx.isPersisted.promise + await tx.when(`settled`) // Item should be removed from collection state expect(collection.size).toBe(4) @@ -580,7 +580,7 @@ describe(`Collection Indexes`, () => { const tx = createTransaction({ mutationFn }) tx.mutate(() => collection.delete(`1`)) - await tx.isPersisted.promise + await tx.when(`settled`) // Item should be removed from collection state expect(collection.size).toBe(4) @@ -615,7 +615,7 @@ describe(`Collection Indexes`, () => { draft.status = `active` }), ) - await tx1.isPersisted.promise + await tx1.when(`settled`) const dataChanges = stripVirtualOnlyUpdates(activeChanges) expect(dataChanges).toHaveLength(1) @@ -629,7 +629,7 @@ describe(`Collection Indexes`, () => { draft.status = `inactive` }), ) - await tx2.isPersisted.promise + await tx2.when(`settled`) // Should trigger delete event for item that no longer matches filter const filteredChanges = stripVirtualOnlyUpdates(activeChanges) @@ -660,7 +660,7 @@ describe(`Collection Indexes`, () => { draft.status = `active` }), ) - await tx1.isPersisted.promise + await tx1.when(`settled`) const dataChanges = stripVirtualOnlyUpdates(activeChanges) expect(dataChanges).toHaveLength(1) @@ -674,7 +674,7 @@ describe(`Collection Indexes`, () => { draft.status = `inactive` }), ) - await tx2.isPersisted.promise + await tx2.when(`settled`) // Subscriber shoiuld not receive any changes // because it is not aware of that key @@ -2160,7 +2160,7 @@ describe(`Collection Indexes`, () => { createdAt: new Date(), }), ) - await tx1.isPersisted.promise + await tx1.when(`settled`) const dataChanges = stripVirtualOnlyUpdates(changes) expect(dataChanges).toHaveLength(1) @@ -2178,7 +2178,7 @@ describe(`Collection Indexes`, () => { createdAt: new Date(), }), ) - await tx2.isPersisted.promise + await tx2.when(`settled`) expect(changes).toHaveLength(0) @@ -2190,7 +2190,7 @@ describe(`Collection Indexes`, () => { draft.status = `inactive` }), ) - await tx3.isPersisted.promise + await tx3.when(`settled`) expect(changes).toHaveLength(1) // Should emit delete event for item leaving filter expect(changes[0]?.type).toBe(`delete`) @@ -2237,7 +2237,7 @@ describe(`Collection Indexes`, () => { draft.age = 32 }), ) - await tx.isPersisted.promise + await tx.when(`settled`) const dataChanges = stripVirtualOnlyUpdates(changes) expect(dataChanges).toHaveLength(1) @@ -2413,9 +2413,9 @@ describe(`Collection Indexes`, () => { tx3.mutate(() => collection.delete(`2`)) await Promise.all([ - tx1.isPersisted.promise, - tx2.isPersisted.promise, - tx3.isPersisted.promise, + tx1.when(`settled`), + tx2.when(`settled`), + tx3.when(`settled`), ]) // Wait a bit for sync to complete and indexes to update diff --git a/packages/db/tests/collection-metadata-publication-oracle.property.test.ts b/packages/db/tests/collection-metadata-publication-oracle.property.test.ts index 518e1472aa..0992eb6f1e 100644 --- a/packages/db/tests/collection-metadata-publication-oracle.property.test.ts +++ b/packages/db/tests/collection-metadata-publication-oracle.property.test.ts @@ -87,7 +87,6 @@ type PublishedPublicationRow = PublicationRow & { $key: number $origin: `local` | `remote` $hasPendingWrites: boolean - $synced: boolean } // Grammar controls: the fixed same-key and cancellation witnesses below are @@ -236,7 +235,6 @@ function selectPublishedRow( $key: published.$key, $origin: published.$origin, $hasPendingWrites: published.$hasPendingWrites, - $synced: published.$synced, } } @@ -334,7 +332,7 @@ async function applyRound( draft.position = next.position }) }) - await transaction.isPersisted.promise + await transaction.when(`settled`) model.set(round.key, next) if (round.outcome === `commit`) { @@ -350,26 +348,25 @@ async function applyRound( corruptCut?.(harness.cuts.slice(cutCountBefore)) const virtualRow = ( row: PublicationRow, - synced: boolean, + hasPendingWrites: boolean, ): PublishedPublicationRow => ({ ...row, $collectionId: harness.rows.id, $key: row.id, $origin: `local`, - $hasPendingWrites: !synced, - $synced: synced, + $hasPendingWrites: hasPendingWrites, }) const expectedOptimisticChange = keyWasPreviouslyPublished ? { type: `update`, key: round.key, - value: virtualRow(next, false), - previousValue: virtualRow(previous, true), + value: virtualRow(next, true), + previousValue: virtualRow(previous, false), } : { type: `insert`, key: round.key, - value: virtualRow(next, false), + value: virtualRow(next, true), previousValue: undefined, } expect( @@ -382,8 +379,8 @@ async function applyRound( { type: `update`, key: round.key, - value: virtualRow(next, true), - previousValue: virtualRow(next, false), + value: virtualRow(next, false), + previousValue: virtualRow(next, true), }, ], ]) @@ -550,7 +547,7 @@ async function expectMetadataCancellationOwnership( ) persistence.resolve() - await heldTransaction.isPersisted.promise + await heldTransaction.when(`settled`) await expect(retained).resolves.toBeUndefined() expect(readMetadata(harness, [0, 1, 2])).toEqual( observableMetadata(expectedMetadata, [0, 1, 2]), @@ -562,7 +559,7 @@ async function expectMetadataCancellationOwnership( }, () => [ () => persistence.resolve(), - () => heldTransaction.isPersisted.promise.catch(() => undefined), + () => heldTransaction.when(`settled`).catch(() => undefined), () => canceled.catch(() => undefined), () => retained.catch(() => undefined), ], diff --git a/packages/db/tests/collection-mutation-startup-oracle.test.ts b/packages/db/tests/collection-mutation-startup-oracle.test.ts index fe3931e774..e0adb0c500 100644 --- a/packages/db/tests/collection-mutation-startup-oracle.test.ts +++ b/packages/db/tests/collection-mutation-startup-oracle.test.ts @@ -318,7 +318,7 @@ describe(`Collection mutation startup oracle`, () => { ).toEqual( operation === `update` ? [{ id: `target`, value: `updated` }] : [], ) - await transaction.isPersisted.promise + await transaction.when(`settled`) expect(onUpdate).toHaveBeenCalledTimes(operation === `update` ? 1 : 0) expect(onDelete).toHaveBeenCalledTimes(operation === `delete` ? 1 : 0) } finally { @@ -475,7 +475,7 @@ describe(`Collection mutation startup oracle`, () => { try { const transaction = collection.insert(local) - await transaction.isPersisted.promise + await transaction.when(`settled`) expect(syncStarts).toBe(1) expect(onInsert).toHaveBeenCalledTimes(1) expect( @@ -521,7 +521,7 @@ describe(`Collection mutation startup oracle`, () => { expect(syncStarts).toBe(1) expect(collection.status).toBe(`ready`) const second = collection.insert({ id: `second`, value: `second` }) - await Promise.all([first.isPersisted.promise, second.isPersisted.promise]) + await Promise.all([first.when(`settled`), second.when(`settled`)]) expect(syncStarts).toBe(1) expect(collection.status).toBe(`ready`) expect(handlerCalls).toBe(2) @@ -663,7 +663,7 @@ describe(`Collection readiness during a pending mutation`, () => { await handlerEntered if (source === `outside-handler`) startup() const settled = await Promise.race([ - insert.isPersisted.promise.then(() => `settled` as const), + insert.when(`settled`).then(() => `settled` as const), new Promise<`pending`>((resolve) => setTimeout(() => resolve(`pending`), 200), ), diff --git a/packages/db/tests/collection-query-publication-boundaries.test.ts b/packages/db/tests/collection-query-publication-boundaries.test.ts index f5197fc4e1..e3d354c155 100644 --- a/packages/db/tests/collection-query-publication-boundaries.test.ts +++ b/packages/db/tests/collection-query-publication-boundaries.test.ts @@ -120,12 +120,12 @@ it.each([1, 99])( sync.write({ type: `insert`, value: { id: 3, value: 3 } }) const blocked = sync.commit() first.resolve() - await tx1.isPersisted.promise + await tx1.when(`settled`) sync.begin() sync.write({ type: `insert`, value: { id: 1, value: echoValue } }) const echo = sync.commit() second.resolve() - await tx2.isPersisted.promise + await tx2.when(`settled`) await blocked await echo await flushPromises() diff --git a/packages/db/tests/collection-row-metadata-composition-oracle.test.ts b/packages/db/tests/collection-row-metadata-composition-oracle.test.ts index ff566c5791..d8e2415d59 100644 --- a/packages/db/tests/collection-row-metadata-composition-oracle.test.ts +++ b/packages/db/tests/collection-row-metadata-composition-oracle.test.ts @@ -385,7 +385,7 @@ async function observeMetadata( if (lane === `rebuilt`) { expect(earlierReceipt).not.toBe(true) release() - await blocker!.isPersisted.promise + await blocker!.when(`settled`) await earlierReceipt } const receipt = sync.commit() @@ -396,7 +396,7 @@ async function observeMetadata( expect(receipt).toBe(true) else expect(receipt).not.toBe(true) release() - await blocker!.isPersisted.promise + await blocker!.when(`settled`) } await receipt return sync.metadata!.row.get(1) diff --git a/packages/db/tests/collection-state-retention-oracle.property.test.ts b/packages/db/tests/collection-state-retention-oracle.property.test.ts index 04018edfb3..21bd1cae8a 100644 --- a/packages/db/tests/collection-state-retention-oracle.property.test.ts +++ b/packages/db/tests/collection-state-retention-oracle.property.test.ts @@ -629,7 +629,6 @@ it(`publishes a virtual-state update when a restarted optimistic row is confirme $key: number $origin: `local` | `remote` $hasPendingWrites: boolean - $synced: boolean } type ObservedChange = { type: string @@ -644,7 +643,6 @@ it(`publishes a virtual-state update when a restarted optimistic row is confirme $key: row.$key, $origin: row.$origin, $hasPendingWrites: row.$hasPendingWrites, - $synced: row.$synced, }) const publications: Array<{ changes: Array @@ -690,7 +688,7 @@ it(`publishes a virtual-state update when a restarted optimistic row is confirme }) readMutationState = () => transaction.state rollbackMutation = () => transaction.rollback() - void transaction.isPersisted.promise.catch(() => undefined) + void transaction.when(`settled`).catch(() => undefined) transaction.mutate(() => collection.insert({ id: 2, value: 2 })) mutationCommit = transaction.commit() @@ -713,7 +711,6 @@ it(`publishes a virtual-state update when a restarted optimistic row is confirme $key: id, $origin: `remote`, $hasPendingWrites: false, - $synced: true, }) const localRow = (id: number): ObservedRow => ({ id, @@ -722,7 +719,6 @@ it(`publishes a virtual-state update when a restarted optimistic row is confirme $key: id, $origin: `local`, $hasPendingWrites: true, - $synced: false, }) const expectedPublications = [ { @@ -1514,7 +1510,7 @@ it(`snapshots a buffered delete before its row object is reused`, async () => { type: string value: LivePreviousRow[`value`] previousValue: LivePreviousRow[`value`] - previousSynced: boolean | undefined + previousHasPendingWrites: boolean | undefined previousOrigin: string | undefined }> > = [] @@ -1523,13 +1519,16 @@ it(`snapshots a buffered delete before its row object is reused`, async () => { publications.push( changes.map((change) => { const previous = change.previousValue as - | (LivePreviousRow & { $synced: boolean; $origin: string }) + | (LivePreviousRow & { + $hasPendingWrites: boolean + $origin: string + }) | undefined return { type: change.type, value: change.value.value, previousValue: previous?.value, - previousSynced: previous?.$synced, + previousHasPendingWrites: previous?.$hasPendingWrites, previousOrigin: previous?.$origin, } }), @@ -1561,7 +1560,7 @@ it(`snapshots a buffered delete before its row object is reused`, async () => { type: `update`, value: 1, previousValue: 0, - previousSynced: true, + previousHasPendingWrites: false, previousOrigin: `remote`, }, ], @@ -1994,7 +1993,7 @@ it(`does not carry previous-value state across a failed sync run`, async () => { throw optimisticFailure }, }) - const persistence = optimistic.isPersisted.promise.catch((error) => error) + const persistence = optimistic.when(`settled`).catch((error) => error) optimistic.mutate(() => collection.insert({ id: 3, value: 30 })) expect(collection.has(3), `failed-sync optimistic overlay appears`).toBe( true, @@ -2661,7 +2660,7 @@ it(`rejects a partial update applied as a full replacement`, async () => { ), // A row observation must reject the replaced row, not the mutant checkpoint. ).rejects.toThrow( - /whole forward publication|: reads .* expected |rows visible when isPersisted settled/, + /whole forward publication|: reads .* expected |rows visible when settlement completed/, ) }) it(`rejects a default lost only after settlement`, async () => { @@ -2773,9 +2772,9 @@ it(`drops a completed direct insert whose handler wrote no sync row`, async () = await collection.stateWhenReady() sync.begin() sync.truncate() - await collection.insert({ id: 1, value: 1 }).isPersisted.promise + await collection.insert({ id: 1, value: 1 }).when(`settled`) expect(sync.commit()).toBe(true) - await collection.insert({ id: 2, value: 2 }).isPersisted.promise + await collection.insert({ id: 2, value: 2 }).when(`settled`) expect(events).toEqual([`insert:1`, `delete:1`, `insert:2`, `delete:2`]) expect([...collection.state.keys()]).toEqual([]) diff --git a/packages/db/tests/collection-subscribe-changes.test.ts b/packages/db/tests/collection-subscribe-changes.test.ts index 940ffc58e9..e29b306a49 100644 --- a/packages/db/tests/collection-subscribe-changes.test.ts +++ b/packages/db/tests/collection-subscribe-changes.test.ts @@ -460,9 +460,9 @@ describe(`Collection.subscribeChanges`, () => { ]) callback.mockReset() - await tx.isPersisted.promise + await tx.when(`settled`) - // Sync confirmation should only change virtual props ($synced/$origin) + // Sync confirmation should only change virtual props ($hasPendingWrites/$origin) expect(callback).toHaveBeenCalledTimes(1) expect( normalizeChangesWithoutVirtualUpdates(callback.mock.calls[0]![0]), @@ -498,9 +498,9 @@ describe(`Collection.subscribeChanges`, () => { ]) callback.mockReset() - await updateTx.isPersisted.promise + await updateTx.when(`settled`) - // Sync confirmation should only change virtual props ($synced/$origin) + // Sync confirmation should only change virtual props ($hasPendingWrites/$origin) expect(callback).toHaveBeenCalledTimes(1) expect( normalizeChangesWithoutVirtualUpdates(callback.mock.calls[0]![0]), @@ -620,7 +620,7 @@ describe(`Collection.subscribeChanges`, () => { // Wait for changes to propagate await waitForChanges() - // Sync confirmation should only change virtual props ($synced/$origin) + // Sync confirmation should only change virtual props ($hasPendingWrites/$origin) expect(callback).toHaveBeenCalledTimes(1) expect( normalizeChangesWithoutVirtualUpdates(callback.mock.calls[0]![0]), @@ -847,10 +847,10 @@ describe(`Collection.subscribeChanges`, () => { // Keep the immediate optimistic assertions isolated above, then prove that // every auto-commit also completes through applied-receipt settlement. await Promise.all([ - tx1.isPersisted.promise, - tx2.isPersisted.promise, - tx3.isPersisted.promise, - tx4.isPersisted.promise, + tx1.when(`settled`), + tx2.when(`settled`), + tx3.when(`settled`), + tx4.when(`settled`), ]) // Clean up @@ -1053,7 +1053,7 @@ describe(`Collection.subscribeChanges`, () => { }) // Wait for transactions to complete - await Promise.all([tx1.isPersisted.promise, tx2.isPersisted.promise]) + await Promise.all([tx1.when(`settled`), tx2.when(`settled`)]) }) it(`should emit insert events for new data after truncate`, async () => { @@ -1353,7 +1353,7 @@ describe(`Collection.subscribeChanges`, () => { callback.mockClear() const tx = collection.delete(1, { optimistic: false }) - await tx.isPersisted.promise + await tx.when(`settled`) expect(callback).toHaveBeenCalledTimes(1) const deleteChanges = callback.mock.calls[0]![0] as ChangesPayload<{ @@ -2250,7 +2250,7 @@ describe(`Virtual properties`, () => { expect(insertChange).toBeDefined() const value = insertChange!.value as Record - expect(value.$synced).toBe(true) + expect(value.$hasPendingWrites).toBe(false) expect(value.$origin).toBe(`remote`) expect(value.$key).toBe(`row-1`) expect(value.$collectionId).toBe(`virtual-props-change-test`) @@ -2258,7 +2258,7 @@ describe(`Virtual properties`, () => { subscription.unsubscribe() }) - it(`should set $synced false and $origin local for optimistic inserts`, async () => { + it(`should set pending writes and local origin for optimistic inserts`, async () => { const changes: Array> = [] const collection = createCollection<{ id: string; value: string }, string>({ @@ -2286,13 +2286,13 @@ describe(`Virtual properties`, () => { expect(insertChange).toBeDefined() const value = insertChange!.value as Record - expect(value.$synced).toBe(false) + expect(value.$hasPendingWrites).toBe(true) expect(value.$origin).toBe(`local`) subscription.unsubscribe() }) - it(`should emit an update when $synced flips on confirmation`, async () => { + it(`should emit an update when pending writes clear on confirmation`, async () => { const changes: Array< ChangeMessage> > = [] @@ -2343,7 +2343,7 @@ describe(`Virtual properties`, () => { (change) => change.type === `insert` && change.key === `row-1`, ) expect(optimisticInsert).toBeDefined() - expect(optimisticInsert!.value.$synced).toBe(false) + expect(optimisticInsert!.value.$hasPendingWrites).toBe(true) changes.length = 0 @@ -2364,8 +2364,8 @@ describe(`Virtual properties`, () => { (change) => change.type === `update` && change.key === `row-1`, ) expect(confirmedUpdate).toBeDefined() - expect(confirmedUpdate!.value.$synced).toBe(true) - expect(confirmedUpdate!.previousValue?.$synced).toBe(false) + expect(confirmedUpdate!.value.$hasPendingWrites).toBe(false) + expect(confirmedUpdate!.previousValue?.$hasPendingWrites).toBe(true) subscription.unsubscribe() }) @@ -2411,9 +2411,9 @@ describe(`Virtual properties`, () => { ) expect(updateChange).toBeDefined() - expect(updateChange!.value.$synced).toBe(false) + expect(updateChange!.value.$hasPendingWrites).toBe(true) expect(updateChange!.value.$origin).toBe(`local`) - expect(updateChange!.previousValue?.$synced).toBe(true) + expect(updateChange!.previousValue?.$hasPendingWrites).toBe(false) expect(updateChange!.previousValue?.$origin).toBe(`remote`) subscription.unsubscribe() @@ -2473,7 +2473,7 @@ describe(`Virtual properties`, () => { expect(insertChange).toBeDefined() const value = insertChange!.value as Record - expect(value.$synced).toBe(true) + expect(value.$hasPendingWrites).toBe(false) expect(value.$origin).toBe(`local`) subscription.unsubscribe() @@ -2515,7 +2515,7 @@ describe(`Virtual properties`, () => { const tx = collection.insert({ id: `row-1`, value: `optimistic` }) try { - await tx.isPersisted.promise + await tx.when(`settled`) } catch { // Expected failure } @@ -2539,7 +2539,7 @@ describe(`Virtual properties`, () => { expect(latestChange).toBeDefined() const value = latestChange!.value as Record - expect(value.$synced).toBe(true) + expect(value.$hasPendingWrites).toBe(false) expect(value.$origin).toBe(`remote`) subscription.unsubscribe() @@ -2580,7 +2580,7 @@ describe(`Virtual properties`, () => { { id: `row-1`, value: `local` }, { optimistic: false }, ) - await expect(tx.isPersisted.promise).rejects.toThrow(`insert failed`) + await expect(tx.when(`settled`)).rejects.toThrow(`insert failed`) if (!syncFns) { throw new Error(`Sync not ready`) @@ -2595,7 +2595,7 @@ describe(`Virtual properties`, () => { await waitForChanges() - expect(collection.state.get(`row-1`)?.$synced).toBe(true) + expect(collection.state.get(`row-1`)?.$hasPendingWrites).toBe(false) expect(collection.state.get(`row-1`)?.$origin).toBe(`remote`) }) @@ -2651,7 +2651,7 @@ describe(`Virtual properties`, () => { { id: `row-1`, value: `local` }, { optimistic: false }, ) - await insertTx.isPersisted.promise + await insertTx.when(`settled`) await waitForChanges() expect(collection.state.get(`row-1`)?.$origin).toBe(`local`) @@ -2703,14 +2703,15 @@ describe(`Virtual properties`, () => { onInsert: () => Promise.resolve(), }) try { - await collection.delete(`row`).isPersisted.promise + await collection.delete(`row`).when(`settled`) // The delete settled without its echo, so the synced row returns. expect(collection.get(`row`)?.value).toBe(`original`) if (deleteEchoed) echoDelete() expect(collection.has(`row`)).toBe(!deleteEchoed) if (deleteEchoed) { - await collection.insert({ id: `row`, value: `replacement` }) - .isPersisted.promise + await collection + .insert({ id: `row`, value: `replacement` }) + .when(`settled`) expect(collection.has(`row`)).toBe(false) } } finally { @@ -2758,7 +2759,7 @@ describe(`Virtual properties`, () => { const transaction = collection.insert({ id: `row-1`, value: `client` }) if (settlement === `before`) { finishMutation() - await transaction.isPersisted.promise + await transaction.when(`settled`) } // A settled insert without a confirmation has dropped. if (settlement === `before`) @@ -2782,8 +2783,8 @@ describe(`Virtual properties`, () => { // acknowledged insertion completes; it must not erase that evidence. const peer = collection.insert({ id: `other`, value: `peer` }) finishMutation() - await transaction.isPersisted.promise - await peer.isPersisted.promise + await transaction.when(`settled`) + await peer.when(`settled`) } if (applied !== true) await applied await waitForChanges() @@ -2792,7 +2793,7 @@ describe(`Virtual properties`, () => { id: `row-1`, value: `server`, }) - expect(collection.state.get(`row-1`)?.$synced).toBe(true) + expect(collection.state.get(`row-1`)?.$hasPendingWrites).toBe(false) // A same-key sync during the active mutation is a local acknowledgement; // after completion, truncate is an independent remote replacement. expect(collection.state.get(`row-1`)?.$origin).toBe( @@ -2853,24 +2854,32 @@ describe(`Virtual properties`, () => { { id: `row-1`, value: `local` }, { optimistic: false }, ) - await tx.isPersisted.promise + await tx.when(`settled`) await waitForChanges() - expect(collection.state.get(`row-1`)?.$synced).toBe(true) + expect(collection.state.get(`row-1`)?.$hasPendingWrites).toBe(false) expect(collection.state.get(`row-1`)?.$origin).toBe(`local`) }) - it(`hasVirtualProps recognizes the legacy four-field shape`, () => { + it(`hasVirtualProps requires the pending-write field`, () => { + expect( + hasVirtualProps({ + $hasPendingWrites: false, + $origin: `remote`, + }), + ).toBe(false) + expect( hasVirtualProps({ - $synced: true, $origin: `remote`, + $key: `row-1`, + $collectionId: `collection-1`, }), ).toBe(false) expect( hasVirtualProps({ - $synced: true, + $hasPendingWrites: false, $origin: `remote`, $key: `row-1`, $collectionId: `collection-1`, @@ -2910,7 +2919,7 @@ describe(`Virtual properties`, () => { await waitForChanges() const liveRow = liveChanges[0]?.value as Record - expect(liveRow.$synced).toBe(true) + expect(liveRow.$hasPendingWrites).toBe(false) expect(liveRow.$origin).toBe(`remote`) expect(liveRow.$collectionId).toBe(`virtual-props-source`) @@ -2919,7 +2928,7 @@ describe(`Virtual properties`, () => { await live.cleanup() }) - it(`should allow filtering on $synced in live queries`, async () => { + it(`should allow filtering on pending writes in live queries`, async () => { const source = createCollection<{ id: string; value: string }, string>({ id: `virtual-props-filter-source`, getKey: (item) => item.id, @@ -2942,7 +2951,9 @@ describe(`Virtual properties`, () => { const syncedOnly = createLiveQueryCollection({ id: `virtual-props-filter-live`, query: (q) => - q.from({ item: source }).where(({ item }) => eq(item.$synced, true)), + q + .from({ item: source }) + .where(({ item }) => eq(item.$hasPendingWrites, false)), }) const liveChanges: Array> = [] @@ -3033,7 +3044,6 @@ describe(`Virtual properties`, () => { const groupRow = grouped.toArray.find((row) => row.group === `g1`) expect(groupRow).toBeDefined() expect(groupRow!.$hasPendingWrites).toBe(true) - expect(groupRow!.$synced).toBe(false) expect(groupRow!.$origin).toBe(`local`) expect(groupRow!.$collectionId).toBe(`virtual-props-aggregate-source`) @@ -3068,7 +3078,7 @@ describe(`Virtual properties`, () => { const value = insertChange!.value as Record expect(value.$hasPendingWrites).toBe(false) - expect(value.$synced).toBe(true) + expect(value.$hasPendingWrites).toBe(false) expect(value.$origin).toBe(`local`) subscription.unsubscribe() diff --git a/packages/db/tests/collection-subscription-replay-oracle.property.test.ts b/packages/db/tests/collection-subscription-replay-oracle.property.test.ts index 4e19378ac6..c494b8950d 100644 --- a/packages/db/tests/collection-subscription-replay-oracle.property.test.ts +++ b/packages/db/tests/collection-subscription-replay-oracle.property.test.ts @@ -1457,7 +1457,7 @@ async function runOptimisticReplayScenario( const transaction = createTransaction({ mutationFn: () => mutation.promise, }) - void transaction.isPersisted.promise.catch(() => {}) + void transaction.when(`settled`).catch(() => {}) let unsubscribed = false try { @@ -1500,7 +1500,7 @@ async function runOptimisticReplayScenario( else replay.reject(replayFailure) await flushPromises() mutation.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await flushPromises() await flushPromises() diff --git a/packages/db/tests/collection-sync-reentrancy-oracle.test.ts b/packages/db/tests/collection-sync-reentrancy-oracle.test.ts index b245c9be51..7761824a4f 100644 --- a/packages/db/tests/collection-sync-reentrancy-oracle.test.ts +++ b/packages/db/tests/collection-sync-reentrancy-oracle.test.ts @@ -551,7 +551,7 @@ describe(`sync publication reentrancy`, () => { expect([...collection.keys()]).toEqual([1, 2]) updatePersistence.resolve() - await update.isPersisted.promise + await update.when(`settled`) if (receipt !== true) await receipt expect([...collection.keys()]).toEqual([2, 1]) @@ -579,7 +579,7 @@ describe(`sync publication reentrancy`, () => { expect(markedReceiptSettled).toBe(true) } finally { updatePersistence.resolve() - await update.isPersisted.promise.catch(() => undefined) + await update.when(`settled`).catch(() => undefined) subscription.unsubscribe() await collection.cleanup() } @@ -684,8 +684,8 @@ describe(`sync publication reentrancy`, () => { subscription.unsubscribe() updatePersistence.resolve() insertPersistence.resolve() - await update.isPersisted.promise.catch(() => undefined) - await insert?.isPersisted.promise.catch(() => undefined) + await update.when(`settled`).catch(() => undefined) + await insert?.when(`settled`).catch(() => undefined) await collection.cleanup() } }) @@ -809,7 +809,8 @@ describe(`sync publication reentrancy`, () => { onUpdate: () => persistence.promise, onDelete: () => persistence.promise, }) - const requests: Array<{ isPersisted: { promise: Promise } }> = [] + const requests: Array<{ when: (state: 'settled') => Promise }> = + [] const apply = (intent: ReadyIntent) => { requests.push( intent.type === `delete` @@ -845,7 +846,8 @@ describe(`sync publication reentrancy`, () => { change.type !== `delete` && sourceValues.has(change.value.value) && prior?.key !== change.key && - (!change.value.$synced || change.value.$origin !== `remote`) + (change.value.$hasPendingWrites || + change.value.$origin !== `remote`) ) violations.push(`source row ${change.key} published as local`) if (change.type === `delete`) mirror.delete(change.key) @@ -932,7 +934,7 @@ describe(`sync publication reentrancy`, () => { persistence.resolve() await Promise.all( requests.map((request) => - request.isPersisted.promise.catch(() => undefined), + request.when(`settled`).catch(() => undefined), ), ) await collection.cleanup() @@ -1017,7 +1019,7 @@ describe(`sync publication reentrancy`, () => { } finally { subscription?.unsubscribe() persistence.resolve() - await blocker.isPersisted.promise.catch(() => undefined) + await blocker.when(`settled`).catch(() => undefined) await collection.cleanup() } }, diff --git a/packages/db/tests/collection-truncate.test.ts b/packages/db/tests/collection-truncate.test.ts index 53c039e398..977c66088e 100644 --- a/packages/db/tests/collection-truncate.test.ts +++ b/packages/db/tests/collection-truncate.test.ts @@ -85,7 +85,7 @@ describe(`Collection truncate operations`, () => { syncOps!.write({ type: `insert`, value: { id: 2, value: `initial-2` } }) syncOps!.commit() - await tx.isPersisted.promise + await tx.when(`settled`) // Verify final state includes all items expect(collection.state.size).toBe(3) @@ -144,7 +144,7 @@ describe(`Collection truncate operations`, () => { syncOps!.truncate() syncOps!.write({ type: `insert`, value: { id: 1, value: `initial-1` } }) - await tx.isPersisted.promise + await tx.when(`settled`) syncOps!.commit() @@ -205,7 +205,7 @@ describe(`Collection truncate operations`, () => { syncOps!.truncate() syncOps!.commit() - await tx.isPersisted.promise + await tx.when(`settled`) // Item should be present in final state expect(getStateValue(collection, 1)).toEqual({ id: 1, value: `user-item` }) @@ -286,7 +286,7 @@ describe(`Collection truncate operations`, () => { // Now explicitly complete the onInsert resolveOnInsert!() - await tx.isPersisted.promise + await tx.when(`settled`) }) it(`should preserve optimistic inserts started after truncate begins`, async () => { @@ -357,7 +357,7 @@ describe(`Collection truncate operations`, () => { while (onInsertResolvers.length > 0) { onInsertResolvers.pop()!() } - await lateTx.isPersisted.promise + await lateTx.when(`settled`) }) it(`should preserve all optimistic inserts when truncate occurs during async mutation handler`, async () => { @@ -411,7 +411,7 @@ describe(`Collection truncate operations`, () => { syncOps!.write({ type: `insert`, value: { id: 1, value: `server-item` } }) syncOps!.commit() - await tx.isPersisted.promise + await tx.when(`settled`) // Both items should be present in final state expect(collection.state.size).toBe(2) @@ -476,7 +476,7 @@ describe(`Collection truncate operations`, () => { // Clean up onDeleteResolvers.forEach((r) => r()) - await deleteTx.isPersisted.promise + await deleteTx.when(`settled`) }) it(`should preserve optimistic value over server value when transaction active during truncate`, async () => { @@ -545,7 +545,7 @@ describe(`Collection truncate operations`, () => { // Clean up onUpdateResolvers.forEach((r) => r()) - await updateTx.isPersisted.promise + await updateTx.when(`settled`) }) it(`should handle multiple consecutive truncate operations`, async () => { @@ -611,7 +611,7 @@ describe(`Collection truncate operations`, () => { // Clean up onInsertResolvers.forEach((r) => r()) - await Promise.all([tx1.isPersisted.promise, tx2.isPersisted.promise]) + await Promise.all([tx1.when(`settled`), tx2.when(`settled`)]) }) it(`should handle new mutation on same key after truncate snapshot`, async () => { @@ -671,7 +671,7 @@ describe(`Collection truncate operations`, () => { // Clean up onUpdateResolvers.forEach((r) => r()) - await Promise.all([tx1.isPersisted.promise, tx2.isPersisted.promise]) + await Promise.all([tx1.when(`settled`), tx2.when(`settled`)]) }) it(`should handle transaction completing between truncate and commit`, async () => { @@ -718,7 +718,7 @@ describe(`Collection truncate operations`, () => { // Transaction completes BEFORE commit onInsertResolver!() - await tx.isPersisted.promise + await tx.when(`settled`) // Now commit the truncate syncOps!.write({ type: `insert`, value: { id: 1, value: `initial` } }) diff --git a/packages/db/tests/collection.test.ts b/packages/db/tests/collection.test.ts index 64b856dbc0..c0d5e2bc66 100644 --- a/packages/db/tests/collection.test.ts +++ b/packages/db/tests/collection.test.ts @@ -84,11 +84,13 @@ describe(`Collection`, () => { const collection = createCollection(config) try { await collection.stateWhenReady() - await collection.insert({ id: 3, v: 0 }).isPersisted.promise - await collection.update(1, (draft) => { - draft.v = 1 - }).isPersisted.promise - await collection.delete(2).isPersisted.promise + await collection.insert({ id: 3, v: 0 }).when(`settled`) + await collection + .update(1, (draft) => { + draft.v = 1 + }) + .when(`settled`) + await collection.delete(2).when(`settled`) expect(calls).toEqual([`insert:config`, `update:config`, `delete:config`]) } finally { await collection.cleanup() @@ -193,7 +195,7 @@ describe(`Collection`, () => { const failed = collection.update(1, (draft) => { draft.value = cycle + 1 }) - await expect(failed.isPersisted.promise).rejects.toThrow(`rejected`) + await expect(failed.when(`settled`)).rejects.toThrow(`rejected`) const rolledBack = createTransaction({ autoCommit: false, @@ -205,7 +207,7 @@ describe(`Collection`, () => { }), ) rolledBack.rollback() - await rolledBack.isPersisted.promise.catch(() => undefined) + await rolledBack.when(`settled`).catch(() => undefined) expect(collection._state.transactions.size).toBe(0) expect(collection.get(1)).toMatchObject({ id: 1, value: 0 }) @@ -244,7 +246,7 @@ describe(`Collection`, () => { }), ) transaction.rollback() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) } } const walkedForOneMutation = async () => { @@ -278,7 +280,7 @@ describe(`Collection`, () => { for (const name of methods) Reflect.deleteProperty(transactions, name) } transaction.rollback() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) return walked } @@ -315,11 +317,9 @@ describe(`Collection`, () => { ) return transaction }) - // Settle all three before the Collection recomputes once. + // Mark all three failed before the Collection recomputes once. for (const transaction of settled) { transaction.setState(`failed`) - transaction.isPersisted.reject(new Error(`settled`)) - transaction.isPersisted.promise.catch(() => undefined) } collection._state.recomputeOptimisticState(false) @@ -366,7 +366,7 @@ describe(`Collection`, () => { draft.value = 2 }), ) - await first.isPersisted.promise.catch(() => undefined) + await first.when(`settled`).catch(() => undefined) collection._state.recomputeOptimisticState(false) expect(collection._state.transactions.get(`retry`)).toBe(successor) @@ -402,7 +402,7 @@ describe(`Collection`, () => { ) await collection.cleanup() expect(() => transaction.rollback()).not.toThrow() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) expect(transaction.collections.size).toBe(0) expect(collection._state.transactions.size).toBe(0) @@ -442,7 +442,7 @@ describe(`Collection`, () => { }) expect(() => transaction.rollback()).toThrow(`subscriber failed`) - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) expect(second.get(1)).toMatchObject({ id: 1, value: 0 }) expect(second._state.transactions.size).toBe(0) @@ -515,14 +515,14 @@ describe(`Collection`, () => { // Without an explicit temp-key -> server-key mapping, core cannot know // whether key 24 is this optimistic insert's server echo or an unrelated // row, so it must not expose both rows at the same time. - expect(tx.isPersisted.isPending()).toBe(true) + expect(tx.state).toBe(`persisting`) expect(collection.has(24)).toBe(false) expect(getStateEntries(collection)).toEqual([ [4733, { id: 4733, text: `two` }], ]) options.utils.resolveSync() - await tx.isPersisted.promise + await tx.when(`settled`) await flushPromises() expect(getStateEntries(collection)).toEqual([[24, { id: 24, text: `two` }]]) @@ -547,7 +547,7 @@ describe(`Collection`, () => { id: item.id, text: item.text, project_id: item.project_id, - $synced: item.$synced, + $hasPendingWrites: item.$hasPendingWrites, $origin: item.$origin, $key: item.$key, })), @@ -568,17 +568,17 @@ describe(`Collection`, () => { options.utils.commit() options.utils.resolveSync() - await tx.isPersisted.promise + await tx.when(`settled`) await flushPromises() expect( liveCollection.toArray.map((todo) => ({ id: todo.id, - synced: todo.$synced, + hasPendingWrites: todo.$hasPendingWrites, origin: todo.$origin, key: todo.$key, })), - ).toEqual([{ id: 24, synced: true, origin: `remote`, key: 24 }]) + ).toEqual([{ id: 24, hasPendingWrites: false, origin: `remote`, key: 24 }]) }) it(`should throw an error when trying to use mutation operations outside of a transaction`, async () => { @@ -816,7 +816,7 @@ describe(`Collection`, () => { value: `bar`, }) - await tx.isPersisted.promise + await tx.when(`settled`) // @ts-expect-error possibly undefined is ok in test const syncData = syncMock.mock.calls[0][0] @@ -845,7 +845,7 @@ describe(`Collection`, () => { id: 2, value: `baz`, }) - await tx2.isPersisted.promise + await tx2.when(`settled`) // Test bulk insert const tx3 = createTransaction({ mutationFn }) @@ -863,7 +863,7 @@ describe(`Collection`, () => { expect(stripVirtualProps(collection.state.get(keys[3]))).toEqual( bulkData[1], ) - await tx3.isPersisted.promise + await tx3.when(`settled`) const tx4 = createTransaction({ mutationFn }) // Test update with callback @@ -879,7 +879,7 @@ describe(`Collection`, () => { id: 1, value: `bar2`, }) - await tx4.isPersisted.promise + await tx4.when(`settled`) const tx5 = createTransaction({ mutationFn }) // Test update with config and callback @@ -901,7 +901,7 @@ describe(`Collection`, () => { newProp: `new value`, }) - await tx5.isPersisted.promise + await tx5.when(`settled`) // If there are two updates, the second should overwrite the first. const tx55 = createTransaction({ mutationFn }) @@ -933,7 +933,7 @@ describe(`Collection`, () => { }) expect(tx55.mutations).toHaveLength(1) - await tx55.isPersisted.promise + await tx55.when(`settled`) const tx6 = createTransaction({ mutationFn }) // Test bulk update @@ -963,21 +963,21 @@ describe(`Collection`, () => { id: 4, value: `item2-updated`, }) - await tx6.isPersisted.promise + await tx6.when(`settled`) const tx7 = createTransaction({ mutationFn }) // Test delete single item tx7.mutate(() => collection.delete(insertedKey)) expect(collection.state.has(insertedKey)).toBe(false) // objectKeyMap check removed as it no longer exists - await tx7.isPersisted.promise + await tx7.when(`settled`) // Test delete with metadata const tx8Insert = createTransaction({ mutationFn }) tx8Insert.mutate(() => collection.insert({ id: 5, value: `foostyle` })) // @ts-expect-error possibly undefined is ok in test const tx8insertKey = tx8Insert.mutations[0].key - await tx8Insert.isPersisted.promise + await tx8Insert.when(`settled`) const tx8 = createTransaction({ mutationFn }) tx8.mutate(() => collection.delete(tx8insertKey, { @@ -986,7 +986,7 @@ describe(`Collection`, () => { ) expect(tx8.mutations[0]?.metadata).toEqual({ reason: `test delete` }) expect(collection.state.has(tx8insertKey)).toBe(false) - await tx8.isPersisted.promise + await tx8.when(`settled`) // Test bulk delete const tx9 = createTransaction({ mutationFn }) @@ -995,7 +995,7 @@ describe(`Collection`, () => { expect(collection.state.has(keys[2])).toBe(false) // @ts-expect-error possibly undefined is ok in test expect(collection.state.has(keys[3])).toBe(false) - await tx9.isPersisted.promise + await tx9.when(`settled`) }) it(`synced updates should *not* be applied while there's a persisting transaction`, async () => { @@ -1075,7 +1075,7 @@ describe(`Collection`, () => { value: `bar`, }) - await tx1.isPersisted.promise + await tx1.when(`settled`) expect(getStateEntries(collection)).toEqual([[1, { id: 1, value: `bar` }]]) }) @@ -1453,9 +1453,9 @@ describe(`Collection`, () => { // Wait for all transactions to complete await Promise.all([ - insertTx.isPersisted.promise, - updateTx.isPersisted.promise, - deleteTx.isPersisted.promise, + insertTx.when(`settled`), + updateTx.when(`settled`), + deleteTx.when(`settled`), ]) // Verify the transactions were created with the correct configuration @@ -1570,7 +1570,7 @@ describe(`Collection`, () => { // Now resolve the mutation and wait for completion pendingMutations[0]?.() - await nonOptimisticInsertTx.isPersisted.promise + await nonOptimisticInsertTx.when(`settled`) // Now the item should appear after server confirmation expect(collection.state.has(2)).toBe(true) @@ -1594,7 +1594,7 @@ describe(`Collection`, () => { // Now resolve the update mutation and wait for completion pendingMutations[1]?.() - await nonOptimisticUpdateTx.isPersisted.promise + await nonOptimisticUpdateTx.when(`settled`) // Now the update should be reflected expect(getStateValue(collection, 1)?.value).toBe(`non-optimistic update`) @@ -1608,7 +1608,7 @@ describe(`Collection`, () => { // Now resolve the delete mutation and wait for completion pendingMutations[2]?.() - await nonOptimisticDeleteTx.isPersisted.promise + await nonOptimisticDeleteTx.when(`settled`) // Now the item should be gone expect(collection.state.has(2)).toBe(false) @@ -1672,7 +1672,7 @@ describe(`Collection`, () => { value: `default optimistic`, }) - await defaultOptimisticTx.isPersisted.promise + await defaultOptimisticTx.when(`settled`) // Test explicit optimistic: true const explicitOptimisticTx = collection.insert( @@ -1688,7 +1688,7 @@ describe(`Collection`, () => { value: `explicit optimistic`, }) - await explicitOptimisticTx.isPersisted.promise + await explicitOptimisticTx.when(`settled`) // Test optimistic update const optimisticUpdateTx = collection.update( @@ -1703,7 +1703,7 @@ describe(`Collection`, () => { expect(getStateValue(collection, 1)?.value).toBe(`optimistic update`) expect(collection._state.optimisticUpserts.has(1)).toBe(true) - await optimisticUpdateTx.isPersisted.promise + await optimisticUpdateTx.when(`settled`) // Test optimistic delete const optimisticDeleteTx = collection.delete(3, { optimistic: true }) @@ -1712,7 +1712,7 @@ describe(`Collection`, () => { expect(collection.state.has(3)).toBe(false) expect(collection._state.optimisticDeletes.has(3)).toBe(true) - await optimisticDeleteTx.isPersisted.promise + await optimisticDeleteTx.when(`settled`) }) it(`should not block user actions when keys are recently synced`, async () => { @@ -1803,7 +1803,7 @@ describe(`Collection`, () => { // Now complete the first transaction to trigger sync and batching txResolvers[0]?.() - await tx1.isPersisted.promise + await tx1.when(`settled`) // Step 4: More rapid clicks after sync starts (this is where the bug occurred) const eventCountBeforeRapidClicks = changeEvents.length @@ -1824,10 +1824,10 @@ describe(`Collection`, () => { txResolvers[i]?.() } await Promise.all([ - tx2.isPersisted.promise, - tx3.isPersisted.promise, - tx4.isPersisted.promise, - tx5.isPersisted.promise, + tx2.when(`settled`), + tx3.when(`settled`), + tx4.when(`settled`), + tx5.when(`settled`), ]) }) diff --git a/packages/db/tests/conformance-result-laws.test.ts b/packages/db/tests/conformance-result-laws.test.ts index 0f6564a62c..eeff59a12d 100644 --- a/packages/db/tests/conformance-result-laws.test.ts +++ b/packages/db/tests/conformance-result-laws.test.ts @@ -33,7 +33,6 @@ it(`separates only the five documented virtual fields from selected values`, () const actual = rows.map((row) => ({ ...row, $hasPendingWrites: false, - $synced: true, $origin: `remote`, $key: row.id, $collectionId: `source`, diff --git a/packages/db/tests/conformance/contract.ts b/packages/db/tests/conformance/contract.ts index 7e28c9cc7f..d9ea2dcc34 100644 --- a/packages/db/tests/conformance/contract.ts +++ b/packages/db/tests/conformance/contract.ts @@ -92,7 +92,7 @@ export interface DbOps { createOptimisticAction: (config: { onMutate: (variables: any) => void mutationFn: (variables: any) => Promise - }) => (variables?: any) => { isPersisted: { promise: Promise } } + }) => (variables?: any) => { when: (state: 'settled') => Promise } } /** Normalized, adapter-agnostic view of a live query's current result. */ diff --git a/packages/db/tests/conformance/result-laws.ts b/packages/db/tests/conformance/result-laws.ts index 437364263d..c7bf036695 100644 --- a/packages/db/tests/conformance/result-laws.ts +++ b/packages/db/tests/conformance/result-laws.ts @@ -20,13 +20,7 @@ export function selectedRow(row: unknown): Record { expect(row).not.toBeNull() expect(typeof row).toBe(`object`) const value = row as Record - const virtual = [ - `$hasPendingWrites`, - `$synced`, - `$origin`, - `$key`, - `$collectionId`, - ] + const virtual = [`$hasPendingWrites`, `$origin`, `$key`, `$collectionId`] return Object.fromEntries( Reflect.ownKeys(value) // Solid stores nonenumerable bookkeeping symbols on its row wrappers. diff --git a/packages/db/tests/conformance/suite.ts b/packages/db/tests/conformance/suite.ts index a3791fc35d..3cfbd26fca 100644 --- a/packages/db/tests/conformance/suite.ts +++ b/packages/db/tests/conformance/suite.ts @@ -951,17 +951,17 @@ export function runSuite(rawDriver: LiveQueryDriver) { }, }) - let tx!: { isPersisted: { promise: Promise } } + let tx!: { when: (state: 'settled') => Promise } await h.apply(() => { tx = add() - void tx.isPersisted.promise.catch(() => undefined) - lifetime!.defer(() => tx.isPersisted.promise) + void tx.when(`settled`).catch(() => undefined) + lifetime!.defer(() => tx.when(`settled`)) }) // Optimistic row is visible before the server confirms. expect(h.current().data.find((r: any) => r.id === `temp`)).toBeDefined() confirmServer() - await tx.isPersisted.promise + await tx.when(`settled`) await h.flush() // Reconciled: temp replaced by the permanent key. expect( diff --git a/packages/db/tests/db-client-hydration-authority-oracle.test.ts b/packages/db/tests/db-client-hydration-authority-oracle.test.ts index 748e131956..7a0aadd598 100644 --- a/packages/db/tests/db-client-hydration-authority-oracle.test.ts +++ b/packages/db/tests/db-client-hydration-authority-oracle.test.ts @@ -422,7 +422,7 @@ describe(`DbClient hydration interleaving oracle`, () => { void Promise.resolve(receipt).catch(() => undefined) } release() - await mutation?.isPersisted.promise.catch(() => undefined) + await mutation?.when(`settled`).catch(() => undefined) expect( observedRows(collection.values()).filter((row) => row.id !== `local`), ).toEqual(expectedRows(history)) diff --git a/packages/db/tests/db-client.test.ts b/packages/db/tests/db-client.test.ts index c3a103af6f..a97291fac1 100644 --- a/packages/db/tests/db-client.test.ts +++ b/packages/db/tests/db-client.test.ts @@ -61,7 +61,7 @@ describe(`DbClient`, () => { const peopleB = clientB.collection(descriptor) const transaction = peopleA.insert(people[0]!) - await transaction.isPersisted.promise + await transaction.when(`settled`) expect(peopleA.get(`1`)).toMatchObject(people[0]!) expect(peopleB.get(`1`)).toBeUndefined() @@ -96,7 +96,7 @@ describe(`DbClient`, () => { autoCommit: false, mutationFn: async () => {}, }) - const rolledBack = transactionA.isPersisted.promise.catch(() => undefined) + const rolledBack = transactionA.when(`settled`).catch(() => undefined) let transactionB: ReturnType | undefined transactionA.mutate(() => { @@ -107,7 +107,7 @@ describe(`DbClient`, () => { expect(clientB.activeTransaction).toBeUndefined() }) - await transactionB!.isPersisted.promise + await transactionB!.when(`settled`) transactionA.rollback() await rolledBack @@ -130,7 +130,7 @@ describe(`DbClient`, () => { autoCommit: false, mutationFn: async () => {}, }) - const rolledBack = transaction.isPersisted.promise.catch(() => undefined) + const rolledBack = transaction.when(`settled`).catch(() => undefined) transaction.mutate(() => { expect(peopleA.insert(people[0]!)).toBe(transaction) @@ -1235,7 +1235,7 @@ describe(`DbClient`, () => { }) collection.utils.resolveSync() - await tx.isPersisted.promise + await tx.when(`settled`) }) it(`applies initialData precedence before hydrated rows`, () => { diff --git a/packages/db/tests/error-sample-arguments.ts b/packages/db/tests/error-sample-arguments.ts index 0f658be69b..7981837e83 100644 --- a/packages/db/tests/error-sample-arguments.ts +++ b/packages/db/tests/error-sample-arguments.ts @@ -108,7 +108,7 @@ export const errorSampleArguments: Record>> = { ['alias', 'collection-1', ['key1', 'key2']], ['alias'], ], - DuplicateAliasInSubqueryError: [['alias', ['parent1', 'parent2']]], + // Error 57 is retired. Keep its code and documentation for older logs. UnsupportedFromTypeError: [['string']], UnknownExpressionTypeError: [['custom']], EmptyReferencePathError: [[]], diff --git a/packages/db/tests/indexed-db/api.test.ts b/packages/db/tests/indexed-db/api.test.ts index f34aec5d04..1fb6ee3670 100644 --- a/packages/db/tests/indexed-db/api.test.ts +++ b/packages/db/tests/indexed-db/api.test.ts @@ -64,7 +64,7 @@ it('validates schema inputs before persistence', async () => { try { await collection.preload() expect(() => collection.insert({ id: 1, name: '' })).toThrow() - await collection.insert({ id: 1, name: 'valid' }).isPersisted.promise + await collection.insert({ id: 1, name: 'valid' }).when(`settled`) expect(await collection.utils.exportData()).toEqual([ { id: 1, name: 'valid' }, ]) @@ -78,7 +78,7 @@ it('supports local persistence when BroadcastChannel is unavailable', async () = await withHarness(async (h) => { vi.stubGlobal('BroadcastChannel', undefined) const collection = await h.open() - await collection.insert({ id: 1, name: 'local' }).isPersisted.promise + await collection.insert({ id: 1, name: 'local' }).when(`settled`) expect(await collection.utils.exportData()).toEqual([ { id: 1, name: 'local' }, ]) @@ -99,7 +99,7 @@ it('rejects opening without IndexedDB', async () => { it('exposes deletion only as an administrative function', async () => { await withHarness(async (h) => { const collection = await h.open() - await collection.insert({ id: 1, name: 'removed' }).isPersisted.promise + await collection.insert({ id: 1, name: 'removed' }).when(`settled`) expect(collection.utils).not.toHaveProperty('deleteDatabase') await deleteDatabase(h.db.name) const reopened = await createIndexedDB({ @@ -250,8 +250,9 @@ it('preserves exported transformed rows when invalid restore input is rejected', ) try { await c.preload() - await c.insert({ id: 1, date: '2026-01-01T00:00:00.000Z' }).isPersisted - .promise + await c + .insert({ id: 1, date: '2026-01-01T00:00:00.000Z' }) + .when(`settled`) const backup = await c.utils.exportData() // @ts-expect-error Schema output is not the string input expected by import. await expect(c.utils.importData(backup)).rejects.toThrow( diff --git a/packages/db/tests/indexed-db/compatibility-oracle.test.ts b/packages/db/tests/indexed-db/compatibility-oracle.test.ts index 42695a15b6..9d62211c70 100644 --- a/packages/db/tests/indexed-db/compatibility-oracle.test.ts +++ b/packages/db/tests/indexed-db/compatibility-oracle.test.ts @@ -49,7 +49,7 @@ for (const capability of ['randomUUID', 'getRandomValues'] as const) { { id: 0, name: 'number' }, { id: '1', name: 'string' }, ] - await writer.insert(expected).isPersisted.promise + await writer.insert(expected).when(`settled`) expect(Channel.sent, 'one completed write notification').toHaveLength(1) expect(await Channel.deliver(), 'receiver path reached').toBe(1) assertRows(writer.values(), expected, 'writer after persistence') @@ -84,7 +84,7 @@ for (const retired of ['first', 'second'] as const) { await first.preload() await second.preload() const expected: Array = [{ id: 1, name: 'before cleanup' }] - await first.insert(expected[0]!).isPersisted.promise + await first.insert(expected[0]!).when(`settled`) expect( await Channel.deliver(), 'second client receives first write', @@ -102,7 +102,7 @@ for (const retired of ['first', 'second'] as const) { 'only the retired sync run releases its channel', ).toBe(1) const added: Row = { id: '1', name: 'after cleanup' } - await survivor.insert(added).isPersisted.promise + await survivor.insert(added).when(`settled`) expected.push(added) assertRows(survivor.values(), expected, 'survivor write') assertRows( @@ -115,7 +115,7 @@ for (const retired of ['first', 'second'] as const) { await restored.preload() assertRows(restored.values(), expected, 'third client restore') const suffix: Row = { id: 2, name: 'new peer' } - await survivor.insert(suffix).isPersisted.promise + await survivor.insert(suffix).when(`settled`) expected.push(suffix) expect( await Channel.deliver(), @@ -282,8 +282,7 @@ for (const idLocation of ['options', 'collection'] as const) { h.collections.push(c) await c.preload() const expected = [{ id: 1, name: 'owned' }] - if (entry === 'automatic') - await c.insert(expected[0]!).isPersisted.promise + if (entry === 'automatic') await c.insert(expected[0]!).when(`settled`) else { const tx = createTransaction({ autoCommit: false, @@ -357,9 +356,7 @@ for (const sharedId of [false, true]) { } }, }) - const outcome = tx.isPersisted.promise.catch( - (error: unknown) => error, - ) + const outcome = tx.when(`settled`).catch((error: unknown) => error) tx.mutate(() => collections.forEach((collection, index) => collection.insert(rows[index]!), @@ -452,14 +449,16 @@ for (const name of legalNames) { // Separate transactions isolate name routing from the already tracked // core same-transaction numeric/string key collision (HC005). for (const row of rows) - await writers[index]!.insert({ ...row }).isPersisted.promise + await writers[index]!.insert({ ...row }).when(`settled`) expected.set(store, rows) } await checkpoint() const versions = await readStore(db, '_versions') - await writers[0]!.update(0, (draft) => { - draft.name = 'changed' - }).isPersisted.promise + await writers[0]! + .update(0, (draft) => { + draft.name = 'changed' + }) + .when(`settled`) expected.set(name, [ { id: 0, name: 'changed' }, { id: '0', name: name + ':string' }, @@ -570,9 +569,8 @@ for (const count of [2, 3]) { } const rows = collections.map((_, id) => ({ id, name: `writer-${id}` })) await Promise.all( - collections.map( - (collection, index) => - collection.insert({ ...rows[index]! }).isPersisted.promise, + collections.map((collection, index) => + collection.insert({ ...rows[index]! }).when(`settled`), ), ) await Channel.deliver() diff --git a/packages/db/tests/indexed-db/cross-tab-boundary-oracle.test.ts b/packages/db/tests/indexed-db/cross-tab-boundary-oracle.test.ts index 0f8e991d0b..5101254d2b 100644 --- a/packages/db/tests/indexed-db/cross-tab-boundary-oracle.test.ts +++ b/packages/db/tests/indexed-db/cross-tab-boundary-oracle.test.ts @@ -65,8 +65,8 @@ async function omission(fault: 'none' | 'partial' | 'error') { await receiver.preload() const record = recordCollection(receiver, 'reader') h.disposers.push(record.stop) - await writer.delete(0).isPersisted.promise - await writer.insert({ ...after }).isPersisted.promise + await writer.delete(0).when(`settled`) + await writer.insert({ ...after }).when(`settled`) const receipts = [] while (Channel.pending.length) { const receipt = Channel.dispatch() @@ -171,7 +171,7 @@ for (const abort of [false, true]) { const record = recordCollection(receiver, 'receiver') h.disposers.push(record.stop) const row = { id: 1, name: 'durable before cleanup' } - await writer.insert(row).isPersisted.promise + await writer.insert(row).when(`settled`) const gate = holdStore(h.db.db) h.disposers.push(gate.release) await gate.started @@ -192,9 +192,11 @@ for (const abort of [false, true]) { 'obsolete receiver completion', ) assertSnapshot(receiver.values(), [row], 'new sync run restore') - await writer.update(1, (draft) => { - draft.name = 'later version' - }).isPersisted.promise + await writer + .update(1, (draft) => { + draft.name = 'later version' + }) + .when(`settled`) await Channel.drain() assertSnapshot( receiver.values(), @@ -212,7 +214,7 @@ it('reports an active receiver abort through Collection status while its callbac const receiver = await h.open('items', { db: descriptor }) const record = recordCollection(receiver, 'receiver') h.disposers.push(record.stop) - await writer.insert({ id: 1, name: 'durable' }).isPersisted.promise + await writer.insert({ id: 1, name: 'durable' }).when(`settled`) const gate = holdStore(h.db.db) h.disposers.push(gate.release) await gate.started @@ -276,7 +278,7 @@ for (const deliver of [false, true]) await seed(h.db, 'items', [before]) const a = await h.open(), b = await h.open('items', { db: await h.connect() }) - await a.delete(0).isPersisted.promise + await a.delete(0).when(`settled`) if (deliver) { await Channel.drain() assertSnapshot(b.values(), [], 'delivered admission') @@ -287,9 +289,11 @@ for (const deliver of [false, true]) ).toThrow() } else { assertSnapshot(b.values(), [before], 'stale admission') - await b.update(0, (row) => { - row.name = 'updated' - }).isPersisted.promise + await b + .update(0, (row) => { + row.name = 'updated' + }) + .when(`settled`) await Channel.drain() assertSnapshot( a.values(), diff --git a/packages/db/tests/indexed-db/cross-tab-driver.ts b/packages/db/tests/indexed-db/cross-tab-driver.ts index a8e03ef91e..5221a512fc 100644 --- a/packages/db/tests/indexed-db/cross-tab-driver.ts +++ b/packages/db/tests/indexed-db/cross-tab-driver.ts @@ -85,16 +85,18 @@ export async function runTransportHistory( else if (operation === 'import') await subject.utils.importData(structuredClone(change.rows)) else if (operation === 'delete') - await subject.delete(change.deletes[0]!).isPersisted.promise + await subject.delete(change.deletes[0]!).when(`settled`) else { const row = change.rows[0]! if (before.some((item) => item.id === row.id)) - await subject.update(row.id, (draft) => { - draft.name = row.name - if (row.optional === undefined) delete draft.optional - else draft.optional = row.optional - }).isPersisted.promise - else await subject.insert(structuredClone(row)).isPersisted.promise + await subject + .update(row.id, (draft) => { + draft.name = row.name + if (row.optional === undefined) delete draft.optional + else draft.optional = row.optional + }) + .when(`settled`) + else await subject.insert(structuredClone(row)).when(`settled`) } model.accept(tab, change) for (const message of Channel.pending) diff --git a/packages/db/tests/indexed-db/cross-tab-oracle.ts b/packages/db/tests/indexed-db/cross-tab-oracle.ts index dd0df9dcc1..b5ba150883 100644 --- a/packages/db/tests/indexed-db/cross-tab-oracle.ts +++ b/packages/db/tests/indexed-db/cross-tab-oracle.ts @@ -54,7 +54,6 @@ export function plain(row: OracleRow): OracleRow { $key: _key, $origin: _origin, $hasPendingWrites: _pending, - $synced: _synced, $collectionId: _id, ...value } = row as OracleRow & Record diff --git a/packages/db/tests/indexed-db/harness.ts b/packages/db/tests/indexed-db/harness.ts index cbf5b78831..8286201fdc 100644 --- a/packages/db/tests/indexed-db/harness.ts +++ b/packages/db/tests/indexed-db/harness.ts @@ -112,7 +112,6 @@ export function userRow(value: Row): Row { $key: _key, $origin: _origin, $hasPendingWrites: _pending, - $synced: _synced, $collectionId: _collectionId, ...row } = value as Row & Record diff --git a/packages/db/tests/indexed-db/local-write-order-oracle.test.ts b/packages/db/tests/indexed-db/local-write-order-oracle.test.ts index e4d3e32228..bb8f390a04 100644 --- a/packages/db/tests/indexed-db/local-write-order-oracle.test.ts +++ b/packages/db/tests/indexed-db/local-write-order-oracle.test.ts @@ -100,7 +100,7 @@ for (const [kind, effects] of Object.entries(grammars)) { : c.insert({ ...effect.row }) : c.delete(effect.key) outcomes.push( - tx.isPersisted.promise.then( + tx.when(`settled`).then( () => { succeeded.add(index) return true @@ -124,11 +124,12 @@ for (const [kind, effects] of Object.entries(grammars)) { ? [peerRow, initial[1]!] : initial if (peerAction === 'update') - await peer.update(1, (row) => { - row.name = 'peer' - }).isPersisted.promise - if (peerAction === 'delete') - await peer.delete(1).isPersisted.promise + await peer + .update(1, (row) => { + row.name = 'peer' + }) + .when(`settled`) + if (peerAction === 'delete') await peer.delete(1).when(`settled`) if (peerAction === 'replace') await peer.utils.importData([peerRow]) await Channel.deliver() assertRows( @@ -214,13 +215,13 @@ it('holds a later accepted update until the earlier handler settles', async () = const a = c.update(1, (row) => { row.name = 'A' }), - aOutcome = a.isPersisted.promise + aOutcome = a.when(`settled`) await entered.promise const b = c.update(1, (row) => { row.name = 'B' }) let settled = false - const bOutcome = b.isPersisted.promise.then(() => { + const bOutcome = b.when(`settled`).then(() => { settled = true }) await later.promise @@ -251,9 +252,11 @@ it('reserves mutation order before a handler authors another update', async () = onUpdate: async () => { if (!reentered) { reentered = true - later = c.update(1, (row) => { - row.name = 'B' - }).isPersisted.promise + later = c + .update(1, (row) => { + row.name = 'B' + }) + .when(`settled`) return gate.promise } return undefined @@ -264,7 +267,7 @@ it('reserves mutation order before a handler authors another update', async () = }) const held = await readStore(h.db, 'items') gate.resolve() - await Promise.all([first.isPersisted.promise, later]) + await Promise.all([first.when(`settled`), later]) expect(reentered).toBe(true) assertRows(held.rows, initial, 'reentered handler held') assertRows( @@ -285,7 +288,7 @@ for (const sameKey of [false, true]) { const c = await h.open() const peer = await h.open('items', { db: await h.connect() }) const first = c.insert({ id: 1, name: 'invalid', unsupported: () => {} }) - const rejected = first.isPersisted.promise.then( + const rejected = first.when(`settled`).then( () => false, () => true, ) @@ -295,7 +298,7 @@ for (const sameKey of [false, true]) { delete row.unsupported }) : c.insert({ id: 2, name: 'valid' }) - await next.isPersisted.promise + await next.when(`settled`) expect(await rejected, 'native failure reaches the earlier caller').toBe( true, ) diff --git a/packages/db/tests/indexed-db/pending-history-oracle.test.ts b/packages/db/tests/indexed-db/pending-history-oracle.test.ts index f921ac1776..bb7e96376f 100644 --- a/packages/db/tests/indexed-db/pending-history-oracle.test.ts +++ b/packages/db/tests/indexed-db/pending-history-oracle.test.ts @@ -174,7 +174,6 @@ async function run( row.id, { pending: row.$hasPendingWrites, - synced: row.$synced, origin: row.$origin, }, ]), @@ -211,7 +210,6 @@ async function run( const metadata = observation.metadata.get(row.id)! expect(metadata, `${label} metadata ${row.id}`).toEqual({ pending, - synced: !pending, origin: pending ? 'local' : origins.get(row.id), }) } @@ -242,7 +240,7 @@ async function run( if (metadata) metadata.origin = 'remote' } } - const outcome = tx.isPersisted.promise.then( + const outcome = tx.when(`settled`).then( () => { captureSettlement() return undefined @@ -302,14 +300,15 @@ async function run( await peer.utils.importData(structuredClone(change.rows)) else if (step.kind === 'delete') { if (!durable.some((row) => row.id === change.deletes[0])) continue - await peer.delete(change.deletes[0]!).isPersisted.promise + await peer.delete(change.deletes[0]!).when(`settled`) } else if (durable.some((row) => row.id === id)) - await peer.update(id, (row) => { - row.name = `peer-${step.value}` - delete row.optional - }).isPersisted.promise - else - await peer.insert(structuredClone(change.rows[0]!)).isPersisted.promise + await peer + .update(id, (row) => { + row.name = `peer-${step.value}` + delete row.optional + }) + .when(`settled`) + else await peer.insert(structuredClone(change.rows[0]!)).when(`settled`) // Collection updates that leave every authored value unchanged author no // mutation, so they provide no source acknowledgement or notification. if ( @@ -367,7 +366,7 @@ async function run( 'settled durable', ) const suffix = { id: 'suffix', name: 'ordinary suffix' } - await subject.insert(suffix).isPersisted.promise + await subject.insert(suffix).when(`settled`) await Channel.drain() durable = apply(durable, { rows: [suffix], deletes: [], replace: false }) assertSnapshot(peer.values(), durable, 'suffix peer') diff --git a/packages/db/tests/indexed-db/persistence-oracle.test.ts b/packages/db/tests/indexed-db/persistence-oracle.test.ts index 9b59fe8c47..454d429019 100644 --- a/packages/db/tests/indexed-db/persistence-oracle.test.ts +++ b/packages/db/tests/indexed-db/persistence-oracle.test.ts @@ -170,17 +170,17 @@ async function runHistory(steps: Array) { } switch (action.kind) { case 'write': - await write(slot, row).isPersisted.promise + await write(slot, row).when(`settled`) remember(slot, row) break case 'delete': { if (!model[slot]!.length) { - await collection.insert({ ...row }).isPersisted.promise + await collection.insert({ ...row }).when(`settled`) remember(slot, row) await Channel.deliver() } const removed = model[slot]![action.key % model[slot]!.length]! - await collection.delete(removed.id).isPersisted.promise + await collection.delete(removed.id).when(`settled`) model[slot] = model[slot]!.filter((item) => item.id !== removed.id) touched.add(JSON.stringify([names[slot], removed.id])) break @@ -213,7 +213,7 @@ async function runHistory(steps: Array) { write(0, row) write(1, other) }) - await tx.isPersisted.promise + await tx.when(`settled`) remember(0, row) remember(1, other) break @@ -436,7 +436,7 @@ it('manual acceptance partitions mixed payloads across three Collections in ever } }, }) - const outcome = tx.isPersisted.promise + const outcome = tx.when(`settled`) void outcome.catch(() => undefined) tx.mutate(() => { for (const [index, collection] of collections.entries()) { @@ -488,9 +488,11 @@ for (const timestamp of [undefined, -1, Number.MAX_SAFE_INTEGER]) { const source = await h.open() const peer = await h.open('items', { db: await h.connect() }) assertRows(source.values(), initial, 'legacy restore') - await source.update(1, (row) => { - row.name = 'after' - }).isPersisted.promise + await source + .update(1, (row) => { + row.name = 'after' + }) + .when(`settled`) await Channel.deliver() const after = [{ id: 1, name: 'after' }, initial[1]!] assertRows( diff --git a/packages/db/tests/indexed-db/persistence-values-oracle.test.ts b/packages/db/tests/indexed-db/persistence-values-oracle.test.ts index 1374034960..8797e4ffee 100644 --- a/packages/db/tests/indexed-db/persistence-values-oracle.test.ts +++ b/packages/db/tests/indexed-db/persistence-values-oracle.test.ts @@ -59,15 +59,19 @@ for (const kind of valueKinds) { expected, ) } - await writer.insert({ - id: 1, - name: 'initial', - nested: { value: createValue(kind) }, - }).isPersisted.promise + await writer + .insert({ + id: 1, + name: 'initial', + nested: { value: createValue(kind) }, + }) + .when(`settled`) await checkpoint('insert') - await writer.update(1, (draft) => { - draft.name = 'updated' - }).isPersisted.promise + await writer + .update(1, (draft) => { + draft.name = 'updated' + }) + .when(`settled`) expected = expectedValueRows(kind, 'updated') await checkpoint('unrelated update') const exported = await writer.utils.exportData() @@ -91,7 +95,7 @@ for (const kind of valueKinds) { })) const outcome = entry === 'insert' - ? writer.insert(batch).isPersisted.promise + ? writer.insert(batch).when(`settled`) : writer.utils.importData(batch) await expect(outcome).rejects.toBeDefined() await checkpoint(`${entry} rejection at ${badIndex}`) @@ -101,9 +105,11 @@ for (const kind of valueKinds) { ).toEqual(before) } } - await writer.update(1, (draft) => { - draft.name = 'suffix' - }).isPersisted.promise + await writer + .update(1, (draft) => { + draft.name = 'suffix' + }) + .when(`settled`) expected = expectedValueRows(kind, 'suffix') await checkpoint('successful suffix') const freshDb = await createIndexedDB({ @@ -176,8 +182,9 @@ for (const kind of valueKinds.filter((value) => value !== 'blob')) { const c = make(h.db) const peer = make(await h.connect()) await Promise.all([c.preload(), peer.preload()]) - await c.insert({ id: 1, name: 'before', nested: { value: null } }) - .isPersisted.promise + await c + .insert({ id: 1, name: 'before', nested: { value: null } }) + .when(`settled`) await Channel.deliver() const gate = holdStore(h.db.db) h.disposers.push(() => gate.release()) @@ -187,10 +194,12 @@ for (const kind of valueKinds.filter((value) => value !== 'blob')) { const pending = entry === 'import' ? c.utils.importData([row]) - : c.update(1, (draft) => { - draft.name = row.name - draft.nested = row.nested - }).isPersisted.promise + : c + .update(1, (draft) => { + draft.name = row.name + draft.nested = row.nested + }) + .when(`settled`) // Date, buffers, views and nested containers each have a native mutation // that preserves their shape but changes content. A shape-only check // would miss the bug; the independent byte/timestamp description cannot. diff --git a/packages/db/tests/indexed-db/retirement-oracle.test.ts b/packages/db/tests/indexed-db/retirement-oracle.test.ts index 38deefef7f..ba789aacf5 100644 --- a/packages/db/tests/indexed-db/retirement-oracle.test.ts +++ b/packages/db/tests/indexed-db/retirement-oracle.test.ts @@ -82,11 +82,13 @@ function start(c: Subject, kind: Kind, owner: number): Promise { if (kind === 'clear') return c.utils.clearObjectStore() if (kind === 'import') return c.utils.importData(apply([], kind, owner)) if (kind === 'insert') - return c.insert(apply([], kind, owner)[0]!).isPersisted.promise - if (kind === 'delete') return c.delete(owner).isPersisted.promise - return c.update(owner, (row) => { - row.name = `after-${owner}` - }).isPersisted.promise + return c.insert(apply([], kind, owner)[0]!).when(`settled`) + if (kind === 'delete') return c.delete(owner).when(`settled`) + return c + .update(owner, (row) => { + row.name = `after-${owner}` + }) + .when(`settled`) } function recordStatus(c: Subject) { const statuses: Array = [] @@ -333,7 +335,7 @@ for (const reason of reasons) { tx.mutate(() => { void start(c, kind, 1) }) - const caller = observe(tx.isPersisted.promise, () => + const caller = observe(tx.when(`settled`), () => sorted(c.base.values()), ) await accepted.promise @@ -379,9 +381,11 @@ for (const reason of reasons) { if (read === 'replacement') await sender.utils.importData([{ id: 3, name: 'new snapshot' }]) else - await sender.update(1, (row) => { - row.name = 'peer update' - }).isPersisted.promise + await sender + .update(1, (row) => { + row.name = 'peer update' + }) + .when(`settled`) } const gate = holdStore(h.db.db) h.disposers.push(() => gate.release()) @@ -552,7 +556,7 @@ it.each(mixedCells)( tx.mutate(() => { void start(c, op.kind, slot + 1) }) - callers[slot] = observe(tx.isPersisted.promise, capture(slot)) + callers[slot] = observe(tx.when(`settled`), capture(slot)) await accepted.promise durable = apply(durable, op.kind, slot + 1) assertRows( @@ -694,7 +698,7 @@ for (const reason of reasons) const native = observeTransactions(h.db.db) h.disposers.push(() => native.restore()) const row = { id: 3, name: 'admitted during startup' } - const caller = observe(c.insert(row).isPersisted.promise, () => + const caller = observe(c.insert(row).when(`settled`), () => sorted(c.base.values()), ) await vi.waitFor(() => diff --git a/packages/db/tests/indexed-db/settlement-oracle.test.ts b/packages/db/tests/indexed-db/settlement-oracle.test.ts index 228d96d6ce..7b686483a5 100644 --- a/packages/db/tests/indexed-db/settlement-oracle.test.ts +++ b/packages/db/tests/indexed-db/settlement-oracle.test.ts @@ -86,7 +86,7 @@ describe('application decision precedes durability', () => { }) : collection.delete(1) let settled = false - const outcome = tx.isPersisted.promise.then( + const outcome = tx.when(`settled`).then( () => { settled = true return undefined @@ -129,7 +129,7 @@ describe('application decision precedes durability', () => { // separate cut because clearing the store could otherwise hide it. const suffix = { id: 3, name: 'suffix' } const afterSuffix = [...expected, suffix] - await collection.insert({ ...suffix }).isPersisted.promise + await collection.insert({ ...suffix }).when(`settled`) await Channel.deliver() assertRows( collection.values(), @@ -184,7 +184,7 @@ describe('atomic persistence and truthful settlement', () => { const beforeVersions = await readStore(h.db, '_versions') let promise: Promise if (entry === 'automatic') - promise = collection.insert(rows).isPersisted.promise + promise = collection.insert(rows).when(`settled`) else if (entry === 'import') promise = collection.utils.importData(rows) else { @@ -193,7 +193,7 @@ describe('atomic persistence and truthful settlement', () => { collection.utils.acceptMutations(transaction), }) tx.mutate(() => collection.insert(rows)) - promise = tx.isPersisted.promise + promise = tx.when(`settled`) } const outcome = await promise.then( () => 'fulfilled', @@ -216,7 +216,7 @@ describe('atomic persistence and truthful settlement', () => { before, 'failure cannot publish a peer change', ) - await collection.insert({ id: 5, name: 'suffix' }).isPersisted.promise + await collection.insert({ id: 5, name: 'suffix' }).when(`settled`) await Channel.deliver() assertRows( (await h.open()).values(), @@ -257,7 +257,7 @@ describe('atomic persistence and truthful settlement', () => { row.name = 'new' }) : collection.delete(1) - const outcome = await tx.isPersisted.promise.then( + const outcome = await tx.when(`settled`).then( () => 'fulfilled', () => 'rejected', ) @@ -288,10 +288,10 @@ for (const remote of ['delete', 'clear'] as const) { autoCommit: false, mutationFn: async () => {}, }) - const done = tx.isPersisted.promise.catch(() => undefined) + const done = tx.when(`settled`).catch(() => undefined) tx.mutate(() => receiver.delete(1)) expect(receiver.has(1), 'pending delete reached').toBe(false) - if (remote === 'delete') await source.delete(1).isPersisted.promise + if (remote === 'delete') await source.delete(1).when(`settled`) else await source.utils.clearObjectStore() expect( await Channel.deliver(), @@ -435,18 +435,19 @@ for (const entry of ['import', 'insert', 'update', 'delete'] as const) { }, ) if (entry === 'import') await c.utils.importData(authored) - else if (entry === 'insert') - await c.insert(authored).isPersisted.promise + else if (entry === 'insert') await c.insert(authored).when(`settled`) else if (entry === 'update') - await c.update( - authored.map((row) => row.id), - (drafts) => { - drafts.forEach((draft) => { - draft.name = String(draft.id) - }) - }, - ).isPersisted.promise - else await c.delete(authored.map((row) => row.id)).isPersisted.promise + await c + .update( + authored.map((row) => row.id), + (drafts) => { + drafts.forEach((draft) => { + draft.name = String(draft.id) + }) + }, + ) + .when(`settled`) + else await c.delete(authored.map((row) => row.id)).when(`settled`) const expected = entry === 'delete' ? [] : authored expect(issued).toBe(2 * count) expect(beforeFirstSuccess).toBe(2 * count) diff --git a/packages/db/tests/indexed-db/transport-oracle.test.ts b/packages/db/tests/indexed-db/transport-oracle.test.ts index cd3399bc3b..b650513e7c 100644 --- a/packages/db/tests/indexed-db/transport-oracle.test.ts +++ b/packages/db/tests/indexed-db/transport-oracle.test.ts @@ -54,8 +54,8 @@ for (const reverse of [false, true]) { { id: '0', name: 'typed key' }, ] await Promise.all([ - left.insert([rows[0]!, rows[2]!]).isPersisted.promise, - right.insert(rows[1]!).isPersisted.promise, + left.insert([rows[0]!, rows[2]!]).when(`settled`), + right.insert(rows[1]!).when(`settled`), ]) expect(Channel.pending.length).toBe(2) // Redelivery retains the original messages, making duplicate output @@ -65,13 +65,14 @@ for (const reverse of [false, true]) { expect(await Channel.deliver()).toBe(4) for (const subject of [left, right, await h.open()]) assertRows(subject.values(), rows, 'disjoint writer convergence') - await left.update(rows[0]!.id, (row) => { - delete row.optional - }).isPersisted.promise - await left.delete(rows[2]!.id).isPersisted.promise - await left.insert({ id: 'temporary', name: 'removed' }).isPersisted - .promise - await left.delete('temporary').isPersisted.promise + await left + .update(rows[0]!.id, (row) => { + delete row.optional + }) + .when(`settled`) + await left.delete(rows[2]!.id).when(`settled`) + await left.insert({ id: 'temporary', name: 'removed' }).when(`settled`) + await left.delete('temporary').when(`settled`) await Channel.deliver() const expected = [{ id: rows[0]!.id, name: 'left' }, rows[1]!] for (const subject of [left, right, await h.open()]) @@ -88,7 +89,7 @@ for (const reverse of [false, true]) { it('ignores self, foreign database and foreign store notifications', async () => { await withHarness(async (h) => { const collection = await h.open() - await collection.insert({ id: 1, name: 'kept' }).isPersisted.promise + await collection.insert({ id: 1, name: 'kept' }).when(`settled`) const original = Channel.sent[0] as { tabId: string database: string @@ -181,7 +182,7 @@ for (const replacement of ['clear', 'import'] as const) { if (replacement === 'clear') await source.utils.clearObjectStore() else await source.utils.importData([imported]) const inserted = { id: 'later', name: 'accepted after replacement' } - await receiver.insert(inserted).isPersisted.promise + await receiver.insert(inserted).when(`settled`) const expected = replacement === 'clear' ? [inserted] : [imported, inserted] const pending = Channel.pending.length @@ -277,7 +278,7 @@ it('publishes manual acceptance after the writer sync run is cleaned up', async 'cleaned-up', ) await transaction.commit() - await transaction.isPersisted.promise + await transaction.when(`settled`) const delivered = await Channel.deliver() assertRows( (await readStore(h.db, 'items')).rows, @@ -669,8 +670,8 @@ for (const count of [2, 3]) { name: 'written through new descriptor', } const addedRow = { id: 'added', name: 'written in new store' } - await restored.insert({ ...suffix }).isPersisted.promise - await added.insert({ ...addedRow }).isPersisted.promise + await restored.insert({ ...suffix }).when(`settled`) + await added.insert({ ...addedRow }).when(`settled`) await Channel.deliver() assertRows( restored.values(), @@ -735,10 +736,12 @@ for (const metadata of ['none', 'mixed', 'all'] as const) { const receiver = await h.open() const sender = await h.open('items', { db: await h.connect() }) if (action === 'update') - await sender.update(key, (row) => { - row.name = 'new' - }).isPersisted.promise - else await sender.delete(key).isPersisted.promise + await sender + .update(key, (row) => { + row.name = 'new' + }) + .when(`settled`) + else await sender.delete(key).when(`settled`) // Repeat delivery: missing versions cannot turn an update into a second // insert, and repeated deletes must leave the independent anchor intact. Channel.pending.push(...Channel.pending) @@ -845,7 +848,7 @@ it('ignores an old native deletion receipt after database recreation', async () }) h.descriptors.push(recreated) const fresh = await h.open('items', { db: recreated }) - await fresh.insert({ id: 1, name: 'new incarnation' }).isPersisted.promise + await fresh.insert({ id: 1, name: 'new incarnation' }).when(`settled`) Channel.pending = [] receipt.resolve() await deletion @@ -881,7 +884,7 @@ it('reports connection closure immediately and supports fresh Collections', asyn expect(old.status).toBe('error') expect(live.status).toBe('error') const fresh = await h.open('items', { db: upgraded }) - await fresh.insert({ id: 1, name: 'new' }).isPersisted.promise + await fresh.insert({ id: 1, name: 'new' }).when(`settled`) await Channel.deliver() expect(old.status).toBe('error') expect(live.status).toBe('error') @@ -911,12 +914,13 @@ for (const present of [false, true]) { const receiver = await h.open() const sender = await h.open('items', { db: await h.connect() }) if (present) - await sender.update(key, (row) => { - row.name = 'notified' - }).isPersisted.promise + await sender + .update(key, (row) => { + row.name = 'notified' + }) + .when(`settled`) else - await sender.insert({ id: key, name: 'notified' }).isPersisted - .promise + await sender.insert({ id: key, name: 'notified' }).when(`settled`) const current = { id: key, name: 'current' } await executeTransaction( h.db.db, @@ -946,9 +950,11 @@ for (const present of [false, true]) { ) const fresh = await h.open() assertRows(fresh.values(), expected, 'current restore') - await fresh.update(key, (row) => { - row.name = 'suffix' - }).isPersisted.promise + await fresh + .update(key, (row) => { + row.name = 'suffix' + }) + .when(`settled`) await Channel.deliver() assertRows( receiver.values(), @@ -1001,7 +1007,7 @@ it('preserves a recreated peer while a newer deletion is blocked', async () => { const fresh = await h.open('items', { db: freshDb }) const peer = await h.open('items', { db: peerDb }) const expected = [{ id: 1, name: 'recreated' }] - await fresh.insert(expected[0]!).isPersisted.promise + await fresh.insert(expected[0]!).when(`settled`) await Channel.deliver() const blockerRequest = indexedDB.open(h.db.name, 1) const blocker = await request(blockerRequest) diff --git a/packages/db/tests/live-query-order-only-move.test.ts b/packages/db/tests/live-query-order-only-move.test.ts index 5d6bf5181d..b7911a9e5e 100644 --- a/packages/db/tests/live-query-order-only-move.test.ts +++ b/packages/db/tests/live-query-order-only-move.test.ts @@ -167,7 +167,7 @@ describe(`order-only move (RFC #1623 phase 4)`, () => { expect(lq._layoutRevision).toBe(collectionLayoutRevisionBefore) persist.resolve() - await mutation.isPersisted.promise + await mutation.when(`settled`) await flush() const after = observer.getSnapshot() diff --git a/packages/db/tests/local-only-direct-write-oracle.test.ts b/packages/db/tests/local-only-direct-write-oracle.test.ts index dc2b58559e..83e4d6a695 100644 --- a/packages/db/tests/local-only-direct-write-oracle.test.ts +++ b/packages/db/tests/local-only-direct-write-oracle.test.ts @@ -64,7 +64,7 @@ describe(`local-only direct writes`, () => { for (const transaction of [inserted, updated, deleted]) { expect(transaction.state).toBe(`completed`) - await expect(transaction.isPersisted.promise).resolves.toBe(transaction) + await expect(transaction.when(`settled`)).resolves.toBe(transaction) await expect(transaction.when(`settled`)).resolves.toBe(transaction) } expect(batches).toEqual([ @@ -75,7 +75,7 @@ describe(`local-only direct writes`, () => { ]) expect(orders.get(1)).toMatchObject({ value: `c`, - $synced: true, + $hasPendingWrites: false, $origin: `local`, }) expect(orders.has(2)).toBe(false) @@ -117,7 +117,7 @@ describe(`local-only direct writes`, () => { expect(orders.get(1)?.value).toBe(`direct`) release.resolve() - await persisting.isPersisted.promise + await persisting.when(`settled`) }) it(`joins an ambient transaction`, () => { @@ -158,7 +158,7 @@ describe(`local-only direct writes`, () => { expect(created.map((transaction) => transaction.state)).toEqual([ `completed`, ]) - await expect(created[0]!.isPersisted.promise).resolves.toBe(created[0]) + await expect(created[0]!.when(`settled`)).resolves.toBe(created[0]) // The next direct write still takes the direct path. expect(orders.insert({ id: 3, value: `c` }).state).toBe(`completed`) }) @@ -191,7 +191,7 @@ describe(`local-only direct writes`, () => { kind === `update` ? `normalized` : undefined, ) await Promise.all( - created.map((transaction) => transaction.isPersisted.promise), + created.map((transaction) => transaction.when(`settled`)), ) expect(created.map((transaction) => transaction.state)).toEqual([ `completed`, @@ -218,7 +218,7 @@ describe(`local-only direct writes`, () => { expect(created.map((transaction) => transaction.state)).toEqual([`failed`]) expect(created[0]!.error?.error).toBeInstanceOf(Error) expect(created[0]!.error?.message).toBe(`bad`) - await expect(created[0]!.isPersisted.promise).rejects.toBeInstanceOf(Error) + await expect(created[0]!.when(`settled`)).rejects.toBeInstanceOf(Error) }) it(`runs the user's handler for that operation type`, async () => { @@ -235,7 +235,7 @@ describe(`local-only direct writes`, () => { }) expect(transaction.state).not.toBe(`completed`) - await transaction.isPersisted.promise + await transaction.when(`settled`) expect(calls).toBe(1) expect(orders.get(1)?.value).toBe(`handled`) // Inserts have no handler, so they still take the direct path. @@ -263,7 +263,7 @@ async function observe(handlers: Handlers, writes: Array) { changes .map( ({ type, key, value }) => - `${type}:${key}:${value.value}:${String(value.$synced)}`, + `${type}:${key}:${value.value}:${String(value.$hasPendingWrites)}`, ) .sort(), ), @@ -278,7 +278,7 @@ async function observe(handlers: Handlers, writes: Array) { errors.push(`${(error as Error).name}: ${(error as Error).message}`) } } - await Promise.all(transactions.map((t) => t.isPersisted.promise)) + await Promise.all(transactions.map((t) => t.when(`settled`))) return { rows: orders.toArray.map(({ id, value }) => ({ id, value })), batches, @@ -431,8 +431,8 @@ describe(`local-only fallbacks for every operation type`, () => { expect(transaction.state).not.toBe(`completed`) expect(visible[kind](orders)).toBe(true) release.resolve() - await persisting.isPersisted.promise - await transaction.isPersisted.promise + await persisting.when(`settled`) + await transaction.when(`settled`) expect(visible[kind](orders)).toBe(true) }) @@ -453,7 +453,7 @@ describe(`local-only fallbacks for every operation type`, () => { expect(transaction.state).not.toBe(`completed`) expect(visible[kind](orders)).toBe(true) pending.rollback() - await transaction.isPersisted.promise + await transaction.when(`settled`) expect(visible[kind](orders)).toBe(true) }) @@ -486,7 +486,7 @@ describe(`local-only fallbacks for every operation type`, () => { const transaction = write[kind](orders) expect(visible[kind](orders)).toBe(true) - await expect(transaction.isPersisted.promise).rejects.toThrow( + await expect(transaction.when(`settled`)).rejects.toThrow( `handler failed`, ) expect(transaction.state).toBe(`failed`) diff --git a/packages/db/tests/local-only.test-d.ts b/packages/db/tests/local-only.test-d.ts index 6ed48ce81c..319d2facbf 100644 --- a/packages/db/tests/local-only.test-d.ts +++ b/packages/db/tests/local-only.test-d.ts @@ -86,9 +86,9 @@ describe(`LocalOnly Collection Types`, () => { const configWithCallbacks = { id: `test-with-callbacks`, getKey: (item: TestItem) => item.id, - onInsert: () => Promise.resolve({}), - onUpdate: () => Promise.resolve({}), - onDelete: () => Promise.resolve({}), + onInsert: () => Promise.resolve(), + onUpdate: () => Promise.resolve(), + onDelete: () => Promise.resolve(), } const options = localOnlyCollectionOptions(configWithCallbacks) diff --git a/packages/db/tests/local-only.test.ts b/packages/db/tests/local-only.test.ts index 71132a1d14..aa102a29fc 100644 --- a/packages/db/tests/local-only.test.ts +++ b/packages/db/tests/local-only.test.ts @@ -744,7 +744,7 @@ describe(`LocalOnly Collection`, () => { } // Catch the rejected promise to avoid unhandled rejection - tx.isPersisted.promise.catch(() => {}) + tx.when(`settled`).catch(() => {}) // Item should be rolled back expect(collection.has(700)).toBe(false) @@ -790,7 +790,7 @@ describe(`LocalOnly Collection`, () => { transaction.state === `persisting` ) transaction.rollback() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await replacementCollection.cleanup() } }) diff --git a/packages/db/tests/local-storage-order-oracle.test.ts b/packages/db/tests/local-storage-order-oracle.test.ts index 47a8fbd5d9..4f4d884420 100644 --- a/packages/db/tests/local-storage-order-oracle.test.ts +++ b/packages/db/tests/local-storage-order-oracle.test.ts @@ -1,7 +1,7 @@ /** * LocalStorage automatic writes preserve mutation order within one Collection. * The public LocalStorage guide promises direct mutations that persist, while - * Collection settlement promises that a fulfilled `isPersisted` reflects the + * Collection settlement promises that a fulfilled `when('settled')` reflects the * accepted mutation. A later authored update must not be replaced by an older * handler that happens to finish later. This law says nothing about concurrent * writers in different Collections or browser tabs. @@ -87,8 +87,9 @@ it('persists a nested fire-and-forget mutation after its handler returns', async getKey: (row) => row.id, onInsert: async ({ transaction }) => { if (transaction.mutations[0].modified.id === 'outer') { - nestedReceipt = collection.insert({ id: 'inner', value: 2 }) - .isPersisted.promise + nestedReceipt = collection + .insert({ id: 'inner', value: 2 }) + .when(`settled`) entered.resolve() await release.promise } @@ -103,7 +104,7 @@ it('persists a nested fire-and-forget mutation after its handler returns', async expect(nestedReceipt, 'nested mutation was admitted').toBeDefined() expect(snapshots, 'no write before the outer handler returns').toEqual([]) release.resolve() - await Promise.all([outer.isPersisted.promise, nestedReceipt]) + await Promise.all([outer.when(`settled`), nestedReceipt]) expect(snapshots).toEqual([ expectedRows([{ id: 'outer', value: 1 }], [true]), expectedRows( @@ -208,7 +209,7 @@ it('orders module-level manual acceptance with a DbClient Collection', async () draft.value = 2 }) }) - const receipt = observeHistoryPromise(manual.isPersisted.promise) + const receipt = observeHistoryPromise(manual.when(`settled`)) const commit = observeHistoryPromise(manual.commit()) await manualEntered.promise // The receipt check must occur after mutationFn registers its write. @@ -220,7 +221,7 @@ it('orders module-level manual acceptance with a DbClient Collection', async () expect(receipt.read().status, 'held manual receipt').toBe('pending') release.resolve() await Promise.all([ - automatic.isPersisted.promise, + automatic.when(`settled`), receipt.settled, commit.settled, ]) @@ -301,7 +302,7 @@ it('routes module-level manual acceptance before explicit preload', async () => () => [ () => { if (transaction) { - void transaction.isPersisted.promise.catch(() => undefined) + void transaction.when(`settled`).catch(() => undefined) if (transaction.state === 'pending') transaction.rollback() } }, @@ -344,7 +345,7 @@ for (const directState of ['idle', 'ready', 'cleaned-up'] as const) { }) manual.mutate(() => materialized.insert({ id: 'owned', value: 1 })) await manual.commit() - await manual.isPersisted.promise + await manual.when(`settled`) const expected = [{ id: 'owned', value: 1 }] expect( storedRows(storage.getItem('rows')), @@ -402,17 +403,17 @@ it('persists handler-free direct mutations before their calls return', async () expect(storedRows(storage.getItem('rows'))).toEqual([ { id: 'a', value: 1 }, ]) - await insert.isPersisted.promise + await insert.when(`settled`) const update = collection.update('a', (draft) => { draft.value = 2 }) expect(storedRows(storage.getItem('rows'))).toEqual([ { id: 'a', value: 2 }, ]) - await update.isPersisted.promise + await update.when(`settled`) const deletion = collection.delete('a') expect(storedRows(storage.getItem('rows'))).toEqual([]) - await deletion.isPersisted.promise + await deletion.when(`settled`) }, () => [() => collection.cleanup()], ) @@ -443,15 +444,15 @@ it('persists a direct write at return after a synchronous handler failure', asyn const rejected = collection.update('a', (draft) => { draft.value = 1 }) - void rejected.isPersisted.promise.catch(() => undefined) + void rejected.when(`settled`).catch(() => undefined) const next = collection.insert({ id: 'next', value: 2 }) const expected = expectedRows([{ id: 'next', value: 2 }], [true]) expect( storedRows(storage.getItem('rows')), 'direct return after decided failure', ).toEqual(expected) - await expect(rejected.isPersisted.promise).rejects.toBe(failure) - await next.isPersisted.promise + await expect(rejected.when(`settled`)).rejects.toBe(failure) + await next.when(`settled`) expect(storedRows(storage.getItem('rows')), 'settled durable').toEqual( expected, ) @@ -468,7 +469,7 @@ it('persists a direct write at return after a synchronous handler failure', asyn [true, true], ), ) - await after.isPersisted.promise + await after.when(`settled`) }, () => [() => collection.cleanup()], ) @@ -503,7 +504,7 @@ it('holds a handler-free write behind an earlier pending handler', async () => { }) expect(storedRows(storage.getItem('rows'))).toEqual(initial) release.resolve() - await Promise.all([first.isPersisted.promise, second.isPersisted.promise]) + await Promise.all([first.when(`settled`), second.when(`settled`)]) expect(storedRows(storage.getItem('rows'))).toEqual([ { id: 'a', value: 2 }, { id: 'b', value: 0 }, @@ -517,7 +518,7 @@ it('holds a handler-free write behind an earlier pending handler', async () => { { id: 'b', value: 4 }, { id: 'new', value: 3 }, ]) - await afterDrain.isPersisted.promise + await afterDrain.when(`settled`) }, () => [() => release.resolve(), () => collection.cleanup()], ) @@ -559,7 +560,7 @@ for (const sameKey of [true, false]) { async () => { await collection.preload() const first = collection.insert({ id: 'k', value: 1 }) - const firstOutcome = first.isPersisted.promise.then( + const firstOutcome = first.when(`settled`).then( () => true, () => false, ) @@ -672,7 +673,7 @@ for (const queued of [false, true]) { }) manual.mutate(() => collection.insert({ id: 'manual', value: 2 })) const commit = observeHistoryPromise(manual.commit()) - const receipt = observeHistoryPromise(manual.isPersisted.promise) + const receipt = observeHistoryPromise(manual.when(`settled`)) await accepted.promise if (queued) { await new Promise((resolve) => setTimeout(resolve, 0)) @@ -682,7 +683,7 @@ for (const queued of [false, true]) { ) expect(storage.getItem('rows')).toBeNull() release.resolve() - await first!.isPersisted.promise + await first!.when(`settled`) } await Promise.all([commit.settled, receipt.settled]) if (storageFails) { @@ -754,13 +755,13 @@ it('waits for queued acceptance before reporting a later mutationFn failure', as }) manual.mutate(() => collection.insert({ id: 'manual', value: 2 })) const commit = observeHistoryPromise(manual.commit()) - const receipt = observeHistoryPromise(manual.isPersisted.promise) + const receipt = observeHistoryPromise(manual.when(`settled`)) await accepted.promise await new Promise((resolve) => setTimeout(resolve, 0)) expect(commit.read().status, 'held commit').toBe('pending') expect(receipt.read().status, 'held persistence receipt').toBe('pending') release.resolve() - await first.isPersisted.promise + await first.when(`settled`) await Promise.all([commit.settled, receipt.settled]) expect(commit.read()).toMatchObject({ status: 'rejected', @@ -852,7 +853,7 @@ it('waits for every un-awaited manual acceptance in one transaction', async () = slow.insert({ id: 'slow-manual', value: 2 }) fast.insert({ id: 'fast-manual', value: 3 }) }) - const receipt = observeHistoryPromise(manual.isPersisted.promise) + const receipt = observeHistoryPromise(manual.when(`settled`)) const commit = observeHistoryPromise(manual.commit()) await new Promise((resolve) => setTimeout(resolve, 0)) expect(storedRows(fastStorage.getItem('fast-rows'))).toEqual([ @@ -863,7 +864,7 @@ it('waits for every un-awaited manual acceptance in one transaction', async () = expect(commit.read().status).toBe('pending') release.resolve() await Promise.all([ - first.isPersisted.promise, + first.when(`settled`), receipt.settled, commit.settled, ]) @@ -935,7 +936,7 @@ for (const queued of [false, true]) { if (queued) { expect(storage.getItem('rows')).toBeNull() release.resolve() - await first!.isPersisted.promise + await first!.when(`settled`) } await expect(receipt).rejects.toBe(storageError) if (queued) @@ -948,7 +949,7 @@ for (const queued of [false, true]) { () => release.resolve(), () => { if (manual) { - void manual.isPersisted.promise.catch(() => undefined) + void manual.when(`settled`).catch(() => undefined) manual.rollback() } }, @@ -1004,7 +1005,7 @@ it('returns a rejected manual acceptance Promise for invalid serialized data', a () => [ () => { if (manual) { - void manual.isPersisted.promise.catch(() => undefined) + void manual.when(`settled`).catch(() => undefined) manual.rollback() } }, @@ -1049,7 +1050,7 @@ for (const secondId of ['a', 'b']) { const second = collection.update(secondId, (draft) => { draft.value = 2 }) - const rejected = second.isPersisted.promise.then( + const rejected = second.when(`settled`).then( () => false, () => true, ) @@ -1067,7 +1068,7 @@ for (const secondId of ['a', 'b']) { ]) expect(storedRows(storage.getItem('rows'))).toEqual(initial) releaseFirst.resolve() - await first.isPersisted.promise + await first.when(`settled`) expect(storedRows(storage.getItem('rows'))).toEqual([ { id: 'a', value: 1 }, { id: 'b', value: 0 }, @@ -1136,7 +1137,7 @@ for (const secondId of ['a', 'b']) { ) const settled = [false, false] const outcomes = transactions.map((tx, index) => - tx.isPersisted.promise.then( + tx.when(`settled`).then( () => { settled[index] = true return true @@ -1222,7 +1223,7 @@ for (const accepted of [ }) const deletion = collection.delete('a') const outcomes = [update, deletion].map((tx) => - tx.isPersisted.promise.then( + tx.when(`settled`).then( () => true, () => false, ), diff --git a/packages/db/tests/local-storage-peer-oracle.test.ts b/packages/db/tests/local-storage-peer-oracle.test.ts index d7563854ea..82a6cc0b5a 100644 --- a/packages/db/tests/local-storage-peer-oracle.test.ts +++ b/packages/db/tests/local-storage-peer-oracle.test.ts @@ -151,14 +151,14 @@ for (const firstId of [1, '1'] as const) { { id: firstId, value: 1 }, { id: firstId === 1 ? '1' : 1, value: 2 }, ] - await first.insert({ ...authored[0]! }).isPersisted.promise + await first.insert({ ...authored[0]! }).when(`settled`) expect(durableRows(host, 'shared'), 'first durable receipt').toEqual( expectedRows(authored.slice(0, 1)), ) expect(publicRows(second), 'first same-tab peer receipt').toEqual( expectedRows(authored.slice(0, 1)), ) - await second.insert({ ...authored[1]! }).isPersisted.promise + await second.insert({ ...authored[1]! }).when(`settled`) const expected = expectedRows(authored) expect( durableRows(host, 'shared'), @@ -203,7 +203,7 @@ it('publishes manual and automatic same-key edits to a same-tab peer', async () await withHistoryCleanup( async () => { await Promise.all([first.preload(), second.preload()]) - await first.insert({ id: 'row', value: 1 }).isPersisted.promise + await first.insert({ id: 'row', value: 1 }).when(`settled`) expect(publicRows(second)).toEqual([{ id: 'row', value: 1 }]) const transaction = createTransaction({ @@ -226,15 +226,17 @@ it('publishes manual and automatic same-key edits to a same-tab peer', async () expected, ) expect(publicRows(second), 'manual writer receipt').toEqual(expected) - await first.update('row', (draft) => { - draft.value = 3 - }).isPersisted.promise + await first + .update('row', (draft) => { + draft.value = 3 + }) + .when(`settled`) const updated = expectedRows([{ id: 'row', value: 3 }]) expect(durableRows(host, 'shared'), 'automatic update durable').toEqual( updated, ) expect(publicRows(second), 'automatic update peer').toEqual(updated) - await second.delete('row').isPersisted.promise + await second.delete('row').when(`settled`) expect(durableRows(host, 'shared'), 'automatic delete durable').toEqual( [], ) @@ -258,7 +260,7 @@ it('does not publish a rejected same-tab write', async () => { await withHistoryCleanup( async () => { await Promise.all([first.preload(), second.preload()]) - await first.insert({ id: 'kept', value: 1 }).isPersisted.promise + await first.insert({ id: 'kept', value: 1 }).when(`settled`) const writeError = new Error('storage write failed') const setItem = host.storage.setItem host.storage.setItem = () => { @@ -266,7 +268,7 @@ it('does not publish a rejected same-tab write', async () => { } try { await expect( - second.insert({ id: 'rejected', value: 2 }).isPersisted.promise, + second.insert({ id: 'rejected', value: 2 }).when(`settled`), ).rejects.toBe(writeError) } finally { host.storage.setItem = setItem @@ -306,7 +308,7 @@ it('keeps a durable receipt when a writer subscriber throws', async () => { }, { includeInitialState: false }, ) - await writer.insert({ id: 'saved', value: 1 }).isPersisted.promise + await writer.insert({ id: 'saved', value: 1 }).when(`settled`) expect( postStorageCallbacks, 'post-Storage callback reached', @@ -358,7 +360,7 @@ it('isolates a failing same-tab receiver after a durable write', async () => { await withHistoryCleanup( async () => { await Promise.all([writer.preload(), failed.preload(), later.preload()]) - await writer.insert({ id: 'accepted', value: 1 }).isPersisted.promise + await writer.insert({ id: 'accepted', value: 1 }).when(`settled`) const expected = [{ id: 'accepted', value: 1 }] expect(durableRows(host, 'shared'), 'durable receipt').toEqual(expected) expect(publicRows(writer), 'writer receipt').toEqual(expected) @@ -412,7 +414,7 @@ it('publishes a durable row even when local confirmation fails', async () => { async () => { await Promise.all([writer.preload(), peer.preload()]) await expect( - writer.insert({ id: 'durable', value: 1 }).isPersisted.promise, + writer.insert({ id: 'durable', value: 1 }).when(`settled`), ).rejects.toBe(confirmationError) const expected = [{ id: 'durable', value: 1 }] expect( @@ -442,8 +444,7 @@ it('preserves a nested same-tab write during peer publication', async () => { (changes) => { if (!admitted && changes.some((change) => change.key === 'first')) { admitted = true - nested = nestedWriter.insert({ id: 'nested', value: 2 }).isPersisted - .promise + nested = nestedWriter.insert({ id: 'nested', value: 2 }).when(`settled`) } }, { includeInitialState: false }, @@ -455,7 +456,7 @@ it('preserves a nested same-tab write during peer publication', async () => { writer.preload(), nestedWriter.preload(), ]) - await writer.insert({ id: 'first', value: 1 }).isPersisted.promise + await writer.insert({ id: 'first', value: 1 }).when(`settled`) expect(nested, 'peer admitted nested write').toBeDefined() await nested const expected = expectedRows([ @@ -470,9 +471,11 @@ it('preserves a nested same-tab write during peer publication', async () => { expect(publicRows(nestedWriter), 'nested writer public receipt').toEqual( expected, ) - await nestedWriter.update('nested', (draft) => { - draft.value = 3 - }).isPersisted.promise + await nestedWriter + .update('nested', (draft) => { + draft.value = 3 + }) + .when(`settled`) const final = expectedRows([ { id: 'first', value: 1 }, { id: 'nested', value: 3 }, @@ -535,7 +538,7 @@ it('avoids reparsing the writer snapshot during same-tab publication', async () await Promise.all(collections.map((collection) => collection.preload())) defaultParse.mockClear() peerParses.fill(0) - await writer.insert({ id: 'new', value: 1 }).isPersisted.promise + await writer.insert({ id: 'new', value: 1 }).when(`settled`) expect( defaultParse.mock.calls.filter( ([raw]) => typeof raw === 'string' && raw.includes('s:prior'), @@ -585,7 +588,7 @@ it('keeps an authored Date in the writer while peers read JSON bytes', async () async () => { await Promise.all([writer.preload(), peer.preload()]) const at = new Date('2020-01-02T03:04:05.000Z') - await writer.insert({ id: 'row', at }).isPersisted.promise + await writer.insert({ id: 'row', at }).when(`settled`) const expected = [{ id: 'row', at: at.toISOString() }] const durable = JSON.parse(host.storage.getItem('dated')!) as Record< string, @@ -636,8 +639,8 @@ it('keeps a native authored Date through a disjoint peer write', async () => { async () => { await Promise.all([first.preload(), second.preload()]) const at = new Date('2020-01-02T03:04:05.000Z') - await first.insert({ id: 'first-row', at }).isPersisted.promise - await second.insert({ id: 'second-row', at: 'later' }).isPersisted.promise + await first.insert({ id: 'first-row', at }).when(`settled`) + await second.insert({ id: 'second-row', at: 'later' }).when(`settled`) const durable = JSON.parse(host.storage.getItem('dated')!) as Record< string, { data: DatedRow } @@ -677,9 +680,11 @@ it('keeps a native authored Date through a disjoint peer write', async () => { // A peer's actual same-key edit has a new version and must replace the // retained native value in the first Collection. - await second.update('first-row', (draft) => { - draft.at = 'changed' - }).isPersisted.promise + await second + .update('first-row', (draft) => { + draft.at = 'changed' + }) + .when(`settled`) expect( [...first.values()].find(({ id }) => id === 'first-row'), ).toMatchObject({ at: 'changed' }) @@ -751,7 +756,7 @@ for (const changesVersion of [false, true]) { async () => { await Promise.all([writer.preload(), peer.preload()]) parses = 0 - await writer.insert({ id: 'new', value: 1 }).isPersisted.promise + await writer.insert({ id: 'new', value: 1 }).when(`settled`) const expected = expectedRows([ { id: 'prior', value: 9 }, { id: 'new', value: 1 }, @@ -809,16 +814,18 @@ it('publishes a parser-normalized authored row with an unchanged version token', await withHistoryCleanup( async () => { await Promise.all([writer.preload(), peer.preload()]) - await writer.insert({ id: 'row', value: 1 }).isPersisted.promise + await writer.insert({ id: 'row', value: 1 }).when(`settled`) const expected = [{ id: 'row', value: 9 }] expect(durableRows(host, 'shared'), 'normalized insert durable').toEqual( expected, ) expect(publicRows(writer), 'normalized insert writer').toEqual(expected) expect(publicRows(peer), 'normalized insert peer').toEqual(expected) - await writer.update('row', (draft) => { - draft.value = 2 - }).isPersisted.promise + await writer + .update('row', (draft) => { + draft.value = 2 + }) + .when(`settled`) expect(durableRows(host, 'shared'), 'normalized update durable').toEqual( expected, ) @@ -848,14 +855,13 @@ it('restores same-tab publication after cleanup and restart', async () => { async () => { await Promise.all([first.preload(), second.preload()]) await first.cleanup() - await second.insert({ id: 'during-cleanup', value: 1 }).isPersisted - .promise + await second.insert({ id: 'during-cleanup', value: 1 }).when(`settled`) expect(first.status).toBe('cleaned-up') first.startSyncImmediate() expect(publicRows(first), 'restart restore').toEqual([ { id: 'during-cleanup', value: 1 }, ]) - await second.insert({ id: 'after-restart', value: 2 }).isPersisted.promise + await second.insert({ id: 'after-restart', value: 2 }).when(`settled`) const expected = expectedRows([ { id: 'during-cleanup', value: 1 }, { id: 'after-restart', value: 2 }, @@ -888,15 +894,16 @@ it('receives a peer write authored during persisted restore publication', async (changes) => { if (!admitted && changes.some((change) => change.key === 'seed')) { admitted = true - nestedReceipt = first.insert({ id: 'nested', value: 2 }).isPersisted - .promise + nestedReceipt = first + .insert({ id: 'nested', value: 2 }) + .when(`settled`) } }, { includeInitialState: false }, ) try { await second.cleanup() - await first.insert({ id: 'seed', value: 1 }).isPersisted.promise + await first.insert({ id: 'seed', value: 1 }).when(`settled`) second.startSyncImmediate() expect(admitted, 'restore callback admitted peer write').toBe(true) await nestedReceipt @@ -910,9 +917,11 @@ it('receives a peer write authored during persisted restore publication', async ) expect(publicRows(first), 'nested writer receipt').toEqual(expected) expect(publicRows(second), 'restored peer receipt').toEqual(expected) - await first.update('nested', (draft) => { - draft.value = 3 - }).isPersisted.promise + await first + .update('nested', (draft) => { + draft.value = 3 + }) + .when(`settled`) const later = expectedRows([ { id: 'seed', value: 1 }, { id: 'nested', value: 3 }, @@ -942,8 +951,7 @@ it('receives a same-tab write started by its ready listener', async () => { let writeReceipt: Promise | undefined const off = second.on('status:change', ({ status }) => { if (status === 'ready') { - writeReceipt = first.insert({ id: 'at-ready', value: 1 }).isPersisted - .promise + writeReceipt = first.insert({ id: 'at-ready', value: 1 }).when(`settled`) } }) await withHistoryCleanup( @@ -987,7 +995,7 @@ it('publishes through the default same-tab Storage object', async () => { await withHistoryCleanup( async () => { await Promise.all([first.preload(), second.preload()]) - await first.insert({ id: 'row', value: 1 }).isPersisted.promise + await first.insert({ id: 'row', value: 1 }).when(`settled`) expect(publicRows(second)).toEqual([{ id: 'row', value: 1 }]) }, () => [ @@ -1069,7 +1077,7 @@ for (const unrelatedKey of ['first-store', 'second-store']) { () => [ () => { if (transaction) { - void transaction.isPersisted.promise.catch(() => undefined) + void transaction.when(`settled`).catch(() => undefined) transaction.rollback() } }, @@ -1188,7 +1196,7 @@ it.each([`copy first`, `owner first`] as const)( await first.stateWhenReady() } expect(host.listenerCount()).toBe(1) - await first.insert({ id: `owned`, value: 1 }).isPersisted.promise + await first.insert({ id: `owned`, value: 1 }).when(`settled`) expect(publicRows(first)).toEqual([{ id: `owned`, value: 1 }]) expect([...selected.values()]).toEqual([]) expect(durableRows(host, `selected-owner`)).toEqual([ @@ -1363,8 +1371,8 @@ it('does not retain a same-tab callback after a late constructor failure', async await Promise.all([failedWriter.preload(), controlWriter.preload()]) failedHost.resetReadCount() controlHost.resetReadCount() - await failedWriter.insert({ id: 'row', value: 1 }).isPersisted.promise - await controlWriter.insert({ id: 'row', value: 1 }).isPersisted.promise + await failedWriter.insert({ id: 'row', value: 1 }).when(`settled`) + await controlWriter.insert({ id: 'row', value: 1 }).when(`settled`) const failedWriteReads = failedHost.readCount() const controlWriteReads = controlHost.readCount() expect(durableRows(failedHost, 'shared')).toEqual( @@ -1421,7 +1429,7 @@ it('does not retain cleared rows after the next local write settles', async () = await withHistoryCleanup( async () => { await Promise.all([collection.preload(), peer.preload()]) - await collection.insert({ id: 'old', value: 1 }).isPersisted.promise + await collection.insert({ id: 'old', value: 1 }).when(`settled`) host.deliver('shared') expect(publicRows(peer)).toEqual([{ id: 'old', value: 1 }]) collection.utils.clearStorage() @@ -1433,7 +1441,7 @@ it('does not retain cleared rows after the next local write settles', async () = expect(publicRows(peer), 'peer clear without event').toEqual([]) host.deliver('shared') expect(publicRows(peer)).toEqual([]) - await collection.insert({ id: 'new', value: 2 }).isPersisted.promise + await collection.insert({ id: 'new', value: 2 }).when(`settled`) const expected = [{ id: 'new', value: 2 }] expect(durableRows(host, 'shared')).toEqual(expected) expect(publicRows(collection)).toEqual(expected) @@ -1475,7 +1483,7 @@ it('keeps a pending mutation through clear until it settles', async () => { await withHistoryCleanup( async () => { await collection.preload() - await collection.insert({ id: 'pending', value: 1 }).isPersisted.promise + await collection.insert({ id: 'pending', value: 1 }).when(`settled`) const update = collection.update('pending', (draft) => { draft.value = 2 }) @@ -1484,7 +1492,7 @@ it('keeps a pending mutation through clear until it settles', async () => { expect(host.storage.getItem('shared')).toBeNull() expect(publicRows(collection)).toEqual([{ id: 'pending', value: 2 }]) release.resolve() - await update.isPersisted.promise + await update.when(`settled`) expect(durableRows(host, 'shared')).toEqual([{ id: 'pending', value: 2 }]) expect(publicRows(collection)).toEqual([{ id: 'pending', value: 2 }]) }, @@ -1503,7 +1511,7 @@ it('refreshes each active Collection once when clearing storage', async () => { await withHistoryCleanup( async () => { await Promise.all([writer.preload(), peer.preload()]) - await writer.insert({ id: 'row', value: 1 }).isPersisted.promise + await writer.insert({ id: 'row', value: 1 }).when(`settled`) const getItem = host.storage.getItem let reads = 0 host.storage.getItem = (key) => { @@ -1530,14 +1538,14 @@ it('distinguishes a missing storage key from malformed empty content', async () await withHistoryCleanup( async () => { await collection.preload() - await collection.insert({ id: 'first', value: 1 }).isPersisted.promise + await collection.insert({ id: 'first', value: 1 }).when(`settled`) expect(durableRows(host, 'shared')).toEqual([{ id: 'first', value: 1 }]) host.storage.setItem('shared', '') const rejected = collection.insert({ id: 'second', value: 2 }) - await expect(rejected.isPersisted.promise).rejects.toThrow(SyntaxError) + await expect(rejected.when(`settled`)).rejects.toThrow(SyntaxError) expect(host.storage.getItem('shared')).toBe('') collection.utils.clearStorage() - await collection.insert({ id: 'third', value: 3 }).isPersisted.promise + await collection.insert({ id: 'third', value: 3 }).when(`settled`) expect(durableRows(host, 'shared')).toEqual([{ id: 'third', value: 3 }]) }, () => [() => collection.cleanup()], @@ -1632,9 +1640,11 @@ it('keeps one legacy unprefixed string key writable and restorable', async () => async () => { await writer.preload() expect(publicRows(writer)).toEqual([{ id: 'legacy', value: 1 }]) - await writer.update('legacy', (draft) => { - draft.value = 2 - }).isPersisted.promise + await writer + .update('legacy', (draft) => { + draft.value = 2 + }) + .when(`settled`) expect(durableRows(host, 'shared')).toEqual([{ id: 'legacy', value: 2 }]) reopened = makeCollection(host, 'shared', 'reopened') await reopened.preload() @@ -1671,7 +1681,7 @@ it('restores a serializable NaN key alongside distinct number and string keys', { id: 'number', key: 1, value: 2 }, { id: 'string', key: '1', value: 3 }, ] - for (const row of authored) await writer.insert(row).isPersisted.promise + for (const row of authored) await writer.insert(row).when(`settled`) expect( [...writer.values()].map(({ id, key, value }) => ({ id, key, value })), ).toEqual(expect.arrayContaining(authored)) @@ -1702,7 +1712,7 @@ for (const invalid of [NaN, Infinity, -Infinity]) { async () => { await writer.preload() await expect( - writer.insert({ id: invalid, value: 1 }).isPersisted.promise, + writer.insert({ id: invalid, value: 1 }).when(`settled`), ).rejects.toThrow() expect(host.storage.getItem('shared')).toBeNull() expect(publicRows(writer)).toEqual([]) @@ -1723,7 +1733,7 @@ it('retains the last valid peer snapshot through a null row and its repair', asy await withHistoryCleanup( async () => { await collection.preload() - await collection.insert({ id: 'row', value: 1 }).isPersisted.promise + await collection.insert({ id: 'row', value: 1 }).when(`settled`) const old = [{ id: 'row', value: 1 }] host.storage.setItem( 'shared', @@ -1950,9 +1960,9 @@ it('a peer insert during a pending local insert confirms the final local row', a async () => { await Promise.all([local.preload(), peer.preload()]) const localWrite = local.insert({ id: 'same', value: 1 }) - const outcome = localWrite.isPersisted.promise + const outcome = localWrite.when(`settled`) await entered.promise - await peer.insert({ id: 'same', value: 2 }).isPersisted.promise + await peer.insert({ id: 'same', value: 2 }).when(`settled`) expect(durableRows(host, 'shared')).toEqual([{ id: 'same', value: 2 }]) expect(publicRows(local), 'pending local intent overlays peer').toEqual([ { id: 'same', value: 1 }, @@ -2016,10 +2026,10 @@ for (const failureKind of ['storage', 'parser'] as const) { await withHistoryCleanup( async () => { await collection.preload() - await collection.insert({ id: 'before', value: 1 }).isPersisted.promise + await collection.insert({ id: 'before', value: 1 }).when(`settled`) failRead = true await expect( - collection.insert({ id: 'rejected', value: 2 }).isPersisted.promise, + collection.insert({ id: 'rejected', value: 2 }).when(`settled`), ).rejects.toBe(readError) expect(durableRows(host, 'shared')).toEqual([ { id: 'before', value: 1 }, @@ -2030,7 +2040,7 @@ for (const failureKind of ['storage', 'parser'] as const) { publicRows(collection), 'failed event read retains public rows', ).toEqual([{ id: 'before', value: 1 }]) - await collection.insert({ id: 'after', value: 3 }).isPersisted.promise + await collection.insert({ id: 'after', value: 3 }).when(`settled`) const expected = expectedRows([ { id: 'before', value: 1 }, { id: 'after', value: 3 }, @@ -2078,7 +2088,7 @@ it('settles an accepted custom-parser write through a postwrite read fault', asy async () => { await Promise.all([writer.preload(), peer.preload()]) faultOnRead = reads + 2 - await writer.insert({ id: 'saved', value: 1 }).isPersisted.promise + await writer.insert({ id: 'saved', value: 1 }).when(`settled`) const expected = [{ id: 'saved', value: 1 }] expect(durableRows(host, 'shared'), 'durable receipt').toEqual(expected) expect(publicRows(writer), 'writer receipt').toEqual(expected) diff --git a/packages/db/tests/local-storage-persistence-failure.test.ts b/packages/db/tests/local-storage-persistence-failure.test.ts index 7c65efe5ba..2caefe9809 100644 --- a/packages/db/tests/local-storage-persistence-failure.test.ts +++ b/packages/db/tests/local-storage-persistence-failure.test.ts @@ -56,7 +56,7 @@ it.each(cases)( const log = vi.spyOn(console, `error`).mockImplementation(() => {}) try { await collection.preload() - await collection.insert({ id: `seed`, value: 1 }).isPersisted.promise + await collection.insert({ id: `seed`, value: 1 }).when(`settled`) const mutate = () => { if (operation === `insert`) return collection.insert({ id: `bad`, value: 2 }) @@ -74,14 +74,14 @@ it.each(cases)( }, }) : mutate() - const rejection = expect(failed.isPersisted.promise).rejects.toBe(error) + const rejection = expect(failed.when(`settled`)).rejects.toBe(error) if (manual) { failed.mutate(mutate) await failed.commit().catch(() => {}) } await rejection fail = false - await collection.insert({ id: `good`, value: 3 }).isPersisted.promise + await collection.insert({ id: `good`, value: 3 }).when(`settled`) const restored = makeCollection() try { await restored.preload() diff --git a/packages/db/tests/local-storage.test-d.ts b/packages/db/tests/local-storage.test-d.ts index 0ca85ccde2..385d3ee9ca 100644 --- a/packages/db/tests/local-storage.test-d.ts +++ b/packages/db/tests/local-storage.test-d.ts @@ -178,21 +178,21 @@ describe(`LocalStorage collection type resolution tests`, () => { expectTypeOf( params.transaction.mutations[0].modified, ).toEqualTypeOf() - return Promise.resolve({ success: true }) + return Promise.resolve() }, onUpdate: (params) => { // Verify that the mutation value has the correct type expectTypeOf( params.transaction.mutations[0].modified, ).toEqualTypeOf() - return Promise.resolve({ success: true }) + return Promise.resolve() }, onDelete: (params) => { // Verify that the mutation value has the correct type expectTypeOf( params.transaction.mutations[0].original, ).toEqualTypeOf() - return Promise.resolve({ success: true }) + return Promise.resolve() }, }) diff --git a/packages/db/tests/local-storage.test.ts b/packages/db/tests/local-storage.test.ts index 7a446b7dbb..c877fa7bcd 100644 --- a/packages/db/tests/local-storage.test.ts +++ b/packages/db/tests/local-storage.test.ts @@ -89,7 +89,6 @@ const publicRemoteTodo = { $key: `remote`, $origin: `remote`, $hasPendingWrites: false, - $synced: true, } function installRemoteTodo(storage: Storage): StorageEvent { @@ -146,7 +145,6 @@ function assertStoredTodos( delete userRow.$key delete userRow.$origin delete userRow.$hasPendingWrites - delete userRow.$synced return userRow }), ).toStrictEqual(expected) @@ -278,7 +276,7 @@ describe(`localStorage collection`, () => { const insertTx = collection.insert(todo) - await insertTx.isPersisted.promise + await insertTx.when(`settled`) const storedData = mockStorage.getItem(`todos`) expect(storedData).toBeDefined() @@ -416,7 +414,7 @@ describe(`localStorage collection`, () => { // Insert without handlers should still persist const insertTx = collection.insert(todo) - await insertTx.isPersisted.promise + await insertTx.when(`settled`) // Check that it was saved to storage let storedData = mockStorage.getItem(`todos`) @@ -428,7 +426,7 @@ describe(`localStorage collection`, () => { const updateTx = collection.update(`1`, (draft) => { draft.title = `Updated Without Handlers` }) - await updateTx.isPersisted.promise + await updateTx.when(`settled`) // Check that update was saved to storage storedData = mockStorage.getItem(`todos`) @@ -438,7 +436,7 @@ describe(`localStorage collection`, () => { // Delete without handlers should still persist const deleteTx = collection.delete(`1`) - await deleteTx.isPersisted.promise + await deleteTx.when(`settled`) // Check that deletion was saved to storage storedData = mockStorage.getItem(`todos`) @@ -478,7 +476,7 @@ describe(`localStorage collection`, () => { // Insert should call handler AND persist const insertTx = collection.insert(todo) - await insertTx.isPersisted.promise + await insertTx.when(`settled`) expect(insertSpy).toHaveBeenCalledOnce() let storedData = mockStorage.getItem(`todos`) @@ -490,7 +488,7 @@ describe(`localStorage collection`, () => { const updateTx = collection.update(`1`, (draft) => { draft.title = `Updated With Handlers` }) - await updateTx.isPersisted.promise + await updateTx.when(`settled`) expect(updateSpy).toHaveBeenCalledOnce() storedData = mockStorage.getItem(`todos`) @@ -500,7 +498,7 @@ describe(`localStorage collection`, () => { // Delete should call handler AND persist const deleteTx = collection.delete(`1`) - await deleteTx.isPersisted.promise + await deleteTx.when(`settled`) expect(deleteSpy).toHaveBeenCalledOnce() storedData = mockStorage.getItem(`todos`) @@ -518,7 +516,7 @@ describe(`localStorage collection`, () => { storage: mockStorage, storageEventApi: mockStorageEventApi, getKey: (todo) => todo.id, - onInsert: () => Promise.resolve({ success: true }), + onInsert: () => Promise.resolve(), }), ) @@ -532,7 +530,7 @@ describe(`localStorage collection`, () => { // When a collection has mutation handlers, calling insert() automatically creates // a transaction and calls the onInsert handler const tx = collection.insert(todo) - await tx.isPersisted.promise + await tx.when(`settled`) // Check that it was saved to storage with version key structure const storedData = mockStorage.getItem(`todos`) @@ -568,7 +566,7 @@ describe(`localStorage collection`, () => { storage: mockStorage, storageEventApi: mockStorageEventApi, getKey: (todo) => todo.id, - onUpdate: () => Promise.resolve({ success: true }), + onUpdate: () => Promise.resolve(), }), ) @@ -579,7 +577,7 @@ describe(`localStorage collection`, () => { const tx = collection.update(`1`, (draft) => { draft.title = `Updated Todo` }) - await tx.isPersisted.promise + await tx.when(`settled`) // Check that it was updated in storage with a new version key const storedData = mockStorage.getItem(`todos`) @@ -613,7 +611,7 @@ describe(`localStorage collection`, () => { storage: mockStorage, storageEventApi: mockStorageEventApi, getKey: (todo) => todo.id, - onDelete: () => Promise.resolve({ success: true }), + onDelete: () => Promise.resolve(), }), ) @@ -622,7 +620,7 @@ describe(`localStorage collection`, () => { // Delete the todo - this automatically creates a transaction and calls onDelete const tx = collection.delete(`1`) - await tx.isPersisted.promise + await tx.when(`settled`) // Check that it was removed from storage const storedData = mockStorage.getItem(`todos`) @@ -1047,7 +1045,7 @@ describe(`localStorage collection`, () => { title: `replacement`, removed: `old`, }) - await insert.isPersisted.promise + await insert.when(`settled`) transaction.mutate(() => { collection.delete(`row`) @@ -1075,7 +1073,7 @@ describe(`localStorage collection`, () => { transaction.state === `persisting` ) transaction.rollback() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await reopened?.cleanup() await collection.cleanup() } @@ -1163,7 +1161,7 @@ describe(`localStorage collection`, () => { createdAt: new Date(), } const existingTx = collection.insert(existingTodo) - await existingTx.isPersisted.promise + await existingTx.when(`settled`) const tx = createTransaction({ mutationFn: async ({ transaction }: any) => { @@ -1221,7 +1219,7 @@ describe(`localStorage collection`, () => { createdAt: new Date(), } const insertTx = collection.insert(todo) - await insertTx.isPersisted.promise + await insertTx.when(`settled`) const tx = createTransaction({ mutationFn: async ({ transaction }: any) => { @@ -1270,7 +1268,7 @@ describe(`localStorage collection`, () => { }, autoCommit: false, }) - const persisted = observeHistoryPromise(tx.isPersisted.promise) + const persisted = observeHistoryPromise(tx.when(`settled`)) await withHistoryCleanup( async () => { tx.mutate(() => { @@ -1309,7 +1307,7 @@ describe(`localStorage collection`, () => { ...next, createdAt: new Date(next.createdAt), }) - await accepted.isPersisted.promise + await accepted.when(`settled`) const storedData = mockStorage.getItem(`todos`) assertStoredTodos(storedData, [...collection.values()], [next]) expect(JSON.parse(storedData!)[`s:rollback-test`]).toBeUndefined() @@ -1430,11 +1428,11 @@ describe(`localStorage collection`, () => { // Wait for all mutations to complete await Promise.all([ - tx1.isPersisted.promise, - tx2.isPersisted.promise, - tx3.isPersisted.promise, - tx4.isPersisted.promise, - tx5.isPersisted.promise, + tx1.when(`settled`), + tx2.when(`settled`), + tx3.when(`settled`), + tx4.when(`settled`), + tx5.when(`settled`), ]) // Verify final state in storage @@ -1567,7 +1565,7 @@ describe(`localStorage collection`, () => { mockStorageEventApi.triggerStorageEvent(storageEvent) // Wait for local mutation to complete - await localTx.isPersisted.promise + await localTx.when(`settled`) // Both items should exist expect(collection.has(`local`)).toBe(true) @@ -1595,7 +1593,7 @@ describe(`localStorage collection`, () => { completed: false, createdAt: new Date(), }) - await tx1.isPersisted.promise + await tx1.when(`settled`) // Simulate another tab updating the item const remoteData = { @@ -1627,7 +1625,7 @@ describe(`localStorage collection`, () => { const tx2 = collection.update(`1`, (draft) => { draft.title = `Local Update After Remote` }) - await tx2.isPersisted.promise + await tx2.when(`settled`) // Verify final state const storedData = mockStorage.getItem(`todos`) @@ -1707,7 +1705,7 @@ describe(`localStorage collection`, () => { completed: false, createdAt: new Date(), }) - await auto1.isPersisted.promise + await auto1.when(`settled`) // Manual transaction const tx = createTransaction({ @@ -1739,7 +1737,7 @@ describe(`localStorage collection`, () => { completed: false, createdAt: new Date(), }) - await auto2.isPersisted.promise + await auto2.when(`settled`) // Verify all items in storage const storedData = mockStorage.getItem(`todos`) @@ -1788,7 +1786,7 @@ describe(`localStorage collection`, () => { }) // The transaction should reject - await expect(tx.isPersisted.promise).rejects.toThrow() + await expect(tx.when(`settled`)).rejects.toThrow() subscription.unsubscribe() }) @@ -1833,22 +1831,22 @@ describe(`localStorage collection`, () => { ...first, createdAt: new Date(first.createdAt), }) - await tx1.isPersisted.promise + await tx1.when(`settled`) verifyConsistency([first]) const tx2 = collection.update(`1`, (draft) => { draft.title = `Updated` }) - await tx2.isPersisted.promise + await tx2.when(`settled`) const updated = { ...first, title: `Updated` } verifyConsistency([updated]) const tx3 = collection.insert({ ...second, createdAt: new Date(second.createdAt), }) - await tx3.isPersisted.promise + await tx3.when(`settled`) verifyConsistency([updated, second]) const tx4 = collection.delete(`1`) - await tx4.isPersisted.promise + await tx4.when(`settled`) verifyConsistency([second]) // Test the exact checker, not membership in the production cache. @@ -1903,7 +1901,7 @@ describe(`localStorage collection`, () => { completed: false, createdAt: new Date(), }) - await tx1.isPersisted.promise + await tx1.when(`settled`) const tx2 = collection.insert({ id: `second`, @@ -1911,7 +1909,7 @@ describe(`localStorage collection`, () => { completed: false, createdAt: new Date(), }) - await tx2.isPersisted.promise + await tx2.when(`settled`) const tx3 = collection.insert({ id: `third`, @@ -1919,13 +1917,13 @@ describe(`localStorage collection`, () => { completed: false, createdAt: new Date(), }) - await tx3.isPersisted.promise + await tx3.when(`settled`) // Update the FIRST item specifically const updateTx = collection.update(`first`, (draft) => { draft.completed = true }) - await updateTx.isPersisted.promise + await updateTx.when(`settled`) // Verify that the FIRST item was updated, not the last one expect(collection.get(`first`)?.completed).toBe(true) @@ -1962,7 +1960,7 @@ describe(`localStorage collection`, () => { completed: false, createdAt: new Date(), }) - await tx1.isPersisted.promise + await tx1.when(`settled`) const tx2 = collection.insert({ id: `second`, @@ -1970,7 +1968,7 @@ describe(`localStorage collection`, () => { completed: false, createdAt: new Date(), }) - await tx2.isPersisted.promise + await tx2.when(`settled`) const tx3 = collection.insert({ id: `third`, @@ -1978,11 +1976,11 @@ describe(`localStorage collection`, () => { completed: false, createdAt: new Date(), }) - await tx3.isPersisted.promise + await tx3.when(`settled`) // Delete the FIRST item specifically const deleteTx = collection.delete(`first`) - await deleteTx.isPersisted.promise + await deleteTx.when(`settled`) // Verify that the FIRST item was deleted, not the last one expect(collection.has(`first`)).toBe(false) @@ -2024,27 +2022,27 @@ describe(`localStorage collection`, () => { title: `First Todo`, completed: false, }) - await tx1.isPersisted.promise + await tx1.when(`settled`) const tx2 = collection.insert({ id: 2, title: `Second Todo`, completed: false, }) - await tx2.isPersisted.promise + await tx2.when(`settled`) const tx3 = collection.insert({ id: 3, title: `Third Todo`, completed: false, }) - await tx3.isPersisted.promise + await tx3.when(`settled`) // Update the FIRST item (id: 1) specifically const updateTx = collection.update(1, (draft) => { draft.completed = true }) - await updateTx.isPersisted.promise + await updateTx.when(`settled`) // Verify that item with id:1 was updated, not the last one expect(collection.get(1)?.completed).toBe(true) @@ -2086,25 +2084,25 @@ describe(`localStorage collection`, () => { title: `First Todo`, completed: false, }) - await tx1.isPersisted.promise + await tx1.when(`settled`) const tx2 = collection.insert({ id: 2, title: `Second Todo`, completed: false, }) - await tx2.isPersisted.promise + await tx2.when(`settled`) const tx3 = collection.insert({ id: 3, title: `Third Todo`, completed: false, }) - await tx3.isPersisted.promise + await tx3.when(`settled`) // Delete the FIRST item (id: 1) specifically const deleteTx = collection.delete(1) - await deleteTx.isPersisted.promise + await deleteTx.when(`settled`) // Verify that item with id:1 was deleted, not the last one expect(collection.has(1)).toBe(false) @@ -2169,7 +2167,7 @@ describe(`localStorage collection`, () => { const updateTx = collection.update(1, (draft) => { draft.completed = true }) - await updateTx.isPersisted.promise + await updateTx.when(`settled`) // Verify the correct item was updated expect(collection.get(1)?.completed).toBe(true) @@ -2235,7 +2233,7 @@ describe(`localStorage collection`, () => { // Now try to delete item with numeric id 1 const deleteTx = collection.delete(1) - await deleteTx.isPersisted.promise + await deleteTx.when(`settled`) // Verify the correct item was deleted expect(collection.has(1)).toBe(false) @@ -2275,7 +2273,7 @@ describe(`localStorage collection`, () => { id: 1, title: `Numeric ID`, }) - await tx1.isPersisted.promise + await tx1.when(`settled`) // Insert item with string ID "1" // With prefixing, these won't collide: @@ -2285,7 +2283,7 @@ describe(`localStorage collection`, () => { id: `1`, title: `String ID`, }) - await tx2.isPersisted.promise + await tx2.when(`settled`) // Both should exist in collection state expect(collection.has(1)).toBe(true) @@ -2330,14 +2328,14 @@ describe(`localStorage collection`, () => { id: 1, title: `Numeric 1`, }) - await tx1.isPersisted.promise + await tx1.when(`settled`) // Insert item with string ID "n:1" (which would collide with old "__number__1" approach) const tx2 = collection.insert({ id: `n:1`, title: `String n:1`, }) - await tx2.isPersisted.promise + await tx2.when(`settled`) // Both should exist in collection expect(collection.has(1)).toBe(true) diff --git a/packages/db/tests/mutation-handler-compatibility.test-d.ts b/packages/db/tests/mutation-handler-compatibility.test-d.ts index d7135369a4..a28c5c545e 100644 --- a/packages/db/tests/mutation-handler-compatibility.test-d.ts +++ b/packages/db/tests/mutation-handler-compatibility.test-d.ts @@ -1,38 +1,25 @@ import { describe, expectTypeOf, it } from 'vitest' import type { Transaction } from '../src/transactions' import type { - BaseCollectionConfig, DeleteMutationFn, InsertMutationFn, TransactionWithMutations, UpdateMutationFn, - UtilsRecord, } from '../src/types' -describe(`mutation handler return compatibility`, () => { +describe(`mutation handler return types`, () => { type Row = { id: string } - type LegacyResult = { txid: number } - it(`keeps default handler aliases compatible with legacy returns`, () => { - const insert: InsertMutationFn = () => Promise.resolve({ txid: 1 }) - const update: UpdateMutationFn = () => Promise.resolve({ txid: 1 }) - const remove: DeleteMutationFn = () => Promise.resolve({ txid: 1 }) - - expectTypeOf(insert).toBeFunction() - expectTypeOf(update).toBeFunction() - expectTypeOf(remove).toBeFunction() - }) - - it(`retains the deprecated fifth BaseCollectionConfig parameter`, () => { - type LegacyConfig = BaseCollectionConfig< - Row, - string, - never, - UtilsRecord, - LegacyResult - > - - expectTypeOf().toBeObject() + it(`requires handlers to settle without a return value`, () => { + expectTypeOf>>().toEqualTypeOf< + Promise + >() + expectTypeOf>>().toEqualTypeOf< + Promise + >() + expectTypeOf>>().toEqualTypeOf< + Promise + >() }) // Adapters pass a handler's transaction to code that takes a Transaction. diff --git a/packages/db/tests/mutation-handler-type-oracle.test-d.ts b/packages/db/tests/mutation-handler-type-oracle.test-d.ts index 5e4f173743..7d3bbbc4fb 100644 --- a/packages/db/tests/mutation-handler-type-oracle.test-d.ts +++ b/packages/db/tests/mutation-handler-type-oracle.test-d.ts @@ -8,7 +8,7 @@ import type { Transaction, TransactionState } from '../src/index.js' * `docs/reference/interfaces/BaseCollectionConfig.md` document direct * insert/update/delete handlers with a typed transaction and sibling Collection. * Every nested mutation carries that operation's row, key, and utilities. - * `docs/reference/interfaces/Transaction.md` documents `isPersisted.promise` + * `docs/reference/interfaces/Transaction.md` documents `when('settled')` * as local transaction settlement, not backend confirmation. * * Model and finite input grammar: independently declared Row, key, and utility @@ -162,7 +162,7 @@ describe(`mutation handler type oracle`, () => { expectTypeOf(collection.utils).toEqualTypeOf() expectTypeOf(collection.get).parameter(0).toEqualTypeOf() expectTypeOf(transaction.state).toEqualTypeOf() - expectTypeOf(transaction.isPersisted.promise).toEqualTypeOf< + expectTypeOf(transaction.when(`settled`)).toEqualTypeOf< Promise>> >() diff --git a/packages/db/tests/optimistic-action.test.ts b/packages/db/tests/optimistic-action.test.ts index a9c8d72fda..8bf6d9c8bf 100644 --- a/packages/db/tests/optimistic-action.test.ts +++ b/packages/db/tests/optimistic-action.test.ts @@ -54,7 +54,7 @@ describe(`createOptimisticAction`, () => { }) // Wait for the mutation to complete - await transaction.isPersisted.promise + await transaction.when(`settled`) // Verify mutationFn was called with the correct arguments expect(mutationFnMock).toHaveBeenCalledTimes(1) @@ -149,7 +149,7 @@ describe(`createOptimisticAction`, () => { expect(stripVirtualProps(collection.get(`2`))).toEqual(todoData) // Wait for the mutation to complete - await transaction.isPersisted.promise + await transaction.when(`settled`) // Verify mutationFn was called with the correct arguments expect(mutationFnMock).toHaveBeenCalledTimes(1) @@ -246,7 +246,7 @@ describe(`createOptimisticAction`, () => { // Wait for the transaction to complete (it will fail) try { - await transaction.isPersisted.promise + await transaction.when(`settled`) // Should not reach here expect(true).toBe(false) } catch (error) { diff --git a/packages/db/tests/optimistic-composition-oracle.test.ts b/packages/db/tests/optimistic-composition-oracle.test.ts index 3bda778288..0be44cbf31 100644 --- a/packages/db/tests/optimistic-composition-oracle.test.ts +++ b/packages/db/tests/optimistic-composition-oracle.test.ts @@ -19,7 +19,7 @@ * Production path and observation: public `update`, `createTransaction`, sync * `begin`/`write`/`commit`, and `get`. The publication case also compares a * subscriber replica and a downstream live query. Each check runs at the named - * cut, after the mutation or rollback returns, or after `isPersisted` and the + * cut, after the mutation or rollback returns, or after `when('settled')` and the * held receipts settle. * * Limits: this file owns one key and simple row snapshots. Generated @@ -74,7 +74,7 @@ function pendingTransaction() { autoCommit: false, mutationFn: () => done.promise, }) - const settled = tx.isPersisted.promise.catch((error: unknown) => error) + const settled = tx.when(`settled`).catch((error: unknown) => error) return { tx, done, settled } } @@ -101,7 +101,7 @@ describe(`optimistic snapshot ownership`, () => { const tx = fixture.collection.update(1, { optimistic }, (row) => { row.a = `failed` }) - const settled = tx.isPersisted.promise.catch(() => {}) + const settled = tx.when(`settled`).catch(() => {}) try { done.reject(new Error(`failed update`)) await settled @@ -136,7 +136,7 @@ describe(`optimistic snapshot ownership`, () => { const tx = fixture.collection.update(1, { optimistic }, (row) => { row.a = `accepted` }) - await tx.isPersisted.promise + await tx.when(`settled`) expect(fixture.collection.get(1)?.$origin).toBe(`local`) expect(stripVirtualProps(fixture.collection.get(1))).toEqual({ ...initial, @@ -165,17 +165,17 @@ describe(`optimistic snapshot ownership`, () => { const second = fixture.collection.update(1, (row) => { row.b = `rejected` }) - const secondSettled = second.isPersisted.promise.catch(() => {}) + const secondSettled = second.when(`settled`).catch(() => {}) try { if (completedFirst) { done[0]!.resolve() - await first.isPersisted.promise + await first.when(`settled`) } done[1]!.reject(new Error(`sibling failure`)) await secondSettled if (!completedFirst) { done[0]!.resolve() - await first.isPersisted.promise + await first.when(`settled`) } fixture.sync.begin() fixture.sync.write({ @@ -192,8 +192,8 @@ describe(`optimistic snapshot ownership`, () => { } finally { done.forEach((entry) => entry.resolve()) await Promise.allSettled([ - first.isPersisted.promise, - second.isPersisted.promise, + first.when(`settled`), + second.when(`settled`), ]) await fixture.collection.cleanup() } @@ -239,7 +239,7 @@ describe(`optimistic snapshot ownership`, () => { expect(applied).not.toBe(true) } done.resolve() - await tx.isPersisted.promise + await tx.when(`settled`) await held await applied // The optimistic row drops, and the held sync rows publish with it. @@ -355,7 +355,7 @@ describe(`optimistic snapshot ownership`, () => { const settled = fixture.collection.update(1, (row) => { row.b = `settled` }) - await settled.isPersisted.promise + await settled.when(`settled`) fixture.sync.begin() fixture.sync.write({ type: `insert`, value: { ...initial, id: 2 } }) expect(fixture.sync.commit()).not.toBe(true) diff --git a/packages/db/tests/optimistic-history-oracle.ts b/packages/db/tests/optimistic-history-oracle.ts index 8b8c6c3a94..90c2ee9622 100644 --- a/packages/db/tests/optimistic-history-oracle.ts +++ b/packages/db/tests/optimistic-history-oracle.ts @@ -35,7 +35,7 @@ import type { CollectionConfig, SyncConfig } from '../src/types.js' * settlement, even if a peer supplied it. Every same-key write in that atomic * transaction keeps that attribution. A later transaction is `'remote'` * without another local owner. A failed mutation gives queued source writes no - * local attribution. `isPersisted` settles after that publication. + * local attribution. `when('settled')` settles after that publication. * A truncate applies at once, with every queued sync transaction before it. * Its same-key source row can receive local attribution while a mutation still * persists, even if that mutation later fails. Still-persisting transactions @@ -66,7 +66,7 @@ import type { CollectionConfig, SyncConfig } from '../src/types.js' * starts. Source writes on its key apply at once and receive local attribution; * rolling it back exposes that source row with the same origin. A pending * manual transaction starts its mutation function at commit. If that function - * rejects, the transaction rolls back and `isPersisted` rejects with its + * rejects, the transaction rolls back and `when('settled')` rejects with its * reason. A later edit cannot determine the earlier manual request's outcome. * The model keeps pending distinct from persisting because the same source * commit publishes immediately in the former state and waits in the latter. @@ -109,7 +109,7 @@ import type { CollectionConfig, SyncConfig } from '../src/types.js' * `runOptimisticHistory` gives the same edit, delete, settle, and sync history * to this model and a real Collection. After every step it compares rows, * metadata, immutable handler payloads, promise outcomes, the rows visible - * when `isPersisted` fulfills, downstream query state, and complete + * when the settlement waiter fulfills, downstream query state, and complete * publication cuts. A second subscriber starts without initial state, so no * sent-key filter can hide an invalid message. Each of its batches must be * valid for its replica: an insert names an absent key, and an update or @@ -194,7 +194,6 @@ type ModelTransaction = { type ObservedRow = HistoryRow & { $origin: `local` | `remote` $hasPendingWrites: boolean - $synced: boolean } /** Pure event-history model. It never reads production caches or callbacks. */ @@ -237,7 +236,6 @@ class HistoryModel { ...row, $origin: this.origins.get(key)!, $hasPendingWrites: false, - $synced: true, }, ]), ) @@ -257,7 +255,6 @@ class HistoryModel { ...transaction.row, $origin: `local`, $hasPendingWrites: true, - $synced: false, }) } return result @@ -394,7 +391,6 @@ const observed = ( row: HistoryRow & { $origin: `local` | `remote` $hasPendingWrites: boolean - $synced: boolean }, ): ObservedRow => ({ ...Object.fromEntries( @@ -408,7 +404,6 @@ const observed = ( c: row.c, $origin: row.$origin, $hasPendingWrites: row.$hasPendingWrites, - $synced: row.$synced, }) const sorted = (rows: Iterable) => [...rows].sort((a, b) => a.id - b.id) @@ -604,7 +599,7 @@ export async function runOptimisticHistory( | ReturnType> done: ReturnType> outcome: ReturnType> - // Rows visible when `isPersisted` fulfilled or rejected. + // Rows visible when `when('settled')` fulfilled or rejected. settledRows?: Array expected: HistoryOutcome changes: object @@ -1048,7 +1043,7 @@ export async function runOptimisticHistory( tx, done, outcome: observeHistoryPromise( - tx.isPersisted.promise.finally(() => { + tx.when(`settled`).finally(() => { operation.settledRows = sorted( [...collection.values()].map(observed), ) @@ -1125,7 +1120,7 @@ export async function runOptimisticHistory( await Promise.resolve() expect( op.settledRows, - `${position}: rows visible when isPersisted settled`, + `${position}: rows visible when settlement completed`, ).toEqual(cuts[0]) counts.settlements++ if (!step.success) counts.failures++ diff --git a/packages/db/tests/optimistic-transaction-oracle.property.test.ts b/packages/db/tests/optimistic-transaction-oracle.property.test.ts index 0b9d8cad88..e110853835 100644 --- a/packages/db/tests/optimistic-transaction-oracle.property.test.ts +++ b/packages/db/tests/optimistic-transaction-oracle.property.test.ts @@ -539,7 +539,7 @@ async function runSameKey( return gate.promise }, }) - const persisted = observeHistoryPromise(tx.isPersisted.promise) + const persisted = observeHistoryPromise(tx.when(`settled`)) let committed: ReturnType | undefined await withHistoryCleanup( async () => { @@ -754,7 +754,7 @@ async function runDeleteInsertReplacement( return Promise.resolve() }, }) - const settlement = transaction.isPersisted.promise.catch(() => undefined) + const settlement = transaction.when(`settled`).catch(() => undefined) return withHistoryCleanup( async () => { @@ -1033,10 +1033,10 @@ describe(`Delete then insert transaction laws`, () => { return Promise.resolve() }, }) - const overlaySettlement = overlay.isPersisted.promise.catch(() => undefined) - const replacementSettlement = replacement.isPersisted.promise.catch( - () => undefined, - ) + const overlaySettlement = overlay.when(`settled`).catch(() => undefined) + const replacementSettlement = replacement + .when(`settled`) + .catch(() => undefined) await withHistoryCleanup( async () => { @@ -1111,12 +1111,12 @@ describe(`Delete then insert transaction laws`, () => { return Promise.resolve() }, }) - const insertSettlement = pendingInsert.isPersisted.promise.catch( - () => undefined, - ) - const replacementSettlement = replacement.isPersisted.promise.catch( - () => undefined, - ) + const insertSettlement = pendingInsert + .when(`settled`) + .catch(() => undefined) + const replacementSettlement = replacement + .when(`settled`) + .catch(() => undefined) await withHistoryCleanup( async () => { @@ -1167,13 +1167,7 @@ describe(`Delete then insert transaction laws`, () => { // Observe user fields without discarding unexpected fields or undefined keys. function userRow(value: T): T { const copy = { ...value } as Record - for (const field of [ - `$hasPendingWrites`, - `$synced`, - `$origin`, - `$key`, - `$collectionId`, - ]) + for (const field of [`$hasPendingWrites`, `$origin`, `$key`, `$collectionId`]) delete copy[field] return copy as T } @@ -1247,7 +1241,7 @@ async function runTransaction(scenario: Scenario, fault?: ObservationFault) { return gate.promise }, }) - const persisted = observeHistoryPromise(tx.isPersisted.promise) + const persisted = observeHistoryPromise(tx.when(`settled`)) const settled = observeHistoryPromise(tx.when(`settled`)) let committed: ReturnType> | undefined let subscription: ReturnType | undefined @@ -1276,10 +1270,6 @@ async function runTransaction(scenario: Scenario, fault?: ObservationFault) { row.$hasPendingWrites, `${label}: ${surface} row ${row.id} pending local write`, ).toBe(expectedPending) - expect( - row.$hasPendingWrites, - `${label}: ${surface} row ${row.id} legacy inverse`, - ).toBe(!row.$synced) } } const read = ordered(source.values()) @@ -1658,7 +1648,7 @@ for (const { numberKey, reversed } of [0, -1, 1, 1.5].flatMap((key) => return Promise.resolve() }, }) - const persisted = observeHistoryPromise(tx.isPersisted.promise) + const persisted = observeHistoryPromise(tx.when(`settled`)) await withHistoryCleanup( async () => { await source.preload() diff --git a/packages/db/tests/paced-mutations-oracle.test.ts b/packages/db/tests/paced-mutations-oracle.test.ts index b311f3be78..ce5b1fe94d 100644 --- a/packages/db/tests/paced-mutations-oracle.test.ts +++ b/packages/db/tests/paced-mutations-oracle.test.ts @@ -316,7 +316,7 @@ function observeReceipt(transaction: Transaction) { returnedSame?: boolean error?: unknown } = { outcome: `pending` } - void transaction.isPersisted.promise.then( + void transaction.when(`settled`).then( (resolved) => { receipt.outcome = `fulfilled` receipt.returnedSame = resolved === transaction @@ -382,7 +382,7 @@ async function runProduction( returnedSame: undefined as boolean | undefined, } persistence.set(transaction, observation) - void transaction.isPersisted.promise.then( + void transaction.when(`settled`).then( (resolved) => { observation.outcome = `fulfilled` observation.returnedSame = resolved === transaction @@ -1187,7 +1187,7 @@ describe(`paced mutation timeline oracle`, () => { await vi.advanceTimersByTimeAsync(0) expect(started).toEqual([1, 2, 3]) for (const transaction of [first, second, third]) { - expect(await transaction.isPersisted.promise).toBe(transaction) + expect(await transaction.when(`settled`)).toBe(transaction) } }, async () => { @@ -2909,7 +2909,7 @@ for (const admission of [`admitted`, `dropped`, `queued`] as const) const transaction = create.mock.results.at(-1)?.value as Transaction<{ id: number }> | undefined if (transaction?.state === `pending`) { - void transaction.isPersisted.promise.catch(() => {}) + void transaction.when(`settled`).catch(() => {}) transaction.rollback({ isSecondaryRollback: true }) } create.mockRestore() diff --git a/packages/db/tests/paced-mutations.test.ts b/packages/db/tests/paced-mutations.test.ts index 83d1692066..47a05efe3a 100644 --- a/packages/db/tests/paced-mutations.test.ts +++ b/packages/db/tests/paced-mutations.test.ts @@ -260,7 +260,7 @@ describe(`createPacedMutations`, () => { // Now trailing edge should have executed expect(mutationFn).toHaveBeenCalledTimes(1) - await tx1.isPersisted.promise + await tx1.when(`settled`) expect(tx1.state).toBe(`completed`) }) }) @@ -299,7 +299,7 @@ describe(`createPacedMutations`, () => { expect(mutationFn).toHaveBeenCalledTimes(1) // Wait for first transaction to complete - await tx1.isPersisted.promise + await tx1.when(`settled`) expect(tx1.state).toBe(`completed`) // Each mutation should be in its own transaction @@ -348,9 +348,9 @@ describe(`createPacedMutations`, () => { // Wait for all to complete await Promise.all([ - tx1.isPersisted.promise, - tx2.isPersisted.promise, - tx3.isPersisted.promise, + tx1.when(`settled`), + tx2.when(`settled`), + tx3.when(`settled`), ]) // Should have executed in order @@ -390,9 +390,9 @@ describe(`createPacedMutations`, () => { expect(tx1).not.toBe(tx3) await Promise.all([ - tx1.isPersisted.promise, - tx2.isPersisted.promise, - tx3.isPersisted.promise, + tx1.when(`settled`), + tx2.when(`settled`), + tx3.when(`settled`), ]) // All mutations should have been executed @@ -427,7 +427,7 @@ describe(`createPacedMutations`, () => { const tx2 = mutateExplicitZero({ id: 2, value: 2 }) // Should still process sequentially even with zero wait - await Promise.all([tx1.isPersisted.promise, tx2.isPersisted.promise]) + await Promise.all([tx1.when(`settled`), tx2.when(`settled`)]) expect(mutationFn).toHaveBeenCalledTimes(2) expect(tx1.state).toBe(`completed`) @@ -447,7 +447,7 @@ describe(`createPacedMutations`, () => { const tx3 = mutateNoWait({ id: 3, value: 3 }) const tx4 = mutateNoWait({ id: 4, value: 4 }) - await Promise.all([tx3.isPersisted.promise, tx4.isPersisted.promise]) + await Promise.all([tx3.when(`settled`), tx4.when(`settled`)]) expect(mutationFn).toHaveBeenCalledTimes(2) expect(tx3.state).toBe(`completed`) @@ -482,7 +482,7 @@ describe(`createPacedMutations`, () => { // Transaction should be in failed state expect(tx.state).toBe(`failed`) - await expect(tx.isPersisted.promise).rejects.toThrow(`Mutation failed`) + await expect(tx.when(`settled`)).rejects.toThrow(`Mutation failed`) }) it(`should continue processing queue after an error`, async () => { @@ -511,9 +511,9 @@ describe(`createPacedMutations`, () => { // Wait for all to settle await Promise.allSettled([ - tx1.isPersisted.promise, - tx2.isPersisted.promise, - tx3.isPersisted.promise, + tx1.when(`settled`), + tx2.when(`settled`), + tx3.when(`settled`), ]) // First should fail, rest should succeed diff --git a/packages/db/tests/proxy-detachment-contract-oracle.test.ts b/packages/db/tests/proxy-detachment-contract-oracle.test.ts index 9f4ea51bfe..933fa58219 100644 --- a/packages/db/tests/proxy-detachment-contract-oracle.test.ts +++ b/packages/db/tests/proxy-detachment-contract-oracle.test.ts @@ -207,7 +207,7 @@ describe(`Mutation result detachment`, () => { const collection = storedDataRow(make(), identitySchema) try { const tx = collection.update(1, run) - await tx.isPersisted.promise + await tx.when(`settled`) expectOwnDataProperty(collection.get(1)!, key, { value: 2 }) } finally { await collection.cleanup() @@ -258,7 +258,7 @@ describe(`Mutation result detachment`, () => { const collection = storedDataRow(make(), identitySchema) try { const tx = collection.update(1, run) - await tx.isPersisted.promise + await tx.when(`settled`) const saved = collection.get(1)! expect(saved.read).toBe(expected.read) // Collection patches represent deletion with an own undefined. @@ -315,7 +315,7 @@ describe(`Mutation result detachment`, () => { const collection = storedRow(make()) try { const tx = collection.update(1, run) - await tx.isPersisted.promise + await tx.when(`settled`) expect(collection.get(1)!.child).toStrictEqual(expected.child) expect(collection.get(1)!.alias).toStrictEqual(expected.alias) } finally { @@ -362,7 +362,7 @@ describe(`Mutation result detachment`, () => { const tx = collection.update(1, (draft) => { for (const value of values) draft.value = makeValue(value) }) - await tx.isPersisted.promise + await tx.when(`settled`) expect(observe(collection.get(1)!.value)).toStrictEqual( observe(expected.value), ) @@ -435,7 +435,7 @@ describe(`Mutation result detachment`, () => { const collection = storedRow(make()) try { const tx = collection.update(1, reorder) - await tx.isPersisted.promise + await tx.when(`settled`) expect(entries(collection.get(1)!.value)).toEqual( entries(expected.value), ) @@ -525,10 +525,10 @@ describe(`Mutation result detachment`, () => { draft.link = cycleContainer(kind, draft) if (edit === `after`) draft.count = 1 }) - settled = Promise.allSettled([tx.isPersisted.promise]) + settled = Promise.allSettled([tx.when(`settled`)]) expectCycleSnapshot(collection.get(1)!, kind) expect(original).toEqual({ id: 1, count: 0 }) - await tx.isPersisted.promise + await tx.when(`settled`) await settled expectCycleSnapshot(collection.get(1)!, kind) expect(original).toEqual({ id: 1, count: 0 }) @@ -576,7 +576,7 @@ describe(`Mutation result detachment`, () => { expect(collection.get(1)!.value).toBe(label) label.rename(`after`) expect((collection.get(1)!.value as Label).read()).toBe(`after`) - await tx.isPersisted.promise + await tx.when(`settled`) } finally { await collection.cleanup() } @@ -620,7 +620,7 @@ describe(`Mutation result detachment`, () => { else if (value instanceof Label) expect((saved as Label).read()).toBe(`saved`) else expect(saved).toEqual(value) - await tx.isPersisted.promise + await tx.when(`settled`) } finally { await collection.cleanup() } @@ -656,7 +656,7 @@ describe(`Mutation result detachment`, () => { }) value.pathname = `/after` expect((collection.get(1)!.value as URL).pathname).toBe(`/before`) - await tx.isPersisted.promise + await tx.when(`settled`) } finally { await collection.cleanup() } @@ -699,7 +699,7 @@ describe(`Mutation result detachment`, () => { ? back.s.values().next().value!.back : back.arr[0]!.owner expect(cycle).toBe(back) - await tx.isPersisted.promise + await tx.when(`settled`) } finally { await collection.cleanup() } diff --git a/packages/db/tests/proxy-iteration-contract-oracle.test.ts b/packages/db/tests/proxy-iteration-contract-oracle.test.ts index 3c34a57a97..871da0f0f5 100644 --- a/packages/db/tests/proxy-iteration-contract-oracle.test.ts +++ b/packages/db/tests/proxy-iteration-contract-oracle.test.ts @@ -637,7 +637,7 @@ it.each( expected, ) expectMembers(collection.get(1)!.values, expected) - await tx.isPersisted.promise + await tx.when(`settled`) expectMembers(collection.get(1)!.values, expected) } finally { await collection.cleanup() @@ -675,7 +675,7 @@ it.each([`Map`, `Set`] as const)( expect([...collection.get(1)!.values.values()]).toEqual([{ x: 2 }]) item.x = 3 expect([...collection.get(1)!.values.values()]).toEqual([{ x: 2 }]) - await tx.isPersisted.promise + await tx.when(`settled`) } finally { await collection.cleanup() } diff --git a/packages/db/tests/proxy-oracle.test.ts b/packages/db/tests/proxy-oracle.test.ts index db57e3901a..324e487daa 100644 --- a/packages/db/tests/proxy-oracle.test.ts +++ b/packages/db/tests/proxy-oracle.test.ts @@ -213,7 +213,7 @@ describe(`native array callback oracle`, () => { }) try { const tx = collection.update(1, run) - await tx.isPersisted.promise + await tx.when(`settled`) const saved = collection.get(1)! // This oracle covers row data, not collection-owned virtual fields. expect({ id: saved.id, items: saved.items }).toStrictEqual(expected) diff --git a/packages/db/tests/query/compiler/group-by-pipeline.test.ts b/packages/db/tests/query/compiler/group-by-pipeline.test.ts index ee4794774e..3a50a02244 100644 --- a/packages/db/tests/query/compiler/group-by-pipeline.test.ts +++ b/packages/db/tests/query/compiler/group-by-pipeline.test.ts @@ -36,32 +36,23 @@ const cases = [false, true].flatMap((grouped) => ) describe(`group-by production pipeline`, () => { - // Current Collections publish both fields. These relation rows also cover - // the new-field-only input expected after the deprecated alias is removed. + // Group metadata reports a pending write when any contributing row has one. test.each([ { - name: `new-field-only pending row`, - rows: [{ key: `first`, pending: true, shape: `new` }], + name: `pending row`, + rows: [{ key: `first`, pending: true }], }, { - name: `new-field-only settled row`, - rows: [{ key: `first`, pending: false, shape: `new` }], + name: `settled row`, + rows: [{ key: `first`, pending: false }], }, { - name: `legacy pending row`, - rows: [{ key: `first`, pending: true, shape: `legacy` }], - }, - { - name: `mixed new and legacy rows`, + name: `mixed pending and settled rows`, rows: [ - { key: `first`, pending: true, shape: `new` }, - { key: `second`, pending: false, shape: `legacy` }, + { key: `first`, pending: true }, + { key: `second`, pending: false }, ], }, - { - name: `current rows with both fields`, - rows: [{ key: `first`, pending: true, shape: `both` }], - }, ] as const)(`derives grouped pending writes from $name`, ({ rows }) => { const graph = new D2() const input = graph.newInput() @@ -84,14 +75,13 @@ describe(`group-by production pipeline`, () => { input.sendData( new MultiSet( - rows.map(({ key, pending, shape }): [KeyedNamespacedRow, number] => [ + rows.map(({ key, pending }): [KeyedNamespacedRow, number] => [ [ key, { row: { id: key, - ...(shape !== `legacy` ? { $hasPendingWrites: pending } : {}), - ...(shape !== `new` ? { $synced: !pending } : {}), + $hasPendingWrites: pending, }, }, ], @@ -105,7 +95,6 @@ describe(`group-by production pipeline`, () => { const grouped = actual.getInner()[0]![0][1] const expectedPending = rows.some((row) => row.pending) expect(grouped.$hasPendingWrites).toBe(expectedPending) - expect(grouped.$synced).toBe(!expectedPending) }) // A source field that is not grouped has no single value, with or without @@ -152,7 +141,7 @@ describe(`group-by production pipeline`, () => { type Result = { key: unknown selected: unknown - synced: unknown + hasPendingWrites: unknown origin: unknown } let actual = new MultiSet() @@ -196,7 +185,7 @@ describe(`group-by production pipeline`, () => { return { key, selected: row.$selected, - synced: row.$synced, + hasPendingWrites: row.$hasPendingWrites, origin: row.$origin, } }), @@ -215,7 +204,7 @@ describe(`group-by production pipeline`, () => { { row: { ...row, - $synced: !row.local, + $hasPendingWrites: row.local, $origin: row.local ? `local` : `remote`, }, }, @@ -252,7 +241,7 @@ describe(`group-by production pipeline`, () => { ...(grouped ? { group: key } : {}), [totalAlias]: amount, }, - synced: members.every((row) => !row.local), + hasPendingWrites: members.some((row) => row.local), origin: members.some((row) => row.local) ? `local` : `remote`, }, ] diff --git a/packages/db/tests/query/derived-delete-reconciliation-oracle.test.ts b/packages/db/tests/query/derived-delete-reconciliation-oracle.test.ts index 7b05413b4a..89348453c4 100644 --- a/packages/db/tests/query/derived-delete-reconciliation-oracle.test.ts +++ b/packages/db/tests/query/derived-delete-reconciliation-oracle.test.ts @@ -53,7 +53,7 @@ async function runFlushHistory( }) const done = createDeferred() const tx = createTransaction({ mutationFn: () => done.promise }) - const settled = tx.isPersisted.promise.catch((error: unknown) => error) + const settled = tx.when(`settled`).catch((error: unknown) => error) const lookup = vi.spyOn(derived._state, `createSyncedKeyLookup`) try { await derived.preload() @@ -221,7 +221,7 @@ describe(`derived updates beneath optimistic deletes`, () => { }) const done = createDeferred() const tx = createTransaction({ mutationFn: () => done.promise }) - const settled = tx.isPersisted.promise.catch(() => {}) + const settled = tx.when(`settled`).catch(() => {}) try { await derived.preload() tx.mutate(() => derived.delete(0)) @@ -296,7 +296,7 @@ describe(`derived updates beneath optimistic deletes`, () => { }) const done = createDeferred() const tx = createTransaction({ mutationFn: () => done.promise }) - const settled = tx.isPersisted.promise.catch((error: unknown) => error) + const settled = tx.when(`settled`).catch((error: unknown) => error) const read = (collection: { values: () => Iterable }) => [...collection.values()] .map((row) => ({ id: row.id, value: row.value })) diff --git a/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts b/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts index 5f4e9d49fa..a3c1ac2324 100644 --- a/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts +++ b/packages/db/tests/query/includes-alias-shadowing-oracle.test.ts @@ -67,7 +67,6 @@ const rootVirtualKeys = [ `$hasPendingWrites`, `$key`, `$origin`, - `$synced`, ] const voteAt = (context: Context, alias: ChildAlias): Vote => diff --git a/packages/db/tests/query/includes-collection-oracle.property.test.ts b/packages/db/tests/query/includes-collection-oracle.property.test.ts index b162ba7e5e..fc55c3a372 100644 --- a/packages/db/tests/query/includes-collection-oracle.property.test.ts +++ b/packages/db/tests/query/includes-collection-oracle.property.test.ts @@ -442,7 +442,7 @@ function createOptimisticCollectionDriver( } pending.add(entry) // Observe both outcomes before the first fallible optimistic checkpoint. - entry.receipt = transaction.isPersisted.promise.then( + entry.receipt = transaction.when(`settled`).then( () => { pending.delete(entry) }, @@ -473,13 +473,11 @@ function createOptimisticCollectionDriver( context.children.write(`insert`, action.row) pendingMutation.syncReleased = true context.children.resolveSync() - await transaction.isPersisted.promise + await transaction.when(`settled`) } else { context.model.children.delete(action.row.id) const message = `rollback insert` - const persisted = transaction.isPersisted.promise.catch( - () => undefined, - ) + const persisted = transaction.when(`settled`).catch(() => undefined) await withExpectedRejection(message, async () => { pendingMutation.syncReleased = true context.children.rejectSync(new Error(message)) @@ -503,11 +501,11 @@ function createOptimisticCollectionDriver( context.children.write(`delete`, previous) pendingMutation.syncReleased = true context.children.resolveSync() - await transaction.isPersisted.promise + await transaction.when(`settled`) } else { context.model.children.set(action.id, previous) const message = `rollback delete` - const persisted = transaction.isPersisted.promise.catch(() => undefined) + const persisted = transaction.when(`settled`).catch(() => undefined) await withExpectedRejection(message, async () => { pendingMutation.syncReleased = true context.children.rejectSync(new Error(message)) diff --git a/packages/db/tests/query/includes-context-transport-oracle.test.ts b/packages/db/tests/query/includes-context-transport-oracle.test.ts index 4768204add..32b377cde2 100644 --- a/packages/db/tests/query/includes-context-transport-oracle.test.ts +++ b/packages/db/tests/query/includes-context-transport-oracle.test.ts @@ -1643,7 +1643,6 @@ function expectNoRouteMetadata(row: object): void { // Public VirtualRowProps names these five keys, not arbitrary $-prefixed data. const publicMetadataKeys = new Set([ `$hasPendingWrites`, - `$synced`, `$origin`, `$key`, `$collectionId`, diff --git a/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts b/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts index 46523e423b..bda198f85a 100644 --- a/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts +++ b/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts @@ -194,7 +194,6 @@ type ReferenceContextChild = { // this projection. const virtualKeys = new Set([ `$hasPendingWrites`, - `$synced`, `$origin`, `$key`, `$collectionId`, @@ -1327,7 +1326,6 @@ describe(`includes cross-formulation oracle`, () => { const rows = captureOrderedNested(expected) Object.assign(rows[0]!, { $hasPendingWrites: false, - $synced: true, $origin: `remote`, $key: 0, $collectionId: `source`, diff --git a/packages/db/tests/query/includes-functional-projection-oracle.test.ts b/packages/db/tests/query/includes-functional-projection-oracle.test.ts index 1ecabac7dd..a539f98da9 100644 --- a/packages/db/tests/query/includes-functional-projection-oracle.test.ts +++ b/packages/db/tests/query/includes-functional-projection-oracle.test.ts @@ -655,7 +655,7 @@ describe(`functional projection output compatibility`, () => { case `virtual-metadata`: ids = view.toArray.map((child) => { expect(child.$collectionId).toBe(children.collection.id) - expect(child.$synced).toBe(true) + expect(child.$hasPendingWrites).toBe(false) expect(child.$origin).toBe(`remote`) return child.id }) diff --git a/packages/db/tests/query/includes-optimistic-oracle.property.test.ts b/packages/db/tests/query/includes-optimistic-oracle.property.test.ts index 1a2013145d..de774bc322 100644 --- a/packages/db/tests/query/includes-optimistic-oracle.property.test.ts +++ b/packages/db/tests/query/includes-optimistic-oracle.property.test.ts @@ -192,7 +192,7 @@ function trackRuntimeMutation( if (entry.syncReleased) context.runtimePending.delete(entry) } // Attach both outcome handlers before a checkpoint or sibling source write. - entry.receipt = transaction.isPersisted.promise.then(settled, settled) + entry.receipt = transaction.when(`settled`).then(settled, settled) return entry } @@ -411,7 +411,7 @@ async function rollback( runtime: PendingRuntimeMutation, ) { const message = `optimistic relationship oracle rollback` - const persisted = transaction.isPersisted.promise.catch(() => undefined) + const persisted = transaction.when(`settled`).catch(() => undefined) await withExpectedRejection(message, async () => { releaseRuntimeMutation(context, runtime, () => source.rejectSync(new Error(message)), @@ -512,7 +512,7 @@ function createDriver( // Sync delivery must not displace the pending optimistic projection. checkpoint() releaseRuntimeMutation(context, pending.runtime, source.resolveSync) - await pending.transaction.isPersisted.promise + await pending.transaction.when(`settled`) context.pending.delete(step.handle) }, cleanup: async (context) => { @@ -821,7 +821,7 @@ describe(`optimistic relationship-transition oracle`, () => { const transaction = roots.update(1, (draft) => { draft.value = `B` }) - persistence = transaction.isPersisted.promise.then( + persistence = transaction.when(`settled`).then( () => undefined, () => undefined, ) diff --git a/packages/db/tests/query/includes-oracle.property.test.ts b/packages/db/tests/query/includes-oracle.property.test.ts index d9da48f4ab..796a935f68 100644 --- a/packages/db/tests/query/includes-oracle.property.test.ts +++ b/packages/db/tests/query/includes-oracle.property.test.ts @@ -791,7 +791,7 @@ function trackPendingWork( entry.settled = true if (entry.released) pending.delete(entry) } - entry.receipt = transaction.isPersisted.promise.then(settled, settled) + entry.receipt = transaction.when(`settled`).then(settled, settled) return entry } @@ -947,7 +947,7 @@ async function applyAction( action, sources.roots.resolveSync, sources.roots.rejectSync, - transaction.isPersisted.promise, + transaction.when(`settled`), pending, entry, ) @@ -1009,7 +1009,7 @@ async function applyAction( action, source.resolveSync, source.rejectSync, - transaction.isPersisted.promise, + transaction.when(`settled`), pending, entry, ) @@ -5360,7 +5360,7 @@ describe(`includes recompute oracle`, () => { position: 0, }) releasePendingWork(pending, entry, children.resolveSync) - await transaction.isPersisted.promise + await transaction.when(`settled`) expect(stripVirtualProperties(live.toArray)).toEqual([ { id: 1, children: [{ id: 1, value: confirmedValue }] }, diff --git a/packages/db/tests/query/includes-publication-oracle.test.ts b/packages/db/tests/query/includes-publication-oracle.test.ts index a05fe7406e..c41e8ec018 100644 --- a/packages/db/tests/query/includes-publication-oracle.test.ts +++ b/packages/db/tests/query/includes-publication-oracle.test.ts @@ -515,13 +515,13 @@ function createPublicationDriver( if (action.type === `optimisticConfirm`) { context.sources.parents.write(`update`, next) context.sources.parents.resolveSync() - await transaction.isPersisted.promise + await transaction.when(`settled`) } else { const beforeRollbackCallbacks = context.callbacks.length context.model.parents.set(previous.id, previous) await settleRollback( context.sources.parents.rejectSync, - transaction.isPersisted.promise, + transaction.when(`settled`), ) checkpointWithCallbackReach(beforeRollbackCallbacks) } diff --git a/packages/db/tests/query/includes-temporal-oracle.test.ts b/packages/db/tests/query/includes-temporal-oracle.test.ts index 7c2fc9f24f..9de178e5cc 100644 --- a/packages/db/tests/query/includes-temporal-oracle.test.ts +++ b/packages/db/tests/query/includes-temporal-oracle.test.ts @@ -817,7 +817,7 @@ it.each( postId: 999, body: `unrelated`, }) - transactionOutcome = transaction.isPersisted.promise.then( + transactionOutcome = transaction.when(`settled`).then( () => undefined, () => undefined, ) @@ -845,7 +845,7 @@ it.each( }) } mutation.resolve() - if (transaction) await transaction.isPersisted.promise + if (transaction) await transaction.when(`settled`) await preload await Promise.all(requests.map(({ outcome }) => outcome)) expect(requests.every(({ settled }) => settled)).toBe(true) diff --git a/packages/db/tests/query/includes-work-counter-oracle.test.ts b/packages/db/tests/query/includes-work-counter-oracle.test.ts index 1296cb0f86..bdb732398d 100644 --- a/packages/db/tests/query/includes-work-counter-oracle.test.ts +++ b/packages/db/tests/query/includes-work-counter-oracle.test.ts @@ -1088,7 +1088,7 @@ async function checkWindowedIncludeWork(testCase: WindowCase): Promise { } await ( type === `insert` ? childSource.insert(row) : childSource.delete(row.id) - ).isPersisted.promise + ).when(`settled`) } const live = createLiveQueryCollection((q) => { @@ -1310,9 +1310,11 @@ async function checkWindowedIncludeWork(testCase: WindowCase): Promise { if (!source) { const added = addChildren(group, testCase.childrenPerGroup) for (const child of added) await changeChild(`insert`, child) - await grandchildSource.insert( - added.map((child) => grandchildren.get(`grand-${child.id}`)!), - ).isPersisted.promise + await grandchildSource + .insert( + added.map((child) => grandchildren.get(`grand-${child.id}`)!), + ) + .when(`settled`) } const parent = { id: `parent-new-${step}`, @@ -1320,12 +1322,12 @@ async function checkWindowedIncludeWork(testCase: WindowCase): Promise { group, } parents.set(parent.id, parent) - await parentSource.insert(parent).isPersisted.promise + await parentSource.insert(parent).when(`settled`) } else if (command.type === `delete-parent`) { if (ids.length === 0) continue const id = ids[command.pick % ids.length]! parents.delete(id) - await parentSource.delete(id).isPersisted.promise + await parentSource.delete(id).when(`settled`) } else if (command.type === `rank`) { if (ids.length === 0) continue const id = ids[command.pick % ids.length]! @@ -1335,9 +1337,11 @@ async function checkWindowedIncludeWork(testCase: WindowCase): Promise { ? Math.max(...ranks) + 1 : Math.min(...ranks) - 1 parents.set(id, { ...parents.get(id)!, rank }) - await parentSource.update(id, (draft) => { - draft.rank = rank - }).isPersisted.promise + await parentSource + .update(id, (draft) => { + draft.rank = rank + }) + .when(`settled`) } else if (command.type === `insert-child`) { const groups = [...new Set([...parents.values()].map((p) => p.group))] if (groups.length === 0) continue diff --git a/packages/db/tests/query/includes.test-d.ts b/packages/db/tests/query/includes.test-d.ts index da1f6f591f..e17af5f7ed 100644 --- a/packages/db/tests/query/includes.test-d.ts +++ b/packages/db/tests/query/includes.test-d.ts @@ -117,7 +117,7 @@ describe(`includes subquery types`, () => { const result = collection.toArray[0]! expectTypeOf(result.id).toEqualTypeOf() expectTypeOf(result.name).toEqualTypeOf() - expectTypeOf(result.$synced).toEqualTypeOf() + expectTypeOf(result.$hasPendingWrites).toEqualTypeOf() expectTypeOf(result.$origin).toEqualTypeOf<`local` | `remote`>() expectTypeOf(result.$key).toEqualTypeOf() expectTypeOf(result.$collectionId).toEqualTypeOf() @@ -136,7 +136,7 @@ describe(`includes subquery types`, () => { const result = collection.toArray[0]! expectTypeOf(result.id).toEqualTypeOf() - expectTypeOf(result.$synced).toEqualTypeOf() + expectTypeOf(result.$hasPendingWrites).toEqualTypeOf() expectTypeOf(result.$origin).toEqualTypeOf<`local` | `remote`>() expectTypeOf(result.$key).toEqualTypeOf() expectTypeOf(result.$collectionId).toEqualTypeOf() @@ -199,7 +199,9 @@ describe(`includes subquery types`, () => { const result = collection.toArray[0]! expectTypeOf(result.issues.toArray[0]!.id).toEqualTypeOf() expectTypeOf(result.issues.toArray[0]!.title).toEqualTypeOf() - expectTypeOf(result.issues.toArray[0]!.$synced).toEqualTypeOf() + expectTypeOf( + result.issues.toArray[0]!.$hasPendingWrites, + ).toEqualTypeOf() expectTypeOf(result.issues.toArray[0]!.$origin).toEqualTypeOf< `local` | `remote` >() @@ -236,7 +238,7 @@ describe(`includes subquery types`, () => { const result = collection.toArray[0]! expectTypeOf(result.id).toEqualTypeOf() expectTypeOf(result.name).toEqualTypeOf() - expectTypeOf(result.$synced).toEqualTypeOf() + expectTypeOf(result.$hasPendingWrites).toEqualTypeOf() expectTypeOf(result.$origin).toEqualTypeOf<`local` | `remote`>() expectTypeOf(result.$key).toEqualTypeOf() expectTypeOf(result.$collectionId).toEqualTypeOf() @@ -258,7 +260,7 @@ describe(`includes subquery types`, () => { const result = collection.toArray[0]! expectTypeOf(result.id).toEqualTypeOf() - expectTypeOf(result.$synced).toEqualTypeOf() + expectTypeOf(result.$hasPendingWrites).toEqualTypeOf() expectTypeOf(result.$origin).toEqualTypeOf<`local` | `remote`>() expectTypeOf(result.$key).toEqualTypeOf() expectTypeOf(result.$collectionId).toEqualTypeOf() @@ -292,7 +294,7 @@ describe(`includes subquery types`, () => { const result = collection.toArray[0]! expectTypeOf(result.id).toEqualTypeOf() - expectTypeOf(result.$synced).toEqualTypeOf() + expectTypeOf(result.$hasPendingWrites).toEqualTypeOf() expectTypeOf(result.$origin).toEqualTypeOf<`local` | `remote`>() expectTypeOf(result.$key).toEqualTypeOf() expectTypeOf(result.$collectionId).toEqualTypeOf() diff --git a/packages/db/tests/query/live-query-collection.test.ts b/packages/db/tests/query/live-query-collection.test.ts index 99f211ceb6..fd621366c5 100644 --- a/packages/db/tests/query/live-query-collection.test.ts +++ b/packages/db/tests/query/live-query-collection.test.ts @@ -1281,7 +1281,7 @@ describe(`createLiveQueryCollection`, () => { const tx2 = base.insert({ id: `2`, created_at: Date.now() + 1 }) await vi.advanceTimersByTimeAsync(2000) - await Promise.all([tx1.isPersisted.promise, tx2.isPersisted.promise]) + await Promise.all([tx1.when(`settled`), tx2.when(`settled`)]) expect(base.size).toBe(2) expect(live.size).toBe(2) @@ -1342,7 +1342,7 @@ describe(`createLiveQueryCollection`, () => { ) await vi.advanceTimersByTimeAsync(5000) - await Promise.all(transactions.map((tx) => tx.isPersisted.promise)) + await Promise.all(transactions.map((tx) => tx.when(`settled`))) expect(base.size).toBe(5) expect(live.size).toBe(5) @@ -1465,8 +1465,8 @@ describe(`createLiveQueryCollection`, () => { draft.completed = !draft.completed }) // Observe each real receipt before any assertion can abandon it. - void tx.isPersisted.promise.catch(() => {}) - receipts.push(tx.isPersisted.promise) + void tx.when(`settled`).catch(() => {}) + receipts.push(tx.when(`settled`)) const previous = expected.get(id)! expected.set(id, { ...previous, diff --git a/packages/db/tests/query/load-subset-oracle.property.test.ts b/packages/db/tests/query/load-subset-oracle.property.test.ts index 5c44b67f41..feea2793f1 100644 --- a/packages/db/tests/query/load-subset-oracle.property.test.ts +++ b/packages/db/tests/query/load-subset-oracle.property.test.ts @@ -471,7 +471,7 @@ async function expectPersistingLoadIsApplied( const transaction = createTransaction({ mutationFn: () => persistence.promise, }) - settlement = transaction.isPersisted.promise + settlement = transaction.when(`settled`) if (persisting) { transaction.mutate(() => source.insert({ id: `other`, projectId: `p2` })) } @@ -497,7 +497,7 @@ async function expectPersistingLoadIsApplied( expect(source.get(`r2`)).toBeUndefined() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) } const result = await ready @@ -571,7 +571,7 @@ async function expectAppliedReceiptTiming( const transaction = createTransaction({ mutationFn: () => persistence.promise, }) - settlement = transaction.isPersisted.promise + settlement = transaction.when(`settled`) if (gate === `parked`) { transaction.mutate(() => source.insert({ id: `local`, projectId: `p2` })) } @@ -607,7 +607,7 @@ async function expectAppliedReceiptTiming( expect(fulfilled).toBe(false) expect(source.get(`remote`)).toBeUndefined() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) } await pending @@ -676,7 +676,7 @@ async function expectAppliedLoadDoesNotFlushEarlierParkedSync() { const transaction = createTransaction({ mutationFn: () => persistence.promise, }) - settlement = transaction.isPersisted.promise + settlement = transaction.when(`settled`) transaction.mutate(() => source.insert({ id: `other`, projectId: `p2` })) publishUnrelated() const live = createLiveQueryCollection((query) => @@ -699,7 +699,7 @@ async function expectAppliedLoadDoesNotFlushEarlierParkedSync() { expect(source.get(`unrelated`)).toBeUndefined() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await expect(ready).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ id: `r1` }), @@ -766,7 +766,7 @@ async function expectCompletionWaitsForAppliedRows() { const transaction = createTransaction({ mutationFn: () => persistence.promise, }) - settlement = transaction.isPersisted.promise + settlement = transaction.when(`settled`) transaction.mutate(() => source.insert({ id: `other`, projectId: `p2` })) publishUnrelated() const first = source._sync.loadSubset({}) @@ -780,7 +780,7 @@ async function expectCompletionWaitsForAppliedRows() { expect(source.get(`r1`)).toBeUndefined() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await Promise.all([first, concurrent]) expect(source.get(`r1`)).toEqual( expect.objectContaining({ id: `r1`, projectId: `p1` }), @@ -867,7 +867,7 @@ async function expectConcurrentStreamCommitStaysParked( const transaction = createTransaction({ mutationFn: () => persistence.promise, }) - settlement = transaction.isPersisted.promise + settlement = transaction.when(`settled`) transaction.mutate(() => source.insert({ id: `other`, projectId: `p2` })) const load = requirePendingLoadSubsetResult(source._sync.loadSubset({})) void load.catch(() => undefined) @@ -892,7 +892,7 @@ async function expectConcurrentStreamCommitStaysParked( return } persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await load expect(source.get(`unrelated`)).toEqual( expect.objectContaining({ id: `unrelated`, projectId: `p2` }), @@ -980,7 +980,7 @@ async function expectAbortedReceiptDoesNotSettleDemand( const transaction = createTransaction({ mutationFn: () => persistence.promise, }) - settlement = transaction.isPersisted.promise + settlement = transaction.when(`settled`) transaction.mutate(() => source.insert({ id: `local`, projectId: `p2` })) const first = requirePendingLoadSubsetResult( source._sync.loadSubset({ signal: controller.signal }), @@ -990,7 +990,7 @@ async function expectAbortedReceiptDoesNotSettleDemand( } controller.abort() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) // An accepted transaction always applies, so the row publishes when the // mutation settles. A caller that aborted still sees `AbortError`. if (abortPhase === `while-parked`) @@ -1054,7 +1054,7 @@ async function expectAbortDuringPublicationDoesNotCancelReceipt() { const transaction = createTransaction({ mutationFn: () => persistence.promise, }) - settlement = transaction.isPersisted.promise + settlement = transaction.when(`settled`) transaction.mutate(() => source.insert({ id: `local`, projectId: `pending` }), ) @@ -1068,7 +1068,7 @@ async function expectAbortDuringPublicationDoesNotCancelReceipt() { source._sync.loadSubset({ signal: controller.signal }), ) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await expect(load).resolves.toBeUndefined() expect(controller.signal.aborted).toBe(true) expect(source.get(`row`)).toEqual(expect.objectContaining({ id: `row` })) @@ -1120,7 +1120,7 @@ async function expectCanceledReceiptReleasesOnlyItsSuppression() { const transaction = createTransaction({ mutationFn: () => persistence.promise, }) - settlement = transaction.isPersisted.promise + settlement = transaction.when(`settled`) transaction.mutate(() => source.insert({ id: `local`, projectId: `pending` }), ) @@ -1221,7 +1221,7 @@ async function expectCleanupRejectsDemandOnce() { const transaction = createTransaction({ mutationFn: () => persistence.promise, }) - settlement = transaction.isPersisted.promise + settlement = transaction.when(`settled`) transaction.mutate(() => source.insert({ id: `local`, projectId: `p2` })) const load = requirePendingLoadSubsetResult(source._sync.loadSubset({})) void load.catch(() => undefined) @@ -1239,7 +1239,7 @@ async function expectCleanupRejectsDemandOnce() { expect(settlements).toBe(1) persistence.resolve() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await Promise.resolve() expect(settlements).toBe(1) @@ -1312,7 +1312,7 @@ async function expectDerivedSyncDuringOptimisticMutation(): Promise { id: `optimistic`, value: `optimistic`, }) - settlement = transaction.isPersisted.promise + settlement = transaction.when(`settled`) begin() write({ type: `insert`, value: { id: `synced`, value: `synced` } }) commit() diff --git a/packages/db/tests/query/load-subset-transaction-refinement-oracle.test.ts b/packages/db/tests/query/load-subset-transaction-refinement-oracle.test.ts index b7f3c5ac2b..a20d072881 100644 --- a/packages/db/tests/query/load-subset-transaction-refinement-oracle.test.ts +++ b/packages/db/tests/query/load-subset-transaction-refinement-oracle.test.ts @@ -190,7 +190,7 @@ describe(`loadSubset transaction refinement`, () => { } persistence.resolve() - await blocker.isPersisted.promise + await blocker.when(`settled`) if (expected.load === `rejects`) { await expect(load).rejects.toMatchObject({ name: `AbortError` }) @@ -212,7 +212,7 @@ describe(`loadSubset transaction refinement`, () => { } finally { persistence.resolve() try { - await blocker.isPersisted.promise + await blocker.when(`settled`) } catch (error) { cleanupFailures.push(error) } diff --git a/packages/db/tests/query/ordered-work-oracle.property.test.ts b/packages/db/tests/query/ordered-work-oracle.property.test.ts index ebf37321c2..d63ae771e7 100644 --- a/packages/db/tests/query/ordered-work-oracle.property.test.ts +++ b/packages/db/tests/query/ordered-work-oracle.property.test.ts @@ -251,13 +251,7 @@ function requestFingerprint(options: LoadSubsetOptions): string { function copiedRow(row: Row): Row { const copy: Record = { ...row } - for (const field of [ - `$key`, - `$collectionId`, - `$origin`, - `$hasPendingWrites`, - `$synced`, - ]) + for (const field of [`$key`, `$collectionId`, `$origin`, `$hasPendingWrites`]) delete copy[field] return copy as Row } @@ -5408,7 +5402,7 @@ async function runEagerWindow(testCase: EagerCase): Promise<{ matched ? markers.insert(marker(rowId)) : markers.delete(current.map(({ key }) => key)) - ).isPersisted.promise + ).when(`settled`) } const markersOf = (rowId: number) => [...markerModel.values()].filter((entry) => entry.rowId === rowId) @@ -5881,7 +5875,7 @@ async function runEagerWindow(testCase: EagerCase): Promise<{ if (testCase.path !== `local-only`) { sourceWrite(`delete`, before) } else { - await source.delete(command.id).isPersisted.promise + await source.delete(command.id).when(`settled`) } // A full join keeps the marker, which becomes a rootless row. An // anti-join's deleted row needs no marker change. @@ -5900,18 +5894,19 @@ async function runEagerWindow(testCase: EagerCase): Promise<{ if (testCase.path !== `local-only`) { sourceWrite(`update`, next) } else { - await source.update(command.id, (draft) => { - draft.eligible = next.eligible - draft.link = next.link - }).isPersisted.promise + await source + .update(command.id, (draft) => { + draft.eligible = next.eligible + draft.link = next.link + }) + .when(`settled`) } await setMarker(command.id, eligible) } else if (command.type === `dup`) { if (!(joined || fullJoin) || viaLink || !before) continue if (markersOf(before.link ?? before.id).length === 0) continue if (ledgerHasRoot(before.link ?? before.id)) noteChange() - await markers.insert(marker(before.link ?? before.id)).isPersisted - .promise + await markers.insert(marker(before.link ?? before.id)).when(`settled`) } else if (command.type === `touch`) { if (!before) continue const next = { ...before, label: `touched-${step}` } @@ -5919,9 +5914,11 @@ async function runEagerWindow(testCase: EagerCase): Promise<{ if (testCase.path !== `local-only`) { sourceWrite(`update`, next) } else { - await source.update(command.id, (draft) => { - draft.label = next.label - }).isPersisted.promise + await source + .update(command.id, (draft) => { + draft.label = next.label + }) + .when(`settled`) } } else { const next = { @@ -5935,13 +5932,15 @@ async function runEagerWindow(testCase: EagerCase): Promise<{ if (testCase.path !== `local-only`) { sourceWrite(before ? `update` : `insert`, next) } else if (before) { - await source.update(command.id, (draft) => { - draft.rank = next.rank - draft.eligible = next.eligible - draft.link = next.link - }).isPersisted.promise + await source + .update(command.id, (draft) => { + draft.rank = next.rank + draft.eligible = next.eligible + draft.link = next.link + }) + .when(`settled`) } else { - await source.insert(next).isPersisted.promise + await source.insert(next).when(`settled`) } await setMarker(command.id, next.eligible) } diff --git a/packages/db/tests/query/pagination-oracle.property.test.ts b/packages/db/tests/query/pagination-oracle.property.test.ts index 7530e8582f..79fc6f6644 100644 --- a/packages/db/tests/query/pagination-oracle.property.test.ts +++ b/packages/db/tests/query/pagination-oracle.property.test.ts @@ -1911,7 +1911,6 @@ async function runPendingMutationScenario( `$collectionId`, `$origin`, `$hasPendingWrites`, - `$synced`, ].includes(key), ), ) @@ -2485,13 +2484,9 @@ async function runPendingHistoryScenario( Object.fromEntries( Object.entries(row).filter( ([key]) => - ![ - `$key`, - `$collectionId`, - `$origin`, - `$hasPendingWrites`, - `$synced`, - ].includes(key), + ![`$key`, `$collectionId`, `$origin`, `$hasPendingWrites`].includes( + key, + ), ), ) const capture = ( @@ -5756,7 +5751,7 @@ describe(`pagination recomputation oracle`, () => { } const receipt = sourceOps.commit() heldMutation.resolve() - await Promise.all(transactions.map((tx) => tx.isPersisted.promise)) + await Promise.all(transactions.map((tx) => tx.when(`settled`))) await receipt await flushPromises() expect([...live.values()].map(({ id }) => id)).toEqual(expected) @@ -5911,7 +5906,7 @@ describe(`pagination recomputation oracle`, () => { } const receipt = sourceOps.commit() heldMutation.resolve() - await Promise.all(transactions.map((tx) => tx.isPersisted.promise)) + await Promise.all(transactions.map((tx) => tx.when(`settled`))) await receipt await windowResult await flushPromises() diff --git a/packages/db/tests/query/pooled-live-query-oracle.property.test.ts b/packages/db/tests/query/pooled-live-query-oracle.property.test.ts index 694f1ab8a1..f10e8905f1 100644 --- a/packages/db/tests/query/pooled-live-query-oracle.property.test.ts +++ b/packages/db/tests/query/pooled-live-query-oracle.property.test.ts @@ -542,7 +542,7 @@ function describeFields(row: Record) { } function describeRow(row: Record) { - return `${describeFields(row)}:${String(row.$synced)}` + return `${describeFields(row)}:${String(row.$hasPendingWrites)}` } type Change = ChangeMessage, string | number> @@ -821,7 +821,7 @@ async function runHistory(history: History): Promise { draft.g = op.g }) : source.delete(key) - const persisted = transaction.isPersisted.promise.catch(() => undefined) + const persisted = transaction.when(`settled`).catch(() => undefined) check(`${checkpoint} pending`) if (cleanupFirst) { // The write settles after cleanup, before the restart. The mock diff --git a/packages/db/tests/query/scheduler.test.ts b/packages/db/tests/query/scheduler.test.ts index 3d51a72dd5..3c37d33af4 100644 --- a/packages/db/tests/query/scheduler.test.ts +++ b/packages/db/tests/query/scheduler.test.ts @@ -576,7 +576,6 @@ describe(`live query scheduler`, () => { $key, $origin, $hasPendingWrites, - $synced, }: UserWithVirtual): UserWithVirtual => ({ id, name: userName, @@ -584,7 +583,6 @@ describe(`live query scheduler`, () => { $key, $origin, $hasPendingWrites, - $synced, }) const source = createCollection({ id: `falsy-row-listener-${name.replaceAll(` `, `-`)}`, @@ -675,7 +673,6 @@ describe(`live query scheduler`, () => { $key: 1, $origin: `remote`, $hasPendingWrites: false, - $synced: true, } return { changes: [ diff --git a/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts b/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts index 135bf61c33..a5cd6ac7ad 100644 --- a/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts +++ b/packages/db/tests/query/virtual-row-fields-oracle.test-d.ts @@ -2,7 +2,7 @@ * # Which types represent virtual-field-bearing rows? * * Law and source: `VirtualRowProps` gives Collection and live-query row roots - * `$hasPendingWrites`, `$synced`, `$origin`, `$key`, and `$collectionId`. The reusable `Ref` + * `$hasPendingWrites`, `$origin`, `$key`, and `$collectionId`. The reusable `Ref` * contract in `docs/guides/live-queries.md` accepts nested refs without claiming * those refs are rows. The output contract established by `includes.test.ts` * keeps inline `toArray` and `materialize` selections in their selected shape. @@ -66,7 +66,6 @@ type PublishedValueFor< > = TSubject extends `row-root` | `whole-row-child` ? T & { readonly $hasPendingWrites: boolean - readonly $synced: boolean readonly $origin: `local` | `remote` readonly $key: string | number readonly $collectionId: string @@ -119,7 +118,8 @@ describe(`virtual row field type boundary`, () => { q.from({ row: rows }).select(({ row }) => { expectTypeOf(row.$key).toEqualTypeOf>() expectTypeOf(row.$hasPendingWrites).toEqualTypeOf>() - expectTypeOf(row.$synced).toEqualTypeOf>() + // @ts-expect-error The removed alias is absent from row refs. + row.$synced expectTypeOf(row.$origin).toEqualTypeOf>() expectTypeOf(row.$collectionId).toEqualTypeOf>() profileLabelIs(row.profile) @@ -130,7 +130,7 @@ describe(`virtual row field type boundary`, () => { // @ts-expect-error Nested user objects are not collection rows. row.profile.$key // @ts-expect-error Optional nested user objects are not collection rows. - row.optionalProfile?.$synced + row.optionalProfile?.$hasPendingWrites // @ts-expect-error Nullable nested user objects are not collection rows. row.nullableProfile?.$origin @@ -142,7 +142,8 @@ describe(`virtual row field type boundary`, () => { expectTypeOf(result.profile).toEqualTypeOf() expectTypeOf(result.$key).toEqualTypeOf() expectTypeOf(result.$hasPendingWrites).toEqualTypeOf() - expectTypeOf(result.$synced).toEqualTypeOf() + // @ts-expect-error The removed alias is absent from published rows. + result.$synced expectTypeOf(result.$origin).toEqualTypeOf<`local` | `remote`>() expectTypeOf(result.$collectionId).toEqualTypeOf() // @ts-expect-error A selected nested user object remains a value, not a row. @@ -158,7 +159,8 @@ describe(`virtual row field type boundary`, () => { expectTypeOf(row.$collectionId).toEqualTypeOf>() expectTypeOf(row.$key).toEqualTypeOf>() expectTypeOf(row.$hasPendingWrites).toEqualTypeOf>() - expectTypeOf(row.$synced).toEqualTypeOf>() + // @ts-expect-error The removed alias is absent from row refs. + row.$synced expectTypeOf(row.$origin).toEqualTypeOf>() profileLabelIs(row.profile) if (row.optionalProfile) profileLabelIs(row.optionalProfile) @@ -168,7 +170,7 @@ describe(`virtual row field type boundary`, () => { // @ts-expect-error Nested user objects are not collection rows. row.profile.$key // @ts-expect-error Optional nested user objects are not collection rows. - row.optionalProfile?.$synced + row.optionalProfile?.$hasPendingWrites // @ts-expect-error Nullable nested user objects are not collection rows. row.nullableProfile?.$origin @@ -203,7 +205,7 @@ describe(`virtual row field type boundary`, () => { // @ts-expect-error Date values are not rows and have no virtual row fields. result.dates[0]!.$key // @ts-expect-error Materialized Date values are not rows either. - result.firstDate?.$synced + result.firstDate?.$hasPendingWrites }) test(`projected child metadata follows row-shaped results`, () => { @@ -272,7 +274,8 @@ describe(`virtual row field type boundary`, () => { expectTypeOf( result.selectedWholeRows[0]!.$hasPendingWrites, ).toEqualTypeOf() - expectTypeOf(result.selectedWholeRows[0]!.$synced).toEqualTypeOf() + // @ts-expect-error The removed alias is absent from whole-row children. + result.selectedWholeRows[0]!.$synced expectTypeOf(result.selectedWholeRows[0]!.$origin).toEqualTypeOf< `local` | `remote` >() @@ -291,9 +294,8 @@ describe(`virtual row field type boundary`, () => { expectTypeOf(result.unmatchedRows[0]!.$hasPendingWrites).toEqualTypeOf< boolean | undefined >() - expectTypeOf(result.unmatchedRows[0]!.$synced).toEqualTypeOf< - boolean | undefined - >() + // @ts-expect-error The removed alias is absent from unmatched row types. + result.unmatchedRows[0]!.$synced expectTypeOf(result.unmatchedRows[0]!.$origin).toEqualTypeOf< `local` | `remote` | undefined >() diff --git a/packages/db/tests/query/virtual-row-fields-oracle.test.ts b/packages/db/tests/query/virtual-row-fields-oracle.test.ts index 93b54f08ed..58d29e4938 100644 --- a/packages/db/tests/query/virtual-row-fields-oracle.test.ts +++ b/packages/db/tests/query/virtual-row-fields-oracle.test.ts @@ -2,10 +2,12 @@ * # Which published values carry virtual row fields? * * Law and source: `VirtualRowProps` gives Collection and live-query row roots - * `$hasPendingWrites`, `$synced`, `$origin`, `$key`, and `$collectionId`. The output contract + * `$hasPendingWrites`, `$origin`, `$key`, and `$collectionId`. The output contract * established by `includes.test.ts` keeps inline `toArray` and `materialize` * selections in their selected shape instead of publishing each selected value - * as a Collection row. + * as a Collection row. Whole-row roots retain source metadata; ordinary + * projections receive the live-query Collection's metadata. An explicit + * selected virtual name cannot replace source metadata at root publication. * * `expectsVirtualFields` is the independent model: row roots and unprojected * whole-row children are rows. Projected children, nested values, and opaque @@ -32,7 +34,7 @@ * observations. `checkWithCleanup` retains the primary mismatch as the cause * when Collection cleanup also fails and attempts every cleanup. */ -import { describe, expect, test } from 'vitest' +import { describe, expect, test, vi } from 'vitest' import { createCollection } from '../../src/collection/index.js' import { createLiveQueryCollection, @@ -64,7 +66,6 @@ type VirtualFieldSubject = const virtualFieldNames = [ `$key`, `$hasPendingWrites`, - `$synced`, `$origin`, `$collectionId`, ] as const @@ -118,6 +119,49 @@ async function checkWithCleanup( } describe(`virtual row field runtime boundary`, () => { + test(`keeps source write metadata authoritative over selected field names`, async () => { + const rows = createCollection( + mockSyncCollectionOptions<{ id: string; label: string }>({ + id: `virtual-row-fields-colliding-projection-source`, + getKey: (row) => row.id, + initialData: [{ id: `row-1`, label: `before` }], + }), + ) + const live = createLiveQueryCollection((q) => + q.from({ row: rows }).select(({ row }) => ({ + id: row.id, + label: row.label, + $hasPendingWrites: false, + $origin: `remote`, + $key: `selected-key`, + $collectionId: `selected-collection`, + })), + ) + + // The model is the source Collection's public row during a held optimistic + // update. Selection changes user fields, but cannot author system metadata. + await checkWithCleanup(async () => { + await live.preload() + const update = rows.update(`row-1`, (draft) => { + draft.label = `pending` + }) + await vi.waitFor(() => { + expect(rows.get(`row-1`)?.$hasPendingWrites).toBe(true) + expect(live.toArray[0]?.label).toBe(`pending`) + }) + + expect(live.toArray[0]).toMatchObject({ + $hasPendingWrites: true, + $origin: `local`, + $key: `row-1`, + $collectionId: rows.id, + }) + + rows.utils.resolveSync() + await update.when(`settled`) + }, [() => live.cleanup(), () => rows.cleanup()]) + }) + test(`publishes every virtual field on implicit, expression, and functional root shapes`, async () => { const rows = createCollection( mockSyncCollectionOptions({ @@ -165,20 +209,21 @@ describe(`virtual row field runtime boundary`, () => { expect(row!.$hasPendingWrites, `${name} has no pending write`).toBe( false, ) - expect(row!.$hasPendingWrites, `${name} legacy inverse`).toBe( - !row!.$synced, - ) + expect(`$synced` in row!, `${name} omits the removed alias`).toBe(false) } expect(implicit.toArray[0]?.id).toBe(`row-1`) + expect(implicit.toArray[0]?.$collectionId).toBe(rows.id) expect(expression.toArray[0]).toMatchObject({ id: `row-1`, label: `nested`, }) + expect(expression.toArray[0]?.$collectionId).toBe(expression.id) expect(functional.toArray[0]).toMatchObject({ id: `row-1`, label: `nested`, }) + expect(functional.toArray[0]?.$collectionId).toBe(functional.id) // Wrong rule: functional projection outputs are treated as ordinary // values and never receive root fields. The direct field check above @@ -398,13 +443,9 @@ describe(`virtual row field runtime boundary`, () => { } expect(result.$key).toBe(`row-1`) - expect(result.$hasPendingWrites).toBe(!result.$synced) - expect(result.wholeRows[0]!.$hasPendingWrites).toBe( - !result.wholeRows[0]!.$synced, - ) - expect(result.selectedWholeRows[0]!.$hasPendingWrites).toBe( - !result.selectedWholeRows[0]!.$synced, - ) + expect(result.$hasPendingWrites).toBe(false) + expect(result.wholeRows[0]!.$hasPendingWrites).toBe(false) + expect(result.selectedWholeRows[0]!.$hasPendingWrites).toBe(false) expect(result.profile).toEqual({ label: `nested` }) expect(`$key` in result.profile).toBe(false) diff --git a/packages/db/tests/query/where-predicate-publication-oracle.property.test.ts b/packages/db/tests/query/where-predicate-publication-oracle.property.test.ts index 67f05b11e7..ff764742ff 100644 --- a/packages/db/tests/query/where-predicate-publication-oracle.property.test.ts +++ b/packages/db/tests/query/where-predicate-publication-oracle.property.test.ts @@ -7,10 +7,10 @@ * null or undefined operand is UNKNOWN, `NaN` equals `NaN` (PostgreSQL float * semantics), a valid Date compares as its millisecond timestamp, and other * values of different types are unequal. `not`, `and`, and `or` follow Kleene - * logic, so `not(UNKNOWN)` is UNKNOWN. Virtual row fields (`$synced`, - * `$origin`, `$key`) are row fields for filtering; a pending optimistic insert - * is `$synced: false` and `$origin: 'local'`, and a synced row from this - * adapter is `$synced: true` and `$origin: 'remote'`. + * logic, so `not(UNKNOWN)` is UNKNOWN. Virtual row fields + * (`$hasPendingWrites`, `$origin`, `$key`) are row fields for filtering; a + * pending optimistic insert has `$hasPendingWrites: true` and local origin. + * A settled source row has `$hasPendingWrites: false` and remote origin. * * Filtered publication must agree with that predicate after every source * transaction. `Collection.subscribeChanges` with a `whereExpression` turns an @@ -143,7 +143,7 @@ const literalValues: ReadonlyArray = [ type Operand = | { kind: `field` } | { kind: `literal`; value: FieldValue } - | { kind: `virtual`; name: `$synced` | `$origin` | `$key` } + | { kind: `virtual`; name: `$hasPendingWrites` | `$origin` | `$key` } type Predicate = | { kind: `eq`; left: Operand; right: Operand } @@ -171,7 +171,7 @@ type History = type ModelRow = { id: string v: FieldValue - $synced: boolean + $hasPendingWrites: boolean $origin: `local` | `remote` $key: string } @@ -249,8 +249,8 @@ function equivalentFieldValues(a: FieldValue, b: FieldValue): boolean { return Object.is(a, b) } -function syncedModelRow(id: string, v: FieldValue): ModelRow { - return { id, v, $synced: true, $origin: `remote`, $key: id } +function settledModelRow(id: string, v: FieldValue): ModelRow { + return { id, v, $hasPendingWrites: false, $origin: `remote`, $key: id } } // --------------------------------------------------------------------------- @@ -269,7 +269,11 @@ const operandArbitrary: fc.Arbitrary = fc.oneof( { weight: 1, arbitrary: fc - .constantFrom(`$synced` as const, `$origin` as const, `$key` as const) + .constantFrom( + `$hasPendingWrites` as const, + `$origin` as const, + `$key` as const, + ) .map((name): Operand => ({ kind: `virtual`, name })), }, ) @@ -410,8 +414,8 @@ const pinnedHistories: ReadonlyArray = [ }, { kind: `eq`, - left: { kind: `virtual`, name: `$synced` }, - right: { kind: `literal`, value: false }, + left: { kind: `virtual`, name: `$hasPendingWrites` }, + right: { kind: `literal`, value: true }, }, ], }, @@ -607,8 +611,8 @@ const peerPredicates: ReadonlyArray = [ ...[`a`, `b`, PREFIXED, true, false].map(fieldEq), { kind: `eq`, - left: { kind: `virtual`, name: `$synced` }, - right: { kind: `literal`, value: true }, + left: { kind: `virtual`, name: `$hasPendingWrites` }, + right: { kind: `literal`, value: false }, }, ] @@ -662,8 +666,8 @@ async function observeHistory( seeds.map((seed) => [ seed.id, { - ...syncedModelRow(seed.id, seed.v), - $synced: !seed.optimistic, + ...settledModelRow(seed.id, seed.v), + $hasPendingWrites: seed.optimistic, $origin: seed.optimistic ? `local` : `remote`, }, ]), @@ -777,7 +781,7 @@ async function observeHistory( type: `insert`, value: sourceRow(id, operation.v), }) - modelRows.set(id, syncedModelRow(id, operation.v)) + modelRows.set(id, settledModelRow(id, operation.v)) touched.add(id) continue } @@ -804,7 +808,7 @@ async function observeHistory( type: `update`, value: { id, v: operation.v === MISSING ? undefined : operation.v }, }) - modelRows.set(id, syncedModelRow(id, operation.v)) + modelRows.set(id, settledModelRow(id, operation.v)) touched.add(id) } collection.utils.commit() @@ -890,8 +894,8 @@ describe(`WHERE predicate publication oracle`, () => { history.rows.map((seed, index): [string, ModelRow] => [ `r${index}`, { - ...syncedModelRow(`r${index}`, seed.v), - $synced: !seed.optimistic, + ...settledModelRow(`r${index}`, seed.v), + $hasPendingWrites: seed.optimistic, $origin: seed.optimistic ? `local` : `remote`, }, ]), diff --git a/packages/db/tests/transaction-ownership-oracle.property.test.ts b/packages/db/tests/transaction-ownership-oracle.property.test.ts index 7c23a54102..7d9757d303 100644 --- a/packages/db/tests/transaction-ownership-oracle.property.test.ts +++ b/packages/db/tests/transaction-ownership-oracle.property.test.ts @@ -17,7 +17,7 @@ import type { Transaction } from '../src/transactions.js' * what the Collection shows. The contract: once a transaction settles, by * success, failure, or rollback, it leaves every Collection that ever tracked * it, and each of those Collections stops showing its optimistic rows. - * Settlement always settles `isPersisted`, even when a subscriber throws + * Settlement always settles `when('settled')`, even when a subscriber throws * during it. * * Ownership is wider than the transaction's current mutations. An insert and @@ -37,7 +37,7 @@ import type { Transaction } from '../src/transactions.js' * surviving writes, in creation order. * - A rollback, or a failed commit, also rolls back every pending (not yet * committed) transaction that wrote one of the same Collection keys. - * - A settled transaction's `isPersisted` has settled, and its set of + * - A settled transaction's `when('settled')` has settled, and its set of * tracking Collections is empty. Its mutations still name their * Collection; this oracle does not claim the Collection is collectable. * - Transaction ids are unique among unsettled transactions in a Collection. @@ -55,7 +55,7 @@ import type { Transaction } from '../src/transactions.js' * admitted makes the write fail: the call rethrows that error, the write's * transaction rolls back without rolling back other transactions, and no * Collection keeps it. Either way the write's own transaction has settled, - * with its `isPersisted`. So a direct write never changes the model. + * with its `when('settled')`. So a direct write never changes the model. * - Error shape: a settling call that ran no throwing subscriber reports no * subscriber error. Otherwise it throws one of the subscriber errors, as * is. The contract does not say which one, because these failures are rare @@ -82,7 +82,7 @@ import type { Transaction } from '../src/transactions.js' * step it then flushes promises and compares, for each * Collection, the tracked transaction ids with the model's set, the rows of * keys 1, 2 and 9 with the model's overlay, and each transaction's - * `isPersisted` settlement with the model's state. It compares tracked + * `when('settled')` settlement with the model's state. It compares tracked * transactions by identity. Each settling call's thrown error, or its * commit's rejection, is compared with the error-shape law, counting the * subscriber errors the step raised. A settled transaction's `collections` @@ -394,7 +394,7 @@ async function runHistory(steps: ReadonlyArray): Promise { const model = new OwnershipModel() const driven: Array = [] const settle = (entry: Driven) => { - entry.tx.isPersisted.promise.then( + entry.tx.when(`settled`).then( () => (entry.settled = true), () => (entry.settled = true), ) @@ -560,7 +560,7 @@ async function runHistory(steps: ReadonlyArray): Promise { } finally { restoreCreate() for (const tx of created) - tx.isPersisted.promise.then( + tx.when(`settled`).then( () => settled.add(tx), () => settled.add(tx), ) @@ -597,7 +597,7 @@ async function runHistory(steps: ReadonlyArray): Promise { expect(tx.state, `${label}: direct transaction settled`).toBe( rejected ? `failed` : `completed`, ) - expect(settled.has(tx), `${label}: direct isPersisted settled`).toBe( + expect(settled.has(tx), `${label}: direct transaction settled`).toBe( true, ) } diff --git a/packages/db/tests/transaction-types.test.ts b/packages/db/tests/transaction-types.test.ts index 0efc4498ea..fc102a5b5b 100644 --- a/packages/db/tests/transaction-types.test.ts +++ b/packages/db/tests/transaction-types.test.ts @@ -64,7 +64,7 @@ describe(`Transaction Types`, () => { mutations: [], metadata: {}, mutationFn: mockMutationFn, - isPersisted: false, + when: async () => Promise.resolve(), autoCommit: false, setState: () => {}, commit: async () => Promise.resolve(), diff --git a/packages/db/tests/transactions.test.ts b/packages/db/tests/transactions.test.ts index b8deb32b41..14ca0f1a1f 100644 --- a/packages/db/tests/transactions.test.ts +++ b/packages/db/tests/transactions.test.ts @@ -63,7 +63,7 @@ describe(`Transactions`, () => { if (batch.some((change) => change.value.value === `server`)) throw failure }) let persisted = false - const receipt = tx.isPersisted.promise.then( + const receipt = tx.when(`settled`).then( () => { persisted = true }, @@ -119,9 +119,9 @@ describe(`Transactions`, () => { autoCommit: false, mutationFn: () => Promise.reject(reason), }) - const persisted = transaction.isPersisted.promise.catch( - (error: unknown) => error, - ) + const persisted = transaction + .when(`settled`) + .catch((error: unknown) => error) try { transaction.mutate(() => collection.insert({ id: 1 })) await expect(transaction.commit()).rejects.toThrow(message) @@ -209,7 +209,7 @@ describe(`Transactions`, () => { transaction.commit() expect(transaction.state).toBe(`completed`) - await expect(transaction.isPersisted.promise).resolves.toBeDefined() + await expect(transaction.when(`settled`)).resolves.toBeDefined() }) it(`thows an error if you don't pass in mutationFn`, () => { // @ts-expect-error missing argument on purpose @@ -239,7 +239,7 @@ describe(`Transactions`, () => { autoCommit: false, }) failed.rollback({ error: first }) - await expect(failed.isPersisted.promise).rejects.toBe(first) + await expect(failed.when(`settled`)).rejects.toBe(first) failed.rollback({ error: new Error(`second`) }) expect(failed.error?.error).toBe(first) @@ -360,7 +360,7 @@ describe(`Transactions`, () => { transaction.rollback() - transaction.isPersisted.promise.catch(() => {}) + transaction.when(`settled`).catch(() => {}) expect(transaction.state).toBe(`failed`) }) it(`keeps a persisting transaction failed when rollback wins`, async () => { @@ -380,7 +380,7 @@ describe(`Transactions`, () => { try { transaction.mutate(() => collection.insert({ id: 1 })) - const persisted = transaction.isPersisted.promise.then( + const persisted = transaction.when(`settled`).then( (value) => ({ status: `fulfilled` as const, value }), (reason: unknown) => ({ status: `rejected` as const, reason }), ) @@ -446,7 +446,7 @@ describe(`Transactions`, () => { }) try { - const persisted = first.isPersisted.promise.then( + const persisted = first.when(`settled`).then( (value) => ({ status: `fulfilled` as const, value }), (reason: unknown) => ({ status: `rejected` as const, reason }), ) @@ -479,7 +479,6 @@ describe(`Transactions`, () => { $key: 1, $origin: `local`, $hasPendingWrites: true, - $synced: false, }) expect(batches).toEqual([ [{ type: `insert`, key: 1 }], @@ -513,7 +512,7 @@ describe(`Transactions`, () => { }) try { - void first.isPersisted.promise.catch(() => undefined) + void first.when(`settled`).catch(() => undefined) first.mutate(() => collection.insert({ id: 1, owner: `first` })) first.rollback() @@ -561,8 +560,8 @@ describe(`Transactions`, () => { await expect(transaction.commit()).rejects.toThrow(`bad`) - await expect(transaction.isPersisted.promise).rejects.toThrow(`bad`) - transaction.isPersisted.promise.catch(() => {}) + await expect(transaction.when(`settled`)).rejects.toThrow(`bad`) + transaction.when(`settled`).catch(() => {}) expect(transaction.state).toBe(`failed`) expect(transaction.error?.message).toBe(`bad`) expect(transaction.error?.error).toBeInstanceOf(Error) @@ -597,8 +596,8 @@ describe(`Transactions`, () => { await expect(transaction.commit()).rejects.toThrow(`bad`) - await expect(transaction.isPersisted.promise).rejects.toThrow(`bad`) - transaction.isPersisted.promise.catch(() => {}) + await expect(transaction.when(`settled`)).rejects.toThrow(`bad`) + transaction.when(`settled`).catch(() => {}) expect(transaction.state).toBe(`failed`) expect(transaction.error?.message).toBe(`bad`) expect(transaction.error?.error).toBeInstanceOf(Error) @@ -638,7 +637,7 @@ describe(`Transactions`, () => { } }) - it(`commit() and isPersisted.promise should reject with the same error instance`, async () => { + it(`commit() and when('settled') should reject with the same error instance`, async () => { const originalError = new Error(`Original API error`) const tx = createTransaction({ mutationFn: () => { @@ -672,9 +671,9 @@ describe(`Transactions`, () => { commitError = error } - // Capture error from isPersisted.promise + // Capture error from when('settled') try { - await tx.isPersisted.promise + await tx.when(`settled`) } catch (error) { persistedError = error } @@ -716,9 +715,9 @@ describe(`Transactions`, () => { expect((error as Error).message).toBe(`string error`) } - // Same error from isPersisted.promise + // Same error from when('settled') try { - await tx.isPersisted.promise + await tx.when(`settled`) } catch (error) { expect(error).toBeInstanceOf(Error) expect((error as Error).message).toBe(`string error`) @@ -861,8 +860,8 @@ describe(`Transactions`, () => { // tx1 should throw the original error expect(tx1CommitError).toBe(originalError) - // tx2's isPersisted.promise should also be rejected (but with undefined since it's a cascading rollback) - await expect(tx2.isPersisted.promise).rejects.toBeUndefined() + // tx2's when('settled') should also be rejected (but with undefined since it's a cascading rollback) + await expect(tx2.when(`settled`)).rejects.toBeUndefined() }) it(`should, when rolling back, find any other pending transactions w/ overlapping mutations and roll them back as well`, async () => { @@ -911,7 +910,7 @@ describe(`Transactions`, () => { }) transaction2.commit() - await transaction2.isPersisted.promise + await transaction2.when(`settled`) transaction3.mutate(() => { collection.state.forEach((object) => { @@ -930,9 +929,9 @@ describe(`Transactions`, () => { }) transaction1.rollback() - transaction1.isPersisted.promise.catch(() => {}) - transaction3.isPersisted.promise.catch(() => {}) - transaction4.isPersisted.promise.catch(() => {}) + transaction1.when(`settled`).catch(() => {}) + transaction3.when(`settled`).catch(() => {}) + transaction4.when(`settled`).catch(() => {}) expect(transaction1.state).toBe(`failed`) expect(transaction2.state).toBe(`completed`) diff --git a/packages/db/tests/truncate-readiness-oracle.test.ts b/packages/db/tests/truncate-readiness-oracle.test.ts index efaa576aef..c33ff87671 100644 --- a/packages/db/tests/truncate-readiness-oracle.test.ts +++ b/packages/db/tests/truncate-readiness-oracle.test.ts @@ -86,7 +86,7 @@ it.each(cells)( await receipt if (held) expect(c.get(9)?.value).toBe('optimistic') finish.resolve() - await optimistic?.isPersisted.promise + await optimistic?.when(`settled`) expect.soft(c.status).toBe(expectedStatus) // A later independent ordinary replacement still recovers normally. sync.begin() diff --git a/packages/db/tests/utils.ts b/packages/db/tests/utils.ts index 54267feecc..75cf422cb1 100644 --- a/packages/db/tests/utils.ts +++ b/packages/db/tests/utils.ts @@ -70,7 +70,6 @@ export const stripVirtualProps = | undefined>( if (!value || typeof value !== `object`) return value const { $hasPendingWrites: _hasPendingWrites, - $synced: _synced, $origin: _origin, $key: _key, $collectionId: _collectionId, @@ -81,13 +80,9 @@ export const stripVirtualProps = | undefined>( export const omitVirtualProps = >( value: T, -): Omit< - T, - '$hasPendingWrites' | '$synced' | '$origin' | '$key' | '$collectionId' -> => { +): Omit => { const { $hasPendingWrites: _hasPendingWrites, - $synced: _synced, $origin: _origin, $key: _key, $collectionId: _collectionId, diff --git a/packages/db/tests/virtual-props-cache.test.ts b/packages/db/tests/virtual-props-cache.test.ts index b9420665f4..7bca5712bf 100644 --- a/packages/db/tests/virtual-props-cache.test.ts +++ b/packages/db/tests/virtual-props-cache.test.ts @@ -84,7 +84,7 @@ describe(`virtual props cache`, () => { const transaction = collection.insert({ id: `y`, a: 1 }) collection.get(`y`) collection.utils.rejectSync(new Error(`rolled back`)) - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) }) expect(collection.has(`y`)).toBe(false) expect(cacheSize(collection)).toBe(0) @@ -143,7 +143,7 @@ describe(`virtual props cache`, () => { const receipt = sync.commit() if (blocker) { release() - await blocker.isPersisted.promise + await blocker.when(`settled`) } await receipt expect(collection.get(1)?.a).toBe(2) diff --git a/packages/db/tests/virtual-row-legacy-guard.test-d.ts b/packages/db/tests/virtual-row-guard.test-d.ts similarity index 61% rename from packages/db/tests/virtual-row-legacy-guard.test-d.ts rename to packages/db/tests/virtual-row-guard.test-d.ts index 1cf06edd28..9642b7dcee 100644 --- a/packages/db/tests/virtual-row-legacy-guard.test-d.ts +++ b/packages/db/tests/virtual-row-guard.test-d.ts @@ -2,9 +2,9 @@ import { expectTypeOf, test } from 'vitest' import { hasVirtualProps } from '../src/virtual-props.js' import type { VirtualRowProps, WithVirtualProps } from '../src/virtual-props.js' -test(`the legacy guard keeps its VirtualRowProps type contract`, () => { +test(`the guard narrows to the complete virtual row shape`, () => { const value: unknown = { - $synced: true, + $hasPendingWrites: false, $origin: `remote`, $key: `row-1`, $collectionId: `collection-1`, @@ -12,11 +12,13 @@ test(`the legacy guard keeps its VirtualRowProps type contract`, () => { if (hasVirtualProps(value)) { const row: VirtualRowProps = value - expectTypeOf(row.$hasPendingWrites).toEqualTypeOf() + expectTypeOf(row.$hasPendingWrites).toEqualTypeOf() + // @ts-expect-error The removed alias is absent from guarded rows. + row.$synced } }) -test(`collection-published rows guarantee the new field`, () => { +test(`collection-published rows guarantee the pending-write field`, () => { type PublishedRow = WithVirtualProps<{ id: string }, string> expectTypeOf().toEqualTypeOf() }) diff --git a/packages/electric-db-collection/src/electric.ts b/packages/electric-db-collection/src/electric.ts index bb5f8b6048..b8a956bec4 100644 --- a/packages/electric-db-collection/src/electric.ts +++ b/packages/electric-db-collection/src/electric.ts @@ -11,7 +11,6 @@ import { SyncTransactionAbortedError, and, validateSyncPersistenceCapability, - warnOnce, whenSyncAccepted, withCollectionConfigFactory, withCollectionSyncConfigCleanup, @@ -288,20 +287,6 @@ export type MatchFunction> = ( message: Message, ) => boolean -/** - * Matching strategies for Electric synchronization - * Handlers can return: - * - Txid strategy: { txid: number | number[], timeout?: number } (deprecated compatibility behavior) - * - Void (no return value) - mutation completes without waiting - * - * The optional timeout property specifies how long to wait for the txid(s) in milliseconds. - * If not specified, defaults to 15000ms. - */ -export type MatchingStrategy = { - txid: Txid | Array - timeout?: number -} | void - /** * Type representing a snapshot end message */ @@ -375,9 +360,6 @@ export interface ElectricCollectionConfig< * @param params Object containing transaction and collection information * @returns Promise that should resolve after synchronization is complete * - * **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0. - * Use `await collection.utils.awaitTxId(txid)` within the handler instead. - * * @example * // Recommended: Wait for txid to sync * onInsert: async ({ transaction, collection }) => { @@ -452,7 +434,7 @@ export interface ElectricCollectionConfig< string | number, ElectricCollectionUtils >, - ) => Promise + ) => Promise /** * Optional asynchronous handler function called before an update operation @@ -468,9 +450,6 @@ export interface ElectricCollectionConfig< * @param params Object containing transaction and collection information * @returns Promise that should resolve after synchronization is complete * - * **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0. - * Use `await collection.utils.awaitTxId(txid)` within the handler instead. - * * @example * // Recommended: Wait for txid to sync * onUpdate: async ({ transaction, collection }) => { @@ -502,7 +481,7 @@ export interface ElectricCollectionConfig< string | number, ElectricCollectionUtils >, - ) => Promise + ) => Promise /** * Optional asynchronous handler function called before a delete operation @@ -518,9 +497,6 @@ export interface ElectricCollectionConfig< * @param params Object containing transaction and collection information * @returns Promise that should resolve after synchronization is complete * - * **Deprecation notice:** Returning `{ txid }` from handlers is deprecated and will be removed in v1.0. - * Use `await collection.utils.awaitTxId(txid)` within the handler instead. - * * @example * // Recommended: Wait for txid to sync * onDelete: async ({ transaction, collection }) => { @@ -551,7 +527,7 @@ export interface ElectricCollectionConfig< string | number, ElectricCollectionUtils >, - ) => Promise + ) => Promise } function isUpToDateMessage>( @@ -1231,107 +1207,7 @@ export function electricCollectionOptions>( }) const sync = createSync() - /** - * Process matching strategy and wait for synchronization - */ - const processMatchingStrategy = async ( - result: unknown, - waitForTxId: AwaitTxIdFn, - ): Promise => { - const canHaveProperties = - (typeof result === `object` && result !== null) || - typeof result === `function` - - // Only wait if result contains txid - if (canHaveProperties && `txid` in result) { - const strategy = result as Exclude - // Warn about deprecated return value pattern - warnOnce( - 'electric-collection-txid-return', - '[TanStack DB] DEPRECATED: Returning { txid } from mutation handlers is deprecated and will be removed in v1.0. ' + - 'Use `await collection.utils.awaitTxId(txid)` instead of returning { txid }. ' + - 'See migration guide: https://tanstack.com/db/latest/docs/collections/electric-collection#persistence-handlers--synchronization', - ) - - const timeout = strategy.timeout - // Handle both single txid and array of txids - if (Array.isArray(strategy.txid)) { - await Promise.all( - strategy.txid.map((txid) => waitForTxId(txid, timeout)), - ) - } else { - await waitForTxId(strategy.txid, timeout) - } - } - // If result is void/undefined, don't wait - mutation completes immediately - } - const getMutationAwaitTxId = (params: unknown): AwaitTxIdFn => { - const collection = ( - params as { - collection?: { utils?: { awaitTxId?: AwaitTxIdFn } } - } - ).collection - return collection?.utils?.awaitTxId ?? awaitTxId - } - - // Create wrapper handlers for direct persistence operations that handle different matching strategies - // These wrappers process deprecated return values but don't pass them through - const wrappedOnInsert = config.onInsert - ? async ( - params: InsertMutationFnParams< - any, - string | number, - ElectricCollectionUtils - >, - ): Promise => { - const handlerResult = await config.onInsert!(params) - await processMatchingStrategy( - handlerResult, - getMutationAwaitTxId(params), - ) - } - : undefined - - const wrappedOnUpdate = config.onUpdate - ? async ( - params: UpdateMutationFnParams< - any, - string | number, - ElectricCollectionUtils - >, - ): Promise => { - const handlerResult = await config.onUpdate!(params) - await processMatchingStrategy( - handlerResult, - getMutationAwaitTxId(params), - ) - } - : undefined - - const wrappedOnDelete = config.onDelete - ? async ( - params: DeleteMutationFnParams< - any, - string | number, - ElectricCollectionUtils - >, - ): Promise => { - const handlerResult = await config.onDelete!(params) - await processMatchingStrategy( - handlerResult, - getMutationAwaitTxId(params), - ) - } - : undefined - - // Extract standard Collection config properties - const { - shapeOptions: _shapeOptions, - onInsert: _onInsert, - onUpdate: _onUpdate, - onDelete: _onDelete, - ...restConfig - } = config + const { shapeOptions: _shapeOptions, ...restConfig } = config const utilityTemplate: ElectricCollectionUtils = { awaitTxId, @@ -1385,9 +1261,6 @@ export function electricCollectionOptions>( ...restConfig, syncMode: finalSyncMode, sync: syncTemplate, - onInsert: wrappedOnInsert, - onUpdate: wrappedOnUpdate, - onDelete: wrappedOnDelete, utils: utilityTemplate, } Object.defineProperty(options, `utils`, { diff --git a/packages/electric-db-collection/tests/electric-descriptor-isolation-oracle.test.ts b/packages/electric-db-collection/tests/electric-descriptor-isolation-oracle.test.ts index 6a4507b50a..688440e37d 100644 --- a/packages/electric-db-collection/tests/electric-descriptor-isolation-oracle.test.ts +++ b/packages/electric-db-collection/tests/electric-descriptor-isolation-oracle.test.ts @@ -806,7 +806,7 @@ fixedCase( mutation.mutate(() => collection.insert({ id: 10, name: `optimistic`, stable: `stable-10` }), ) - const handlerOutcome = mutation.isPersisted.promise.then( + const handlerOutcome = mutation.when(`settled`).then( () => undefined, (error: unknown) => error, ) @@ -1805,7 +1805,9 @@ fixedCase.each([`shared storage`, `distinct storage`] as const)( }, getKey: (row) => row.id, startSync: false, - onInsert: () => Promise.resolve({ txid: 200, timeout: 100 }), + onInsert: async ({ collection }) => { + await collection.utils.awaitTxId(200, 100) + }, }) // The Electric descriptor is reusable; each persisted wrapper binds one // Collection so its receipt and sync controls cannot move to another owner. @@ -1845,12 +1847,12 @@ fixedCase.each([`shared storage`, `distinct storage`] as const)( name: `own`, stable: `stable-1`, }) - void transaction.isPersisted.promise.catch(() => undefined) + void transaction.when(`settled`).catch(() => undefined) streams[0]!.send([ insert(1, `own`), { headers: { control: `up-to-date`, txids: [200] } }, ]) - await expect(transaction.isPersisted.promise).resolves.toBeDefined() + await expect(transaction.when(`settled`)).resolves.toBeDefined() expect(first.get(1)?.name).toBe(`own`) expect(second.has(1)).toBe(false) } finally { diff --git a/packages/electric-db-collection/tests/electric-oracle.property.test.ts b/packages/electric-db-collection/tests/electric-oracle.property.test.ts index fa62022c93..649a573d74 100644 --- a/packages/electric-db-collection/tests/electric-oracle.property.test.ts +++ b/packages/electric-db-collection/tests/electric-oracle.property.test.ts @@ -3109,12 +3109,12 @@ describeUnlessQueuedPresenceReplay(`Electric adapter laws`, () => { void acquired.catch(() => {}) trace.subscriber([change(`update`, 1, `partial`), upToDate]) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await acquired expect(trace.collection.has(1)).toBe(false) } finally { persistence.resolve() - await transaction?.isPersisted.promise + await transaction?.when(`settled`) await trace.collection.cleanup() await acquired } @@ -5136,7 +5136,7 @@ describeUnlessQueuedPresenceReplay(`Electric adapter laws`, () => { expect(trace.collection.has(2)).toBe(false) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await trace.collection.cleanup() }) @@ -5167,7 +5167,7 @@ describeUnlessQueuedPresenceReplay(`Electric adapter laws`, () => { expect(trace.collection.has(1)).toBe(false) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await trace.collection.stateWhenReady() expect(trace.collection.status).toBe(`ready`) @@ -5234,7 +5234,7 @@ describeUnlessQueuedPresenceReplay(`Electric adapter laws`, () => { `pending`, ) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await atCheckpoint(loading!, `full recovery applied settlement`) expect(outcome).toBe(`fulfilled`) expect(trace.collection.get(1)).toMatchObject({ @@ -5585,7 +5585,7 @@ describeUnlessQueuedPresenceReplay(`Electric adapter laws`, () => { break case `release`: persistence.resolve() - await transaction!.isPersisted.promise + await transaction!.when(`settled`) break case `hold-acceptance`: armAcceptance = true @@ -6199,7 +6199,7 @@ describeUnlessQueuedPresenceReplay(`Electric adapter laws`, () => { expect(trace.collection.has(1), `applied receipt is held`).toBe(false) external.abort() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await expect( atCheckpoint(first, `externally aborted held subset`), ).rejects.toMatchObject({ name: `AbortError` }) @@ -6270,7 +6270,7 @@ describeUnlessQueuedPresenceReplay(`Electric adapter laws`, () => { expect(firstOutcome).toBe(`rejected`) expect(firstError).toMatchObject({ name: `AbortError` }) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await observedFirst expect(trace.collection.has(1)).toBe(true) }, [ @@ -6397,7 +6397,7 @@ describeUnlessQueuedPresenceReplay(`Electric adapter laws`, () => { await trace.collection.cleanup() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await Promise.resolve() expect(trace.collection.status).toBe(`cleaned-up`) diff --git a/packages/electric-db-collection/tests/electric-recovery-oracle.test.ts b/packages/electric-db-collection/tests/electric-recovery-oracle.test.ts index cfeef06173..45be1abac5 100644 --- a/packages/electric-db-collection/tests/electric-recovery-oracle.test.ts +++ b/packages/electric-db-collection/tests/electric-recovery-oracle.test.ts @@ -301,7 +301,6 @@ function fixture( Array.from(collection.values(), (row) => { const { $hasPendingWrites: _pending, - $synced: _synced, $origin: _origin, $key: _key, $collectionId: _collectionId, diff --git a/packages/electric-db-collection/tests/electric.test-d.ts b/packages/electric-db-collection/tests/electric.test-d.ts index f1d4ee109c..6033adbec2 100644 --- a/packages/electric-db-collection/tests/electric.test-d.ts +++ b/packages/electric-db-collection/tests/electric.test-d.ts @@ -124,21 +124,21 @@ describe(`Electric collection type resolution tests`, () => { collection.utils expectTypeOf(testCollectionUtils.awaitTxId).toBeFunction expectTypeOf(collection.utils.awaitTxId).toBeFunction - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, onUpdate: async ({ collection }) => { const testCollectionUtils: ElectricCollectionUtils = collection.utils expectTypeOf(testCollectionUtils.awaitTxId).toBeFunction expectTypeOf(collection.utils.awaitTxId).toBeFunction - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, onDelete: async ({ collection }) => { const testCollectionUtils: ElectricCollectionUtils = collection.utils expectTypeOf(testCollectionUtils.awaitTxId).toBeFunction expectTypeOf(collection.utils.awaitTxId).toBeFunction - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, }) @@ -175,13 +175,13 @@ describe(`Electric collection type resolution tests`, () => { schema: todoSchema, getKey: (item) => item.id, onInsert: async () => { - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, onUpdate: async () => { - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, onDelete: async () => { - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, }) @@ -217,7 +217,7 @@ describe(`Electric collection type resolution tests`, () => { ).toEqualTypeOf() // @ts-expect-error Electric Collection does not expose Query refetch utilities params.transaction.mutations[0].collection.utils.refetch() - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, onUpdate: (params) => { // Verify that the mutation value has the correct type @@ -230,7 +230,7 @@ describe(`Electric collection type resolution tests`, () => { expectTypeOf( params.transaction.mutations[0].collection.utils.awaitTxId, ).toEqualTypeOf() - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, onDelete: (params) => { // Verify that the mutation value has the correct type @@ -243,7 +243,7 @@ describe(`Electric collection type resolution tests`, () => { expectTypeOf( params.transaction.mutations[0].collection.utils.awaitTxId, ).toEqualTypeOf() - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, }) @@ -313,7 +313,7 @@ describe(`Electric collection type resolution tests`, () => { return mutation.original.id }) - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, onInsert: (params) => { // Direct index access should be correctly typed @@ -327,7 +327,7 @@ describe(`Electric collection type resolution tests`, () => { return mutation.modified.id }) - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, onUpdate: (params) => { // Direct index access should be correctly typed @@ -345,7 +345,7 @@ describe(`Electric collection type resolution tests`, () => { return mutation.modified.id }) - return Promise.resolve({ txid: 1 }) + return Promise.resolve() }, }) diff --git a/packages/electric-db-collection/tests/electric.test.ts b/packages/electric-db-collection/tests/electric.test.ts index 3f0aa80c5f..46c4a239e7 100644 --- a/packages/electric-db-collection/tests/electric.test.ts +++ b/packages/electric-db-collection/tests/electric.test.ts @@ -1,12 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ShapeStream } from '@electric-sql/client' -import { - CollectionImpl, - IR, - createCollection, - createTransaction, - resetWarnings, -} from '@tanstack/db' +import { IR, createCollection, createTransaction } from '@tanstack/db' import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' import { electricCollectionOptions, isChangeMessage } from '../src/electric' import { stripVirtualProps } from '../../db/tests/utils' @@ -19,7 +13,6 @@ import type { PendingMutation, SyncMetadataApi, Transaction, - TransactionWithMutations, } from '@tanstack/db' import type { Message, Row } from '@electric-sql/client' import type { StandardSchemaV1 } from '@standard-schema/spec' @@ -293,7 +286,7 @@ describe(`Electric Integration`, () => { expect(collection.get(1)).toBeUndefined() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await collection.stateWhenReady() expect(collection.status).toBe(`ready`) @@ -860,7 +853,7 @@ describe(`Electric Integration`, () => { collection.insert({ id: 1, name: `Test item 1` }), ) - await transaction.isPersisted.promise + await transaction.when(`settled`) transaction = collection._state.transactions.get(transaction.id)! @@ -879,9 +872,9 @@ describe(`Electric Integration`, () => { describe(`Direct persistence handlers`, () => { it(`should pass through direct persistence handlers to collection options`, () => { // Create mock handlers - const onInsert = vi.fn().mockResolvedValue({ txid: 123 }) - const onUpdate = vi.fn().mockResolvedValue({ txid: 456 }) - const onDelete = vi.fn().mockResolvedValue({ txid: 789 }) + const onInsert = vi.fn().mockResolvedValue(undefined) + const onUpdate = vi.fn().mockResolvedValue(undefined) + const onDelete = vi.fn().mockResolvedValue(undefined) const config = { id: `test-handlers`, @@ -905,123 +898,6 @@ describe(`Electric Integration`, () => { expect(options.onDelete).toBeDefined() }) - it(`warns once while continuing to handle legacy txid returns`, async () => { - resetWarnings() - const warning = vi.spyOn(console, `warn`).mockImplementation(() => {}) - const awaitTxId = vi.fn().mockResolvedValue(true) - const onInsert = vi.fn().mockResolvedValue({ txid: 123 }) - const options = electricCollectionOptions({ - id: `legacy-handler-warning`, - shapeOptions: { - url: `http://test-url`, - params: { table: `test_table` }, - }, - getKey: (item: Row) => item.id as number, - onInsert, - }) - const params = { - transaction: { - id: `legacy-handler-transaction`, - mutations: [], - }, - collection: { utils: { awaitTxId } }, - } as unknown as InsertMutationFnParams< - Row, - string | number, - ElectricCollectionUtils - > - - try { - await options.onInsert!(params) - await options.onInsert!(params) - - expect(onInsert).toHaveBeenCalledTimes(2) - expect(awaitTxId).toHaveBeenCalledTimes(2) - expect(warning).toHaveBeenCalledTimes(1) - expect(warning).toHaveBeenCalledWith( - expect.stringContaining(`Returning { txid }`), - ) - } finally { - resetWarnings() - warning.mockRestore() - } - }) - - it(`treats an empty handler result as no synchronization strategy`, async () => { - // Create a mock transaction for testing - const mockTransaction = { - id: `test-transaction`, - mutations: [], - } as unknown as TransactionWithMutations - const mockParams: InsertMutationFnParams< - Row, - string | number, - ElectricCollectionUtils - > = { - transaction: mockTransaction, - // @ts-expect-error not relevant to test - collection: CollectionImpl, - } - - // Create a handler that doesn't return a txid - const onInsert = vi.fn().mockResolvedValue({}) - - const config = { - id: `test-handlers`, - shapeOptions: { - url: `http://test-url`, - params: { - table: `test_table`, - }, - }, - getKey: (item: Row) => item.id as number, - onInsert, - } - - const options = electricCollectionOptions(config) - - // Empty objects retain the existing no-wait behavior. - await expect(options.onInsert!(mockParams)).resolves.not.toThrow() - }) - - it.each([ - [`string`, `legacy-result`], - [`number`, 42], - [`null`, null], - [`function`, () => undefined], - [`object`, {}], - [`void`, undefined], - ])(`tolerates an out-of-contract %s handler result`, async (_, result) => { - const awaitTxId = vi.fn().mockResolvedValue(true) - const options = electricCollectionOptions({ - id: `out-of-contract-handler-result`, - shapeOptions: { - url: `http://test-url`, - params: { table: `test_table` }, - }, - getKey: (item: Row) => item.id as number, - onInsert: vi.fn().mockResolvedValue(result), - }) - const params = { - transaction: { id: `test-transaction`, mutations: [] }, - collection: { utils: { awaitTxId } }, - } as unknown as InsertMutationFnParams< - Row, - string | number, - ElectricCollectionUtils - > - - let thrown: unknown - try { - await options.onInsert!(params) - } catch (error) { - thrown = error - } - - expect(thrown).toBeUndefined() - expect(awaitTxId).not.toHaveBeenCalled() - }) - it(`should simulate complete flow with direct persistence handlers`, async () => { // Create a fake backend store to simulate server-side storage const fakeBackend = { @@ -1078,10 +954,18 @@ describe(`Electric Integration`, () => { return txid }) - // Create direct persistence handler that returns the txid - const onInsert = vi.fn(async (params: MutationFnParams) => { - return { txid: await mutationFn(params) } - }) + const onInsert = vi.fn( + async ( + params: InsertMutationFnParams< + Row, + string | number, + ElectricCollectionUtils + >, + ) => { + const txid = await mutationFn(params) + await params.collection.utils.awaitTxId(txid) + }, + ) // Create a test collection with our direct persistence handler const config = { @@ -1105,13 +989,13 @@ describe(`Electric Integration`, () => { name: `Direct Persistence User`, }) - // If awaitTxId wasn't called automatically, this wouldn't be true. + // The handler is still waiting for awaitTxId at this checkpoint. expect(testCollection._state.syncedData.size).toEqual(0) // Verify that our onInsert handler was called expect(onInsert).toHaveBeenCalled() - await tx.isPersisted.promise + await tx.when(`settled`) // Verify that the data was added to the collection via the sync process expect(testCollection.has(1)).toBe(true) @@ -1124,7 +1008,17 @@ describe(`Electric Integration`, () => { it(`should remove optimistic insert when txid sync confirms a different server-generated key`, async () => { const txid = 1234 - const onInsert = vi.fn().mockResolvedValue({ txid }) + const onInsert = vi.fn( + async ({ + collection, + }: InsertMutationFnParams< + Row, + string | number, + ElectricCollectionUtils + >) => { + await collection.utils.awaitTxId(txid) + }, + ) const testCollection = createCollection( electricCollectionOptions({ @@ -1155,7 +1049,7 @@ describe(`Electric Integration`, () => { { headers: { control: `up-to-date` } }, ]) - await tx.isPersisted.promise + await tx.when(`settled`) expect(testCollection.has(4733)).toBe(false) expect(stripVirtualProps(testCollection.get(24))).toEqual({ @@ -1165,7 +1059,7 @@ describe(`Electric Integration`, () => { expect(Array.from(testCollection.state.keys())).toEqual([24]) }) - it(`should support void strategy when handler returns nothing`, async () => { + it(`settles immediately when a handler does not wait for sync`, async () => { const onInsert = vi.fn().mockResolvedValue(undefined) const config = { @@ -1184,15 +1078,22 @@ describe(`Electric Integration`, () => { // Insert with void strategy - should complete immediately without waiting const tx = testCollection.insert({ id: 1, name: `Void Test` }) - await expect(tx.isPersisted.promise).resolves.toBeDefined() + await expect(tx.when(`settled`)).resolves.toBeDefined() expect(onInsert).toHaveBeenCalled() }) - it(`should support custom timeout in matching strategy`, async () => { - const onInsert = vi.fn(() => { - // Return a txid that will never arrive with a very short timeout - return Promise.resolve({ txid: 999999, timeout: 100 }) - }) + it(`supports a custom awaitTxId timeout`, async () => { + const onInsert = vi.fn( + async ({ + collection, + }: InsertMutationFnParams< + Row, + string | number, + ElectricCollectionUtils + >) => { + await collection.utils.awaitTxId(999999, 100) + }, + ) const config = { id: `test-custom-timeout`, @@ -1214,12 +1115,12 @@ describe(`Electric Integration`, () => { expect(onInsert).toHaveBeenCalled() // The transaction should reject due to timeout - await expect(tx.isPersisted.promise).rejects.toThrow( + await expect(tx.when(`settled`)).rejects.toThrow( `Timeout waiting for txId: 999999`, ) }) - it(`should handle array of txids returned from handler`, async () => { + it(`waits for every txid requested by a handler`, async () => { // Create a fake backend that returns multiple txids const fakeBackend = { persist: ( @@ -1231,43 +1132,52 @@ describe(`Electric Integration`, () => { }, } - // Create handler that returns array of txids - const onInsert = vi.fn(async (params: MutationFnParams) => { - const txids = await fakeBackend.persist(params.transaction.mutations) - - // Simulate server sending sync messages on different ticks - // In the real world, multiple txids rarely arrive together - setTimeout(() => { - subscriber([ - { - key: `1`, - value: { id: 1, name: `Item 1` }, - headers: { - operation: `insert`, - txids: [txids[0]!], + // Create handler that waits for each txid + const onInsert = vi.fn( + async ( + params: InsertMutationFnParams< + Row, + string | number, + ElectricCollectionUtils + >, + ) => { + const txids = await fakeBackend.persist(params.transaction.mutations) + + // Simulate server sending sync messages on different ticks + // In the real world, multiple txids rarely arrive together + setTimeout(() => { + subscriber([ + { + key: `1`, + value: { id: 1, name: `Item 1` }, + headers: { + operation: `insert`, + txids: [txids[0]!], + }, }, - }, - { headers: { control: `up-to-date` } }, - ]) - }, 1) + { headers: { control: `up-to-date` } }, + ]) + }, 1) - setTimeout(() => { - subscriber([ - { - key: `2`, - value: { id: 2, name: `Item 2` }, - headers: { - operation: `insert`, - txids: [txids[1]!], + setTimeout(() => { + subscriber([ + { + key: `2`, + value: { id: 2, name: `Item 2` }, + headers: { + operation: `insert`, + txids: [txids[1]!], + }, }, - }, - { headers: { control: `up-to-date` } }, - ]) - }, 2) + { headers: { control: `up-to-date` } }, + ]) + }, 2) - // Return array of txids - this is the pattern that's failing - return { txid: txids } - }) + await Promise.all( + txids.map((txid) => params.collection.utils.awaitTxId(txid)), + ) + }, + ) const config = { id: `test-array-txids`, @@ -1289,7 +1199,7 @@ describe(`Electric Integration`, () => { ]) // This should resolve when all txids are seen - await expect(tx.isPersisted.promise).resolves.toBeDefined() + await expect(tx.when(`settled`)).resolves.toBeDefined() expect(onInsert).toHaveBeenCalled() // Verify both items were added @@ -1352,7 +1262,7 @@ describe(`Electric Integration`, () => { }, 100) // Wait for both the custom match and persistence - await Promise.all([customMatchPromise, insertPromise.isPersisted.promise]) + await Promise.all([customMatchPromise, insertPromise.when(`settled`)]) expect(onInsert).toHaveBeenCalled() expect(testCollection.has(1)).toBe(true) @@ -1388,7 +1298,7 @@ describe(`Electric Integration`, () => { const tx = testCollection.insert({ id: 1, name: `Timeout Test` }) // Capture the rejection promise before advancing timers - const rejectionPromise = expect(tx.isPersisted.promise).rejects.toThrow( + const rejectionPromise = expect(tx.when(`settled`)).rejects.toThrow( `Timeout waiting for custom match function`, ) @@ -1472,7 +1382,7 @@ describe(`Electric Integration`, () => { resolveServerCall!() // Should complete successfully - awaitMatch should find the message in the buffer - await insertPromise.isPersisted.promise + await insertPromise.when(`settled`) expect(onInsert).toHaveBeenCalled() expect(testCollection.has(42)).toBe(true) @@ -1540,7 +1450,7 @@ describe(`Electric Integration`, () => { subscriber([{ headers: { control: `up-to-date` } }]) }, 150) - await insertPromise.isPersisted.promise + await insertPromise.when(`settled`) expect(onInsert).toHaveBeenCalled() // Match should have been found before resolve (both times should exist) @@ -1626,7 +1536,7 @@ describe(`Electric Integration`, () => { // even though item 200 from the first batch is no longer in the buffer resolveServerCall!() - await insertPromise.isPersisted.promise + await insertPromise.when(`settled`) expect(testCollection.has(201)).toBe(true) }) @@ -1673,7 +1583,7 @@ describe(`Electric Integration`, () => { // Insert looking for id 999 which doesn't exist const tx = testCollection.insert({ id: 301, name: `Test` }) - await expect(tx.isPersisted.promise).rejects.toThrow( + await expect(tx.when(`settled`)).rejects.toThrow( `Timeout waiting for custom match function`, ) }) @@ -1756,9 +1666,9 @@ describe(`Electric Integration`, () => { serverCalls.forEach((call) => call.resolve()) await Promise.all([ - insert1.isPersisted.promise, - insert2.isPersisted.promise, - insert3.isPersisted.promise, + insert1.when(`settled`), + insert2.when(`settled`), + insert3.when(`settled`), ]) expect(matches).toContain(400) @@ -1826,7 +1736,7 @@ describe(`Electric Integration`, () => { // Resolve server call for insert 1 const call1 = serverCalls.find((c) => c.id === 500) call1?.resolve() - await insert1.isPersisted.promise + await insert1.when(`settled`) expect(testCollection.has(500)).toBe(true) // Insert 2 - starts waiting on server call @@ -1847,7 +1757,7 @@ describe(`Electric Integration`, () => { // Resolve server call for insert 2 const call2 = serverCalls.find((c) => c.id === 501) call2?.resolve() - await insert2.isPersisted.promise + await insert2.when(`settled`) expect(testCollection.has(501)).toBe(true) // Insert 3 - starts waiting on server call @@ -1868,7 +1778,7 @@ describe(`Electric Integration`, () => { // Resolve server call for insert 3 const call3 = serverCalls.find((c) => c.id === 502) call3?.resolve() - await insert3.isPersisted.promise + await insert3.when(`settled`) expect(testCollection.has(502)).toBe(true) }) @@ -1953,7 +1863,7 @@ describe(`Electric Integration`, () => { resolveServerCall!() // Should complete successfully - await insertPromise.isPersisted.promise + await insertPromise.when(`settled`) expect(onInsert).toHaveBeenCalled() expect(testCollection.has(600)).toBe(true) @@ -2017,9 +1927,9 @@ describe(`Electric Integration`, () => { expect(typeof testCollection.utils.awaitMatch).toBe(`function`) }) - it(`should support multiple strategies in different handlers`, () => { - const onInsert = vi.fn().mockResolvedValue({ txid: 100 }) // Txid strategy - const onUpdate = vi.fn().mockResolvedValue(undefined) // Void strategy (no return) + it(`passes distinct handlers through to collection options`, () => { + const onInsert = vi.fn().mockResolvedValue(undefined) + const onUpdate = vi.fn().mockResolvedValue(undefined) const onDelete = vi .fn() .mockImplementation(async ({ collection: col }) => { @@ -2045,7 +1955,7 @@ describe(`Electric Integration`, () => { const options = electricCollectionOptions(config) - // All handlers should be wrapped properly + // All handlers are present on the resulting options expect(options.onInsert).toBeDefined() expect(options.onUpdate).toBeDefined() expect(options.onDelete).toBeDefined() @@ -2083,7 +1993,7 @@ describe(`Electric Integration`, () => { const tx = testCollection.insert({ id: 1, name: `Timeout Test` }) // Capture the rejection promise before advancing timers - const rejectionPromise = expect(tx.isPersisted.promise).rejects.toThrow( + const rejectionPromise = expect(tx.when(`settled`)).rejects.toThrow( `Timeout waiting for custom match function`, ) @@ -2150,7 +2060,7 @@ describe(`Electric Integration`, () => { }) // Set up persistence completion tracking - insertPromise.isPersisted.promise.then(() => { + insertPromise.when(`settled`).then(() => { persistenceCompleted = true }) @@ -2177,7 +2087,7 @@ describe(`Electric Integration`, () => { subscriber([{ headers: { control: `up-to-date` } }]) // Wait for persistence to complete - await insertPromise.isPersisted.promise + await insertPromise.when(`settled`) // Verify persistence completed after up-to-date expect(persistenceCompleted).toBe(true) @@ -2213,7 +2123,7 @@ describe(`Electric Integration`, () => { // Use runOnlyPendingTimers to execute the timeout await vi.runOnlyPendingTimersAsync() - await expect(tx.isPersisted.promise).resolves.toBeDefined() + await expect(tx.when(`settled`)).resolves.toBeDefined() expect(onInsert).toHaveBeenCalled() vi.useRealTimers() @@ -3013,7 +2923,7 @@ describe(`Electric Integration`, () => { expect(testCollection.has(2)).toBe(false) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await load expect(stripVirtualProps(testCollection.get(2))).toEqual({ id: 2, @@ -3022,7 +2932,7 @@ describe(`Electric Integration`, () => { } finally { request.resolve() persistence.resolve() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await testCollection.cleanup() } }) @@ -3385,7 +3295,7 @@ describe(`Electric Integration`, () => { expect(testCollection.has(2)).toBe(false) abortController.abort() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) // The accepted snapshot applies, but the aborted caller sees // `AbortError`. if (load === true) throw new Error(`Expected a pending subset load`) @@ -3395,7 +3305,7 @@ describe(`Electric Integration`, () => { } finally { abortController.abort() persistence.resolve() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await testCollection.cleanup() } }) diff --git a/packages/electron-db-sqlite-persistence/tests/electron-persisted-collection.e2e.test.ts b/packages/electron-db-sqlite-persistence/tests/electron-persisted-collection.e2e.test.ts index 0051deb86e..0a85fcb4bd 100644 --- a/packages/electron-db-sqlite-persistence/tests/electron-persisted-collection.e2e.test.ts +++ b/packages/electron-db-sqlite-persistence/tests/electron-persisted-collection.e2e.test.ts @@ -154,15 +154,13 @@ function createPersistedCollection( } type PersistedTransactionHandle = { - isPersisted: { - promise: Promise - } + when: (state: 'settled') => Promise } async function waitForPersisted( transaction: PersistedTransactionHandle, ): Promise { - await transaction.isPersisted.promise + await transaction.when(`settled`) } async function seedCollection( diff --git a/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/App.tsx b/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/App.tsx index 178bae89f6..d854184055 100644 --- a/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/App.tsx +++ b/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/App.tsx @@ -162,7 +162,7 @@ export default function App() { title: `Persisted from Expo runtime`, score: 7, }) - await insertTx.isPersisted.promise + await insertTx.when(`settled`) await collection.cleanup() collection.startSyncImmediate() await collection.stateWhenReady() diff --git a/packages/expo-db-sqlite-persistence/e2e/mobile-persisted-collection-conformance-suite.ts b/packages/expo-db-sqlite-persistence/e2e/mobile-persisted-collection-conformance-suite.ts index cc5fc9b727..4836c13cf0 100644 --- a/packages/expo-db-sqlite-persistence/e2e/mobile-persisted-collection-conformance-suite.ts +++ b/packages/expo-db-sqlite-persistence/e2e/mobile-persisted-collection-conformance-suite.ts @@ -80,15 +80,13 @@ function createPersistedCollection( } type PersistedTransactionHandle = { - isPersisted: { - promise: Promise - } + when: (state: 'settled') => Promise } async function waitForPersisted( transaction: PersistedTransactionHandle, ): Promise { - await transaction.isPersisted.promise + await transaction.when(`settled`) } async function seedCollection( diff --git a/packages/expo-db-sqlite-persistence/tests/expo-persistence.test.ts b/packages/expo-db-sqlite-persistence/tests/expo-persistence.test.ts index e5bc19f674..dd2f4c9b70 100644 --- a/packages/expo-db-sqlite-persistence/tests/expo-persistence.test.ts +++ b/packages/expo-db-sqlite-persistence/tests/expo-persistence.test.ts @@ -154,7 +154,7 @@ it(`resumes persisted sync after simulated background/foreground transitions`, a title: `Before background`, score: 1, }) - await initialInsert.isPersisted.promise + await initialInsert.when(`settled`) expect(collection.get(`1`)?.title).toBe(`Before background`) await collection.cleanup() @@ -166,7 +166,7 @@ it(`resumes persisted sync after simulated background/foreground transitions`, a title: `Post resume write`, score: 2, }) - await postResumeInsert.isPersisted.promise + await postResumeInsert.when(`settled`) expect(collection.get(`2`)?.title).toBe(`Post resume write`) const persistedRows = await persistence.adapter.loadSubset( diff --git a/packages/node-db-sqlite-persistence/e2e/node-persisted-collection.e2e.test.ts b/packages/node-db-sqlite-persistence/e2e/node-persisted-collection.e2e.test.ts index 465f3c2eb5..1a81a598f4 100644 --- a/packages/node-db-sqlite-persistence/e2e/node-persisted-collection.e2e.test.ts +++ b/packages/node-db-sqlite-persistence/e2e/node-persisted-collection.e2e.test.ts @@ -78,15 +78,13 @@ function createPersistedCollection( } type PersistedTransactionHandle = { - isPersisted: { - promise: Promise - } + when: (state: 'settled') => Promise } async function waitForPersisted( transaction: PersistedTransactionHandle, ): Promise { - await transaction.isPersisted.promise + await transaction.when(`settled`) } async function seedCollection( diff --git a/packages/node-db-sqlite-persistence/tests/node-persistence.test.ts b/packages/node-db-sqlite-persistence/tests/node-persistence.test.ts index df9b102116..3092862150 100644 --- a/packages/node-db-sqlite-persistence/tests/node-persistence.test.ts +++ b/packages/node-db-sqlite-persistence/tests/node-persistence.test.ts @@ -112,7 +112,7 @@ describe(`node persistence helpers`, () => { source.write({ type: `update`, value: { id: `row`, title: `two` } }) const predecessor = Promise.resolve(source.commit()) releaseHandler() - await mutation.isPersisted.promise + await mutation.when(`settled`) await predecessor expect(collection.get(`row`)?.title).toBe(`three`) diff --git a/packages/offline-transactions/README.md b/packages/offline-transactions/README.md index b0aa4ed24e..9587a000e2 100644 --- a/packages/offline-transactions/README.md +++ b/packages/offline-transactions/README.md @@ -86,7 +86,7 @@ const transaction = offlineTx.mutate(() => { // Commit can remain pending while offline. Observe final failure. void offlineTx.commit().catch((error) => console.error(error)) -void transaction.isPersisted.promise.catch((error) => console.error(error)) +void transaction.when('settled').catch((error) => console.error(error)) ``` On React Native, pass a custom `storage` adapter to `startOfflineExecutor`. @@ -291,7 +291,7 @@ await tx.commit() The `Transaction` returned by `mutate()` exposes local transaction state. Its `state` starts as `pending`, becomes `persisting` during commit, and settles as `completed` or `failed`. A rollback can move it to `failed` before or during -persistence. `isPersisted.promise` settles at the same success or failure +persistence. `when('settled')` settles at the same success or failure boundary. On the offline-execution path, successful settlement means the configured @@ -302,7 +302,7 @@ sync observation before returning. After `mutationFn` returns, the executor records a `deletion-pending` outbox phase before removing the row. If recording that phase or removing the row -fails, `commit()`, `isPersisted.promise`, and the per-ID completion waiter reject +fails, `commit()`, `when('settled')`, and the per-ID completion waiter reject with the storage error. The executor stops: it does not retry the deletion, process queued peers, or admit new transactions. A fresh executor can remove a marked row without calling `mutationFn` again; `beforeRetry` only filters rows @@ -333,7 +333,7 @@ const tx = offlineTx.mutate(() => { console.log(tx.state) // 'pending' try { - await Promise.all([offlineTx.commit(), tx.isPersisted.promise]) + await Promise.all([offlineTx.commit(), tx.when('settled')]) console.log(tx.state) // 'completed' } catch (error) { showSubmissionError(error) @@ -365,7 +365,7 @@ function trackPending(itemId: string, tx: Transaction) { // Handle fulfillment and rejection so cleanup does not create another // rejected Promise chain. - void tx.isPersisted.promise.then(removeThisTransaction, removeThisTransaction) + void tx.when('settled').then(removeThisTransaction, removeThisTransaction) } function isPending(itemId: string): boolean { diff --git a/packages/offline-transactions/src/OfflineExecutor.ts b/packages/offline-transactions/src/OfflineExecutor.ts index 5690d257a3..6e87647223 100644 --- a/packages/offline-transactions/src/OfflineExecutor.ts +++ b/packages/offline-transactions/src/OfflineExecutor.ts @@ -548,8 +548,7 @@ export class OfflineExecutor { // Mark as completed: every Collection that tracked it recomputes and // releases it. The actual data will come from the sync. - restorationTx.setState(`completed`) - restorationTx.isPersisted.resolve(restorationTx) + restorationTx.completeWithoutCommit() restorationTx.touchCollection() } diff --git a/packages/offline-transactions/tests/leadership-replay-oracle.property.test.ts b/packages/offline-transactions/tests/leadership-replay-oracle.property.test.ts index 6fb6473459..dc7bb45885 100644 --- a/packages/offline-transactions/tests/leadership-replay-oracle.property.test.ts +++ b/packages/offline-transactions/tests/leadership-replay-oracle.property.test.ts @@ -338,7 +338,7 @@ it.each([ if (loss === `before-call`) env.leader.setLeader(false) const transaction = action(row) transactionId = transaction.id - const settled = transaction.isPersisted.promise.then( + const settled = transaction.when(`settled`).then( () => { outcome.status = `fulfilled` }, @@ -423,7 +423,7 @@ it(`rejects a selected manual offline transaction that loses leadership before a }) const transaction = offline.mutate(() => env.collection.insert(row)) transactionId = transaction.id - persistedObserved = transaction.isPersisted.promise.then( + persistedObserved = transaction.when(`settled`).then( () => `fulfilled`, (error: unknown) => error, ) @@ -1587,9 +1587,7 @@ it.each( autoCommit: false, mutationFn: async () => {}, }) - const rolledBack = transaction.isPersisted.promise.catch( - () => undefined, - ) + const rolledBack = transaction.when(`settled`).catch(() => undefined) const expected = [{ id: `valid`, key: `once`, row }] const report = async (count: number) => { expect(callbacks.size).toBe(1) diff --git a/packages/offline-transactions/tests/restoration-ownership.test.ts b/packages/offline-transactions/tests/restoration-ownership.test.ts index a36bd3a224..5d85104bf2 100644 --- a/packages/offline-transactions/tests/restoration-ownership.test.ts +++ b/packages/offline-transactions/tests/restoration-ownership.test.ts @@ -20,7 +20,7 @@ import type { OfflineTransaction, TransactionSignaler } from '../src/types' * * - One offline transaction is one transaction: repeated `mutate()` calls * add to it, so every written row stays visible and no id is reused. - * - A restoration transaction that completes also settles `isPersisted`, as + * - A restoration transaction that completes also settles `when('settled')`, as * every settled transaction does. * - A restoration that cannot track every Collection it wrote leaves no * Collection tracking it, so no restored row outlives its owner. @@ -89,7 +89,7 @@ it(`rejects a mutate() call after an auto-committed one`, async () => { expect(first.mutations.map((mutation) => mutation.key)).toEqual([`a`]) expect(env.collection.get(`a`)?.value).toBe(`first`) expect(env.collection.get(`b`)).toBeUndefined() - await first.isPersisted.promise + await first.when(`settled`) env.executor.dispose() }) @@ -131,7 +131,7 @@ it(`settles a restoration transaction when its offline transaction completes`, a const restorationTx = tracked[0]! let settled = false - void restorationTx.isPersisted.promise.then(() => { + void restorationTx.when(`settled`).then(() => { settled = true }) release() diff --git a/packages/offline-transactions/tests/transaction-serializer-oracle.property.test.ts b/packages/offline-transactions/tests/transaction-serializer-oracle.property.test.ts index 08e12dc5ed..080734eac2 100644 --- a/packages/offline-transactions/tests/transaction-serializer-oracle.property.test.ts +++ b/packages/offline-transactions/tests/transaction-serializer-oracle.property.test.ts @@ -205,7 +205,7 @@ async function checkRoundtrip( autoCommit: false, mutationFn: async () => {}, }) - const rollback = transaction.isPersisted.promise.catch(() => undefined) + const rollback = transaction.when(`settled`).catch(() => undefined) let hasPrimaryFailure = false let primaryFailure: unknown try { diff --git a/packages/offline-transactions/tests/transaction-settlement-oracle.property.test.ts b/packages/offline-transactions/tests/transaction-settlement-oracle.property.test.ts index 6f78560330..fef0e50d0d 100644 --- a/packages/offline-transactions/tests/transaction-settlement-oracle.property.test.ts +++ b/packages/offline-transactions/tests/transaction-settlement-oracle.property.test.ts @@ -45,7 +45,7 @@ import type { * keys, and success or permanent failure at each position. Each transaction * either commits manually or auto-commits after its `mutate()` callback, with * `autoCommit: true`, with the option omitted, or as an `OfflineAction`. An - * auto-commit has no `commit()` caller, so its `isPersisted` is the commit + * auto-commit has no `commit()` caller, so its `when('settled')` is the commit * receipt. No failure, manual or auto-committed, may escape as an unhandled * process rejection, be logged again, or write debug output. Any position may * be preceded by an empty transaction on the same commit path: its callback @@ -82,7 +82,7 @@ import type { * * Held and failed deletion witnesses split named mutation function fulfillment * from durable outbox removal. At the held-delete checkpoint, the public caller - * and `isPersisted` promises remain pending. A phase-write or deletion failure + * and `when('settled')` promises remain pending. A phase-write or deletion failure * rejects the caller promise with the storage error, stops the executor, and * holds queued peers. An offline executor restart removes a marked outbox entry * without another named mutation function call. @@ -333,7 +333,7 @@ it.each(oracleSeeds(20260913, settlementOracle))( ) const expectedStatuses = Object.fromEntries([ ...emptyReceipts.map((name) => [name, `fulfilled`]), - // A plain rollback rejects isPersisted with no reason (core + // A plain rollback rejects when('settled') with no reason (core // contract); the waiter names the rollback with an Error. ...rolledBack.flatMap(({ wait, receipt }) => [ [receipt, undefined], @@ -395,7 +395,7 @@ it.each(oracleSeeds(20260913, settlementOracle))( // Receipts of transactions rolled back before they commit. const rolledBack: Array<{ wait: string; receipt: string }> = [] // Start a transaction on one commit path. An auto-commit has no - // commit() caller; its receipt is isPersisted, which settles like a + // commit() caller; its receipt is when('settled'), which settles like a // manual commit's. const start = ( commit: SettlementCommit, @@ -429,7 +429,7 @@ it.each(oracleSeeds(20260913, settlementOracle))( } let hasPrimaryFailure = false // No failure, manual or auto-committed, escapes as an unhandled - // process rejection, and none is logged again: isPersisted reports + // process rejection, and none is logged again: when('settled') reports // it. const { rejections: unhandled, cleanup: cleanupUnhandled } = captureUnhandledRejections() @@ -757,7 +757,7 @@ it.each(oracleSeeds(20260913, settlementOracle))( }, ], // A failed auto-commit with the default option, and a failed - // action, settle through isPersisted only. + // action, settle through when('settled') only. [ { sharedKeys: false, @@ -868,7 +868,7 @@ it.each([ rollbackSeeds.push(() => { tx.rollback({ isSecondaryRollback: true }) }) - void tx.isPersisted.promise.catch(() => {}) + void tx.when(`settled`).catch(() => {}) tx.mutate(() => { if (sharedKey && index > 0) seed.update(row.id, (draft) => { @@ -2906,7 +2906,7 @@ it(`keeps commit pending until successful outbox deletion`, async () => { updatedAt: new Date(0), }), ) - observedPersistence = localTransaction.isPersisted.promise.then( + observedPersistence = localTransaction.when(`settled`).then( () => { persistedStatus = `fulfilled` }, @@ -3000,7 +3000,7 @@ it.each([`removeFromOutbox`, `clearOutbox`] as const)( updatedAt: new Date(0), }), ) - observedPersistence = localTransaction.isPersisted.promise.then( + observedPersistence = localTransaction.when(`settled`).then( () => { persistedStatus = `fulfilled` }, @@ -3125,7 +3125,7 @@ it.each([ updatedAt: new Date(0), }), ) - headPersistence = localHead.isPersisted.promise.then( + headPersistence = localHead.when(`settled`).then( () => { headPersistenceResult = `fulfilled` }, @@ -3267,7 +3267,7 @@ it(`stops successful provider work when outbox deletion fails`, async () => { updatedAt: new Date(0), }), ) - observedPersistence = localTransaction.isPersisted.promise.then( + observedPersistence = localTransaction.when(`settled`).then( () => { persistedStatus = `fulfilled` }, @@ -3440,7 +3440,7 @@ it(`stops after a failed deletion-pending write and holds its queued peer`, asyn updatedAt: new Date(0), }), ) - observedPersistence = localTransaction.isPersisted.promise.then( + observedPersistence = localTransaction.when(`settled`).then( () => { persistedStatus = `fulfilled` }, @@ -3601,7 +3601,7 @@ it(`restarts a fulfilled provider transaction after a stopped deletion failure`, updatedAt: new Date(0), }), ) - observedPersistence = localTransaction.isPersisted.promise.then( + observedPersistence = localTransaction.when(`settled`).then( () => { persistedStatus = `fulfilled` }, @@ -3723,7 +3723,7 @@ it(`replays an admitted row with no fulfilled-provider checkpoint`, async () => autoCommit: false, mutationFn: () => Promise.resolve(), }) - void seedTransaction.isPersisted.promise.catch(() => {}) + void seedTransaction.when(`settled`).catch(() => {}) try { seedTransaction.mutate(() => seed.insert({ diff --git a/packages/powersync-db-collection/tests/collection-schema.test.ts b/packages/powersync-db-collection/tests/collection-schema.test.ts index 10a502cf84..5b1837c518 100644 --- a/packages/powersync-db-collection/tests/collection-schema.test.ts +++ b/packages/powersync-db-collection/tests/collection-schema.test.ts @@ -85,10 +85,7 @@ describePowerSyncSchema(`PowerSync Schema Integration`, () => { const first = collection.insert({ id: randomUUID(), name: `aname` }) const second = collection.insert({ id: randomUUID(), name: null }) expect(collection.size).eq(2) - await Promise.all([ - first.isPersisted.promise, - second.isPersisted.promise, - ]) + await Promise.all([first.when(`settled`), second.when(`settled`)]) expectInsertValidation( () => collection.insert({} as any), @@ -203,7 +200,7 @@ describePowerSyncSchema(`PowerSync Schema Integration`, () => { ? item.created_at.getTime() : undefined, ).eq(1710381566535) - await result.isPersisted.promise + await result.when(`settled`) expect( await db.getAll( `SELECT id, name, author, created_at, archived FROM documents WHERE id = ?`, @@ -303,7 +300,7 @@ describePowerSyncSchema(`PowerSync Schema Integration`, () => { expect(item?.created_at instanceof Date).true expect(item?.created_at?.getTime()).eq(1710381566535) expect(item?.archived).toBe(false) - await result.isPersisted.promise + await result.when(`settled`) expect( await db.getAll( `SELECT id, name, author, created_at, archived FROM documents WHERE id = ?`, @@ -388,7 +385,7 @@ describePowerSyncSchema(`PowerSync Schema Integration`, () => { archived: 0, }) const item = collection.get(id) - await result.isPersisted.promise + await result.when(`settled`) expect(item?.name instanceof MyDataClass).true expect(item?.name?.options.value).eq(`document`) expect( diff --git a/packages/powersync-db-collection/tests/correctness-oracle.test.ts b/packages/powersync-db-collection/tests/correctness-oracle.test.ts index a974b7b781..f62a546fd5 100644 --- a/packages/powersync-db-collection/tests/correctness-oracle.test.ts +++ b/packages/powersync-db-collection/tests/correctness-oracle.test.ts @@ -243,7 +243,7 @@ describePowerSync(`PowerSync correctness oracle`, () => { }) await heldChanges.flush() - await transaction.isPersisted.promise + await transaction.when(`settled`) const crud = await db.getAll( `SELECT data FROM ps_crud ORDER BY id`, @@ -544,9 +544,11 @@ describePowerSync(`PowerSync correctness oracle`, () => { watcherDeliveries.length = 0 const metadata = { source: `Collection.update` } - await collection.update(`t1`, { metadata }, (draft) => { - draft.done = 1 - }).isPersisted.promise + await collection + .update(`t1`, { metadata }, (draft) => { + draft.done = 1 + }) + .when(`settled`) expect( watcherDeliveries.some((tables) => @@ -627,12 +629,14 @@ describePowerSync(`PowerSync correctness oracle`, () => { let failure: { error: unknown } | undefined try { await collection.preload() - await collection.update(`f1`, (draft) => { - draft.label = `` - draft.note = null - draft.count = 0 - draft.active = false - }).isPersisted.promise + await collection + .update(`f1`, (draft) => { + draft.label = `` + draft.note = null + draft.count = 0 + draft.active = false + }) + .when(`settled`) const crud = await db.getAll( `SELECT data FROM ps_crud ORDER BY id`, @@ -676,9 +680,11 @@ describePowerSync(`PowerSync correctness oracle`, () => { let failure: { error: unknown } | undefined try { await collection.preload() - await collection.update(`t1`, { metadata: ignoredMetadata }, (draft) => { - draft.title = `after` - }).isPersisted.promise + await collection + .update(`t1`, { metadata: ignoredMetadata }, (draft) => { + draft.title = `after` + }) + .when(`settled`) await collection.cleanup() expect( @@ -763,7 +769,8 @@ describePowerSync(`PowerSync correctness oracle`, () => { .update(`t1`, (draft) => { draft.title = `Write report today` }) - .isPersisted.promise.catch((error: unknown) => { + .when(`settled`) + .catch((error: unknown) => { persistenceError = error }) .finally(() => { diff --git a/packages/powersync-db-collection/tests/on-demand-sync.test.ts b/packages/powersync-db-collection/tests/on-demand-sync.test.ts index 6c3926542f..2a847f9582 100644 --- a/packages/powersync-db-collection/tests/on-demand-sync.test.ts +++ b/packages/powersync-db-collection/tests/on-demand-sync.test.ts @@ -593,7 +593,7 @@ describe(`On-Demand Sync Mode`, () => { } resolvePersistence() - await transaction.isPersisted.promise + await transaction.when(`settled`) if (outcome === `apply`) { expect(await preload).toEqual({ status: `fulfilled` }) expect( @@ -623,7 +623,7 @@ describe(`On-Demand Sync Mode`, () => { } }, [ resolvePersistence, - () => transaction.isPersisted.promise, + () => transaction.when(`settled`), () => subscription.unsubscribe(), () => electronics.cleanup(), () => collection.cleanup(), @@ -1514,7 +1514,7 @@ describe(`On-Demand Sync Mode`, () => { ) const tx = collection.delete(productA!.id) - await tx.isPersisted.promise + await tx.when(`settled`) await vi.waitFor( () => { @@ -1582,7 +1582,7 @@ describe(`On-Demand Sync Mode`, () => { price: 99, category: `electronics`, }) - await tx.isPersisted.promise + await tx.when(`settled`) await vi.waitFor( () => { @@ -1650,7 +1650,7 @@ describe(`On-Demand Sync Mode`, () => { const tx = collection.update(productA!.id, (d) => { d.price = 999 }) - await tx.isPersisted.promise + await tx.when(`settled`) await vi.waitFor( () => { @@ -1717,7 +1717,7 @@ describe(`On-Demand Sync Mode`, () => { // Delete Product A const tx = collection.delete(productA.id) - await tx.isPersisted.promise + await tx.when(`settled`) await vi.waitFor( () => { @@ -1785,7 +1785,7 @@ describe(`On-Demand Sync Mode`, () => { price: 99, category: `electronics`, }) - await tx.isPersisted.promise + await tx.when(`settled`) await vi.waitFor( () => { @@ -1850,7 +1850,7 @@ describe(`On-Demand Sync Mode`, () => { const tx = collection.update(productA.id, (d) => { d.price = 999 }) - await tx.isPersisted.promise + await tx.when(`settled`) await vi.waitFor( () => { @@ -2399,7 +2399,7 @@ describe(`On-Demand Sync Mode`, () => { }) describe(`Pending mutations during filter changes`, () => { - it(`should resolve isPersisted when loadSubset is called during a pending mutation`, async () => { + it(`should resolve when('settled') when loadSubset is called during a pending mutation`, async () => { const db = await createDatabase() await createTestProducts(db) @@ -2463,16 +2463,16 @@ describe(`On-Demand Sync Mode`, () => { await clothingQuery.preload() - // isPersisted.promise should resolve — if the bug is present, this hangs forever + // when('settled') should resolve — if the bug is present, this hangs forever await vi.waitFor( async () => { - await insertResult.isPersisted.promise + await insertResult.when(`settled`) }, { timeout: 5000 }, ) }) - it(`should resolve isPersisted when unloadSubset is called during a pending mutation`, async () => { + it(`should resolve when('settled') when unloadSubset is called during a pending mutation`, async () => { const db = await createDatabase() await createTestProducts(db) @@ -2545,10 +2545,10 @@ describe(`On-Demand Sync Mode`, () => { // which rebuilds the diff trigger, potentially dropping unprocessed diff records clothingQuery.cleanup() - // isPersisted.promise should resolve — if the bug is present, this hangs forever + // when('settled') should resolve — if the bug is present, this hangs forever await vi.waitFor( async () => { - await insertResult.isPersisted.promise + await insertResult.when(`settled`) }, { timeout: 5000 }, ) @@ -2609,7 +2609,7 @@ describe(`On-Demand Sync Mode`, () => { }) : collection.delete(id) let settled = false - const observed = mutation.isPersisted.promise.then( + const observed = mutation.when(`settled`).then( () => { settled = true return { status: `fulfilled` as const } diff --git a/packages/powersync-db-collection/tests/powersync.test.ts b/packages/powersync-db-collection/tests/powersync.test.ts index 571da3dc24..052bb58b89 100644 --- a/packages/powersync-db-collection/tests/powersync.test.ts +++ b/packages/powersync-db-collection/tests/powersync.test.ts @@ -211,13 +211,14 @@ describePowerSync(`PowerSync Integration`, () => { expect(newDoc!.name).toBe(`new`) expect(newDoc!.author).toBe(`somebody`) - await tx.isPersisted.promise + await tx.when(`settled`) // The item should now be present in PowerSync // We should also have patched it back in to Tanstack DB (removing the optimistic state) // Now do an update - await collection.update(id, (d) => (d.name = `updatedNew`)).isPersisted - .promise + await collection + .update(id, (d) => (d.name = `updatedNew`)) + .when(`settled`) const updatedDoc = collection.get(id) expect(updatedDoc).toBeDefined() @@ -225,7 +226,7 @@ describePowerSync(`PowerSync Integration`, () => { // Only the updated field should be updated expect(updatedDoc!.author).toBe(`somebody`) - await collection.delete(id).isPersisted.promise + await collection.delete(id).when(`settled`) // There should be a crud entries for this const crudEntries = await db.getAll<{ op: string }>(` @@ -282,7 +283,7 @@ describePowerSync(`PowerSync Integration`, () => { }) await addTx.commit() - await addTx.isPersisted.promise + await addTx.when(`settled`) expect(collection.size).toBe(8) @@ -362,7 +363,7 @@ describePowerSync(`PowerSync Integration`, () => { await collection.stateWhenReady() // Seed the row which will be deleted in the transaction - await collection.insert({ id: `a`, name: `row a` }).isPersisted.promise + await collection.insert({ id: `a`, name: `row a` }).when(`settled`) const { trackedTableName } = options.utils.getMeta() @@ -473,7 +474,7 @@ describePowerSync(`PowerSync Integration`, () => { }) await addTx.commit() - await addTx.isPersisted.promise + await addTx.when(`settled`) expect(documentsCollection.size).toBe(8) expect(usersCollection.size).toBe(5) @@ -582,25 +583,25 @@ describePowerSync(`PowerSync Integration`, () => { boolean: true, } const id = randomUUID() - await collection.insert( - { - id, - name: `new`, - author: `somebody`, - }, - { - metadata, - }, - ).isPersisted.promise + await collection + .insert( + { + id, + name: `new`, + author: `somebody`, + }, + { + metadata, + }, + ) + .when(`settled`) // Now do an update - await collection.update( - id, - { metadata: metadata }, - (d) => (d.name = `updatedNew`), - ).isPersisted.promise + await collection + .update(id, { metadata: metadata }, (d) => (d.name = `updatedNew`)) + .when(`settled`) - await collection.delete(id, { metadata }).isPersisted.promise + await collection.delete(id, { metadata }).when(`settled`) // There should be a crud entries for this const crudBatch = await db.getCrudBatch(100) @@ -666,7 +667,7 @@ describePowerSync(`PowerSync Integration`, () => { try { await addTx.commit() - await addTx.isPersisted.promise + await addTx.when(`settled`) expect.fail(`Should have thrown an error`) } catch (error) { expect(error).toBeDefined() @@ -707,14 +708,18 @@ describePowerSync(`PowerSync Integration`, () => { .forEach((change) => bookNames.add(change)) }) - await collection.insert({ - id: randomUUID(), - name: `notabook`, - }).isPersisted.promise - await collection.insert({ - id: randomUUID(), - name: `book`, - }).isPersisted.promise + await collection + .insert({ + id: randomUUID(), + name: `notabook`, + }) + .when(`settled`) + await collection + .insert({ + id: randomUUID(), + name: `book`, + }) + .when(`settled`) expect(collection.size).eq(2) await vi.waitFor( diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 087babc903..adf1b27dc6 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -31,9 +31,12 @@ import type { BaseCollectionConfig, ChangeMessage, CollectionConfig, + DeleteMutationFnParams, + InsertMutationFnParams, LoadSubsetOptions, SyncAppliedReceipt, SyncConfig, + UpdateMutationFnParams, } from '@tanstack/db' import type { FetchStatus, @@ -123,17 +126,57 @@ export interface QueryCollectionConfig< TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never, TQueryData = Awaited>, -> extends BaseCollectionConfig< - T, - TKey, - TSchema, - QueryCollectionUtils< +> extends Omit< + BaseCollectionConfig< T, TKey, - [TSchema] extends [never] ? T : InferSchemaInput, - TError - > + TSchema, + QueryCollectionUtils< + T, + TKey, + [TSchema] extends [never] ? T : InferSchemaInput, + TError + > + >, + `onInsert` | `onUpdate` | `onDelete` > { + // Query Collection retains its 1.x return-value compatibility at this adapter boundary. + onInsert?: ( + params: InsertMutationFnParams< + T, + TKey, + QueryCollectionUtils< + T, + TKey, + [TSchema] extends [never] ? T : InferSchemaInput, + TError + > + >, + ) => Promise + onUpdate?: ( + params: UpdateMutationFnParams< + T, + TKey, + QueryCollectionUtils< + T, + TKey, + [TSchema] extends [never] ? T : InferSchemaInput, + TError + > + >, + ) => Promise + onDelete?: ( + params: DeleteMutationFnParams< + T, + TKey, + QueryCollectionUtils< + T, + TKey, + [TSchema] extends [never] ? T : InferSchemaInput, + TError + > + >, + ) => Promise /** The query key used by TanStack Query to identify this query */ queryKey: TQueryKey | TQueryKeyBuilder /** Function that fetches data from the server. Must return the complete collection state */ diff --git a/packages/query-db-collection/tests/optimistic-writeback.test.ts b/packages/query-db-collection/tests/optimistic-writeback.test.ts index f9ace4cfb9..56c4a59f6c 100644 --- a/packages/query-db-collection/tests/optimistic-writeback.test.ts +++ b/packages/query-db-collection/tests/optimistic-writeback.test.ts @@ -55,12 +55,12 @@ it.each([`insert`, `upsert`] as const)( }) try { await live.preload() - await insert({ id: `one`, text: `created` }).isPersisted.promise + await insert({ id: `one`, text: `created` }).when(`settled`) for (const text of [`renamed`, `renamed again`]) { - await rename({ id: `one`, text }).isPersisted.promise + await rename({ id: `one`, text }).when(`settled`) expect([...live.values()].map((row) => row.text)).toEqual([text]) } - await insert({ id: `two`, text: `second` }).isPersisted.promise + await insert({ id: `two`, text: `second` }).when(`settled`) expect([...live.values()].map((row) => row.text).sort()).toEqual([ `renamed again`, `second`, @@ -103,7 +103,7 @@ it(`keeps repeated optimistic updates valid after direct upsert acknowledgement` draft.position += 1 }), ) - await tx.isPersisted.promise + await tx.when(`settled`) expect([...live.values()].map((row) => row.position)).toEqual([next]) } } finally { diff --git a/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts b/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts index 18b33a2e1b..fc34fe9f5e 100644 --- a/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts +++ b/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts @@ -2033,7 +2033,7 @@ describe(`query collection ownership lifecycle`, () => { }) cleanups.push(async () => { persistence.resolve() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) consoleError.mockRestore() await collection.cleanup() queryClient.clear() @@ -2081,7 +2081,7 @@ describe(`query collection ownership lifecycle`, () => { expect(collection.get(shared.id)?.name).toBe(`Optimistic`) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) const results = await refetch model = reduceMutationRefreshLiveness(model, { type: `settle-application`, @@ -2253,7 +2253,7 @@ describe(`query collection ownership lifecycle`, () => { cleanups.push(async () => { unsubscribeCache() persistence.resolve() - await transaction?.isPersisted.promise.catch(() => undefined) + await transaction?.when(`settled`).catch(() => undefined) await collection.cleanup() queryClient.clear() }) @@ -2305,7 +2305,7 @@ describe(`query collection ownership lifecycle`, () => { outcome: `rejected`, }) persistence.resolve() - await transaction?.isPersisted.promise + await transaction?.when(`settled`) const results = await refetch expect(actualOutcome).toBe(observeRefetchCallSettlement(model).refetch) @@ -2437,7 +2437,7 @@ describe(`query collection ownership lifecycle`, () => { }) cleanups.push(async () => { persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) }) let model: MutationRefreshLivenessModel = { @@ -2485,7 +2485,7 @@ describe(`query collection ownership lifecycle`, () => { }).toEqual(observeMutationRefreshLiveness(model)) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await refetch model = reduceMutationRefreshLiveness(model, { type: `settle-application`, @@ -2543,7 +2543,7 @@ describe(`query collection ownership lifecycle`, () => { explicitResult.resolve([authoritative]) persistence.resolve() if (transactionStarted) { - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) } await Promise.allSettled([eager.cleanup(), onDemand.cleanup()]) queryClient.clear() @@ -2600,7 +2600,7 @@ describe(`query collection ownership lifecycle`, () => { }).toEqual(observeMutationRefreshLiveness(model)) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) const results = await refetch model = reduceMutationRefreshLiveness(model, { type: `settle-application`, @@ -2718,7 +2718,7 @@ describe(`query collection ownership lifecycle`, () => { }).toEqual(observeMutationRefreshLiveness(model)) releaseHandler.resolve() - await mutation.isPersisted.promise + await mutation.when(`settled`) await refresh model = reduceMutationRefreshLiveness(model, { type: `settle-application`, @@ -2819,7 +2819,7 @@ describe(`query collection ownership lifecycle`, () => { mutationFn: () => persistence.promise, }) transaction.mutate(() => collection.insert(optimistic)) - void transaction.isPersisted.promise.catch(() => undefined) + void transaction.when(`settled`).catch(() => undefined) applicationScan.resolve([]) model = reduceMutationRefreshLiveness(model, { type: `accept-application`, @@ -2836,7 +2836,7 @@ describe(`query collection ownership lifecycle`, () => { expect(collection.get(shared.id)?.name).not.toBe(authoritative.name) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await refresh model = reduceMutationRefreshLiveness(model, { type: `settle-application`, @@ -2951,7 +2951,7 @@ describe(`query collection ownership lifecycle`, () => { // breaks the deliberately reproduced cycle during RED cleanup. collection.utils.writeUpdate(authoritative) releaseMutation.resolve() - await transactionReference.current.isPersisted.promise + await transactionReference.current.when(`settled`) } await collection.cleanup() queryClient.clear() @@ -2980,7 +2980,7 @@ describe(`query collection ownership lifecycle`, () => { transaction = action(`refresh`) } transactionReference.current = transaction - void transaction.isPersisted.promise.catch(() => undefined) + void transaction.when(`settled`).catch(() => undefined) await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) let model: MutationRefreshLivenessModel = { @@ -3070,7 +3070,7 @@ describe(`query collection ownership lifecycle`, () => { if (transactionReference.current) { collection.utils.writeUpdate(authoritative) releaseHandler.resolve() - await transactionReference.current.isPersisted.promise + await transactionReference.current.when(`settled`) } await collection.cleanup() queryClient.clear() @@ -3081,7 +3081,7 @@ describe(`query collection ownership lifecycle`, () => { draft.name = `Optimistic` }) transactionReference.current = transaction - void transaction.isPersisted.promise.catch(() => undefined) + void transaction.when(`settled`).catch(() => undefined) await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) let model: MutationRefreshLivenessModel = { @@ -3417,7 +3417,7 @@ describe(`query collection ownership lifecycle`, () => { : collection.delete(initial.map(({ id }) => id)) externalTransactionReference.current = mutation inspectIdentity.resolve() - void mutation.isPersisted.promise.catch(() => undefined) + void mutation.when(`settled`).catch(() => undefined) await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) expect(handlerOutcome).toBe(observeHandlerRefetchBoundary(model).refetch) if (!identityObservation) { @@ -3444,7 +3444,7 @@ describe(`query collection ownership lifecycle`, () => { ) }) - await mutation.isPersisted.promise + await mutation.when(`settled`) model = reduceHandlerRefetchBoundary(model, { type: `settle-application`, outcome: `fulfilled`, @@ -3592,7 +3592,7 @@ describe(`query collection ownership lifecycle`, () => { const mutation = collection.update(shared.id, (draft) => { draft.name = `Optimistic` }) - await mutation.isPersisted.promise + await mutation.when(`settled`) model = reduceHandlerRefetchBoundary(model, { type: `accept-application`, }) @@ -5168,7 +5168,7 @@ describe(`query collection ownership lifecycle`, () => { await handlerEntered.promise await collection.cleanup() releaseHandler.resolve() - await expect(mutation.isPersisted.promise).rejects.toBeInstanceOf( + await expect(mutation.when(`settled`)).rejects.toBeInstanceOf( SyncNotInitializedError, ) @@ -5280,7 +5280,7 @@ describe(`query collection ownership lifecycle`, () => { const settled = new Set() for (const transactionIndex of settlementOrder) { persistenceGates[transactionIndex]!.resolve() - await transactions[transactionIndex]!.isPersisted.promise + await transactions[transactionIndex]!.when(`settled`) settled.add(transactionIndex) const authoritativeIds = [...settled].map((index) => items[index]!.id) @@ -5317,7 +5317,7 @@ describe(`query collection ownership lifecycle`, () => { ) persistenceGates[2]!.resolve() - await third.isPersisted.promise + await third.when(`settled`) expect(queryFn).toHaveBeenCalledTimes(4) expectMutationLifecycleSnapshot( @@ -5494,7 +5494,7 @@ describe(`query collection ownership lifecycle`, () => { ) releaseHandler.resolve() - await mutation.isPersisted.promise + await mutation.when(`settled`) await waiter await vi.waitFor(() => expect(itemIds(derived.toArray)).toEqual([initial.id, inserted.id]), @@ -5589,7 +5589,7 @@ describe(`query collection ownership lifecycle`, () => { ]) releaseHandler.resolve() - await mutation.isPersisted.promise + await mutation.when(`settled`) await collection._sync.loadSubset({}) expect(queryFn).toHaveBeenCalledOnce() @@ -5836,7 +5836,7 @@ describe(`query collection ownership lifecycle`, () => { ]) releaseHandler.resolve() - await mutation.isPersisted.promise + await mutation.when(`settled`) await vi.waitFor(() => expect(unloaded).toBe(true)) await vi.waitFor(() => { expect({ @@ -5915,7 +5915,7 @@ describe(`query collection ownership lifecycle`, () => { cleanups.push(async () => { released.resolve() if (holder === `mutation`) - await mutation.isPersisted.promise.catch(() => undefined) + await mutation.when(`settled`).catch(() => undefined) try { await collection.cleanup() } finally { @@ -5962,7 +5962,7 @@ describe(`query collection ownership lifecycle`, () => { released.resolve() await Promise.all([first, second]) if (holder === `mutation`) - await mutation.isPersisted.promise.catch(() => undefined) + await mutation.when(`settled`).catch(() => undefined) await vi.waitFor(() => expect(observe()).toEqual({ visible: [`b`], stored: [`b`] }), ) @@ -6110,11 +6110,13 @@ describe(`query collection ownership lifecycle`, () => { await vi.waitFor(() => expect(held).toHaveLength(1)) hold = false const fetched = server - await collection.update(writtenKey, (draft) => { - draft.value = `B-optimistic` - }).isPersisted.promise + await collection + .update(writtenKey, (draft) => { + draft.value = `B-optimistic` + }) + .when(`settled`) held[0]!.resolve() - await first.isPersisted.promise + await first.when(`settled`) expected = merge(fetched, [{ id: writtenKey, value: `B` }]) } await vi.waitFor(() => expect(visible()).toEqual(expected)) @@ -6576,7 +6578,7 @@ describe(`query collection ownership lifecycle`, () => { await collection.stateWhenReady() const mutation = collection.insert(inserted) - await mutation.isPersisted.promise + await mutation.when(`settled`) expect({ cache: itemIds(queryClient.getQueryData>(queryKey) ?? []), synced: itemIds(collection._state.syncedData.values()), @@ -6723,7 +6725,7 @@ describe(`query collection ownership lifecycle`, () => { persistenceGate.resolve() await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(3)) mutationRefetchResult.resolve([]) - await mutation.isPersisted.promise + await mutation.when(`settled`) expect(queryFn).toHaveBeenCalledTimes(3) expect(commitRequests.some(({ writes }) => writes.includes(`delete`))).toBe( diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index 1dbd38b2df..72d489cee1 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -845,7 +845,7 @@ describe(`QueryCollection`, () => { expect(collection.get(`server`)).toBeUndefined() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) expect(collection.status).toBe(`ready`) expect(collection.get(`server`)).toEqual( @@ -853,7 +853,7 @@ describe(`QueryCollection`, () => { ) } finally { persistence.resolve() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await collection.cleanup() } }) @@ -890,14 +890,14 @@ describe(`QueryCollection`, () => { collection._sync.unloadSubset({}) persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await flushPromises() expect(collection.has(`first`)).toBe(false) expect(collection.has(`second`)).toBe(false) } finally { persistence.resolve() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await collection.cleanup() } }) @@ -1065,14 +1065,14 @@ describe(`QueryCollection`, () => { await flushPromises() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await vi.waitFor(() => { expect(collection.get(`server`)).toBeUndefined() }) } finally { persistence.resolve() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await collection.cleanup() } }) @@ -2579,7 +2579,7 @@ describe(`QueryCollection`, () => { queryFn.mockClear() const mutation = collection.insert({ id: `2`, name: `Item 2` }) - await mutation.isPersisted.promise + await mutation.when(`settled`) expect(queryFn).toHaveBeenCalledTimes(1) expect(warning).not.toHaveBeenCalled() @@ -4644,14 +4644,14 @@ describe(`QueryCollection`, () => { const tx = collection.update(`123`, (draft) => { draft.brandName = `B` }) - await tx.isPersisted.promise + await tx.when(`settled`) // The queued server row is hidden behind the optimistic row until the // mutation settles, then both change in one publication. expect(rowWhileHandlerRuns).toEqual({ id: `123`, brandName: `B` }) expect(collection.get(`123`)).toMatchObject({ brandName: `B (server)`, - $synced: true, + $hasPendingWrites: false, }) expect(queryClient.getQueryData(queryKey)).toEqual([ { id: `123`, brandName: `B (server)` }, @@ -4792,25 +4792,25 @@ describe(`QueryCollection`, () => { const first = collection.update(`p`, (draft) => { draft.a = 1 }) - await first.isPersisted.promise + await first.when(`settled`) expect(adapter.rows.get(`p`)?.revision).toBe(2) expect(collection.get(`p`)).toMatchObject({ a: 10, revision: 2, - $synced: true, + $hasPendingWrites: false, }) const second = collection.update(`p`, (draft) => { draft.b = 1 }) - await second.isPersisted.promise + await second.when(`settled`) expect(requestRevisions).toEqual([1, 2]) expect(adapter.rows.get(`p`)?.revision).toBe(3) expect(collection.get(`p`)).toMatchObject({ a: 10, b: 1, revision: 3, - $synced: true, + $hasPendingWrites: false, }) } finally { await collection.cleanup() @@ -4887,9 +4887,11 @@ describe(`QueryCollection`, () => { server = { id: `row`, value: 3 } second = collection.utils.refetch() } else { - second = collection.update(`row`, (draft) => { - draft.value = 0 - }).isPersisted.promise + second = collection + .update(`row`, (draft) => { + draft.value = 0 + }) + .when(`settled`) } release.resolve() await Promise.all([first, second]) @@ -4943,9 +4945,11 @@ describe(`QueryCollection`, () => { const refetch = collection.utils.refetch() await vi.waitFor(() => expect(fetches).toHaveLength(1)) - await collection.update(`row`, (draft) => { - draft.value = 0 - }).isPersisted.promise + await collection + .update(`row`, (draft) => { + draft.value = 0 + }) + .when(`settled`) expect(collection.get(`row`)?.value).toBe(3) fetches[0]!.resolve([]) @@ -6168,7 +6172,7 @@ describe(`QueryCollection`, () => { }) const transaction = collection.delete(`1`) - await transaction.isPersisted.promise + await transaction.when(`settled`) // Verify the fix: writeDelete should work, transaction completes, item is deleted expect(transaction.state).toBe(`completed`) @@ -7418,7 +7422,7 @@ describe(`QueryCollection`, () => { { id: `4`, name: `Item 4` }, ]) persistInsert() - await inserted.isPersisted.promise + await inserted.when(`settled`) expect(queryFn).toHaveBeenCalledTimes(1) queryFn.mockResolvedValue([ { id: `1`, name: `Provider changed`, category: `A` }, @@ -11319,7 +11323,7 @@ describe(`QueryCollection`, () => { hours_per_day: 8, }) outcomes.push( - firstInsert.isPersisted.promise.then( + firstInsert.when(`settled`).then( () => { settled.push(`first insert`) return `fulfilled` @@ -11343,7 +11347,7 @@ describe(`QueryCollection`, () => { expect(settled).toEqual([]) expect(firstLoad.outcome.status).toBe(`pending`) creates[0]!.release.resolve() - await firstInsert.isPersisted.promise + await firstInsert.when(`settled`) await firstLoad.promise await flushPromises() world.set(100, { @@ -11360,7 +11364,7 @@ describe(`QueryCollection`, () => { // Step 2: Delete const firstDelete = collection.delete(carolId) outcomes.push( - firstDelete.isPersisted.promise.then( + firstDelete.when(`settled`).then( () => { settled.push(`delete`) return `fulfilled` @@ -11384,7 +11388,7 @@ describe(`QueryCollection`, () => { expect(settled).toEqual([`first insert`]) expect(deleteLoad.outcome.status).toBe(`pending`) deletion.release.resolve() - await firstDelete.isPersisted.promise + await firstDelete.when(`settled`) await deleteLoad.promise await flushPromises() world.delete(100) @@ -11415,7 +11419,7 @@ describe(`QueryCollection`, () => { hours_per_day: 8, }) outcomes.push( - t2.isPersisted.promise.then( + t2.when(`settled`).then( () => { settled.push(`second insert`) return `fulfilled` @@ -11440,7 +11444,7 @@ describe(`QueryCollection`, () => { expect(settled).toEqual([`first insert`, `delete`]) expect(secondLoad.outcome.status).toBe(`pending`) creates[1]!.release.resolve() - await t2.isPersisted.promise + await t2.when(`settled`) await secondLoad.promise await flushPromises() world.set(101, { diff --git a/packages/react-db/skills/react-db/SKILL.md b/packages/react-db/skills/react-db/SKILL.md index 562d9b3d36..77d7e301f4 100644 --- a/packages/react-db/skills/react-db/SKILL.md +++ b/packages/react-db/skills/react-db/SKILL.md @@ -241,8 +241,8 @@ See db-core/live-queries/SKILL.md for full includes rules (correlation condition Live query results include computed, read-only virtual properties on every row: -- `$synced`: `true` when no pending local optimistic write affects the row; - `false` while one does. It does not prove backend confirmation. +- `$hasPendingWrites`: `true` while a pending local optimistic write affects + the row; `false` otherwise. It does not prove backend confirmation. - `$origin`: `"local"` if the last confirmed change came from this client, otherwise `"remote"`. - `$key`: the row key for the result. - `$collectionId`: the source collection ID. @@ -254,7 +254,7 @@ const { data } = useLiveQuery({ query: (q) => q .from({ todo: todoCollection }) - .where(({ todo }) => eq(todo.$synced, false)), + .where(({ todo }) => eq(todo.$hasPendingWrites, true)), }) // Shows rows with pending local optimistic writes ``` diff --git a/packages/react-db/src/live-query-internals.ts b/packages/react-db/src/live-query-internals.ts index b68cdf1abc..35748dc4aa 100644 --- a/packages/react-db/src/live-query-internals.ts +++ b/packages/react-db/src/live-query-internals.ts @@ -1,15 +1,10 @@ -import type { - DbClient, - LiveQueryObserver, - UnhashableQueryIRError, -} from '@tanstack/db' +import type { DbClient, LiveQueryObserver } from '@tanstack/db' const liveQueryResultInfo = Symbol(`liveQueryResultInfo`) export type LiveQueryResultInfo = { client: DbClient | undefined queryHash: string | undefined - identityError: UnhashableQueryIRError | undefined observer: LiveQueryObserver } diff --git a/packages/react-db/src/useLiveInfiniteQuery.ts b/packages/react-db/src/useLiveInfiniteQuery.ts index ca4e0b8da5..3806471fb7 100644 --- a/packages/react-db/src/useLiveInfiniteQuery.ts +++ b/packages/react-db/src/useLiveInfiniteQuery.ts @@ -20,12 +20,7 @@ import { createSourceIdBindings, resumeSyncStarts, } from './source-id-bindings' -import { - prepareDerivedQuery, - prepareQueryValue, - warnDeprecatedDepsArray, - warnUnhashableDerivedIdentity, -} from './useLiveQuery' +import { prepareDerivedQuery, prepareQueryValue } from './useLiveQuery' import type { SourceIdBindings } from './source-id-bindings' import type { DerivedIdentityProfiler, @@ -113,7 +108,7 @@ type InfiniteQueryRenderState = { inputCollection: Collection | null inputQuery: unknown client: DbClient | undefined - identityMode: `collection` | `queryKey` | `legacyDeps` | `derived` + identityMode: `collection` | `queryKey` | `derived` dependencies: Array | null pageSize: number initialPageParam: number @@ -135,19 +130,12 @@ type RenderedWindowCollection = { > } -/** - * Whether a recorded query identity matches the current one. Legacy deps - * compare by reference. Derived identities and query keys compare by structure. - */ +/** Whether a recorded query identity matches the current one. */ function queryIdentityMatches( comparison: { changed: boolean; structurallyEqual: boolean }, sameClient: boolean, - usesLegacyDeps: boolean, ): boolean { - return ( - sameClient && - (usesLegacyDeps ? !comparison.changed : comparison.structurallyEqual) - ) + return sameClient && comparison.structurallyEqual } /** @@ -158,7 +146,6 @@ function queryIdentityMatches( * * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work. * @param config - Configuration including pageSize and an optional initial page label - * @param deps - Deprecated array of dependencies that trigger query re-execution when changed * @returns Object with pages, data, and pagination controls */ @@ -176,14 +163,12 @@ export function useLiveInfiniteQuery< export function useLiveInfiniteQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, config: UseLiveInfiniteQueryConfig, - deps?: Array, ): UseLiveInfiniteQueryReturn // Implementation export function useLiveInfiniteQuery( queryFnOrCollection: any, config: UseLiveInfiniteQueryConfig, - deps?: Array, ): UseLiveInfiniteQueryReturn { if (`getNextPageParam` in config) { throw new Error( @@ -214,9 +199,6 @@ export function useLiveInfiniteQuery( maxMs: 0, warned: false, }) - const legacyUnhashableIdentityRef = useRef>([ - `legacy-unhashable`, - ]) const deferredCollections = new Set< CollectionImplType >() @@ -230,10 +212,6 @@ export function useLiveInfiniteQuery( if (config.queryKey !== undefined) { identityMode = `queryKey` identityDeps = config.queryKey - } else if (deps !== undefined) { - identityMode = `legacyDeps` - identityDeps = deps - warnDeprecatedDepsArray(`useLiveInfiniteQuery`) } else if ( committed?.identityMode === `derived` && committed.inputQuery === queryFnOrCollection && @@ -257,17 +235,10 @@ export function useLiveInfiniteQuery( `useLiveInfiniteQuery`, () => resumeSyncStarts(deferredCollections), ) - if (preparation.status === `hashable`) { - identityDeps = preparation.identityDeps - } else { - warnUnhashableDerivedIdentity(preparation.error) - identityDeps = legacyUnhashableIdentityRef.current - } + identityDeps = preparation.identityDeps } } - const usesLegacyDeps = - !inputIsCollection && config.queryKey === undefined && deps !== undefined const dependencyComparison = compareLiveQueryWindowDependencies( committed?.dependencies, identityDeps, @@ -275,9 +246,7 @@ export function useLiveInfiniteQuery( const sameClient = committed?.client === dbClient const dependenciesChanged = !inputIsCollection && - !queryIdentityMatches(dependencyComparison, sameClient, usesLegacyDeps) - const dependenciesStructurallyEqual = - usesLegacyDeps && sameClient && dependencyComparison.structurallyEqual + !queryIdentityMatches(dependencyComparison, sameClient) const needsNewCollection = committed === null || committed.inputKind !== inputKind || @@ -305,7 +274,7 @@ export function useLiveInfiniteQuery( sameCollection: inputIsCollection && committed?.inputCollection === queryFnOrCollection, dependenciesChanged, - dependenciesStructurallyEqual, + dependenciesStructurallyEqual: false, pageShapeChanged, }) const previousPageCount = committed @@ -337,7 +306,6 @@ export function useLiveInfiniteQuery( identityDeps, ), rendered.client === dbClient, - usesLegacyDeps, ) && rendered.collection.status !== `cleaned-up` && rendered.collection.status !== `error` && diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index d8f532ef6e..a0e47e7ea3 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -45,9 +45,7 @@ const DEFAULT_SUSPENSE_GC_TIME_MS = 5000 const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16 const DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10 const DERIVED_IDENTITY_TOTAL_WARN_MS = 50 -const warnedDepsCallsites = new Set() const warnedDerivedIdentityCallsites = new Set() -const warnedUnhashableIdentityCallsites = new Set() const unpreparedQueryValue = Symbol(`unpreparedQueryValue`) type SuspenseCollection = Collection @@ -139,27 +137,6 @@ export type ConditionalUseLiveQueryConfig = ) => ConfiguredQueryBuilder | undefined | null) } -export function warnDeprecatedDepsArray( - hookName: `useLiveQuery` | `useLiveInfiniteQuery` = `useLiveQuery`, -): void { - if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_DEPRECATION_WARNINGS`)) { - return - } - - const callsite = getWarningCallsite(4) - if (warnedDepsCallsites.has(callsite)) { - return - } - warnedDepsCallsites.add(callsite) - const replacement = - hookName === `useLiveQuery` - ? `useLiveQuery({ query })` - : `useLiveInfiniteQuery(query, { queryKey })` - console.warn( - `[${hookName}] The dependency-array form is deprecated and will be removed in 1.0. Use ${replacement} instead. Provide queryKey only for functional/opaque queries or to avoid deriving identity from structured query IR on render.`, - ) -} - function getCurrentTime(): number { return typeof performance !== `undefined` && typeof performance.now === `function` @@ -246,17 +223,10 @@ export function prepareQueryValue( return prepareLiveQueryValue(value, dbClient, deferredCollections) } -type DerivedQueryPreparation = - | { - status: `hashable` - value: unknown - identityDeps: Array - } - | { - status: `unhashable` - value: unknown - error: UnhashableQueryIRError - } +type DerivedQueryPreparation = { + value: unknown + identityDeps: Array +} export function prepareDerivedQuery( value: unknown, @@ -272,16 +242,13 @@ export function prepareDerivedQuery( try { const identity = getPreparedLiveQueryIdentity(preparedValue) - return { - status: `hashable`, - value: preparedValue, - identityDeps: [`derived`, identity], - } + return { value: preparedValue, identityDeps: [`derived`, identity] } } catch (error) { - if (error instanceof UnhashableQueryIRError) { - return { status: `unhashable`, value: preparedValue, error } - } - + if (error instanceof UnhashableQueryIRError) + throw new Error( + `Cannot derive a stable live query identity because ${error.reason} appears at ${error.path}. Provide a serializable queryKey.`, + { cause: error }, + ) throw error } finally { if (shouldProfile) { @@ -294,30 +261,9 @@ export function prepareDerivedQuery( } } -export function warnUnhashableDerivedIdentity( - error: UnhashableQueryIRError, -): void { - if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) { - return - } - - const callsite = getWarningCallsite(4) - if (warnedUnhashableIdentityCallsites.has(callsite)) { - return - } - warnedUnhashableIdentityCallsites.add(callsite) - - console.warn( - `[useLiveQuery] This query cannot derive a stable identity because ${error.reason} at ${error.path}. ` + - `It will keep the legacy mount-stable behavior for now. Add queryKey: [...] to make captured values reactive. ` + - `Unhashable queries without queryKey will throw in 1.0.`, - ) -} - /** * Create a live query using a query function. * @param queryFn - Query function that defines what data to fetch - * @param deps - Deprecated array of dependencies that trigger query re-execution when changed * @returns Object with reactive data, state, and status information * @example * // Prefer config object syntax @@ -386,7 +332,6 @@ export function warnUnhashableDerivedIdentity( // Overload 1: Accept query function that always returns QueryBuilder export function useLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, - deps?: Array, ): { state: Map> data: InferResultType @@ -408,7 +353,6 @@ export function useLiveQuery( queryFn: ( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, - deps?: Array, ): { state: Map> | undefined data: InferResultType | undefined @@ -430,7 +374,6 @@ export function useLiveQuery( queryFn: ( q: InitialQueryBuilder, ) => LiveQueryCollectionConfig | undefined | null, - deps?: Array, ): { state: Map> | undefined data: InferResultType | undefined @@ -456,7 +399,6 @@ export function useLiveQuery< queryFn: ( q: InitialQueryBuilder, ) => Collection | undefined | null, - deps?: Array, ): { state: Map | undefined data: Array | undefined @@ -488,7 +430,6 @@ export function useLiveQuery< | Collection | undefined | null, - deps?: Array, ): { state: Map> | Map | undefined @@ -512,7 +453,6 @@ export function useLiveQuery< /** * Create a live query using configuration object * @param config - Configuration object with query and options - * @param deps - Deprecated array of dependencies that trigger query re-execution when changed * @returns Object with reactive data, state, and status information * @example * // Basic config object usage @@ -582,46 +522,6 @@ export function useLiveQuery( isEnabled: boolean } -// Overload 8: Accept config object with legacy deps -export function useLiveQuery( - config: LiveQueryCollectionConfig, - deps?: Array, -): { - state: Map> - data: InferResultType - collection: Collection, string | number, {}> - status: CollectionStatus // Can't be disabled when query always returns a builder - isLoading: boolean - isReady: boolean - persistedStatus: LiveQueryPersistedStatus - isPersistedReady: boolean - persistedError: unknown | undefined - isIdle: boolean - isError: boolean - isCleanedUp: boolean - isEnabled: true // Always true when query always returns a builder -} - -// Overload 9: Accept config object with legacy deps and a query that can return undefined/null -export function useLiveQuery( - config: ConditionalUseLiveQueryConfig, - deps: Array, -): { - state: Map> | undefined - data: InferResultType | undefined - collection: Collection, string | number, {}> | undefined - status: UseLiveQueryStatus - isLoading: boolean - isReady: boolean - persistedStatus: LiveQueryPersistedStatus - isPersistedReady: boolean - persistedError: unknown | undefined - isIdle: boolean - isError: boolean - isCleanedUp: boolean - isEnabled: boolean -} - /** * Subscribe to an existing live query collection * @param liveQueryCollection - Pre-created live query collection to subscribe to @@ -698,19 +598,13 @@ export function useLiveQuery< } // Implementation - use function overloads to infer the actual collection type -export function useLiveQuery( - configOrQueryOrCollection: any, - deps?: Array, -) { - return useLiveQueryImpl(configOrQueryOrCollection, deps) +export function useLiveQuery(configOrQueryOrCollection: any) { + return useLiveQueryImpl(configOrQueryOrCollection) } /** @internal Shared implementation for the Suspense wrapper. */ -export function useLiveQueryForSuspense( - configOrQueryOrCollection: any, - deps: Array | undefined, -) { - return useLiveQueryImpl(configOrQueryOrCollection, deps, true) +export function useLiveQueryForSuspense(configOrQueryOrCollection: any) { + return useLiveQueryImpl(configOrQueryOrCollection, true) } // What one hook instance keeps across renders. It lives in a single ref @@ -718,10 +612,9 @@ export function useLiveQueryForSuspense( function createHookInstance(dbClient: DbClient | undefined) { return { collection: null as Collection | null, - deps: null as Array | null, + identityDeps: null as Array | null, config: null as unknown, client: dbClient, - legacyUnhashableIdentity: [`legacy-unhashable`] as Array, derivedIdentityProfiler: { renderCount: 0, totalMs: 0, @@ -735,23 +628,17 @@ function createHookInstance(dbClient: DbClient | undefined) { observer: null as LiveQueryObserver | null, queryHash: undefined as string | undefined, suspenseKey: undefined as string | undefined, - identityError: undefined as UnhashableQueryIRError | undefined, subscribe: null as ((onStoreChange: () => void) => () => void) | null, } } -function useLiveQueryImpl( - configOrQueryOrCollection: any, - deps: Array | undefined, - forSuspense = false, -) { +function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { const contextDbClient = useOptionalDbClient() // Check if it's already a collection const inputIsCollection = isCollection(configOrQueryOrCollection) const dbClient = inputIsCollection ? contextDbClient : (getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient) - const resolvedDeps = deps ?? [] const instanceRef = useRef | null>(null) const instance = (instanceRef.current ??= createHookInstance(dbClient)) @@ -765,36 +652,10 @@ function useLiveQueryImpl( unpreparedQueryValue let identityDeps: ReadonlyArray let streamIdentity: unknown = undefined - let identityError: UnhashableQueryIRError | undefined if (queryKey) { identityDeps = queryKey streamIdentity = [`queryKey`, queryKey] - } else if (deps !== undefined) { - identityDeps = resolvedDeps - // Deps decide reuse. Only hydration and Suspense read the query hash; the - // development warning about unhashable queries still derives it. - if ( - dbClient || - forSuspense || - shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`) - ) - try { - preparedQueryValue = prepareQueryValue( - configOrQueryOrCollection, - dbClient, - instance.deferredCollections, - ) - streamIdentity = [ - `deps`, - resolvedDeps, - getPreparedLiveQueryIdentity(preparedQueryValue), - ] - } catch (error) { - if (!(error instanceof UnhashableQueryIRError)) throw error - warnUnhashableDerivedIdentity(error) - identityError = error - } } else if (inputIsCollection) { identityDeps = [] streamIdentity = [`collection`, configOrQueryOrCollection.id] @@ -806,39 +667,13 @@ function useLiveQueryImpl( instance.deferredCollections, ) preparedQueryValue = preparation.value - if (preparation.status === `hashable`) { - identityDeps = preparation.identityDeps - streamIdentity = preparation.identityDeps - } else { - warnUnhashableDerivedIdentity(preparation.error) - identityDeps = instance.legacyUnhashableIdentity - identityError = preparation.error - } + identityDeps = preparation.identityDeps + streamIdentity = preparation.identityDeps } - let queryHash: string | undefined - if (streamIdentity !== undefined) { - try { - queryHash = getStableValueHash(streamIdentity, `queryKey`) - } catch (error) { - if (error instanceof UnhashableQueryIRError) { - if (queryKey !== undefined) throw error - identityError = error - } else { - throw error - } - } - } + const queryHash = getStableValueHash(streamIdentity, `queryKey`) - if (deps !== undefined) { - warnDeprecatedDepsArray() - } - - if ( - queryKey === undefined && - deps === undefined && - preparedQueryValue !== unpreparedQueryValue - ) { + if (queryKey === undefined && preparedQueryValue !== unpreparedQueryValue) { claimSourceIds( instance.sourceIds, preparedQueryValue, @@ -851,7 +686,6 @@ function useLiveQueryImpl( const canReuseSuspenseKey = forSuspense && !inputIsCollection && - queryHash !== undefined && instance.collection !== null && instance.client === dbClient && instance.queryHash === queryHash && @@ -888,11 +722,8 @@ function useLiveQueryImpl( const suspenseCollection = suspenseEntry?.collection const identityChanged = - instance.deps === null || - (deps !== undefined - ? instance.deps.length !== identityDeps.length || - instance.deps.some((dep, index) => dep !== identityDeps[index]) - : !deepEquals(instance.deps, identityDeps)) + instance.identityDeps === null || + !deepEquals(instance.identityDeps, identityDeps) // Check if we need to create/recreate the collection const needsNewCollection = @@ -981,12 +812,11 @@ function useLiveQueryImpl( } } instance.config = configOrQueryOrCollection - instance.deps = [...identityDeps] + instance.identityDeps = [...identityDeps] } instance.client = dbClient instance.queryHash = queryHash instance.suspenseKey = suspenseKey - instance.identityError = identityError } // Recreate the observer when the underlying collection changes. The observer @@ -1043,7 +873,6 @@ function useLiveQueryImpl( setLiveQueryResultInfo(returned, { client: dbClient, queryHash: instance.queryHash, - identityError: instance.identityError, observer, }) } diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index 28c39bab8e..f468ca9406 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -11,7 +11,6 @@ import type { GetResult, InferResultType, InitialQueryBuilder, - LiveQueryCollectionConfig, NonSingleResult, QueryBuilder, SingleResult, @@ -60,7 +59,6 @@ function rememberInitialRenderError( /** * Create a live query with React Suspense support * @param queryFn - Query function that defines what data to fetch - * @param deps - Deprecated array of dependencies that trigger query re-execution when changed * @returns Object with reactive data and state - data is guaranteed to be defined * @throws Promise when data is loading (caught by Suspense boundary) * @throws Error when collection fails (caught by Error boundary) @@ -150,7 +148,6 @@ function rememberInitialRenderError( // Overload 1: Accept query function that always returns QueryBuilder export function useLiveSuspenseQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, - deps?: Array, ): { state: Map> data: InferResultType @@ -166,16 +163,6 @@ export function useLiveSuspenseQuery( collection: Collection, string | number, {}> } -// Overload 3: Accept legacy config object -export function useLiveSuspenseQuery( - config: LiveQueryCollectionConfig, - deps?: Array, -): { - state: Map> - data: InferResultType - collection: Collection, string | number, {}> -} - // Overload 4: Accept pre-created live query collection export function useLiveSuspenseQuery< TResult extends object, @@ -203,19 +190,13 @@ export function useLiveSuspenseQuery< } // Implementation - uses useLiveQuery internally and adds Suspense logic -export function useLiveSuspenseQuery( - configOrQueryOrCollection: any, - deps?: Array, -) { +export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { const promiseRef = useRef | null>(null) const collectionRef = useRef | null>(null) const hasBeenReadyRef = useRef(false) // Use useLiveQuery to handle collection management and reactivity - const result = - deps === undefined - ? useLiveQueryForSuspense(configOrQueryOrCollection, undefined) - : useLiveQueryForSuspense(configOrQueryOrCollection, deps) + const result = useLiveQueryForSuspense(configOrQueryOrCollection) if (!result.isEnabled) { // Suspense queries cannot be disabled - this matches TanStack Query's useSuspenseQuery behavior @@ -305,11 +286,8 @@ export function useLiveSuspenseQuery( result.persistedStatus !== `unavailable`)) ) { if (queryInfo.client?._isSsrStreamingEnabled() && !queryInfo.queryHash) { - const reason = queryInfo.identityError - ? `${queryInfo.identityError.reason} at ${queryInfo.identityError.path}` - : `the query has no stable identity` throw new Error( - `Cannot stream this live query during SSR because ${reason}. Provide an explicit serializable queryKey.`, + `Cannot stream this live query during SSR because it has no stable identity. Provide an explicit serializable queryKey.`, ) } // Create or reuse promise for current collection diff --git a/packages/react-db/src/usePacedMutations.ts b/packages/react-db/src/usePacedMutations.ts index 7fc001e4a6..b78f87ae2b 100644 --- a/packages/react-db/src/usePacedMutations.ts +++ b/packages/react-db/src/usePacedMutations.ts @@ -34,7 +34,7 @@ import type { PacedMutationsConfig, Transaction } from '@tanstack/db' * * // Optional: await persistence or handle errors * try { - * await tx.isPersisted.promise + * await tx.when('settled') * console.log('Saved!') * } catch (error) { * console.error('Save failed:', error) diff --git a/packages/react-db/tests/conformance.test.tsx b/packages/react-db/tests/conformance.test.tsx index 1454151942..412f2f80fc 100644 --- a/packages/react-db/tests/conformance.test.tsx +++ b/packages/react-db/tests/conformance.test.tsx @@ -151,7 +151,7 @@ function mountControllable

( const hook = renderHook( ({ param }: { param: P }) => // Param goes in the dependency list so the hook recompiles when it changes. - useLiveQuery((q: any) => build(q, param), [param]), + useLiveQuery({ query: (q: any) => build(q, param), queryKey: [param] }), { initialProps: { param: initial } }, ) const handle = makeHandle(hook) @@ -267,13 +267,15 @@ it(`publishes a synchronous full-source ordered limit in the first layout commit ]) const commits: Array<{ ids: Array; status: string }> = [] const hook = renderHook(() => { - const result = useLiveQuery((q) => - q - .from({ row: source.collection }) - .fn.where(({ row }) => row.rank > 0) - .orderBy(({ row }) => row.rank) - .limit(2), - ) + const result = useLiveQuery({ + queryKey: [source.collection.id, `ordered-function-filter`], + query: (q) => + q + .from({ row: source.collection }) + .fn.where(({ row }) => row.rank > 0) + .orderBy(({ row }) => row.rank) + .limit(2), + }) useLayoutEffect(() => { commits.push({ ids: result.data.map(({ id }) => id), diff --git a/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx b/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx index 5042bef2d4..7202f8c952 100644 --- a/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx +++ b/packages/react-db/tests/descriptor-query-binding-oracle.test.tsx @@ -346,7 +346,9 @@ describe(`standalone descriptor query binding`, () => { ), ).toThrow(/requires a DbClient.*DbProvider/) expect(() => - renderHook(() => useLiveQuery({ query: unboundQuery }, [])), + renderHook(() => + useLiveQuery({ ...{ query: unboundQuery }, queryKey: [] }), + ), ).toThrow(/requires a DbClient.*DbProvider/) expect(() => renderHook(() => @@ -357,7 +359,9 @@ describe(`standalone descriptor query binding`, () => { ), ).toThrow(/requires a DbClient.*DbProvider/) expect(() => - renderHook(() => useLiveSuspenseQuery(() => unboundQuery, [])), + renderHook(() => + useLiveSuspenseQuery({ query: () => unboundQuery, queryKey: [] }), + ), ).toThrow(/requires a DbClient.*DbProvider/) }) @@ -782,7 +786,6 @@ describe(`standalone descriptor query binding`, () => { `$hasPendingWrites`, `$key`, `$origin`, - `$synced`, `id`, `value`, ].sort(), diff --git a/packages/react-db/tests/infinite-query-conformance.test.tsx b/packages/react-db/tests/infinite-query-conformance.test.tsx index a00fd70f2a..5919517174 100644 --- a/packages/react-db/tests/infinite-query-conformance.test.tsx +++ b/packages/react-db/tests/infinite-query-conformance.test.tsx @@ -143,9 +143,10 @@ function mountControllable

( const hook = renderHook( ({ param }: { param: P }) => useRecorded( - useLiveInfiniteQuery((q: any) => build(q, param), config as any, [ - param, - ]), + useLiveInfiniteQuery((q: any) => build(q, param), { + ...(config as any), + queryKey: [param], + }), log, ), { initialProps: { param: initial } }, @@ -216,11 +217,10 @@ function mountInputControllable( const hook = renderHook( ({ kind }: { kind: `collection` | `query` }) => useRecorded( - useLiveInfiniteQuery( - kind === `collection` ? collection : build, - config as any, - [kind], - ), + useLiveInfiniteQuery(kind === `collection` ? collection : build, { + ...(config as any), + queryKey: [kind], + }), log, ), { @@ -281,8 +281,7 @@ it(`publishes query-function pages on the first layout commit after mount and de .from({ row: source.collection }) .where(({ row }) => gt(row.rank, minimum)) .orderBy(({ row }) => row.rank, `desc`), - { pageSize: 3 }, - [minimum], + { ...{ pageSize: 3 }, queryKey: [minimum] }, ) useLayoutEffect(() => { if (!result.collection) { diff --git a/packages/react-db/tests/infinite-query-render-cuts-oracle.test.tsx b/packages/react-db/tests/infinite-query-render-cuts-oracle.test.tsx index 5fa1731d37..e589aaf4eb 100644 --- a/packages/react-db/tests/infinite-query-render-cuts-oracle.test.tsx +++ b/packages/react-db/tests/infinite-query-render-cuts-oracle.test.tsx @@ -256,7 +256,10 @@ describe(`on-demand sources match useLiveQuery`, () => { .orderBy(({ items }: any) => items.rank, `desc`) const result: any = hook === `live` - ? useLiveQuery((q: any) => query(q).limit(PAGE_SIZE + 1), [fail]) + ? useLiveQuery({ + query: (q: any) => query(q).limit(PAGE_SIZE + 1), + queryKey: [fail], + }) : useLiveInfiniteQuery(query, { pageSize: PAGE_SIZE, queryKey: [`startup-error`, fail], @@ -377,8 +380,7 @@ describe(`retries after GC`, () => { .from({ items: source }) .where(({ items }) => gt(items.rank, minimum)) .orderBy(({ items }) => items.rank, `desc`), - { pageSize: 3 }, - [minimum], + { ...{ pageSize: 3 }, queryKey: [minimum] }, ) useLayoutEffect(() => { commits.push({ diff --git a/packages/react-db/tests/useLiveInfiniteQuery.test.tsx b/packages/react-db/tests/useLiveInfiniteQuery.test.tsx index 1e67758e2f..6fcb631cc9 100644 --- a/packages/react-db/tests/useLiveInfiniteQuery.test.tsx +++ b/packages/react-db/tests/useLiveInfiniteQuery.test.tsx @@ -141,8 +141,7 @@ describe(`useLiveInfiniteQuery`, () => { .from({ post: source }) .where(({ post }) => gt(post.createdAt, minimum)) .orderBy(({ post }) => post.createdAt, `desc`), - { pageSize: 3 }, - [minimum], + { ...{ pageSize: 3 }, queryKey: [minimum] }, ) if (shouldSuspend) throw never return null @@ -378,7 +377,7 @@ describe(`useLiveInfiniteQuery`, () => { }) }) - it(`compares dependencies by identity instead of serialization`, async () => { + it(`uses a serializable query key for values read from a Map`, async () => { const source = createCollection( mockSyncCollectionOptions({ id: `infinite-query-map-deps`, @@ -394,8 +393,7 @@ describe(`useLiveInfiniteQuery`, () => { .from({ post: source }) .where(({ post }) => eq(post.category, filter.get(`category`))) .orderBy(({ post }) => post.createdAt, `desc`), - { pageSize: 3 }, - [filter], + { pageSize: 3, queryKey: [filter.get(`category`)] }, ), { initialProps: { @@ -422,7 +420,7 @@ describe(`useLiveInfiniteQuery`, () => { }) }) - it(`preserves loaded pages when dependencies are structurally unchanged`, async () => { + it(`preserves loaded pages when query keys are structurally unchanged`, async () => { const source = createCollection( mockSyncCollectionOptions({ id: `infinite-query-structurally-equal-deps`, @@ -438,8 +436,7 @@ describe(`useLiveInfiniteQuery`, () => { .from({ post: source }) .where(({ post }) => eq(post.category, filter.category)) .orderBy(({ post }) => post.createdAt, `desc`), - { pageSize: 2 }, - [filter], + { ...{ pageSize: 2 }, queryKey: [filter] }, ), { initialProps: { filter: { category: `tech` } } }, ) @@ -454,7 +451,7 @@ describe(`useLiveInfiniteQuery`, () => { rerender({ filter: { category: `tech` } }) await waitFor(() => { - expect(result.current.collection).not.toBe(firstCollection) + expect(result.current.collection).toBe(firstCollection) expect(result.current.isReady).toBe(true) }) expect(result.current.pages).toHaveLength(2) diff --git a/packages/react-db/tests/useLiveQuery.test-d.tsx b/packages/react-db/tests/useLiveQuery.test-d.tsx index 8148d2f832..f7be7bd7b8 100644 --- a/packages/react-db/tests/useLiveQuery.test-d.tsx +++ b/packages/react-db/tests/useLiveQuery.test-d.tsx @@ -151,7 +151,7 @@ describe(`useLiveQuery type assertions`, () => { >() }) - it(`types a conditional config with deprecated dependencies`, () => { + it(`types a conditional config with an explicit query key`, () => { const collection = createCollection( mockSyncCollectionOptions({ id: `test-conditional-config-deps`, @@ -162,13 +162,10 @@ describe(`useLiveQuery type assertions`, () => { const enabled = null as unknown as boolean const { result } = renderHook(() => - useLiveQuery( - { - query: (q) => - enabled ? q.from({ collection }).findOne() : undefined, - }, - [enabled], - ), + useLiveQuery({ + query: (q) => (enabled ? q.from({ collection }).findOne() : undefined), + queryKey: [enabled], + }), ) expectTypeOf(result.current.data).toMatchTypeOf< @@ -286,13 +283,13 @@ describe(`useLiveQuery type assertions`, () => { ) const { result } = renderHook(() => - useLiveQuery( - (q) => + useLiveQuery({ + query: (q) => q .from({ person: collection }) .where(({ person }) => eq(person.team, `team-1`)), - [`team-1`], - ), + queryKey: [`team-1`], + }), ) expectTypeOf(result.current.data).toMatchTypeOf< diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index 6d09e4152f..5beffddeb2 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -17,8 +17,7 @@ import { toArray, } from '@tanstack/db' import { useEffect } from 'react' -import { useLiveQuery, useLiveQueryForSuspense } from '../src/useLiveQuery' -import { getLiveQueryResultInfo } from '../src/live-query-internals' +import { useLiveQuery } from '../src/useLiveQuery' import { DbProvider } from '../src/DbProvider' import { mockSyncCollectionOptions, @@ -564,8 +563,8 @@ describe(`Query Collections`, () => { const { result, rerender } = renderHook( ({ minAge }: { minAge: number }) => { - return useLiveQuery( - (q) => + return useLiveQuery({ + query: (q) => q .from({ collection }) .where(({ collection: c }) => gt(c.age, minAge)) @@ -574,8 +573,8 @@ describe(`Query Collections`, () => { name: c.name, age: c.age, })), - [minAge], - ) + queryKey: [minAge], + }) }, { initialProps: { minAge: 30 } }, ) @@ -638,8 +637,8 @@ describe(`Query Collections`, () => { const { result, rerender } = renderHook( ({ minAge }: { minAge: number }) => { - return useLiveQuery( - (q) => + return useLiveQuery({ + query: (q) => q .from({ collection }) .where(({ collection: c }) => gt(c.age, minAge)) @@ -647,8 +646,8 @@ describe(`Query Collections`, () => { id: c.id, name: c.name, })), - [minAge], - ) + queryKey: [minAge], + }) }, { initialProps: { minAge: 30 } }, ) @@ -925,7 +924,7 @@ describe(`Query Collections`, () => { }) // Wait for the transaction to be committed - await transaction.isPersisted.promise + await transaction.when(`settled`) await waitFor(() => { // Wait for the permanent key to appear @@ -1545,8 +1544,8 @@ describe(`Query Collections`, () => { const { result, rerender } = renderHook( ({ minAge }: { minAge: number }) => { - return useLiveQuery( - (q) => + return useLiveQuery({ + query: (q) => q .from({ collection }) .where(({ collection: c }) => gt(c.age, minAge)) @@ -1554,8 +1553,8 @@ describe(`Query Collections`, () => { id: c.id, name: c.name, })), - [minAge], - ) + queryKey: [minAge], + }) }, { initialProps: { minAge: 30 } }, ) @@ -2114,17 +2113,15 @@ describe(`Query Collections`, () => { const { result, rerender } = renderHook( ({ enabled }: { enabled: boolean }) => - useLiveQuery( - { - query: (q) => { - if (!enabled) return undefined - return q - .from({ persons: collection }) - .where(({ persons }) => gt(persons.age, 30)) - }, + useLiveQuery({ + query: (q) => { + if (!enabled) return undefined + return q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) }, - [enabled], - ), + queryKey: [enabled], + }), { initialProps: { enabled: false } }, ) @@ -2337,34 +2334,31 @@ describe(`Query Collections`, () => { const { result, rerender } = renderHook( ({ useConfig }: { useConfig: boolean }) => { - return useLiveQuery( - (q) => { - if (useConfig) { - return { - query: q - .from({ persons: collection }) - .where(({ persons }) => gt(persons.age, 30)) - .select(({ persons }) => ({ - id: persons.id, - name: persons.name, - age: persons.age, - })), - startSync: true, - gcTime: 0, - } + return useLiveQuery((q) => { + if (useConfig) { + return { + query: q + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + age: persons.age, + })), + startSync: true, + gcTime: 0, } - return q - .from({ persons: collection }) - .where(({ persons }) => lte(persons.age, 30)) - .select(({ persons }) => ({ - id: persons.id, - name: persons.name, - age: persons.age, - })) - .orderBy(({ persons }) => persons.age) - }, - [useConfig], - ) + } + return q + .from({ persons: collection }) + .where(({ persons }) => lte(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + age: persons.age, + })) + .orderBy(({ persons }) => persons.age) + }) }, { initialProps: { useConfig: false } }, ) @@ -2437,22 +2431,19 @@ describe(`Query Collections`, () => { const { result, rerender } = renderHook( ({ useCollection }: { useCollection: boolean }) => { - return useLiveQuery( - (q) => { - if (useCollection) { - return liveQueryCollection - } - return q - .from({ persons: collection }) - .where(({ persons }) => lte(persons.age, 30)) - .select(({ persons }) => ({ - id: persons.id, - name: persons.name, - age: persons.age, - })) - }, - [useCollection], - ) + return useLiveQuery((q) => { + if (useCollection) { + return liveQueryCollection + } + return q + .from({ persons: collection }) + .where(({ persons }) => lte(persons.age, 30)) + .select(({ persons }) => ({ + id: persons.id, + name: persons.name, + age: persons.age, + })) + }) }, { initialProps: { useCollection: false } }, ) @@ -2513,8 +2504,8 @@ describe(`Query Collections`, () => { const { result, rerender } = renderHook( ({ minAge, enabled }: { minAge: number; enabled: boolean }) => { - return useLiveQuery( - (q) => { + return useLiveQuery({ + query: (q) => { if (!enabled) return undefined return q .from({ persons: collection }) @@ -2525,8 +2516,8 @@ describe(`Query Collections`, () => { age: persons.age, })) }, - [minAge, enabled], - ) + queryKey: [minAge, enabled], + }) }, { initialProps: { minAge: 30, enabled: false } }, ) @@ -2629,25 +2620,28 @@ describe(`Query Collections`, () => { ) const { result } = renderHook(() => { - return useLiveQuery((q) => { - const issueCountSubquery = q - .from({ issues: issueCollection }) - .groupBy(({ issues }) => issues.userId) - .select(({ issues }) => ({ - userId: issues.userId, - issueCount: coalesce(count(issues.id), 0), - })) - - return q - .from({ persons: personCollection }) - .leftJoin({ ic: issueCountSubquery }, ({ persons, ic }) => - eq(persons.id, ic.userId), - ) - .select(({ persons, ic }) => ({ - name: persons.name, - issueCount: ic.issueCount, - })) - }, []) + return useLiveQuery({ + query: (q) => { + const issueCountSubquery = q + .from({ issues: issueCollection }) + .groupBy(({ issues }) => issues.userId) + .select(({ issues }) => ({ + userId: issues.userId, + issueCount: coalesce(count(issues.id), 0), + })) + + return q + .from({ persons: personCollection }) + .leftJoin({ ic: issueCountSubquery }, ({ persons, ic }) => + eq(persons.id, ic.userId), + ) + .select(({ persons, ic }) => ({ + name: persons.name, + issueCount: ic.issueCount, + })) + }, + queryKey: [], + }) }) await waitFor(() => { @@ -3262,8 +3256,7 @@ describe(`Query Collections`, () => { expect(result.current.collection).not.toBe(firstCollection) }) - it(`warns and preserves legacy behavior when a functional query has no queryKey`, () => { - const warnSpy = vi.spyOn(console, `warn`).mockImplementation(() => {}) + it(`requires queryKey for a functional query`, () => { const collection = createCollection( mockSyncCollectionOptions({ id: `derived-identity-functional-missing-key`, @@ -3283,15 +3276,7 @@ describe(`Query Collections`, () => { }), { initialProps: { minAge: 25 } }, ), - ).not.toThrow() - - const warnings = warnSpy.mock.calls.filter(([message]) => - String(message).includes(`cannot derive a stable identity`), - ) - expect(warnings).toHaveLength(1) - expect(warnings[0]![0]).toContain(`queryKey`) - expect(warnings[0]![0]).toContain(`1.0`) - warnSpy.mockRestore() + ).toThrow(/Provide a serializable queryKey/) }) it(`uses runtime identity for opaque values in a structured query without queryKey`, () => { @@ -3328,7 +3313,7 @@ describe(`Query Collections`, () => { warnSpy.mockRestore() }) - it(`does not emit identity warnings in production`, () => { + it(`requires queryKey for a functional query in production`, () => { const previousNodeEnv = process.env.NODE_ENV process.env.NODE_ENV = `production` const warnSpy = vi.spyOn(console, `warn`).mockImplementation(() => {}) @@ -3340,24 +3325,19 @@ describe(`Query Collections`, () => { }), ) - let unmount: (() => void) | undefined try { - ;({ unmount } = renderHook(() => - useLiveQuery({ - query: (q) => - q - .from({ people: collection }) - .fn.where(({ people }) => people.age > 25), - }), - )) - - expect( - warnSpy.mock.calls.some(([message]) => - String(message).includes(`cannot derive a stable identity`), + expect(() => + renderHook(() => + useLiveQuery({ + query: (q) => + q + .from({ people: collection }) + .fn.where(({ people }) => people.age > 25), + }), ), - ).toBe(false) + ).toThrow(/Provide a serializable queryKey/) + expect(warnSpy).not.toHaveBeenCalled() } finally { - unmount?.() process.env.NODE_ENV = previousNodeEnv warnSpy.mockRestore() } @@ -3449,35 +3429,6 @@ describe(`Query Collections`, () => { expect(queryExecutions).toBe(1) }) - it(`preserves reference semantics for deprecated dependency arrays`, async () => { - const collection = createCollection( - mockSyncCollectionOptions({ - id: `legacy-deps-reference-semantics`, - getKey: (person: Person) => person.id, - initialData: initialPersons, - }), - ) - - const { result, rerender } = renderHook( - ({ filter }: { filter: { minAge: number } }) => - useLiveQuery( - (q) => - q - .from({ people: collection }) - .where(({ people }) => gt(people.age, filter.minAge)), - [filter], - ), - { initialProps: { filter: { minAge: 25 } } }, - ) - - await waitFor(() => expect(result.current.data).toHaveLength(2)) - const firstCollection = result.current.collection - - rerender({ filter: { minAge: 25 } }) - - expect(result.current.collection).not.toBe(firstCollection) - }) - it(`warns when derived query identity is slow enough to need queryKey`, () => { const warnSpy = vi.spyOn(console, `warn`).mockImplementation(() => {}) const nowSpy = vi.spyOn(globalThis.performance, `now`) @@ -3561,88 +3512,6 @@ describe(`Query Collections`, () => { nowSpy.mockRestore() warnSpy.mockRestore() }) - - it(`warns once for the deprecated dependency-array form`, () => { - const warnSpy = vi.spyOn(console, `warn`).mockImplementation(() => {}) - const collection = createCollection( - mockSyncCollectionOptions({ - id: `deps-warning`, - getKey: (person: Person) => person.id, - initialData: initialPersons, - }), - ) - - const { rerender } = renderHook( - ({ minAge }) => - useLiveQuery( - (q) => - q - .from({ people: collection }) - .where(({ people }) => gt(people.age, minAge)), - [minAge], - ), - { initialProps: { minAge: 25 } }, - ) - - rerender({ minAge: 30 }) - rerender({ minAge: 30 }) - - expect(warnSpy).toHaveBeenCalledTimes(1) - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining(`will be removed in 1.0`), - ) - - warnSpy.mockRestore() - }) - - it(`warns for an explicitly passed empty dependency array`, () => { - const warnSpy = vi.spyOn(console, `warn`).mockImplementation(() => {}) - const collection = createCollection( - mockSyncCollectionOptions({ - id: `empty-deps-warning`, - getKey: (person: Person) => person.id, - initialData: initialPersons, - }), - ) - - renderHook(() => useLiveQuery((q) => q.from({ people: collection }), [])) - - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining(`useLiveQuery({ query })`), - ) - - warnSpy.mockRestore() - }) - - it(`includes the query in legacy dependency-array SSR identity`, () => { - const collection = createCollection( - mockSyncCollectionOptions({ - id: `legacy-deps-query-identity`, - getKey: (person) => person.id, - initialData: initialPersons, - }), - ) - // Suspense is the reader of this identity; plain useLiveQuery skips it. - const first = renderHook(() => - useLiveQueryForSuspense( - (q: any) => q.from({ people: collection }), - [1], - ), - ) - const second = renderHook(() => - useLiveQueryForSuspense( - (q: any) => - q - .from({ people: collection }) - .where(({ people }: any) => gt(people.age, 30)), - [1], - ), - ) - - expect(getLiveQueryResultInfo(first.result.current).queryHash).not.toBe( - getLiveQueryResultInfo(second.result.current).queryHash, - ) - }) }) }) diff --git a/packages/react-db/tests/useLiveSuspenseQuery.test.tsx b/packages/react-db/tests/useLiveSuspenseQuery.test.tsx index 2b4d56aa55..c7647fbf3d 100644 --- a/packages/react-db/tests/useLiveSuspenseQuery.test.tsx +++ b/packages/react-db/tests/useLiveSuspenseQuery.test.tsx @@ -591,7 +591,6 @@ describe(`useLiveSuspenseQuery`, () => { }) it(`requires queryKey for an opaque query during SSR streaming`, () => { - const warn = vi.spyOn(console, `warn`).mockImplementation(() => {}) const descriptor = collectionOptions(`streamed-people`, () => ({ id: `streamed-people`, getKey: (person: Person) => person.id, @@ -620,11 +619,7 @@ describe(`useLiveSuspenseQuery`, () => { }), { wrapper }, ), - ).toThrow(/Provide an explicit serializable queryKey/) - expect(warn).toHaveBeenCalledWith( - expect.stringContaining(`cannot derive a stable identity`), - ) - warn.mockRestore() + ).toThrow(/Provide a serializable queryKey/) }) it(`throws the original streamed query error`, async () => { @@ -1218,13 +1213,13 @@ describe(`useLiveSuspenseQuery`, () => { const { result, rerender } = renderHook( ({ minAge }) => - useLiveSuspenseQuery( - (q) => + useLiveSuspenseQuery({ + query: (q) => q .from({ persons: collection }) .where(({ persons }) => gt(persons.age, minAge)), - [minAge], - ), + queryKey: [minAge], + }), { wrapper: SuspenseWrapper, initialProps: { minAge: 20 }, diff --git a/packages/react-db/tests/usePacedMutations.test.tsx b/packages/react-db/tests/usePacedMutations.test.tsx index cbd058c18a..b8b8dc6554 100644 --- a/packages/react-db/tests/usePacedMutations.test.tsx +++ b/packages/react-db/tests/usePacedMutations.test.tsx @@ -123,7 +123,7 @@ describe(`usePacedMutations React hook`, () => { tx = result.current({ id: 1, value: 1 }) }) - await tx!.isPersisted.promise + await tx!.when(`settled`) expect(mutationFn).toHaveBeenCalledTimes(1) expect(tx!.state).toBe(`completed`) }) diff --git a/packages/react-native-db-sqlite-persistence/e2e/mobile-persisted-collection-conformance-suite.ts b/packages/react-native-db-sqlite-persistence/e2e/mobile-persisted-collection-conformance-suite.ts index a6dc32bcee..8080c4f9f4 100644 --- a/packages/react-native-db-sqlite-persistence/e2e/mobile-persisted-collection-conformance-suite.ts +++ b/packages/react-native-db-sqlite-persistence/e2e/mobile-persisted-collection-conformance-suite.ts @@ -80,15 +80,13 @@ function createPersistedCollection( } type PersistedTransactionHandle = { - isPersisted: { - promise: Promise - } + when: (state: 'settled') => Promise } async function waitForPersisted( transaction: PersistedTransactionHandle, ): Promise { - await transaction.isPersisted.promise + await transaction.when(`settled`) } async function seedCollection( diff --git a/packages/react-native-db-sqlite-persistence/tests/react-native-persistence.test.ts b/packages/react-native-db-sqlite-persistence/tests/react-native-persistence.test.ts index aad0c6fe4d..6f6650f4c3 100644 --- a/packages/react-native-db-sqlite-persistence/tests/react-native-persistence.test.ts +++ b/packages/react-native-db-sqlite-persistence/tests/react-native-persistence.test.ts @@ -383,7 +383,7 @@ it(`resumes persisted sync after simulated background/foreground transitions`, a title: `Before background`, score: 1, }) - await initialInsert.isPersisted.promise + await initialInsert.when(`settled`) expect(collection.get(`1`)?.title).toBe(`Before background`) await collection.cleanup() @@ -395,7 +395,7 @@ it(`resumes persisted sync after simulated background/foreground transitions`, a title: `Post resume write`, score: 2, }) - await postResumeInsert.isPersisted.promise + await postResumeInsert.when(`settled`) expect(collection.get(`2`)?.title).toBe(`Post resume write`) const persistedRows = await persistence.adapter.loadSubset( @@ -440,7 +440,7 @@ it(`shared api resumes persisted sync in expo-style lifecycle`, async () => { title: `Before background`, score: 1, }) - await initialInsert.isPersisted.promise + await initialInsert.when(`settled`) expect(collection.get(`1`)?.title).toBe(`Before background`) await collection.cleanup() @@ -452,7 +452,7 @@ it(`shared api resumes persisted sync in expo-style lifecycle`, async () => { title: `Post resume write`, score: 2, }) - await postResumeInsert.isPersisted.promise + await postResumeInsert.when(`settled`) expect(collection.get(`2`)?.title).toBe(`Post resume write`) const persistedRows = await persistence.adapter.loadSubset( diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index a5140b2031..b5c0ef2d8e 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -297,12 +297,10 @@ export function rxdbCollectionOptions( onInsert: async (params) => { debug(`insert`, params) const newItems = params.transaction.mutations.map((m) => m.modified) - return rxCollection.bulkUpsert(newItems).then((result) => { - if (result.error.length > 0) { - throw rxStorageWriteErrorToRxError(ensureNotFalsy(result.error[0])) - } - return result.success - }) + const result = await rxCollection.bulkUpsert(newItems) + if (result.error.length > 0) { + throw rxStorageWriteErrorToRxError(ensureNotFalsy(result.error[0])) + } }, onUpdate: async (params) => { debug(`update`, params) @@ -328,12 +326,10 @@ export function rxdbCollectionOptions( (m) => m.type === `delete`, ) const ids = mutations.map((mutation) => getKey(mutation.original)) - return rxCollection.bulkRemove(ids).then((result) => { - if (result.error.length > 0) { - throw result.error - } - return result.success - }) + const result = await rxCollection.bulkRemove(ids) + if (result.error.length > 0) { + throw result.error + } }, } return withCollectionConfigFactory(collectionConfig, () => diff --git a/packages/rxdb-db-collection/tests/rxdb.test.ts b/packages/rxdb-db-collection/tests/rxdb.test.ts index 70607c3e22..b574643952 100644 --- a/packages/rxdb-db-collection/tests/rxdb.test.ts +++ b/packages/rxdb-db-collection/tests/rxdb.test.ts @@ -194,7 +194,7 @@ describe(`RxDB Integration`, () => { expect(collection.get(`buffered`)).toBeUndefined() persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) await ready expect(collection.get(`server`)).toEqual( @@ -207,7 +207,7 @@ describe(`RxDB Integration`, () => { } finally { releaseInitialQuery.resolve() persistence.resolve() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) query.mockRestore() await collection.cleanup() await db.remove() @@ -355,7 +355,7 @@ describe(`RxDB Integration`, () => { // inserts const tx = collection.insert({ id: `3`, name: `inserted` }) - await tx.isPersisted.promise + await tx.when(`settled`) let doc = await rxCollection.findOne(`3`).exec(true) expect(doc.name).toEqual(`inserted`) @@ -554,7 +554,7 @@ describe(`RxDB Integration`, () => { // Both old invalid inputs reach the actual provider through persistence. const invalid = { id: `3`, name: `invalid`, foo: `bar` } const insert = collection.insert(invalid) - const inserted = observe(insert.isPersisted.promise) + const inserted = observe(insert.when(`settled`)) expect(collection.has(`3`)).toBe(true) const insertOutcome = await inserted expect(insertOutcome.status).toBe(`rejected`) @@ -568,7 +568,7 @@ describe(`RxDB Integration`, () => { const update = collection.update(`2`, (draft) => { Object.assign(draft, { name: `invalid`, foo: `bar` }) }) - const updated = observe(update.isPersisted.promise) + const updated = observe(update.when(`settled`)) expect(collection.get(`2`)?.name).toBe(`invalid`) const updateOutcome = await updated expect(updateOutcome.status).toBe(`rejected`) @@ -582,7 +582,7 @@ describe(`RxDB Integration`, () => { const next = collection.update(`2`, (draft) => { draft.name = `Valid` }) - expect(await observe(next.isPersisted.promise)).toEqual({ + expect(await observe(next.when(`settled`))).toEqual({ status: `fulfilled`, }) await expectRows([ diff --git a/packages/solid-db/skills/solid-db/SKILL.md b/packages/solid-db/skills/solid-db/SKILL.md index 0039c6cc89..9c4ce7f6c7 100644 --- a/packages/solid-db/skills/solid-db/SKILL.md +++ b/packages/solid-db/skills/solid-db/SKILL.md @@ -59,7 +59,6 @@ Returns an `Accessor>` (or `Accessor` with `findOne`) wi // Query function — call result as function for data const query = useLiveQuery((q) => q.from({ todo: todoCollection })) // query() → Array (data) — or T | undefined when using findOne() -// query.data → DEPRECATED — use query() instead. Migrate any existing code. // query.status → CollectionStatus // query.isLoading, query.isReady, query.isError // query.state → ReactiveMap @@ -232,7 +231,7 @@ Solid's reactivity tracks signal reads inside reactive contexts. Reading outside Source: docs/framework/solid/overview.md -### MEDIUM Using deprecated query.data instead of query() +### MEDIUM Reading query.data instead of calling query() Wrong: @@ -246,7 +245,7 @@ Correct: {(todo) =>

  • {todo.text}
  • } ``` -`query.data` is deprecated. Always use `query()` to access data. If you encounter existing code using `.data`, migrate it to the function call form. +`query.data` has been removed. Call `query()` to access data. See also: db-core/live-queries/SKILL.md — for query builder API. diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 21963fa948..37d9eabf8b 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -112,11 +112,6 @@ type InferConditionalResultType = export function useLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, ): Accessor> & { - /** - * @deprecated use function result instead - * query.data -> query() - */ - data: InferResultType state: ReactiveMap> collection: Collection, string | number, {}> status: CollectionStatus @@ -136,11 +131,6 @@ export function useLiveQuery( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, ): Accessor> & { - /** - * @deprecated use function result instead - * query.data -> query() - */ - data: InferConditionalResultType state: ReactiveMap> collection: Collection, string | number, {}> | null status: CollectionStatus | `disabled` @@ -198,11 +188,6 @@ export function useLiveQuery( export function useLiveQuery( config: Accessor>, ): Accessor> & { - /** - * @deprecated use function result instead - * query.data -> query() - */ - data: InferResultType state: ReactiveMap> collection: Collection, string | number, {}> status: CollectionStatus @@ -261,11 +246,6 @@ export function useLiveQuery< Collection & NonSingleResult >, ): Accessor> & { - /** - * @deprecated use function result instead - * query.data -> query() - */ - data: Array state: ReactiveMap collection: Collection status: CollectionStatus @@ -289,11 +269,6 @@ export function useLiveQuery< Collection & SingleResult >, ): Accessor & { - /** - * @deprecated use function result instead - * query.data -> query() - */ - data: TResult | undefined state: ReactiveMap collection: Collection & SingleResult status: CollectionStatus @@ -545,11 +520,6 @@ export function useLiveQuery( } Object.defineProperties(getData, { - data: { - get() { - return getData() - }, - }, status: { get() { return status() diff --git a/packages/solid-db/tests/conformance.test.tsx b/packages/solid-db/tests/conformance.test.tsx index bc631913ca..cee2074d68 100644 --- a/packages/solid-db/tests/conformance.test.tsx +++ b/packages/solid-db/tests/conformance.test.tsx @@ -137,7 +137,7 @@ function makeHandle( current(): ConformanceResult { const result = getResult() return expectResultSurface({ - data: result?.data, + data: result?.(), state: result?.state, status: result?.status, isReady: result?.isReady, @@ -289,36 +289,36 @@ describe(`owned native scope setup`, () => { runSuite(solidDriver) it(`preserves raw result types through the actual driver reader`, () => { - const raw: Record = { - data: [{ id: `a`, value: undefined }], + const rawData = [{ id: `a`, value: undefined }] + const raw = Object.assign(() => rawData, { state: new Map(), status: `disabled`, isReady: false, isError: false, isEnabled: false, - } + }) const handle = makeHandle( () => raw, () => {}, ) try { const healthy = handle.current() - expect(healthy.data).toBe(raw.data) + expect(healthy.data).toBe(rawData) expect(healthy.state).toBe(raw.state) expect(healthy.isReady).toBe(false) expect(healthy.isError).toBe(false) expect(healthy.isEnabled).toBe(false) - for (const key of [`status`, `isReady`, `isError`]) { + for (const key of [`status`, `isReady`, `isError`] as const) { const original = raw[key] - delete raw[key] + Reflect.deleteProperty(raw, key) expect(() => handle.current()).toThrowError(new RegExp(`raw ${key}`)) for (const invalid of key === `status` ? [undefined, 0] : [undefined, 0, ``]) { - raw[key] = invalid + Reflect.set(raw, key, invalid) expect(() => handle.current()).toThrowError(new RegExp(`raw ${key}`)) } - raw[key] = original + Reflect.set(raw, key, original) expect(handle.current()[key as keyof ConformanceResult]).toBe(original) } } finally { diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index 83a3c4c219..f5ac209c20 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -729,7 +729,7 @@ describe(`Query Collections`, () => { expect(rendered.result.state.has(`stale`)).toBe(false) expect(rendered.result.state.has(`1`)).toBe(true) - expect(rendered.result.data.map((p: any) => p.id)).toEqual([`1`]) + expect(rendered.result().map((p: any) => p.id)).toEqual([`1`]) expect(rendered.result.status).toBe(`ready`) dispose() @@ -961,7 +961,7 @@ describe(`Query Collections`, () => { expect(result.state.get(`["4","1"]`)).toBeUndefined() // Wait for the transaction to be committed - await transaction.isPersisted.promise + await transaction.when(`settled`) await waitFor(() => { // Wait for the permanent key to appear @@ -2000,7 +2000,7 @@ describe(`Query Collections`, () => { // When callback returns undefined, should return disabled state expect(rendered.result.state.size).toBe(0) - expect(rendered.result.data).toEqual([]) + expect(rendered.result()).toEqual([]) expect(rendered.result.collection).toBeNull() expect(rendered.result.status).toBe(`disabled`) expect(rendered.result.isLoading).toBe(false) @@ -2013,7 +2013,7 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(rendered.result.state.size).toBe(1) // Only John Smith (age 35) }) - expect(rendered.result.data).toHaveLength(1) + expect(rendered.result()).toHaveLength(1) expect(rendered.result.isReady).toBe(true) // Disable the query again @@ -2057,7 +2057,7 @@ describe(`Query Collections`, () => { // When callback returns null, should return disabled state expect(rendered.result.state.size).toBe(0) - expect(rendered.result.data).toEqual([]) + expect(rendered.result()).toEqual([]) expect(rendered.result.collection).toBeNull() expect(rendered.result.status).toBe(`disabled`) expect(rendered.result.isLoading).toBe(false) @@ -2070,7 +2070,7 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(rendered.result.state.size).toBe(1) }) - expect(rendered.result.data).toHaveLength(1) + expect(rendered.result()).toHaveLength(1) expect(rendered.result.isReady).toBe(true) dispose() @@ -2819,7 +2819,7 @@ describe(`Query Collections`, () => { // Resolve the pending sync to complete the transaction collection.utils.resolveSync() - await tx.isPersisted.promise + await tx.when(`settled`) // After sync, should still have 4 items (no duplicates) await waitFor(() => { @@ -2892,7 +2892,7 @@ describe(`Query Collections`, () => { // Resolve the pending sync collection.utils.resolveSync() - await tx.isPersisted.promise + await tx.when(`settled`) // After sync, should still have 3 items with the update persisted await waitFor(() => { @@ -2961,7 +2961,7 @@ describe(`Query Collections`, () => { // Resolve the pending sync collection.utils.resolveSync() - await tx.isPersisted.promise + await tx.when(`settled`) // After sync, should still have 2 items await waitFor(() => { @@ -3057,8 +3057,8 @@ describe(`Query Collections`, () => { // Resolve the shared sync promise — both onInsert and onUpdate complete collection.utils.resolveSync() - await txInsert.isPersisted.promise - await txUpdate.isPersisted.promise + await txInsert.when(`settled`) + await txUpdate.when(`settled`) // After sync complete, should have 4 items with correct data await waitFor(() => { diff --git a/packages/tauri-db-sqlite-persistence/e2e/shared/tauri-persisted-collection-harness.ts b/packages/tauri-db-sqlite-persistence/e2e/shared/tauri-persisted-collection-harness.ts index dc31bd5158..0a936ccf70 100644 --- a/packages/tauri-db-sqlite-persistence/e2e/shared/tauri-persisted-collection-harness.ts +++ b/packages/tauri-db-sqlite-persistence/e2e/shared/tauri-persisted-collection-harness.ts @@ -23,9 +23,7 @@ type PersistedCollectionHarness = { } type PersistedTransactionHandle = { - isPersisted: { - promise: Promise - } + when: (state: 'settled') => Promise } type PersistenceFactory = ( @@ -89,7 +87,7 @@ function createPersistedCollection( async function waitForPersisted( transaction: PersistedTransactionHandle, ): Promise { - await transaction.isPersisted.promise + await transaction.when(`settled`) } async function seedCollection( diff --git a/packages/tauri-db-sqlite-persistence/tests/tauri-persistence.test.ts b/packages/tauri-db-sqlite-persistence/tests/tauri-persistence.test.ts index a2a95b4a00..01fd6b7070 100644 --- a/packages/tauri-db-sqlite-persistence/tests/tauri-persistence.test.ts +++ b/packages/tauri-db-sqlite-persistence/tests/tauri-persistence.test.ts @@ -370,7 +370,7 @@ it(`resumes persisted sync after cleanup and restart`, async () => { title: `Before cleanup`, score: 1, }) - await initialInsert.isPersisted.promise + await initialInsert.when(`settled`) expect(collection.get(`1`)?.title).toBe(`Before cleanup`) await collection.cleanup() @@ -382,7 +382,7 @@ it(`resumes persisted sync after cleanup and restart`, async () => { title: `After restart`, score: 2, }) - await resumedInsert.isPersisted.promise + await resumedInsert.when(`settled`) expect(collection.get(`2`)?.title).toBe(`After restart`) const persistedRows = await persistence.adapter.loadSubset( diff --git a/packages/trailbase-db-collection/src/trailbase.ts b/packages/trailbase-db-collection/src/trailbase.ts index 27c3a9decb..d04cc0f908 100644 --- a/packages/trailbase-db-collection/src/trailbase.ts +++ b/packages/trailbase-db-collection/src/trailbase.ts @@ -519,9 +519,7 @@ export function trailBaseCollectionOptions< ...collectionConfig, sync, getKey, - onInsert: async ( - params: InsertMutationFnParams, - ): Promise> => { + onInsert: async (params: InsertMutationFnParams) => { const ids = await config.recordApi.createBulk( params.transaction.mutations.map((tx) => { const { type, modified } = tx @@ -536,8 +534,6 @@ export function trailBaseCollectionOptions< // we have to ensure that the new record was properly added to the local // DB by the subscription. await awaitIds(ids.map((id) => String(id))) - - return ids }, onUpdate: async (params: UpdateMutationFnParams) => { const ids: Array = await Promise.all( diff --git a/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts b/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts index b2ae460952..f2a2b44b7b 100644 --- a/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts +++ b/packages/trailbase-db-collection/tests/lifecycle-oracle.property.test.ts @@ -1092,9 +1092,11 @@ async function checkHeldPage(plan: HeldPlan) { let mutation: ReturnType | undefined if (plan.held) { mutation = observe( - collection.update(2, (draft) => { - draft.data = `local` - }).isPersisted.promise, + collection + .update(2, (draft) => { + draft.data = `local` + }) + .when(`settled`), ) await turn() } diff --git a/packages/trailbase-db-collection/tests/trailbase.test.ts b/packages/trailbase-db-collection/tests/trailbase.test.ts index 63448f0988..67c260f07e 100644 --- a/packages/trailbase-db-collection/tests/trailbase.test.ts +++ b/packages/trailbase-db-collection/tests/trailbase.test.ts @@ -398,7 +398,7 @@ describe(`TrailBase Integration`, () => { expect(collection.get(1)).toBeUndefined() resolvePersistence() - await transaction.isPersisted.promise + await transaction.when(`settled`) await preload expect(collection.status).toBe(`ready`) @@ -412,7 +412,7 @@ describe(`TrailBase Integration`, () => { } finally { resolveList({ records: [] }) resolvePersistence() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await collection.cleanup() await Promise.allSettled([preload]) } @@ -556,7 +556,7 @@ describe(`TrailBase Integration`, () => { expect(collection.get(1)).toBeUndefined() abortController.abort() resolvePersistence() - await transaction.isPersisted.promise + await transaction.when(`settled`) if (load === true) { throw new Error(`Expected a pending applied receipt`) } @@ -567,7 +567,7 @@ describe(`TrailBase Integration`, () => { } finally { abortController.abort() resolvePersistence() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await collection.cleanup() } }) @@ -614,14 +614,14 @@ describe(`TrailBase Integration`, () => { expect(collection.get(1)).toBeUndefined() resolvePersistence() - await transaction.isPersisted.promise + await transaction.when(`settled`) if (load instanceof Promise) await load expect(collection.get(1)?.data).toBe(`remote`) expect(collection.get(257)?.data).toBe(`remote`) } finally { resolvePersistence() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await collection.cleanup() } }) @@ -882,12 +882,12 @@ describe(`TrailBase Integration`, () => { } const inserted = collection.insert(data) - outcomes.push(inserted.isPersisted.promise) + outcomes.push(inserted.when(`settled`)) expect(createBulkMock).toHaveBeenCalledOnce() expect(stripState(collection.state)).toEqual(new Map([[data.id, data]])) - await inserted.isPersisted.promise + await inserted.when(`settled`) const updatedData: Data = { ...data, @@ -904,14 +904,14 @@ describe(`TrailBase Integration`, () => { const updated = collection.update(data.id!, (old: Data) => { old.updated = updatedData.updated }) - outcomes.push(updated.isPersisted.promise) + outcomes.push(updated.when(`settled`)) expect(updateMock).toHaveBeenCalledOnce() expect(stripState(collection.state)).toEqual( new Map([[updatedData.id, updatedData]]), ) - await updated.isPersisted.promise + await updated.when(`settled`) const deleteMock = recordApi.delete.mockImplementation( async (_id: string | number) => { @@ -920,12 +920,12 @@ describe(`TrailBase Integration`, () => { ) const deleted = collection.delete(updatedData.id!) - outcomes.push(deleted.isPersisted.promise) + outcomes.push(deleted.when(`settled`)) expect(deleteMock).toHaveBeenCalledOnce() expect(stripState(collection.state)).toEqual(new Map([])) - await deleted.isPersisted.promise + await deleted.when(`settled`) } finally { await collection.cleanup() await Promise.allSettled(outcomes) @@ -1011,8 +1011,9 @@ describe(`TrailBase Integration`, () => { await collection.preload() expect(rows()).toEqual([{ id: 1, updated: 2000, data: `peer` }]) const first = observe( - collection.insert({ id: 2, updated: new Date(7000), data: `local` }) - .isPersisted.promise, + collection + .insert({ id: 2, updated: new Date(7000), data: `local` }) + .when(`settled`), ) await apiEntered.promise expect(first.result).toEqual({ status: `pending` }) @@ -1034,8 +1035,9 @@ describe(`TrailBase Integration`, () => { { id: 2, updated: 8000, data: `acknowledged` }, ]) const rejected = observe( - collection.insert({ id: 3, updated: new Date(9000), data: `rejected` }) - .isPersisted.promise, + collection + .insert({ id: 3, updated: new Date(9000), data: `rejected` }) + .when(`settled`), ) expect(rows()).toContainEqual({ id: 3, updated: 9000, data: `rejected` }) await rejected.outcome @@ -1045,8 +1047,9 @@ describe(`TrailBase Integration`, () => { { id: 2, updated: 8000, data: `acknowledged` }, ]) const next = observe( - collection.insert({ id: 4, updated: new Date(11000), data: `next` }) - .isPersisted.promise, + collection + .insert({ id: 4, updated: new Date(11000), data: `next` }) + .when(`settled`), ) await vi.waitFor(() => expect(recordApi.createBulk).toHaveBeenCalledTimes(3), @@ -1175,7 +1178,7 @@ describe(`TrailBase aborted subset loads`, () => { expect(settled, `load waits for its accepted page`).toBe(false) } release() - await transaction.isPersisted.promise + await transaction.when(`settled`) const outcome = await load.then( () => `resolved`, (error: { name?: string }) => error.name, @@ -1188,7 +1191,7 @@ describe(`TrailBase aborted subset loads`, () => { } finally { controller.abort() release() - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) await collection.cleanup() } }) diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index 58e43da6fd..2687d2f593 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -702,7 +702,7 @@ describe(`Query Collections`, () => { expect(state.value.get(`["4","1"]`)).toBeUndefined() // Wait for the transaction to be committed - await transaction.isPersisted.promise + await transaction.when(`settled`) await waitForVueUpdate() diff --git a/scripts/version-packages.mjs b/scripts/version-packages.mjs new file mode 100644 index 0000000000..bee2ca05a6 --- /dev/null +++ b/scripts/version-packages.mjs @@ -0,0 +1,128 @@ +import { execFileSync } from 'node:child_process' +import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const changesetPath = join(root, '.changeset', 'first-rc.md') +const configPath = join(root, '.changeset', 'config.json') + +function run(command, args) { + execFileSync(command, args, { cwd: root, stdio: 'inherit' }) +} + +function packageManifests() { + return readdirSync(join(root, 'packages')).flatMap((directory) => { + const path = join(root, 'packages', directory, 'package.json') + return existsSync(path) + ? [{ path, value: JSON.parse(readFileSync(path, 'utf8')) }] + : [] + }) +} + +function privateWorkspaceNames(directory) { + const names = [] + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const path = join(directory, entry.name) + if ( + entry.isDirectory() && + !['node_modules', 'dist', 'build', '.next', '.output'].includes( + entry.name, + ) + ) { + names.push(...privateWorkspaceNames(path)) + } else if (entry.isFile() && entry.name === 'package.json') { + const manifest = JSON.parse(readFileSync(path, 'utf8')) + if (manifest.private && typeof manifest.name === 'string') { + names.push(manifest.name) + } + } + } + return names +} + +function seedFirstCandidate() { + const changeset = readFileSync(changesetPath, 'utf8') + const releaseEntries = [ + ...changeset.matchAll(/^'([^']+)': (major|minor|patch)$/gm), + ] + const releaseNames = new Set(releaseEntries.map((match) => match[1])) + const manifests = packageManifests() + const preOne = manifests.filter( + ({ value }) => !value.private && /^0\./.test(value.version), + ) + const alreadyOne = manifests + .filter(({ value }) => !value.private && /^1\./.test(value.version)) + .map(({ value }) => value.name) + const expectedNames = new Set(preOne.map(({ value }) => value.name)) + if ( + releaseNames.size !== expectedNames.size || + releaseEntries.length !== releaseNames.size || + releaseEntries.some((match) => match[2] !== 'major') || + [...releaseNames].some((name) => !expectedNames.has(name)) + ) { + throw new Error( + 'The first RC changeset must list every public pre-1.0 package exactly once.', + ) + } + + const originalConfig = readFileSync(configPath, 'utf8') + const config = JSON.parse(originalConfig) + const ignored = new Set([ + ...config.ignore, + ...alreadyOne, + ...privateWorkspaceNames(join(root, 'packages')), + ...privateWorkspaceNames(join(root, 'examples')), + ]) + config.ignore = [...ignored] + config.privatePackages = { version: false, tag: false } + const removedDependencies = [] + + for (const { path, value } of preOne) { + // Changesets increments the existing prerelease counter to rc.1. + value.version = '1.0.0-rc.0' + // Changesets requires a package that depends on an ignored package to be + // ignored too. Hide those edges only while calculating this RC release. + for (const field of [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'optionalDependencies', + ]) { + for (const [index, [name, range]] of Object.entries( + value[field] ?? {}, + ).entries()) { + if (ignored.has(name)) { + removedDependencies.push({ path, field, name, range, index }) + delete value[field][name] + } + } + } + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) + } + writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`) + + try { + run('pnpm', ['exec', 'changeset', 'pre', 'enter', 'rc']) + run('pnpm', ['exec', 'changeset', 'version']) + } finally { + writeFileSync(configPath, originalConfig) + for (const { path, field, name, range, index } of removedDependencies) { + const value = JSON.parse(readFileSync(path, 'utf8')) + const entries = Object.entries(value[field]) + entries.splice(index, 0, [name, range]) + value[field] = Object.fromEntries(entries) + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) + } + } +} + +if ( + existsSync(changesetPath) && + !existsSync(join(root, '.changeset', 'pre.json')) +) + seedFirstCandidate() +else run('pnpm', ['exec', 'changeset', 'version']) + +run('pnpm', ['exec', 'tsx', 'scripts/update-example-deps.ts']) +run('pnpm', ['install', '--no-frozen-lockfile']) From 72d1faeed0bde419bca980d8165b0f9d85a83118 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 15 Oct 2026 08:50:34 -0700 Subject: [PATCH 02/13] feat: automate 1.0 RC patch and stable releases --- .github/workflows/release.yml | 2 + CONTRIBUTING.md | 17 +++ package.json | 7 +- pnpm-lock.yaml | 12 ++ scripts/publish-packages.mjs | 101 ++++++++++++++++ scripts/publish-packages.test.mjs | 97 +++++++++++++++ scripts/version-packages.mjs | 120 +++++++++++++++++-- scripts/version-packages.test.mjs | 193 ++++++++++++++++++++++++++++++ 8 files changed, 541 insertions(+), 8 deletions(-) create mode 100644 scripts/publish-packages.mjs create mode 100644 scripts/publish-packages.test.mjs create mode 100644 scripts/version-packages.test.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 729fca27c7..825a0f5f10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,8 @@ jobs: uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 - name: Check lint run: pnpm run lint:check + - name: Check release versioning + run: pnpm run test:release test-db: name: Test (${{ matrix.group }}) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc6f65a9e7..f64439a39c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,6 +135,14 @@ If you want to run an example without installing dependencies for the whole repo This repo uses [Changesets](https://github.com/changesets/changesets) to automate releases. If your PR should release a new package version (patch, minor, or major), please run `pnpm changeset` and commit the generated file. If your PR affects docs, examples, styles, etc., you probably don't need to generate a changeset. +During the 1.0 release candidate period, use the same Changeset types. For a +package at `1.0.0-rc.1`, a patch produces `1.0.0-rc.1.1`, and another patch +produces `1.0.0-rc.1.2`. A minor or major change advances the candidate number: +`1.0.0-rc.1.2` becomes `1.0.0-rc.2`. Packages already on stable 1.x versions, +including `@tanstack/query-db-collection` and +`@tanstack/offline-transactions`, continue to use normal semantic versioning +and publish under npm's `latest` tag. + ## Pull requests Maintainers merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface. @@ -144,3 +152,12 @@ Use an appropriate commit type. Be especially careful with breaking changes. ## Releases For each new commit added to `main`, a GitHub Workflow is triggered which runs the [Changesets Action](https://github.com/changesets/action). This generates a preview PR showing the impact of all changesets. When this PR is merged, the package will be published to npm. + +The first 1.0 candidate Changeset enters Changesets prerelease mode through +`pnpm run changeset:version`. Leave `.changeset/pre.json` in prerelease mode +for subsequent candidate releases; the normal release workflow handles each +patch or minor Changeset. When the candidates are ready for stable 1.0, run +`pnpm changeset pre exit` and commit the resulting `.changeset/pre.json` change. +The next Version Packages PR promotes the candidate packages to `1.0.0` and +keeps already stable packages at their current versions. Merge that PR to +publish 1.0. diff --git a/package.json b/package.json index 8c52401ca3..fbaf3b334a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build": "pnpm --filter \"./packages/**\" build", "build:minified": "pnpm --filter \"./packages/**\" build:minified", "changeset": "changeset", - "changeset:publish": "changeset publish", + "changeset:publish": "node scripts/publish-packages.mjs", "changeset:version": "node scripts/version-packages.mjs", "format": "prettier --experimental-cli --ignore-unknown '**/*' --write", "lint": "pnpm --filter \"./packages/**\" lint", @@ -25,6 +25,7 @@ "test:oracles": "pnpm --filter @tanstack/db test:oracles && pnpm --filter @tanstack/query-db-collection test:oracles && pnpm --filter @tanstack/db test:indexed-db:oracles", "test:minified-db": "node scripts/test-minified-db.mjs", "test:production-errors": "node scripts/generate-error-docs.mjs --check && node scripts/test-production-errors.mjs", + "test:release": "node --test scripts/version-packages.test.mjs scripts/publish-packages.test.mjs", "check:mangle": "node scripts/mangle-private-members.mjs", "typecheck:tests": "pnpm --filter @tanstack/db-ivm --filter @tanstack/offline-transactions typecheck", "test:docs": "node scripts/verify-links.ts", @@ -32,9 +33,13 @@ "generate-docs": "node scripts/generate-docs.ts" }, "devDependencies": { + "@changesets/apply-release-plan": "7.0.14", "@changesets/cli": "^2.29.8", + "@changesets/config": "3.1.2", + "@changesets/get-release-plan": "4.0.14", "@eslint/js": "^9.39.2", "@fast-check/vitest": "^0.2.0", + "@manypkg/get-packages": "1.1.3", "@svitejs/changesets-changelog-github-compact": "^1.2.0", "@tanstack/eslint-config": "0.3.4", "@tanstack/typedoc-config": "0.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2af9121d8..159257d992 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,15 +26,27 @@ importers: .: devDependencies: + '@changesets/apply-release-plan': + specifier: 7.0.14 + version: 7.0.14 '@changesets/cli': specifier: ^2.29.8 version: 2.29.8(@types/node@25.2.2) + '@changesets/config': + specifier: 3.1.2 + version: 3.1.2 + '@changesets/get-release-plan': + specifier: 4.0.14 + version: 4.0.14 '@eslint/js': specifier: ^9.39.2 version: 9.39.4 '@fast-check/vitest': specifier: ^0.2.0 version: 0.2.4(vitest@3.2.4) + '@manypkg/get-packages': + specifier: 1.1.3 + version: 1.1.3 '@svitejs/changesets-changelog-github-compact': specifier: ^1.2.0 version: 1.2.0 diff --git a/scripts/publish-packages.mjs b/scripts/publish-packages.mjs new file mode 100644 index 0000000000..b304364591 --- /dev/null +++ b/scripts/publish-packages.mjs @@ -0,0 +1,101 @@ +import { execFileSync } from 'node:child_process' +import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { getPackages } from '@manypkg/get-packages' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const candidateVersion = /^1\.0\.0-rc\.\d+(?:\.\d+)?$/ +const stableVersion = /^\d+\.\d+\.\d+$/ + +function publishChangesets(rootDirectory) { + execFileSync('pnpm', ['exec', 'changeset', 'publish'], { + cwd: rootDirectory, + stdio: 'inherit', + }) +} + +async function publishWithHiddenPackages( + rootDirectory, + hiddenPackages, + hidePreState, + publishCommand, +) { + const originals = hiddenPackages.map(({ dir }) => { + const path = join(dir, 'package.json') + return { path, contents: readFileSync(path) } + }) + const preStatePath = join(rootDirectory, '.changeset', 'pre.json') + const preState = hidePreState ? readFileSync(preStatePath) : undefined + + try { + for (const { path, contents } of originals) { + const manifest = JSON.parse(contents.toString()) + writeFileSync( + path, + `${JSON.stringify({ ...manifest, private: true }, null, 2)}\n`, + ) + } + if (hidePreState) unlinkSync(preStatePath) + await publishCommand(rootDirectory) + } finally { + for (const { path, contents } of originals) writeFileSync(path, contents) + if (preState !== undefined) writeFileSync(preStatePath, preState) + } +} + +/** + * Changesets uses the `rc` npm tag for every package while pre mode is active. + * Publish candidates first, then publish stable packages with pre mode hidden. + */ +export async function publishDuringRc( + rootDirectory = root, + publishCommand = publishChangesets, +) { + const { packages } = await getPackages(rootDirectory) + const publicPackages = packages.filter( + ({ packageJson }) => !packageJson.private, + ) + const candidates = publicPackages.filter(({ packageJson }) => + candidateVersion.test(packageJson.version), + ) + const stable = publicPackages.filter(({ packageJson }) => + stableVersion.test(packageJson.version), + ) + if ( + candidates.length === 0 || + candidates.length + stable.length !== publicPackages.length + ) { + throw new Error( + 'RC publishing found a public package with an unexpected version.', + ) + } + + await publishWithHiddenPackages(rootDirectory, stable, false, publishCommand) + if (stable.length > 0) { + await publishWithHiddenPackages( + rootDirectory, + candidates, + true, + publishCommand, + ) + } +} + +async function main() { + const preStatePath = join(root, '.changeset', 'pre.json') + const preState = existsSync(preStatePath) + ? JSON.parse(readFileSync(preStatePath, 'utf8')) + : undefined + if (preState?.mode === 'pre' && preState.tag === 'rc') { + await publishDuringRc() + } else { + publishChangesets(root) + } +} + +if ( + process.argv[1] && + resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) + await main() diff --git a/scripts/publish-packages.test.mjs b/scripts/publish-packages.test.mjs new file mode 100644 index 0000000000..cbd0cac7cc --- /dev/null +++ b/scripts/publish-packages.test.mjs @@ -0,0 +1,97 @@ +import assert from 'node:assert/strict' +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { test } from 'node:test' +import { publishDuringRc } from './publish-packages.mjs' + +function createWorkspace() { + const root = mkdtempSync(join(tmpdir(), 'tanstack-db-publish-test-')) + mkdirSync(join(root, '.changeset')) + writeFileSync( + join(root, 'package.json'), + JSON.stringify({ name: 'release-fixture', private: true }), + ) + writeFileSync( + join(root, 'pnpm-workspace.yaml'), + 'packages:\n - packages/*\n', + ) + writeFileSync( + join(root, '.changeset', 'pre.json'), + JSON.stringify({ mode: 'pre', tag: 'rc' }), + ) + const manifests = [ + { name: '@tanstack/db', version: '1.0.0-rc.2' }, + { name: '@tanstack/query-db-collection', version: '1.4.6' }, + { name: '@tanstack/offline-transactions', version: '1.1.3' }, + { name: 'private-fixture', version: '0.1.0', private: true }, + ] + for (const [index, manifest] of manifests.entries()) { + const directory = join(root, 'packages', String(index)) + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'package.json'), JSON.stringify(manifest)) + } + return root +} + +function packageState(root) { + return [0, 1, 2, 3].map((index) => + JSON.parse( + readFileSync(join(root, 'packages', String(index), 'package.json')), + ), + ) +} + +test('RC publishing gives candidate and stable packages their own npm tags', async (t) => { + const root = createWorkspace() + t.after(() => rmSync(root, { recursive: true, force: true })) + const before = packageState(root) + const phases = [] + + await publishDuringRc(root, () => { + phases.push({ + publicNames: packageState(root) + .filter((pkg) => !pkg.private) + .map((pkg) => pkg.name), + preMode: existsSync(join(root, '.changeset', 'pre.json')), + }) + }) + + assert.deepEqual(phases, [ + { publicNames: ['@tanstack/db'], preMode: true }, + { + publicNames: [ + '@tanstack/query-db-collection', + '@tanstack/offline-transactions', + ], + preMode: false, + }, + ]) + assert.deepEqual(packageState(root), before) + assert.equal(existsSync(join(root, '.changeset', 'pre.json')), true) +}) + +test('failed stable publishing restores package manifests and prerelease state', async (t) => { + const root = createWorkspace() + t.after(() => rmSync(root, { recursive: true, force: true })) + const before = packageState(root) + let phase = 0 + + await assert.rejects( + publishDuringRc(root, () => { + phase++ + if (phase === 2) throw new Error('publish failed') + }), + /publish failed/, + ) + assert.equal(phase, 2) + assert.deepEqual(packageState(root), before) + assert.equal(existsSync(join(root, '.changeset', 'pre.json')), true) +}) diff --git a/scripts/version-packages.mjs b/scripts/version-packages.mjs index bee2ca05a6..c93bbcdcb3 100644 --- a/scripts/version-packages.mjs +++ b/scripts/version-packages.mjs @@ -2,6 +2,10 @@ import { execFileSync } from 'node:child_process' import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' +import applyReleasePlan from '@changesets/apply-release-plan' +import { read as readChangesetsConfig } from '@changesets/config' +import getReleasePlan from '@changesets/get-release-plan' +import { getPackages } from '@manypkg/get-packages' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const changesetPath = join(root, '.changeset', 'first-rc.md') @@ -117,12 +121,114 @@ function seedFirstCandidate() { } } +/** + * Changesets tracks which changesets were consumed and determines dependent + * releases. Its prerelease counter does not distinguish patch from minor, so + * replace only the planned version for each package during the RC window. + */ +export function versionForRcRelease(release) { + if (release.type === 'none') return release.oldVersion + + const candidate = /^1\.0\.0-rc\.(\d+)(?:\.(\d+))?$/.exec(release.oldVersion) + if (candidate) { + const generation = Number(candidate[1]) + if (release.type === 'patch') { + const patch = Number(candidate[2] ?? 0) + 1 + return `1.0.0-rc.${generation}.${patch}` + } + if (release.type === 'minor' || release.type === 'major') { + return `1.0.0-rc.${generation + 1}` + } + } + + if (/^\d+\.\d+\.\d+$/.test(release.oldVersion)) { + const stableVersion = release.newVersion.replace(/-rc\.0$/, '') + if (/^\d+\.\d+\.\d+$/.test(stableVersion)) return stableVersion + } + + throw new Error( + `Cannot calculate an RC version for ${release.name}: ${release.oldVersion} (${release.type}).`, + ) +} + +async function rcReleasePlan(rootDirectory) { + const packages = await getPackages(rootDirectory) + const config = await readChangesetsConfig(rootDirectory, packages) + const releaseConfig = { + ...config, + privatePackages: { version: false, tag: false }, + } + const plan = await getReleasePlan(rootDirectory, undefined, releaseConfig) + return { packages, releaseConfig, plan } +} + +export async function versionDuringRc(rootDirectory = root) { + const { packages, releaseConfig, plan } = await rcReleasePlan(rootDirectory) + if (plan.releases.length === 0) return + + for (const release of plan.releases) { + release.newVersion = versionForRcRelease(release) + } + await applyReleasePlan( + plan, + packages, + releaseConfig, + undefined, + rootDirectory, + ) +} + +export async function versionOnRcExit(rootDirectory = root) { + const { packages, releaseConfig, plan } = await rcReleasePlan(rootDirectory) + const privatePackages = new Set( + packages.packages + .filter(({ packageJson }) => packageJson.private) + .map(({ packageJson }) => packageJson.name), + ) + plan.releases = plan.releases.filter((release) => { + if (privatePackages.has(release.name)) return false + if (/^1\.0\.0-rc\.\d+(?:\.\d+)?$/.test(release.oldVersion)) { + release.newVersion = '1.0.0' + return true + } + if (/^\d+\.\d+\.\d+$/.test(release.oldVersion)) return false + throw new Error( + `Cannot exit RC mode for ${release.name}: ${release.oldVersion}.`, + ) + }) + if (plan.releases.length === 0) { + throw new Error('No 1.0 RC packages found while exiting prerelease mode.') + } + await applyReleasePlan( + plan, + packages, + releaseConfig, + undefined, + rootDirectory, + ) +} + +async function main() { + const preStatePath = join(root, '.changeset', 'pre.json') + const preState = existsSync(preStatePath) + ? JSON.parse(readFileSync(preStatePath, 'utf8')) + : undefined + if (existsSync(changesetPath) && preState === undefined) { + seedFirstCandidate() + } else if (preState?.mode === 'pre' && preState.tag === 'rc') { + await versionDuringRc() + } else if (preState?.mode === 'exit' && preState.tag === 'rc') { + await versionOnRcExit() + } else { + run('pnpm', ['exec', 'changeset', 'version']) + } + + run('pnpm', ['exec', 'tsx', 'scripts/update-example-deps.ts']) + run('pnpm', ['install', '--no-frozen-lockfile']) +} + if ( - existsSync(changesetPath) && - !existsSync(join(root, '.changeset', 'pre.json')) + process.argv[1] && + resolve(process.argv[1]) === fileURLToPath(import.meta.url) ) - seedFirstCandidate() -else run('pnpm', ['exec', 'changeset', 'version']) - -run('pnpm', ['exec', 'tsx', 'scripts/update-example-deps.ts']) -run('pnpm', ['install', '--no-frozen-lockfile']) + await main() diff --git a/scripts/version-packages.test.mjs b/scripts/version-packages.test.mjs new file mode 100644 index 0000000000..0e963df985 --- /dev/null +++ b/scripts/version-packages.test.mjs @@ -0,0 +1,193 @@ +import assert from 'node:assert/strict' +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { test } from 'node:test' +import { + versionDuringRc, + versionForRcRelease, + versionOnRcExit, +} from './version-packages.mjs' + +function writeJson(path, value) { + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) +} + +function packageVersion(root, directory) { + return JSON.parse( + readFileSync(join(root, 'packages', directory, 'package.json'), 'utf8'), + ).version +} + +function addChangeset(root, id, releases) { + const frontmatter = Object.entries(releases) + .map(([name, type]) => `'${name}': ${type}`) + .join('\n') + writeFileSync( + join(root, '.changeset', `${id}.md`), + `---\n${frontmatter}\n---\n\nFixture change.\n`, + ) +} + +function createRcWorkspace() { + const root = mkdtempSync(join(tmpdir(), 'tanstack-db-rc-test-')) + mkdirSync(join(root, '.changeset')) + mkdirSync(join(root, 'examples', 'app'), { recursive: true }) + mkdirSync(join(root, 'examples', 'no-version'), { recursive: true }) + writeJson(join(root, 'package.json'), { + name: 'release-fixture', + private: true, + packageManager: 'pnpm@11.1.0', + }) + writeFileSync( + join(root, 'pnpm-workspace.yaml'), + 'packages:\n - packages/*\n - examples/*\n', + ) + writeJson(join(root, '.changeset', 'config.json'), { + changelog: false, + commit: false, + access: 'public', + baseBranch: 'main', + updateInternalDependencies: 'patch', + fixed: [], + linked: [], + ignore: [], + }) + + const packages = { + db: { name: '@tanstack/db', version: '1.0.0-rc.1' }, + 'react-db': { + name: '@tanstack/react-db', + version: '1.0.0-rc.1', + dependencies: { '@tanstack/db': 'workspace:*' }, + }, + 'query-db-collection': { + name: '@tanstack/query-db-collection', + version: '1.4.5', + }, + 'offline-transactions': { + name: '@tanstack/offline-transactions', + version: '1.1.2', + }, + } + for (const [directory, manifest] of Object.entries(packages)) { + mkdirSync(join(root, 'packages', directory), { recursive: true }) + writeJson(join(root, 'packages', directory, 'package.json'), manifest) + } + writeJson(join(root, 'examples', 'app', 'package.json'), { + name: 'release-fixture-app', + version: '0.1.0', + private: true, + dependencies: { '@tanstack/db': 'workspace:*' }, + }) + writeJson(join(root, 'examples', 'no-version', 'package.json'), { + name: 'release-fixture-no-version', + private: true, + }) + writeJson(join(root, '.changeset', 'pre.json'), { + mode: 'pre', + tag: 'rc', + initialVersions: { + 'release-fixture-app': '0.1.0', + '@tanstack/db': '1.0.0-rc.0', + '@tanstack/react-db': '1.0.0-rc.0', + '@tanstack/query-db-collection': '1.4.5', + '@tanstack/offline-transactions': '1.1.2', + }, + changesets: ['first-rc'], + }) + addChangeset(root, 'first-rc', { + '@tanstack/db': 'major', + '@tanstack/react-db': 'major', + }) + return root +} + +test('RC version numbers follow the Changeset release type', () => { + const version = (oldVersion, type, newVersion = '1.0.0-rc.2') => + versionForRcRelease({ + name: '@tanstack/db', + oldVersion, + newVersion, + type, + }) + + assert.equal(version('1.0.0-rc.1', 'patch'), '1.0.0-rc.1.1') + assert.equal(version('1.0.0-rc.1.1', 'patch'), '1.0.0-rc.1.2') + assert.equal(version('1.0.0-rc.1.2', 'minor'), '1.0.0-rc.2') + assert.equal(version('1.0.0-rc.2', 'patch'), '1.0.0-rc.2.1') + assert.equal(version('1.0.0-rc.2.1', 'major'), '1.0.0-rc.3') + assert.equal(version('1.4.5', 'patch', '1.4.6-rc.0'), '1.4.6') + assert.equal(version('1.1.2', 'minor', '1.2.0-rc.0'), '1.2.0') + assert.throws( + () => version('0.9.0', 'patch'), + /Cannot calculate an RC version/, + ) +}) + +test('RC versioning consumes Changesets while preserving stable and private packages', async (t) => { + const root = createRcWorkspace() + t.after(() => rmSync(root, { recursive: true, force: true })) + + addChangeset(root, 'patch-one', { + '@tanstack/db': 'patch', + '@tanstack/query-db-collection': 'patch', + '@tanstack/offline-transactions': 'patch', + }) + await versionDuringRc(root) + assert.equal(packageVersion(root, 'db'), '1.0.0-rc.1.1') + assert.equal(packageVersion(root, 'react-db'), '1.0.0-rc.1.1') + assert.equal(packageVersion(root, 'query-db-collection'), '1.4.6') + assert.equal(packageVersion(root, 'offline-transactions'), '1.1.3') + assert.equal( + JSON.parse(readFileSync(join(root, 'examples', 'app', 'package.json'))) + .version, + '0.1.0', + ) + + await versionDuringRc(root) + assert.equal(packageVersion(root, 'db'), '1.0.0-rc.1.1') + assert.deepEqual( + JSON.parse(readFileSync(join(root, '.changeset', 'pre.json'))).changesets, + ['first-rc', 'patch-one'], + ) + + addChangeset(root, 'patch-two', { '@tanstack/db': 'patch' }) + await versionDuringRc(root) + assert.equal(packageVersion(root, 'db'), '1.0.0-rc.1.2') + assert.equal(packageVersion(root, 'react-db'), '1.0.0-rc.1.2') + + addChangeset(root, 'patch-three', { '@tanstack/db': 'patch' }) + addChangeset(root, 'minor-one', { '@tanstack/db': 'minor' }) + await versionDuringRc(root) + assert.equal(packageVersion(root, 'db'), '1.0.0-rc.2') + + addChangeset(root, 'patch-four', { '@tanstack/db': 'patch' }) + await versionDuringRc(root) + assert.equal(packageVersion(root, 'db'), '1.0.0-rc.2.1') + + const preStatePath = join(root, '.changeset', 'pre.json') + const preState = JSON.parse(readFileSync(preStatePath, 'utf8')) + writeJson(preStatePath, { ...preState, mode: 'exit' }) + await versionOnRcExit(root) + assert.equal(packageVersion(root, 'db'), '1.0.0') + assert.equal(packageVersion(root, 'react-db'), '1.0.0') + assert.equal(packageVersion(root, 'query-db-collection'), '1.4.6') + assert.equal(packageVersion(root, 'offline-transactions'), '1.1.3') + assert.equal( + JSON.parse( + readFileSync(join(root, 'examples', 'no-version', 'package.json')), + ).version, + undefined, + ) + assert.equal(existsSync(preStatePath), false) + assert.equal(existsSync(join(root, '.changeset', 'first-rc.md')), false) + assert.equal(existsSync(join(root, '.changeset', 'patch-one.md')), false) +}) From 169c1518a545e9abb599071208002b021b096008 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:59:53 +0000 Subject: [PATCH 03/13] ci: apply automated fixes --- examples/react/projects/src/routes/_authenticated/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/react/projects/src/routes/_authenticated/index.tsx b/examples/react/projects/src/routes/_authenticated/index.tsx index cfca6623e4..097a84c00f 100644 --- a/examples/react/projects/src/routes/_authenticated/index.tsx +++ b/examples/react/projects/src/routes/_authenticated/index.tsx @@ -1,6 +1,6 @@ -import { createFileRoute, redirect } from "@tanstack/react-router" -import { projectCollection, todoCollection } from "@/lib/collections" -import { authClient } from "@/lib/auth-client" +import { createFileRoute, redirect } from '@tanstack/react-router' +import { projectCollection, todoCollection } from '@/lib/collections' +import { authClient } from '@/lib/auth-client' export const Route = createFileRoute(`/_authenticated/`)({ component: () => null, From af3b9d7401ac16a49e13f97d5ed85c90a29180ba Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 15 Oct 2026 09:04:06 -0700 Subject: [PATCH 04/13] fix: migrate remaining RC API consumers --- examples/react/todo/src/lib/collections.ts | 20 +++++++------- examples/solid/todo/src/lib/collections.ts | 20 +++++++------- .../skills/db-core/collection-setup/SKILL.md | 6 ++--- .../references/electric-adapter.md | 26 +++++++++---------- .../db-core/mutations-optimistic/SKILL.md | 6 ++--- scripts/bench/incremental-update.ts | 2 +- scripts/test-minified-db.mjs | 3 +-- 7 files changed, 41 insertions(+), 42 deletions(-) diff --git a/examples/react/todo/src/lib/collections.ts b/examples/react/todo/src/lib/collections.ts index e8154c5a2b..212ffbde39 100644 --- a/examples/react/todo/src/lib/collections.ts +++ b/examples/react/todo/src/lib/collections.ts @@ -29,7 +29,7 @@ export const electricTodoCollection = createCollection( }, getKey: (item) => item.id, schema: selectTodoSchema, - onInsert: async ({ transaction }) => { + onInsert: async ({ transaction, collection }) => { const { id: _id, created_at: _f, @@ -37,9 +37,9 @@ export const electricTodoCollection = createCollection( ...modified } = transaction.mutations[0].modified const response = await api.todos.create(modified) - return { txid: response.txid } + await collection.utils.awaitTxId(response.txid) }, - onUpdate: async ({ transaction }) => { + onUpdate: async ({ transaction, collection }) => { const txids = await Promise.all( transaction.mutations.map(async (mutation) => { const { original, changes } = mutation @@ -50,9 +50,9 @@ export const electricTodoCollection = createCollection( return response.txid }), ) - return { txid: txids } + await Promise.all(txids.map((txid) => collection.utils.awaitTxId(txid))) }, - onDelete: async ({ transaction }) => { + onDelete: async ({ transaction, collection }) => { const txids = await Promise.all( transaction.mutations.map(async (mutation) => { const { original } = mutation @@ -63,7 +63,7 @@ export const electricTodoCollection = createCollection( return response.txid }), ) - return { txid: txids } + await Promise.all(txids.map((txid) => collection.utils.awaitTxId(txid))) }, }), ) @@ -160,12 +160,12 @@ export const electricConfigCollection = createCollection( }, getKey: (item) => item.id, schema: selectConfigSchema, - onInsert: async ({ transaction }) => { + onInsert: async ({ transaction, collection }) => { const modified = transaction.mutations[0].modified const response = await api.config.create(modified) - return { txid: response.txid } + await collection.utils.awaitTxId(response.txid) }, - onUpdate: async ({ transaction }) => { + onUpdate: async ({ transaction, collection }) => { const txids = await Promise.all( transaction.mutations.map(async (mutation) => { const { original, changes } = mutation @@ -176,7 +176,7 @@ export const electricConfigCollection = createCollection( return response.txid }), ) - return { txid: txids } + await Promise.all(txids.map((txid) => collection.utils.awaitTxId(txid))) }, }), ) diff --git a/examples/solid/todo/src/lib/collections.ts b/examples/solid/todo/src/lib/collections.ts index e252809e55..2cebd2cff1 100644 --- a/examples/solid/todo/src/lib/collections.ts +++ b/examples/solid/todo/src/lib/collections.ts @@ -29,7 +29,7 @@ export const electricTodoCollection = createCollection( }, getKey: (item) => item.id, schema: selectTodoSchema, - onInsert: async ({ transaction }) => { + onInsert: async ({ transaction, collection }) => { const { id: _id, created_at: _f, @@ -37,9 +37,9 @@ export const electricTodoCollection = createCollection( ...modified } = transaction.mutations[0].modified const response = await api.todos.create(modified) - return { txid: response.txid } + await collection.utils.awaitTxId(response.txid) }, - onUpdate: async ({ transaction }) => { + onUpdate: async ({ transaction, collection }) => { const txids = await Promise.all( transaction.mutations.map(async (mutation) => { const { original, changes } = mutation @@ -47,9 +47,9 @@ export const electricTodoCollection = createCollection( return response.txid }), ) - return { txid: txids } + await Promise.all(txids.map((txid) => collection.utils.awaitTxId(txid))) }, - onDelete: async ({ transaction }) => { + onDelete: async ({ transaction, collection }) => { const txids = await Promise.all( transaction.mutations.map(async (mutation) => { const { original } = mutation @@ -57,7 +57,7 @@ export const electricTodoCollection = createCollection( return response.txid }), ) - return { txid: txids } + await Promise.all(txids.map((txid) => collection.utils.awaitTxId(txid))) }, }), ) @@ -148,12 +148,12 @@ export const electricConfigCollection = createCollection( }, getKey: (item) => item.id, schema: selectConfigSchema, - onInsert: async ({ transaction }) => { + onInsert: async ({ transaction, collection }) => { const modified = transaction.mutations[0].modified const response = await api.config.create(modified) - return { txid: response.txid } + await collection.utils.awaitTxId(response.txid) }, - onUpdate: async ({ transaction }) => { + onUpdate: async ({ transaction, collection }) => { const txids = await Promise.all( transaction.mutations.map(async (mutation) => { const { original, changes } = mutation @@ -161,7 +161,7 @@ export const electricConfigCollection = createCollection( return response.txid }), ) - return { txid: txids } + await Promise.all(txids.map((txid) => collection.utils.awaitTxId(txid))) }, }), ) diff --git a/packages/db/skills/db-core/collection-setup/SKILL.md b/packages/db/skills/db-core/collection-setup/SKILL.md index 71cbe97b50..85d6cfd876 100644 --- a/packages/db/skills/db-core/collection-setup/SKILL.md +++ b/packages/db/skills/db-core/collection-setup/SKILL.md @@ -184,15 +184,15 @@ const todoCollection = createCollection( schema: todoSchema, shapeOptions: { url: '/api/electric/todos' }, getKey: (item) => item.id, - onInsert: async ({ transaction }) => { + onInsert: async ({ transaction, collection }) => { const res = await api.todos.create(transaction.mutations[0].modified) - return { txid: res.txid } + await collection.utils.awaitTxId(res.txid) }, }), ) ``` -The returned `txid` tells the collection to hold optimistic state until Electric streams back that transaction. See the [Electric adapter reference](references/electric-adapter.md) for the full dual-path pattern (schema + parser). +The handler waits for Electric to stream the `txid` before it completes, keeping optimistic state visible until then. See the [Electric adapter reference](references/electric-adapter.md) for the full dual-path pattern (schema + parser). ## Common Mistakes diff --git a/packages/db/skills/db-core/collection-setup/references/electric-adapter.md b/packages/db/skills/db-core/collection-setup/references/electric-adapter.md index 5886791c35..0fc1a55ed3 100644 --- a/packages/db/skills/db-core/collection-setup/references/electric-adapter.md +++ b/packages/db/skills/db-core/collection-setup/references/electric-adapter.md @@ -31,9 +31,9 @@ const collection = createCollection( | `schema` | (none) | StandardSchema validator | | `shapeOptions.params` | (none) | Additional shape params (e.g. `{ table: 'todos' }`) | | `syncMode` | `eager` | `eager`, `on-demand`, or `progressive` | -| `onInsert` | (none) | Persistence handler; should return `{ txid }` | -| `onUpdate` | (none) | Persistence handler; should return `{ txid }` | -| `onDelete` | (none) | Persistence handler; should return `{ txid }` | +| `onInsert` | (none) | Persistence handler; await sync before completing | +| `onUpdate` | (none) | Persistence handler; await sync before completing | +| `onDelete` | (none) | Persistence handler; await sync before completing | ## Sync Modes @@ -49,14 +49,14 @@ forever. ## Three Sync Strategies -### 1. Txid Return (Recommended) +### 1. Await Txid (Recommended) -Handler returns `{ txid }`. Client waits for that txid in the Electric stream. +The handler waits for the backend's txid in the Electric stream. ```typescript -onInsert: async ({ transaction }) => { +onInsert: async ({ transaction, collection }) => { const response = await api.todos.create(transaction.mutations[0].modified) - return { txid: response.txid } + await collection.utils.awaitTxId(response.txid) }, ``` @@ -230,21 +230,21 @@ const todosCollection = createCollection( timestamptz: (value: string) => new Date(value), // sync path }, }, - onInsert: async ({ transaction }) => { + onInsert: async ({ transaction, collection }) => { const response = await api.todos.create(transaction.mutations[0].modified) - return { txid: response.txid } + await collection.utils.awaitTxId(response.txid) }, - onUpdate: async ({ transaction }) => { + onUpdate: async ({ transaction, collection }) => { const { original, changes } = transaction.mutations[0] const response = await api.todos.update({ where: { id: original.id }, data: changes, }) - return { txid: response.txid } + await collection.utils.awaitTxId(response.txid) }, - onDelete: async ({ transaction }) => { + onDelete: async ({ transaction, collection }) => { const response = await api.todos.delete(transaction.mutations[0].key) - return { txid: response.txid } + await collection.utils.awaitTxId(response.txid) }, }), ) diff --git a/packages/db/skills/db-core/mutations-optimistic/SKILL.md b/packages/db/skills/db-core/mutations-optimistic/SKILL.md index 0c56c693c9..79312c1a36 100644 --- a/packages/db/skills/db-core/mutations-optimistic/SKILL.md +++ b/packages/db/skills/db-core/mutations-optimistic/SKILL.md @@ -258,17 +258,17 @@ const todoCollection = createCollection( ) ``` -For ElectricCollection, return `{ txid }` instead of refetching: +For ElectricCollection, wait for each txid in the Electric stream: ```ts -onUpdate: async ({ transaction }) => { +onUpdate: async ({ transaction, collection }) => { const txids = await Promise.all( transaction.mutations.map(async (m) => { const res = await api.todos.update(m.original.id, m.changes) return res.txid }), ) - return { txid: txids } + await Promise.all(txids.map((txid) => collection.utils.awaitTxId(txid))) } ``` diff --git a/scripts/bench/incremental-update.ts b/scripts/bench/incremental-update.ts index c9362604c0..7b8b7270bd 100644 --- a/scripts/bench/incremental-update.ts +++ b/scripts/bench/incremental-update.ts @@ -675,7 +675,7 @@ function writeOptimistic(write: () => void) { autoCommit: false, mutationFn: async () => {}, }) - transaction.isPersisted.promise.catch(() => undefined) + void transaction.when(`settled`).catch(() => undefined) transaction.mutate(write) return transaction } diff --git a/scripts/test-minified-db.mjs b/scripts/test-minified-db.mjs index 03cbaad5b8..9e30a9e143 100644 --- a/scripts/test-minified-db.mjs +++ b/scripts/test-minified-db.mjs @@ -92,7 +92,7 @@ const errorGroups = { DebounceCallDroppedError: `DebounceCallDroppedError`, OnMutateMustBeSynchronousError: `OnMutateMustBeSynchronousError`, QueryBuilderError: `QueryBuilderError OnlyOneSourceAllowedError SubQueryMustHaveFromClauseError InvalidSourceError InvalidSourceTypeError JoinConditionMustBeEqualityError QueryMustHaveFromClauseError InvalidWhereExpressionError`, - QueryCompilationError: `QueryCompilationError DistinctRequiresSelectError FnSelectWithGroupByError UnsupportedFnSelectResultError UnsupportedRootScalarSelectError HavingRequiresGroupByError LimitOffsetRequireOrderByError CollectionInputNotFoundError DuplicateAliasInSubqueryError UnsupportedFromTypeError UnknownExpressionTypeError EmptyReferencePathError UnknownFunctionError JoinCollectionNotFoundError MissingAliasInputsError SetWindowRequiresOrderByError SetWindowFindOneLimitError`, + QueryCompilationError: `QueryCompilationError DistinctRequiresSelectError FnSelectWithGroupByError UnsupportedFnSelectResultError UnsupportedRootScalarSelectError HavingRequiresGroupByError LimitOffsetRequireOrderByError CollectionInputNotFoundError UnsupportedFromTypeError UnknownExpressionTypeError EmptyReferencePathError UnknownFunctionError JoinCollectionNotFoundError MissingAliasInputsError SetWindowRequiresOrderByError SetWindowFindOneLimitError`, UnsafeAliasPathError: `UnsafeAliasPathError`, JoinError: `JoinError UnsupportedJoinTypeError InvalidJoinConditionSameSourceError InvalidJoinConditionSourceMismatchError InvalidJoinConditionLeftSourceError InvalidJoinConditionRightSourceError InvalidJoinCondition UnsupportedJoinSourceTypeError`, GroupByError: `GroupByError NonAggregateExpressionNotInGroupByError UnsupportedAggregateFunctionError AggregateFunctionNotInSelectError UnknownHavingExpressionTypeError`, @@ -206,7 +206,6 @@ async function checkCollectionsAndQueries(db) { rows.map((row) => { const selected = { ...row } delete selected.$hasPendingWrites - delete selected.$synced delete selected.$origin delete selected.$key delete selected.$collectionId From 13eb8497b0e457cc1e50e19e9762ed7a7b489628 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 15 Oct 2026 09:08:21 -0700 Subject: [PATCH 05/13] fix: update todo example RC usage --- examples/react/todo/src/routes/electric.tsx | 2 +- examples/solid/todo/src/routes/trailbase.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/react/todo/src/routes/electric.tsx b/examples/react/todo/src/routes/electric.tsx index 16da41b9ff..398f968f3f 100644 --- a/examples/react/todo/src/routes/electric.tsx +++ b/examples/react/todo/src/routes/electric.tsx @@ -65,7 +65,7 @@ function ElectricPage() { // Wait for all txids to sync back to the collection await Promise.all( - txids.map((txid) => electricConfigCollection.utils.awaitTxid(txid)), + txids.map((txid) => electricConfigCollection.utils.awaitTxId(txid)), ) } diff --git a/examples/solid/todo/src/routes/trailbase.tsx b/examples/solid/todo/src/routes/trailbase.tsx index c9f450ea47..dcc7f6e985 100644 --- a/examples/solid/todo/src/routes/trailbase.tsx +++ b/examples/solid/todo/src/routes/trailbase.tsx @@ -20,21 +20,21 @@ export const Route = createFileRoute(`/trailbase`)({ }) function TrailBasePage() { - // Get data using live queries with Electric collections - const { data: todos } = useLiveQuery((q) => + // Get data using live queries with TrailBase collections + const todos = useLiveQuery((q) => q .from({ todo: trailBaseTodoCollection }) .orderBy(({ todo }) => todo.created_at, `asc`), ) - const { data: configData } = useLiveQuery((q) => + const configData = useLiveQuery((q) => q.from({ config: trailBaseConfigCollection }), ) return ( Date: Thu, 15 Oct 2026 09:17:45 -0700 Subject: [PATCH 06/13] fix: preserve pending stable changesets on RC exit --- CONTRIBUTING.md | 6 +++--- scripts/version-packages.mjs | 19 ++++++++++++++++++- scripts/version-packages.test.mjs | 6 +++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f64439a39c..87c193afc0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -158,6 +158,6 @@ The first 1.0 candidate Changeset enters Changesets prerelease mode through for subsequent candidate releases; the normal release workflow handles each patch or minor Changeset. When the candidates are ready for stable 1.0, run `pnpm changeset pre exit` and commit the resulting `.changeset/pre.json` change. -The next Version Packages PR promotes the candidate packages to `1.0.0` and -keeps already stable packages at their current versions. Merge that PR to -publish 1.0. +The next Version Packages PR promotes the candidate packages to `1.0.0`. +Already stable packages keep their versions unless a pending Changeset +explicitly releases them. Merge that PR to publish 1.0. diff --git a/scripts/version-packages.mjs b/scripts/version-packages.mjs index c93bbcdcb3..67aa2ac93c 100644 --- a/scripts/version-packages.mjs +++ b/scripts/version-packages.mjs @@ -180,18 +180,35 @@ export async function versionDuringRc(rootDirectory = root) { export async function versionOnRcExit(rootDirectory = root) { const { packages, releaseConfig, plan } = await rcReleasePlan(rootDirectory) + // The release plan includes newly pending Changesets in its pre-state. + // The saved pre-state identifies which Changesets were already versioned. + const preState = JSON.parse( + readFileSync(join(rootDirectory, '.changeset', 'pre.json'), 'utf8'), + ) const privatePackages = new Set( packages.packages .filter(({ packageJson }) => packageJson.private) .map(({ packageJson }) => packageJson.name), ) + const consumedChangesets = new Set(preState.changesets) + const pendingStableNames = new Set( + plan.changesets + .filter(({ id }) => !consumedChangesets.has(id)) + .flatMap(({ releases }) => + releases.filter(({ type }) => type !== 'none').map(({ name }) => name), + ), + ) plan.releases = plan.releases.filter((release) => { if (privatePackages.has(release.name)) return false if (/^1\.0\.0-rc\.\d+(?:\.\d+)?$/.test(release.oldVersion)) { release.newVersion = '1.0.0' return true } - if (/^\d+\.\d+\.\d+$/.test(release.oldVersion)) return false + if (/^\d+\.\d+\.\d+$/.test(release.oldVersion)) { + if (!pendingStableNames.has(release.name)) return false + release.newVersion = versionForRcRelease(release) + return true + } throw new Error( `Cannot exit RC mode for ${release.name}: ${release.oldVersion}.`, ) diff --git a/scripts/version-packages.test.mjs b/scripts/version-packages.test.mjs index 0e963df985..b773bc8c74 100644 --- a/scripts/version-packages.test.mjs +++ b/scripts/version-packages.test.mjs @@ -173,13 +173,16 @@ test('RC versioning consumes Changesets while preserving stable and private pack await versionDuringRc(root) assert.equal(packageVersion(root, 'db'), '1.0.0-rc.2.1') + addChangeset(root, 'stable-on-exit', { + '@tanstack/query-db-collection': 'patch', + }) const preStatePath = join(root, '.changeset', 'pre.json') const preState = JSON.parse(readFileSync(preStatePath, 'utf8')) writeJson(preStatePath, { ...preState, mode: 'exit' }) await versionOnRcExit(root) assert.equal(packageVersion(root, 'db'), '1.0.0') assert.equal(packageVersion(root, 'react-db'), '1.0.0') - assert.equal(packageVersion(root, 'query-db-collection'), '1.4.6') + assert.equal(packageVersion(root, 'query-db-collection'), '1.4.7') assert.equal(packageVersion(root, 'offline-transactions'), '1.1.3') assert.equal( JSON.parse( @@ -190,4 +193,5 @@ test('RC versioning consumes Changesets while preserving stable and private pack assert.equal(existsSync(preStatePath), false) assert.equal(existsSync(join(root, '.changeset', 'first-rc.md')), false) assert.equal(existsSync(join(root, '.changeset', 'patch-one.md')), false) + assert.equal(existsSync(join(root, '.changeset', 'stable-on-exit.md')), false) }) From abd54552bf065aca663471d762693cc32f1001e8 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 15 Oct 2026 09:19:46 -0700 Subject: [PATCH 07/13] ci: check release versioning on PRs --- .github/workflows/pr.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b5eb9dd20d..7fb419844f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -26,6 +26,8 @@ jobs: uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 - name: Check lint run: pnpm run lint:check + - name: Check release versioning + run: pnpm run test:release minified-db: name: Minified DB public API runs-on: ubuntu-latest From b83dab9e052938088974e934e948f07117ca7109 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 15 Oct 2026 10:09:51 -0700 Subject: [PATCH 08/13] docs: add 1.0 RC migration guide --- docs/config.json | 4 + docs/framework/solid/overview.md | 6 +- docs/guides/migrate-to-1-0-rc.md | 177 +++++++++++++++++++++++++++++++ docs/overview.md | 1 + 4 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 docs/guides/migrate-to-1-0-rc.md diff --git a/docs/config.json b/docs/config.json index 14ce831d6a..c737e21d73 100644 --- a/docs/config.json +++ b/docs/config.json @@ -26,6 +26,10 @@ { "label": "Guides", "children": [ + { + "label": "Migrate to the 1.0 RC", + "to": "guides/migrate-to-1-0-rc" + }, { "label": "Live Queries", "to": "guides/live-queries" diff --git a/docs/framework/solid/overview.md b/docs/framework/solid/overview.md index df184cb89a..378c213d23 100644 --- a/docs/framework/solid/overview.md +++ b/docs/framework/solid/overview.md @@ -19,7 +19,7 @@ For comprehensive documentation on writing queries (filtering, joins, aggregatio ### useLiveQuery -The `useLiveQuery` primitive creates a live query that automatically updates your component when data changes. It returns an object where `data` is a plain array and status fields (e.g. `isLoading()`, `status()`) are accessors: +The `useLiveQuery` primitive creates a live query that automatically updates your component when data changes. Call the returned accessor to read its data; status fields such as `isLoading` and `status` are reactive properties: ```tsx import { useLiveQuery } from '@tanstack/solid-db' @@ -34,7 +34,7 @@ function TodoList() { ) return ( - Loading...}> + Loading...}>
      {(todo) =>
    • {todo.text}
    • } @@ -45,7 +45,7 @@ function TodoList() { } ``` -**Note:** `query()` reads the array, while status fields such as `isLoading` and `status` are properties. +**Note:** `query()` reads the array; `query.isLoading` and `query.status` read reactive status properties. ### Reactive Queries with Signals diff --git a/docs/guides/migrate-to-1-0-rc.md b/docs/guides/migrate-to-1-0-rc.md new file mode 100644 index 0000000000..02d525bf0e --- /dev/null +++ b/docs/guides/migrate-to-1-0-rc.md @@ -0,0 +1,177 @@ +--- +title: Migrating to the 1.0 RC +id: migrate-to-1-0-rc +--- + +This guide covers the API changes in `1.0.0-rc.1` for apps upgrading from a +pre-1.0 TanStack DB release. For the earlier `DbClient` and SSR migration, see +the [SSR and Hydration guide](./ssr.md#migration-guide). + +## Update your packages + +Update the pre-1.0 TanStack DB packages your app uses together. Once the RC is +published, install them from npm's `rc` tag. For example: + +```sh +npm install @tanstack/react-db@rc @tanstack/electric-db-collection@rc +``` + +The first candidate is `1.0.0-rc.1`. `@tanstack/query-db-collection` and +`@tanstack/offline-transactions` are already stable 1.x packages; keep them on +their 1.x releases rather than changing them to an RC version. + +## Replace `$synced` with `$hasPendingWrites` + +The deprecated `$synced` virtual row property has been removed. Its replacement +has the opposite boolean meaning: + +| Before | After | +| --- | --- | +| `row.$synced` | `!row.$hasPendingWrites` | +| `!row.$synced` | `row.$hasPendingWrites` | +| `eq(row.$synced, true)` | `eq(row.$hasPendingWrites, false)` | +| `eq(row.$synced, false)` | `eq(row.$hasPendingWrites, true)` | + +`$hasPendingWrites` reports whether a pending local optimistic mutation affects +the row. A `false` value does not, by itself, prove that the server confirmed +the write. See [Virtual properties](./live-queries.md#virtual-properties) for +the other row metadata fields. + +If you create virtual rows yourself, include `$hasPendingWrites`: +`hasVirtualProps(row)` now requires it. If you call +`enrichRowWithVirtualProps`, its fourth argument computes pending writes rather +than the inverse `$synced` value. + +### Keep virtual field names for row metadata + +The fields `$hasPendingWrites`, `$origin`, `$key`, and `$collectionId` are +reserved on published Collection rows. A selected field with one of those names +cannot replace the metadata at a live-query row root. Whole-row outputs retain +their source row's metadata; ordinary projections receive the live-query +Collection's metadata. Rename application data fields that collide with these +names before selecting them as row roots. + +## Wait for transaction settlement + +Replace `transaction.isPersisted.promise` with `transaction.when('settled')`: + +```ts +const transaction = todoCollection.update(todoId, (draft) => { + draft.completed = true +}) + +// Before: await transaction.isPersisted.promise +await transaction.when('settled') +``` + +`when('settled')` resolves with the transaction after its mutation handler +completes successfully and rejects when the transaction fails or rolls back. +Transaction settlement confirms a backend write only if the handler waits for +that confirmation before returning. See the [Mutation Lifecycle](./mutations.md#mutation-lifecycle) +for the full boundary. + +## Await synchronization inside mutation handlers + +The pre-1.0 Collection adapters no longer use values returned by +`onInsert`, `onUpdate`, or `onDelete` to coordinate synchronization. Their +handlers now return `Promise`. Await the work that must finish before +the transaction settles: + +```ts +onUpdate: async ({ transaction }) => { + await api.todos.update(transaction.mutations[0].changes) + // Await any required backend confirmation or read-back here. +} +``` + +For Electric Collection, replace a returned `{ txid }` matching strategy with +`collection.utils.awaitTxId(txid)` inside the handler: + +```ts +// Before +onUpdate: async ({ transaction }) => { + const { txid } = await api.todos.update(transaction.mutations[0].changes) + return { txid, timeout: 10_000 } +} + +// After +onUpdate: async ({ transaction, collection }) => { + const { txid } = await api.todos.update(transaction.mutations[0].changes) + await collection.utils.awaitTxId(txid, 10_000) +} +``` + +For several transaction IDs, await all of them with `Promise.all`. The +[Electric Collection guide](../collections/electric-collection.md#persistence-handlers--synchronization) +also covers custom matching when your API cannot return transaction IDs. + +**Query Collection remains on stable 1.x.** It still refetches automatically +after a handler returns and still accepts `{ refetch: false }` to skip that +refetch. If your handler calls `collection.utils.refetch()` itself, return +`{ refetch: false }` to avoid a second refetch. See +[Controlling Refetch Behavior](../collections/query-collection.md#controlling-refetch-behavior). + +For TypeScript code that uses the handler type aliases directly, +`InsertMutationFn`, `UpdateMutationFn`, and `DeleteMutationFn` no longer take a +return-value type parameter. `BaseCollectionConfig` no longer takes its old +`TReturn` type parameter. The Electric `MatchingStrategy` type was removed with +the returned matching strategy. + +## Update React live-query hooks + +Dependency arrays were removed from `useLiveQuery`, `useLiveSuspenseQuery`, and +`useLiveInfiniteQuery`. For structured queries, omit the array: the hooks +derive identity from the query, including captured values used in structured +expressions. + +```tsx +// Before +useLiveQuery( + (q) => q.from({ todos: todoCollection }) + .where(({ todos }) => eq(todos.status, status)), + [status], +) + +// After +useLiveQuery({ + query: (q) => q.from({ todos: todoCollection }) + .where(({ todos }) => eq(todos.status, status)), +}) +``` + +Opaque functional query clauses, such as `.fn.where`, need an explicit, +serializable `queryKey` that includes their captured values: + +```tsx +useLiveQuery({ + queryKey: [todoCollection.id, 'status', status], + query: (q) => q.from({ todos: todoCollection }) + .fn.where(({ todos }) => todos.status === status), +}) +``` + +An unhashable query without `queryKey` now throws. Put the key in the config +object for `useLiveQuery` or `useLiveSuspenseQuery`; pass it in the second +argument's config for `useLiveInfiniteQuery(query, { pageSize, queryKey })`. +See [Query Identity](../framework/react/overview.md#query-identity) for details. + +## Read Solid query data through the accessor + +Solid `useLiveQuery` no longer exposes the deprecated `.data` alias. Call the +returned accessor wherever you read the result: + +```tsx +const todosQuery = useLiveQuery((q) => q.from({ todos: todoCollection })) + +// Before: ... +{(todo) =>
    • {todo.text}
    • }
      +``` + +Status fields remain reactive properties, such as `todosQuery.isLoading`. See the +[Solid adapter guide](../framework/solid/overview.md). + +## Remove the obsolete alias error import + +`DuplicateAliasInSubqueryError` has been removed. Nested queries may reuse an +ancestor's alias; aliases must still be unique within one lexical scope. +Remove imports and catches for this obsolete error. diff --git a/docs/overview.md b/docs/overview.md index 2e1009d3c8..a362812542 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -32,6 +32,7 @@ It extends TanStack Query with collections, live queries and optimistic mutation ## Contents +- [Migrating to the 1.0 RC](./guides/migrate-to-1-0-rc.md) — update apps from pre-1.0 releases - [How it works](#how-it-works) — understand the TanStack DB development model and how the pieces fit together - [SSR and hydration](./guides/ssr.md) — use `DbClient` to transport explicit collection rows or live-query result snapshots - [SQLite persistence](./guides/sqlite-persistence.md) — retain Collection rows across restarts and save sync metadata for supported adapters From b76133989c3ff9abce7e0f9a23ba39520c628f85 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 15 Oct 2026 10:20:29 -0700 Subject: [PATCH 09/13] docs: clarify accidental 1.0 releases --- docs/guides/migrate-to-1-0-rc.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/migrate-to-1-0-rc.md b/docs/guides/migrate-to-1-0-rc.md index 02d525bf0e..a7417afa06 100644 --- a/docs/guides/migrate-to-1-0-rc.md +++ b/docs/guides/migrate-to-1-0-rc.md @@ -17,8 +17,8 @@ npm install @tanstack/react-db@rc @tanstack/electric-db-collection@rc ``` The first candidate is `1.0.0-rc.1`. `@tanstack/query-db-collection` and -`@tanstack/offline-transactions` are already stable 1.x packages; keep them on -their 1.x releases rather than changing them to an RC version. +`@tanstack/offline-transactions` were both accidentally released as 1.0 before +the RC. Keep them on their existing 1.x versions. ## Replace `$synced` with `$hasPendingWrites` From 244776f29e080d845555dc101b9eca72c3389882 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 15 Oct 2026 10:25:41 -0700 Subject: [PATCH 10/13] docs: cover recent breaking changes in RC migration guide --- docs/guides/migrate-to-1-0-rc.md | 118 ++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/docs/guides/migrate-to-1-0-rc.md b/docs/guides/migrate-to-1-0-rc.md index a7417afa06..d493de6daa 100644 --- a/docs/guides/migrate-to-1-0-rc.md +++ b/docs/guides/migrate-to-1-0-rc.md @@ -3,9 +3,10 @@ title: Migrating to the 1.0 RC id: migrate-to-1-0-rc --- -This guide covers the API changes in `1.0.0-rc.1` for apps upgrading from a -pre-1.0 TanStack DB release. For the earlier `DbClient` and SSR migration, see -the [SSR and Hydration guide](./ssr.md#migration-guide). +This guide covers the API changes in `1.0.0-rc.1` and the changes shipped since +`@tanstack/db@0.9.2` in September 2026 that may require changes to an app. For +the earlier `DbClient` and SSR migration, see the +[SSR and Hydration guide](./ssr.md#migration-guide). ## Update your packages @@ -175,3 +176,114 @@ Status fields remain reactive properties, such as `todosQuery.isLoading`. See th `DuplicateAliasInSubqueryError` has been removed. Nested queries may reuse an ancestor's alias; aliases must still be unique within one lexical scope. Remove imports and catches for this obsolete error. + +## Changes since `@tanstack/db@0.9.2` + +The following changes shipped before the RC. Check the sections that match the +packages and APIs your app uses when upgrading directly from an older release. + +### Await Query Collection direct writes + +`writeInsert`, `writeUpdate`, `writeDelete`, `writeUpsert`, and `writeBatch` now +return promises whose validation errors reject. Catch errors from an awaited +write, including `DuplicateKeySyncError` when `writeInsert` uses an existing key +in a persisted Collection: + +```ts +try { + await todosCollection.utils.writeInsert(todo) +} catch (error) { + // Handle the rejected write. +} +``` + +Direct writes no longer automatically refetch active on-demand Query subsets. +They update Collection and Query cache rows where the write can be represented. +Call `await todosCollection.utils.refetch()` when you need current server rows. +This change concerns the `write*` utilities; the handler auto-refetch behavior +described above still applies to Query Collection 1.x. Use TanStack Query Core +5.90.15 or later for post-write refetch replacement and error recovery. See +[Direct Writes](../collections/query-collection.md#direct-writes). + +### Import IndexedDB APIs from core + +The standalone `@tanstack/indexeddb-db-collection` package was removed. Import +`createIndexedDB` and `indexedDBCollectionOptions` from `@tanstack/db` and +remove the old package dependency. See the +[IndexedDB Collection guide](../collections/indexed-db-collection.md). + +### Update sync and persistence integrations + +- Remove the `immediate` argument from `begin({ immediate: true })` in sync + callbacks. Call `begin()` instead. Sync writes made while a mutation persists + now queue behind it; code must not rely on them appearing immediately. +- Custom SQLite `PersistenceAdapter` implementations must implement + `loadResumeSnapshot`. A custom sync wrapper must forward + `metadata.persistence` unchanged. When metadata is present, its `persistence` + field is required and is `null` when there is no persistence capability. +- Lower-level SQLite adapter data operations now take an explicit + `PersistedStorageTarget`, such as `eagerStorageTarget(collectionId)` or a + claim-bound `managedStorageTarget(storageCollectionId, claimId)`, instead of a + bare Collection ID. Ordinary Collection configuration does not need a target. +- Keep Electron persistence code in the main and renderer processes on matching + package versions; mixed IPC protocol versions fail closed. +- `openBrowserWASQLiteOPFSDatabase` now rejects if opening takes more than 30 + seconds. Set `timeoutMs: 0` only if the previous unlimited wait is required. +- If a custom React Native op-sqlite wrapper returns ambiguous bare arrays, set + `arrayResultMode` to `rows` or `statement-results`. These arrays now reject + without an explicit mode. + +### Review query windows, joins, and aggregates + +- An ordered `findOne` keeps its compiled limit of one, or zero after + `.limit(0).findOne()`. `setWindow` may move its offset but cannot change its + limit. An unordered `findOne` stays at offset zero. Move to an ordered, + limited multi-row query if the result needs a larger or movable window. + `setWindow` controls only the outer query's own window, not a window inside a + subquery or include. +- Joined row keys now use JSON arrays of source keys, with `null` for a missing + side. Code that constructs a key to call `collection.get()` must use the new + format. Reading the published row's `$key` avoids reconstructing it, + especially for non-finite numeric keys. +- A source field selected outside an aggregate or `groupBy` key now throws + `NonAggregateExpressionNotInGroupByError`; the same rule applies in `having`. + Aggregate the field or include it in `groupBy`. Nested includes inside a + grouped or aggregate select now throw as well. +- Clean up the owning live query rather than calling `cleanup()` on one of its + included child Collections. The child cleanup now rejects because several + parent rows can share that Collection. + +### Check stricter inputs and adapter requirements + +- A custom index `compareFn` or collation `compare` must return a number other + than `NaN`. Invalid comparator results now throw instead of silently + misordering an index. +- Browser development builds now detect two loaded copies of `@tanstack/db` + and throw `DuplicateDbInstanceError`. Deduplicate the package if the check + appears after upgrading. +- Pass fresh persisted or LocalStorage sync options to each direct + `createCollection` call. Reusing mutable options across Collections now + rejects. Reusable `DbClient` descriptors are unaffected. +- `Collection.update` keys must match the Collection's declared key type. + Narrow possibly undefined keys, and use the branded key type if the + Collection declares one. Reusable `Ref` helpers that require virtual row + metadata should set their third type parameter to `true`. +- Upgrade to PowerSync 2 before updating `@tanstack/powersync-db-collection`, + and to RxDB 17 before updating `@tanstack/rxdb-db-collection`. +- Vue live-query `data.value` is a snapshot. Read `data.value` again to get + later rows instead of keeping a previously read array. +- A default auto-committed offline transaction accepts only its first + `mutate()` call. Create a new offline transaction for each later write; + calling `mutate()` again on the committed transaction now throws + `TransactionNotPendingMutateError`. +- If you import `@tanstack/db-ivm` directly, update removed operator imports. + The old join helpers become `join(other, type)`; use + `orderByWithFractionalIndex` or `groupedOrderByWithFractionalIndex` in place + of the removed ordering helpers. See the + [0.2.0 release notes](https://github.com/TanStack/db/blob/main/packages/db-ivm/CHANGELOG.md#020) + for the full list. + +Production error messages now contain a short code and a link to the +[error reference](../errors.md). Code that compared the old message text should +use error classes, names, or fields instead. The development messages remain +descriptive. From b74f6192c3bc16ab7ba32bc656ef51d5e79a61ad Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 16 Oct 2026 13:55:03 -0700 Subject: [PATCH 11/13] fix: address first RC review findings --- .changeset/first-rc.md | 4 + docs/contributing/oracle-coverage.md | 14 +- docs/framework/react/overview.md | 6 + docs/guides/live-queries.md | 2 +- docs/guides/migrate-to-1-0-rc.md | 17 +- package.json | 2 + packages/db/src/live-query-options.ts | 18 +- packages/db/src/query/compiler/index.ts | 89 +++++-- packages/db/src/query/live/ARCHITECTURE.md | 5 + packages/db/tests/live-query-options.test.ts | 17 +- .../query/virtual-row-fields-oracle.test.ts | 248 +++++++++++++++++- .../electric-db-collection/src/electric.ts | 18 ++ .../tests/electric.test.ts | 23 ++ packages/offline-transactions/package.json | 4 +- packages/query-db-collection/package.json | 4 +- packages/query-db-collection/src/query.ts | 27 +- .../optimistic-ordered-refetch-oracle.test.ts | 2 +- .../query-db-collection/tests/query.test.ts | 36 ++- .../refetch-ordered-prefix-oracle.test.ts | 2 +- packages/react-db/src/live-query-internals.ts | 2 +- packages/react-db/src/useLiveInfiniteQuery.ts | 35 +-- packages/react-db/src/useLiveQuery.ts | 71 ++--- packages/react-db/src/useLiveSuspenseQuery.ts | 16 +- .../tests/source-id-reuse-oracle.test.tsx | 74 ++++++ packages/react-db/tests/useLiveQuery.test.tsx | 39 +++ pnpm-lock.yaml | 10 +- scripts/publish-packages.mjs | 4 +- scripts/publish-packages.test.mjs | 4 +- scripts/version-packages.mjs | 208 ++++++++++----- scripts/version-packages.test.mjs | 163 ++++++++++++ 30 files changed, 959 insertions(+), 205 deletions(-) diff --git a/.changeset/first-rc.md b/.changeset/first-rc.md index 059843eda9..2ca379b700 100644 --- a/.changeset/first-rc.md +++ b/.changeset/first-rc.md @@ -10,7 +10,9 @@ '@tanstack/electron-db-sqlite-persistence': major '@tanstack/expo-db-sqlite-persistence': major '@tanstack/node-db-sqlite-persistence': major +'@tanstack/offline-transactions': patch '@tanstack/powersync-db-collection': major +'@tanstack/query-db-collection': patch '@tanstack/react-db': major '@tanstack/react-native-db-sqlite-persistence': major '@tanstack/react-router-with-db': major @@ -24,3 +26,5 @@ Release the first 1.0 candidate. Remove deprecated pre-1.0 APIs and give source metadata precedence over colliding selected fields in live queries. +Move the two packages already on stable 1.x to a compatible peer dependency on +`@tanstack/db`, so they can share the app's DB instance during the RC. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 03a8bfaa38..342eab5ca5 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -124,10 +124,16 @@ functional root publication. It also holds an optimistic source update and checks that a root projection cannot replace the source's pending-write, origin, key, or Collection ID metadata with explicitly selected fields. Whole-row roots retain source metadata, while ordinary projections receive -metadata from the live-query Collection. Inline Effects and nested subquery -results retain their selected shapes; they are not Collection row publication -boundaries. These fixed controlled-Collection cases do not establish every -nested projection shape or framework receiving boundary. +metadata from the live-query Collection. Matched joined-row roots compare all +four fields with the selected joined source before, during, and after one +pending update; an extended spread, both orders of two source spreads, a +FROM-row selection, and an unmatched RIGHT join distinguish source choice. A +Collection-valued include rejects selected virtual metadata +that conflicts with its child source; its inline `toArray` sibling retains a +selected `$key`. Inline Effects and nested subquery results retain their +selected shapes; they are not Collection row publication boundaries. These +fixed controlled-Collection cases do not establish arbitrary joined +projections, every nested projection shape, or framework receiving boundaries. Three type-only owners check compile-time boundaries: `packages/db-sqlite-persistence-core/tests/persisted-options-type-oracle.test-d.ts` diff --git a/docs/framework/react/overview.md b/docs/framework/react/overview.md index 7a9e25fecb..8363c2f795 100644 --- a/docs/framework/react/overview.md +++ b/docs/framework/react/overview.md @@ -112,6 +112,12 @@ An explicit `queryKey` is the caller's identity contract. If its source object changes, change the key too, even when the source ID stays the same. Use a primitive scope token in the key; Collection objects cannot be hashed there. +Configs with `getKey` or `schema` work without a `queryKey` in client renders. +The hook identifies those opaque options by object reference, so keep a stable +function or schema reference across renders to avoid rebuilding the query. For +SSR streaming, provide a serializable `queryKey` that is the same on the server +and client; object references cannot have a shared identity across runtimes. + An unhashable query without `queryKey` throws. Include captured values from opaque logic in the key so changes to those values recreate the query. If deriving identity becomes expensive across renders, the hook warns once and diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index bcb4a3aa2c..1b899d68a3 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -34,7 +34,7 @@ Live query results include computed, read-only virtual properties on every row: - `$hasPendingWrites`: `true` while a pending local optimistic mutation affects the row; otherwise `false`. It is always `false` for local-only collections. It does not indicate server acknowledgement. - `$origin`: Collection attribution for the current row. Optimistic rows are `"local"`; source rows use key and timing, not a source client ID. A peer write can be `"local"`. A truncate does not consume an active mutation's attribution for an untouched key. See [VirtualOrigin](../reference/type-aliases/VirtualOrigin.md) for settlement, truncate, and overlap rules. Local-only Collections always use `"local"`. - `$key`: the row key for the result. -- `$collectionId`: the source collection ID. +- `$collectionId`: the selected source Collection's ID for a whole-row `.select` or a `.select` projection that spreads a source row, including a joined row. With multiple source spreads, the last spread supplies the metadata. An ordinary projection without selected virtual fields receives the live-query Collection's ID. These props can be used in `where`, `select`, and `orderBy` clauses. They are added to query outputs automatically and should not be persisted back to storage. diff --git a/docs/guides/migrate-to-1-0-rc.md b/docs/guides/migrate-to-1-0-rc.md index d493de6daa..618ddf9f85 100644 --- a/docs/guides/migrate-to-1-0-rc.md +++ b/docs/guides/migrate-to-1-0-rc.md @@ -19,7 +19,9 @@ npm install @tanstack/react-db@rc @tanstack/electric-db-collection@rc The first candidate is `1.0.0-rc.1`. `@tanstack/query-db-collection` and `@tanstack/offline-transactions` were both accidentally released as 1.0 before -the RC. Keep them on their existing 1.x versions. +the RC. Keep them on 1.x and update to their latest 1.x patch releases. Those +patches use the app's `@tanstack/db` as a peer dependency, so the RC and stable +packages share one DB instance. ## Replace `$synced` with `$hasPendingWrites` @@ -39,18 +41,17 @@ the write. See [Virtual properties](./live-queries.md#virtual-properties) for the other row metadata fields. If you create virtual rows yourself, include `$hasPendingWrites`: -`hasVirtualProps(row)` now requires it. If you call -`enrichRowWithVirtualProps`, its fourth argument computes pending writes rather -than the inverse `$synced` value. +`hasVirtualProps(row)` now requires it. ### Keep virtual field names for row metadata The fields `$hasPendingWrites`, `$origin`, `$key`, and `$collectionId` are reserved on published Collection rows. A selected field with one of those names -cannot replace the metadata at a live-query row root. Whole-row outputs retain -their source row's metadata; ordinary projections receive the live-query -Collection's metadata. Rename application data fields that collide with these -names before selecting them as row roots. +cannot author conflicting metadata at a live-query row root. Whole-row outputs, +including a selected joined row, retain that source row's metadata. Ordinary +projections without selected virtual fields receive the live-query Collection's +metadata. Rename application data fields that collide with these names before +selecting them as row roots. ## Wait for transaction settlement diff --git a/package.json b/package.json index fbaf3b334a..240078f7f8 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,11 @@ }, "devDependencies": { "@changesets/apply-release-plan": "7.0.14", + "@changesets/assemble-release-plan": "6.0.9", "@changesets/cli": "^2.29.8", "@changesets/config": "3.1.2", "@changesets/get-release-plan": "4.0.14", + "@changesets/read": "0.6.6", "@eslint/js": "^9.39.2", "@fast-check/vitest": "^0.2.0", "@manypkg/get-packages": "1.1.3", diff --git a/packages/db/src/live-query-options.ts b/packages/db/src/live-query-options.ts index 25abc0d25c..5fb2730065 100644 --- a/packages/db/src/live-query-options.ts +++ b/packages/db/src/live-query-options.ts @@ -10,7 +10,10 @@ import { getStableQueryBuilderHash, getStableValueHash, } from './query/ir-stable-identity.js' -import { getStringCollationIdentity } from './query/runtime-reference-identity.js' +import { + getRuntimeReferenceIdentity, + getStringCollationIdentity, +} from './query/runtime-reference-identity.js' import { codedMessage, devBuild } from './error-message.js' import type { Collection, CollectionImpl } from './collection/index.js' import type { CollectionOptionsIdentity } from './collection-options.js' @@ -174,11 +177,20 @@ export function getPreparedLiveQueryIdentity(value: unknown): unknown { } if (value && typeof value === `object` && `query` in value) { const config = value as LiveQueryCollectionConfig + const schema = (value as { schema?: object }).schema return [ `config`, getPreparedLiveQueryIdentity(config.query), - [`getKey`, config.getKey], - [`schema`, config.schema], + [ + `getKey`, + config.getKey === undefined + ? undefined + : getRuntimeReferenceIdentity(config.getKey), + ], + [ + `schema`, + schema === undefined ? undefined : getRuntimeReferenceIdentity(schema), + ], [`singleResult`, config.singleResult === true], [ `defaultStringCollation`, diff --git a/packages/db/src/query/compiler/index.ts b/packages/db/src/query/compiler/index.ts index 88a59d8a44..cf58f3c386 100644 --- a/packages/db/src/query/compiler/index.ts +++ b/packages/db/src/query/compiler/index.ts @@ -914,6 +914,7 @@ function compileQueryInternal( ancestorHasTopK || parentHasTopK, ], true, + subquery.materialization === `collection`, ) // Each include retains its own compilation result and lexical aliases. @@ -1195,6 +1196,10 @@ function compileQueryInternal( pipeline = pipeline.pipe(distinct(([_key, row]) => row.$selected)) } + const metadataSource = getWholeRowSourceAlias(query.select) ?? mainSource + const functionalSourceAliases = query.fnSelect + ? [...sourceBindings.keys()] + : undefined const finalizeRow = ( key: unknown, row: Record, @@ -1203,8 +1208,9 @@ function compileQueryInternal( const finalResults = attachVirtualPropsToSelected( unwrapValue(row.$selected), row, - publishCollectionRoot && parentKeyStream === undefined, - mainSource, + publishCollectionRoot, + metadataSource, + functionalSourceAliases, ) // When in includes mode, embed the correlation key and parentContext if (parentKeyStream) { @@ -2044,11 +2050,59 @@ function unwrapValue(value: any): any { return value instanceof ValClass ? value.value : value } +function getWholeRowSourceAlias(select: unknown): string | undefined { + if (select instanceof PropRef && select.path.length === 1) { + return select.path[0] + } + if (!isPlainObject(select)) return undefined + let sourceAlias: string | undefined + for (const [name, value] of Object.entries(select)) { + if ( + name.startsWith(`__SPREAD_SENTINEL__`) && + value instanceof PropRef && + value.path.length === 1 + ) { + // Later spreads replace earlier virtual fields in a selected object. + sourceAlias = value.path[0] + } + } + return sourceAlias +} + +function copySelectedRow(selected: Record) { + return Array.isArray(selected) ? [...selected] : { ...selected } +} + +function findMatchingSelectedSource( + selected: Record, + row: Record, + sourceAliases: ReadonlyArray | undefined, +): Record | undefined { + if (!sourceAliases) return undefined + for (const prop of VIRTUAL_PROP_NAMES) { + if (!(prop in selected)) return undefined + } + for (const alias of sourceAliases) { + const source = row[alias] + if (!source || typeof source !== `object`) continue + let matches = true + for (const prop of VIRTUAL_PROP_NAMES) { + if (!(prop in source) || !Object.is(selected[prop], source[prop])) { + matches = false + break + } + } + if (matches) return source as Record + } + return undefined +} + function attachVirtualPropsToSelected( selected: any, row: Record, isCollectionRoot: boolean, - mainSource: string, + metadataSource: string, + functionalSourceAliases?: ReadonlyArray, ): any { if ( !selected || @@ -2059,39 +2113,40 @@ function attachVirtualPropsToSelected( } const selectedRecord = selected as Record - const source = row[mainSource] + const matchingSource = isCollectionRoot + ? findMatchingSelectedSource(selectedRecord, row, functionalSourceAliases) + : undefined + const source = matchingSource ?? row[metadataSource] const sourceRecord = isCollectionRoot && source !== null && typeof source === `object` ? (source as Record) : undefined let result = selectedRecord - const copy = () => { - if (result === selectedRecord) { - result = Array.isArray(selected) ? [...selected] : { ...selected } - } - } for (const prop of VIRTUAL_PROP_NAMES) { if (isCollectionRoot && prop in selectedRecord) { // An explicit virtual name in a root projection cannot replace source // metadata. Ordinary projections acquire their own Collection metadata. const owner = - prop in row - ? row - : sourceRecord && prop in sourceRecord - ? sourceRecord - : undefined + matchingSource && prop in matchingSource + ? matchingSource + : prop in row + ? row + : sourceRecord && prop in sourceRecord + ? sourceRecord + : undefined if (owner) { const value = owner[prop] if (selectedRecord[prop] !== value) { - copy() + if (result === selectedRecord) + result = copySelectedRow(selectedRecord) result[prop] = value } } else { - copy() + if (result === selectedRecord) result = copySelectedRow(selectedRecord) delete result[prop] } } else if (selectedRecord[prop] == null && prop in row) { - copy() + if (result === selectedRecord) result = copySelectedRow(selectedRecord) result[prop] = row[prop] } } diff --git a/packages/db/src/query/live/ARCHITECTURE.md b/packages/db/src/query/live/ARCHITECTURE.md index 725c39b355..899575a035 100644 --- a/packages/db/src/query/live/ARCHITECTURE.md +++ b/packages/db/src/query/live/ARCHITECTURE.md @@ -631,6 +631,11 @@ layout. A bare child query is a Collection-valued include. It exposes one stable public Collection facade per active bucket in that edge: +Facade rows obey the Collection virtual-field boundary: selected +`$hasPendingWrites`, `$origin`, `$key`, and `$collectionId` values cannot replace +the selected source row's metadata. Inline include values keep their selected +shape because they are not published as Collection rows. + ```text ActiveBucket + BucketRow -> ActiveBucketRow -> BucketFacade(bucket, Collection) Route + BucketFacade -> CellValue(cell, Collection) diff --git a/packages/db/tests/live-query-options.test.ts b/packages/db/tests/live-query-options.test.ts index b138edf23e..218951752b 100644 --- a/packages/db/tests/live-query-options.test.ts +++ b/packages/db/tests/live-query-options.test.ts @@ -395,21 +395,28 @@ describe(`live query identity`, () => { expect(getLiveQueryHash(first, [])).not.toBe(getLiveQueryHash(second, [])) }) - it(`does not collapse configs with opaque row identity behavior`, () => { + it(`keys opaque row identity and schema behavior by reference`, () => { const source = createCollection<{ id: string }>({ id: `live-query-config-identity-source`, getKey: (row) => row.id, sync: { sync: ({ markReady }) => markReady() }, }) const query = new BaseQueryBuilder().from({ source }) - const first = { query, getKey: (row: { id: string }) => row.id } - const second = { query, getKey: (row: { id: string }) => `x-${row.id}` } + const getKey = (row: { id: string }) => row.id + const otherGetKey = (row: { id: string }) => `x-${row.id}` + const schema = { parse: (row: unknown) => row } + const otherSchema = { parse: (row: unknown) => row } + const first = { query, getKey, schema } + const same = { query, getKey, schema } + const second = { query, getKey: otherGetKey, schema } + const third = { query, getKey, schema: otherSchema } expect(getPreparedLiveQueryIdentity(first)).not.toEqual( getPreparedLiveQueryIdentity(second), ) - expect(() => getLiveQueryHash(first)).toThrow(/function value/) - expect(() => getLiveQueryHash(second)).toThrow(/function value/) + expect(getLiveQueryHash(first)).toBe(getLiveQueryHash(same)) + expect(getLiveQueryHash(first)).not.toBe(getLiveQueryHash(second)) + expect(getLiveQueryHash(first)).not.toBe(getLiveQueryHash(third)) }) it(`uses exact custom comparator identity in live-query config hashes`, () => { diff --git a/packages/db/tests/query/virtual-row-fields-oracle.test.ts b/packages/db/tests/query/virtual-row-fields-oracle.test.ts index 58d29e4938..fa3861c0bc 100644 --- a/packages/db/tests/query/virtual-row-fields-oracle.test.ts +++ b/packages/db/tests/query/virtual-row-fields-oracle.test.ts @@ -17,8 +17,11 @@ * * The legal query forms exercised here are expression-projected, unprojected, * and functional root rows; unprojected and directly selected whole-row - * children; an unmatched left-joined whole row; and object, nested-object, - * array, `findOne`, and Date child projections. The production + * children; an unmatched left-joined whole row; matched and unmatched + * whole-row joins, including an extended spread and both orders of two source + * spreads; a Collection-valued child with colliding selected metadata + * and its inline-value control; and object, nested-object, array, `findOne`, + * and Date child projections. The production * driver calls `createLiveQueryCollection`, `preload`, `toArray`, and * `materialize`. The checkpoint is `live.toArray` after `preload` resolves. * @@ -26,8 +29,14 @@ * `hasVirtualProps`, so a missing field cannot hide behind the production * classifier. Exact `$key`, selected shapes, and nonempty child results prove * the intended paths ran. This oracle - * does not cover matched join shapes, ordering, updates after publication, - * framework receiving boundaries, or sync-state transitions. The root-shape + * does not cover ordering, arbitrary joined projections, framework receiving + * boundaries, or sync-state transitions beyond one pending joined-row update. + * The selected joined row is compared with the joined source's public row at + * preload, pending-write, and settled cuts; a FROM-source selection, two + * opposite spread orders, and an unmatched RIGHT join distinguish a + * main-source, first-spread, or last-join shortcut. The + * Collection-valued child is compared with its source Collection, while its + * inline sibling must retain the selected `$key` value. The root-shape * matrix rejects the wrong rule that only expression projections receive * virtual row fields. Prior hostile controls made projected-value * enrichment and a missing whole-row classification fail at these @@ -119,6 +128,217 @@ async function checkWithCleanup( } describe(`virtual row field runtime boundary`, () => { + test(`keeps source metadata authoritative on Collection-valued include rows`, async () => { + const parents = createCollection( + mockSyncCollectionOptions<{ id: string }>({ + id: `included-virtual-parents`, + getKey: (row) => row.id, + initialData: [{ id: `parent-1` }], + }), + ) + const children = createCollection( + mockSyncCollectionOptions<{ id: string; parentId: string }>({ + id: `included-virtual-children`, + getKey: (row) => row.id, + initialData: [{ id: `child-1`, parentId: `parent-1` }], + }), + ) + const live = createLiveQueryCollection((q) => + q.from({ parent: parents }).select(({ parent }) => ({ + id: parent.id, + children: q + .from({ child: children }) + .where(({ child }) => eq(child.parentId, parent.id)) + .select(({ child }) => ({ + id: child.id, + $key: `selected-key`, + $collectionId: `selected-collection`, + $hasPendingWrites: true, + $origin: `local`, + })), + inlineChildren: toArray( + q + .from({ child: children }) + .where(({ child }) => eq(child.parentId, parent.id)) + .select(({ child }) => ({ + id: child.id, + $key: `inline-selected-key`, + })), + ), + })), + ) + // A child facade is a public Collection, unlike an inline toArray value. + // Its row cannot publish selected literals as system metadata. + await checkWithCleanup(async () => { + await live.preload() + const child = live.toArray[0]!.children.toArray[0]! + const source = children.get(`child-1`)! + for (const name of virtualFieldNames) { + expect(child[name], name).toBe(source[name]) + } + expect(live.toArray[0]!.inlineChildren).toEqual([ + { id: `child-1`, $key: `inline-selected-key` }, + ]) + }, [ + () => live.cleanup(), + () => children.cleanup(), + () => parents.cleanup(), + ]) + }) + + test(`keeps a selected joined row's source metadata across match and pending update`, async () => { + const lists = createCollection( + mockSyncCollectionOptions<{ id: string }>({ + id: `joined-virtual-lists`, + getKey: (row) => row.id, + initialData: [{ id: `list-1` }], + }), + ) + const items = createCollection( + mockSyncCollectionOptions<{ + id: string + listId: string + label: string + }>({ + id: `joined-virtual-items`, + getKey: (row) => row.id, + initialData: [{ id: `item-1`, listId: `list-1`, label: `before` }], + }), + ) + const live = createLiveQueryCollection((q) => + q + .from({ list: lists }) + .join({ item: items }, ({ list, item }) => eq(list.id, item.listId)) + .select(({ item }) => ({ ...item })), + ) + const fromLive = createLiveQueryCollection((q) => + q + .from({ list: lists }) + .join({ item: items }, ({ list, item }) => eq(list.id, item.listId)) + .select(({ list }) => ({ ...list })), + ) + const extended = createLiveQueryCollection((q) => + q + .from({ list: lists }) + .join({ item: items }, ({ list, item }) => eq(list.id, item.listId)) + .select(({ item }) => ({ + ...item, + copyOfLabel: item.label, + $key: `selected-key`, + })), + ) + const lastItem = createLiveQueryCollection((q) => + q + .from({ list: lists }) + .join({ item: items }, ({ list, item }) => eq(list.id, item.listId)) + .select(({ list, item }) => ({ ...list, ...item })), + ) + const lastList = createLiveQueryCollection((q) => + q + .from({ list: lists }) + .join({ item: items }, ({ list, item }) => eq(list.id, item.listId)) + .select(({ list, item }) => ({ ...item, ...list })), + ) + const functional = createLiveQueryCollection((q) => + q + .from({ list: lists }) + .join({ item: items }, ({ list, item }) => eq(list.id, item.listId)) + .fn.select(({ item }) => ({ ...item })), + ) + + // A whole-row selection names the joined item as its metadata source. + // The independent expected row is the item's public Collection row at + // each checkpoint; list metadata has a different key and Collection ID. + await checkWithCleanup(async () => { + await Promise.all([ + live.preload(), + fromLive.preload(), + extended.preload(), + lastItem.preload(), + lastList.preload(), + functional.preload(), + ]) + const expectItemMetadata = () => { + const actual = live.toArray[0]! + const item = items.get(`item-1`)! + expect(actual.label).toBe(item.label) + for (const name of virtualFieldNames) { + expect(actual[name], name).toBe(item[name]) + expect(extended.toArray[0]![name], `extended ${name}`).toBe( + item[name], + ) + expect(lastItem.toArray[0]![name], `last item ${name}`).toBe( + item[name], + ) + expect(functional.toArray[0]![name], `functional ${name}`).toBe( + item[name], + ) + } + } + + expectItemMetadata() + for (const name of virtualFieldNames) { + expect(fromLive.toArray[0]![name], name).toBe( + lists.get(`list-1`)![name], + ) + expect(lastList.toArray[0]![name], `last list ${name}`).toBe( + lists.get(`list-1`)![name], + ) + } + const update = items.update(`item-1`, (draft) => { + draft.label = `pending` + }) + await vi.waitFor(() => expect(live.toArray[0]?.label).toBe(`pending`)) + expect(items.get(`item-1`)?.$hasPendingWrites).toBe(true) + expectItemMetadata() + items.utils.resolveSync() + await update.when(`settled`) + expectItemMetadata() + }, [ + () => fromLive.cleanup(), + () => extended.cleanup(), + () => lastItem.cleanup(), + () => lastList.cleanup(), + () => functional.cleanup(), + () => live.cleanup(), + () => items.cleanup(), + () => lists.cleanup(), + ]) + }) + + test(`keeps the selected right-join row's metadata when the FROM row is absent`, async () => { + const lists = createCollection( + mockSyncCollectionOptions<{ id: string }>({ + id: `right-virtual-lists`, + getKey: (row) => row.id, + initialData: [], + }), + ) + const items = createCollection( + mockSyncCollectionOptions<{ id: string; listId: string }>({ + id: `right-virtual-items`, + getKey: (row) => row.id, + initialData: [{ id: `item-1`, listId: `missing` }], + }), + ) + const live = createLiveQueryCollection((q) => + q + .from({ list: lists }) + .rightJoin({ item: items }, ({ list, item }) => + eq(list.id, item.listId), + ) + .select(({ item }) => item), + ) + + await checkWithCleanup(async () => { + await live.preload() + expect(live.toArray).toHaveLength(1) + for (const name of virtualFieldNames) { + expect(live.toArray[0]![name], name).toBe(items.get(`item-1`)![name]) + } + }, [() => live.cleanup(), () => items.cleanup(), () => lists.cleanup()]) + }) + test(`keeps source write metadata authoritative over selected field names`, async () => { const rows = createCollection( mockSyncCollectionOptions<{ id: string; label: string }>({ @@ -137,11 +357,21 @@ describe(`virtual row field runtime boundary`, () => { $collectionId: `selected-collection`, })), ) + const functional = createLiveQueryCollection((q) => + q.from({ row: rows }).fn.select(({ row }) => ({ + id: row.id, + label: row.label, + $hasPendingWrites: false, + $origin: `remote`, + $key: `selected-key`, + $collectionId: `selected-collection`, + })), + ) // The model is the source Collection's public row during a held optimistic // update. Selection changes user fields, but cannot author system metadata. await checkWithCleanup(async () => { - await live.preload() + await Promise.all([live.preload(), functional.preload()]) const update = rows.update(`row-1`, (draft) => { draft.label = `pending` }) @@ -156,10 +386,16 @@ describe(`virtual row field runtime boundary`, () => { $key: `row-1`, $collectionId: rows.id, }) + expect(functional.toArray[0]).toMatchObject({ + $hasPendingWrites: true, + $origin: `local`, + $key: `row-1`, + $collectionId: rows.id, + }) rows.utils.resolveSync() await update.when(`settled`) - }, [() => live.cleanup(), () => rows.cleanup()]) + }, [() => functional.cleanup(), () => live.cleanup(), () => rows.cleanup()]) }) test(`publishes every virtual field on implicit, expression, and functional root shapes`, async () => { diff --git a/packages/electric-db-collection/src/electric.ts b/packages/electric-db-collection/src/electric.ts index b8a956bec4..683f2eb773 100644 --- a/packages/electric-db-collection/src/electric.ts +++ b/packages/electric-db-collection/src/electric.ts @@ -1259,6 +1259,9 @@ export function electricCollectionOptions>( ) const options = { ...restConfig, + onInsert: rejectReturnedHandlerValue(config.onInsert, `onInsert`), + onUpdate: rejectReturnedHandlerValue(config.onUpdate, `onUpdate`), + onDelete: rejectReturnedHandlerValue(config.onDelete, `onDelete`), syncMode: finalSyncMode, sync: syncTemplate, utils: utilityTemplate, @@ -1277,6 +1280,21 @@ export function electricCollectionOptions>( ) } +function rejectReturnedHandlerValue( + handler: ((params: TParams) => Promise) | undefined, + name: string, +): ((params: TParams) => Promise) | undefined { + if (!handler) return undefined + return async (params) => { + const result: unknown = await handler(params) + if (result !== undefined) { + throw new Error( + `Electric ${name} returned a value. Await collection.utils.awaitTxId() or awaitMatch() inside the handler instead.`, + ) + } + } +} + /** * Internal function to create ElectricSQL sync configuration */ diff --git a/packages/electric-db-collection/tests/electric.test.ts b/packages/electric-db-collection/tests/electric.test.ts index 46c4a239e7..c0df3f019f 100644 --- a/packages/electric-db-collection/tests/electric.test.ts +++ b/packages/electric-db-collection/tests/electric.test.ts @@ -1006,6 +1006,29 @@ describe(`Electric Integration`, () => { expect(testCollection._state.syncedData.size).toEqual(1) }) + it(`rejects a handler that returns a legacy txid instead of awaiting it`, async () => { + const testCollection = createCollection( + electricCollectionOptions({ + id: `legacy-return-value`, + shapeOptions: { + url: `http://test-url`, + params: { table: `test_table` }, + }, + startSync: true, + getKey: (item: Row) => item.id as number, + onInsert: (() => + Promise.resolve({ txid: 123 })) as unknown as () => Promise, + }), + ) + + const transaction = testCollection.insert({ id: 1, name: `pending` }) + await expect(transaction.when(`settled`)).rejects.toThrow( + /returned a value.*awaitTxId/i, + ) + expect(testCollection.has(1)).toBe(false) + await testCollection.cleanup() + }) + it(`should remove optimistic insert when txid sync confirms a different server-generated key`, async () => { const txid = 1234 const onInsert = vi.fn( diff --git a/packages/offline-transactions/package.json b/packages/offline-transactions/package.json index f13e183fcc..36e4b80fc5 100644 --- a/packages/offline-transactions/package.json +++ b/packages/offline-transactions/package.json @@ -59,10 +59,8 @@ "src", "skills" ], - "dependencies": { - "@tanstack/db": "workspace:*" - }, "peerDependencies": { + "@tanstack/db": "^0.15.0 || ^1.0.0-rc.0", "@react-native-community/netinfo": ">=11.0.0", "react-native": ">=0.70.0" }, diff --git a/packages/query-db-collection/package.json b/packages/query-db-collection/package.json index fce2c814d2..26ba14df0b 100644 --- a/packages/query-db-collection/package.json +++ b/packages/query-db-collection/package.json @@ -47,10 +47,10 @@ "src" ], "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@tanstack/db": "workspace:*" + "@standard-schema/spec": "^1.1.0" }, "peerDependencies": { + "@tanstack/db": "^0.15.0 || ^1.0.0-rc.0", "@tanstack/query-core": "^5.90.15", "typescript": ">=4.7" }, diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index acdd1d09f7..b0fe7fefda 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -12,7 +12,6 @@ import { makeComparator, toBooleanPredicate, validateSyncPersistenceCapability, - warnOnce, whenSyncAccepted, withCollectionConfigFactory, withCollectionSyncConfigFactory, @@ -4475,10 +4474,8 @@ export function queryCollectionOptions( () => writeContext, ) - // Helper to handle deprecated auto-refetch behavior with warnings - async function handleDeprecatedAutoRefetch( - handlerResult: unknown, - ): Promise { + // Query Collection 1.x keeps automatic refetch unless the handler opts out. + async function handleAutoRefetch(handlerResult: unknown): Promise { const canHaveProperties = (typeof handlerResult === `object` && handlerResult !== null) || typeof handlerResult === `function` @@ -4487,19 +4484,7 @@ export function queryCollectionOptions( 'refetch' in handlerResult && (handlerResult as Record).refetch === false - if (explicitRefetchFalse) { - return - } else { - warnOnce( - 'query-collection-auto-refetch', - '[TanStack DB] DEPRECATED: QueryCollection handlers currently auto-refetch after completion. ' + - 'This behavior will be removed in v1.0. To prepare: ' + - '(1) Add `await collection.utils.refetch()` and temporarily return `{ refetch: false }` to avoid a second refetch, or ' + - "(2) Return `{ refetch: false }` to opt out now if you don't need it. " + - 'See: https://tanstack.com/db/latest/docs/collections/query-collection#controlling-refetch-behavior', - ) - await refetchQueryResults(undefined, false) - } + if (!explicitRefetchFalse) await refetchQueryResults(undefined, false) } const runMutationHandler = async ( @@ -4521,7 +4506,7 @@ export function queryCollectionOptions( params: Parameters>[0], ): Promise => { const handlerResult = (await runMutationHandler(onInsert, params)) ?? {} - await handleDeprecatedAutoRefetch(handlerResult) + await handleAutoRefetch(handlerResult) } : undefined @@ -4530,7 +4515,7 @@ export function queryCollectionOptions( params: Parameters>[0], ): Promise => { const handlerResult = (await runMutationHandler(onUpdate, params)) ?? {} - await handleDeprecatedAutoRefetch(handlerResult) + await handleAutoRefetch(handlerResult) } : undefined @@ -4539,7 +4524,7 @@ export function queryCollectionOptions( params: Parameters>[0], ): Promise => { const handlerResult = (await runMutationHandler(onDelete, params)) ?? {} - await handleDeprecatedAutoRefetch(handlerResult) + await handleAutoRefetch(handlerResult) } : undefined diff --git a/packages/query-db-collection/tests/optimistic-ordered-refetch-oracle.test.ts b/packages/query-db-collection/tests/optimistic-ordered-refetch-oracle.test.ts index 7c5a7622ba..f3792f7f0c 100644 --- a/packages/query-db-collection/tests/optimistic-ordered-refetch-oracle.test.ts +++ b/packages/query-db-collection/tests/optimistic-ordered-refetch-oracle.test.ts @@ -206,7 +206,7 @@ async function checkOptimisticOrderedRefetch( } persistence.resolve() - await transaction.isPersisted.promise + await transaction.when(`settled`) if (!reportDuringHold) { expect( requests.some( diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index 5d4400485a..4cbedf472e 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -2596,7 +2596,41 @@ describe(`QueryCollection`, () => { ]) }) - it(`does not reject a deprecated auto-refetch when result application fails`, async () => { + it(`does not warn that retained automatic refetch is being removed`, async () => { + resetWarnings() + const warning = vi.spyOn(console, `warn`).mockImplementation(() => {}) + const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `Item 1` }]) + const options = queryCollectionOptions({ + id: `retained-auto-refetch`, + queryClient, + queryKey: [`retained-auto-refetch`], + queryFn, + getKey, + onInsert: () => Promise.resolve(), + startSync: true, + }) + const collection = createCollection(options) + const transaction = { + id: `retained-auto-refetch-transaction`, + mutations: [], + } as unknown as TransactionWithMutations + + try { + await collection.stateWhenReady() + await options.onInsert!({ transaction, collection }) + expect(queryFn).toHaveBeenCalledTimes(2) + expect( + warning.mock.calls.some(([message]) => + String(message).includes(`removed in v1.0`), + ), + ).toBe(false) + } finally { + warning.mockRestore() + await collection.cleanup() + } + }) + + it(`does not reject automatic refetch when result application fails`, async () => { resetWarnings() const warning = vi.spyOn(console, `warn`).mockImplementation(() => {}) const consoleError = vi diff --git a/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts b/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts index 9e2591408d..a0b0e4fb6e 100644 --- a/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts +++ b/packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts @@ -601,7 +601,7 @@ async function startRun(testCase: Case): Promise { : items.delete(rowId) return async () => { release() - await transaction.isPersisted.promise + await transaction.when(`settled`) await settle() } } diff --git a/packages/react-db/src/live-query-internals.ts b/packages/react-db/src/live-query-internals.ts index 35748dc4aa..4d1e78d568 100644 --- a/packages/react-db/src/live-query-internals.ts +++ b/packages/react-db/src/live-query-internals.ts @@ -4,7 +4,7 @@ const liveQueryResultInfo = Symbol(`liveQueryResultInfo`) export type LiveQueryResultInfo = { client: DbClient | undefined - queryHash: string | undefined + queryHash: string observer: LiveQueryObserver } diff --git a/packages/react-db/src/useLiveInfiniteQuery.ts b/packages/react-db/src/useLiveInfiniteQuery.ts index 3806471fb7..e4dbc050e8 100644 --- a/packages/react-db/src/useLiveInfiniteQuery.ts +++ b/packages/react-db/src/useLiveInfiniteQuery.ts @@ -221,21 +221,26 @@ export function useLiveInfiniteQuery( identityDeps = committed.dependencies ?? [] } else { identityMode = `derived` - const preparation = prepareDerivedQuery( - queryFnOrCollection, - dbClient, - derivedIdentityProfilerRef.current, - deferredCollections, - ) - preparedQueryValue = preparation.value - claimSourceIds( - (sourceIdsRef.current ??= createSourceIdBindings()), - preparation.value, - dbClient, - `useLiveInfiniteQuery`, - () => resumeSyncStarts(deferredCollections), - ) - identityDeps = preparation.identityDeps + try { + const preparation = prepareDerivedQuery( + queryFnOrCollection, + dbClient, + derivedIdentityProfilerRef.current, + deferredCollections, + ) + preparedQueryValue = preparation.value + claimSourceIds( + (sourceIdsRef.current ??= createSourceIdBindings()), + preparation.value, + dbClient, + `useLiveInfiniteQuery`, + () => resumeSyncStarts(deferredCollections), + ) + identityDeps = preparation.identityDeps + } catch (error) { + resumeSyncStarts(deferredCollections) + throw error + } } } diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index a0e47e7ea3..2dc3278dd5 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -653,34 +653,40 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { let identityDeps: ReadonlyArray let streamIdentity: unknown = undefined - if (queryKey) { - identityDeps = queryKey - streamIdentity = [`queryKey`, queryKey] - } else if (inputIsCollection) { - identityDeps = [] - streamIdentity = [`collection`, configOrQueryOrCollection.id] - } else { - const preparation = prepareDerivedQuery( - configOrQueryOrCollection, - dbClient, - instance.derivedIdentityProfiler, - instance.deferredCollections, - ) - preparedQueryValue = preparation.value - identityDeps = preparation.identityDeps - streamIdentity = preparation.identityDeps - } + let queryHash: string + try { + if (queryKey) { + identityDeps = queryKey + streamIdentity = [`queryKey`, queryKey] + } else if (inputIsCollection) { + identityDeps = [] + streamIdentity = [`collection`, configOrQueryOrCollection.id] + } else { + const preparation = prepareDerivedQuery( + configOrQueryOrCollection, + dbClient, + instance.derivedIdentityProfiler, + instance.deferredCollections, + ) + preparedQueryValue = preparation.value + identityDeps = preparation.identityDeps + streamIdentity = preparation.identityDeps + } - const queryHash = getStableValueHash(streamIdentity, `queryKey`) + queryHash = getStableValueHash(streamIdentity, `queryKey`) - if (queryKey === undefined && preparedQueryValue !== unpreparedQueryValue) { - claimSourceIds( - instance.sourceIds, - preparedQueryValue, - dbClient, - `useLiveQuery`, - resumeDeferredCollections, - ) + if (queryKey === undefined && preparedQueryValue !== unpreparedQueryValue) { + claimSourceIds( + instance.sourceIds, + preparedQueryValue, + dbClient, + `useLiveQuery`, + resumeDeferredCollections, + ) + } + } catch (error) { + resumeDeferredCollections() + throw error } const canReuseSuspenseKey = @@ -694,7 +700,6 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { if ( forSuspense && !inputIsCollection && - queryHash && !canReuseSuspenseKey && preparedQueryValue === unpreparedQueryValue ) { @@ -706,19 +711,17 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { } const suspenseKey = - queryHash && forSuspense && !inputIsCollection + forSuspense && !inputIsCollection ? canReuseSuspenseKey - ? instance.suspenseKey + ? instance.suspenseKey! : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash, dbClient) : queryHash const suspenseCollections = - forSuspense && !inputIsCollection && suspenseKey + forSuspense && !inputIsCollection ? getSuspenseCollections(dbClient) : undefined - const suspenseEntry = suspenseKey - ? suspenseCollections?.get(suspenseKey) - : undefined + const suspenseEntry = suspenseCollections?.get(suspenseKey) const suspenseCollection = suspenseEntry?.collection const identityChanged = @@ -872,7 +875,7 @@ function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) { if (forSuspense) { setLiveQueryResultInfo(returned, { client: dbClient, - queryHash: instance.queryHash, + queryHash, observer, }) } diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index f468ca9406..c17ab9a6ce 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -232,10 +232,7 @@ export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { const observerError = queryInfo.observer.getError() // A client request can reject with undefined, which getError() cannot // distinguish from no error. The request status preserves that distinction. - const clientQuery = - queryInfo.client && queryInfo.queryHash - ? queryInfo.client._getLiveQuery(queryInfo.queryHash) - : undefined + const clientQuery = queryInfo.client?._getLiveQuery(queryInfo.queryHash) // A configured persisted restore may still satisfy this render after the // client query fails. Let the observer classify that failure below. if ( @@ -285,11 +282,6 @@ export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { (collectionStatus === `error` && result.persistedStatus !== `unavailable`)) ) { - if (queryInfo.client?._isSsrStreamingEnabled() && !queryInfo.queryHash) { - throw new Error( - `Cannot stream this live query during SSR because it has no stable identity. Provide an explicit serializable queryKey.`, - ) - } // Create or reuse promise for current collection if (!promiseRef.current) { const collection = result.collection @@ -306,12 +298,10 @@ export function useLiveSuspenseQuery(configOrQueryOrCollection: any) { stopWatchingCleanup() throw error } - const preloadClientQuery = - client && queryHash ? client._getLiveQuery(queryHash) : undefined + const preloadClientQuery = client?._getLiveQuery(queryHash) promiseRef.current = preload .catch((error: unknown) => { - const latestClientQuery = - client && queryHash ? client._getLiveQuery(queryHash) : undefined + const latestClientQuery = client?._getLiveQuery(queryHash) if (active && latestClientQuery === preloadClientQuery) rememberInitialRenderError( collection, diff --git a/packages/react-db/tests/source-id-reuse-oracle.test.tsx b/packages/react-db/tests/source-id-reuse-oracle.test.tsx index aacbe853bc..65f2d6a1ef 100644 --- a/packages/react-db/tests/source-id-reuse-oracle.test.tsx +++ b/packages/react-db/tests/source-id-reuse-oracle.test.tsx @@ -17,6 +17,8 @@ * entries must remain distinct, including before the first hook commits. * A rejected render must release any sync-start deferral it acquired for a * shared client descriptor, so a later direct reader can start that Collection. + * This also applies when an unhashable functional query rejects before the + * hook can claim its source IDs. * This remains true when earlier deferred sync starts throw: each pending * Collection is attempted before the first startup error is rethrown. * The bounded histories below distinguish this law from a hash-only check, @@ -99,6 +101,78 @@ function checkRerender( } describe(`React source ID reuse`, () => { + it.each([`normal`, `infinite`] as const)( + `releases deferred descriptor sync after an unhashable %s query rejects`, + async (variant) => { + const client = new DbClient() + let syncStarts = 0 + const descriptor = collectionOptions(`unhashable-${variant}`, () => ({ + id: `unhashable-${variant}`, + getKey: (row: Row) => row.id, + startSync: true, + sync: { + sync: ({ begin, write, commit, markReady }) => { + syncStarts++ + begin() + write({ + type: `insert`, + value: { id: `one`, value: `ready`, rank: 1 }, + }) + commit() + markReady() + }, + }, + })) + const consoleError = vi + .spyOn(console, `error`) + .mockImplementation(() => {}) + + function Broken() { + if (variant === `normal`) { + useLiveQuery({ + client, + query: (q) => + q.from({ row: descriptor }).fn.select(({ row }) => ({ + id: row.id, + value: row.value, + })), + }) + } else { + useLiveInfiniteQuery( + (q) => + q + .from({ row: descriptor }) + .orderBy(({ row }) => row.rank) + .fn.select(({ row }) => ({ id: row.id, value: row.value })), + { client, pageSize: 1 }, + ) + } + return
      Query
      + } + + try { + const root = render( + + + , + ) + expect(root.getByText(`Rejected`)).toBeDefined() + expect(syncStarts).toBe(1) + + const shared = client.collection(descriptor) + const reader = renderHook(() => useLiveQuery(shared)) + await act(async () => {}) + expect(syncStarts).toBe(1) + expect(reader.result.current.data[0]?.value).toBe(`ready`) + reader.unmount() + root.unmount() + await shared.cleanup() + } finally { + consoleError.mockRestore() + } + }, + ) + it(`releases a later shared source when earlier deferred sync starts fail`, async () => { const client = new DbClient() const first = source(`settings`, `first`) diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index 5beffddeb2..e3c7135285 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -3551,4 +3551,43 @@ describe(`retained useLiveQuery results`, () => { expect([...read.state.values()].map((row) => row.version)).toEqual([1]) }, ) + + it(`accepts getKey without queryKey and replaces the query when getKey changes`, async () => { + const source = createCollection( + localOnlyCollectionOptions<{ id: string; value: string }, string>({ + id: `hook-opaque-config-identity`, + getKey: (row) => row.id, + initialData: [{ id: `one`, value: `value` }], + }), + ) + const firstKey = (row: { id: string }) => row.id + const secondKey = (row: { id: string }) => `other-${row.id}` + const { result, rerender, unmount } = renderHook( + ({ getKey }) => + useLiveQuery({ + query: (q) => q.from({ row: source }), + getKey, + }), + { initialProps: { getKey: firstKey } }, + ) + let firstCollection: typeof result.current.collection | undefined + + try { + await waitFor(() => expect(result.current.isReady).toBe(true)) + firstCollection = result.current.collection + expect(firstCollection.has(`one`)).toBe(true) + + rerender({ getKey: secondKey }) + await waitFor(() => expect(result.current.isReady).toBe(true)) + expect(result.current.collection).not.toBe(firstCollection) + expect(result.current.collection.has(`other-one`)).toBe(true) + } finally { + unmount() + await firstCollection?.cleanup() + if (result.current.collection !== firstCollection) { + await result.current.collection.cleanup() + } + await source.cleanup() + } + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 159257d992..16a765bf87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@changesets/apply-release-plan': specifier: 7.0.14 version: 7.0.14 + '@changesets/assemble-release-plan': + specifier: 6.0.9 + version: 6.0.9 '@changesets/cli': specifier: ^2.29.8 version: 2.29.8(@types/node@25.2.2) @@ -38,6 +41,9 @@ importers: '@changesets/get-release-plan': specifier: 4.0.14 version: 4.0.14 + '@changesets/read': + specifier: 0.6.6 + version: 0.6.6 '@eslint/js': specifier: ^9.39.2 version: 9.39.4 @@ -1440,7 +1446,7 @@ importers: packages/offline-transactions: dependencies: '@tanstack/db': - specifier: workspace:* + specifier: ^0.15.0 || ^1.0.0-rc.0 version: link:../db devDependencies: '@react-native-community/netinfo': @@ -1502,7 +1508,7 @@ importers: specifier: ^1.1.0 version: 1.1.0 '@tanstack/db': - specifier: workspace:* + specifier: ^0.15.0 || ^1.0.0-rc.0 version: link:../db typescript: specifier: '>=4.7' diff --git a/scripts/publish-packages.mjs b/scripts/publish-packages.mjs index b304364591..1d604524bf 100644 --- a/scripts/publish-packages.mjs +++ b/scripts/publish-packages.mjs @@ -46,7 +46,7 @@ async function publishWithHiddenPackages( /** * Changesets uses the `rc` npm tag for every package while pre mode is active. - * Publish candidates first, then publish stable packages with pre mode hidden. + * Publish stable compatibility patches first, then publish candidates. */ export async function publishDuringRc( rootDirectory = root, @@ -71,7 +71,6 @@ export async function publishDuringRc( ) } - await publishWithHiddenPackages(rootDirectory, stable, false, publishCommand) if (stable.length > 0) { await publishWithHiddenPackages( rootDirectory, @@ -80,6 +79,7 @@ export async function publishDuringRc( publishCommand, ) } + await publishWithHiddenPackages(rootDirectory, stable, false, publishCommand) } async function main() { diff --git a/scripts/publish-packages.test.mjs b/scripts/publish-packages.test.mjs index cbd0cac7cc..c8fe30be24 100644 --- a/scripts/publish-packages.test.mjs +++ b/scripts/publish-packages.test.mjs @@ -65,7 +65,6 @@ test('RC publishing gives candidate and stable packages their own npm tags', asy }) assert.deepEqual(phases, [ - { publicNames: ['@tanstack/db'], preMode: true }, { publicNames: [ '@tanstack/query-db-collection', @@ -73,12 +72,13 @@ test('RC publishing gives candidate and stable packages their own npm tags', asy ], preMode: false, }, + { publicNames: ['@tanstack/db'], preMode: true }, ]) assert.deepEqual(packageState(root), before) assert.equal(existsSync(join(root, '.changeset', 'pre.json')), true) }) -test('failed stable publishing restores package manifests and prerelease state', async (t) => { +test('failed candidate publishing restores package manifests and prerelease state', async (t) => { const root = createWorkspace() t.after(() => rmSync(root, { recursive: true, force: true })) const before = packageState(root) diff --git a/scripts/version-packages.mjs b/scripts/version-packages.mjs index 67aa2ac93c..7390fbc451 100644 --- a/scripts/version-packages.mjs +++ b/scripts/version-packages.mjs @@ -1,23 +1,30 @@ import { execFileSync } from 'node:child_process' -import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' +import { + existsSync, + readFileSync, + readdirSync, + unlinkSync, + writeFileSync, +} from 'node:fs' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import applyReleasePlan from '@changesets/apply-release-plan' +import assembleReleasePlan from '@changesets/assemble-release-plan' import { read as readChangesetsConfig } from '@changesets/config' import getReleasePlan from '@changesets/get-release-plan' +import readChangesets from '@changesets/read' import { getPackages } from '@manypkg/get-packages' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const changesetPath = join(root, '.changeset', 'first-rc.md') -const configPath = join(root, '.changeset', 'config.json') -function run(command, args) { - execFileSync(command, args, { cwd: root, stdio: 'inherit' }) +function run(command, args, rootDirectory = root) { + execFileSync(command, args, { cwd: rootDirectory, stdio: 'inherit' }) } -function packageManifests() { - return readdirSync(join(root, 'packages')).flatMap((directory) => { - const path = join(root, 'packages', directory, 'package.json') +function packageManifests(rootDirectory) { + return readdirSync(join(rootDirectory, 'packages')).flatMap((directory) => { + const path = join(rootDirectory, 'packages', directory, 'package.json') return existsSync(path) ? [{ path, value: JSON.parse(readFileSync(path, 'utf8')) }] : [] @@ -45,13 +52,20 @@ function privateWorkspaceNames(directory) { return names } -function seedFirstCandidate() { - const changeset = readFileSync(changesetPath, 'utf8') +export async function seedFirstCandidate( + rootDirectory = root, + enterPreMode = () => + run('pnpm', ['exec', 'changeset', 'pre', 'enter', 'rc'], rootDirectory), +) { + const firstChangesetPath = join(rootDirectory, '.changeset', 'first-rc.md') + const releaseConfigPath = join(rootDirectory, '.changeset', 'config.json') + const preStatePath = join(rootDirectory, '.changeset', 'pre.json') + const changeset = readFileSync(firstChangesetPath, 'utf8') const releaseEntries = [ ...changeset.matchAll(/^'([^']+)': (major|minor|patch)$/gm), ] const releaseNames = new Set(releaseEntries.map((match) => match[1])) - const manifests = packageManifests() + const manifests = packageManifests(rootDirectory) const preOne = manifests.filter( ({ value }) => !value.private && /^0\./.test(value.version), ) @@ -59,64 +73,97 @@ function seedFirstCandidate() { .filter(({ value }) => !value.private && /^1\./.test(value.version)) .map(({ value }) => value.name) const expectedNames = new Set(preOne.map(({ value }) => value.name)) + const stableNames = new Set(alreadyOne) if ( - releaseNames.size !== expectedNames.size || releaseEntries.length !== releaseNames.size || - releaseEntries.some((match) => match[2] !== 'major') || - [...releaseNames].some((name) => !expectedNames.has(name)) + [...expectedNames].some((name) => !releaseNames.has(name)) || + releaseEntries.some( + (match) => + (expectedNames.has(match[1]) && match[2] !== 'major') || + (stableNames.has(match[1]) && match[2] !== 'patch') || + (!expectedNames.has(match[1]) && !stableNames.has(match[1])), + ) ) { throw new Error( - 'The first RC changeset must list every public pre-1.0 package exactly once.', + 'The first RC changeset must list every public pre-1.0 package as major and any already-1.x package as patch, each exactly once.', ) } - const originalConfig = readFileSync(configPath, 'utf8') + const originalConfig = readFileSync(releaseConfigPath, 'utf8') const config = JSON.parse(originalConfig) const ignored = new Set([ ...config.ignore, - ...alreadyOne, - ...privateWorkspaceNames(join(root, 'packages')), - ...privateWorkspaceNames(join(root, 'examples')), + ...privateWorkspaceNames(join(rootDirectory, 'packages')), + ...privateWorkspaceNames(join(rootDirectory, 'examples')), ]) config.ignore = [...ignored] config.privatePackages = { version: false, tag: false } const removedDependencies = [] - for (const { path, value } of preOne) { - // Changesets increments the existing prerelease counter to rc.1. - value.version = '1.0.0-rc.0' - // Changesets requires a package that depends on an ignored package to be - // ignored too. Hide those edges only while calculating this RC release. - for (const field of [ - 'dependencies', - 'devDependencies', - 'peerDependencies', - 'optionalDependencies', - ]) { - for (const [index, [name, range]] of Object.entries( - value[field] ?? {}, - ).entries()) { - if (ignored.has(name)) { - removedDependencies.push({ path, field, name, range, index }) - delete value[field][name] + // Changesets can rewrite manifests and changelogs before failing. A failed + // first attempt must leave no rc.0 seed or pre.json that changes a rerun. + const restorePaths = [ + releaseConfigPath, + preStatePath, + ...manifests.flatMap(({ path }) => [ + path, + join(dirname(path), 'CHANGELOG.md'), + ]), + ...readdirSync(join(rootDirectory, '.changeset')) + .filter((name) => name.endsWith('.md')) + .map((name) => join(rootDirectory, '.changeset', name)), + ] + const originals = restorePaths.map((path) => ({ + path, + contents: existsSync(path) ? readFileSync(path) : undefined, + })) + + let succeeded = false + try { + for (const { path, value } of preOne) { + // Changesets increments the existing prerelease counter to rc.1. + value.version = '1.0.0-rc.0' + // Changesets requires a package that depends on an ignored package to + // be ignored too. Hide those edges only while calculating this release. + for (const field of [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'optionalDependencies', + ]) { + for (const [index, [name, range]] of Object.entries( + value[field] ?? {}, + ).entries()) { + if (ignored.has(name)) { + removedDependencies.push({ path, field, name, range, index }) + delete value[field][name] + } } } + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) } - writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) - } - writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`) - - try { - run('pnpm', ['exec', 'changeset', 'pre', 'enter', 'rc']) - run('pnpm', ['exec', 'changeset', 'version']) + writeFileSync(releaseConfigPath, `${JSON.stringify(config, null, 2)}\n`) + await enterPreMode() + await versionDuringRc(rootDirectory) + succeeded = true } finally { - writeFileSync(configPath, originalConfig) - for (const { path, field, name, range, index } of removedDependencies) { - const value = JSON.parse(readFileSync(path, 'utf8')) - const entries = Object.entries(value[field]) - entries.splice(index, 0, [name, range]) - value[field] = Object.fromEntries(entries) - writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) + if (succeeded) { + writeFileSync(releaseConfigPath, originalConfig) + for (const { path, field, name, range, index } of removedDependencies) { + const value = JSON.parse(readFileSync(path, 'utf8')) + const entries = Object.entries(value[field]) + entries.splice(index, 0, [name, range]) + value[field] = Object.fromEntries(entries) + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) + } + } else { + for (const { path, contents } of originals) { + if (contents === undefined) { + if (existsSync(path)) unlinkSync(path) + } else { + writeFileSync(path, contents) + } + } } } } @@ -179,25 +226,56 @@ export async function versionDuringRc(rootDirectory = root) { } export async function versionOnRcExit(rootDirectory = root) { - const { packages, releaseConfig, plan } = await rcReleasePlan(rootDirectory) - // The release plan includes newly pending Changesets in its pre-state. - // The saved pre-state identifies which Changesets were already versioned. + const packages = await getPackages(rootDirectory) + const config = await readChangesetsConfig(rootDirectory, packages) + const releaseConfig = { + ...config, + privatePackages: { version: false, tag: false }, + } const preState = JSON.parse( readFileSync(join(rootDirectory, '.changeset', 'pre.json'), 'utf8'), ) + const consumedChangesets = new Set(preState.changesets) + const allChangesets = await readChangesets(rootDirectory) + const pendingChangesets = allChangesets.filter( + ({ id }) => !consumedChangesets.has(id), + ) + const exitChangesetId = 'rc-exit-version-plan' + if (allChangesets.some(({ id }) => id === exitChangesetId)) { + throw new Error(`The reserved changeset ID ${exitChangesetId} is in use.`) + } + const candidateNames = packages.packages + .filter(({ packageJson }) => + /^1\.0\.0-rc\.\d+(?:\.\d+)?$/.test(packageJson.version), + ) + .map(({ packageJson }) => packageJson.name) + const exitChangeset = { + id: exitChangesetId, + summary: '', + releases: candidateNames.map((name) => ({ name, type: 'patch' })), + } + // Exit planning must use only pending changesets. Replaying the changesets + // already applied during pre mode repeats changelog entries and can turn a + // stable package's pending patch into a major release. The synthetic patch + // exposes the candidate-to-1.0 dependency change before dependents are + // calculated; Changesets otherwise adds exit patches after that pass. + const plan = assembleReleasePlan( + [...pendingChangesets, exitChangeset], + packages, + releaseConfig, + preState, + ) + plan.changesets = pendingChangesets + for (const release of plan.releases) { + release.changesets = release.changesets.filter( + (id) => id !== exitChangesetId, + ) + } const privatePackages = new Set( packages.packages .filter(({ packageJson }) => packageJson.private) .map(({ packageJson }) => packageJson.name), ) - const consumedChangesets = new Set(preState.changesets) - const pendingStableNames = new Set( - plan.changesets - .filter(({ id }) => !consumedChangesets.has(id)) - .flatMap(({ releases }) => - releases.filter(({ type }) => type !== 'none').map(({ name }) => name), - ), - ) plan.releases = plan.releases.filter((release) => { if (privatePackages.has(release.name)) return false if (/^1\.0\.0-rc\.\d+(?:\.\d+)?$/.test(release.oldVersion)) { @@ -205,8 +283,6 @@ export async function versionOnRcExit(rootDirectory = root) { return true } if (/^\d+\.\d+\.\d+$/.test(release.oldVersion)) { - if (!pendingStableNames.has(release.name)) return false - release.newVersion = versionForRcRelease(release) return true } throw new Error( @@ -223,6 +299,12 @@ export async function versionOnRcExit(rootDirectory = root) { undefined, rootDirectory, ) + for (const { id } of allChangesets) { + if (consumedChangesets.has(id)) { + const path = join(rootDirectory, '.changeset', `${id}.md`) + if (existsSync(path)) unlinkSync(path) + } + } } async function main() { @@ -231,7 +313,7 @@ async function main() { ? JSON.parse(readFileSync(preStatePath, 'utf8')) : undefined if (existsSync(changesetPath) && preState === undefined) { - seedFirstCandidate() + await seedFirstCandidate() } else if (preState?.mode === 'pre' && preState.tag === 'rc') { await versionDuringRc() } else if (preState?.mode === 'exit' && preState.tag === 'rc') { diff --git a/scripts/version-packages.test.mjs b/scripts/version-packages.test.mjs index b773bc8c74..ddcf7661cb 100644 --- a/scripts/version-packages.test.mjs +++ b/scripts/version-packages.test.mjs @@ -5,17 +5,40 @@ import { mkdtempSync, readFileSync, rmSync, + unlinkSync, writeFileSync, } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { test } from 'node:test' import { + seedFirstCandidate, versionDuringRc, versionForRcRelease, versionOnRcExit, } from './version-packages.mjs' +test('the two existing 1.x packages publish a compatible DB peer and a first-RC patch', () => { + const firstRc = readFileSync( + new URL('../.changeset/first-rc.md', import.meta.url), + 'utf8', + ) + for (const directory of ['query-db-collection', 'offline-transactions']) { + const manifest = JSON.parse( + readFileSync( + new URL(`../packages/${directory}/package.json`, import.meta.url), + 'utf8', + ), + ) + assert.equal(manifest.dependencies?.['@tanstack/db'], undefined) + assert.equal( + manifest.peerDependencies['@tanstack/db'], + '^0.15.0 || ^1.0.0-rc.0', + ) + assert.match(firstRc, new RegExp(`'${manifest.name}': patch`)) + } +}) + function writeJson(path, value) { writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) } @@ -26,6 +49,12 @@ function packageVersion(root, directory) { ).version } +function packageManifest(root, directory) { + return JSON.parse( + readFileSync(join(root, 'packages', directory, 'package.json'), 'utf8'), + ) +} + function addChangeset(root, id, releases) { const frontmatter = Object.entries(releases) .map(([name, type]) => `'${name}': ${type}`) @@ -59,6 +88,9 @@ function createRcWorkspace() { fixed: [], linked: [], ignore: [], + ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { + onlyUpdatePeerDependentsWhenOutOfRange: true, + }, }) const packages = { @@ -71,10 +103,12 @@ function createRcWorkspace() { 'query-db-collection': { name: '@tanstack/query-db-collection', version: '1.4.5', + peerDependencies: { '@tanstack/db': '^0.15.0 || ^1.0.0-rc.0' }, }, 'offline-transactions': { name: '@tanstack/offline-transactions', version: '1.1.2', + peerDependencies: { '@tanstack/db': '^0.15.0 || ^1.0.0-rc.0' }, }, } for (const [directory, manifest] of Object.entries(packages)) { @@ -110,6 +144,132 @@ function createRcWorkspace() { return root } +function createFirstCandidateWorkspace() { + const root = createRcWorkspace() + unlinkSync(join(root, '.changeset', 'pre.json')) + for (const [directory, version] of [ + ['db', '0.15.0'], + ['react-db', '0.5.10'], + ]) { + const path = join(root, 'packages', directory, 'package.json') + writeJson(path, { ...packageManifest(root, directory), version }) + } + addChangeset(root, 'first-rc', { + '@tanstack/db': 'major', + '@tanstack/react-db': 'major', + '@tanstack/query-db-collection': 'patch', + '@tanstack/offline-transactions': 'patch', + }) + return root +} + +function enterFixturePreMode(root) { + writeJson(join(root, '.changeset', 'pre.json'), { + mode: 'pre', + tag: 'rc', + initialVersions: { + '@tanstack/db': '1.0.0-rc.0', + '@tanstack/react-db': '1.0.0-rc.0', + '@tanstack/query-db-collection': '1.4.5', + '@tanstack/offline-transactions': '1.1.2', + }, + changesets: [], + }) +} + +test('the first RC versions mixed and stable-only changesets without changing stable major lines', async (t) => { + const root = createFirstCandidateWorkspace() + t.after(() => rmSync(root, { recursive: true, force: true })) + addChangeset(root, 'mixed', { + '@tanstack/db': 'patch', + '@tanstack/query-db-collection': 'patch', + }) + addChangeset(root, 'stable-only', { + '@tanstack/offline-transactions': 'patch', + }) + + await seedFirstCandidate(root, () => enterFixturePreMode(root)) + + assert.equal(packageVersion(root, 'db'), '1.0.0-rc.1') + assert.equal(packageVersion(root, 'react-db'), '1.0.0-rc.1') + assert.equal(packageVersion(root, 'query-db-collection'), '1.4.6') + assert.equal(packageVersion(root, 'offline-transactions'), '1.1.3') + assert.deepEqual( + JSON.parse(readFileSync(join(root, '.changeset', 'pre.json'))).changesets, + ['first-rc', 'mixed', 'stable-only'], + ) + assert.deepEqual( + JSON.parse(readFileSync(join(root, '.changeset', 'config.json'))).ignore, + [], + ) +}) + +test('a failed first RC restores versions, config, changelogs, and pre-state', async (t) => { + const root = createFirstCandidateWorkspace() + t.after(() => rmSync(root, { recursive: true, force: true })) + addChangeset(root, 'bad-package', { '@tanstack/not-in-workspace': 'patch' }) + const paths = [ + join(root, '.changeset', 'config.json'), + ...['db', 'react-db', 'query-db-collection', 'offline-transactions'].map( + (directory) => join(root, 'packages', directory, 'package.json'), + ), + ] + const originalFiles = paths.map((path) => readFileSync(path, 'utf8')) + + await assert.rejects( + seedFirstCandidate(root, () => enterFixturePreMode(root)), + /not in the workspace/, + ) + assert.deepEqual( + paths.map((path) => readFileSync(path, 'utf8')), + originalFiles, + ) + assert.equal(existsSync(join(root, '.changeset', 'pre.json')), false) + assert.equal(existsSync(join(root, 'packages', 'db', 'CHANGELOG.md')), false) +}) + +test('RC exit retains a dependent bump when a stable package pins db directly', async (t) => { + const root = createRcWorkspace() + t.after(() => rmSync(root, { recursive: true, force: true })) + const path = join(root, 'packages', 'query-db-collection', 'package.json') + const manifest = packageManifest(root, 'query-db-collection') + delete manifest.peerDependencies + manifest.dependencies = { '@tanstack/db': 'workspace:*' } + writeJson(path, manifest) + const preStatePath = join(root, '.changeset', 'pre.json') + writeJson(preStatePath, { + ...JSON.parse(readFileSync(preStatePath, 'utf8')), + mode: 'exit', + }) + + await versionOnRcExit(root) + assert.equal(packageVersion(root, 'db'), '1.0.0') + assert.equal(packageVersion(root, 'query-db-collection'), '1.4.6') +}) + +test('RC exit applies only pending stable changesets after a consumed minor', async (t) => { + const root = createRcWorkspace() + t.after(() => rmSync(root, { recursive: true, force: true })) + addChangeset(root, 'consumed-minor', { + '@tanstack/query-db-collection': 'minor', + }) + await versionDuringRc(root) + assert.equal(packageVersion(root, 'query-db-collection'), '1.5.0') + + addChangeset(root, 'pending-patch', { + '@tanstack/query-db-collection': 'patch', + }) + const preStatePath = join(root, '.changeset', 'pre.json') + writeJson(preStatePath, { + ...JSON.parse(readFileSync(preStatePath, 'utf8')), + mode: 'exit', + }) + await versionOnRcExit(root) + assert.equal(packageVersion(root, 'query-db-collection'), '1.5.1') + assert.equal(existsSync(join(root, '.changeset', 'consumed-minor.md')), false) + assert.equal(existsSync(join(root, '.changeset', 'pending-patch.md')), false) +}) + test('RC version numbers follow the Changeset release type', () => { const version = (oldVersion, type, newVersion = '1.0.0-rc.2') => versionForRcRelease({ @@ -163,11 +323,14 @@ test('RC versioning consumes Changesets while preserving stable and private pack await versionDuringRc(root) assert.equal(packageVersion(root, 'db'), '1.0.0-rc.1.2') assert.equal(packageVersion(root, 'react-db'), '1.0.0-rc.1.2') + assert.equal(packageVersion(root, 'query-db-collection'), '1.4.6') + assert.equal(packageVersion(root, 'offline-transactions'), '1.1.3') addChangeset(root, 'patch-three', { '@tanstack/db': 'patch' }) addChangeset(root, 'minor-one', { '@tanstack/db': 'minor' }) await versionDuringRc(root) assert.equal(packageVersion(root, 'db'), '1.0.0-rc.2') + assert.equal(packageVersion(root, 'query-db-collection'), '1.4.6') addChangeset(root, 'patch-four', { '@tanstack/db': 'patch' }) await versionDuringRc(root) From 509b37880b89b6dc0f2fe53634b11abad307d8c6 Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 16 Oct 2026 14:01:57 -0700 Subject: [PATCH 12/13] fix: order offline transaction peer dependencies --- packages/offline-transactions/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/offline-transactions/package.json b/packages/offline-transactions/package.json index 7aa2315342..ac6e25a448 100644 --- a/packages/offline-transactions/package.json +++ b/packages/offline-transactions/package.json @@ -60,8 +60,8 @@ "skills" ], "peerDependencies": { - "@tanstack/db": "^0.15.0 || ^1.0.0-rc.0", "@react-native-community/netinfo": ">=11.0.0", + "@tanstack/db": "^0.15.0 || ^1.0.0-rc.0", "react-native": ">=0.70.0" }, "peerDependenciesMeta": { From fa5a48a6d326edef2ba7c8b45e9a8d08ffe65305 Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 16 Oct 2026 14:11:17 -0700 Subject: [PATCH 13/13] test: migrate merged ordered-query oracles to RC APIs --- .../db/tests/index-suggestion-oracle.test.ts | 2 +- .../ordered-lifecycle-oracle.property.test.ts | 23 +++++++++++-------- .../ordered-work-oracle.property.test.ts | 6 ++--- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/db/tests/index-suggestion-oracle.test.ts b/packages/db/tests/index-suggestion-oracle.test.ts index c6a0ae7408..3758389c3c 100644 --- a/packages/db/tests/index-suggestion-oracle.test.ts +++ b/packages/db/tests/index-suggestion-oracle.test.ts @@ -545,7 +545,7 @@ describe(`bounded orderBy index diagnostics`, () => { query = createLiveQueryCollection((q) => q .from({ row: source }) - .orderBy(({ row }) => row.$synced) + .orderBy(({ row }) => row.$hasPendingWrites) .limit(1), ) await query.preload() diff --git a/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts b/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts index 3914e394bc..4609e68b6a 100644 --- a/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts +++ b/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts @@ -1845,7 +1845,7 @@ async function observeLaterFallback( let markerSync!: MarkerSync let markerCommitImmediate = false let blockerStarted = false - let blockerPersistence: Promise | undefined + let blockerSettlement: Promise | undefined let receiptPending = false let fallbackEntry: { rows: Array; status: string } | undefined @@ -1943,10 +1943,13 @@ async function observeLaterFallback( ) { // The page's own receipt applied. Only the later full-source // transaction waits behind this unrelated optimistic update. - blockerPersistence = rootCollection.update(1, (draft) => { - draft.note = `pending` - }).isPersisted.promise - void blockerPersistence.catch(() => {}) + const blocker = rootCollection + .update(1, (draft) => { + draft.note = `pending` + }) + .when(`settled`) + blockerSettlement = blocker + void blocker.catch(() => {}) } } if (isFullSource) { @@ -2223,14 +2226,14 @@ async function observeLaterFallback( blockerGate.resolve() providerGate.resolve() childGate.resolve() - // A persistence failure is a distinct cleanup diagnostic if the public + // A settlement failure is a distinct cleanup diagnostic if the public // comparison already failed. Bound the wait so cleanup still reports it. - const awaitBlockerPersistence = async () => { - if (!blockerPersistence) return + const awaitBlockerSettlement = async () => { + if (!blockerSettlement) return let timeout: ReturnType | undefined try { await Promise.race([ - blockerPersistence, + blockerSettlement, new Promise((_, reject) => { timeout = setTimeout( () => @@ -2246,7 +2249,7 @@ async function observeLaterFallback( await finishOracleCleanup( primaryFailure, [ - awaitBlockerPersistence, + awaitBlockerSettlement, () => observer?.unsubscribe(), () => unsubscribeRootLoading?.(), () => unsubscribeLiveLoading?.(), diff --git a/packages/db/tests/query/ordered-work-oracle.property.test.ts b/packages/db/tests/query/ordered-work-oracle.property.test.ts index 5607ee44b3..a4b289f14f 100644 --- a/packages/db/tests/query/ordered-work-oracle.property.test.ts +++ b/packages/db/tests/query/ordered-work-oracle.property.test.ts @@ -6682,7 +6682,7 @@ async function checkOptimisticWindow( else if (shape !== `source`) draft.link = -draft.id }) let settlementOutcome: `fulfilled` | `rejected` = `fulfilled` - settled = transaction.isPersisted.promise.then( + settled = transaction.when(`settled`).then( () => undefined, () => { settlementOutcome = `rejected` @@ -6778,7 +6778,7 @@ async function checkOptimisticWindow( .map(copyOptimisticWindowPublication) await awaitOptimisticCheckpoint( `marker restoration`, - restoration.isPersisted.promise, + restoration.when(`settled`), ) } @@ -7769,7 +7769,7 @@ describe(`eager indexed ordered windows`, () => { ]) if (outcome === `acknowledge`) persistence.resolve() else persistence.reject(new Error(`rollback`)) - await transaction.isPersisted.promise.catch(() => undefined) + await transaction.when(`settled`).catch(() => undefined) expect(ids(), `settled installed rows`).toEqual( outcome === `acknowledge` ? expected : initial, )