@tanstack/solid-db 0.1.53 → 0.1.54

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: (q: InitialQueryBuilder) => 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":";;;;AAkOO,SAAS,aACd,2BACA;AACA,QAAM,aAAa;AAAA,IACjB,MAAM;AACJ,UAAI,0BAA0B,WAAW,GAAG;AAE1C,cAAM,eAAe,IAAI,iBAAA;AACzB,cAAM,SAAS,0BAA0B,YAAY;AAErD,YAAI,WAAW,UAAa,WAAW,MAAM;AAE3C,iBAAO;AAAA,QACT;AAEA,eAAO,0BAA0B;AAAA,UAC/B,OAAO;AAAA,UACP,WAAW;AAAA,QAAA,CACZ;AAAA,MACH;AAEA,YAAM,kBAAkB,0BAAA;AAExB,UAAI,oBAAoB,UAAa,oBAAoB,MAAM;AAE7D,eAAO;AAAA,MACT;AAEA,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,IAC1B,WAAA,IAAe,WAAA,EAAc,SAAU;AAAA,IACvC;AAAA,MACE,MAAM;AAAA,IAAA;AAAA,EACR;AAIF,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,UAAI,CAAC,mBAAmB;AACtB,kBAAU,UAAmB;AAC7B,cAAM,MAAA;AACN,gBAAQ,CAAA,CAAE;AACV,YAAI,oBAAoB;AACtB,6BAAA;AACA,+BAAqB;AAAA,QACvB;AACA;AAAA,MACF;AAGA,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,WAAW,aAAa;AAAA,IACpD,QAAQ,MAAM,aAAa;AAAA,IAC3B,SAAS,MAAM,aAAa;AAAA,IAC5B,aAAa,MAAM,OAAA,MAAa;AAAA,EAAA;AAEpC;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/solid-db",
3
3
  "description": "Solid integration for @tanstack/db",
4
- "version": "0.1.53",
4
+ "version": "0.1.54",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@solid-primitives/map": "^0.7.2",
20
- "@tanstack/db": "0.5.10"
20
+ "@tanstack/db": "0.5.11"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@electric-sql/client": "1.2.0",
@@ -7,7 +7,11 @@ import {
7
7
  onCleanup,
8
8
  } from "solid-js"
9
9
  import { ReactiveMap } from "@solid-primitives/map"
10
- import { CollectionImpl, createLiveQueryCollection } from "@tanstack/db"
10
+ import {
11
+ BaseQueryBuilder,
12
+ CollectionImpl,
13
+ createLiveQueryCollection,
14
+ } from "@tanstack/db"
11
15
  import { createStore, reconcile } from "solid-js/store"
12
16
  import type { Accessor } from "solid-js"
13
17
  import type {
@@ -76,7 +80,7 @@ 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
85
  queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>
82
86
  ): {
@@ -91,6 +95,25 @@ 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: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null
101
+ ): {
102
+ state: ReactiveMap<string | number, GetResult<TContext>>
103
+ data: Array<GetResult<TContext>>
104
+ collection: Accessor<Collection<
105
+ GetResult<TContext>,
106
+ string | number,
107
+ {}
108
+ > | null>
109
+ status: Accessor<CollectionStatus | `disabled`>
110
+ isLoading: Accessor<boolean>
111
+ isReady: Accessor<boolean>
112
+ isIdle: Accessor<boolean>
113
+ isError: Accessor<boolean>
114
+ isCleanedUp: Accessor<boolean>
115
+ }
116
+
94
117
  /**
95
118
  * Create a live query using configuration object
96
119
  * @param config - Configuration object with query and options
@@ -207,6 +230,15 @@ export function useLiveQuery(
207
230
  const collection = createMemo(
208
231
  () => {
209
232
  if (configOrQueryOrCollection.length === 1) {
233
+ // This is a query function - check if it returns null/undefined
234
+ const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder
235
+ const result = configOrQueryOrCollection(queryBuilder)
236
+
237
+ if (result === undefined || result === null) {
238
+ // Disabled query - return null
239
+ return null
240
+ }
241
+
210
242
  return createLiveQueryCollection({
211
243
  query: configOrQueryOrCollection,
212
244
  startSync: true,
@@ -214,6 +246,12 @@ export function useLiveQuery(
214
246
  }
215
247
 
216
248
  const innerCollection = configOrQueryOrCollection()
249
+
250
+ if (innerCollection === undefined || innerCollection === null) {
251
+ // Disabled query - return null
252
+ return null
253
+ }
254
+
217
255
  if (innerCollection instanceof CollectionImpl) {
218
256
  innerCollection.startSyncImmediate()
219
257
  return innerCollection as Collection
@@ -237,9 +275,12 @@ export function useLiveQuery(
237
275
  })
238
276
 
239
277
  // Track collection status reactively
240
- const [status, setStatus] = createSignal(collection().status, {
241
- name: `TanstackDBStatus`,
242
- })
278
+ const [status, setStatus] = createSignal(
279
+ collection() ? collection()!.status : (`disabled` as const),
280
+ {
281
+ name: `TanstackDBStatus`,
282
+ }
283
+ )
243
284
 
244
285
  // Helper to sync data array from collection in correct order
245
286
  const syncDataFromCollection = (
@@ -257,6 +298,18 @@ export function useLiveQuery(
257
298
  () => {
258
299
  const currentCollection = collection()
259
300
 
301
+ // Handle null collection (disabled query)
302
+ if (!currentCollection) {
303
+ setStatus(`disabled` as const)
304
+ state.clear()
305
+ setData([])
306
+ if (currentUnsubscribe) {
307
+ currentUnsubscribe()
308
+ currentUnsubscribe = null
309
+ }
310
+ return
311
+ }
312
+
260
313
  // Update status ref whenever the effect runs
261
314
  setStatus(currentCollection.status)
262
315
 
@@ -320,7 +373,7 @@ export function useLiveQuery(
320
373
  collection,
321
374
  status,
322
375
  isLoading: () => status() === `loading`,
323
- isReady: () => status() === `ready`,
376
+ isReady: () => status() === `ready` || status() === `disabled`,
324
377
  isIdle: () => status() === `idle`,
325
378
  isError: () => status() === `error`,
326
379
  isCleanedUp: () => status() === `cleaned-up`,