diff --git a/.changeset/octane-db-package.md b/.changeset/octane-db-package.md new file mode 100644 index 0000000000..633b1256fe --- /dev/null +++ b/.changeset/octane-db-package.md @@ -0,0 +1,5 @@ +--- +'@tanstack/octane-db': minor +--- + +Add `@tanstack/octane-db`, an Octane framework adapter for TanStack DB with full hook parity to `@tanstack/react-db` and Octane compiler hook-slot forwarding. diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml index abbfac7dfa..aec63c5118 100644 --- a/_artifacts/skill_tree.yaml +++ b/_artifacts/skill_tree.yaml @@ -163,6 +163,29 @@ skills: - 'TanStack/db:packages/react-db/src/useLiveInfiniteQuery.ts' - 'TanStack/db:packages/react-db/src/usePacedMutations.ts' + - name: 'Octane DB' + slug: 'octane-db' + type: 'framework' + domain: 'framework-integration' + path: 'skills/octane-db/SKILL.md' + package: 'packages/octane-db' + description: > + Octane bindings for TanStack DB. useLiveQuery hook with dependency arrays + and 8 overloads (query function, config object, pre-created collection, + disabled state). useLiveSuspenseQuery for Octane Suspense with error + boundaries. useLiveInfiniteQuery for cursor-based pagination (pageSize, + fetchNextPage, hasNextPage). usePacedMutations for debounced mutations. + useLiveQueryEffect for row enter/exit/update side effects. + Return shape: data, state, collection, status, isLoading, isReady, isError. + requires: + - 'db-core' + sources: + - 'TanStack/db:docs/framework/octane/overview.md' + - 'TanStack/db:packages/octane-db/src/useLiveQuery.ts' + - 'TanStack/db:packages/octane-db/src/useLiveInfiniteQuery.ts' + - 'TanStack/db:packages/octane-db/src/usePacedMutations.ts' + - 'TanStack/db:packages/octane-db/src/useLiveQueryEffect.ts' + - name: 'Vue DB' slug: 'vue-db' type: 'framework' diff --git a/docs/framework/octane/overview.md b/docs/framework/octane/overview.md new file mode 100644 index 0000000000..899afe9cc5 --- /dev/null +++ b/docs/framework/octane/overview.md @@ -0,0 +1,129 @@ +--- +title: TanStack DB Octane Adapter +id: adapter +--- + +## Installation + +```sh +npm install @tanstack/octane-db octane @octanejs/vite-plugin +``` + +Configure the Octane compiler in your Vite app — see [Octane build tools](https://octanejs.dev/docs/build-tools). + +`@tanstack/octane-db` re-exports everything from `@tanstack/db`. Import collections, query helpers, and hooks from `@tanstack/octane-db`. + +## Octane Hooks + +See the [Octane Functions Reference](./reference/index.md) for the full hook list. + +For comprehensive documentation on writing queries (filtering, joins, aggregations, etc.), see the [Live Queries Guide](../../guides/live-queries). + +## Basic Usage + +The examples below assume `todosCollection` and `postsCollection` are collections you've already created (see the [Collections guide](../../guides/collections)), and that query helpers such as `eq` and `gt` are imported from `@tanstack/octane-db`. + +### useLiveQuery + +```tsx +import { useLiveQuery, eq } from '@tanstack/octane-db' + +function TodoList() { + const { data, isLoading } = useLiveQuery((q) => + q.from({ todos: todosCollection }) + .where(({ todos }) => eq(todos.completed, false)) + .select(({ todos }) => ({ id: todos.id, text: todos.text })) + ) + + if (isLoading) return
Loading...
+ + return ( + + ) +} +``` + +### Dependency Arrays + +All query hooks (`useLiveQuery`, `useLiveInfiniteQuery`, `useLiveSuspenseQuery`) accept an optional dependency array as their last parameter. When any value in the array changes, the query is recreated and re-executed. + +```tsx +function FilteredTodos({ minPriority }: { minPriority: number }) { + const { data } = useLiveQuery( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => gt(todos.priority, minPriority)), + [minPriority] + ) + + return
{data.length} high-priority todos
+} +``` + +### useLiveInfiniteQuery + +```tsx +import { useLiveInfiniteQuery, eq } from '@tanstack/octane-db' + +function PostFeed({ category }: { category: string }) { + const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery( + (q) => q + .from({ posts: postsCollection }) + .where(({ posts }) => eq(posts.category, category)) + .orderBy(({ posts }) => posts.createdAt, 'desc'), + { + pageSize: 20, + getNextPageParam: (lastPage, allPages) => + lastPage.length === 20 ? allPages.length : undefined + }, + [category] + ) + + return ( +
+ + {hasNextPage && ( + + )} +
+ ) +} +``` + +### useLiveSuspenseQuery + +Wrap components in Octane `Suspense` (and `@try` / `@catch` or an error boundary for failures): + +```tsx +import { Suspense } from 'octane' +import { useLiveSuspenseQuery, eq } from '@tanstack/octane-db' + +function TodoList({ filter }: { filter: string }) { + const { data } = useLiveSuspenseQuery( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => eq(todos.filter, filter)), + [filter] + ) + + return ( + + ) +} + +function App() { + return ( + Loading...}> + + + ) +} +``` + +### Incremental adoption from React + +You can host compiled Octane components inside an existing React 19 app with `OctaneCompat` from `octane/react`. Islands can use `@tanstack/octane-db` hooks while the rest of the app keeps `@tanstack/react-db`. See [OctaneCompat](https://octanejs.dev/docs/differences-from-react). diff --git a/docs/framework/octane/reference/index.md b/docs/framework/octane/reference/index.md new file mode 100644 index 0000000000..9a457e39d4 --- /dev/null +++ b/docs/framework/octane/reference/index.md @@ -0,0 +1,9 @@ +# Octane Functions Reference + +Generated API reference for `@tanstack/octane-db`. Run `pnpm generate-docs` at the repo root to refresh these pages. + +- [useLiveQuery](./functions/useLiveQuery.md) +- [useLiveSuspenseQuery](./functions/useLiveSuspenseQuery.md) +- [useLiveInfiniteQuery](./functions/useLiveInfiniteQuery.md) +- [usePacedMutations](./functions/usePacedMutations.md) +- [useLiveQueryEffect](./functions/useLiveQueryEffect.md) diff --git a/docs/installation.md b/docs/installation.md index 2d643373fd..27416aa5bc 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -41,6 +41,14 @@ npm install @tanstack/angular-db TanStack DB is compatible with Angular v16.0.0+ +## Octane + +```sh +npm install @tanstack/octane-db octane @octanejs/vite-plugin +``` + +TanStack DB is compatible with Octane v0.1.0+. Configure the Octane compiler in your build tool — see [octanejs.dev](https://octanejs.dev/docs/build-tools). + ## Vanilla JS ```sh diff --git a/domain_map.yaml b/domain_map.yaml index 20997e9002..5884e5a82b 100644 --- a/domain_map.yaml +++ b/domain_map.yaml @@ -303,6 +303,9 @@ skills: - name: 'Angular' package: '@tanstack/angular-db' config_surface: 'injectLiveQuery with Signal, inject(DestroyRef)' + - name: 'Octane' + package: '@tanstack/octane-db' + config_surface: 'useLiveQuery, useLiveSuspenseQuery, useLiveInfiniteQuery, usePacedMutations, useLiveQueryEffect' failure_modes: - mistake: 'Missing external values in useLiveQuery dependency array' mechanism: "When query uses external state (props, local state) not included in deps array, the query won't re-run when those values change, showing stale results" diff --git a/packages/db/src/live-query-adapter.ts b/packages/db/src/live-query-adapter.ts index b13001d0a8..6e8dd1d6fc 100644 --- a/packages/db/src/live-query-adapter.ts +++ b/packages/db/src/live-query-adapter.ts @@ -4,7 +4,7 @@ import type { CollectionStatus } from './types.js' /** * Shared helpers for the first-party framework adapters (`@tanstack/react-db`, * `@tanstack/vue-db`, `@tanstack/svelte-db`, `@tanstack/solid-db`, - * `@tanstack/angular-db`). + * `@tanstack/angular-db`, `@tanstack/octane-db`). * * These centralize small pieces of logic every adapter used to duplicate, so * they stay consistent across frameworks. They are intended for the official diff --git a/packages/octane-db/CHANGELOG.md b/packages/octane-db/CHANGELOG.md new file mode 100644 index 0000000000..84265af4ec --- /dev/null +++ b/packages/octane-db/CHANGELOG.md @@ -0,0 +1,5 @@ +# @tanstack/octane-db + +## 0.0.1 + +Initial release of the Octane framework adapter for TanStack DB. diff --git a/packages/octane-db/README.md b/packages/octane-db/README.md new file mode 100644 index 0000000000..ea93d3aa9a --- /dev/null +++ b/packages/octane-db/README.md @@ -0,0 +1,5 @@ +# @tanstack/octane-db + +Octane hooks for TanStack DB. See [TanStack/db](https://github.com/TanStack/db) for more details. + +Install `octane` alongside this package and configure the Octane compiler in your build tool (see [octanejs.dev](https://octanejs.dev/docs/build-tools)). diff --git a/packages/octane-db/package.json b/packages/octane-db/package.json new file mode 100644 index 0000000000..873c47b6d6 --- /dev/null +++ b/packages/octane-db/package.json @@ -0,0 +1,70 @@ +{ + "name": "@tanstack/octane-db", + "version": "0.0.1", + "description": "Octane integration for @tanstack/db", + "author": "Kyle Mathews", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/db.git", + "directory": "packages/octane-db" + }, + "homepage": "https://tanstack.com/db", + "keywords": [ + "optimistic", + "octane", + "typescript", + "tanstack-intent" + ], + "octane": { + "hookSlots": { + "manual": [ + "src" + ] + } + }, + "scripts": { + "build": "vite build", + "build:minified": "vite build --minify", + "dev": "vite build --watch", + "test": "vitest --run", + "lint": "eslint . --fix" + }, + "type": "module", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src", + "skills" + ], + "dependencies": { + "@tanstack/db": "workspace:*" + }, + "peerDependencies": { + "octane": ">=0.1.0" + }, + "devDependencies": { + "@electric-sql/client": "^1.5.15", + "@octanejs/testing-library": "^0.1.10", + "@octanejs/vite-plugin": "^0.1.13", + "@vitest/coverage-istanbul": "^3.2.4", + "octane": "^0.1.13", + "vitest": "^3.2.4" + } +} diff --git a/packages/octane-db/skills/octane-db/SKILL.md b/packages/octane-db/skills/octane-db/SKILL.md new file mode 100644 index 0000000000..cb5638b4b1 --- /dev/null +++ b/packages/octane-db/skills/octane-db/SKILL.md @@ -0,0 +1,432 @@ +--- +name: octane-db +description: > + Octane bindings for TanStack DB. useLiveQuery hook with dependency arrays + (8 overloads: query function, config object, pre-created collection, + disabled state via returning undefined/null). useLiveSuspenseQuery for + Octane Suspense with Error Boundaries (data always defined). + useLiveInfiniteQuery for cursor-based pagination (pageSize, fetchNextPage, + hasNextPage, isFetchingNextPage). usePacedMutations for debounced Octane + state updates. Return shape: data, state, collection, status, isLoading, + isReady, isError. Import from @tanstack/octane-db (re-exports all of + @tanstack/db). +type: framework +library: db +framework: octane +library_version: '0.6.0' +requires: + - db-core +sources: + - 'TanStack/db:docs/framework/octane/overview.md' + - 'TanStack/db:docs/guides/live-queries.md' + - 'TanStack/db:packages/octane-db/src/useLiveQuery.ts' + - 'TanStack/db:packages/octane-db/src/useLiveInfiniteQuery.ts' + - 'TanStack/db:packages/octane-db/src/useLiveQueryEffect.ts' +--- + +This skill builds on db-core. Read it first for collection setup, query builder, and mutation patterns. + +# TanStack DB — Octane + +## Setup + +```tsx +import { useLiveQuery, eq, not } from '@tanstack/octane-db' + +function TodoList() { + const { data: todos, isLoading } = useLiveQuery((q) => + q + .from({ todo: todoCollection }) + .where(({ todo }) => not(todo.completed)) + .orderBy(({ todo }) => todo.created_at, 'asc'), + ) + + if (isLoading) return
Loading...
+ + return ( + + ) +} +``` + +`@tanstack/octane-db` re-exports everything from `@tanstack/db`. In Octane projects, import everything from `@tanstack/octane-db`. + +## Hooks + +### useLiveQuery + +```tsx +// Query function with dependency array +const { + data, + state, + collection, + status, + isLoading, + isReady, + isError, + isIdle, + isCleanedUp, +} = useLiveQuery( + (q) => + q + .from({ todo: todoCollection }) + .where(({ todo }) => eq(todo.userId, userId)), + [userId], +) + +// Config object +const { data } = useLiveQuery({ + query: (q) => q.from({ todo: todoCollection }), + gcTime: 60000, +}) + +// Pre-created collection (from route loader) +const { data } = useLiveQuery(preloadedCollection) + +// Conditional query — return undefined/null to disable +const { data, status } = useLiveQuery( + (q) => { + if (!userId) return undefined + return q + .from({ todo: todoCollection }) + .where(({ todo }) => eq(todo.userId, userId)) + }, + [userId], +) +// When disabled: status='disabled', data=undefined +``` + +### useLiveSuspenseQuery + +```tsx +// data is ALWAYS defined — never undefined +// Must wrap in and +function TodoList() { + const { data: todos } = useLiveSuspenseQuery((q) => + q.from({ todo: todoCollection }), + ) + + return ( + + ) +} + +// With deps — re-suspends when deps change +const { data } = useLiveSuspenseQuery( + (q) => + q + .from({ todo: todoCollection }) + .where(({ todo }) => eq(todo.category, category)), + [category], +) +``` + +### useLiveInfiniteQuery + +```tsx +const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = + useLiveInfiniteQuery( + (q) => + q + .from({ posts: postsCollection }) + .where(({ posts }) => eq(posts.category, category)) + .orderBy(({ posts }) => posts.createdAt, 'desc'), + { pageSize: 20 }, + [category], + ) + +// data is the flat array of all loaded pages +// fetchNextPage() loads the next page +// hasNextPage is true when more data is available +``` + +### usePacedMutations + +```tsx +import { usePacedMutations, debounceStrategy } from "@tanstack/octane-db" + +const mutate = usePacedMutations({ + onMutate: (value: string) => { + noteCollection.update(noteId, (draft) => { + draft.content = value + }) + }, + mutationFn: async ({ transaction }) => { + await api.notes.update(noteId, transaction.mutations[0].changes) + }, + strategy: debounceStrategy({ wait: 500 }), +}) + +// In handler: +