From eefee1df5a0514b4cce750d85a47549eda248ada Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Nov 2025 01:11:23 +0000 Subject: [PATCH 1/7] fix(svelte-db): Remove flushSync from effect to support async compiler mode Fixes #744 The flushSync() call inside the onFirstReady callback was breaking Svelte 5's async compiler mode. The async compiler enforces a rule that flushSync cannot be called inside effects, as documented at svelte.dev/e/flush_sync_in_effect. The fix removes the flushSync call and updates status directly. Svelte's reactivity system handles the update automatically without needing synchronous flushing. This matches the pattern used in Vue's implementation. Changes: - Removed flushSync() wrapper from onFirstReady callback - Removed unused flushSync import - Updated comment to explain why flushSync cannot be used - All 23 existing tests pass, confirming no regression This fix is backward compatible: - For users WITHOUT async mode (current default): Works as before - For users WITH async mode: Now works instead of throwing error - Future-proof: async mode will be default in Svelte 6 --- packages/svelte-db/src/useLiveQuery.svelte.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index c9ae8d474..d769d9237 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line import/no-duplicates -- See https://github.com/un-ts/eslint-plugin-import-x/issues/308 -import { flushSync, untrack } from "svelte" +import { untrack } from "svelte" // eslint-disable-next-line import/no-duplicates -- See https://github.com/un-ts/eslint-plugin-import-x/issues/308 import { SvelteMap } from "svelte/reactivity" import { createLiveQueryCollection } from "@tanstack/db" @@ -353,10 +353,9 @@ export function useLiveQuery( // Listen for the first ready event to catch status transitions // that might not trigger change events (fixes async status transition bug) currentCollection.onFirstReady(() => { - // Use flushSync to ensure Svelte reactivity updates properly - flushSync(() => { - status = currentCollection.status - }) + // Update status directly - Svelte's reactivity system handles the update automatically + // Note: We cannot use flushSync here as it's disallowed inside effects in async mode + status = currentCollection.status }) // Subscribe to collection changes with granular updates From d4df3276b4a347b3ffe4fc14f76c20022163f519 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Nov 2025 01:13:03 +0000 Subject: [PATCH 2/7] chore: Add package-lock.json to .gitignore (project uses pnpm) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 528e53f21..9e71f3da7 100644 --- a/.gitignore +++ b/.gitignore @@ -151,3 +151,4 @@ tasks/ .output .tanstack .claude +package-lock.json From 2494fc8e1dc2f64dd3aa8e2e9ba2726264a72262 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Nov 2025 01:15:56 +0000 Subject: [PATCH 3/7] chore: Add changeset for Svelte async mode fix --- .changeset/fix-svelte-async-mode.md | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .changeset/fix-svelte-async-mode.md diff --git a/.changeset/fix-svelte-async-mode.md b/.changeset/fix-svelte-async-mode.md new file mode 100644 index 000000000..6db6a0e98 --- /dev/null +++ b/.changeset/fix-svelte-async-mode.md @@ -0,0 +1,40 @@ +--- +"@tanstack/svelte-db": patch +--- + +Fix flushSync error in Svelte 5 async compiler mode + +Previously, `useLiveQuery` threw an error when Svelte 5's async compiler mode was enabled: + +``` +Uncaught Svelte error: flush_sync_in_effect +Cannot use flushSync inside an effect +``` + +This occurred because `flushSync()` was called inside the `onFirstReady` callback, which executes within a `$effect` block. Svelte 5's async compiler enforces a strict rule that `flushSync()` cannot be called inside effects, as documented at svelte.dev/e/flush_sync_in_effect. + +**The Fix:** + +Removed the unnecessary `flushSync()` call from the `onFirstReady` callback. Svelte 5's reactivity system automatically propagates state changes without needing synchronous flushing. This matches the pattern already used in Vue's implementation. + +**Compatibility:** + +- ✅ For users WITHOUT async mode (current default): Works as before +- ✅ For users WITH async mode: Now works instead of throwing error +- ✅ Future-proof: async mode will be default in Svelte 6 +- ✅ All 23 existing tests pass, confirming no regression + +**How to enable async mode:** + +```javascript +// svelte.config.js +export default { + compilerOptions: { + experimental: { + async: true + } + } +} +``` + +Fixes #744 From 77b6fd49f1de6d466a8074d706fc5dd8aa98df07 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Nov 2025 01:30:05 +0000 Subject: [PATCH 4/7] style: Apply prettier formatting to changeset --- .changeset/fix-svelte-async-mode.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/fix-svelte-async-mode.md b/.changeset/fix-svelte-async-mode.md index 6db6a0e98..0eb4e072f 100644 --- a/.changeset/fix-svelte-async-mode.md +++ b/.changeset/fix-svelte-async-mode.md @@ -31,9 +31,9 @@ Removed the unnecessary `flushSync()` call from the `onFirstReady` callback. Sve export default { compilerOptions: { experimental: { - async: true - } - } + async: true, + }, + }, } ``` From 146d3e60aad8b36ece513a0fe241a6c07b4ae4b7 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Wed, 5 Nov 2025 08:48:13 -0700 Subject: [PATCH 5/7] test(svelte-db): Add reactive status change test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add test to verify that status changes in useLiveQuery properly trigger reactive re-execution in Svelte effects. This validates that accessing query.isReady/isLoading in component templates will automatically re-render when status changes from loading to ready. The test uses $effect() to track execution counts, confirming that Svelte's $state reactivity system works correctly with the status updates that occur via the onFirstReady callback (which doesn't use flushSync in async compiler mode). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../tests/useLiveQuery.svelte.test.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts index cd48a248a..c7ad28cc1 100644 --- a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts +++ b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts @@ -1346,6 +1346,91 @@ describe(`Query Collections`, () => { expect(query.status).toBe(`ready`) }) }) + + it(`should reactively trigger effects when status changes`, () => { + let beginFn: (() => void) | undefined + let commitFn: (() => void) | undefined + let markReadyFn: (() => void) | undefined + + const collection = createCollection({ + id: `reactive-status-test`, + getKey: (person: Person) => person.id, + startSync: false, + sync: { + sync: ({ begin, commit, markReady }) => { + beginFn = begin + commitFn = commit + markReadyFn = markReady + }, + }, + onInsert: () => Promise.resolve(), + onUpdate: () => Promise.resolve(), + onDelete: () => Promise.resolve(), + }) + + cleanup = $effect.root(() => { + const query = useLiveQuery((q) => + q + .from({ collection }) + .where(({ collection: c }) => gt(c.age, 30)) + .select(({ collection: c }) => ({ + id: c.id, + name: c.name, + })) + ) + + let readyEffectCount = 0 + let loadingEffectCount = 0 + let lastReadyValue: boolean | undefined + let lastLoadingValue: boolean | undefined + + // This effect should re-run whenever query.isReady changes + $effect(() => { + readyEffectCount++ + lastReadyValue = query.isReady + }) + + // This effect should re-run whenever query.isLoading changes + $effect(() => { + loadingEffectCount++ + lastLoadingValue = query.isLoading + }) + + flushSync() + + // Initial execution + expect(readyEffectCount).toBe(1) + expect(loadingEffectCount).toBe(1) + expect(lastReadyValue).toBe(false) + expect(lastLoadingValue).toBe(true) + + // Start sync and mark ready + collection.preload() + if (beginFn && commitFn && markReadyFn) { + beginFn() + commitFn() + markReadyFn() + } + + // Insert data + collection.insert({ + id: `1`, + name: `John Doe`, + age: 35, + email: `john.doe@example.com`, + isActive: true, + team: `team1`, + }) + + flushSync() + + // Effects should have re-executed due to reactive status change + expect(readyEffectCount).toBeGreaterThan(1) + expect(loadingEffectCount).toBeGreaterThan(1) + expect(lastReadyValue).toBe(true) + expect(lastLoadingValue).toBe(false) + }) + }) }) it(`should accept config object with pre-built QueryBuilder instance`, async () => { From 36f8e45e721040c2aa36ec43f9ab6111fb3484a4 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Wed, 5 Nov 2025 09:11:12 -0700 Subject: [PATCH 6/7] docs: Regenerate API documentation with correct casing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regenerate docs after adding reactive status change test. All files now use correct PascalCase naming to match main branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/collections/local-only-collection.md | 311 ++++ docs/collections/local-storage-collection.md | 299 ++++ docs/collections/powersync-collection.md | 475 +++++++ docs/collections/trailbase-collection.md | 226 +++ docs/config.json | 12 + .../reference/functions/injectLiveQuery.md | 120 ++ docs/framework/angular/reference/index.md | 14 + .../interfaces/InjectLiveQueryResult.md | 134 ++ .../functions/useLiveInfiniteQuery.md | 217 +++ .../react/reference/functions/useLiveQuery.md | 1251 +++++++++++++++++ .../reference/functions/usePacedMutations.md | 131 ++ .../react/reference/functions/uselivequery.md | 384 ----- docs/framework/react/reference/index.md | 12 +- .../UseLiveInfiniteQueryConfig.md | 70 + .../UseLiveInfiniteQueryReturn.md | 60 + .../type-aliases/UseLiveQueryStatus.md | 12 + .../{uselivequery.md => useLiveQuery.md} | 34 +- docs/framework/solid/reference/index.md | 4 +- .../{uselivequery.md => useLiveQuery.md} | 40 +- docs/framework/vue/reference/index.md | 8 +- ...vequeryreturn.md => UseLiveQueryReturn.md} | 29 +- ...md => UseLiveQueryReturnWithCollection.md} | 34 +- docs/guides/collection-options-creator.md | 119 +- docs/guides/error-handling.md | 286 +++- docs/guides/live-queries.md | 175 +++ docs/guides/mutations.md | 254 +++- docs/guides/schemas.md | 1098 +++++++++++++++ docs/overview.md | 417 +----- .../namespaces/IR/classes/Aggregate.md | 104 ++ .../namespaces/IR/classes/CollectionRef.md | 98 ++ .../@tanstack/namespaces/IR/classes/Func.md | 104 ++ .../namespaces/IR/classes/PropRef.md | 90 ++ .../namespaces/IR/classes/QueryRef.md | 98 ++ .../@tanstack/namespaces/IR/classes/Value.md | 90 ++ .../IR/functions/createResidualWhere.md | 24 + .../namespaces/IR/functions/followRef.md | 47 + .../IR/functions/getHavingExpression.md | 28 + .../IR/functions/getWhereExpression.md | 24 + .../IR/functions/isExpressionLike.md | 25 + .../IR/functions/isResidualWhere.md | 24 + .../@tanstack/namespaces/IR/index.md | 44 + .../namespaces/IR/interfaces/JoinClause.md | 50 + .../namespaces/IR/interfaces/QueryIR.md | 178 +++ .../IR/type-aliases/BasicExpression.md | 21 + .../namespaces/IR/type-aliases/From.md | 14 + .../namespaces/IR/type-aliases/GroupBy.md | 12 + .../namespaces/IR/type-aliases/Having.md | 12 + .../namespaces/IR/type-aliases/Join.md | 12 + .../namespaces/IR/type-aliases/Limit.md | 12 + .../namespaces/IR/type-aliases/Offset.md | 12 + .../namespaces/IR/type-aliases/OrderBy.md | 12 + .../IR/type-aliases/OrderByClause.md | 32 + .../IR/type-aliases/OrderByDirection.md | 12 + .../namespaces/IR/type-aliases/Select.md | 21 + .../namespaces/IR/type-aliases/Where.md | 17 + .../AggregateFunctionNotInSelectError.md | 220 +++ .../classes/AggregateNotSupportedError.md | 216 +++ docs/reference/classes/BTreeIndex.md | 848 +++++++++++ docs/reference/classes/BaseIndex.md | 760 ++++++++++ ...asequerybuilder.md => BaseQueryBuilder.md} | 210 +-- .../CannotCombineEmptyExpressionListError.md | 214 +++ .../classes/CollectionConfigurationError.md | 227 +++ .../{collectionimpl.md => CollectionImpl.md} | 659 +++++---- .../classes/CollectionInErrorStateError.md | 224 +++ .../classes/CollectionInputNotFoundError.md | 234 +++ .../classes/CollectionIsInErrorStateError.md | 214 +++ .../classes/CollectionOperationError.md | 232 +++ .../classes/CollectionRequiresConfigError.md | 214 +++ .../CollectionRequiresSyncConfigError.md | 214 +++ .../reference/classes/CollectionStateError.md | 227 +++ .../classes/DeleteKeyNotFoundError.md | 220 +++ .../classes/DistinctRequiresSelectError.md | 214 +++ .../classes/DuplicateAliasInSubqueryError.md | 228 +++ .../classes/DuplicateDbInstanceError.md | 214 +++ docs/reference/classes/DuplicateKeyError.md | 220 +++ .../classes/DuplicateKeySyncError.md | 224 +++ .../classes/EmptyReferencePathError.md | 214 +++ docs/reference/classes/GroupByError.md | 227 +++ .../classes/HavingRequiresGroupByError.md | 214 +++ .../classes/{indexproxy.md => IndexProxy.md} | 60 +- .../InvalidCollectionStatusTransitionError.md | 231 +++ .../reference/classes/InvalidJoinCondition.md | 214 +++ .../InvalidJoinConditionLeftSourceError.md | 220 +++ .../InvalidJoinConditionRightSourceError.md | 220 +++ .../InvalidJoinConditionSameSourceError.md | 220 +++ ...InvalidJoinConditionSourceMismatchError.md | 214 +++ docs/reference/classes/InvalidSchemaError.md | 214 +++ docs/reference/classes/InvalidSourceError.md | 220 +++ .../classes/InvalidStorageDataFormatError.md | 224 +++ .../InvalidStorageObjectFormatError.md | 220 +++ .../classes/JoinCollectionNotFoundError.md | 220 +++ .../JoinConditionMustBeEqualityError.md | 214 +++ docs/reference/classes/JoinError.md | 230 +++ .../classes/KeyUpdateNotAllowedError.md | 224 +++ ...azyindexwrapper.md => LazyIndexWrapper.md} | 38 +- .../classes/LimitOffsetRequireOrderByError.md | 214 +++ .../classes/LocalStorageCollectionError.md | 226 +++ .../classes/MissingAliasInputsError.md | 223 +++ .../classes/MissingDeleteHandlerError.md | 214 +++ docs/reference/classes/MissingHandlerError.md | 226 +++ .../classes/MissingInsertHandlerError.md | 214 +++ .../classes/MissingMutationFunctionError.md | 214 +++ .../classes/MissingUpdateArgumentError.md | 214 +++ .../classes/MissingUpdateHandlerError.md | 214 +++ .../classes/NegativeActiveSubscribersError.md | 214 +++ .../classes/NoKeysPassedToDeleteError.md | 214 +++ .../classes/NoKeysPassedToUpdateError.md | 214 +++ .../NoPendingSyncTransactionCommitError.md | 214 +++ .../NoPendingSyncTransactionWriteError.md | 214 +++ ...NonAggregateExpressionNotInGroupByError.md | 220 +++ docs/reference/classes/NonRetriableError.md | 220 +++ .../classes/OnMutateMustBeSynchronousError.md | 214 +++ .../classes/OnlyOneSourceAllowedError.md | 220 +++ docs/reference/classes/QueryBuilderError.md | 228 +++ .../classes/QueryCompilationError.md | 237 ++++ .../classes/QueryMustHaveFromClauseError.md | 214 +++ docs/reference/classes/QueryOptimizerError.md | 225 +++ .../classes/SchemaMustBeSynchronousError.md | 214 +++ .../classes/SchemaValidationError.md | 251 ++++ docs/reference/classes/SerializationError.md | 224 +++ .../classes/SetWindowRequiresOrderByError.md | 216 +++ .../classes/{sortedmap.md => SortedMap.md} | 40 +- docs/reference/classes/StorageError.md | 225 +++ .../classes/StorageKeyRequiredError.md | 214 +++ .../SubQueryMustHaveFromClauseError.md | 220 +++ .../classes/SubscriptionNotFoundError.md | 239 ++++ docs/reference/classes/SyncCleanupError.md | 224 +++ .../SyncTransactionAlreadyCommittedError.md | 214 +++ ...ncTransactionAlreadyCommittedWriteError.md | 214 +++ docs/reference/classes/TanStackDBError.md | 254 ++++ ...ransactionAlreadyCompletedRollbackError.md | 214 +++ docs/reference/classes/TransactionError.md | 232 +++ .../TransactionNotPendingCommitError.md | 214 +++ .../TransactionNotPendingMutateError.md | 214 +++ docs/reference/classes/UndefinedKeyError.md | 220 +++ .../classes/UnknownExpressionTypeError.md | 220 +++ .../reference/classes/UnknownFunctionError.md | 220 +++ .../UnknownHavingExpressionTypeError.md | 220 +++ .../UnsupportedAggregateFunctionError.md | 220 +++ .../classes/UnsupportedFromTypeError.md | 220 +++ .../classes/UnsupportedJoinSourceTypeError.md | 220 +++ .../classes/UnsupportedJoinTypeError.md | 220 +++ .../classes/UpdateKeyNotFoundError.md | 220 +++ .../classes/WhereClauseConversionError.md | 226 +++ .../aggregatefunctionnotinselecterror.md | 172 --- docs/reference/classes/btreeindex.md | 625 -------- .../cannotcombineemptyexpressionlisterror.md | 166 --- .../classes/collectionconfigurationerror.md | 179 --- .../classes/collectioninerrorstateerror.md | 176 --- .../classes/collectioninputnotfounderror.md | 172 --- .../classes/collectionisinerrorstateerror.md | 166 --- .../classes/collectionoperationerror.md | 184 --- .../classes/collectionrequiresconfigerror.md | 166 --- .../collectionrequiressyncconfigerror.md | 166 --- .../reference/classes/collectionstateerror.md | 179 --- .../classes/deletekeynotfounderror.md | 172 --- .../classes/distinctrequiresselecterror.md | 166 --- docs/reference/classes/duplicatekeyerror.md | 172 --- .../classes/duplicatekeysyncerror.md | 176 --- .../classes/emptyreferencepatherror.md | 166 --- docs/reference/classes/groupbyerror.md | 179 --- .../classes/havingrequiresgroupbyerror.md | 166 --- .../invalidcollectionstatustransitionerror.md | 183 --- .../invalidjoinconditionsametableerror.md | 172 --- .../invalidjoinconditiontablemismatcherror.md | 176 --- .../invalidjoinconditionwrongtableserror.md | 188 --- docs/reference/classes/invalidschemaerror.md | 166 --- docs/reference/classes/invalidsourceerror.md | 166 --- .../classes/invalidstoragedataformaterror.md | 176 --- .../invalidstorageobjectformaterror.md | 172 --- .../joinconditionmustbeequalityerror.md | 166 --- docs/reference/classes/joinerror.md | 180 --- .../classes/keyupdatenotallowederror.md | 176 --- .../classes/limitoffsetrequireorderbyerror.md | 166 --- .../classes/localstoragecollectionerror.md | 180 --- .../classes/missingdeletehandlererror.md | 166 --- docs/reference/classes/missinghandlererror.md | 178 --- .../classes/missinginserthandlererror.md | 166 --- .../classes/missingmutationfunctionerror.md | 166 --- .../classes/missingupdateargumenterror.md | 166 --- .../classes/missingupdatehandlererror.md | 166 --- .../classes/negativeactivesubscriberserror.md | 166 --- .../classes/nokeyspassedtodeleteerror.md | 166 --- .../classes/nokeyspassedtoupdateerror.md | 166 --- ...nonaggregateexpressionnotingroupbyerror.md | 172 --- docs/reference/classes/nonretriableerror.md | 172 --- .../nopendingsynctransactioncommiterror.md | 166 --- .../nopendingsynctransactionwriteerror.md | 166 --- .../classes/nostorageavailableerror.md | 166 --- .../classes/nostorageeventapierror.md | 166 --- .../classes/onlyonesourceallowederror.md | 172 --- docs/reference/classes/querybuildererror.md | 180 --- .../classes/querycompilationerror.md | 183 --- .../classes/querymusthavefromclauseerror.md | 166 --- docs/reference/classes/queryoptimizererror.md | 176 --- .../classes/schemamustbesynchronouserror.md | 166 --- .../classes/schemavalidationerror.md | 203 --- docs/reference/classes/serializationerror.md | 176 --- docs/reference/classes/storageerror.md | 177 --- .../classes/storagekeyrequirederror.md | 166 --- .../subquerymusthavefromclauseerror.md | 172 --- docs/reference/classes/synccleanuperror.md | 176 --- .../synctransactionalreadycommittederror.md | 166 --- ...nctransactionalreadycommittedwriteerror.md | 166 --- docs/reference/classes/tanstackdberror.md | 205 --- ...ransactionalreadycompletedrollbackerror.md | 166 --- docs/reference/classes/transactionerror.md | 183 --- .../transactionnotpendingcommiterror.md | 166 --- .../transactionnotpendingmutateerror.md | 166 --- docs/reference/classes/undefinedkeyerror.md | 172 --- .../classes/unknownexpressiontypeerror.md | 172 --- .../reference/classes/unknownfunctionerror.md | 172 --- .../unknownhavingexpressiontypeerror.md | 172 --- .../unsupportedaggregatefunctionerror.md | 172 --- .../classes/unsupportedfromtypeerror.md | 172 --- .../classes/unsupportedjoinsourcetypeerror.md | 172 --- .../classes/unsupportedjointypeerror.md | 172 --- .../classes/updatekeynotfounderror.md | 172 --- .../classes/ElectricDBCollectionError.md | 247 ++++ .../classes/ExpectedNumberInAwaitTxIdError.md | 224 +++ .../classes/StreamAbortedError.md | 220 +++ .../classes/TimeoutWaitingForMatchError.md | 220 +++ .../classes/TimeoutWaitingForTxIdError.md | 224 +++ .../classes/electricdbcollectionerror.md | 196 --- ...lectricdeletehandlermustreturntxiderror.md | 166 --- ...lectricinserthandlermustreturntxiderror.md | 166 --- ...lectricupdatehandlermustreturntxiderror.md | 166 --- .../classes/expectednumberinawaittxiderror.md | 172 --- .../classes/timeoutwaitingfortxiderror.md | 172 --- .../functions/electricCollectionOptions.md | 70 + .../functions/electriccollectionoptions.md | 122 -- .../reference/electric-db-collection/index.md | 22 +- .../interfaces/ElectricCollectionConfig.md | 208 +++ .../interfaces/ElectricCollectionUtils.md | 46 + .../interfaces/electriccollectionconfig.md | 338 ----- .../interfaces/electriccollectionutils.md | 32 - .../type-aliases/AwaitTxIdFn.md | 28 + .../type-aliases/{txid.md => Txid.md} | 4 +- docs/reference/functions/add.md | 22 +- docs/reference/functions/and.md | 14 +- docs/reference/functions/avg.md | 16 +- docs/reference/functions/coalesce.md | 8 +- docs/reference/functions/compileQuery.md | 90 ++ docs/reference/functions/compilequery.md | 52 - docs/reference/functions/concat.md | 8 +- docs/reference/functions/count.md | 8 +- ...angeproxy.md => createArrayChangeProxy.md} | 10 +- ...atechangeproxy.md => createChangeProxy.md} | 10 +- docs/reference/functions/createCollection.md | 442 ++++++ ...ection.md => createLiveQueryCollection.md} | 43 +- ...ticaction.md => createOptimisticAction.md} | 33 +- .../functions/createPacedMutations.md | 98 ++ ...atetransaction.md => createTransaction.md} | 14 +- docs/reference/functions/createcollection.md | 119 -- docs/reference/functions/debounceStrategy.md | 46 + docs/reference/functions/eq.md | 42 +- ...transaction.md => getActiveTransaction.md} | 10 +- docs/reference/functions/gt.md | 42 +- docs/reference/functions/gte.md | 42 +- docs/reference/functions/ilike.md | 12 +- docs/reference/functions/inArray.md | 26 + docs/reference/functions/inarray.md | 28 - docs/reference/functions/isNull.md | 22 + docs/reference/functions/isUndefined.md | 22 + docs/reference/functions/length.md | 16 +- docs/reference/functions/like.md | 12 +- ...tions.md => liveQueryCollectionOptions.md} | 20 +- .../functions/localOnlyCollectionOptions.md | 230 +++ .../localStorageCollectionOptions.md | 236 ++++ .../functions/localonlycollectionoptions.md | 198 --- .../localstoragecollectionoptions.md | 195 --- docs/reference/functions/lower.md | 16 +- docs/reference/functions/lt.md | 42 +- docs/reference/functions/lte.md | 42 +- docs/reference/functions/max.md | 16 +- docs/reference/functions/min.md | 16 +- docs/reference/functions/not.md | 8 +- docs/reference/functions/or.md | 14 +- docs/reference/functions/queueStrategy.md | 63 + docs/reference/functions/sum.md | 16 +- docs/reference/functions/throttleStrategy.md | 65 + docs/reference/functions/upper.md | 16 +- ...tracking.md => withArrayChangeTracking.md} | 10 +- ...hangetracking.md => withChangeTracking.md} | 10 +- docs/reference/index.md | 375 ++--- ...eeindexoptions.md => BTreeIndexOptions.md} | 16 +- ...ctionconfig.md => BaseCollectionConfig.md} | 103 +- docs/reference/interfaces/BaseStrategy.md | 83 ++ docs/reference/interfaces/ChangeMessage.md | 72 + .../{collection.md => Collection.md} | 752 +++++----- docs/reference/interfaces/CollectionConfig.md | 465 ++++++ docs/reference/interfaces/Context.md | 99 ++ ...s.md => CreateOptimisticActionsOptions.md} | 30 +- .../CurrentStateAsChangesOptions.md | 52 + docs/reference/interfaces/DebounceStrategy.md | 97 ++ .../interfaces/DebounceStrategyOptions.md | 47 + .../IndexInterface.md} | 361 +++-- .../{indexoptions.md => IndexOptions.md} | 6 +- .../{indexstats.md => IndexStats.md} | 12 +- docs/reference/interfaces/InsertConfig.md | 30 + .../interfaces/LiveQueryCollectionConfig.md | 172 +++ .../interfaces/LocalOnlyCollectionConfig.md | 442 ++++++ .../interfaces/LocalOnlyCollectionUtils.md | 62 + .../LocalStorageCollectionConfig.md | 510 +++++++ .../interfaces/LocalStorageCollectionUtils.md | 82 ++ docs/reference/interfaces/OperationConfig.md | 30 + .../interfaces/OptimisticChangeMessage.md | 98 ++ .../interfaces/PacedMutationsConfig.md | 81 ++ docs/reference/interfaces/Parser.md | 48 + docs/reference/interfaces/PendingMutation.md | 157 +++ docs/reference/interfaces/QueueStrategy.md | 99 ++ .../interfaces/QueueStrategyOptions.md | 59 + ...gequeryoptions.md => RangeQueryOptions.md} | 12 +- .../interfaces/SubscribeChangesOptions.md | 34 + .../SubscribeChangesSnapshotOptions.md | 48 + docs/reference/interfaces/Subscription.md | 280 ++++ .../SubscriptionStatusChangeEvent.md | 50 + .../interfaces/SubscriptionStatusEvent.md | 56 + .../SubscriptionUnsubscribedEvent.md | 30 + docs/reference/interfaces/SyncConfig.md | 104 ++ docs/reference/interfaces/ThrottleStrategy.md | 97 ++ .../interfaces/ThrottleStrategyOptions.md | 47 + .../Transaction.md} | 108 +- .../reference/interfaces/TransactionConfig.md | 58 + docs/reference/interfaces/changemessage.md | 70 - docs/reference/interfaces/context.md | 70 - .../currentstateaschangesoptions.md | 50 - docs/reference/interfaces/insertconfig.md | 32 - .../interfaces/livequerycollectionconfig.md | 158 --- .../interfaces/localonlycollectionconfig.md | 172 --- .../interfaces/localonlycollectionutils.md | 22 - .../localstoragecollectionconfig.md | 203 --- .../interfaces/localstoragecollectionutils.md | 42 - docs/reference/interfaces/operationconfig.md | 32 - .../interfaces/optimisticchangemessage.md | 98 -- docs/reference/interfaces/pendingmutation.md | 153 -- .../interfaces/subscribechangesoptions.md | 62 - docs/reference/interfaces/syncconfig.md | 93 -- .../reference/interfaces/transactionconfig.md | 58 - .../classes/PowerSyncTransactor.md | 240 ++++ .../functions/powerSyncCollectionOptions.md | 213 +++ .../powersync-db-collection/index.md | 33 + .../BasePowerSyncCollectionConfig.md | 58 + .../ConfigWithArbitraryCollectionTypes.md | 62 + .../type-aliases/ConfigWithSQLiteInputType.md | 33 + .../type-aliases/ConfigWithSQLiteTypes.md | 14 + .../type-aliases/CustomSQLiteSerializer.md | 48 + .../EnhancedPowerSyncCollectionConfig.md | 48 + .../type-aliases/InferPowerSyncOutputType.md | 26 + .../type-aliases/PowerSyncCollectionConfig.md | 51 + .../type-aliases/PowerSyncCollectionMeta.md | 66 + .../type-aliases/PowerSyncCollectionUtils.md | 34 + .../type-aliases/SerializerConfig.md | 77 + .../type-aliases/TransactorOptions.md | 22 + .../variables/DEFAULT_BATCH_SIZE.md | 14 + .../DeleteOperationItemNotFoundError.md | 220 +++ .../classes/DuplicateKeyInBatchError.md | 220 +++ .../classes/GetKeyRequiredError.md | 214 +++ .../classes/InvalidItemStructureError.md | 220 +++ .../classes/InvalidSyncOperationError.md | 220 +++ .../classes/ItemNotFoundError.md | 220 +++ .../classes/MissingKeyFieldError.md | 224 +++ .../classes/QueryClientRequiredError.md | 214 +++ .../classes/QueryCollectionError.md | 252 ++++ .../classes/QueryFnRequiredError.md | 214 +++ .../classes/QueryKeyRequiredError.md | 214 +++ .../classes/SyncNotInitializedError.md | 214 +++ .../classes/UnknownOperationTypeError.md | 220 +++ .../UpdateOperationItemNotFoundError.md | 220 +++ .../classes/getkeyrequirederror.md | 166 --- .../classes/queryclientrequirederror.md | 166 --- .../classes/querycollectionerror.md | 195 --- .../classes/queryfnrequirederror.md | 166 --- .../classes/querykeyrequirederror.md | 166 --- .../functions/queryCollectionOptions.md | 550 ++++++++ .../functions/querycollectionoptions.md | 42 - docs/reference/query-db-collection/index.md | 31 +- .../interfaces/QueryCollectionConfig.md | 196 +++ .../interfaces/QueryCollectionUtils.md | 240 ++++ .../interfaces/querycollectionconfig.md | 427 ------ .../interfaces/querycollectionutils.md | 140 -- .../type-aliases/SyncOperation.md | 42 + .../{changelistener.md => ChangeListener.md} | 14 +- docs/reference/type-aliases/ChangesPayload.md | 18 + docs/reference/type-aliases/CleanupFn.md | 16 + docs/reference/type-aliases/ClearStorageFn.md | 18 + .../CollectionConfigSingleRowOption.md | 31 + ...ollectionstatus.md => CollectionStatus.md} | 8 +- .../type-aliases/DeleteMutationFn.md | 40 + .../type-aliases/DeleteMutationFnParams.md | 46 + docs/reference/type-aliases/{fn.md => Fn.md} | 4 +- docs/reference/type-aliases/GetResult.md | 42 + ...etstoragesizefn.md => GetStorageSizeFn.md} | 4 +- ...ndexconstructor.md => IndexConstructor.md} | 12 +- docs/reference/type-aliases/IndexOperation.md | 14 + .../{indexresolver.md => IndexResolver.md} | 8 +- .../reference/type-aliases/InferResultType.md | 20 + ...nferschemainput.md => InferSchemaInput.md} | 8 +- ...erschemaoutput.md => InferSchemaOutput.md} | 8 +- ...querybuilder.md => InitialQueryBuilder.md} | 4 +- docs/reference/type-aliases/InputRow.md | 14 + .../type-aliases/InsertMutationFn.md | 40 + .../type-aliases/InsertMutationFnParams.md | 46 + ...namespacedrow.md => KeyedNamespacedRow.md} | 4 +- .../{keyedstream.md => KeyedStream.md} | 4 +- .../type-aliases/LiveQueryCollectionUtils.md | 78 + docs/reference/type-aliases/LoadSubsetFn.md | 22 + .../type-aliases/LoadSubsetOptions.md | 68 + .../type-aliases/MaybeSingleResult.md | 24 + docs/reference/type-aliases/MutationFn.md | 28 + .../type-aliases/MutationFnParams.md | 30 + ...dstream.md => NamespacedAndKeyedStream.md} | 4 +- .../{namespacedrow.md => NamespacedRow.md} | 4 +- .../{nonemptyarray.md => NonEmptyArray.md} | 8 +- .../reference/type-aliases/NonSingleResult.md | 22 + docs/reference/type-aliases/OperationType.md | 12 + .../{querybuilder.md => QueryBuilder.md} | 8 +- docs/reference/type-aliases/Ref.md | 39 + ...hanges.md => ResolveTransactionChanges.md} | 12 +- .../{resultstream.md => ResultStream.md} | 4 +- docs/reference/type-aliases/Row.md | 18 + docs/reference/type-aliases/SingleResult.md | 22 + docs/reference/type-aliases/Source.md | 29 + .../{standardschema.md => StandardSchema.md} | 10 +- ...dschemaalias.md => StandardSchemaAlias.md} | 8 +- .../{storageapi.md => StorageApi.md} | 2 - ...{storageeventapi.md => StorageEventApi.md} | 10 +- docs/reference/type-aliases/Strategy.md | 18 + .../reference/type-aliases/StrategyOptions.md | 20 + .../type-aliases/SubscriptionEvents.md | 54 + .../type-aliases/SubscriptionStatus.md | 14 + docs/reference/type-aliases/SyncConfigRes.md | 32 + docs/reference/type-aliases/SyncMode.md | 12 + .../type-aliases/TransactionState.md | 12 + ...tations.md => TransactionWithMutations.md} | 14 +- .../type-aliases/UpdateMutationFn.md | 40 + .../type-aliases/UpdateMutationFnParams.md | 46 + .../{utilsrecord.md => UtilsRecord.md} | 4 +- docs/reference/type-aliases/WritableDeep.md | 18 + docs/reference/type-aliases/changespayload.md | 18 - docs/reference/type-aliases/clearstoragefn.md | 20 - .../type-aliases/deletemutationfn.md | 32 - .../type-aliases/deletemutationfnparams.md | 36 - docs/reference/type-aliases/getresult.md | 18 - docs/reference/type-aliases/indexoperation.md | 16 - docs/reference/type-aliases/inputrow.md | 16 - .../type-aliases/insertmutationfn.md | 32 - .../type-aliases/insertmutationfnparams.md | 36 - docs/reference/type-aliases/mutationfn.md | 28 - .../type-aliases/mutationfnparams.md | 28 - docs/reference/type-aliases/operationtype.md | 14 - docs/reference/type-aliases/ref.md | 18 - .../type-aliases/resolveinsertinput.md | 34 - docs/reference/type-aliases/resolvetype.md | 32 - docs/reference/type-aliases/row.md | 18 - docs/reference/type-aliases/source.md | 22 - .../type-aliases/transactionstate.md | 14 - .../type-aliases/updatemutationfn.md | 32 - .../type-aliases/updatemutationfnparams.md | 36 - .../{indexoperation.md => IndexOperation.md} | 4 +- .../variables/{query.md => Query.md} | 4 +- 461 files changed, 40896 insertions(+), 21033 deletions(-) create mode 100644 docs/collections/local-only-collection.md create mode 100644 docs/collections/local-storage-collection.md create mode 100644 docs/collections/powersync-collection.md create mode 100644 docs/collections/trailbase-collection.md create mode 100644 docs/framework/angular/reference/functions/injectLiveQuery.md create mode 100644 docs/framework/angular/reference/index.md create mode 100644 docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md create mode 100644 docs/framework/react/reference/functions/useLiveInfiniteQuery.md create mode 100644 docs/framework/react/reference/functions/useLiveQuery.md create mode 100644 docs/framework/react/reference/functions/usePacedMutations.md delete mode 100644 docs/framework/react/reference/functions/uselivequery.md create mode 100644 docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryConfig.md create mode 100644 docs/framework/react/reference/type-aliases/UseLiveInfiniteQueryReturn.md create mode 100644 docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md rename docs/framework/solid/reference/functions/{uselivequery.md => useLiveQuery.md} (88%) rename docs/framework/vue/reference/functions/{uselivequery.md => useLiveQuery.md} (84%) rename docs/framework/vue/reference/interfaces/{uselivequeryreturn.md => UseLiveQueryReturn.md} (86%) rename docs/framework/vue/reference/interfaces/{uselivequeryreturnwithcollection.md => UseLiveQueryReturnWithCollection.md} (80%) create mode 100644 docs/guides/schemas.md create mode 100644 docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md create mode 100644 docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md create mode 100644 docs/reference/@tanstack/namespaces/IR/classes/Func.md create mode 100644 docs/reference/@tanstack/namespaces/IR/classes/PropRef.md create mode 100644 docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md create mode 100644 docs/reference/@tanstack/namespaces/IR/classes/Value.md create mode 100644 docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md create mode 100644 docs/reference/@tanstack/namespaces/IR/functions/followRef.md create mode 100644 docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md create mode 100644 docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md create mode 100644 docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md create mode 100644 docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md create mode 100644 docs/reference/@tanstack/namespaces/IR/index.md create mode 100644 docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md create mode 100644 docs/reference/@tanstack/namespaces/IR/interfaces/QueryIR.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/From.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md create mode 100644 docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md create mode 100644 docs/reference/classes/AggregateFunctionNotInSelectError.md create mode 100644 docs/reference/classes/AggregateNotSupportedError.md create mode 100644 docs/reference/classes/BTreeIndex.md create mode 100644 docs/reference/classes/BaseIndex.md rename docs/reference/classes/{basequerybuilder.md => BaseQueryBuilder.md} (51%) create mode 100644 docs/reference/classes/CannotCombineEmptyExpressionListError.md create mode 100644 docs/reference/classes/CollectionConfigurationError.md rename docs/reference/classes/{collectionimpl.md => CollectionImpl.md} (52%) create mode 100644 docs/reference/classes/CollectionInErrorStateError.md create mode 100644 docs/reference/classes/CollectionInputNotFoundError.md create mode 100644 docs/reference/classes/CollectionIsInErrorStateError.md create mode 100644 docs/reference/classes/CollectionOperationError.md create mode 100644 docs/reference/classes/CollectionRequiresConfigError.md create mode 100644 docs/reference/classes/CollectionRequiresSyncConfigError.md create mode 100644 docs/reference/classes/CollectionStateError.md create mode 100644 docs/reference/classes/DeleteKeyNotFoundError.md create mode 100644 docs/reference/classes/DistinctRequiresSelectError.md create mode 100644 docs/reference/classes/DuplicateAliasInSubqueryError.md create mode 100644 docs/reference/classes/DuplicateDbInstanceError.md create mode 100644 docs/reference/classes/DuplicateKeyError.md create mode 100644 docs/reference/classes/DuplicateKeySyncError.md create mode 100644 docs/reference/classes/EmptyReferencePathError.md create mode 100644 docs/reference/classes/GroupByError.md create mode 100644 docs/reference/classes/HavingRequiresGroupByError.md rename docs/reference/classes/{indexproxy.md => IndexProxy.md} (81%) create mode 100644 docs/reference/classes/InvalidCollectionStatusTransitionError.md create mode 100644 docs/reference/classes/InvalidJoinCondition.md create mode 100644 docs/reference/classes/InvalidJoinConditionLeftSourceError.md create mode 100644 docs/reference/classes/InvalidJoinConditionRightSourceError.md create mode 100644 docs/reference/classes/InvalidJoinConditionSameSourceError.md create mode 100644 docs/reference/classes/InvalidJoinConditionSourceMismatchError.md create mode 100644 docs/reference/classes/InvalidSchemaError.md create mode 100644 docs/reference/classes/InvalidSourceError.md create mode 100644 docs/reference/classes/InvalidStorageDataFormatError.md create mode 100644 docs/reference/classes/InvalidStorageObjectFormatError.md create mode 100644 docs/reference/classes/JoinCollectionNotFoundError.md create mode 100644 docs/reference/classes/JoinConditionMustBeEqualityError.md create mode 100644 docs/reference/classes/JoinError.md create mode 100644 docs/reference/classes/KeyUpdateNotAllowedError.md rename docs/reference/classes/{lazyindexwrapper.md => LazyIndexWrapper.md} (74%) create mode 100644 docs/reference/classes/LimitOffsetRequireOrderByError.md create mode 100644 docs/reference/classes/LocalStorageCollectionError.md create mode 100644 docs/reference/classes/MissingAliasInputsError.md create mode 100644 docs/reference/classes/MissingDeleteHandlerError.md create mode 100644 docs/reference/classes/MissingHandlerError.md create mode 100644 docs/reference/classes/MissingInsertHandlerError.md create mode 100644 docs/reference/classes/MissingMutationFunctionError.md create mode 100644 docs/reference/classes/MissingUpdateArgumentError.md create mode 100644 docs/reference/classes/MissingUpdateHandlerError.md create mode 100644 docs/reference/classes/NegativeActiveSubscribersError.md create mode 100644 docs/reference/classes/NoKeysPassedToDeleteError.md create mode 100644 docs/reference/classes/NoKeysPassedToUpdateError.md create mode 100644 docs/reference/classes/NoPendingSyncTransactionCommitError.md create mode 100644 docs/reference/classes/NoPendingSyncTransactionWriteError.md create mode 100644 docs/reference/classes/NonAggregateExpressionNotInGroupByError.md create mode 100644 docs/reference/classes/NonRetriableError.md create mode 100644 docs/reference/classes/OnMutateMustBeSynchronousError.md create mode 100644 docs/reference/classes/OnlyOneSourceAllowedError.md create mode 100644 docs/reference/classes/QueryBuilderError.md create mode 100644 docs/reference/classes/QueryCompilationError.md create mode 100644 docs/reference/classes/QueryMustHaveFromClauseError.md create mode 100644 docs/reference/classes/QueryOptimizerError.md create mode 100644 docs/reference/classes/SchemaMustBeSynchronousError.md create mode 100644 docs/reference/classes/SchemaValidationError.md create mode 100644 docs/reference/classes/SerializationError.md create mode 100644 docs/reference/classes/SetWindowRequiresOrderByError.md rename docs/reference/classes/{sortedmap.md => SortedMap.md} (89%) create mode 100644 docs/reference/classes/StorageError.md create mode 100644 docs/reference/classes/StorageKeyRequiredError.md create mode 100644 docs/reference/classes/SubQueryMustHaveFromClauseError.md create mode 100644 docs/reference/classes/SubscriptionNotFoundError.md create mode 100644 docs/reference/classes/SyncCleanupError.md create mode 100644 docs/reference/classes/SyncTransactionAlreadyCommittedError.md create mode 100644 docs/reference/classes/SyncTransactionAlreadyCommittedWriteError.md create mode 100644 docs/reference/classes/TanStackDBError.md create mode 100644 docs/reference/classes/TransactionAlreadyCompletedRollbackError.md create mode 100644 docs/reference/classes/TransactionError.md create mode 100644 docs/reference/classes/TransactionNotPendingCommitError.md create mode 100644 docs/reference/classes/TransactionNotPendingMutateError.md create mode 100644 docs/reference/classes/UndefinedKeyError.md create mode 100644 docs/reference/classes/UnknownExpressionTypeError.md create mode 100644 docs/reference/classes/UnknownFunctionError.md create mode 100644 docs/reference/classes/UnknownHavingExpressionTypeError.md create mode 100644 docs/reference/classes/UnsupportedAggregateFunctionError.md create mode 100644 docs/reference/classes/UnsupportedFromTypeError.md create mode 100644 docs/reference/classes/UnsupportedJoinSourceTypeError.md create mode 100644 docs/reference/classes/UnsupportedJoinTypeError.md create mode 100644 docs/reference/classes/UpdateKeyNotFoundError.md create mode 100644 docs/reference/classes/WhereClauseConversionError.md delete mode 100644 docs/reference/classes/aggregatefunctionnotinselecterror.md delete mode 100644 docs/reference/classes/btreeindex.md delete mode 100644 docs/reference/classes/cannotcombineemptyexpressionlisterror.md delete mode 100644 docs/reference/classes/collectionconfigurationerror.md delete mode 100644 docs/reference/classes/collectioninerrorstateerror.md delete mode 100644 docs/reference/classes/collectioninputnotfounderror.md delete mode 100644 docs/reference/classes/collectionisinerrorstateerror.md delete mode 100644 docs/reference/classes/collectionoperationerror.md delete mode 100644 docs/reference/classes/collectionrequiresconfigerror.md delete mode 100644 docs/reference/classes/collectionrequiressyncconfigerror.md delete mode 100644 docs/reference/classes/collectionstateerror.md delete mode 100644 docs/reference/classes/deletekeynotfounderror.md delete mode 100644 docs/reference/classes/distinctrequiresselecterror.md delete mode 100644 docs/reference/classes/duplicatekeyerror.md delete mode 100644 docs/reference/classes/duplicatekeysyncerror.md delete mode 100644 docs/reference/classes/emptyreferencepatherror.md delete mode 100644 docs/reference/classes/groupbyerror.md delete mode 100644 docs/reference/classes/havingrequiresgroupbyerror.md delete mode 100644 docs/reference/classes/invalidcollectionstatustransitionerror.md delete mode 100644 docs/reference/classes/invalidjoinconditionsametableerror.md delete mode 100644 docs/reference/classes/invalidjoinconditiontablemismatcherror.md delete mode 100644 docs/reference/classes/invalidjoinconditionwrongtableserror.md delete mode 100644 docs/reference/classes/invalidschemaerror.md delete mode 100644 docs/reference/classes/invalidsourceerror.md delete mode 100644 docs/reference/classes/invalidstoragedataformaterror.md delete mode 100644 docs/reference/classes/invalidstorageobjectformaterror.md delete mode 100644 docs/reference/classes/joinconditionmustbeequalityerror.md delete mode 100644 docs/reference/classes/joinerror.md delete mode 100644 docs/reference/classes/keyupdatenotallowederror.md delete mode 100644 docs/reference/classes/limitoffsetrequireorderbyerror.md delete mode 100644 docs/reference/classes/localstoragecollectionerror.md delete mode 100644 docs/reference/classes/missingdeletehandlererror.md delete mode 100644 docs/reference/classes/missinghandlererror.md delete mode 100644 docs/reference/classes/missinginserthandlererror.md delete mode 100644 docs/reference/classes/missingmutationfunctionerror.md delete mode 100644 docs/reference/classes/missingupdateargumenterror.md delete mode 100644 docs/reference/classes/missingupdatehandlererror.md delete mode 100644 docs/reference/classes/negativeactivesubscriberserror.md delete mode 100644 docs/reference/classes/nokeyspassedtodeleteerror.md delete mode 100644 docs/reference/classes/nokeyspassedtoupdateerror.md delete mode 100644 docs/reference/classes/nonaggregateexpressionnotingroupbyerror.md delete mode 100644 docs/reference/classes/nonretriableerror.md delete mode 100644 docs/reference/classes/nopendingsynctransactioncommiterror.md delete mode 100644 docs/reference/classes/nopendingsynctransactionwriteerror.md delete mode 100644 docs/reference/classes/nostorageavailableerror.md delete mode 100644 docs/reference/classes/nostorageeventapierror.md delete mode 100644 docs/reference/classes/onlyonesourceallowederror.md delete mode 100644 docs/reference/classes/querybuildererror.md delete mode 100644 docs/reference/classes/querycompilationerror.md delete mode 100644 docs/reference/classes/querymusthavefromclauseerror.md delete mode 100644 docs/reference/classes/queryoptimizererror.md delete mode 100644 docs/reference/classes/schemamustbesynchronouserror.md delete mode 100644 docs/reference/classes/schemavalidationerror.md delete mode 100644 docs/reference/classes/serializationerror.md delete mode 100644 docs/reference/classes/storageerror.md delete mode 100644 docs/reference/classes/storagekeyrequirederror.md delete mode 100644 docs/reference/classes/subquerymusthavefromclauseerror.md delete mode 100644 docs/reference/classes/synccleanuperror.md delete mode 100644 docs/reference/classes/synctransactionalreadycommittederror.md delete mode 100644 docs/reference/classes/synctransactionalreadycommittedwriteerror.md delete mode 100644 docs/reference/classes/tanstackdberror.md delete mode 100644 docs/reference/classes/transactionalreadycompletedrollbackerror.md delete mode 100644 docs/reference/classes/transactionerror.md delete mode 100644 docs/reference/classes/transactionnotpendingcommiterror.md delete mode 100644 docs/reference/classes/transactionnotpendingmutateerror.md delete mode 100644 docs/reference/classes/undefinedkeyerror.md delete mode 100644 docs/reference/classes/unknownexpressiontypeerror.md delete mode 100644 docs/reference/classes/unknownfunctionerror.md delete mode 100644 docs/reference/classes/unknownhavingexpressiontypeerror.md delete mode 100644 docs/reference/classes/unsupportedaggregatefunctionerror.md delete mode 100644 docs/reference/classes/unsupportedfromtypeerror.md delete mode 100644 docs/reference/classes/unsupportedjoinsourcetypeerror.md delete mode 100644 docs/reference/classes/unsupportedjointypeerror.md delete mode 100644 docs/reference/classes/updatekeynotfounderror.md create mode 100644 docs/reference/electric-db-collection/classes/ElectricDBCollectionError.md create mode 100644 docs/reference/electric-db-collection/classes/ExpectedNumberInAwaitTxIdError.md create mode 100644 docs/reference/electric-db-collection/classes/StreamAbortedError.md create mode 100644 docs/reference/electric-db-collection/classes/TimeoutWaitingForMatchError.md create mode 100644 docs/reference/electric-db-collection/classes/TimeoutWaitingForTxIdError.md delete mode 100644 docs/reference/electric-db-collection/classes/electricdbcollectionerror.md delete mode 100644 docs/reference/electric-db-collection/classes/electricdeletehandlermustreturntxiderror.md delete mode 100644 docs/reference/electric-db-collection/classes/electricinserthandlermustreturntxiderror.md delete mode 100644 docs/reference/electric-db-collection/classes/electricupdatehandlermustreturntxiderror.md delete mode 100644 docs/reference/electric-db-collection/classes/expectednumberinawaittxiderror.md delete mode 100644 docs/reference/electric-db-collection/classes/timeoutwaitingfortxiderror.md create mode 100644 docs/reference/electric-db-collection/functions/electricCollectionOptions.md delete mode 100644 docs/reference/electric-db-collection/functions/electriccollectionoptions.md create mode 100644 docs/reference/electric-db-collection/interfaces/ElectricCollectionConfig.md create mode 100644 docs/reference/electric-db-collection/interfaces/ElectricCollectionUtils.md delete mode 100644 docs/reference/electric-db-collection/interfaces/electriccollectionconfig.md delete mode 100644 docs/reference/electric-db-collection/interfaces/electriccollectionutils.md create mode 100644 docs/reference/electric-db-collection/type-aliases/AwaitTxIdFn.md rename docs/reference/electric-db-collection/type-aliases/{txid.md => Txid.md} (53%) create mode 100644 docs/reference/functions/compileQuery.md delete mode 100644 docs/reference/functions/compilequery.md rename docs/reference/functions/{createarraychangeproxy.md => createArrayChangeProxy.md} (67%) rename docs/reference/functions/{createchangeproxy.md => createChangeProxy.md} (67%) create mode 100644 docs/reference/functions/createCollection.md rename docs/reference/functions/{createlivequerycollection.md => createLiveQueryCollection.md} (62%) rename docs/reference/functions/{createoptimisticaction.md => createOptimisticAction.md} (61%) create mode 100644 docs/reference/functions/createPacedMutations.md rename docs/reference/functions/{createtransaction.md => createTransaction.md} (74%) delete mode 100644 docs/reference/functions/createcollection.md create mode 100644 docs/reference/functions/debounceStrategy.md rename docs/reference/functions/{getactivetransaction.md => getActiveTransaction.md} (62%) create mode 100644 docs/reference/functions/inArray.md delete mode 100644 docs/reference/functions/inarray.md create mode 100644 docs/reference/functions/isNull.md create mode 100644 docs/reference/functions/isUndefined.md rename docs/reference/functions/{livequerycollectionoptions.md => liveQueryCollectionOptions.md} (54%) create mode 100644 docs/reference/functions/localOnlyCollectionOptions.md create mode 100644 docs/reference/functions/localStorageCollectionOptions.md delete mode 100644 docs/reference/functions/localonlycollectionoptions.md delete mode 100644 docs/reference/functions/localstoragecollectionoptions.md create mode 100644 docs/reference/functions/queueStrategy.md create mode 100644 docs/reference/functions/throttleStrategy.md rename docs/reference/functions/{witharraychangetracking.md => withArrayChangeTracking.md} (71%) rename docs/reference/functions/{withchangetracking.md => withChangeTracking.md} (69%) rename docs/reference/interfaces/{btreeindexoptions.md => BTreeIndexOptions.md} (61%) rename docs/reference/interfaces/{collectionconfig.md => BaseCollectionConfig.md} (64%) create mode 100644 docs/reference/interfaces/BaseStrategy.md create mode 100644 docs/reference/interfaces/ChangeMessage.md rename docs/reference/interfaces/{collection.md => Collection.md} (52%) create mode 100644 docs/reference/interfaces/CollectionConfig.md create mode 100644 docs/reference/interfaces/Context.md rename docs/reference/interfaces/{createoptimisticactionsoptions.md => CreateOptimisticActionsOptions.md} (50%) create mode 100644 docs/reference/interfaces/CurrentStateAsChangesOptions.md create mode 100644 docs/reference/interfaces/DebounceStrategy.md create mode 100644 docs/reference/interfaces/DebounceStrategyOptions.md rename docs/reference/{classes/baseindex.md => interfaces/IndexInterface.md} (57%) rename docs/reference/interfaces/{indexoptions.md => IndexOptions.md} (84%) rename docs/reference/interfaces/{indexstats.md => IndexStats.md} (74%) create mode 100644 docs/reference/interfaces/InsertConfig.md create mode 100644 docs/reference/interfaces/LiveQueryCollectionConfig.md create mode 100644 docs/reference/interfaces/LocalOnlyCollectionConfig.md create mode 100644 docs/reference/interfaces/LocalOnlyCollectionUtils.md create mode 100644 docs/reference/interfaces/LocalStorageCollectionConfig.md create mode 100644 docs/reference/interfaces/LocalStorageCollectionUtils.md create mode 100644 docs/reference/interfaces/OperationConfig.md create mode 100644 docs/reference/interfaces/OptimisticChangeMessage.md create mode 100644 docs/reference/interfaces/PacedMutationsConfig.md create mode 100644 docs/reference/interfaces/Parser.md create mode 100644 docs/reference/interfaces/PendingMutation.md create mode 100644 docs/reference/interfaces/QueueStrategy.md create mode 100644 docs/reference/interfaces/QueueStrategyOptions.md rename docs/reference/interfaces/{rangequeryoptions.md => RangeQueryOptions.md} (79%) create mode 100644 docs/reference/interfaces/SubscribeChangesOptions.md create mode 100644 docs/reference/interfaces/SubscribeChangesSnapshotOptions.md create mode 100644 docs/reference/interfaces/Subscription.md create mode 100644 docs/reference/interfaces/SubscriptionStatusChangeEvent.md create mode 100644 docs/reference/interfaces/SubscriptionStatusEvent.md create mode 100644 docs/reference/interfaces/SubscriptionUnsubscribedEvent.md create mode 100644 docs/reference/interfaces/SyncConfig.md create mode 100644 docs/reference/interfaces/ThrottleStrategy.md create mode 100644 docs/reference/interfaces/ThrottleStrategyOptions.md rename docs/reference/{classes/transaction.md => interfaces/Transaction.md} (55%) create mode 100644 docs/reference/interfaces/TransactionConfig.md delete mode 100644 docs/reference/interfaces/changemessage.md delete mode 100644 docs/reference/interfaces/context.md delete mode 100644 docs/reference/interfaces/currentstateaschangesoptions.md delete mode 100644 docs/reference/interfaces/insertconfig.md delete mode 100644 docs/reference/interfaces/livequerycollectionconfig.md delete mode 100644 docs/reference/interfaces/localonlycollectionconfig.md delete mode 100644 docs/reference/interfaces/localonlycollectionutils.md delete mode 100644 docs/reference/interfaces/localstoragecollectionconfig.md delete mode 100644 docs/reference/interfaces/localstoragecollectionutils.md delete mode 100644 docs/reference/interfaces/operationconfig.md delete mode 100644 docs/reference/interfaces/optimisticchangemessage.md delete mode 100644 docs/reference/interfaces/pendingmutation.md delete mode 100644 docs/reference/interfaces/subscribechangesoptions.md delete mode 100644 docs/reference/interfaces/syncconfig.md delete mode 100644 docs/reference/interfaces/transactionconfig.md create mode 100644 docs/reference/powersync-db-collection/classes/PowerSyncTransactor.md create mode 100644 docs/reference/powersync-db-collection/functions/powerSyncCollectionOptions.md create mode 100644 docs/reference/powersync-db-collection/index.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/BasePowerSyncCollectionConfig.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/ConfigWithArbitraryCollectionTypes.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/ConfigWithSQLiteInputType.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/ConfigWithSQLiteTypes.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/CustomSQLiteSerializer.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/EnhancedPowerSyncCollectionConfig.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/InferPowerSyncOutputType.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/PowerSyncCollectionConfig.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/PowerSyncCollectionMeta.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/PowerSyncCollectionUtils.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/SerializerConfig.md create mode 100644 docs/reference/powersync-db-collection/type-aliases/TransactorOptions.md create mode 100644 docs/reference/powersync-db-collection/variables/DEFAULT_BATCH_SIZE.md create mode 100644 docs/reference/query-db-collection/classes/DeleteOperationItemNotFoundError.md create mode 100644 docs/reference/query-db-collection/classes/DuplicateKeyInBatchError.md create mode 100644 docs/reference/query-db-collection/classes/GetKeyRequiredError.md create mode 100644 docs/reference/query-db-collection/classes/InvalidItemStructureError.md create mode 100644 docs/reference/query-db-collection/classes/InvalidSyncOperationError.md create mode 100644 docs/reference/query-db-collection/classes/ItemNotFoundError.md create mode 100644 docs/reference/query-db-collection/classes/MissingKeyFieldError.md create mode 100644 docs/reference/query-db-collection/classes/QueryClientRequiredError.md create mode 100644 docs/reference/query-db-collection/classes/QueryCollectionError.md create mode 100644 docs/reference/query-db-collection/classes/QueryFnRequiredError.md create mode 100644 docs/reference/query-db-collection/classes/QueryKeyRequiredError.md create mode 100644 docs/reference/query-db-collection/classes/SyncNotInitializedError.md create mode 100644 docs/reference/query-db-collection/classes/UnknownOperationTypeError.md create mode 100644 docs/reference/query-db-collection/classes/UpdateOperationItemNotFoundError.md delete mode 100644 docs/reference/query-db-collection/classes/getkeyrequirederror.md delete mode 100644 docs/reference/query-db-collection/classes/queryclientrequirederror.md delete mode 100644 docs/reference/query-db-collection/classes/querycollectionerror.md delete mode 100644 docs/reference/query-db-collection/classes/queryfnrequirederror.md delete mode 100644 docs/reference/query-db-collection/classes/querykeyrequirederror.md create mode 100644 docs/reference/query-db-collection/functions/queryCollectionOptions.md delete mode 100644 docs/reference/query-db-collection/functions/querycollectionoptions.md create mode 100644 docs/reference/query-db-collection/interfaces/QueryCollectionConfig.md create mode 100644 docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md delete mode 100644 docs/reference/query-db-collection/interfaces/querycollectionconfig.md delete mode 100644 docs/reference/query-db-collection/interfaces/querycollectionutils.md create mode 100644 docs/reference/query-db-collection/type-aliases/SyncOperation.md rename docs/reference/type-aliases/{changelistener.md => ChangeListener.md} (71%) create mode 100644 docs/reference/type-aliases/ChangesPayload.md create mode 100644 docs/reference/type-aliases/CleanupFn.md create mode 100644 docs/reference/type-aliases/ClearStorageFn.md create mode 100644 docs/reference/type-aliases/CollectionConfigSingleRowOption.md rename docs/reference/type-aliases/{collectionstatus.md => CollectionStatus.md} (58%) create mode 100644 docs/reference/type-aliases/DeleteMutationFn.md create mode 100644 docs/reference/type-aliases/DeleteMutationFnParams.md rename docs/reference/type-aliases/{fn.md => Fn.md} (52%) create mode 100644 docs/reference/type-aliases/GetResult.md rename docs/reference/type-aliases/{getstoragesizefn.md => GetStorageSizeFn.md} (50%) rename docs/reference/type-aliases/{indexconstructor.md => IndexConstructor.md} (58%) create mode 100644 docs/reference/type-aliases/IndexOperation.md rename docs/reference/type-aliases/{indexresolver.md => IndexResolver.md} (61%) create mode 100644 docs/reference/type-aliases/InferResultType.md rename docs/reference/type-aliases/{inferschemainput.md => InferSchemaInput.md} (69%) rename docs/reference/type-aliases/{inferschemaoutput.md => InferSchemaOutput.md} (69%) rename docs/reference/type-aliases/{initialquerybuilder.md => InitialQueryBuilder.md} (61%) create mode 100644 docs/reference/type-aliases/InputRow.md create mode 100644 docs/reference/type-aliases/InsertMutationFn.md create mode 100644 docs/reference/type-aliases/InsertMutationFnParams.md rename docs/reference/type-aliases/{keyednamespacedrow.md => KeyedNamespacedRow.md} (59%) rename docs/reference/type-aliases/{keyedstream.md => KeyedStream.md} (53%) create mode 100644 docs/reference/type-aliases/LiveQueryCollectionUtils.md create mode 100644 docs/reference/type-aliases/LoadSubsetFn.md create mode 100644 docs/reference/type-aliases/LoadSubsetOptions.md create mode 100644 docs/reference/type-aliases/MaybeSingleResult.md create mode 100644 docs/reference/type-aliases/MutationFn.md create mode 100644 docs/reference/type-aliases/MutationFnParams.md rename docs/reference/type-aliases/{namespacedandkeyedstream.md => NamespacedAndKeyedStream.md} (64%) rename docs/reference/type-aliases/{namespacedrow.md => NamespacedRow.md} (55%) rename docs/reference/type-aliases/{nonemptyarray.md => NonEmptyArray.md} (50%) create mode 100644 docs/reference/type-aliases/NonSingleResult.md create mode 100644 docs/reference/type-aliases/OperationType.md rename docs/reference/type-aliases/{querybuilder.md => QueryBuilder.md} (55%) create mode 100644 docs/reference/type-aliases/Ref.md rename docs/reference/type-aliases/{resolvetransactionchanges.md => ResolveTransactionChanges.md} (62%) rename docs/reference/type-aliases/{resultstream.md => ResultStream.md} (63%) create mode 100644 docs/reference/type-aliases/Row.md create mode 100644 docs/reference/type-aliases/SingleResult.md create mode 100644 docs/reference/type-aliases/Source.md rename docs/reference/type-aliases/{standardschema.md => StandardSchema.md} (69%) rename docs/reference/type-aliases/{standardschemaalias.md => StandardSchemaAlias.md} (50%) rename docs/reference/type-aliases/{storageapi.md => StorageApi.md} (82%) rename docs/reference/type-aliases/{storageeventapi.md => StorageEventApi.md} (71%) create mode 100644 docs/reference/type-aliases/Strategy.md create mode 100644 docs/reference/type-aliases/StrategyOptions.md create mode 100644 docs/reference/type-aliases/SubscriptionEvents.md create mode 100644 docs/reference/type-aliases/SubscriptionStatus.md create mode 100644 docs/reference/type-aliases/SyncConfigRes.md create mode 100644 docs/reference/type-aliases/SyncMode.md create mode 100644 docs/reference/type-aliases/TransactionState.md rename docs/reference/type-aliases/{transactionwithmutations.md => TransactionWithMutations.md} (53%) create mode 100644 docs/reference/type-aliases/UpdateMutationFn.md create mode 100644 docs/reference/type-aliases/UpdateMutationFnParams.md rename docs/reference/type-aliases/{utilsrecord.md => UtilsRecord.md} (50%) create mode 100644 docs/reference/type-aliases/WritableDeep.md delete mode 100644 docs/reference/type-aliases/changespayload.md delete mode 100644 docs/reference/type-aliases/clearstoragefn.md delete mode 100644 docs/reference/type-aliases/deletemutationfn.md delete mode 100644 docs/reference/type-aliases/deletemutationfnparams.md delete mode 100644 docs/reference/type-aliases/getresult.md delete mode 100644 docs/reference/type-aliases/indexoperation.md delete mode 100644 docs/reference/type-aliases/inputrow.md delete mode 100644 docs/reference/type-aliases/insertmutationfn.md delete mode 100644 docs/reference/type-aliases/insertmutationfnparams.md delete mode 100644 docs/reference/type-aliases/mutationfn.md delete mode 100644 docs/reference/type-aliases/mutationfnparams.md delete mode 100644 docs/reference/type-aliases/operationtype.md delete mode 100644 docs/reference/type-aliases/ref.md delete mode 100644 docs/reference/type-aliases/resolveinsertinput.md delete mode 100644 docs/reference/type-aliases/resolvetype.md delete mode 100644 docs/reference/type-aliases/row.md delete mode 100644 docs/reference/type-aliases/source.md delete mode 100644 docs/reference/type-aliases/transactionstate.md delete mode 100644 docs/reference/type-aliases/updatemutationfn.md delete mode 100644 docs/reference/type-aliases/updatemutationfnparams.md rename docs/reference/variables/{indexoperation.md => IndexOperation.md} (57%) rename docs/reference/variables/{query.md => Query.md} (55%) diff --git a/docs/collections/local-only-collection.md b/docs/collections/local-only-collection.md new file mode 100644 index 000000000..f7bde5f1f --- /dev/null +++ b/docs/collections/local-only-collection.md @@ -0,0 +1,311 @@ +--- +title: LocalOnly Collection +--- + +# LocalOnly Collection + +LocalOnly collections are designed for in-memory client data or UI state that doesn't need to persist across browser sessions or sync across tabs. + +## Overview + +The `localOnlyCollectionOptions` allows you to create collections that: +- Store data only in memory (no persistence) +- Support optimistic updates with automatic rollback on errors +- Provide optional initial data +- Work perfectly for temporary UI state and session-only data +- Automatically manage the transition from optimistic to confirmed state + +## Installation + +LocalOnly collections are included in the core TanStack DB package: + +```bash +npm install @tanstack/react-db +``` + +## Basic Usage + +```typescript +import { createCollection } from '@tanstack/react-db' +import { localOnlyCollectionOptions } from '@tanstack/react-db' + +const uiStateCollection = createCollection( + localOnlyCollectionOptions({ + id: 'ui-state', + getKey: (item) => item.id, + }) +) +``` + +## Configuration Options + +The `localOnlyCollectionOptions` function accepts the following options: + +### Required Options + +- `id`: Unique identifier for the collection +- `getKey`: Function to extract the unique key from an item + +### Optional Options + +- `schema`: [Standard Schema](https://standardschema.dev) compatible schema (e.g., Zod, Effect) for client-side validation +- `initialData`: Array of items to populate the collection with on creation +- `onInsert`: Optional handler function called before confirming inserts +- `onUpdate`: Optional handler function called before confirming updates +- `onDelete`: Optional handler function called before confirming deletes + +## Initial Data + +Populate the collection with initial data on creation: + +```typescript +const uiStateCollection = createCollection( + localOnlyCollectionOptions({ + id: 'ui-state', + getKey: (item) => item.id, + initialData: [ + { id: 'sidebar', isOpen: false }, + { id: 'theme', mode: 'light' }, + { id: 'modal', visible: false }, + ], + }) +) +``` + +## Mutation Handlers + +Mutation handlers are **completely optional**. When provided, they are called before the optimistic state is confirmed: + +```typescript +const tempDataCollection = createCollection( + localOnlyCollectionOptions({ + id: 'temp-data', + getKey: (item) => item.id, + onInsert: async ({ transaction }) => { + // Custom logic before confirming the insert + console.log('Inserting:', transaction.mutations[0].modified) + }, + onUpdate: async ({ transaction }) => { + // Custom logic before confirming the update + const { original, modified } = transaction.mutations[0] + console.log('Updating from', original, 'to', modified) + }, + onDelete: async ({ transaction }) => { + // Custom logic before confirming the delete + console.log('Deleting:', transaction.mutations[0].original) + }, + }) +) +``` + +## Manual Transactions + +When using LocalOnly collections with manual transactions (created via `createTransaction`), you must call `utils.acceptMutations()` to persist the changes: + +```typescript +import { createTransaction } from '@tanstack/react-db' + +const localData = createCollection( + localOnlyCollectionOptions({ + id: 'form-draft', + getKey: (item) => item.id, + }) +) + +const serverCollection = createCollection( + queryCollectionOptions({ + queryKey: ['items'], + queryFn: async () => api.items.getAll(), + getKey: (item) => item.id, + onInsert: async ({ transaction }) => { + await api.items.create(transaction.mutations[0].modified) + }, + }) +) + +const tx = createTransaction({ + mutationFn: async ({ transaction }) => { + // Handle server collection mutations explicitly in mutationFn + await Promise.all( + transaction.mutations + .filter((m) => m.collection === serverCollection) + .map((m) => api.items.create(m.modified)) + ) + + // After server mutations succeed, accept local collection mutations + localData.utils.acceptMutations(transaction) + }, +}) + +// Apply mutations to both collections in one transaction +tx.mutate(() => { + localData.insert({ id: 'draft-1', data: '...' }) + serverCollection.insert({ id: '1', name: 'Item' }) +}) + +await tx.commit() +``` + +## Complete Example: Modal State Management + +```typescript +import { createCollection } from '@tanstack/react-db' +import { localOnlyCollectionOptions } from '@tanstack/react-db' +import { useLiveQuery } from '@tanstack/react-db' +import { z } from 'zod' + +// Define schema +const modalStateSchema = z.object({ + id: z.string(), + isOpen: z.boolean(), + data: z.any().optional(), +}) + +type ModalState = z.infer + +// Create collection +export const modalStateCollection = createCollection( + localOnlyCollectionOptions({ + id: 'modal-state', + getKey: (item) => item.id, + schema: modalStateSchema, + initialData: [ + { id: 'user-profile', isOpen: false }, + { id: 'settings', isOpen: false }, + { id: 'confirm-delete', isOpen: false }, + ], + }) +) + +// Use in component +function UserProfileModal() { + const { data: modals } = useLiveQuery((q) => + q.from({ modal: modalStateCollection }) + .where(({ modal }) => modal.id === 'user-profile') + ) + + const modalState = modals[0] + + const openModal = (data?: any) => { + modalStateCollection.update('user-profile', (draft) => { + draft.isOpen = true + draft.data = data + }) + } + + const closeModal = () => { + modalStateCollection.update('user-profile', (draft) => { + draft.isOpen = false + draft.data = undefined + }) + } + + if (!modalState?.isOpen) return null + + return ( +
+

User Profile

+
{JSON.stringify(modalState.data, null, 2)}
+ +
+ ) +} +``` + +## Complete Example: Form Draft State + +```typescript +import { createCollection } from '@tanstack/react-db' +import { localOnlyCollectionOptions } from '@tanstack/react-db' +import { useLiveQuery } from '@tanstack/react-db' + +type FormDraft = { + id: string + formData: Record + lastModified: Date +} + +// Create collection for form drafts +export const formDraftsCollection = createCollection( + localOnlyCollectionOptions({ + id: 'form-drafts', + getKey: (item) => item.id, + }) +) + +// Use in component +function CreatePostForm() { + const { data: drafts } = useLiveQuery((q) => + q.from({ draft: formDraftsCollection }) + .where(({ draft }) => draft.id === 'new-post') + ) + + const currentDraft = drafts[0] + + const updateDraft = (field: string, value: any) => { + if (currentDraft) { + formDraftsCollection.update('new-post', (draft) => { + draft.formData[field] = value + draft.lastModified = new Date() + }) + } else { + formDraftsCollection.insert({ + id: 'new-post', + formData: { [field]: value }, + lastModified: new Date(), + }) + } + } + + const clearDraft = () => { + if (currentDraft) { + formDraftsCollection.delete('new-post') + } + } + + const submitForm = async () => { + if (!currentDraft) return + + await api.posts.create(currentDraft.formData) + clearDraft() + } + + return ( +
{ e.preventDefault(); submitForm() }}> + updateDraft('title', e.target.value)} + /> + + +
+ ) +} +``` + +## Use Cases + +LocalOnly collections are perfect for: +- Temporary UI state (modals, sidebars, tooltips) +- Form draft data during the current session +- Client-side computed or derived data +- Wizard/multi-step form state +- Temporary filters or search state +- In-memory caches + +## Comparison with LocalStorageCollection + +| Feature | LocalOnly | LocalStorage | +|---------|-----------|--------------| +| Persistence | None (in-memory only) | localStorage | +| Cross-tab sync | No | Yes | +| Survives page reload | No | Yes | +| Performance | Fastest | Fast | +| Size limits | Memory limits | ~5-10MB | +| Best for | Temporary UI state | User preferences | + +## Learn More + +- [Optimistic Mutations](../guides/mutations.md) +- [Live Queries](../guides/live-queries.md) +- [LocalStorage Collection](./local-storage-collection.md) diff --git a/docs/collections/local-storage-collection.md b/docs/collections/local-storage-collection.md new file mode 100644 index 000000000..a8dbeb20c --- /dev/null +++ b/docs/collections/local-storage-collection.md @@ -0,0 +1,299 @@ +--- +title: LocalStorage Collection +--- + +# LocalStorage Collection + +LocalStorage collections store small amounts of local-only state that persists across browser sessions and syncs across browser tabs in real-time. + +## Overview + +The `localStorageCollectionOptions` allows you to create collections that: +- Persist data to localStorage (or sessionStorage) +- Automatically sync across browser tabs using storage events +- Support optimistic updates with automatic rollback on errors +- Store all data under a single localStorage key +- Work with any storage API that matches the localStorage interface + +## Installation + +LocalStorage collections are included in the core TanStack DB package: + +```bash +npm install @tanstack/react-db +``` + +## Basic Usage + +```typescript +import { createCollection } from '@tanstack/react-db' +import { localStorageCollectionOptions } from '@tanstack/react-db' + +const userPreferencesCollection = createCollection( + localStorageCollectionOptions({ + id: 'user-preferences', + storageKey: 'app-user-prefs', + getKey: (item) => item.id, + }) +) +``` + +## Configuration Options + +The `localStorageCollectionOptions` function accepts the following options: + +### Required Options + +- `id`: Unique identifier for the collection +- `storageKey`: The localStorage key where all collection data is stored +- `getKey`: Function to extract the unique key from an item + +### Optional Options + +- `schema`: [Standard Schema](https://standardschema.dev) compatible schema (e.g., Zod, Effect) for client-side validation +- `storage`: Custom storage implementation (defaults to `localStorage`). Can be `sessionStorage` or any object with the localStorage API +- `storageEventApi`: Event API for subscribing to storage events (defaults to `window`). Enables custom cross-tab, cross-window, or cross-process synchronization +- `onInsert`: Optional handler function called when items are inserted +- `onUpdate`: Optional handler function called when items are updated +- `onDelete`: Optional handler function called when items are deleted + +## Cross-Tab Synchronization + +LocalStorage collections automatically sync across browser tabs in real-time: + +```typescript +const settingsCollection = createCollection( + localStorageCollectionOptions({ + id: 'settings', + storageKey: 'app-settings', + getKey: (item) => item.id, + }) +) + +// Changes in one tab are automatically reflected in all other tabs +// This works automatically via storage events +``` + +## Using SessionStorage + +You can use `sessionStorage` instead of `localStorage` for session-only persistence: + +```typescript +const sessionCollection = createCollection( + localStorageCollectionOptions({ + id: 'session-data', + storageKey: 'session-key', + storage: sessionStorage, // Use sessionStorage instead + getKey: (item) => item.id, + }) +) +``` + +## Custom Storage Backend + +Provide any storage implementation that matches the localStorage API: + +```typescript +// Example: Custom storage wrapper with encryption +const encryptedStorage = { + getItem(key: string) { + const encrypted = localStorage.getItem(key) + return encrypted ? decrypt(encrypted) : null + }, + setItem(key: string, value: string) { + localStorage.setItem(key, encrypt(value)) + }, + removeItem(key: string) { + localStorage.removeItem(key) + }, +} + +const secureCollection = createCollection( + localStorageCollectionOptions({ + id: 'secure-data', + storageKey: 'encrypted-key', + storage: encryptedStorage, + getKey: (item) => item.id, + }) +) +``` + +### Cross-Tab Sync with Custom Storage + +The `storageEventApi` option (defaults to `window`) allows the collection to subscribe to storage events for cross-tab synchronization. A custom storage implementation can provide this API to enable custom cross-tab, cross-window, or cross-process sync: + +```typescript +// Example: Custom storage event API for cross-process sync +const customStorageEventApi = { + addEventListener(event: string, handler: (e: StorageEvent) => void) { + // Custom event subscription logic + // Could be IPC, WebSocket, or any other mechanism + myCustomEventBus.on('storage-change', handler) + }, + removeEventListener(event: string, handler: (e: StorageEvent) => void) { + myCustomEventBus.off('storage-change', handler) + }, +} + +const syncedCollection = createCollection( + localStorageCollectionOptions({ + id: 'synced-data', + storageKey: 'data-key', + storage: customStorage, + storageEventApi: customStorageEventApi, // Custom event API + getKey: (item) => item.id, + }) +) +``` + +This enables synchronization across different contexts beyond just browser tabs, such as: +- Cross-process communication in Electron apps +- WebSocket-based sync across multiple browser windows +- Custom IPC mechanisms in desktop applications + +## Mutation Handlers + +Mutation handlers are **completely optional**. Data will persist to localStorage whether or not you provide handlers: + +```typescript +const preferencesCollection = createCollection( + localStorageCollectionOptions({ + id: 'preferences', + storageKey: 'user-prefs', + getKey: (item) => item.id, + // Optional: Add custom logic when preferences are updated + onUpdate: async ({ transaction }) => { + const { modified } = transaction.mutations[0] + console.log('Preference updated:', modified) + // Maybe send analytics or trigger other side effects + }, + }) +) +``` + +## Manual Transactions + +When using LocalStorage collections with manual transactions (created via `createTransaction`), you must call `utils.acceptMutations()` to persist the changes: + +```typescript +import { createTransaction } from '@tanstack/react-db' + +const localData = createCollection( + localStorageCollectionOptions({ + id: 'form-draft', + storageKey: 'draft-data', + getKey: (item) => item.id, + }) +) + +const serverCollection = createCollection( + queryCollectionOptions({ + queryKey: ['items'], + queryFn: async () => api.items.getAll(), + getKey: (item) => item.id, + onInsert: async ({ transaction }) => { + await api.items.create(transaction.mutations[0].modified) + }, + }) +) + +const tx = createTransaction({ + mutationFn: async ({ transaction }) => { + // Handle server collection mutations explicitly in mutationFn + await Promise.all( + transaction.mutations + .filter((m) => m.collection === serverCollection) + .map((m) => api.items.create(m.modified)) + ) + + // After server mutations succeed, persist local collection mutations + localData.utils.acceptMutations(transaction) + }, +}) + +// Apply mutations to both collections in one transaction +tx.mutate(() => { + localData.insert({ id: 'draft-1', data: '...' }) + serverCollection.insert({ id: '1', name: 'Item' }) +}) + +await tx.commit() +``` + +## Complete Example + +```typescript +import { createCollection } from '@tanstack/react-db' +import { localStorageCollectionOptions } from '@tanstack/react-db' +import { useLiveQuery } from '@tanstack/react-db' +import { z } from 'zod' + +// Define schema +const userPrefsSchema = z.object({ + id: z.string(), + theme: z.enum(['light', 'dark', 'auto']), + language: z.string(), + notifications: z.boolean(), +}) + +type UserPrefs = z.infer + +// Create collection +export const userPreferencesCollection = createCollection( + localStorageCollectionOptions({ + id: 'user-preferences', + storageKey: 'app-user-prefs', + getKey: (item) => item.id, + schema: userPrefsSchema, + }) +) + +// Use in component +function SettingsPanel() { + const { data: prefs } = useLiveQuery((q) => + q.from({ pref: userPreferencesCollection }) + .where(({ pref }) => pref.id === 'current-user') + ) + + const currentPrefs = prefs[0] + + const updateTheme = (theme: 'light' | 'dark' | 'auto') => { + if (currentPrefs) { + userPreferencesCollection.update(currentPrefs.id, (draft) => { + draft.theme = theme + }) + } else { + userPreferencesCollection.insert({ + id: 'current-user', + theme, + language: 'en', + notifications: true, + }) + } + } + + return ( +
+

Theme: {currentPrefs?.theme}

+ + +
+ ) +} +``` + +## Use Cases + +LocalStorage collections are perfect for: +- User preferences and settings +- UI state that should persist across sessions +- Form drafts +- Recently viewed items +- User-specific configurations +- Small amounts of cached data + +## Learn More + +- [Optimistic Mutations](../guides/mutations.md) +- [Live Queries](../guides/live-queries.md) +- [LocalOnly Collection](./local-only-collection.md) diff --git a/docs/collections/powersync-collection.md b/docs/collections/powersync-collection.md new file mode 100644 index 000000000..afc665836 --- /dev/null +++ b/docs/collections/powersync-collection.md @@ -0,0 +1,475 @@ +--- +title: PowerSync Collection +--- + +# PowerSync Collection + +PowerSync collections provide seamless integration between TanStack DB and [PowerSync](https://powersync.com), enabling automatic synchronization between your in-memory TanStack DB collections and PowerSync's SQLite database. This gives you offline-ready persistence, real-time sync capabilities, and powerful conflict resolution. + +## Overview + +The `@tanstack/powersync-db-collection` package allows you to create collections that: + +- Automatically mirror the state of an underlying PowerSync SQLite database +- Reactively update when PowerSync records change +- Support optimistic mutations with rollback on error +- Provide persistence handlers to keep PowerSync in sync with TanStack DB transactions +- Use PowerSync's efficient SQLite-based storage engine +- Work with PowerSync's real-time sync features for offline-first scenarios +- Leverage PowerSync's built-in conflict resolution and data consistency guarantees +- Enable real-time synchronization with PostgreSQL, MongoDB and MySQL backends + +## 1. Installation + +Install the PowerSync collection package along with your preferred framework integration. +PowerSync currently works with Web, React Native and Node.js. The examples below use the Web SDK. +See the PowerSync quickstart [docs](https://docs.powersync.com/installation/quickstart-guide) for more details. + +```bash +npm install @tanstack/powersync-db-collection @powersync/web @journeyapps/wa-sqlite +``` + +### 2. Create a PowerSync Database and Schema + +```ts +import { Schema, Table, column } from "@powersync/web" + +// Define your schema +const APP_SCHEMA = new Schema({ + documents: new Table({ + name: column.text, + author: column.text, + created_at: column.text, + archived: column.integer, + }), +}) + +// Initialize PowerSync database +const db = new PowerSyncDatabase({ + database: { + dbFilename: "app.sqlite", + }, + schema: APP_SCHEMA, +}) +``` + +### 3. (optional) Configure Sync with a Backend + +```ts +import { + AbstractPowerSyncDatabase, + PowerSyncBackendConnector, + PowerSyncCredentials, +} from "@powersync/web" + +// TODO implement your logic here +class Connector implements PowerSyncBackendConnector { + fetchCredentials: () => Promise + + /** Upload local changes to the app backend. + * + * Use {@link AbstractPowerSyncDatabase.getCrudBatch} to get a batch of changes to upload. + * + * Any thrown errors will result in a retry after the configured wait period (default: 5 seconds). + */ + uploadData: (database: AbstractPowerSyncDatabase) => Promise +} + +// Configure the client to connect to a PowerSync service and your backend +db.connect(new Connector()) +``` + +### 4. Create a TanStack DB Collection + +There are two main ways to create a collection: using type inference or using schema validation. Type inference will infer collection types from the underlying PowerSync SQLite tables. Schema validation can be used for additional input/output validations and type transforms. + +#### Option 1: Using Table Type Inference + +The collection types are automatically inferred from the PowerSync schema table definition. The table is used to construct a default standard schema validator which is used internally to validate collection operations. + +Collection mutations accept SQLite types and queries report data with SQLite types. + +```ts +import { createCollection } from "@tanstack/react-db" +import { powerSyncCollectionOptions } from "@tanstack/powersync-db-collection" + +const documentsCollection = createCollection( + powerSyncCollectionOptions({ + database: db, + table: APP_SCHEMA.props.documents, + }) +) + +/** Note: The types for input and output are defined as this */ +// Used for mutations like `insert` or `update` +type DocumentCollectionInput = { + id: string + name: string | null + author: string | null + created_at: string | null // SQLite TEXT + archived: number | null // SQLite integer +} +// The type of query/data results +type DocumentCollectionOutput = DocumentCollectionInput +``` + +The standard PowerSync SQLite types map to these TypeScript types: + +| PowerSync Column Type | TypeScript Type | Description | +| --------------------- | ---------------- | -------------------------------------------------------------------- | +| `column.text` | `string \| null` | Text values, commonly used for strings, JSON, dates (as ISO strings) | +| `column.integer` | `number \| null` | Integer values, also used for booleans (0/1) | +| `column.real` | `number \| null` | Floating point numbers | + +Note: All PowerSync column types are nullable by default. + +#### Option 2: SQLite Types with Schema Validation + +Additional validations for collection mutations can be performed with a custom schema. The Schema below asserts that +the `name`, `author` and `created_at` fields are required as input. `name` also has an additional string length check. + +Note: The input and output types specified in this example still satisfy the underlying SQLite types. An additional `deserializationSchema` is required if the typing differs. See the examples below for more details. + +The application logic (including the backend) should enforce that all incoming synced data passes validation with the `schema`. Failing to validate data will result in inconsistency of the collection data. This is a fatal error! An `onDeserializationError` handler must be provided to react to this case. + +```ts +import { createCollection } from "@tanstack/react-db" +import { powerSyncCollectionOptions } from "@tanstack/powersync-db-collection" +import { z } from "zod" + +// Schema validates SQLite types but adds constraints +const schema = z.object({ + id: z.string(), + name: z.string().min(3, { message: "Should be at least 3 characters" }), + author: z.string(), + created_at: z.string(), // SQLite TEXT for dates + archived: z.number(), +}) + +const documentsCollection = createCollection( + powerSyncCollectionOptions({ + database: db, + table: APP_SCHEMA.props.documents, + schema, + onDeserializationError: (error) => { + // Present fatal error + }, + }) +) + +/** Note: The types for input and output are defined as this */ +// Used for mutations like `insert` or `update` +type DocumentCollectionInput = { + id: string + name: string + author: string + created_at: string // SQLite TEXT + archived: number // SQLite integer +} +// The type of query/data results +type DocumentCollectionOutput = DocumentCollectionInput +``` + +#### Option 3: Transform SQLite Input Types to Rich Output Types + +You can transform SQLite types to richer types (like Date objects) while keeping SQLite-compatible input types: + +Note: The Transformed types are provided by TanStackDB to the PowerSync SQLite persister. These types need to be serialized in +order to be persisted to SQLite. Most types are converted by default. For custom types, override the serialization by providing a +`serializer` param. + +The example below uses `nullable` columns, this is not a requirement. + +The application logic (including the backend) should enforce that all incoming synced data passes validation with the `schema`. Failing to validate data will result in inconsistency of the collection data. This is a fatal error! An `onDeserializationError` handler must be provided to react to this case. + +```ts +const schema = z.object({ + id: z.string(), + name: z.string().nullable(), + created_at: z + .string() + .nullable() + .transform((val) => (val ? new Date(val) : null)), // Transform SQLite TEXT to Date + archived: z + .number() + .nullable() + .transform((val) => (val != null ? val > 0 : null)), // Transform SQLite INTEGER to boolean +}) + +const documentsCollection = createCollection( + powerSyncCollectionOptions({ + database: db, + table: APP_SCHEMA.props.documents, + schema, + onDeserializationError: (error) => { + // Present fatal error + }, + // Optional: custom column serialization + serializer: { + // Dates are serialized by default, this is just an example + created_at: (value) => (value ? value.toISOString() : null), + }, + }) +) + +/** Note: The types for input and output are defined as this */ +// Used for mutations like `insert` or `update` +type DocumentCollectionInput = { + id: string + name: string | null + author: string | null + created_at: string | null // SQLite TEXT + archived: number | null +} +// The type of query/data results +type DocumentCollectionOutput = { + id: string + name: string | null + author: string | null + created_at: Date | null // JS Date instance + archived: boolean | null // JS boolean +} +``` + +#### Option 4: Custom Input/Output Types with Deserialization + +The input and output types can be completely decoupled from the internal SQLite types. This can be used to accept rich values for input mutations. +We require an additional `deserializationSchema` in order to validate and transform incoming synced (SQLite) updates. This schema should convert the incoming SQLite update to the output type. + +The application logic (including the backend) should enforce that all incoming synced data passes validation with the `deserializationSchema`. Failing to validate data will result in inconsistency of the collection data. This is a fatal error! An `onDeserializationError` handler must be provided to react to this case. + +```ts +// Our input/output types use Date and boolean +const schema = z.object({ + id: z.string(), + name: z.string(), + author: z.string(), + created_at: z.date(), // Accept Date objects as input + archived: z.boolean(), // Accept Booleans as input +}) + +// Schema to transform from SQLite types to our output types +const deserializationSchema = z.object({ + id: z.string(), + name: z.string(), + author: z.string(), + created_at: z + .string() + .transform((val) => (new Date(val))), // SQLite TEXT to Date + archived: z + .number() + .transform((val) => (val > 0), // SQLite INTEGER to Boolean +}) + +const documentsCollection = createCollection( + powerSyncCollectionOptions({ + database: db, + table: APP_SCHEMA.props.documents, + schema, + deserializationSchema, + onDeserializationError: (error) => { + // Present fatal error + }, + }) +) + +/** Note: The types for input and output are defined as this */ +// Used for mutations like `insert` or `update` +type DocumentCollectionInput = { + id: string + name: string + author: string + created_at: Date + archived: boolean +} +// The type of query/data results +type DocumentCollectionOutput = DocumentCollectionInput +``` + +## Features + +### Offline-First + +PowerSync collections are offline-first by default. All data is stored locally in a SQLite database, allowing your app to work without an internet connection. Changes are automatically synced when connectivity is restored. + +### Real-Time Sync + +When connected to a PowerSync backend, changes are automatically synchronized in real-time across all connected clients. The sync process handles: + +- Bi-directional sync with the server +- Conflict resolution +- Queue management for offline changes +- Automatic retries on connection loss + +### Working with Rich JavaScript Types + +PowerSync collections support rich JavaScript types like `Date`, `Boolean`, and custom objects while maintaining SQLite compatibility. The collection handles serialization and deserialization automatically: + +```typescript +import { z } from "zod" +import { Schema, Table, column } from "@powersync/web" +import { createCollection } from "@tanstack/react-db" +import { powerSyncCollectionOptions } from "@tanstack/powersync-db-collection" + +// Define PowerSync SQLite schema +const APP_SCHEMA = new Schema({ + tasks: new Table({ + title: column.text, + due_date: column.text, // Stored as ISO string in SQLite + completed: column.integer, // Stored as 0/1 in SQLite + metadata: column.text, // Stored as JSON string in SQLite + }), +}) + +// Define rich types schema +const taskSchema = z.object({ + id: z.string(), + title: z.string().nullable(), + due_date: z + .string() + .nullable() + .transform((val) => (val ? new Date(val) : null)), // Convert to Date + completed: z + .number() + .nullable() + .transform((val) => (val != null ? val > 0 : null)), // Convert to boolean + metadata: z + .string() + .nullable() + .transform((val) => (val ? JSON.parse(val) : null)), // Parse JSON +}) + +// Create collection with rich types +const tasksCollection = createCollection( + powerSyncCollectionOptions({ + database: db, + table: APP_SCHEMA.props.tasks, + schema: taskSchema, + }) +) + +// Work with rich types in your code +await tasksCollection.insert({ + id: crypto.randomUUID(), + title: "Review PR", + due_date: "2025-10-30T10:00:00Z", // String input is automatically converted to Date + completed: 0, // Number input is automatically converted to boolean + metadata: JSON.stringify({ priority: "high" }), +}) + +// Query returns rich types +const task = tasksCollection.get("task-1") +console.log(task.due_date instanceof Date) // true +console.log(typeof task.completed) // "boolean" +console.log(task.metadata.priority) // "high" +``` + +### Type Safety with Rich Types + +The collection maintains type safety throughout: + +```typescript +type TaskInput = { + id: string + title: string | null + due_date: string | null // Accept ISO string for mutations + completed: number | null // Accept 0/1 for mutations + metadata: string | null // Accept JSON string for mutations +} + +type TaskOutput = { + id: string + title: string | null + due_date: Date | null // Get Date object in queries + completed: boolean | null // Get boolean in queries + metadata: { + priority: string + [key: string]: any + } | null +} + +// TypeScript enforces correct types: +tasksCollection.insert({ + due_date: new Date(), // Error: Type 'Date' is not assignable to type 'string' +}) + +const task = tasksCollection.get("task-1") +task.due_date.getTime() // OK - TypeScript knows this is a Date +``` + +### Optimistic Updates + +Updates to the collection are applied optimistically to the local state first, then synchronized with PowerSync and the backend. If an error occurs during sync, the changes are automatically rolled back. + +## Configuration Options + +The `powerSyncCollectionOptions` function accepts the following options: + +```ts +interface PowerSyncCollectionConfig { + // Required options + database: PowerSyncDatabase + table: Table + + // Schema validation and type transformation + schema?: StandardSchemaV1 + deserializationSchema?: StandardSchemaV1 // Required for custom input types + onDeserializationError?: (error: StandardSchemaV1.FailureResult) => void // Required for custom input types + + // Optional Custom serialization + serializer?: { + [Key in keyof TOutput]?: (value: TOutput[Key]) => SQLiteCompatibleType + } + + // Performance tuning + syncBatchSize?: number // Control batch size for initial sync, defaults to 1000 +} +``` + +## Advanced Transactions + +When you need more control over transaction handling, such as batching multiple operations or handling complex transaction scenarios, you can use PowerSync's transaction system directly with TanStack DB transactions. + +```ts +import { createTransaction } from "@tanstack/react-db" +import { PowerSyncTransactor } from "@tanstack/powersync-db-collection" + +// Create a transaction that won't auto-commit +const batchTx = createTransaction({ + autoCommit: false, + mutationFn: async ({ transaction }) => { + // Use PowerSyncTransactor to apply the transaction to PowerSync + await new PowerSyncTransactor({ database: db }).applyTransaction( + transaction + ) + }, +}) + +// Perform multiple operations in the transaction +batchTx.mutate(() => { + // Add multiple documents in a single transaction + for (let i = 0; i < 5; i++) { + documentsCollection.insert({ + id: crypto.randomUUID(), + name: `Document ${i}`, + content: `Content ${i}`, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }) + } +}) + +// Commit the transaction +await batchTx.commit() + +// Wait for the changes to be persisted +await batchTx.isPersisted.promise +``` + +This approach allows you to: + +- Batch multiple operations into a single transaction +- Control when the transaction is committed +- Ensure all operations are atomic +- Wait for persistence confirmation +- Handle complex transaction scenarios diff --git a/docs/collections/trailbase-collection.md b/docs/collections/trailbase-collection.md new file mode 100644 index 000000000..1b1d60d42 --- /dev/null +++ b/docs/collections/trailbase-collection.md @@ -0,0 +1,226 @@ +--- +title: TrailBase Collection +--- + +# TrailBase Collection + +TrailBase collections provide seamless integration between TanStack DB and [TrailBase](https://trailbase.io), enabling real-time data synchronization with TrailBase's self-hosted application backend. + +## Overview + +[TrailBase](https://trailbase.io) is an easy-to-self-host, single-executable application backend with built-in SQLite, a V8 JS runtime, auth, admin UIs and sync functionality. + +The `@tanstack/trailbase-db-collection` package allows you to create collections that: +- Automatically sync data from TrailBase Record APIs +- Support real-time subscriptions when `enable_subscriptions` is enabled +- Handle optimistic updates with automatic rollback on errors +- Provide parse/serialize functions for data transformation + +## Installation + +```bash +npm install @tanstack/trailbase-db-collection @tanstack/react-db trailbase +``` + +## Basic Usage + +```typescript +import { createCollection } from '@tanstack/react-db' +import { trailBaseCollectionOptions } from '@tanstack/trailbase-db-collection' +import { initClient } from 'trailbase' + +const trailBaseClient = initClient(`https://your-trailbase-instance.com`) + +const todosCollection = createCollection( + trailBaseCollectionOptions({ + id: 'todos', + recordApi: trailBaseClient.records('todos'), + getKey: (item) => item.id, + }) +) +``` + +## Configuration Options + +The `trailBaseCollectionOptions` function accepts the following options: + +### Required Options + +- `id`: Unique identifier for the collection +- `recordApi`: TrailBase Record API instance created via `trailBaseClient.records()` +- `getKey`: Function to extract the unique key from an item + +### Optional Options + +- `schema`: [Standard Schema](https://standardschema.dev) compatible schema (e.g., Zod, Effect) for client-side validation +- `parse`: Object mapping field names to parsing functions that transform data coming from TrailBase +- `serialize`: Object mapping field names to serialization functions that transform data going to TrailBase +- `onInsert`: Handler function called when items are inserted +- `onUpdate`: Handler function called when items are updated +- `onDelete`: Handler function called when items are deleted + +## Data Transformation + +TrailBase uses different data formats for storage (e.g., Unix timestamps). Use `parse` and `serialize` to handle these transformations: + +```typescript +type SelectTodo = { + id: string + text: string + created_at: number // Unix timestamp from TrailBase + completed: boolean +} + +type Todo = { + id: string + text: string + created_at: Date // JavaScript Date for app usage + completed: boolean +} + +const todosCollection = createCollection( + trailBaseCollectionOptions({ + id: 'todos', + recordApi: trailBaseClient.records('todos'), + getKey: (item) => item.id, + schema: todoSchema, + // Transform TrailBase data to application format + parse: { + created_at: (ts) => new Date(ts * 1000), + }, + // Transform application data to TrailBase format + serialize: { + created_at: (date) => Math.floor(date.valueOf() / 1000), + }, + }) +) +``` + +## Real-time Subscriptions + +TrailBase supports real-time subscriptions when enabled on the server. The collection automatically subscribes to changes and updates in real-time: + +```typescript +const todosCollection = createCollection( + trailBaseCollectionOptions({ + id: 'todos', + recordApi: trailBaseClient.records('todos'), + getKey: (item) => item.id, + // Real-time updates work automatically when + // enable_subscriptions is set in TrailBase config + }) +) + +// Changes from other clients will automatically update +// the collection in real-time +``` + +## Mutation Handlers + +Handle inserts, updates, and deletes by providing mutation handlers: + +```typescript +const todosCollection = createCollection( + trailBaseCollectionOptions({ + id: 'todos', + recordApi: trailBaseClient.records('todos'), + getKey: (item) => item.id, + onInsert: async ({ transaction }) => { + const newTodo = transaction.mutations[0].modified + // TrailBase handles the persistence automatically + // Add custom logic here if needed + }, + onUpdate: async ({ transaction }) => { + const { original, modified } = transaction.mutations[0] + // TrailBase handles the persistence automatically + // Add custom logic here if needed + }, + onDelete: async ({ transaction }) => { + const deletedTodo = transaction.mutations[0].original + // TrailBase handles the persistence automatically + // Add custom logic here if needed + }, + }) +) +``` + +## Complete Example + +```typescript +import { createCollection } from '@tanstack/react-db' +import { trailBaseCollectionOptions } from '@tanstack/trailbase-db-collection' +import { initClient } from 'trailbase' +import { z } from 'zod' + +const trailBaseClient = initClient(`https://your-trailbase-instance.com`) + +// Define schema +const todoSchema = z.object({ + id: z.string(), + text: z.string(), + completed: z.boolean(), + created_at: z.date(), +}) + +type SelectTodo = { + id: string + text: string + completed: boolean + created_at: number +} + +type Todo = z.infer + +// Create collection +export const todosCollection = createCollection( + trailBaseCollectionOptions({ + id: 'todos', + recordApi: trailBaseClient.records('todos'), + getKey: (item) => item.id, + schema: todoSchema, + parse: { + created_at: (ts) => new Date(ts * 1000), + }, + serialize: { + created_at: (date) => Math.floor(date.valueOf() / 1000), + }, + onInsert: async ({ transaction }) => { + const newTodo = transaction.mutations[0].modified + console.log('Todo created:', newTodo) + }, + }) +) + +// Use in component +function TodoList() { + const { data: todos } = useLiveQuery((q) => + q.from({ todo: todosCollection }) + .where(({ todo }) => !todo.completed) + .orderBy(({ todo }) => todo.created_at, 'desc') + ) + + const addTodo = (text: string) => { + todosCollection.insert({ + id: crypto.randomUUID(), + text, + completed: false, + created_at: new Date(), + }) + } + + return ( +
+ {todos.map((todo) => ( +
{todo.text}
+ ))} +
+ ) +} +``` + +## Learn More + +- [TrailBase Documentation](https://trailbase.io/documentation/) +- [TrailBase Record APIs](https://trailbase.io/documentation/apis_record/) +- [Optimistic Mutations](../guides/mutations.md) +- [Live Queries](../guides/live-queries.md) diff --git a/docs/config.json b/docs/config.json index b16df7560..570043680 100644 --- a/docs/config.json +++ b/docs/config.json @@ -105,6 +105,10 @@ { "label": "RxDB Collection", "to": "collections/rxdb-collection" + }, + { + "label": "PowerSync Collection", + "to": "collections/powersync-collection" } ] }, @@ -171,6 +175,14 @@ { "label": "rxdbCollectionOptions", "to": "reference/rxdb-db-collection/functions/rxdbcollectionoptions" + }, + { + "label": "PowerSync Collection", + "to": "reference/powersync-db-collection/index" + }, + { + "label": "powersyncCollectionOptions", + "to": "reference/powersync-db-collection/functions/powerSyncCollectionOptions" } ], "frameworks": [ diff --git a/docs/framework/angular/reference/functions/injectLiveQuery.md b/docs/framework/angular/reference/functions/injectLiveQuery.md new file mode 100644 index 000000000..4b3209aca --- /dev/null +++ b/docs/framework/angular/reference/functions/injectLiveQuery.md @@ -0,0 +1,120 @@ +--- +id: injectLiveQuery +title: injectLiveQuery +--- + +# Function: injectLiveQuery() + +## Call Signature + +```ts +function injectLiveQuery(options): InjectLiveQueryResult<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>; +``` + +Defined in: [index.ts:51](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L51) + +### Type Parameters + +#### TContext + +`TContext` *extends* `Context` + +#### TParams + +`TParams` *extends* `unknown` + +### Parameters + +#### options + +##### params + +() => `TParams` + +##### query + +(`args`) => `QueryBuilder`\<`TContext`\> + +### Returns + +[`InjectLiveQueryResult`](../../interfaces/InjectLiveQueryResult.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}\> + +## Call Signature + +```ts +function injectLiveQuery(queryFn): InjectLiveQueryResult<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>; +``` + +Defined in: [index.ts:61](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L61) + +### Type Parameters + +#### TContext + +`TContext` *extends* `Context` + +### Parameters + +#### queryFn + +(`q`) => `QueryBuilder`\<`TContext`\> + +### Returns + +[`InjectLiveQueryResult`](../../interfaces/InjectLiveQueryResult.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}\> + +## Call Signature + +```ts +function injectLiveQuery(config): InjectLiveQueryResult<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>; +``` + +Defined in: [index.ts:64](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L64) + +### Type Parameters + +#### TContext + +`TContext` *extends* `Context` + +### Parameters + +#### config + +`LiveQueryCollectionConfig`\<`TContext`\> + +### Returns + +[`InjectLiveQueryResult`](../../interfaces/InjectLiveQueryResult.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}\> + +## Call Signature + +```ts +function injectLiveQuery(liveQueryCollection): InjectLiveQueryResult; +``` + +Defined in: [index.ts:67](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L67) + +### Type Parameters + +#### TResult + +`TResult` *extends* `object` + +#### TKey + +`TKey` *extends* `string` \| `number` + +#### TUtils + +`TUtils` *extends* `Record`\<`string`, `any`\> + +### Parameters + +#### liveQueryCollection + +`Collection`\<`TResult`, `TKey`, `TUtils`\> + +### Returns + +[`InjectLiveQueryResult`](../../interfaces/InjectLiveQueryResult.md)\<`TResult`, `TKey`, `TUtils`\> diff --git a/docs/framework/angular/reference/index.md b/docs/framework/angular/reference/index.md new file mode 100644 index 000000000..3dfb77c21 --- /dev/null +++ b/docs/framework/angular/reference/index.md @@ -0,0 +1,14 @@ +--- +id: "@tanstack/angular-db" +title: "@tanstack/angular-db" +--- + +# @tanstack/angular-db + +## Interfaces + +- [InjectLiveQueryResult](../interfaces/InjectLiveQueryResult.md) + +## Functions + +- [injectLiveQuery](../functions/injectLiveQuery.md) diff --git a/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md b/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md new file mode 100644 index 000000000..aa45218bc --- /dev/null +++ b/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md @@ -0,0 +1,134 @@ +--- +id: InjectLiveQueryResult +title: InjectLiveQueryResult +--- + +# Interface: InjectLiveQueryResult\ + +Defined in: [index.ts:26](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L26) + +The result of calling `injectLiveQuery`. +Contains reactive signals for the query state and data. + +## Type Parameters + +### TResult + +`TResult` *extends* `object` = `any` + +### TKey + +`TKey` *extends* `string` \| `number` = `string` \| `number` + +### TUtils + +`TUtils` *extends* `Record`\<`string`, `any`\> = \{ +\} + +## Properties + +### collection + +```ts +collection: Signal, TResult>>; +``` + +Defined in: [index.ts:36](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L36) + +A signal containing the underlying collection instance + +*** + +### data + +```ts +data: Signal; +``` + +Defined in: [index.ts:34](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L34) + +A signal containing the results as an array + +*** + +### isCleanedUp + +```ts +isCleanedUp: Signal; +``` + +Defined in: [index.ts:48](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L48) + +A signal indicating whether the collection has been cleaned up + +*** + +### isError + +```ts +isError: Signal; +``` + +Defined in: [index.ts:46](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L46) + +A signal indicating whether the collection has an error + +*** + +### isIdle + +```ts +isIdle: Signal; +``` + +Defined in: [index.ts:44](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L44) + +A signal indicating whether the collection is idle + +*** + +### isLoading + +```ts +isLoading: Signal; +``` + +Defined in: [index.ts:40](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L40) + +A signal indicating whether the collection is currently loading + +*** + +### isReady + +```ts +isReady: Signal; +``` + +Defined in: [index.ts:42](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L42) + +A signal indicating whether the collection is ready + +*** + +### state + +```ts +state: Signal>; +``` + +Defined in: [index.ts:32](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L32) + +A signal containing the complete state map of results keyed by their ID + +*** + +### status + +```ts +status: Signal; +``` + +Defined in: [index.ts:38](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L38) + +A signal containing the current status of the collection diff --git a/docs/framework/react/reference/functions/useLiveInfiniteQuery.md b/docs/framework/react/reference/functions/useLiveInfiniteQuery.md new file mode 100644 index 000000000..49f258cf6 --- /dev/null +++ b/docs/framework/react/reference/functions/useLiveInfiniteQuery.md @@ -0,0 +1,217 @@ +--- +id: useLiveInfiniteQuery +title: useLiveInfiniteQuery +--- + +# Function: useLiveInfiniteQuery() + +## Call Signature + +```ts +function useLiveInfiniteQuery(liveQueryCollection, config): UseLiveInfiniteQueryReturn; +``` + +Defined in: [useLiveInfiniteQuery.ts:113](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L113) + +Create an infinite query using a query function with live updates + +Uses `utils.setWindow()` to dynamically adjust the limit/offset window +without recreating the live query collection on each page change. + +### Type Parameters + +#### TResult + +`TResult` *extends* `object` + +#### TKey + +`TKey` *extends* `string` \| `number` + +#### TUtils + +`TUtils` *extends* `Record`\<`string`, `any`\> + +### Parameters + +#### liveQueryCollection + +`Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> & `NonSingleResult` + +#### config + +[`UseLiveInfiniteQueryConfig`](../../type-aliases/UseLiveInfiniteQueryConfig.md)\<`any`\> + +Configuration including pageSize and getNextPageParam + +### Returns + +[`UseLiveInfiniteQueryReturn`](../../type-aliases/UseLiveInfiniteQueryReturn.md)\<`any`\> + +Object with pages, data, and pagination controls + +### Examples + +```ts +// Basic infinite query +const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery( + (q) => q + .from({ posts: postsCollection }) + .orderBy(({ posts }) => posts.createdAt, 'desc') + .select(({ posts }) => ({ + id: posts.id, + title: posts.title + })), + { + pageSize: 20, + getNextPageParam: (lastPage, allPages) => + lastPage.length === 20 ? allPages.length : undefined + } +) +``` + +```ts +// With dependencies +const { pages, fetchNextPage } = useLiveInfiniteQuery( + (q) => q + .from({ posts: postsCollection }) + .where(({ posts }) => eq(posts.category, category)) + .orderBy(({ posts }) => posts.createdAt, 'desc'), + { + pageSize: 10, + getNextPageParam: (lastPage) => + lastPage.length === 10 ? lastPage.length : undefined + }, + [category] +) +``` + +```ts +// Router loader pattern with pre-created collection +// In loader: +const postsQuery = createLiveQueryCollection({ + query: (q) => q + .from({ posts: postsCollection }) + .orderBy(({ posts }) => posts.createdAt, 'desc') + .limit(20) +}) +await postsQuery.preload() +return { postsQuery } + +// In component: +const { postsQuery } = useLoaderData() +const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery( + postsQuery, + { + pageSize: 20, + getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined + } +) +``` + +## Call Signature + +```ts +function useLiveInfiniteQuery( + queryFn, + config, +deps?): UseLiveInfiniteQueryReturn; +``` + +Defined in: [useLiveInfiniteQuery.ts:123](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveInfiniteQuery.ts#L123) + +Create an infinite query using a query function with live updates + +Uses `utils.setWindow()` to dynamically adjust the limit/offset window +without recreating the live query collection on each page change. + +### Type Parameters + +#### TContext + +`TContext` *extends* `Context` + +### Parameters + +#### queryFn + +(`q`) => `QueryBuilder`\<`TContext`\> + +Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work. + +#### config + +[`UseLiveInfiniteQueryConfig`](../../type-aliases/UseLiveInfiniteQueryConfig.md)\<`TContext`\> + +Configuration including pageSize and getNextPageParam + +#### deps? + +`unknown`[] + +Array of dependencies that trigger query re-execution when changed + +### Returns + +[`UseLiveInfiniteQueryReturn`](../../type-aliases/UseLiveInfiniteQueryReturn.md)\<`TContext`\> + +Object with pages, data, and pagination controls + +### Examples + +```ts +// Basic infinite query +const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery( + (q) => q + .from({ posts: postsCollection }) + .orderBy(({ posts }) => posts.createdAt, 'desc') + .select(({ posts }) => ({ + id: posts.id, + title: posts.title + })), + { + pageSize: 20, + getNextPageParam: (lastPage, allPages) => + lastPage.length === 20 ? allPages.length : undefined + } +) +``` + +```ts +// With dependencies +const { pages, fetchNextPage } = useLiveInfiniteQuery( + (q) => q + .from({ posts: postsCollection }) + .where(({ posts }) => eq(posts.category, category)) + .orderBy(({ posts }) => posts.createdAt, 'desc'), + { + pageSize: 10, + getNextPageParam: (lastPage) => + lastPage.length === 10 ? lastPage.length : undefined + }, + [category] +) +``` + +```ts +// Router loader pattern with pre-created collection +// In loader: +const postsQuery = createLiveQueryCollection({ + query: (q) => q + .from({ posts: postsCollection }) + .orderBy(({ posts }) => posts.createdAt, 'desc') + .limit(20) +}) +await postsQuery.preload() +return { postsQuery } + +// In component: +const { postsQuery } = useLoaderData() +const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery( + postsQuery, + { + pageSize: 20, + getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined + } +) +``` diff --git a/docs/framework/react/reference/functions/useLiveQuery.md b/docs/framework/react/reference/functions/useLiveQuery.md new file mode 100644 index 000000000..84162b674 --- /dev/null +++ b/docs/framework/react/reference/functions/useLiveQuery.md @@ -0,0 +1,1251 @@ +--- +id: useLiveQuery +title: useLiveQuery +--- + +# Function: useLiveQuery() + +## Call Signature + +```ts +function useLiveQuery(queryFn, deps?): object; +``` + +Defined in: [useLiveQuery.ts:84](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L84) + +Create a live query using a query function + +### Type Parameters + +#### TContext + +`TContext` *extends* `Context` + +### Parameters + +#### queryFn + +(`q`) => `QueryBuilder`\<`TContext`\> + +Query function that defines what data to fetch + +#### deps? + +`unknown`[] + +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]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[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; +``` + +#### isReady + +```ts +isReady: boolean; +``` + +#### state + +```ts +state: Map; +``` + +#### status + +```ts +status: CollectionStatus; +``` + +### Examples + +```ts +// Basic query with object syntax +const { data, isLoading } = useLiveQuery((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( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => eq(todos.id, 1)) + .findOne() +) +``` + +```ts +// With dependencies that trigger re-execution +const { data, state } = useLiveQuery( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => gt(todos.priority, minPriority)), + [minPriority] // Re-run when minPriority changes +) +``` + +```ts +// Join pattern +const { data } = useLiveQuery((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((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(queryFn, deps?): object; +``` + +Defined in: [useLiveQuery.ts:101](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L101) + +Create a live query using a query function + +### Type Parameters + +#### TContext + +`TContext` *extends* `Context` + +### Parameters + +#### queryFn + +(`q`) => `QueryBuilder`\<`TContext`\> \| `null` \| `undefined` + +Query function that defines what data to fetch + +#### deps? + +`unknown`[] + +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]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, string | number, { +}, StandardSchemaV1, { [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[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; +``` + +#### isReady + +```ts +isReady: boolean; +``` + +#### state + +```ts +state: + | Map + | undefined; +``` + +#### status + +```ts +status: UseLiveQueryStatus; +``` + +### Examples + +```ts +// Basic query with object syntax +const { data, isLoading } = useLiveQuery((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( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => eq(todos.id, 1)) + .findOne() +) +``` + +```ts +// With dependencies that trigger re-execution +const { data, state } = useLiveQuery( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => gt(todos.priority, minPriority)), + [minPriority] // Re-run when minPriority changes +) +``` + +```ts +// Join pattern +const { data } = useLiveQuery((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((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(queryFn, deps?): object; +``` + +Defined in: [useLiveQuery.ts:120](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L120) + +Create a live query using a query function + +### Type Parameters + +#### TContext + +`TContext` *extends* `Context` + +### Parameters + +#### queryFn + +(`q`) => + \| `LiveQueryCollectionConfig`\<`TContext`, \{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \} & `object`\> + \| `null` + \| `undefined` + +Query function that defines what data to fetch + +#### deps? + +`unknown`[] + +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]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, string | number, { +}, StandardSchemaV1, { [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[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; +``` + +#### isReady + +```ts +isReady: boolean; +``` + +#### state + +```ts +state: + | Map + | undefined; +``` + +#### status + +```ts +status: UseLiveQueryStatus; +``` + +### Examples + +```ts +// Basic query with object syntax +const { data, isLoading } = useLiveQuery((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( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => eq(todos.id, 1)) + .findOne() +) +``` + +```ts +// With dependencies that trigger re-execution +const { data, state } = useLiveQuery( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => gt(todos.priority, minPriority)), + [minPriority] // Re-run when minPriority changes +) +``` + +```ts +// Join pattern +const { data } = useLiveQuery((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((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(queryFn, deps?): object; +``` + +Defined in: [useLiveQuery.ts:139](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L139) + +Create a live query using a query function + +### Type Parameters + +#### TResult + +`TResult` *extends* `object` + +#### TKey + +`TKey` *extends* `string` \| `number` + +#### TUtils + +`TUtils` *extends* `Record`\<`string`, `any`\> + +### Parameters + +#### queryFn + +(`q`) => + \| `Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> + \| `null` + \| `undefined` + +Query function that defines what data to fetch + +#### deps? + +`unknown`[] + +Array of dependencies that trigger query re-execution when changed + +### Returns + +`object` + +Object with reactive data, state, and status information + +#### collection + +```ts +collection: + | Collection, TResult> + | undefined; +``` + +#### data + +```ts +data: TResult[] | undefined; +``` + +#### isCleanedUp + +```ts +isCleanedUp: boolean; +``` + +#### isEnabled + +```ts +isEnabled: boolean; +``` + +#### isError + +```ts +isError: boolean; +``` + +#### isIdle + +```ts +isIdle: boolean; +``` + +#### isLoading + +```ts +isLoading: boolean; +``` + +#### isReady + +```ts +isReady: boolean; +``` + +#### state + +```ts +state: Map | undefined; +``` + +#### status + +```ts +status: UseLiveQueryStatus; +``` + +### Examples + +```ts +// Basic query with object syntax +const { data, isLoading } = useLiveQuery((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( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => eq(todos.id, 1)) + .findOne() +) +``` + +```ts +// With dependencies that trigger re-execution +const { data, state } = useLiveQuery( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => gt(todos.priority, minPriority)), + [minPriority] // Re-run when minPriority changes +) +``` + +```ts +// Join pattern +const { data } = useLiveQuery((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((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(queryFn, deps?): object; +``` + +Defined in: [useLiveQuery.ts:162](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L162) + +Create a live query using a query function + +### Type Parameters + +#### TContext + +`TContext` *extends* `Context` + +#### TResult + +`TResult` *extends* `object` + +#### TKey + +`TKey` *extends* `string` \| `number` + +#### TUtils + +`TUtils` *extends* `Record`\<`string`, `any`\> + +### Parameters + +#### queryFn + +(`q`) => + \| `QueryBuilder`\<`TContext`\> + \| `LiveQueryCollectionConfig`\<`TContext`, \{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \} & `object`\> + \| `Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> + \| `null` + \| `undefined` + +Query function that defines what data to fetch + +#### deps? + +`unknown`[] + +Array of dependencies that trigger query re-execution when changed + +### Returns + +`object` + +Object with reactive data, state, and status information + +#### collection + +```ts +collection: + | Collection, TResult> + | Collection<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, string | number, { +}, StandardSchemaV1, { [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }> + | undefined; +``` + +#### data + +```ts +data: InferResultType | TResult[] | undefined; +``` + +#### isCleanedUp + +```ts +isCleanedUp: boolean; +``` + +#### isEnabled + +```ts +isEnabled: boolean; +``` + +#### isError + +```ts +isError: boolean; +``` + +#### isIdle + +```ts +isIdle: boolean; +``` + +#### isLoading + +```ts +isLoading: boolean; +``` + +#### isReady + +```ts +isReady: boolean; +``` + +#### state + +```ts +state: + | Map + | Map + | undefined; +``` + +#### status + +```ts +status: UseLiveQueryStatus; +``` + +### Examples + +```ts +// Basic query with object syntax +const { data, isLoading } = useLiveQuery((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( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => eq(todos.id, 1)) + .findOne() +) +``` + +```ts +// With dependencies that trigger re-execution +const { data, state } = useLiveQuery( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => gt(todos.priority, minPriority)), + [minPriority] // Re-run when minPriority changes +) +``` + +```ts +// Join pattern +const { data } = useLiveQuery((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((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:230](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L230) + +Create a live query using configuration object + +### Type Parameters + +#### TContext + +`TContext` *extends* `Context` + +### Parameters + +#### config + +`LiveQueryCollectionConfig`\<`TContext`\> + +Configuration object with query and options + +#### deps? + +`unknown`[] + +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]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[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; +``` + +#### isReady + +```ts +isReady: boolean; +``` + +#### state + +```ts +state: Map; +``` + +#### status + +```ts +status: CollectionStatus; +``` + +### Examples + +```ts +// Basic config object usage +const { data, status } = useLiveQuery({ + query: (q) => q.from({ todos: todosCollection }), + gcTime: 60000 +}) +``` + +```ts +// With query builder and options +const queryBuilder = new Query() + .from({ persons: collection }) + .where(({ persons }) => gt(persons.age, 30)) + .select(({ persons }) => ({ id: persons.id, name: persons.name })) + +const { data, isReady } = useLiveQuery({ query: queryBuilder }) +``` + +```ts +// Handle all states uniformly +const { data, isLoading, isReady, isError } = useLiveQuery({ + query: (q) => q.from({ items: itemCollection }) +}) + +if (isLoading) return
Loading...
+if (isError) return
Something went wrong
+if (!isReady) return
Preparing...
+ +return
{data.length} items loaded
+``` + +## Call Signature + +```ts +function useLiveQuery(liveQueryCollection): object; +``` + +Defined in: [useLiveQuery.ts:276](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L276) + +Subscribe to an existing live query collection + +### Type Parameters + +#### TResult + +`TResult` *extends* `object` + +#### TKey + +`TKey` *extends* `string` \| `number` + +#### TUtils + +`TUtils` *extends* `Record`\<`string`, `any`\> + +### Parameters + +#### liveQueryCollection + +`Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> & `NonSingleResult` + +Pre-created live query collection to subscribe to + +### Returns + +`object` + +Object with reactive data, state, and status information + +#### collection + +```ts +collection: Collection; +``` + +#### data + +```ts +data: TResult[]; +``` + +#### isCleanedUp + +```ts +isCleanedUp: boolean; +``` + +#### isEnabled + +```ts +isEnabled: true; +``` + +#### isError + +```ts +isError: boolean; +``` + +#### isIdle + +```ts +isIdle: boolean; +``` + +#### isLoading + +```ts +isLoading: boolean; +``` + +#### isReady + +```ts +isReady: boolean; +``` + +#### state + +```ts +state: Map; +``` + +#### status + +```ts +status: CollectionStatus; +``` + +### Examples + +```ts +// Using pre-created live query collection +const myLiveQuery = createLiveQueryCollection((q) => + q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true)) +) +const { data, collection } = useLiveQuery(myLiveQuery) +``` + +```ts +// Access collection methods directly +const { data, collection, isReady } = useLiveQuery(existingCollection) + +// Use collection for mutations +const handleToggle = (id) => { + collection.update(id, draft => { draft.completed = !draft.completed }) +} +``` + +```ts +// Handle states consistently +const { data, isLoading, isError } = useLiveQuery(sharedCollection) + +if (isLoading) return
Loading...
+if (isError) return
Error loading data
+ +return
{data.map(item => )}
+``` + +## Call Signature + +```ts +function useLiveQuery(liveQueryCollection): object; +``` + +Defined in: [useLiveQuery.ts:296](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L296) + +Create a live query using a query function + +### Type Parameters + +#### TResult + +`TResult` *extends* `object` + +#### TKey + +`TKey` *extends* `string` \| `number` + +#### TUtils + +`TUtils` *extends* `Record`\<`string`, `any`\> + +### Parameters + +#### liveQueryCollection + +`Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> & `SingleResult` + +### Returns + +`object` + +Object with reactive data, state, and status information + +#### collection + +```ts +collection: Collection, TResult> & SingleResult; +``` + +#### data + +```ts +data: TResult | undefined; +``` + +#### isCleanedUp + +```ts +isCleanedUp: boolean; +``` + +#### isEnabled + +```ts +isEnabled: true; +``` + +#### isError + +```ts +isError: boolean; +``` + +#### isIdle + +```ts +isIdle: boolean; +``` + +#### isLoading + +```ts +isLoading: boolean; +``` + +#### isReady + +```ts +isReady: boolean; +``` + +#### state + +```ts +state: Map; +``` + +#### status + +```ts +status: CollectionStatus; +``` + +### Examples + +```ts +// Basic query with object syntax +const { data, isLoading } = useLiveQuery((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( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => eq(todos.id, 1)) + .findOne() +) +``` + +```ts +// With dependencies that trigger re-execution +const { data, state } = useLiveQuery( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => gt(todos.priority, minPriority)), + [minPriority] // Re-run when minPriority changes +) +``` + +```ts +// Join pattern +const { data } = useLiveQuery((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((q) => + q.from({ todos: todoCollection }) +) + +if (isLoading) return
Loading...
+if (isError) return
Error: {status}
+ +return ( +
    + {data.map(todo =>
  • {todo.text}
  • )} +
+) +``` diff --git a/docs/framework/react/reference/functions/usePacedMutations.md b/docs/framework/react/reference/functions/usePacedMutations.md new file mode 100644 index 000000000..4f50b0411 --- /dev/null +++ b/docs/framework/react/reference/functions/usePacedMutations.md @@ -0,0 +1,131 @@ +--- +id: usePacedMutations +title: usePacedMutations +--- + +# Function: usePacedMutations() + +```ts +function usePacedMutations(config): (variables) => Transaction; +``` + +Defined in: [usePacedMutations.ts:93](https://github.com/TanStack/db/blob/main/packages/react-db/src/usePacedMutations.ts#L93) + +React hook for managing paced mutations with timing strategies. + +Provides optimistic mutations with pluggable strategies like debouncing, +queuing, or throttling. The optimistic updates are applied immediately via +`onMutate`, and the actual persistence is controlled by the strategy. + +## Type Parameters + +### TVariables + +`TVariables` = `unknown` + +### T + +`T` *extends* `object` = `Record`\<`string`, `unknown`\> + +## Parameters + +### config + +`PacedMutationsConfig`\<`TVariables`, `T`\> + +Configuration including onMutate, mutationFn and strategy + +## Returns + +A mutate function that accepts variables and returns Transaction objects + +```ts +(variables): Transaction; +``` + +### Parameters + +#### variables + +`TVariables` + +### Returns + +`Transaction`\<`T`\> + +## Examples + +```tsx +// Debounced auto-save +function AutoSaveForm({ formId }: { formId: string }) { + const mutate = usePacedMutations({ + onMutate: (value) => { + // Apply optimistic update immediately + formCollection.update(formId, draft => { + draft.content = value + }) + }, + mutationFn: async ({ transaction }) => { + await api.save(transaction.mutations) + }, + strategy: debounceStrategy({ wait: 500 }) + }) + + const handleChange = async (value: string) => { + const tx = mutate(value) + + // Optional: await persistence or handle errors + try { + await tx.isPersisted.promise + console.log('Saved!') + } catch (error) { + console.error('Save failed:', error) + } + } + + return