diff --git a/.changeset/cute-falcons-wear.md b/.changeset/cute-falcons-wear.md new file mode 100644 index 000000000..85447c9dc --- /dev/null +++ b/.changeset/cute-falcons-wear.md @@ -0,0 +1,49 @@ +--- +'@tanstack/solid-db': minor +--- + +Update solid-db to enable suspense support. +You can now run do + +```tsx +// Use Suspense boundaries +const todosQuery = useLiveQuery((q) => q.from({ todos: todoCollection })) + +return ( + <> + {/* Status and other getters don't trigger Suspense */} +
Status {todosQuery.status}
+
Loading {todosQuery.isLoading ? 'yes' : 'no'}
+ + Loading...}> + + {(todo) =>
  • {todo.text}
  • } +
    +
    + +) +``` + +All values returned from useLiveQuery are now getters, so no longer need to be called as functions. This is a breaking change. This is to match how createResource works, and everything still stays reactive. + +```tsx +const todos = useLiveQuery(() => existingCollection) + +const handleToggle = (id) => { + // Can now access collection directly + todos.collection.update(id, (draft) => { + draft.completed = !draft.completed + }) +} + +return ( + <> + {/* Status and other getters don't trigger Suspense */} +
    Status {todos.status}
    +
    Loading {todos.isLoading ? 'yes' : 'no'}
    +
    Ready {todos.isReady ? 'yes' : 'no'}
    +
    Idle {todos.isIdle ? 'yes' : 'no'}
    +
    Error {todos.isError ? 'yes' : 'no'}
    + +) +``` diff --git a/examples/solid/todo/src/db/validation.ts b/examples/solid/todo/src/db/validation.ts index a3e71c7af..2fa8408af 100644 --- a/examples/solid/todo/src/db/validation.ts +++ b/examples/solid/todo/src/db/validation.ts @@ -1,34 +1,24 @@ import { createInsertSchema, createSelectSchema } from 'drizzle-zod' -import { z } from 'zod' import { config, todos } from './schema' +import type { z } from 'zod' -// Date transformation schema - handles Date objects, ISO strings, and parseable date strings -const dateStringToDate = z - .union([ - z.date(), // Already a Date object - z - .string() - .datetime() - .transform((str) => new Date(str)), // ISO datetime string - z.string().transform((str) => new Date(str)), // Any parseable date string - ]) - .optional() - -// Auto-generated schemas from Drizzle schema with date transformation -export const insertTodoSchema = createInsertSchema(todos, { - created_at: dateStringToDate, - updated_at: dateStringToDate, +// Auto-generated schemas from Drizzle schema (omit auto-generated fields) +export const insertTodoSchema = createInsertSchema(todos).omit({ + id: true, + created_at: true, + updated_at: true, }) export const selectTodoSchema = createSelectSchema(todos) // Partial schema for updates export const updateTodoSchema = insertTodoSchema.partial().strict() -// Config schemas with date transformation -export const insertConfigSchema = createInsertSchema(config, { - created_at: dateStringToDate, - updated_at: dateStringToDate, -}).strict() +// Config schemas (omit auto-generated fields) +export const insertConfigSchema = createInsertSchema(config).omit({ + id: true, + created_at: true, + updated_at: true, +}) export const selectConfigSchema = createSelectSchema(config) export const updateConfigSchema = insertConfigSchema.partial().strict() diff --git a/examples/solid/todo/src/main.tsx b/examples/solid/todo/src/main.tsx deleted file mode 100644 index 5384d2a20..000000000 --- a/examples/solid/todo/src/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { RouterProvider } from '@tanstack/solid-router' -import { createRouter } from './router' -import './index.css' -import { render } from 'solid-js/web' - -const router = createRouter() - -render( - () => , - document.getElementById(`root`)!, -) diff --git a/examples/solid/todo/src/routeTree.gen.ts b/examples/solid/todo/src/routeTree.gen.ts index 149aec6dc..e567fc6d2 100644 --- a/examples/solid/todo/src/routeTree.gen.ts +++ b/examples/solid/todo/src/routeTree.gen.ts @@ -8,19 +8,15 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { createServerRootRoute } from '@tanstack/solid-start/server' - import { Route as rootRouteImport } from './routes/__root' import { Route as TrailbaseRouteImport } from './routes/trailbase' import { Route as QueryRouteImport } from './routes/query' import { Route as ElectricRouteImport } from './routes/electric' import { Route as IndexRouteImport } from './routes/index' -import { ServerRoute as ApiTodosServerRouteImport } from './routes/api/todos' -import { ServerRoute as ApiConfigServerRouteImport } from './routes/api/config' -import { ServerRoute as ApiTodosIdServerRouteImport } from './routes/api/todos.$id' -import { ServerRoute as ApiConfigIdServerRouteImport } from './routes/api/config.$id' - -const rootServerRouteImport = createServerRootRoute() +import { Route as ApiTodosRouteImport } from './routes/api/todos' +import { Route as ApiConfigRouteImport } from './routes/api/config' +import { Route as ApiTodosIdRouteImport } from './routes/api/todos.$id' +import { Route as ApiConfigIdRouteImport } from './routes/api/config.$id' const TrailbaseRoute = TrailbaseRouteImport.update({ id: '/trailbase', @@ -42,25 +38,25 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) -const ApiTodosServerRoute = ApiTodosServerRouteImport.update({ +const ApiTodosRoute = ApiTodosRouteImport.update({ id: '/api/todos', path: '/api/todos', - getParentRoute: () => rootServerRouteImport, + getParentRoute: () => rootRouteImport, } as any) -const ApiConfigServerRoute = ApiConfigServerRouteImport.update({ +const ApiConfigRoute = ApiConfigRouteImport.update({ id: '/api/config', path: '/api/config', - getParentRoute: () => rootServerRouteImport, + getParentRoute: () => rootRouteImport, } as any) -const ApiTodosIdServerRoute = ApiTodosIdServerRouteImport.update({ +const ApiTodosIdRoute = ApiTodosIdRouteImport.update({ id: '/$id', path: '/$id', - getParentRoute: () => ApiTodosServerRoute, + getParentRoute: () => ApiTodosRoute, } as any) -const ApiConfigIdServerRoute = ApiConfigIdServerRouteImport.update({ +const ApiConfigIdRoute = ApiConfigIdRouteImport.update({ id: '/$id', path: '/$id', - getParentRoute: () => ApiConfigServerRoute, + getParentRoute: () => ApiConfigRoute, } as any) export interface FileRoutesByFullPath { @@ -68,12 +64,20 @@ export interface FileRoutesByFullPath { '/electric': typeof ElectricRoute '/query': typeof QueryRoute '/trailbase': typeof TrailbaseRoute + '/api/config': typeof ApiConfigRouteWithChildren + '/api/todos': typeof ApiTodosRouteWithChildren + '/api/config/$id': typeof ApiConfigIdRoute + '/api/todos/$id': typeof ApiTodosIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/electric': typeof ElectricRoute '/query': typeof QueryRoute '/trailbase': typeof TrailbaseRoute + '/api/config': typeof ApiConfigRouteWithChildren + '/api/todos': typeof ApiTodosRouteWithChildren + '/api/config/$id': typeof ApiConfigIdRoute + '/api/todos/$id': typeof ApiTodosIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -81,56 +85,51 @@ export interface FileRoutesById { '/electric': typeof ElectricRoute '/query': typeof QueryRoute '/trailbase': typeof TrailbaseRoute + '/api/config': typeof ApiConfigRouteWithChildren + '/api/todos': typeof ApiTodosRouteWithChildren + '/api/config/$id': typeof ApiConfigIdRoute + '/api/todos/$id': typeof ApiTodosIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/electric' | '/query' | '/trailbase' + fullPaths: + | '/' + | '/electric' + | '/query' + | '/trailbase' + | '/api/config' + | '/api/todos' + | '/api/config/$id' + | '/api/todos/$id' fileRoutesByTo: FileRoutesByTo - to: '/' | '/electric' | '/query' | '/trailbase' - id: '__root__' | '/' | '/electric' | '/query' | '/trailbase' - fileRoutesById: FileRoutesById -} -export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - ElectricRoute: typeof ElectricRoute - QueryRoute: typeof QueryRoute - TrailbaseRoute: typeof TrailbaseRoute -} -export interface FileServerRoutesByFullPath { - '/api/config': typeof ApiConfigServerRouteWithChildren - '/api/todos': typeof ApiTodosServerRouteWithChildren - '/api/config/$id': typeof ApiConfigIdServerRoute - '/api/todos/$id': typeof ApiTodosIdServerRoute -} -export interface FileServerRoutesByTo { - '/api/config': typeof ApiConfigServerRouteWithChildren - '/api/todos': typeof ApiTodosServerRouteWithChildren - '/api/config/$id': typeof ApiConfigIdServerRoute - '/api/todos/$id': typeof ApiTodosIdServerRoute -} -export interface FileServerRoutesById { - __root__: typeof rootServerRouteImport - '/api/config': typeof ApiConfigServerRouteWithChildren - '/api/todos': typeof ApiTodosServerRouteWithChildren - '/api/config/$id': typeof ApiConfigIdServerRoute - '/api/todos/$id': typeof ApiTodosIdServerRoute -} -export interface FileServerRouteTypes { - fileServerRoutesByFullPath: FileServerRoutesByFullPath - fullPaths: '/api/config' | '/api/todos' | '/api/config/$id' | '/api/todos/$id' - fileServerRoutesByTo: FileServerRoutesByTo - to: '/api/config' | '/api/todos' | '/api/config/$id' | '/api/todos/$id' + to: + | '/' + | '/electric' + | '/query' + | '/trailbase' + | '/api/config' + | '/api/todos' + | '/api/config/$id' + | '/api/todos/$id' id: | '__root__' + | '/' + | '/electric' + | '/query' + | '/trailbase' | '/api/config' | '/api/todos' | '/api/config/$id' | '/api/todos/$id' - fileServerRoutesById: FileServerRoutesById + fileRoutesById: FileRoutesById } -export interface RootServerRouteChildren { - ApiConfigServerRoute: typeof ApiConfigServerRouteWithChildren - ApiTodosServerRoute: typeof ApiTodosServerRouteWithChildren +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ElectricRoute: typeof ElectricRoute + QueryRoute: typeof QueryRoute + TrailbaseRoute: typeof TrailbaseRoute + ApiConfigRoute: typeof ApiConfigRouteWithChildren + ApiTodosRoute: typeof ApiTodosRouteWithChildren } declare module '@tanstack/solid-router' { @@ -163,63 +162,59 @@ declare module '@tanstack/solid-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - } -} -declare module '@tanstack/solid-start/server' { - interface ServerFileRoutesByPath { '/api/todos': { id: '/api/todos' path: '/api/todos' fullPath: '/api/todos' - preLoaderRoute: typeof ApiTodosServerRouteImport - parentRoute: typeof rootServerRouteImport + preLoaderRoute: typeof ApiTodosRouteImport + parentRoute: typeof rootRouteImport } '/api/config': { id: '/api/config' path: '/api/config' fullPath: '/api/config' - preLoaderRoute: typeof ApiConfigServerRouteImport - parentRoute: typeof rootServerRouteImport + preLoaderRoute: typeof ApiConfigRouteImport + parentRoute: typeof rootRouteImport } '/api/todos/$id': { id: '/api/todos/$id' path: '/$id' fullPath: '/api/todos/$id' - preLoaderRoute: typeof ApiTodosIdServerRouteImport - parentRoute: typeof ApiTodosServerRoute + preLoaderRoute: typeof ApiTodosIdRouteImport + parentRoute: typeof ApiTodosRoute } '/api/config/$id': { id: '/api/config/$id' path: '/$id' fullPath: '/api/config/$id' - preLoaderRoute: typeof ApiConfigIdServerRouteImport - parentRoute: typeof ApiConfigServerRoute + preLoaderRoute: typeof ApiConfigIdRouteImport + parentRoute: typeof ApiConfigRoute } } } -interface ApiConfigServerRouteChildren { - ApiConfigIdServerRoute: typeof ApiConfigIdServerRoute +interface ApiConfigRouteChildren { + ApiConfigIdRoute: typeof ApiConfigIdRoute } -const ApiConfigServerRouteChildren: ApiConfigServerRouteChildren = { - ApiConfigIdServerRoute: ApiConfigIdServerRoute, +const ApiConfigRouteChildren: ApiConfigRouteChildren = { + ApiConfigIdRoute: ApiConfigIdRoute, } -const ApiConfigServerRouteWithChildren = ApiConfigServerRoute._addFileChildren( - ApiConfigServerRouteChildren, +const ApiConfigRouteWithChildren = ApiConfigRoute._addFileChildren( + ApiConfigRouteChildren, ) -interface ApiTodosServerRouteChildren { - ApiTodosIdServerRoute: typeof ApiTodosIdServerRoute +interface ApiTodosRouteChildren { + ApiTodosIdRoute: typeof ApiTodosIdRoute } -const ApiTodosServerRouteChildren: ApiTodosServerRouteChildren = { - ApiTodosIdServerRoute: ApiTodosIdServerRoute, +const ApiTodosRouteChildren: ApiTodosRouteChildren = { + ApiTodosIdRoute: ApiTodosIdRoute, } -const ApiTodosServerRouteWithChildren = ApiTodosServerRoute._addFileChildren( - ApiTodosServerRouteChildren, +const ApiTodosRouteWithChildren = ApiTodosRoute._addFileChildren( + ApiTodosRouteChildren, ) const rootRouteChildren: RootRouteChildren = { @@ -227,14 +222,19 @@ const rootRouteChildren: RootRouteChildren = { ElectricRoute: ElectricRoute, QueryRoute: QueryRoute, TrailbaseRoute: TrailbaseRoute, + ApiConfigRoute: ApiConfigRouteWithChildren, + ApiTodosRoute: ApiTodosRouteWithChildren, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() -const rootServerRouteChildren: RootServerRouteChildren = { - ApiConfigServerRoute: ApiConfigServerRouteWithChildren, - ApiTodosServerRoute: ApiTodosServerRouteWithChildren, + +import type { getRouter } from './router.tsx' +import type { startInstance } from './start.tsx' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + config: Awaited> + } } -export const serverRouteTree = rootServerRouteImport - ._addFileChildren(rootServerRouteChildren) - ._addFileTypes() diff --git a/examples/solid/todo/src/router.tsx b/examples/solid/todo/src/router.tsx index 7d743ad93..35abef268 100644 --- a/examples/solid/todo/src/router.tsx +++ b/examples/solid/todo/src/router.tsx @@ -7,20 +7,11 @@ import { NotFound } from './components/NotFound' import './styles.css' // Create a new router instance -export const createRouter = () => { - const router = createTanstackRouter({ +export function getRouter() { + return createTanstackRouter({ routeTree, scrollRestoration: true, defaultPreloadStaleTime: 0, defaultNotFoundComponent: NotFound, }) - - return router -} - -// Register the router instance for type safety -declare module '@tanstack/solid-router' { - interface Register { - router: ReturnType - } } diff --git a/examples/solid/todo/src/routes/__root.tsx b/examples/solid/todo/src/routes/__root.tsx index cf695549c..dde228f17 100644 --- a/examples/solid/todo/src/routes/__root.tsx +++ b/examples/solid/todo/src/routes/__root.tsx @@ -1,6 +1,12 @@ -import { Outlet, createRootRoute } from '@tanstack/solid-router' - +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from 'solid-js/web' import appCss from '../styles.css?url' +import type { ParentProps } from 'solid-js' export const Route = createRootRoute({ head: () => ({ @@ -23,6 +29,21 @@ export const Route = createRootRoute({ }, ], }), - + shellComponent: RootDocument, component: () => , }) + +function RootDocument(props: ParentProps) { + return ( + + + + + + + {props.children} + + + + ) +} diff --git a/examples/solid/todo/src/routes/api/config.$id.ts b/examples/solid/todo/src/routes/api/config.$id.ts index e4a751091..160aee8b1 100644 --- a/examples/solid/todo/src/routes/api/config.$id.ts +++ b/examples/solid/todo/src/routes/api/config.$id.ts @@ -1,4 +1,4 @@ -import { createServerFileRoute } from '@tanstack/solid-start/server' +import { createFileRoute } from '@tanstack/solid-router' import { json } from '@tanstack/solid-start' import { sql } from '../../db/postgres' import { validateUpdateConfig } from '../../db/validation' @@ -16,101 +16,105 @@ async function generateTxId(tx: any): Promise { return parseInt(txid, 10) } -export const ServerRoute = createServerFileRoute(`/api/config/$id`).methods({ - GET: async ({ params }) => { - try { - const { id } = params - const [config] = await sql`SELECT * FROM config WHERE id = ${id}` - - if (!config) { - return json({ error: `Config not found` }, { status: 404 }) - } - - return json(config) - } catch (error) { - console.error(`Error fetching config:`, error) - return json( - { - error: `Failed to fetch config`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - PUT: async ({ params, request }) => { - try { - const { id } = params - const body = await request.json() - const configData = validateUpdateConfig(body) - - let txid!: Txid - const updatedConfig = await sql.begin(async (tx) => { - txid = await generateTxId(tx) - - const [result] = await tx` +export const Route = createFileRoute(`/api/config/$id`)({ + server: { + handlers: { + GET: async ({ params }) => { + try { + const { id } = params + const [config] = await sql`SELECT * FROM config WHERE id = ${id}` + + if (!config) { + return json({ error: `Config not found` }, { status: 404 }) + } + + return json(config) + } catch (error) { + console.error(`Error fetching config:`, error) + return json( + { + error: `Failed to fetch config`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + PUT: async ({ params, request }) => { + try { + const { id } = params + const body = await request.json() + const configData = validateUpdateConfig(body) + + let txid!: Txid + const updatedConfig = await sql.begin(async (tx) => { + txid = await generateTxId(tx) + + const [result] = await tx` UPDATE config SET ${tx(configData)} WHERE id = ${id} RETURNING * ` - if (!result) { - throw new Error(`Config not found`) + if (!result) { + throw new Error(`Config not found`) + } + + return result + }) + + return json({ config: updatedConfig, txid }) + } catch (error) { + if (error instanceof Error && error.message === `Config not found`) { + return json({ error: `Config not found` }, { status: 404 }) + } + + console.error(`Error updating config:`, error) + return json( + { + error: `Failed to update config`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) } + }, + DELETE: async ({ params }) => { + try { + const { id } = params - return result - }) - - return json({ config: updatedConfig, txid }) - } catch (error) { - if (error instanceof Error && error.message === `Config not found`) { - return json({ error: `Config not found` }, { status: 404 }) - } - - console.error(`Error updating config:`, error) - return json( - { - error: `Failed to update config`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - DELETE: async ({ params }) => { - try { - const { id } = params - - let txid!: Txid - await sql.begin(async (tx) => { - txid = await generateTxId(tx) + let txid!: Txid + await sql.begin(async (tx) => { + txid = await generateTxId(tx) - const [result] = await tx` + const [result] = await tx` DELETE FROM config WHERE id = ${id} RETURNING id ` - if (!result) { - throw new Error(`Config not found`) + if (!result) { + throw new Error(`Config not found`) + } + }) + + return json({ success: true, txid }) + } catch (error) { + if (error instanceof Error && error.message === `Config not found`) { + return json({ error: `Config not found` }, { status: 404 }) + } + + console.error(`Error deleting config:`, error) + return json( + { + error: `Failed to delete config`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) } - }) - - return json({ success: true, txid }) - } catch (error) { - if (error instanceof Error && error.message === `Config not found`) { - return json({ error: `Config not found` }, { status: 404 }) - } - - console.error(`Error deleting config:`, error) - return json( - { - error: `Failed to delete config`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } + }, + }, }, }) diff --git a/examples/solid/todo/src/routes/api/config.ts b/examples/solid/todo/src/routes/api/config.ts index 7d25c555b..cd0cd5657 100644 --- a/examples/solid/todo/src/routes/api/config.ts +++ b/examples/solid/todo/src/routes/api/config.ts @@ -1,4 +1,4 @@ -import { createServerFileRoute } from '@tanstack/solid-start/server' +import { createFileRoute } from '@tanstack/solid-router' import { json } from '@tanstack/solid-start' import { sql } from '../../db/postgres' import { validateInsertConfig } from '../../db/validation' @@ -16,49 +16,53 @@ async function generateTxId(tx: any): Promise { return parseInt(txid, 10) } -export const ServerRoute = createServerFileRoute(`/api/config`).methods({ - GET: async ({ request: _request }) => { - try { - const config = await sql`SELECT * FROM config` - return json(config) - } catch (error) { - console.error(`Error fetching config:`, error) - return json( - { - error: `Failed to fetch config`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - POST: async ({ request }) => { - try { - const body = await request.json() - console.log(`POST /api/config`, body) - const configData = validateInsertConfig(body) +export const Route = createFileRoute(`/api/config`)({ + server: { + handlers: { + GET: async ({ request: _request }) => { + try { + const config = await sql`SELECT * FROM config` + return json(config) + } catch (error) { + console.error(`Error fetching config:`, error) + return json( + { + error: `Failed to fetch config`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + POST: async ({ request }) => { + try { + const body = await request.json() + console.log(`POST /api/config`, body) + const configData = validateInsertConfig(body) - let txid!: Txid - const newConfig = await sql.begin(async (tx) => { - txid = await generateTxId(tx) + let txid!: Txid + const newConfig = await sql.begin(async (tx) => { + txid = await generateTxId(tx) - const [result] = await tx` + const [result] = await tx` INSERT INTO config ${tx(configData)} RETURNING * ` - return result - }) + return result + }) - return json({ config: newConfig, txid }, { status: 201 }) - } catch (error) { - console.error(`Error creating config:`, error) - return json( - { - error: `Failed to create config`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } + return json({ config: newConfig, txid }, { status: 201 }) + } catch (error) { + console.error(`Error creating config:`, error) + return json( + { + error: `Failed to create config`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + }, }, }) diff --git a/examples/solid/todo/src/routes/api/todos.$id.ts b/examples/solid/todo/src/routes/api/todos.$id.ts index 32d6ce9f4..3b2b742cd 100644 --- a/examples/solid/todo/src/routes/api/todos.$id.ts +++ b/examples/solid/todo/src/routes/api/todos.$id.ts @@ -1,4 +1,4 @@ -import { createServerFileRoute } from '@tanstack/solid-start/server' +import { createFileRoute } from '@tanstack/solid-router' import { json } from '@tanstack/solid-start' import { sql } from '../../db/postgres' import { validateUpdateTodo } from '../../db/validation' @@ -16,101 +16,105 @@ async function generateTxId(tx: any): Promise { return parseInt(txid, 10) } -export const ServerRoute = createServerFileRoute(`/api/todos/$id`).methods({ - GET: async ({ params }) => { - try { - const { id } = params - const [todo] = await sql`SELECT * FROM todos WHERE id = ${id}` - - if (!todo) { - return json({ error: `Todo not found` }, { status: 404 }) - } - - return json(todo) - } catch (error) { - console.error(`Error fetching todo:`, error) - return json( - { - error: `Failed to fetch todo`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - PUT: async ({ params, request }) => { - try { - const { id } = params - const body = await request.json() - const todoData = validateUpdateTodo(body) - - let txid!: Txid - const updatedTodo = await sql.begin(async (tx) => { - txid = await generateTxId(tx) - - const [result] = await tx` +export const Route = createFileRoute(`/api/todos/$id`)({ + server: { + handlers: { + GET: async ({ params }) => { + try { + const { id } = params + const [todo] = await sql`SELECT * FROM todos WHERE id = ${id}` + + if (!todo) { + return json({ error: `Todo not found` }, { status: 404 }) + } + + return json(todo) + } catch (error) { + console.error(`Error fetching todo:`, error) + return json( + { + error: `Failed to fetch todo`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + PUT: async ({ params, request }) => { + try { + const { id } = params + const body = await request.json() + const todoData = validateUpdateTodo(body) + + let txid!: Txid + const updatedTodo = await sql.begin(async (tx) => { + txid = await generateTxId(tx) + + const [result] = await tx` UPDATE todos SET ${tx(todoData)} WHERE id = ${id} RETURNING * ` - if (!result) { - throw new Error(`Todo not found`) + if (!result) { + throw new Error(`Todo not found`) + } + + return result + }) + + return json({ todo: updatedTodo, txid }) + } catch (error) { + if (error instanceof Error && error.message === `Todo not found`) { + return json({ error: `Todo not found` }, { status: 404 }) + } + + console.error(`Error updating todo:`, error) + return json( + { + error: `Failed to update todo`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) } + }, + DELETE: async ({ params }) => { + try { + const { id } = params - return result - }) - - return json({ todo: updatedTodo, txid }) - } catch (error) { - if (error instanceof Error && error.message === `Todo not found`) { - return json({ error: `Todo not found` }, { status: 404 }) - } - - console.error(`Error updating todo:`, error) - return json( - { - error: `Failed to update todo`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - DELETE: async ({ params }) => { - try { - const { id } = params - - let txid!: Txid - await sql.begin(async (tx) => { - txid = await generateTxId(tx) + let txid!: Txid + await sql.begin(async (tx) => { + txid = await generateTxId(tx) - const [result] = await tx` + const [result] = await tx` DELETE FROM todos WHERE id = ${id} RETURNING id ` - if (!result) { - throw new Error(`Todo not found`) + if (!result) { + throw new Error(`Todo not found`) + } + }) + + return json({ success: true, txid }) + } catch (error) { + if (error instanceof Error && error.message === `Todo not found`) { + return json({ error: `Todo not found` }, { status: 404 }) + } + + console.error(`Error deleting todo:`, error) + return json( + { + error: `Failed to delete todo`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) } - }) - - return json({ success: true, txid }) - } catch (error) { - if (error instanceof Error && error.message === `Todo not found`) { - return json({ error: `Todo not found` }, { status: 404 }) - } - - console.error(`Error deleting todo:`, error) - return json( - { - error: `Failed to delete todo`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } + }, + }, }, }) diff --git a/examples/solid/todo/src/routes/api/todos.ts b/examples/solid/todo/src/routes/api/todos.ts index 931d5996b..b28c649c1 100644 --- a/examples/solid/todo/src/routes/api/todos.ts +++ b/examples/solid/todo/src/routes/api/todos.ts @@ -1,4 +1,4 @@ -import { createServerFileRoute } from '@tanstack/solid-start/server' +import { createFileRoute } from '@tanstack/solid-router' import { json } from '@tanstack/solid-start' import { sql } from '../../db/postgres' import { validateInsertTodo } from '../../db/validation' @@ -20,48 +20,52 @@ async function generateTxId(tx: any): Promise { return parseInt(txid, 10) } -export const ServerRoute = createServerFileRoute(`/api/todos`).methods({ - GET: async ({ request: _request }) => { - try { - const todos = await sql`SELECT * FROM todos` - return json(todos) - } catch (error) { - console.error(`Error fetching todos:`, error) - return json( - { - error: `Failed to fetch todos`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } - }, - POST: async ({ request }) => { - try { - const body = await request.json() - const todoData = validateInsertTodo(body) +export const Route = createFileRoute(`/api/todos`)({ + server: { + handlers: { + GET: async ({ request: _request }) => { + try { + const todos = await sql`SELECT * FROM todos` + return json(todos) + } catch (error) { + console.error(`Error fetching todos:`, error) + return json( + { + error: `Failed to fetch todos`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + POST: async ({ request }) => { + try { + const body = await request.json() + const todoData = validateInsertTodo(body) - let txid!: Txid - const newTodo = await sql.begin(async (tx) => { - txid = await generateTxId(tx) + let txid!: Txid + const newTodo = await sql.begin(async (tx) => { + txid = await generateTxId(tx) - const [result] = await tx` + const [result] = await tx` INSERT INTO todos ${tx(todoData)} RETURNING * ` - return result - }) + return result + }) - return json({ todo: newTodo, txid }, { status: 201 }) - } catch (error) { - console.error(`Error creating todo:`, error) - return json( - { - error: `Failed to create todo`, - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ) - } + return json({ todo: newTodo, txid }, { status: 201 }) + } catch (error) { + console.error(`Error creating todo:`, error) + return json( + { + error: `Failed to create todo`, + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 }, + ) + } + }, + }, }, }) diff --git a/examples/solid/todo/src/routes/electric.tsx b/examples/solid/todo/src/routes/electric.tsx index 2bf391cfe..63442e719 100644 --- a/examples/solid/todo/src/routes/electric.tsx +++ b/examples/solid/todo/src/routes/electric.tsx @@ -1,5 +1,6 @@ import { createFileRoute } from '@tanstack/solid-router' import { useLiveQuery } from '@tanstack/solid-db' +import { Suspense } from 'solid-js' import { electricConfigCollection, electricTodoCollection, @@ -21,23 +22,25 @@ export const Route = createFileRoute(`/electric`)({ function ElectricPage() { // Get data using live queries with Electric collections - const { data: todos } = useLiveQuery((q) => + const todos = useLiveQuery((q) => q .from({ todo: electricTodoCollection }) .orderBy(({ todo }) => todo.created_at, `asc`), ) - const { data: configData } = useLiveQuery((q) => + const configData = useLiveQuery((q) => q.from({ config: electricConfigCollection }), ) return ( - + + + ) } diff --git a/examples/solid/todo/src/routes/query.tsx b/examples/solid/todo/src/routes/query.tsx index 11448cffd..5523668ac 100644 --- a/examples/solid/todo/src/routes/query.tsx +++ b/examples/solid/todo/src/routes/query.tsx @@ -1,5 +1,6 @@ import { createFileRoute } from '@tanstack/solid-router' import { useLiveQuery } from '@tanstack/solid-db' +import { Suspense } from 'solid-js' import { queryConfigCollection, queryTodoCollection } from '../lib/collections' import { TodoApp } from '../components/TodoApp' @@ -18,23 +19,25 @@ export const Route = createFileRoute(`/query`)({ function QueryPage() { // Get data using live queries with Query collections - const { data: todos } = useLiveQuery((q) => + const todos = useLiveQuery((q) => q .from({ todo: queryTodoCollection }) .orderBy(({ todo }) => todo.created_at, `asc`), ) - const { data: configData } = useLiveQuery((q) => + const configData = useLiveQuery((q) => q.from({ config: queryConfigCollection }), ) return ( - + + + ) } diff --git a/examples/solid/todo/src/server.ts b/examples/solid/todo/src/server.ts new file mode 100644 index 000000000..3682c04c2 --- /dev/null +++ b/examples/solid/todo/src/server.ts @@ -0,0 +1,7 @@ +import handler from '@tanstack/solid-start/server-entry' + +export default { + fetch(request: Request) { + return handler.fetch(request) + }, +} diff --git a/examples/solid/todo/src/start.tsx b/examples/solid/todo/src/start.tsx new file mode 100644 index 000000000..9c70dc5c4 --- /dev/null +++ b/examples/solid/todo/src/start.tsx @@ -0,0 +1,7 @@ +import { createStart } from '@tanstack/solid-start' + +export const startInstance = createStart(() => { + return { + defaultSsr: false, + } +}) diff --git a/examples/solid/todo/vite.config.ts b/examples/solid/todo/vite.config.ts index f66ba076c..620fb5e6d 100644 --- a/examples/solid/todo/vite.config.ts +++ b/examples/solid/todo/vite.config.ts @@ -13,11 +13,9 @@ export default defineConfig({ }), tailwindcss(), tanstackStart({ - customViteSolidPlugin: true, - spa: { - prerender: { enabled: false }, - enabled: true, - }, + srcDirectory: `src`, + start: { entry: `./start.tsx` }, + server: { entry: `./server.ts` }, }), solid({ ssr: true }), ], diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index b804d3d5f..4fa7d264c 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -1,6 +1,6 @@ import { batch, - createComputed, + createEffect, createMemo, createResource, createSignal, @@ -28,7 +28,7 @@ import type { /** * Create a live query using a query function * @param queryFn - Query function that defines what data to fetch - * @returns Object with reactive data, state, and status information + * @returns Accessor that returns data with Suspense support, with state and status infomation as properties * @example * // Basic query with object syntax * const todosQuery = useLiveQuery((q) => @@ -66,33 +66,51 @@ import type { * * return ( * - * + * *
    Loading...
    *
    - * + * *
    Error: {todosQuery.status()}
    *
    - * - * + * + * * {(todo) =>
  • {todo.text}
  • } *
    *
    *
    * ) + * + * @example + * // Use Suspense boundaries + * const todosQuery = useLiveQuery((q) => + * q.from({ todos: todoCollection }) + * ) + * + * return ( + * Loading...}> + * + * {(todo) =>
  • {todo.text}
  • } + *
    + *
    + * ) */ // Overload 1: Accept query function that always returns QueryBuilder export function useLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, -): { - state: ReactiveMap> +): Accessor>> & { + /** + * @deprecated use function result instead + * query.data -> query() + */ data: Array> - collection: Accessor, string | number, {}>> - status: Accessor - isLoading: Accessor - isReady: Accessor - isIdle: Accessor - isError: Accessor - isCleanedUp: Accessor + state: ReactiveMap> + collection: Collection, string | number, {}> + status: CollectionStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean } // Overload 1b: Accept query function that can return undefined/null @@ -100,7 +118,7 @@ export function useLiveQuery( queryFn: ( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, -): { +): Accessor>> & { state: ReactiveMap> data: Array> collection: Accessor( /** * Create a live query using configuration object * @param config - Configuration object with query and options - * @returns Object with reactive data, state, and status information + * @returns Accessor that returns data with Suspense support, with state and status infomation as properties * @example * // Basic config object usage * const todosQuery = useLiveQuery(() => ({ @@ -143,14 +161,14 @@ export function useLiveQuery( * })) * * return ( - * {itemsQuery.data.length} items loaded}> - * + * {itemsQuery().length} items loaded}> + * *
    Loading...
    *
    - * + * *
    Something went wrong
    *
    - * + * *
    Preparing...
    *
    *
    @@ -159,22 +177,26 @@ export function useLiveQuery( // Overload 2: Accept config object export function useLiveQuery( config: Accessor>, -): { - state: ReactiveMap> +): Accessor>> & { + /** + * @deprecated use function result instead + * query.data -> query() + */ data: Array> - collection: Accessor, string | number, {}>> - status: Accessor - isLoading: Accessor - isReady: Accessor - isIdle: Accessor - isError: Accessor - isCleanedUp: Accessor + state: ReactiveMap> + collection: Collection, string | number, {}> + status: CollectionStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean } /** * Subscribe to an existing live query collection * @param liveQueryCollection - Pre-created live query collection to subscribe to - * @returns Object with reactive data, state, and status information + * @returns Accessor that returns data with Suspense support, with state and status infomation as properties * @example * // Using pre-created live query collection * const myLiveQuery = createLiveQueryCollection((q) => @@ -188,7 +210,7 @@ export function useLiveQuery( * * // Use collection for mutations * const handleToggle = (id) => { - * existingQuery.collection().update(id, draft => { draft.completed = !draft.completed }) + * existingQuery.collection.update(id, draft => { draft.completed = !draft.completed }) * } * * @example @@ -196,11 +218,11 @@ export function useLiveQuery( * const sharedQuery = useLiveQuery(() => sharedCollection) * * return ( - * {(item) => }}> - * + * {(item) => }}> + * *
    Loading...
    *
    - * + * *
    Error loading data
    *
    *
    @@ -213,16 +235,20 @@ export function useLiveQuery< TUtils extends Record, >( liveQueryCollection: Accessor>, -): { - state: ReactiveMap +): Accessor> & { + /** + * @deprecated use function result instead + * query.data -> query() + */ data: Array - collection: Accessor> - status: Accessor - isLoading: Accessor - isReady: Accessor - isIdle: Accessor - isError: Accessor - isCleanedUp: Accessor + state: ReactiveMap + collection: Collection + status: CollectionStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean } // Implementation - use function overloads to infer the actual collection type @@ -293,91 +319,121 @@ export function useLiveQuery( ) } - // Track current unsubscribe function - let currentUnsubscribe: (() => void) | null = null - - createComputed( - () => { - const currentCollection = collection() - - // Handle null collection (disabled query) + const [getDataResource] = createResource( + () => ({ currentCollection: collection() }), + async ({ currentCollection }) => { if (!currentCollection) { - setStatus(`disabled` as const) - state.clear() - setData([]) - if (currentUnsubscribe) { - currentUnsubscribe() - currentUnsubscribe = null - } - return + return [] } - - // Update status ref whenever the effect runs setStatus(currentCollection.status) - + await currentCollection.toArrayWhenReady() // Initialize state with current collection data - state.clear() - for (const [key, value] of currentCollection.entries()) { - state.set(key, value) - } + batch(() => { + state.clear() + for (const [key, value] of currentCollection.entries()) { + state.set(key, value) + } + syncDataFromCollection(currentCollection) + setStatus(currentCollection.status) + }) + return data + }, + { + name: `TanstackDBData`, + deferStream: false, + initialValue: data, + }, + ) - // Subscribe to collection changes with granular updates - const subscription = currentCollection.subscribeChanges( - (changes: Array>) => { - // Apply each change individually to the reactive state - batch(() => { - for (const change of changes) { - switch (change.type) { - case `insert`: - case `update`: - state.set(change.key, change.value) - break - case `delete`: - state.delete(change.key) - break - } + createEffect(() => { + const currentCollection = collection() + if (!currentCollection) { + return + } + const subscription = currentCollection.subscribeChanges( + // Changes is fine grained, so does not work great with an array + (changes: Array>) => { + // Apply each change individually to the reactive state + batch(() => { + for (const change of changes) { + switch (change.type) { + case `insert`: + case `update`: + state.set(change.key, change.value) + break + case `delete`: + state.delete(change.key) + break } - }) + } - // Update the data array to maintain sorted order syncDataFromCollection(currentCollection) // Update status ref on every change setStatus(currentCollection.status) - }, - { - includeInitialState: true, - }, - ) + }) + }, + { + includeInitialState: true, + }, + ) - currentUnsubscribe = subscription.unsubscribe.bind(subscription) + onCleanup(() => { + subscription.unsubscribe() + }) + }) - // Preload collection data if not already started - if (currentCollection.status === `idle`) { - createResource(() => currentCollection.preload()) - } + // We have to remove getters from the resource function so we wrap it + function getData() { + return getDataResource() + } - // Cleanup when computed is invalidated - onCleanup(() => { - if (currentUnsubscribe) { - currentUnsubscribe() - currentUnsubscribe = null - } - }) + Object.defineProperties(getData, { + data: { + get() { + return getData() + }, }, - undefined, - { name: `TanstackDBSyncComputed` }, - ) - - return { - state, - data, - collection, - status, - isLoading: () => status() === `loading`, - isReady: () => status() === `ready` || status() === `disabled`, - isIdle: () => status() === `idle`, - isError: () => status() === `error`, - isCleanedUp: () => status() === `cleaned-up`, - } + status: { + get() { + return status() + }, + }, + collection: { + get() { + return collection() + }, + }, + state: { + get() { + return state + }, + }, + isLoading: { + get() { + return status() === `loading` + }, + }, + isReady: { + get() { + return status() === `ready` + }, + }, + isIdle: { + get() { + return status() === `idle` + }, + }, + isError: { + get() { + return status() === `error` + }, + }, + isCleanedUp: { + get() { + return status() === `cleaned-up` + }, + }, + }) + return getData } diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index 2afca16d9..4b4641b9c 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -105,9 +105,9 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(rendered.result.state.size).toBe(1) // Only John Smith (age 35) }) - expect(rendered.result.data).toHaveLength(1) + expect(rendered.result()).toHaveLength(1) - const johnSmith = rendered.result.data[0] + const johnSmith = rendered.result()[0] expect(johnSmith).toMatchObject({ id: `3`, name: `John Smith`, @@ -146,8 +146,8 @@ describe(`Query Collections`, () => { name: `John Smith`, }) - expect(rendered.result.data.length).toBe(1) - expect(rendered.result.data[0]).toMatchObject({ + expect(rendered.result().length).toBe(1) + expect(rendered.result()[0]).toMatchObject({ id: `3`, name: `John Smith`, }) @@ -179,8 +179,8 @@ describe(`Query Collections`, () => { name: `Kyle Doe`, }) - expect(rendered.result.data.length).toBe(2) - expect(rendered.result.data).toEqual( + expect(rendered.result().length).toBe(2) + expect(rendered.result()).toEqual( expect.arrayContaining([ expect.objectContaining({ id: `3`, @@ -216,8 +216,8 @@ describe(`Query Collections`, () => { name: `Kyle Doe 2`, }) - expect(rendered.result.data.length).toBe(2) - expect(rendered.result.data).toEqual( + expect(rendered.result().length).toBe(2) + expect(rendered.result()).toEqual( expect.arrayContaining([ expect.objectContaining({ id: `3`, @@ -250,8 +250,8 @@ describe(`Query Collections`, () => { }) expect(rendered.result.state.get(`4`)).toBeUndefined() - expect(rendered.result.data.length).toBe(1) - expect(rendered.result.data[0]).toMatchObject({ + expect(rendered.result().length).toBe(1) + expect(rendered.result()[0]).toMatchObject({ id: `3`, name: `John Smith`, }) @@ -373,11 +373,11 @@ describe(`Query Collections`, () => { }) issueCollection.utils.commit() - await new Promise((resolve) => setTimeout(resolve, 10)) - - // After deletion, issue 3 should no longer have a joined result - expect(result.state.get(`[3,1]`)).toBeUndefined() - expect(result.state.size).toBe(3) + await waitFor(() => { + // After deletion, issue 3 should no longer have a joined result + expect(result.state.get(`[3,1]`)).toBeUndefined() + expect(result.state.size).toBe(3) + }) }) it(`should recompile query when parameters change and change results`, async () => { @@ -548,7 +548,7 @@ describe(`Query Collections`, () => { const groupedLiveQuery = renderHook(() => { return useLiveQuery((q) => q - .from({ queryResult: rendered.result.collection() }) + .from({ queryResult: rendered.result.collection }) .groupBy(({ queryResult }) => queryResult.team) .select(({ queryResult }) => ({ team: queryResult.team, @@ -798,9 +798,9 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(1) // Only John Smith (age 35) }) - expect(result.data).toHaveLength(1) + expect(result()).toHaveLength(1) - const johnSmith = result.data[0] + const johnSmith = result()[0] expect(johnSmith).toMatchObject({ id: `3`, name: `John Smith`, @@ -808,7 +808,7 @@ describe(`Query Collections`, () => { }) // Verify that the returned collection is the same instance - expect(result.collection()).toBe(liveQueryCollection) + expect(result.collection).toBe(liveQueryCollection) }) it(`should switch to a different pre-created live query collection when changed`, async () => { @@ -887,7 +887,7 @@ describe(`Query Collections`, () => { id: `3`, name: `John Smith`, }) - expect(rendered.result.collection()).toBe(liveQueryCollection1) + expect(rendered.result.collection).toBe(liveQueryCollection1) // Switch to the second collection setCollection(liveQueryCollection2) @@ -904,7 +904,7 @@ describe(`Query Collections`, () => { id: `5`, name: `Bob Dylan`, }) - expect(rendered.result.collection()).toBe(liveQueryCollection2) + expect(rendered.result.collection).toBe(liveQueryCollection2) // Verify we no longer have data from the first collection expect(rendered.result.state.get(`3`)).toBeUndefined() @@ -940,9 +940,9 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(1) // Only John Smith (age 35) }) - expect(result.data).toHaveLength(1) + expect(result()).toHaveLength(1) - const johnSmith = result.data[0] + const johnSmith = result()[0] expect(johnSmith).toMatchObject({ id: `3`, name: `John Smith`, @@ -988,7 +988,7 @@ describe(`Query Collections`, () => { }) // Initially isLoading should be true - expect(rendered.result.isLoading()).toBe(true) + expect(rendered.result.isLoading).toBe(true) // Start sync manually collection.preload() @@ -1011,7 +1011,7 @@ describe(`Query Collections`, () => { // Wait for collection to become ready await waitFor(() => { - expect(rendered.result.isLoading()).toBe(false) + expect(rendered.result.isLoading).toBe(false) }) // Note: Data may not appear immediately due to live query evaluation timing // The main test is that isLoading transitions from true to false @@ -1047,7 +1047,7 @@ describe(`Query Collections`, () => { }) // For pre-created collections that are already syncing, isLoading should be true - expect(rendered.result.isLoading()).toBe(false) + expect(rendered.result.isLoading).toBe(false) expect(rendered.result.state.size).toBe(1) }) @@ -1086,7 +1086,7 @@ describe(`Query Collections`, () => { }) // Initially should be true - expect(rendered.result.isLoading()).toBe(true) + expect(rendered.result.isLoading).toBe(true) // Start sync manually collection.preload() @@ -1110,14 +1110,14 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 100)) - expect(rendered.result.isLoading()).toBe(false) - expect(rendered.result.isReady()).toBe(true) + expect(rendered.result.isLoading).toBe(false) + expect(rendered.result.isReady).toBe(true) // Wait for collection to become ready await waitFor(() => { - expect(rendered.result.isLoading()).toBe(false) + expect(rendered.result.isLoading).toBe(false) }) - expect(rendered.result.status()).toBe(`ready`) + expect(rendered.result.status).toBe(`ready`) }) it(`should maintain isReady state during live updates`, async () => { @@ -1143,10 +1143,10 @@ describe(`Query Collections`, () => { // Wait for initial load await waitFor(() => { - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) }) - const initialIsReady = result.isReady() + const initialIsReady = result.isReady // Perform live updates collection.utils.begin() @@ -1169,8 +1169,8 @@ describe(`Query Collections`, () => { }) // isReady should remain true during live updates - expect(result.isReady()).toBe(true) - expect(result.isReady()).toBe(initialIsReady) + expect(result.isReady).toBe(true) + expect(result.isReady).toBe(initialIsReady) }) it(`should handle isLoading with complex queries including joins`, async () => { @@ -1231,7 +1231,7 @@ describe(`Query Collections`, () => { }) // Initially should be true - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) // Start sync for both collections personCollection.preload() @@ -1267,7 +1267,7 @@ describe(`Query Collections`, () => { // Wait for both collections to sync await waitFor(() => { - expect(result.isReady()).toBe(true) + expect(result.isReady).toBe(true) }) // Note: Joined data may not appear immediately due to live query evaluation timing // The main test is that isLoading transitions from false to true @@ -1314,7 +1314,7 @@ describe(`Query Collections`, () => { ) // Initially should be false - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) // Start sync manually collection.preload() @@ -1345,7 +1345,7 @@ describe(`Query Collections`, () => { // Wait for initial load await waitFor(() => { - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) }) // Change parameters @@ -1353,7 +1353,7 @@ describe(`Query Collections`, () => { // isReady should remain true even when parameters change await waitFor(() => { - expect(result.isReady()).toBe(true) + expect(result.isReady).toBe(true) }) // Note: Data size may not change immediately due to live query evaluation timing // The main test is that isReady remains true when parameters change @@ -1400,9 +1400,9 @@ describe(`Query Collections`, () => { }) // Initially isLoading should be true - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) expect(result.state.size).toBe(0) - expect(result.data).toEqual([]) + expect(result()).toEqual([]) // Start sync manually collection.preload() @@ -1410,7 +1410,7 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 10)) // Still loading - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) // Add first batch of data (but don't mark ready yet) syncBegin!() @@ -1431,9 +1431,9 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(1) }) - expect(result.isLoading()).toBe(true) // Still loading - expect(result.data).toHaveLength(1) - expect(result.data[0]).toMatchObject({ + expect(result.isLoading).toBe(true) // Still loading + expect(result()).toHaveLength(1) + expect(result()[0]).toMatchObject({ id: `1`, name: `John Smith`, }) @@ -1457,18 +1457,18 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(2) }) - expect(result.isLoading()).toBe(true) // Still loading - expect(result.data).toHaveLength(2) + expect(result.isLoading).toBe(true) // Still loading + expect(result()).toHaveLength(2) // Now mark as ready syncMarkReady!() // Should now be ready await waitFor(() => { - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) }) expect(result.state.size).toBe(2) - expect(result.data).toHaveLength(2) + expect(result()).toHaveLength(2) }) it(`should show filtered results during sync with isLoading true`, async () => { @@ -1512,7 +1512,7 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 10)) - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) // Add items from different teams syncBegin!() @@ -1555,15 +1555,15 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(2) }) - expect(result.isLoading()).toBe(true) - expect(result.data).toHaveLength(2) - expect(result.data.every((p) => p.team === `team1`)).toBe(true) + expect(result.isLoading).toBe(true) + expect(result()).toHaveLength(2) + expect(result().every((p) => p.team === `team1`)).toBe(true) // Mark ready syncMarkReady!() await waitFor(() => { - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) }) expect(result.state.size).toBe(2) }) @@ -1634,7 +1634,7 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 10)) - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) // Add a person first userSyncBegin!() @@ -1653,7 +1653,7 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 10)) - expect(result.isLoading()).toBe(true) + expect(result.isLoading).toBe(true) expect(result.state.size).toBe(0) // No joins yet // Add an issue for that person @@ -1673,9 +1673,9 @@ describe(`Query Collections`, () => { await waitFor(() => { expect(result.state.size).toBe(1) }) - expect(result.isLoading()).toBe(true) - expect(result.data).toHaveLength(1) - expect(result.data[0]).toMatchObject({ + expect(result.isLoading).toBe(true) + expect(result()).toHaveLength(1) + expect(result()[0]).toMatchObject({ id: `1`, title: `First Issue`, userName: `John Doe`, @@ -1686,7 +1686,7 @@ describe(`Query Collections`, () => { issueSyncMarkReady!() await waitFor(() => { - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) }) expect(result.state.size).toBe(1) }) @@ -1722,10 +1722,10 @@ describe(`Query Collections`, () => { }) // Initially isLoading should be true - expect(result.isLoading()).toBe(true) - expect(result.isReady()).toBe(false) + expect(result.isLoading).toBe(true) + expect(result.isReady).toBe(false) expect(result.state.size).toBe(0) - expect(result.data).toEqual([]) + expect(result()).toEqual([]) // Start sync manually collection.preload() @@ -1733,20 +1733,20 @@ describe(`Query Collections`, () => { await new Promise((resolve) => setTimeout(resolve, 10)) // Still loading - expect(result.isLoading()).toBe(true) - expect(result.isReady()).toBe(false) + expect(result.isLoading).toBe(true) + expect(result.isReady).toBe(false) // Mark ready without any data commits syncMarkReady!() // Should now be ready, even with no data await waitFor(() => { - expect(result.isReady()).toBe(true) + expect(result.isReady).toBe(true) }) - expect(result.isLoading()).toBe(false) + expect(result.isLoading).toBe(false) expect(result.state.size).toBe(0) // Still no data - expect(result.data).toEqual([]) // Empty array - expect(result.status()).toBe(`ready`) + expect(result()).toEqual([]) // Empty array + expect(result.status).toBe(`ready`) }) })