@tanstack/solid-db 0.1.53 → 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,6 +67,17 @@ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: Init
67
67
  isError: Accessor<boolean>;
68
68
  isCleanedUp: Accessor<boolean>;
69
69
  };
70
+ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null): {
71
+ state: ReactiveMap<string | number, GetResult<TContext>>;
72
+ data: Array<GetResult<TContext>>;
73
+ collection: Accessor<Collection<GetResult<TContext>, string | number, {}> | null>;
74
+ status: Accessor<CollectionStatus | `disabled`>;
75
+ isLoading: Accessor<boolean>;
76
+ isReady: Accessor<boolean>;
77
+ isIdle: Accessor<boolean>;
78
+ isError: Accessor<boolean>;
79
+ isCleanedUp: Accessor<boolean>;
80
+ };
70
81
  /**
71
82
  * Create a live query using configuration object
72
83
  * @param config - Configuration object with query and options
@@ -1,17 +1,25 @@
1
1
  import { createMemo, createSignal, createComputed, batch, createResource, onCleanup } from "solid-js";
2
2
  import { ReactiveMap } from "@solid-primitives/map";
3
- import { createLiveQueryCollection, CollectionImpl } from "@tanstack/db";
3
+ import { BaseQueryBuilder, createLiveQueryCollection, CollectionImpl } from "@tanstack/db";
4
4
  import { createStore, reconcile } from "solid-js/store";
5
5
  function useLiveQuery(configOrQueryOrCollection) {
6
6
  const collection = createMemo(
7
7
  () => {
8
8
  if (configOrQueryOrCollection.length === 1) {
9
+ const queryBuilder = new BaseQueryBuilder();
10
+ const result = configOrQueryOrCollection(queryBuilder);
11
+ if (result === void 0 || result === null) {
12
+ return null;
13
+ }
9
14
  return createLiveQueryCollection({
10
15
  query: configOrQueryOrCollection,
11
16
  startSync: true
12
17
  });
13
18
  }
14
19
  const innerCollection = configOrQueryOrCollection();
20
+ if (innerCollection === void 0 || innerCollection === null) {
21
+ return null;
22
+ }
15
23
  if (innerCollection instanceof CollectionImpl) {
16
24
  innerCollection.startSyncImmediate();
17
25
  return innerCollection;
@@ -28,9 +36,12 @@ function useLiveQuery(configOrQueryOrCollection) {
28
36
  const [data, setData] = createStore([], {
29
37
  name: `TanstackDBData`
30
38
  });
31
- const [status, setStatus] = createSignal(collection().status, {
32
- name: `TanstackDBStatus`
33
- });
39
+ const [status, setStatus] = createSignal(
40
+ collection() ? collection().status : `disabled`,
41
+ {
42
+ name: `TanstackDBStatus`
43
+ }
44
+ );
34
45
  const syncDataFromCollection = (currentCollection) => {
35
46
  setData(
36
47
  (prev) => reconcile(Array.from(currentCollection.values()))(prev).filter(Boolean)
@@ -40,6 +51,16 @@ function useLiveQuery(configOrQueryOrCollection) {
40
51
  createComputed(
41
52
  () => {
42
53
  const currentCollection = collection();
54
+ if (!currentCollection) {
55
+ setStatus(`disabled`);
56
+ state.clear();
57
+ setData([]);
58
+ if (currentUnsubscribe) {
59
+ currentUnsubscribe();
60
+ currentUnsubscribe = null;
61
+ }
62
+ return;
63
+ }
43
64
  setStatus(currentCollection.status);
44
65
  state.clear();
45
66
  for (const [key, value] of currentCollection.entries()) {
@@ -87,7 +108,7 @@ function useLiveQuery(configOrQueryOrCollection) {
87
108
  collection,
88
109
  status,
89
110
  isLoading: () => status() === `loading`,
90
- isReady: () => status() === `ready`,
111
+ isReady: () => status() === `ready` || status() === `disabled`,
91
112
  isIdle: () => status() === `idle`,
92
113
  isError: () => status() === `error`,
93
114
  isCleanedUp: () => status() === `cleaned-up`
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import {\n batch,\n createComputed,\n createMemo,\n createResource,\n createSignal,\n onCleanup,\n} from \"solid-js\"\nimport { ReactiveMap } from \"@solid-primitives/map\"\nimport { CollectionImpl, createLiveQueryCollection } from \"@tanstack/db\"\nimport { createStore, reconcile } from \"solid-js/store\"\nimport type { Accessor } from \"solid-js\"\nimport type {\n ChangeMessage,\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from \"@tanstack/db\"\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const todosQuery = useLiveQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const todosQuery = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority())),\n * )\n *\n * @example\n * // Join pattern\n * const personIssues = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const todosQuery = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * return (\n * <Switch>\n * <Match when={todosQuery.isLoading()}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={todosQuery.isError()}>\n * <div>Error: {todosQuery.status()}</div>\n * </Match>\n * <Match when={todosQuery.isReady()}>\n * <For each={todosQuery.data()}>\n * {(todo) => <li key={todo.id}>{todo.text}</li>}\n * </For>\n * </Match>\n * </Switch>\n * )\n */\n// Overload 1: Accept just the query function\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>\n): {\n state: ReactiveMap<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>\n status: Accessor<CollectionStatus>\n isLoading: Accessor<boolean>\n isReady: Accessor<boolean>\n isIdle: Accessor<boolean>\n isError: Accessor<boolean>\n isCleanedUp: Accessor<boolean>\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const todosQuery = useLiveQuery(() => ({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * }))\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const personsQuery = useLiveQuery(() => ({ query: queryBuilder }))\n *\n * @example\n * // Handle all states uniformly\n * const itemsQuery = useLiveQuery(() => ({\n * query: (q) => q.from({ items: itemCollection })\n * }))\n *\n * return (\n * <Switch fallback={<div>{itemsQuery.data.length} items loaded</div>}>\n * <Match when={itemsQuery.isLoading()}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={itemsQuery.isError()}>\n * <div>Something went wrong</div>\n * </Match>\n * <Match when={!itemsQuery.isReady()}>\n * <div>Preparing...</div>\n * </Match>\n * </Switch>\n * )\n */\n// Overload 2: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: Accessor<LiveQueryCollectionConfig<TContext>>\n): {\n state: ReactiveMap<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>\n status: Accessor<CollectionStatus>\n isLoading: Accessor<boolean>\n isReady: Accessor<boolean>\n isIdle: Accessor<boolean>\n isError: Accessor<boolean>\n isCleanedUp: Accessor<boolean>\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const todosQuery = useLiveQuery(() => myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const existingQuery = useLiveQuery(() => existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * existingQuery.collection().update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const sharedQuery = useLiveQuery(() => sharedCollection)\n *\n * return (\n * <Switch fallback={<div><For each={sharedQuery.data()}>{(item) => <Item key={item.id} {...item} />}</For></div>}>\n * <Match when={sharedQuery.isLoading()}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={sharedQuery.isError()}>\n * <div>Error loading data</div>\n * </Match>\n * </Switch>\n * )\n */\n// Overload 3: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Accessor<Collection<TResult, TKey, TUtils>>\n): {\n state: ReactiveMap<TKey, TResult>\n data: Array<TResult>\n collection: Accessor<Collection<TResult, TKey, TUtils>>\n status: Accessor<CollectionStatus>\n isLoading: Accessor<boolean>\n isReady: Accessor<boolean>\n isIdle: Accessor<boolean>\n isError: Accessor<boolean>\n isCleanedUp: Accessor<boolean>\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: (queryFn?: any) => any\n) {\n const collection = createMemo(\n () => {\n if (configOrQueryOrCollection.length === 1) {\n return createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n })\n }\n\n const innerCollection = configOrQueryOrCollection()\n if (innerCollection instanceof CollectionImpl) {\n innerCollection.startSyncImmediate()\n return innerCollection as Collection\n }\n\n return createLiveQueryCollection({\n ...innerCollection,\n startSync: true,\n })\n },\n undefined,\n { name: `TanstackDBCollectionMemo` }\n )\n\n // Reactive state that gets updated granularly through change events\n const state = new ReactiveMap<string | number, any>()\n\n // Reactive data array that maintains sorted order\n const [data, setData] = createStore<Array<any>>([], {\n name: `TanstackDBData`,\n })\n\n // Track collection status reactively\n const [status, setStatus] = createSignal(collection().status, {\n name: `TanstackDBStatus`,\n })\n\n // Helper to sync data array from collection in correct order\n const syncDataFromCollection = (\n currentCollection: Collection<any, any, any>\n ) => {\n setData((prev) =>\n reconcile(Array.from(currentCollection.values()))(prev).filter(Boolean)\n )\n }\n\n // Track current unsubscribe function\n let currentUnsubscribe: (() => void) | null = null\n\n createComputed(\n () => {\n const currentCollection = collection()\n\n // Update status ref whenever the effect runs\n setStatus(currentCollection.status)\n\n // Initialize state with current collection data\n state.clear()\n for (const [key, value] of currentCollection.entries()) {\n state.set(key, value)\n }\n\n // Subscribe to collection changes with granular updates\n const subscription = currentCollection.subscribeChanges(\n (changes: Array<ChangeMessage<any>>) => {\n // Apply each change individually to the reactive state\n batch(() => {\n for (const change of changes) {\n switch (change.type) {\n case `insert`:\n case `update`:\n state.set(change.key, change.value)\n break\n case `delete`:\n state.delete(change.key)\n break\n }\n }\n })\n\n // Update the data array to maintain sorted order\n syncDataFromCollection(currentCollection)\n\n // Update status ref on every change\n setStatus(currentCollection.status)\n },\n {\n includeInitialState: true,\n }\n )\n\n currentUnsubscribe = subscription.unsubscribe.bind(subscription)\n\n // Preload collection data if not already started\n if (currentCollection.status === `idle`) {\n createResource(() => currentCollection.preload())\n }\n\n // Cleanup when computed is invalidated\n onCleanup(() => {\n if (currentUnsubscribe) {\n currentUnsubscribe()\n currentUnsubscribe = null\n }\n })\n },\n undefined,\n { name: `TanstackDBSyncComputed` }\n )\n\n return {\n state,\n data,\n collection,\n status,\n isLoading: () => status() === `loading`,\n isReady: () => status() === `ready`,\n isIdle: () => status() === `idle`,\n isError: () => status() === `error`,\n isCleanedUp: () => status() === `cleaned-up`,\n }\n}\n"],"names":[],"mappings":";;;;AA2MO,SAAS,aACd,2BACA;AACA,QAAM,aAAa;AAAA,IACjB,MAAM;AACJ,UAAI,0BAA0B,WAAW,GAAG;AAC1C,eAAO,0BAA0B;AAAA,UAC/B,OAAO;AAAA,UACP,WAAW;AAAA,QAAA,CACZ;AAAA,MACH;AAEA,YAAM,kBAAkB,0BAAA;AACxB,UAAI,2BAA2B,gBAAgB;AAC7C,wBAAgB,mBAAA;AAChB,eAAO;AAAA,MACT;AAEA,aAAO,0BAA0B;AAAA,QAC/B,GAAG;AAAA,QACH,WAAW;AAAA,MAAA,CACZ;AAAA,IACH;AAAA,IACA;AAAA,IACA,EAAE,MAAM,2BAAA;AAAA,EAA2B;AAIrC,QAAM,QAAQ,IAAI,YAAA;AAGlB,QAAM,CAAC,MAAM,OAAO,IAAI,YAAwB,CAAA,GAAI;AAAA,IAClD,MAAM;AAAA,EAAA,CACP;AAGD,QAAM,CAAC,QAAQ,SAAS,IAAI,aAAa,WAAA,EAAa,QAAQ;AAAA,IAC5D,MAAM;AAAA,EAAA,CACP;AAGD,QAAM,yBAAyB,CAC7B,sBACG;AACH;AAAA,MAAQ,CAAC,SACP,UAAU,MAAM,KAAK,kBAAkB,OAAA,CAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,OAAO;AAAA,IAAA;AAAA,EAE1E;AAGA,MAAI,qBAA0C;AAE9C;AAAA,IACE,MAAM;AACJ,YAAM,oBAAoB,WAAA;AAG1B,gBAAU,kBAAkB,MAAM;AAGlC,YAAM,MAAA;AACN,iBAAW,CAAC,KAAK,KAAK,KAAK,kBAAkB,WAAW;AACtD,cAAM,IAAI,KAAK,KAAK;AAAA,MACtB;AAGA,YAAM,eAAe,kBAAkB;AAAA,QACrC,CAAC,YAAuC;AAEtC,gBAAM,MAAM;AACV,uBAAW,UAAU,SAAS;AAC5B,sBAAQ,OAAO,MAAA;AAAA,gBACb,KAAK;AAAA,gBACL,KAAK;AACH,wBAAM,IAAI,OAAO,KAAK,OAAO,KAAK;AAClC;AAAA,gBACF,KAAK;AACH,wBAAM,OAAO,OAAO,GAAG;AACvB;AAAA,cAAA;AAAA,YAEN;AAAA,UACF,CAAC;AAGD,iCAAuB,iBAAiB;AAGxC,oBAAU,kBAAkB,MAAM;AAAA,QACpC;AAAA,QACA;AAAA,UACE,qBAAqB;AAAA,QAAA;AAAA,MACvB;AAGF,2BAAqB,aAAa,YAAY,KAAK,YAAY;AAG/D,UAAI,kBAAkB,WAAW,QAAQ;AACvC,uBAAe,MAAM,kBAAkB,SAAS;AAAA,MAClD;AAGA,gBAAU,MAAM;AACd,YAAI,oBAAoB;AACtB,6BAAA;AACA,+BAAqB;AAAA,QACvB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA;AAAA,IACA,EAAE,MAAM,yBAAA;AAAA,EAAyB;AAGnC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,MAAM,aAAa;AAAA,IAC9B,SAAS,MAAM,aAAa;AAAA,IAC5B,QAAQ,MAAM,aAAa;AAAA,IAC3B,SAAS,MAAM,aAAa;AAAA,IAC5B,aAAa,MAAM,OAAA,MAAa;AAAA,EAAA;AAEpC;"}
1
+ {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import {\n batch,\n createComputed,\n createMemo,\n createResource,\n createSignal,\n onCleanup,\n} from 'solid-js'\nimport { ReactiveMap } from '@solid-primitives/map'\nimport {\n BaseQueryBuilder,\n CollectionImpl,\n createLiveQueryCollection,\n} from '@tanstack/db'\nimport { createStore, reconcile } from 'solid-js/store'\nimport type { Accessor } from 'solid-js'\nimport type {\n ChangeMessage,\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from '@tanstack/db'\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const todosQuery = useLiveQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const todosQuery = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority())),\n * )\n *\n * @example\n * // Join pattern\n * const personIssues = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const todosQuery = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * return (\n * <Switch>\n * <Match when={todosQuery.isLoading()}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={todosQuery.isError()}>\n * <div>Error: {todosQuery.status()}</div>\n * </Match>\n * <Match when={todosQuery.isReady()}>\n * <For each={todosQuery.data()}>\n * {(todo) => <li key={todo.id}>{todo.text}</li>}\n * </For>\n * </Match>\n * </Switch>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n): {\n state: ReactiveMap<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>\n status: Accessor<CollectionStatus>\n isLoading: Accessor<boolean>\n isReady: Accessor<boolean>\n isIdle: Accessor<boolean>\n isError: Accessor<boolean>\n isCleanedUp: Accessor<boolean>\n}\n\n// Overload 1b: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n): {\n state: ReactiveMap<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Accessor<Collection<\n GetResult<TContext>,\n string | number,\n {}\n > | null>\n status: Accessor<CollectionStatus | `disabled`>\n isLoading: Accessor<boolean>\n isReady: Accessor<boolean>\n isIdle: Accessor<boolean>\n isError: Accessor<boolean>\n isCleanedUp: Accessor<boolean>\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const todosQuery = useLiveQuery(() => ({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * }))\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const personsQuery = useLiveQuery(() => ({ query: queryBuilder }))\n *\n * @example\n * // Handle all states uniformly\n * const itemsQuery = useLiveQuery(() => ({\n * query: (q) => q.from({ items: itemCollection })\n * }))\n *\n * return (\n * <Switch fallback={<div>{itemsQuery.data.length} items loaded</div>}>\n * <Match when={itemsQuery.isLoading()}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={itemsQuery.isError()}>\n * <div>Something went wrong</div>\n * </Match>\n * <Match when={!itemsQuery.isReady()}>\n * <div>Preparing...</div>\n * </Match>\n * </Switch>\n * )\n */\n// Overload 2: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: Accessor<LiveQueryCollectionConfig<TContext>>,\n): {\n state: ReactiveMap<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>\n status: Accessor<CollectionStatus>\n isLoading: Accessor<boolean>\n isReady: Accessor<boolean>\n isIdle: Accessor<boolean>\n isError: Accessor<boolean>\n isCleanedUp: Accessor<boolean>\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const todosQuery = useLiveQuery(() => myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const existingQuery = useLiveQuery(() => existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * existingQuery.collection().update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const sharedQuery = useLiveQuery(() => sharedCollection)\n *\n * return (\n * <Switch fallback={<div><For each={sharedQuery.data()}>{(item) => <Item key={item.id} {...item} />}</For></div>}>\n * <Match when={sharedQuery.isLoading()}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={sharedQuery.isError()}>\n * <div>Error loading data</div>\n * </Match>\n * </Switch>\n * )\n */\n// Overload 3: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Accessor<Collection<TResult, TKey, TUtils>>,\n): {\n state: ReactiveMap<TKey, TResult>\n data: Array<TResult>\n collection: Accessor<Collection<TResult, TKey, TUtils>>\n status: Accessor<CollectionStatus>\n isLoading: Accessor<boolean>\n isReady: Accessor<boolean>\n isIdle: Accessor<boolean>\n isError: Accessor<boolean>\n isCleanedUp: Accessor<boolean>\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: (queryFn?: any) => any,\n) {\n const collection = createMemo(\n () => {\n if (configOrQueryOrCollection.length === 1) {\n // This is a query function - check if it returns null/undefined\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Disabled query - return null\n return null\n }\n\n return createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n })\n }\n\n const innerCollection = configOrQueryOrCollection()\n\n if (innerCollection === undefined || innerCollection === null) {\n // Disabled query - return null\n return null\n }\n\n if (innerCollection instanceof CollectionImpl) {\n innerCollection.startSyncImmediate()\n return innerCollection as Collection\n }\n\n return createLiveQueryCollection({\n ...innerCollection,\n startSync: true,\n })\n },\n undefined,\n { name: `TanstackDBCollectionMemo` },\n )\n\n // Reactive state that gets updated granularly through change events\n const state = new ReactiveMap<string | number, any>()\n\n // Reactive data array that maintains sorted order\n const [data, setData] = createStore<Array<any>>([], {\n name: `TanstackDBData`,\n })\n\n // Track collection status reactively\n const [status, setStatus] = createSignal(\n collection() ? collection()!.status : (`disabled` as const),\n {\n name: `TanstackDBStatus`,\n },\n )\n\n // Helper to sync data array from collection in correct order\n const syncDataFromCollection = (\n currentCollection: Collection<any, any, any>,\n ) => {\n setData((prev) =>\n reconcile(Array.from(currentCollection.values()))(prev).filter(Boolean),\n )\n }\n\n // Track current unsubscribe function\n let currentUnsubscribe: (() => void) | null = null\n\n createComputed(\n () => {\n const currentCollection = collection()\n\n // Handle null collection (disabled query)\n if (!currentCollection) {\n setStatus(`disabled` as const)\n state.clear()\n setData([])\n if (currentUnsubscribe) {\n currentUnsubscribe()\n currentUnsubscribe = null\n }\n return\n }\n\n // Update status ref whenever the effect runs\n setStatus(currentCollection.status)\n\n // Initialize state with current collection data\n state.clear()\n for (const [key, value] of currentCollection.entries()) {\n state.set(key, value)\n }\n\n // Subscribe to collection changes with granular updates\n const subscription = currentCollection.subscribeChanges(\n (changes: Array<ChangeMessage<any>>) => {\n // Apply each change individually to the reactive state\n batch(() => {\n for (const change of changes) {\n switch (change.type) {\n case `insert`:\n case `update`:\n state.set(change.key, change.value)\n break\n case `delete`:\n state.delete(change.key)\n break\n }\n }\n })\n\n // Update the data array to maintain sorted order\n syncDataFromCollection(currentCollection)\n\n // Update status ref on every change\n setStatus(currentCollection.status)\n },\n {\n includeInitialState: true,\n },\n )\n\n currentUnsubscribe = subscription.unsubscribe.bind(subscription)\n\n // Preload collection data if not already started\n if (currentCollection.status === `idle`) {\n createResource(() => currentCollection.preload())\n }\n\n // Cleanup when computed is invalidated\n onCleanup(() => {\n if (currentUnsubscribe) {\n currentUnsubscribe()\n currentUnsubscribe = null\n }\n })\n },\n undefined,\n { name: `TanstackDBSyncComputed` },\n )\n\n return {\n state,\n data,\n collection,\n status,\n isLoading: () => status() === `loading`,\n isReady: () => status() === `ready` || status() === `disabled`,\n isIdle: () => status() === `idle`,\n isError: () => status() === `error`,\n isCleanedUp: () => status() === `cleaned-up`,\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@tanstack/solid-db",
3
+ "version": "0.1.55",
3
4
  "description": "Solid integration for @tanstack/db",
4
- "version": "0.1.53",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/TanStack/db.git",
9
+ "url": "git+https://github.com/TanStack/db.git",
10
10
  "directory": "packages/solid-db"
11
11
  },
12
12
  "homepage": "https://tanstack.com/db",
@@ -15,19 +15,10 @@
15
15
  "solid",
16
16
  "typescript"
17
17
  ],
18
- "dependencies": {
19
- "@solid-primitives/map": "^0.7.2",
20
- "@tanstack/db": "0.5.10"
21
- },
22
- "devDependencies": {
23
- "@electric-sql/client": "1.2.0",
24
- "@solidjs/testing-library": "^0.8.10",
25
- "@vitest/coverage-istanbul": "^3.2.4",
26
- "jsdom": "^27.2.0",
27
- "solid-js": "^1.9.10",
28
- "vite-plugin-solid": "^2.11.10",
29
- "vitest": "^3.2.4"
30
- },
18
+ "type": "module",
19
+ "main": "dist/esm/index.js",
20
+ "module": "dist/esm/index.js",
21
+ "types": "dist/esm/index.d.ts",
31
22
  "exports": {
32
23
  ".": {
33
24
  "import": {
@@ -37,18 +28,27 @@
37
28
  },
38
29
  "./package.json": "./package.json"
39
30
  },
31
+ "sideEffects": false,
40
32
  "files": [
41
33
  "dist",
42
34
  "src"
43
35
  ],
44
- "main": "dist/cjs/index.cjs",
45
- "module": "dist/esm/index.js",
36
+ "dependencies": {
37
+ "@solid-primitives/map": "^0.7.2",
38
+ "@tanstack/db": "0.5.12"
39
+ },
46
40
  "peerDependencies": {
47
41
  "solid-js": ">=1.9.0"
48
42
  },
49
- "sideEffects": false,
50
- "type": "module",
51
- "types": "dist/esm/index.d.ts",
43
+ "devDependencies": {
44
+ "@electric-sql/client": "1.2.0",
45
+ "@solidjs/testing-library": "^0.8.10",
46
+ "@vitest/coverage-istanbul": "^3.2.4",
47
+ "jsdom": "^27.2.0",
48
+ "solid-js": "^1.9.10",
49
+ "vite-plugin-solid": "^2.11.10",
50
+ "vitest": "^3.2.4"
51
+ },
52
52
  "scripts": {
53
53
  "build": "vite build",
54
54
  "dev": "vite build --watch",
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // Re-export all public APIs
2
- export * from "./useLiveQuery"
2
+ export * from './useLiveQuery'
3
3
 
4
4
  // Re-export everything from @tanstack/db
5
- export * from "@tanstack/db"
5
+ export * from '@tanstack/db'
6
6
 
7
7
  // Re-export some stuff explicitly to ensure the type & value is exported
8
- export type { Collection } from "@tanstack/db"
9
- export { createTransaction } from "@tanstack/db"
8
+ export type { Collection } from '@tanstack/db'
9
+ export { createTransaction } from '@tanstack/db'
@@ -5,11 +5,15 @@ import {
5
5
  createResource,
6
6
  createSignal,
7
7
  onCleanup,
8
- } from "solid-js"
9
- import { ReactiveMap } from "@solid-primitives/map"
10
- import { CollectionImpl, createLiveQueryCollection } from "@tanstack/db"
11
- import { createStore, reconcile } from "solid-js/store"
12
- import type { Accessor } from "solid-js"
8
+ } from 'solid-js'
9
+ import { ReactiveMap } from '@solid-primitives/map'
10
+ import {
11
+ BaseQueryBuilder,
12
+ CollectionImpl,
13
+ createLiveQueryCollection,
14
+ } from '@tanstack/db'
15
+ import { createStore, reconcile } from 'solid-js/store'
16
+ import type { Accessor } from 'solid-js'
13
17
  import type {
14
18
  ChangeMessage,
15
19
  Collection,
@@ -19,7 +23,7 @@ import type {
19
23
  InitialQueryBuilder,
20
24
  LiveQueryCollectionConfig,
21
25
  QueryBuilder,
22
- } from "@tanstack/db"
26
+ } from '@tanstack/db'
23
27
 
24
28
  /**
25
29
  * Create a live query using a query function
@@ -76,9 +80,9 @@ import type {
76
80
  * </Switch>
77
81
  * )
78
82
  */
79
- // Overload 1: Accept just the query function
83
+ // Overload 1: Accept query function that always returns QueryBuilder
80
84
  export function useLiveQuery<TContext extends Context>(
81
- queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>
85
+ queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
82
86
  ): {
83
87
  state: ReactiveMap<string | number, GetResult<TContext>>
84
88
  data: Array<GetResult<TContext>>
@@ -91,6 +95,27 @@ export function useLiveQuery<TContext extends Context>(
91
95
  isCleanedUp: Accessor<boolean>
92
96
  }
93
97
 
98
+ // Overload 1b: Accept query function that can return undefined/null
99
+ export function useLiveQuery<TContext extends Context>(
100
+ queryFn: (
101
+ q: InitialQueryBuilder,
102
+ ) => QueryBuilder<TContext> | undefined | null,
103
+ ): {
104
+ state: ReactiveMap<string | number, GetResult<TContext>>
105
+ data: Array<GetResult<TContext>>
106
+ collection: Accessor<Collection<
107
+ GetResult<TContext>,
108
+ string | number,
109
+ {}
110
+ > | null>
111
+ status: Accessor<CollectionStatus | `disabled`>
112
+ isLoading: Accessor<boolean>
113
+ isReady: Accessor<boolean>
114
+ isIdle: Accessor<boolean>
115
+ isError: Accessor<boolean>
116
+ isCleanedUp: Accessor<boolean>
117
+ }
118
+
94
119
  /**
95
120
  * Create a live query using configuration object
96
121
  * @param config - Configuration object with query and options
@@ -133,7 +158,7 @@ export function useLiveQuery<TContext extends Context>(
133
158
  */
134
159
  // Overload 2: Accept config object
135
160
  export function useLiveQuery<TContext extends Context>(
136
- config: Accessor<LiveQueryCollectionConfig<TContext>>
161
+ config: Accessor<LiveQueryCollectionConfig<TContext>>,
137
162
  ): {
138
163
  state: ReactiveMap<string | number, GetResult<TContext>>
139
164
  data: Array<GetResult<TContext>>
@@ -187,7 +212,7 @@ export function useLiveQuery<
187
212
  TKey extends string | number,
188
213
  TUtils extends Record<string, any>,
189
214
  >(
190
- liveQueryCollection: Accessor<Collection<TResult, TKey, TUtils>>
215
+ liveQueryCollection: Accessor<Collection<TResult, TKey, TUtils>>,
191
216
  ): {
192
217
  state: ReactiveMap<TKey, TResult>
193
218
  data: Array<TResult>
@@ -202,11 +227,20 @@ export function useLiveQuery<
202
227
 
203
228
  // Implementation - use function overloads to infer the actual collection type
204
229
  export function useLiveQuery(
205
- configOrQueryOrCollection: (queryFn?: any) => any
230
+ configOrQueryOrCollection: (queryFn?: any) => any,
206
231
  ) {
207
232
  const collection = createMemo(
208
233
  () => {
209
234
  if (configOrQueryOrCollection.length === 1) {
235
+ // This is a query function - check if it returns null/undefined
236
+ const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder
237
+ const result = configOrQueryOrCollection(queryBuilder)
238
+
239
+ if (result === undefined || result === null) {
240
+ // Disabled query - return null
241
+ return null
242
+ }
243
+
210
244
  return createLiveQueryCollection({
211
245
  query: configOrQueryOrCollection,
212
246
  startSync: true,
@@ -214,6 +248,12 @@ export function useLiveQuery(
214
248
  }
215
249
 
216
250
  const innerCollection = configOrQueryOrCollection()
251
+
252
+ if (innerCollection === undefined || innerCollection === null) {
253
+ // Disabled query - return null
254
+ return null
255
+ }
256
+
217
257
  if (innerCollection instanceof CollectionImpl) {
218
258
  innerCollection.startSyncImmediate()
219
259
  return innerCollection as Collection
@@ -225,7 +265,7 @@ export function useLiveQuery(
225
265
  })
226
266
  },
227
267
  undefined,
228
- { name: `TanstackDBCollectionMemo` }
268
+ { name: `TanstackDBCollectionMemo` },
229
269
  )
230
270
 
231
271
  // Reactive state that gets updated granularly through change events
@@ -237,16 +277,19 @@ export function useLiveQuery(
237
277
  })
238
278
 
239
279
  // Track collection status reactively
240
- const [status, setStatus] = createSignal(collection().status, {
241
- name: `TanstackDBStatus`,
242
- })
280
+ const [status, setStatus] = createSignal(
281
+ collection() ? collection()!.status : (`disabled` as const),
282
+ {
283
+ name: `TanstackDBStatus`,
284
+ },
285
+ )
243
286
 
244
287
  // Helper to sync data array from collection in correct order
245
288
  const syncDataFromCollection = (
246
- currentCollection: Collection<any, any, any>
289
+ currentCollection: Collection<any, any, any>,
247
290
  ) => {
248
291
  setData((prev) =>
249
- reconcile(Array.from(currentCollection.values()))(prev).filter(Boolean)
292
+ reconcile(Array.from(currentCollection.values()))(prev).filter(Boolean),
250
293
  )
251
294
  }
252
295
 
@@ -257,6 +300,18 @@ export function useLiveQuery(
257
300
  () => {
258
301
  const currentCollection = collection()
259
302
 
303
+ // Handle null collection (disabled query)
304
+ if (!currentCollection) {
305
+ setStatus(`disabled` as const)
306
+ state.clear()
307
+ setData([])
308
+ if (currentUnsubscribe) {
309
+ currentUnsubscribe()
310
+ currentUnsubscribe = null
311
+ }
312
+ return
313
+ }
314
+
260
315
  // Update status ref whenever the effect runs
261
316
  setStatus(currentCollection.status)
262
317
 
@@ -292,7 +347,7 @@ export function useLiveQuery(
292
347
  },
293
348
  {
294
349
  includeInitialState: true,
295
- }
350
+ },
296
351
  )
297
352
 
298
353
  currentUnsubscribe = subscription.unsubscribe.bind(subscription)
@@ -311,7 +366,7 @@ export function useLiveQuery(
311
366
  })
312
367
  },
313
368
  undefined,
314
- { name: `TanstackDBSyncComputed` }
369
+ { name: `TanstackDBSyncComputed` },
315
370
  )
316
371
 
317
372
  return {
@@ -320,7 +375,7 @@ export function useLiveQuery(
320
375
  collection,
321
376
  status,
322
377
  isLoading: () => status() === `loading`,
323
- isReady: () => status() === `ready`,
378
+ isReady: () => status() === `ready` || status() === `disabled`,
324
379
  isIdle: () => status() === `idle`,
325
380
  isError: () => status() === `error`,
326
381
  isCleanedUp: () => status() === `cleaned-up`,