@tanstack/angular-db 0.2.11 → 1.0.0-rc.1

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.
package/README.md CHANGED
@@ -226,11 +226,16 @@ Template Usage
226
226
  Use Angular's new control flow syntax for best performance:
227
227
 
228
228
  @if (query.isReady()) {
229
+ @if (query.isSyncError()) {
230
+ <div>Data may be out of date</div>
231
+ }
229
232
  @for (item of query.data(); track item.id) {
230
233
  <div>{{ item.text }}</div>
231
234
  }
232
- } @else if (query.isError()) {
235
+ } @else if (query.isLoadingError()) {
233
236
  <div>Error loading data</div>
237
+ } @else if (query.isCrashed()) {
238
+ <div>Local storage failed</div>
234
239
  } @else {
235
240
  <div>Loading...</div>
236
241
  }
@@ -274,12 +279,15 @@ An object with Angular signals:
274
279
  - data: Signal<Array<T>> - Array of query results, automatically updates
275
280
  - state: Signal<Map<Key, T>> - Map of results by key, automatically updates
276
281
  - collection: Signal<Collection> - The underlying collection instance
277
- - status: Signal<CollectionStatus> - Current status ('idle' | 'loading' | 'ready' | 'error' | 'cleaned-up')
282
+ - status: Signal<CollectionStatus | 'disabled'> - Current status ('idle' | 'loading' | 'ready' | 'error' | 'crashed' | 'cleaned-up' | 'disabled')
278
283
  - isLoading: Signal<boolean> - true when status is 'loading'
279
284
  - isReady: Signal<boolean> - true when status is 'ready'
280
285
  - isIdle: Signal<boolean> - true when status is 'idle'
281
- - isError: Signal<boolean> - true when status is 'error'
286
+ - isLoadingError: Signal<boolean> - true when status is 'error': the first load failed
287
+ - isCrashed: Signal<boolean> - true when status is 'crashed': local storage broke
282
288
  - isCleanedUp: Signal<boolean> - true when status is 'cleaned-up'
289
+ - isSyncError: Signal<boolean> - true when status is 'ready' and the health tree has a problem
290
+ - health: Signal<HealthNode | undefined> - The health tree: one node for the query and one per source; undefined for a disabled query
283
291
 
284
292
  Parameters
285
293
 
@@ -20,14 +20,15 @@ function injectLiveQuery(opts) {
20
20
  }
21
21
  const isReactiveQueryOptions = opts && typeof opts === `object` && typeof opts.query === `function` && typeof opts.params === `function`;
22
22
  if (isReactiveQueryOptions) {
23
- const { params, query } = opts;
24
- const currentParams = params();
25
- const queryBuilder = new db.BaseQueryBuilder();
26
- const result = query({ params: currentParams, q: queryBuilder });
27
- if (result === void 0 || result === null) {
28
- return null;
29
- }
30
- return db.resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS });
23
+ const pool = db.configPool(opts);
24
+ const result = opts.query({
25
+ params: opts.params(),
26
+ q: new db.BaseQueryBuilder()
27
+ });
28
+ return db.resolveLiveQueryValue(result, {
29
+ gcTime: DEFAULT_GC_TIME_MS,
30
+ pool
31
+ });
31
32
  }
32
33
  if (opts && typeof opts === `object` && typeof opts.query === `function`) {
33
34
  return db.resolveLiveQueryValue(opts, { gcTime: DEFAULT_GC_TIME_MS });
@@ -41,8 +42,10 @@ function injectLiveQuery(opts) {
41
42
  const value = statusValue();
42
43
  return value === `idle` && !collection() ? `disabled` : value;
43
44
  });
44
- const persistedStatus = core.signal(`unavailable`);
45
- const persistedError = core.signal(void 0);
45
+ const health = core.signal(void 0);
46
+ const flags = core.computed(
47
+ () => db.getLiveQueryStatusFlags(status(), health())
48
+ );
46
49
  const data = core.computed(() => {
47
50
  const currentCollection = collection();
48
51
  if (!currentCollection) {
@@ -57,8 +60,7 @@ function injectLiveQuery(opts) {
57
60
  internalData.set(newData);
58
61
  const snapshot = observer.getSnapshot();
59
62
  statusValue.set(snapshot.status);
60
- persistedStatus.set(snapshot.persistedStatus);
61
- persistedError.set(snapshot.persistedError);
63
+ health.set(snapshot.health);
62
64
  };
63
65
  let unsub = null;
64
66
  const cleanup = () => {
@@ -69,8 +71,7 @@ function injectLiveQuery(opts) {
69
71
  const currentCollection = collection();
70
72
  if (!currentCollection) {
71
73
  statusValue.set(`disabled`);
72
- persistedStatus.set(`unavailable`);
73
- persistedError.set(void 0);
74
+ health.set(void 0);
74
75
  state.set(/* @__PURE__ */ new Map());
75
76
  internalData.set([]);
76
77
  cleanup();
@@ -100,14 +101,14 @@ function injectLiveQuery(opts) {
100
101
  () => db.getPublicCollection(collection())
101
102
  ),
102
103
  status,
103
- isLoading: core.computed(() => status() === `loading`),
104
- isReady: core.computed(() => status() === `ready` || status() === `disabled`),
105
- persistedStatus,
106
- isPersistedReady: core.computed(() => persistedStatus() === `ready`),
107
- persistedError,
108
- isIdle: core.computed(() => status() === `idle`),
109
- isError: core.computed(() => status() === `error`),
110
- isCleanedUp: core.computed(() => status() === `cleaned-up`)
104
+ isLoading: core.computed(() => flags().isLoading),
105
+ isReady: core.computed(() => flags().isReady || status() === `disabled`),
106
+ isIdle: core.computed(() => flags().isIdle),
107
+ isLoadingError: core.computed(() => flags().isLoadingError),
108
+ isCrashed: core.computed(() => flags().isCrashed),
109
+ isCleanedUp: core.computed(() => flags().isCleanedUp),
110
+ isSyncError: core.computed(() => flags().isSyncError),
111
+ health
111
112
  };
112
113
  }
113
114
  exports.injectLiveQuery = injectLiveQuery;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":["../../src/index.ts"],"sourcesContent":["import {\n DestroyRef,\n assertInInjectionContext,\n computed,\n effect,\n inject,\n signal,\n} from '@angular/core'\nimport {\n BaseQueryBuilder,\n createLiveQueryObserver,\n getPublicCollection,\n isCollection,\n isSingleResultCollection,\n resolveLiveQueryValue,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\n LiveQueryPersistedStatus,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\nimport type { Signal } from '@angular/core'\n\nconst DEFAULT_GC_TIME_MS = 1\n\nexport * from '@tanstack/db'\n\n/**\n * The result of calling `injectLiveQuery`.\n * Contains reactive signals for the query state and data.\n */\nexport interface InjectLiveQueryResult<TContext extends Context> {\n /** A signal containing the complete state map of results keyed by their ID */\n state: Signal<Map<string | number, GetResult<TContext>>>\n /** A signal containing the results as an array, or single result for findOne queries */\n data: Signal<InferResultType<TContext>>\n /** A signal containing the underlying collection instance (null for disabled queries) */\n collection: Signal<Collection<\n GetResult<TContext>,\n string | number,\n {}\n > | null>\n /** A signal containing the current status of the collection */\n status: Signal<CollectionStatus | `disabled`>\n /** A signal indicating whether the collection is currently loading */\n isLoading: Signal<boolean>\n /** A signal indicating whether the collection is ready */\n isReady: Signal<boolean>\n persistedStatus: Signal<LiveQueryPersistedStatus>\n isPersistedReady: Signal<boolean>\n persistedError: Signal<unknown | undefined>\n /** A signal indicating whether the collection is idle */\n isIdle: Signal<boolean>\n /** A signal indicating whether the collection has an error */\n isError: Signal<boolean>\n /** A signal indicating whether the collection has been cleaned up */\n isCleanedUp: Signal<boolean>\n}\n\ntype InferConditionalResultType<TContext extends Context> =\n TContext extends SingleResult\n ? InferResultType<TContext> | []\n : InferResultType<TContext>\n\nexport type InjectConditionalLiveQueryResult<TContext extends Context> = Omit<\n InjectLiveQueryResult<TContext>,\n `data`\n> & {\n data: Signal<InferConditionalResultType<TContext>>\n}\n\nexport interface InjectLiveQueryResultWithCollection<\n TResult extends object = any,\n TKey extends string | number = string | number,\n TUtils extends Record<string, any> = {},\n> {\n state: Signal<Map<TKey, TResult>>\n data: Signal<Array<TResult>>\n collection: Signal<Collection<TResult, TKey, TUtils> | null>\n status: Signal<CollectionStatus | `disabled`>\n isLoading: Signal<boolean>\n isReady: Signal<boolean>\n persistedStatus: Signal<LiveQueryPersistedStatus>\n isPersistedReady: Signal<boolean>\n persistedError: Signal<unknown | undefined>\n isIdle: Signal<boolean>\n isError: Signal<boolean>\n isCleanedUp: Signal<boolean>\n}\n\nexport interface InjectLiveQueryResultWithSingleResultCollection<\n TResult extends object = any,\n TKey extends string | number = string | number,\n TUtils extends Record<string, any> = {},\n> {\n state: Signal<Map<TKey, TResult>>\n data: Signal<TResult | undefined>\n collection: Signal<(Collection<TResult, TKey, TUtils> & SingleResult) | null>\n status: Signal<CollectionStatus | `disabled`>\n isLoading: Signal<boolean>\n isReady: Signal<boolean>\n persistedStatus: Signal<LiveQueryPersistedStatus>\n isPersistedReady: Signal<boolean>\n persistedError: Signal<unknown | undefined>\n isIdle: Signal<boolean>\n isError: Signal<boolean>\n isCleanedUp: Signal<boolean>\n}\n\nexport function injectLiveQuery<\n TContext extends Context,\n TParams extends any,\n>(options: {\n params: () => TParams\n query: (args: {\n params: TParams\n q: InitialQueryBuilder\n }) => QueryBuilder<TContext>\n}): InjectLiveQueryResult<TContext>\nexport function injectLiveQuery<\n TContext extends Context,\n TParams extends any,\n>(options: {\n params: () => TParams\n query: (args: {\n params: TParams\n q: InitialQueryBuilder\n }) => QueryBuilder<TContext> | undefined | null\n}): InjectConditionalLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n): InjectLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n): InjectConditionalLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n): InjectLiveQueryResult<TContext>\n// Pre-created collection without singleResult\nexport function injectLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): InjectLiveQueryResultWithCollection<TResult, TKey, TUtils>\n// Pre-created collection with singleResult\nexport function injectLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): InjectLiveQueryResultWithSingleResultCollection<TResult, TKey, TUtils>\nexport function injectLiveQuery(opts: any) {\n assertInInjectionContext(injectLiveQuery)\n const destroyRef = inject(DestroyRef)\n\n const collection = computed(() => {\n // Check if it's an existing collection\n if (isCollection(opts)) {\n return opts\n }\n\n if (typeof opts === `function`) {\n // Check if query function returns null/undefined (disabled query)\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = opts(queryBuilder)\n\n if (result === undefined || result === null) {\n // Disabled query - return null\n return null\n }\n\n return resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS })\n }\n\n // Check if it's reactive query options\n const isReactiveQueryOptions =\n opts &&\n typeof opts === `object` &&\n typeof opts.query === `function` &&\n typeof opts.params === `function`\n\n if (isReactiveQueryOptions) {\n const { params, query } = opts\n const currentParams = params()\n\n // Check if query function returns null/undefined (disabled query)\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = query({ params: currentParams, q: queryBuilder })\n\n if (result === undefined || result === null) {\n // Disabled query - return null\n return null\n }\n\n return resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS })\n }\n\n // Handle LiveQueryCollectionConfig objects. Default startSync/gcTime to\n // match the query-fn and reactive-options paths, but let an explicit value\n // in the config win — otherwise a bare `{ query }` never syncs.\n if (opts && typeof opts === `object` && typeof opts.query === `function`) {\n return resolveLiveQueryValue(opts, { gcTime: DEFAULT_GC_TIME_MS })\n }\n\n throw new Error(`Invalid options provided to injectLiveQuery`)\n })\n\n const state = signal(new Map<string | number, any>())\n const internalData = signal<Array<any>>([])\n const statusValue = signal<CollectionStatus | `disabled`>(`idle`)\n const status = computed(() => {\n const value = statusValue()\n return value === `idle` && !collection() ? `disabled` : value\n })\n const persistedStatus = signal<LiveQueryPersistedStatus>(`unavailable`)\n const persistedError = signal<unknown>(undefined)\n\n // Returns single item for singleResult collections, array otherwise\n const data = computed(() => {\n const currentCollection = collection()\n if (!currentCollection) {\n return internalData()\n }\n return isSingleResultCollection(currentCollection)\n ? internalData()[0]\n : internalData()\n })\n\n const syncDataFromCollection = (\n currentCollection: Collection<any, any, any>,\n observer: LiveQueryObserver<any, any>,\n ) => {\n const newState = new Map(currentCollection.entries())\n const newData = Array.from(newState.values())\n\n state.set(newState)\n internalData.set(newData)\n const snapshot = observer.getSnapshot()\n statusValue.set(snapshot.status)\n persistedStatus.set(snapshot.persistedStatus)\n persistedError.set(snapshot.persistedError)\n }\n\n let unsub: (() => void) | null = null\n const cleanup = () => {\n unsub?.()\n unsub = null\n }\n\n effect((onCleanup) => {\n const currentCollection = collection()\n\n // Handle null collection (disabled query)\n if (!currentCollection) {\n statusValue.set(`disabled` as const)\n persistedStatus.set(`unavailable`)\n persistedError.set(undefined)\n state.set(new Map())\n internalData.set([])\n cleanup()\n return\n }\n\n cleanup()\n\n // The shared observer owns sync start, subscription, the ready-race, and\n // status transitions; Angular re-reads the whole collection on each notify\n // (wholesale) into its signals.\n // Angular re-reads the collection on notify; wholesale mode preserves its\n // pre-observer loading policy (no initial-state snapshot request).\n const observer = createLiveQueryObserver(currentCollection, {\n mode: `wholesale`,\n })\n\n const unsubscribe = observer.subscribe(() => {\n syncDataFromCollection(currentCollection, observer)\n })\n // Wholesale attach suppresses listener calls raised by synchronous sync\n // startup. Read once after subscribe returns to capture that final state.\n syncDataFromCollection(currentCollection, observer)\n unsub = () => {\n unsubscribe()\n observer.dispose()\n }\n\n onCleanup(cleanup)\n })\n\n destroyRef.onDestroy(cleanup)\n\n return {\n state,\n data,\n // Loosely typed so the impl return stays compatible with every overload\n // (the shared `isCollection` guard narrows the computed to `Collection | null`).\n collection: computed(() =>\n getPublicCollection(collection()),\n ) as Signal<any>,\n status,\n isLoading: computed(() => status() === `loading`),\n isReady: computed(() => status() === `ready` || status() === `disabled`),\n persistedStatus,\n isPersistedReady: computed(() => persistedStatus() === `ready`),\n persistedError,\n isIdle: computed(() => status() === `idle`),\n isError: computed(() => status() === `error`),\n isCleanedUp: computed(() => status() === `cleaned-up`),\n }\n}\n"],"names":["assertInInjectionContext","inject","DestroyRef","computed","isCollection","BaseQueryBuilder","resolveLiveQueryValue","signal","isSingleResultCollection","effect","createLiveQueryObserver","getPublicCollection"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","sources":["../../src/index.ts"],"sourcesContent":["import {\n DestroyRef,\n assertInInjectionContext,\n computed,\n effect,\n inject,\n signal,\n} from '@angular/core'\nimport {\n BaseQueryBuilder,\n configPool,\n createLiveQueryObserver,\n getLiveQueryStatusFlags,\n getPublicCollection,\n isCollection,\n isSingleResultCollection,\n resolveLiveQueryValue,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n HealthNode,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryHookConfig,\n LiveQueryObserver,\n LiveQueryStatusFlags,\n NonSingleResult,\n PooledLiveQueries,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\nimport type { Signal } from '@angular/core'\n\nconst DEFAULT_GC_TIME_MS = 1\n\nexport * from '@tanstack/db'\n\n/**\n * The result of calling `injectLiveQuery`.\n * Contains reactive signals for the query state and data.\n */\n/** The status and flag signals every `injectLiveQuery` result has. */\nexport type InjectLiveQueryFlags = {\n /** A signal containing the current status of the collection */\n status: Signal<CollectionStatus | `disabled`>\n} & {\n [K in keyof LiveQueryStatusFlags]: Signal<LiveQueryStatusFlags[K]>\n}\n\nexport interface InjectLiveQueryResult<\n TContext extends Context,\n> extends InjectLiveQueryFlags {\n /** A signal containing the complete state map of results keyed by their ID */\n state: Signal<Map<string | number, GetResult<TContext>>>\n /** A signal containing the results as an array, or single result for findOne queries */\n data: Signal<InferResultType<TContext>>\n /** A signal containing the underlying collection instance (null for disabled queries) */\n collection: Signal<Collection<\n GetResult<TContext>,\n string | number,\n {}\n > | null>\n}\n\ntype InferConditionalResultType<TContext extends Context> =\n TContext extends SingleResult\n ? InferResultType<TContext> | []\n : InferResultType<TContext>\n\nexport type InjectConditionalLiveQueryResult<TContext extends Context> = Omit<\n InjectLiveQueryResult<TContext>,\n `data`\n> & {\n data: Signal<InferConditionalResultType<TContext>>\n}\n\nexport interface InjectLiveQueryResultWithCollection<\n TResult extends object = any,\n TKey extends string | number = string | number,\n TUtils extends Record<string, any> = {},\n> extends InjectLiveQueryFlags {\n state: Signal<Map<TKey, TResult>>\n data: Signal<Array<TResult>>\n collection: Signal<Collection<TResult, TKey, TUtils> | null>\n}\n\nexport interface InjectLiveQueryResultWithSingleResultCollection<\n TResult extends object = any,\n TKey extends string | number = string | number,\n TUtils extends Record<string, any> = {},\n> extends InjectLiveQueryFlags {\n state: Signal<Map<TKey, TResult>>\n data: Signal<TResult | undefined>\n collection: Signal<(Collection<TResult, TKey, TUtils> & SingleResult) | null>\n}\n\nexport function injectLiveQuery<\n TContext extends Context,\n TParams extends any,\n>(options: {\n params: () => TParams\n /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */\n pool?: PooledLiveQueries\n query: (args: {\n params: TParams\n q: InitialQueryBuilder\n }) => QueryBuilder<TContext>\n}): InjectLiveQueryResult<TContext>\nexport function injectLiveQuery<\n TContext extends Context,\n TParams extends any,\n>(options: {\n params: () => TParams\n /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */\n pool?: PooledLiveQueries\n query: (args: {\n params: TParams\n q: InitialQueryBuilder\n }) => QueryBuilder<TContext> | undefined | null\n}): InjectConditionalLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n): InjectLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n): InjectConditionalLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n config: LiveQueryHookConfig<TContext>,\n): InjectLiveQueryResult<TContext>\n// Pre-created collection without singleResult\nexport function injectLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): InjectLiveQueryResultWithCollection<TResult, TKey, TUtils>\n// Pre-created collection with singleResult\nexport function injectLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): InjectLiveQueryResultWithSingleResultCollection<TResult, TKey, TUtils>\nexport function injectLiveQuery(opts: any) {\n assertInInjectionContext(injectLiveQuery)\n const destroyRef = inject(DestroyRef)\n\n const collection = computed(() => {\n // Check if it's an existing collection\n if (isCollection(opts)) {\n return opts\n }\n\n if (typeof opts === `function`) {\n // Check if query function returns null/undefined (disabled query)\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = opts(queryBuilder)\n\n if (result === undefined || result === null) {\n // Disabled query - return null\n return null\n }\n\n return resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS })\n }\n\n // Check if it's reactive query options\n const isReactiveQueryOptions =\n opts &&\n typeof opts === `object` &&\n typeof opts.query === `function` &&\n typeof opts.params === `function`\n\n if (isReactiveQueryOptions) {\n // The pool is checked first, so a disabled query rejects it too.\n const pool = configPool(opts)\n // A query function returning null or undefined disables the query.\n const result = opts.query({\n params: opts.params(),\n q: new BaseQueryBuilder() as InitialQueryBuilder,\n })\n return resolveLiveQueryValue(result, {\n gcTime: DEFAULT_GC_TIME_MS,\n pool,\n })\n }\n\n // Handle LiveQueryCollectionConfig objects. Default startSync/gcTime to\n // match the query-fn and reactive-options paths, but let an explicit value\n // in the config win — otherwise a bare `{ query }` never syncs.\n if (opts && typeof opts === `object` && typeof opts.query === `function`) {\n return resolveLiveQueryValue(opts, { gcTime: DEFAULT_GC_TIME_MS })\n }\n\n throw new Error(`Invalid options provided to injectLiveQuery`)\n })\n\n const state = signal(new Map<string | number, any>())\n const internalData = signal<Array<any>>([])\n const statusValue = signal<CollectionStatus | `disabled`>(`idle`)\n const status = computed(() => {\n const value = statusValue()\n return value === `idle` && !collection() ? `disabled` : value\n })\n const health = signal<HealthNode | undefined>(undefined)\n // Every status flag comes from the shared rule; a disabled query is ready.\n const flags = computed(() =>\n getLiveQueryStatusFlags(status() as CollectionStatus, health()),\n )\n\n // Returns single item for singleResult collections, array otherwise\n const data = computed(() => {\n const currentCollection = collection()\n if (!currentCollection) {\n return internalData()\n }\n return isSingleResultCollection(currentCollection)\n ? internalData()[0]\n : internalData()\n })\n\n const syncDataFromCollection = (\n currentCollection: Collection<any, any, any>,\n observer: LiveQueryObserver<any, any>,\n ) => {\n const newState = new Map(currentCollection.entries())\n const newData = Array.from(newState.values())\n\n state.set(newState)\n internalData.set(newData)\n const snapshot = observer.getSnapshot()\n statusValue.set(snapshot.status)\n health.set(snapshot.health)\n }\n\n let unsub: (() => void) | null = null\n const cleanup = () => {\n unsub?.()\n unsub = null\n }\n\n effect((onCleanup) => {\n const currentCollection = collection()\n\n // Handle null collection (disabled query)\n if (!currentCollection) {\n statusValue.set(`disabled` as const)\n health.set(undefined)\n state.set(new Map())\n internalData.set([])\n cleanup()\n return\n }\n\n cleanup()\n\n // The shared observer owns sync start, subscription, the ready-race, and\n // status transitions; Angular re-reads the whole collection on each notify\n // (wholesale) into its signals.\n // Angular re-reads the collection on notify; wholesale mode preserves its\n // pre-observer loading policy (no initial-state snapshot request).\n const observer = createLiveQueryObserver(currentCollection, {\n mode: `wholesale`,\n })\n\n const unsubscribe = observer.subscribe(() => {\n syncDataFromCollection(currentCollection, observer)\n })\n // Wholesale attach suppresses listener calls raised by synchronous sync\n // startup. Read once after subscribe returns to capture that final state.\n syncDataFromCollection(currentCollection, observer)\n unsub = () => {\n unsubscribe()\n observer.dispose()\n }\n\n onCleanup(cleanup)\n })\n\n destroyRef.onDestroy(cleanup)\n\n return {\n state,\n data,\n // Loosely typed so the impl return stays compatible with every overload\n // (the shared `isCollection` guard narrows the computed to `Collection | null`).\n collection: computed(() =>\n getPublicCollection(collection()),\n ) as Signal<any>,\n status,\n isLoading: computed(() => flags().isLoading),\n isReady: computed(() => flags().isReady || status() === `disabled`),\n isIdle: computed(() => flags().isIdle),\n isLoadingError: computed(() => flags().isLoadingError),\n isCrashed: computed(() => flags().isCrashed),\n isCleanedUp: computed(() => flags().isCleanedUp),\n isSyncError: computed(() => flags().isSyncError),\n health,\n }\n}\n"],"names":["assertInInjectionContext","inject","DestroyRef","computed","isCollection","BaseQueryBuilder","resolveLiveQueryValue","configPool","signal","getLiveQueryStatusFlags","isSingleResultCollection","effect","createLiveQueryObserver","getPublicCollection"],"mappings":"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"}
@@ -1,67 +1,43 @@
1
- import { Collection, CollectionStatus, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, LiveQueryPersistedStatus, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
1
+ import { Collection, CollectionStatus, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryHookConfig, LiveQueryStatusFlags, NonSingleResult, PooledLiveQueries, QueryBuilder, SingleResult } from '@tanstack/db';
2
2
  import { Signal } from '@angular/core';
3
3
  export * from '@tanstack/db';
4
4
  /**
5
5
  * The result of calling `injectLiveQuery`.
6
6
  * Contains reactive signals for the query state and data.
7
7
  */
8
- export interface InjectLiveQueryResult<TContext extends Context> {
8
+ /** The status and flag signals every `injectLiveQuery` result has. */
9
+ export type InjectLiveQueryFlags = {
10
+ /** A signal containing the current status of the collection */
11
+ status: Signal<CollectionStatus | `disabled`>;
12
+ } & {
13
+ [K in keyof LiveQueryStatusFlags]: Signal<LiveQueryStatusFlags[K]>;
14
+ };
15
+ export interface InjectLiveQueryResult<TContext extends Context> extends InjectLiveQueryFlags {
9
16
  /** A signal containing the complete state map of results keyed by their ID */
10
17
  state: Signal<Map<string | number, GetResult<TContext>>>;
11
18
  /** A signal containing the results as an array, or single result for findOne queries */
12
19
  data: Signal<InferResultType<TContext>>;
13
20
  /** A signal containing the underlying collection instance (null for disabled queries) */
14
21
  collection: Signal<Collection<GetResult<TContext>, string | number, {}> | null>;
15
- /** A signal containing the current status of the collection */
16
- status: Signal<CollectionStatus | `disabled`>;
17
- /** A signal indicating whether the collection is currently loading */
18
- isLoading: Signal<boolean>;
19
- /** A signal indicating whether the collection is ready */
20
- isReady: Signal<boolean>;
21
- persistedStatus: Signal<LiveQueryPersistedStatus>;
22
- isPersistedReady: Signal<boolean>;
23
- persistedError: Signal<unknown | undefined>;
24
- /** A signal indicating whether the collection is idle */
25
- isIdle: Signal<boolean>;
26
- /** A signal indicating whether the collection has an error */
27
- isError: Signal<boolean>;
28
- /** A signal indicating whether the collection has been cleaned up */
29
- isCleanedUp: Signal<boolean>;
30
22
  }
31
23
  type InferConditionalResultType<TContext extends Context> = TContext extends SingleResult ? InferResultType<TContext> | [] : InferResultType<TContext>;
32
24
  export type InjectConditionalLiveQueryResult<TContext extends Context> = Omit<InjectLiveQueryResult<TContext>, `data`> & {
33
25
  data: Signal<InferConditionalResultType<TContext>>;
34
26
  };
35
- export interface InjectLiveQueryResultWithCollection<TResult extends object = any, TKey extends string | number = string | number, TUtils extends Record<string, any> = {}> {
27
+ export interface InjectLiveQueryResultWithCollection<TResult extends object = any, TKey extends string | number = string | number, TUtils extends Record<string, any> = {}> extends InjectLiveQueryFlags {
36
28
  state: Signal<Map<TKey, TResult>>;
37
29
  data: Signal<Array<TResult>>;
38
30
  collection: Signal<Collection<TResult, TKey, TUtils> | null>;
39
- status: Signal<CollectionStatus | `disabled`>;
40
- isLoading: Signal<boolean>;
41
- isReady: Signal<boolean>;
42
- persistedStatus: Signal<LiveQueryPersistedStatus>;
43
- isPersistedReady: Signal<boolean>;
44
- persistedError: Signal<unknown | undefined>;
45
- isIdle: Signal<boolean>;
46
- isError: Signal<boolean>;
47
- isCleanedUp: Signal<boolean>;
48
31
  }
49
- export interface InjectLiveQueryResultWithSingleResultCollection<TResult extends object = any, TKey extends string | number = string | number, TUtils extends Record<string, any> = {}> {
32
+ export interface InjectLiveQueryResultWithSingleResultCollection<TResult extends object = any, TKey extends string | number = string | number, TUtils extends Record<string, any> = {}> extends InjectLiveQueryFlags {
50
33
  state: Signal<Map<TKey, TResult>>;
51
34
  data: Signal<TResult | undefined>;
52
35
  collection: Signal<(Collection<TResult, TKey, TUtils> & SingleResult) | null>;
53
- status: Signal<CollectionStatus | `disabled`>;
54
- isLoading: Signal<boolean>;
55
- isReady: Signal<boolean>;
56
- persistedStatus: Signal<LiveQueryPersistedStatus>;
57
- isPersistedReady: Signal<boolean>;
58
- persistedError: Signal<unknown | undefined>;
59
- isIdle: Signal<boolean>;
60
- isError: Signal<boolean>;
61
- isCleanedUp: Signal<boolean>;
62
36
  }
63
37
  export declare function injectLiveQuery<TContext extends Context, TParams extends any>(options: {
64
38
  params: () => TParams;
39
+ /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */
40
+ pool?: PooledLiveQueries;
65
41
  query: (args: {
66
42
  params: TParams;
67
43
  q: InitialQueryBuilder;
@@ -69,6 +45,8 @@ export declare function injectLiveQuery<TContext extends Context, TParams extend
69
45
  }): InjectLiveQueryResult<TContext>;
70
46
  export declare function injectLiveQuery<TContext extends Context, TParams extends any>(options: {
71
47
  params: () => TParams;
48
+ /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */
49
+ pool?: PooledLiveQueries;
72
50
  query: (args: {
73
51
  params: TParams;
74
52
  q: InitialQueryBuilder;
@@ -76,6 +54,6 @@ export declare function injectLiveQuery<TContext extends Context, TParams extend
76
54
  }): InjectConditionalLiveQueryResult<TContext>;
77
55
  export declare function injectLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>): InjectLiveQueryResult<TContext>;
78
56
  export declare function injectLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null): InjectConditionalLiveQueryResult<TContext>;
79
- export declare function injectLiveQuery<TContext extends Context>(config: LiveQueryCollectionConfig<TContext>): InjectLiveQueryResult<TContext>;
57
+ export declare function injectLiveQuery<TContext extends Context>(config: LiveQueryHookConfig<TContext>): InjectLiveQueryResult<TContext>;
80
58
  export declare function injectLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult): InjectLiveQueryResultWithCollection<TResult, TKey, TUtils>;
81
59
  export declare function injectLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult): InjectLiveQueryResultWithSingleResultCollection<TResult, TKey, TUtils>;
@@ -1,67 +1,43 @@
1
- import { Collection, CollectionStatus, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, LiveQueryPersistedStatus, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
1
+ import { Collection, CollectionStatus, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryHookConfig, LiveQueryStatusFlags, NonSingleResult, PooledLiveQueries, QueryBuilder, SingleResult } from '@tanstack/db';
2
2
  import { Signal } from '@angular/core';
3
3
  export * from '@tanstack/db';
4
4
  /**
5
5
  * The result of calling `injectLiveQuery`.
6
6
  * Contains reactive signals for the query state and data.
7
7
  */
8
- export interface InjectLiveQueryResult<TContext extends Context> {
8
+ /** The status and flag signals every `injectLiveQuery` result has. */
9
+ export type InjectLiveQueryFlags = {
10
+ /** A signal containing the current status of the collection */
11
+ status: Signal<CollectionStatus | `disabled`>;
12
+ } & {
13
+ [K in keyof LiveQueryStatusFlags]: Signal<LiveQueryStatusFlags[K]>;
14
+ };
15
+ export interface InjectLiveQueryResult<TContext extends Context> extends InjectLiveQueryFlags {
9
16
  /** A signal containing the complete state map of results keyed by their ID */
10
17
  state: Signal<Map<string | number, GetResult<TContext>>>;
11
18
  /** A signal containing the results as an array, or single result for findOne queries */
12
19
  data: Signal<InferResultType<TContext>>;
13
20
  /** A signal containing the underlying collection instance (null for disabled queries) */
14
21
  collection: Signal<Collection<GetResult<TContext>, string | number, {}> | null>;
15
- /** A signal containing the current status of the collection */
16
- status: Signal<CollectionStatus | `disabled`>;
17
- /** A signal indicating whether the collection is currently loading */
18
- isLoading: Signal<boolean>;
19
- /** A signal indicating whether the collection is ready */
20
- isReady: Signal<boolean>;
21
- persistedStatus: Signal<LiveQueryPersistedStatus>;
22
- isPersistedReady: Signal<boolean>;
23
- persistedError: Signal<unknown | undefined>;
24
- /** A signal indicating whether the collection is idle */
25
- isIdle: Signal<boolean>;
26
- /** A signal indicating whether the collection has an error */
27
- isError: Signal<boolean>;
28
- /** A signal indicating whether the collection has been cleaned up */
29
- isCleanedUp: Signal<boolean>;
30
22
  }
31
23
  type InferConditionalResultType<TContext extends Context> = TContext extends SingleResult ? InferResultType<TContext> | [] : InferResultType<TContext>;
32
24
  export type InjectConditionalLiveQueryResult<TContext extends Context> = Omit<InjectLiveQueryResult<TContext>, `data`> & {
33
25
  data: Signal<InferConditionalResultType<TContext>>;
34
26
  };
35
- export interface InjectLiveQueryResultWithCollection<TResult extends object = any, TKey extends string | number = string | number, TUtils extends Record<string, any> = {}> {
27
+ export interface InjectLiveQueryResultWithCollection<TResult extends object = any, TKey extends string | number = string | number, TUtils extends Record<string, any> = {}> extends InjectLiveQueryFlags {
36
28
  state: Signal<Map<TKey, TResult>>;
37
29
  data: Signal<Array<TResult>>;
38
30
  collection: Signal<Collection<TResult, TKey, TUtils> | null>;
39
- status: Signal<CollectionStatus | `disabled`>;
40
- isLoading: Signal<boolean>;
41
- isReady: Signal<boolean>;
42
- persistedStatus: Signal<LiveQueryPersistedStatus>;
43
- isPersistedReady: Signal<boolean>;
44
- persistedError: Signal<unknown | undefined>;
45
- isIdle: Signal<boolean>;
46
- isError: Signal<boolean>;
47
- isCleanedUp: Signal<boolean>;
48
31
  }
49
- export interface InjectLiveQueryResultWithSingleResultCollection<TResult extends object = any, TKey extends string | number = string | number, TUtils extends Record<string, any> = {}> {
32
+ export interface InjectLiveQueryResultWithSingleResultCollection<TResult extends object = any, TKey extends string | number = string | number, TUtils extends Record<string, any> = {}> extends InjectLiveQueryFlags {
50
33
  state: Signal<Map<TKey, TResult>>;
51
34
  data: Signal<TResult | undefined>;
52
35
  collection: Signal<(Collection<TResult, TKey, TUtils> & SingleResult) | null>;
53
- status: Signal<CollectionStatus | `disabled`>;
54
- isLoading: Signal<boolean>;
55
- isReady: Signal<boolean>;
56
- persistedStatus: Signal<LiveQueryPersistedStatus>;
57
- isPersistedReady: Signal<boolean>;
58
- persistedError: Signal<unknown | undefined>;
59
- isIdle: Signal<boolean>;
60
- isError: Signal<boolean>;
61
- isCleanedUp: Signal<boolean>;
62
36
  }
63
37
  export declare function injectLiveQuery<TContext extends Context, TParams extends any>(options: {
64
38
  params: () => TParams;
39
+ /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */
40
+ pool?: PooledLiveQueries;
65
41
  query: (args: {
66
42
  params: TParams;
67
43
  q: InitialQueryBuilder;
@@ -69,6 +45,8 @@ export declare function injectLiveQuery<TContext extends Context, TParams extend
69
45
  }): InjectLiveQueryResult<TContext>;
70
46
  export declare function injectLiveQuery<TContext extends Context, TParams extends any>(options: {
71
47
  params: () => TParams;
48
+ /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */
49
+ pool?: PooledLiveQueries;
72
50
  query: (args: {
73
51
  params: TParams;
74
52
  q: InitialQueryBuilder;
@@ -76,6 +54,6 @@ export declare function injectLiveQuery<TContext extends Context, TParams extend
76
54
  }): InjectConditionalLiveQueryResult<TContext>;
77
55
  export declare function injectLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>): InjectLiveQueryResult<TContext>;
78
56
  export declare function injectLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null): InjectConditionalLiveQueryResult<TContext>;
79
- export declare function injectLiveQuery<TContext extends Context>(config: LiveQueryCollectionConfig<TContext>): InjectLiveQueryResult<TContext>;
57
+ export declare function injectLiveQuery<TContext extends Context>(config: LiveQueryHookConfig<TContext>): InjectLiveQueryResult<TContext>;
80
58
  export declare function injectLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult): InjectLiveQueryResultWithCollection<TResult, TKey, TUtils>;
81
59
  export declare function injectLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult): InjectLiveQueryResultWithSingleResultCollection<TResult, TKey, TUtils>;
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { assertInInjectionContext, inject, DestroyRef, computed, signal, effect } from "@angular/core";
2
- import { isCollection, BaseQueryBuilder, resolveLiveQueryValue, isSingleResultCollection, createLiveQueryObserver, getPublicCollection } from "@tanstack/db";
2
+ import { isCollection, BaseQueryBuilder, resolveLiveQueryValue, configPool, getLiveQueryStatusFlags, isSingleResultCollection, createLiveQueryObserver, getPublicCollection } from "@tanstack/db";
3
3
  export * from "@tanstack/db";
4
4
  const DEFAULT_GC_TIME_MS = 1;
5
5
  function injectLiveQuery(opts) {
@@ -19,14 +19,15 @@ function injectLiveQuery(opts) {
19
19
  }
20
20
  const isReactiveQueryOptions = opts && typeof opts === `object` && typeof opts.query === `function` && typeof opts.params === `function`;
21
21
  if (isReactiveQueryOptions) {
22
- const { params, query } = opts;
23
- const currentParams = params();
24
- const queryBuilder = new BaseQueryBuilder();
25
- const result = query({ params: currentParams, q: queryBuilder });
26
- if (result === void 0 || result === null) {
27
- return null;
28
- }
29
- return resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS });
22
+ const pool = configPool(opts);
23
+ const result = opts.query({
24
+ params: opts.params(),
25
+ q: new BaseQueryBuilder()
26
+ });
27
+ return resolveLiveQueryValue(result, {
28
+ gcTime: DEFAULT_GC_TIME_MS,
29
+ pool
30
+ });
30
31
  }
31
32
  if (opts && typeof opts === `object` && typeof opts.query === `function`) {
32
33
  return resolveLiveQueryValue(opts, { gcTime: DEFAULT_GC_TIME_MS });
@@ -40,8 +41,10 @@ function injectLiveQuery(opts) {
40
41
  const value = statusValue();
41
42
  return value === `idle` && !collection() ? `disabled` : value;
42
43
  });
43
- const persistedStatus = signal(`unavailable`);
44
- const persistedError = signal(void 0);
44
+ const health = signal(void 0);
45
+ const flags = computed(
46
+ () => getLiveQueryStatusFlags(status(), health())
47
+ );
45
48
  const data = computed(() => {
46
49
  const currentCollection = collection();
47
50
  if (!currentCollection) {
@@ -56,8 +59,7 @@ function injectLiveQuery(opts) {
56
59
  internalData.set(newData);
57
60
  const snapshot = observer.getSnapshot();
58
61
  statusValue.set(snapshot.status);
59
- persistedStatus.set(snapshot.persistedStatus);
60
- persistedError.set(snapshot.persistedError);
62
+ health.set(snapshot.health);
61
63
  };
62
64
  let unsub = null;
63
65
  const cleanup = () => {
@@ -68,8 +70,7 @@ function injectLiveQuery(opts) {
68
70
  const currentCollection = collection();
69
71
  if (!currentCollection) {
70
72
  statusValue.set(`disabled`);
71
- persistedStatus.set(`unavailable`);
72
- persistedError.set(void 0);
73
+ health.set(void 0);
73
74
  state.set(/* @__PURE__ */ new Map());
74
75
  internalData.set([]);
75
76
  cleanup();
@@ -99,14 +100,14 @@ function injectLiveQuery(opts) {
99
100
  () => getPublicCollection(collection())
100
101
  ),
101
102
  status,
102
- isLoading: computed(() => status() === `loading`),
103
- isReady: computed(() => status() === `ready` || status() === `disabled`),
104
- persistedStatus,
105
- isPersistedReady: computed(() => persistedStatus() === `ready`),
106
- persistedError,
107
- isIdle: computed(() => status() === `idle`),
108
- isError: computed(() => status() === `error`),
109
- isCleanedUp: computed(() => status() === `cleaned-up`)
103
+ isLoading: computed(() => flags().isLoading),
104
+ isReady: computed(() => flags().isReady || status() === `disabled`),
105
+ isIdle: computed(() => flags().isIdle),
106
+ isLoadingError: computed(() => flags().isLoadingError),
107
+ isCrashed: computed(() => flags().isCrashed),
108
+ isCleanedUp: computed(() => flags().isCleanedUp),
109
+ isSyncError: computed(() => flags().isSyncError),
110
+ health
110
111
  };
111
112
  }
112
113
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../src/index.ts"],"sourcesContent":["import {\n DestroyRef,\n assertInInjectionContext,\n computed,\n effect,\n inject,\n signal,\n} from '@angular/core'\nimport {\n BaseQueryBuilder,\n createLiveQueryObserver,\n getPublicCollection,\n isCollection,\n isSingleResultCollection,\n resolveLiveQueryValue,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\n LiveQueryPersistedStatus,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\nimport type { Signal } from '@angular/core'\n\nconst DEFAULT_GC_TIME_MS = 1\n\nexport * from '@tanstack/db'\n\n/**\n * The result of calling `injectLiveQuery`.\n * Contains reactive signals for the query state and data.\n */\nexport interface InjectLiveQueryResult<TContext extends Context> {\n /** A signal containing the complete state map of results keyed by their ID */\n state: Signal<Map<string | number, GetResult<TContext>>>\n /** A signal containing the results as an array, or single result for findOne queries */\n data: Signal<InferResultType<TContext>>\n /** A signal containing the underlying collection instance (null for disabled queries) */\n collection: Signal<Collection<\n GetResult<TContext>,\n string | number,\n {}\n > | null>\n /** A signal containing the current status of the collection */\n status: Signal<CollectionStatus | `disabled`>\n /** A signal indicating whether the collection is currently loading */\n isLoading: Signal<boolean>\n /** A signal indicating whether the collection is ready */\n isReady: Signal<boolean>\n persistedStatus: Signal<LiveQueryPersistedStatus>\n isPersistedReady: Signal<boolean>\n persistedError: Signal<unknown | undefined>\n /** A signal indicating whether the collection is idle */\n isIdle: Signal<boolean>\n /** A signal indicating whether the collection has an error */\n isError: Signal<boolean>\n /** A signal indicating whether the collection has been cleaned up */\n isCleanedUp: Signal<boolean>\n}\n\ntype InferConditionalResultType<TContext extends Context> =\n TContext extends SingleResult\n ? InferResultType<TContext> | []\n : InferResultType<TContext>\n\nexport type InjectConditionalLiveQueryResult<TContext extends Context> = Omit<\n InjectLiveQueryResult<TContext>,\n `data`\n> & {\n data: Signal<InferConditionalResultType<TContext>>\n}\n\nexport interface InjectLiveQueryResultWithCollection<\n TResult extends object = any,\n TKey extends string | number = string | number,\n TUtils extends Record<string, any> = {},\n> {\n state: Signal<Map<TKey, TResult>>\n data: Signal<Array<TResult>>\n collection: Signal<Collection<TResult, TKey, TUtils> | null>\n status: Signal<CollectionStatus | `disabled`>\n isLoading: Signal<boolean>\n isReady: Signal<boolean>\n persistedStatus: Signal<LiveQueryPersistedStatus>\n isPersistedReady: Signal<boolean>\n persistedError: Signal<unknown | undefined>\n isIdle: Signal<boolean>\n isError: Signal<boolean>\n isCleanedUp: Signal<boolean>\n}\n\nexport interface InjectLiveQueryResultWithSingleResultCollection<\n TResult extends object = any,\n TKey extends string | number = string | number,\n TUtils extends Record<string, any> = {},\n> {\n state: Signal<Map<TKey, TResult>>\n data: Signal<TResult | undefined>\n collection: Signal<(Collection<TResult, TKey, TUtils> & SingleResult) | null>\n status: Signal<CollectionStatus | `disabled`>\n isLoading: Signal<boolean>\n isReady: Signal<boolean>\n persistedStatus: Signal<LiveQueryPersistedStatus>\n isPersistedReady: Signal<boolean>\n persistedError: Signal<unknown | undefined>\n isIdle: Signal<boolean>\n isError: Signal<boolean>\n isCleanedUp: Signal<boolean>\n}\n\nexport function injectLiveQuery<\n TContext extends Context,\n TParams extends any,\n>(options: {\n params: () => TParams\n query: (args: {\n params: TParams\n q: InitialQueryBuilder\n }) => QueryBuilder<TContext>\n}): InjectLiveQueryResult<TContext>\nexport function injectLiveQuery<\n TContext extends Context,\n TParams extends any,\n>(options: {\n params: () => TParams\n query: (args: {\n params: TParams\n q: InitialQueryBuilder\n }) => QueryBuilder<TContext> | undefined | null\n}): InjectConditionalLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n): InjectLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n): InjectConditionalLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n): InjectLiveQueryResult<TContext>\n// Pre-created collection without singleResult\nexport function injectLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): InjectLiveQueryResultWithCollection<TResult, TKey, TUtils>\n// Pre-created collection with singleResult\nexport function injectLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): InjectLiveQueryResultWithSingleResultCollection<TResult, TKey, TUtils>\nexport function injectLiveQuery(opts: any) {\n assertInInjectionContext(injectLiveQuery)\n const destroyRef = inject(DestroyRef)\n\n const collection = computed(() => {\n // Check if it's an existing collection\n if (isCollection(opts)) {\n return opts\n }\n\n if (typeof opts === `function`) {\n // Check if query function returns null/undefined (disabled query)\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = opts(queryBuilder)\n\n if (result === undefined || result === null) {\n // Disabled query - return null\n return null\n }\n\n return resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS })\n }\n\n // Check if it's reactive query options\n const isReactiveQueryOptions =\n opts &&\n typeof opts === `object` &&\n typeof opts.query === `function` &&\n typeof opts.params === `function`\n\n if (isReactiveQueryOptions) {\n const { params, query } = opts\n const currentParams = params()\n\n // Check if query function returns null/undefined (disabled query)\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = query({ params: currentParams, q: queryBuilder })\n\n if (result === undefined || result === null) {\n // Disabled query - return null\n return null\n }\n\n return resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS })\n }\n\n // Handle LiveQueryCollectionConfig objects. Default startSync/gcTime to\n // match the query-fn and reactive-options paths, but let an explicit value\n // in the config win — otherwise a bare `{ query }` never syncs.\n if (opts && typeof opts === `object` && typeof opts.query === `function`) {\n return resolveLiveQueryValue(opts, { gcTime: DEFAULT_GC_TIME_MS })\n }\n\n throw new Error(`Invalid options provided to injectLiveQuery`)\n })\n\n const state = signal(new Map<string | number, any>())\n const internalData = signal<Array<any>>([])\n const statusValue = signal<CollectionStatus | `disabled`>(`idle`)\n const status = computed(() => {\n const value = statusValue()\n return value === `idle` && !collection() ? `disabled` : value\n })\n const persistedStatus = signal<LiveQueryPersistedStatus>(`unavailable`)\n const persistedError = signal<unknown>(undefined)\n\n // Returns single item for singleResult collections, array otherwise\n const data = computed(() => {\n const currentCollection = collection()\n if (!currentCollection) {\n return internalData()\n }\n return isSingleResultCollection(currentCollection)\n ? internalData()[0]\n : internalData()\n })\n\n const syncDataFromCollection = (\n currentCollection: Collection<any, any, any>,\n observer: LiveQueryObserver<any, any>,\n ) => {\n const newState = new Map(currentCollection.entries())\n const newData = Array.from(newState.values())\n\n state.set(newState)\n internalData.set(newData)\n const snapshot = observer.getSnapshot()\n statusValue.set(snapshot.status)\n persistedStatus.set(snapshot.persistedStatus)\n persistedError.set(snapshot.persistedError)\n }\n\n let unsub: (() => void) | null = null\n const cleanup = () => {\n unsub?.()\n unsub = null\n }\n\n effect((onCleanup) => {\n const currentCollection = collection()\n\n // Handle null collection (disabled query)\n if (!currentCollection) {\n statusValue.set(`disabled` as const)\n persistedStatus.set(`unavailable`)\n persistedError.set(undefined)\n state.set(new Map())\n internalData.set([])\n cleanup()\n return\n }\n\n cleanup()\n\n // The shared observer owns sync start, subscription, the ready-race, and\n // status transitions; Angular re-reads the whole collection on each notify\n // (wholesale) into its signals.\n // Angular re-reads the collection on notify; wholesale mode preserves its\n // pre-observer loading policy (no initial-state snapshot request).\n const observer = createLiveQueryObserver(currentCollection, {\n mode: `wholesale`,\n })\n\n const unsubscribe = observer.subscribe(() => {\n syncDataFromCollection(currentCollection, observer)\n })\n // Wholesale attach suppresses listener calls raised by synchronous sync\n // startup. Read once after subscribe returns to capture that final state.\n syncDataFromCollection(currentCollection, observer)\n unsub = () => {\n unsubscribe()\n observer.dispose()\n }\n\n onCleanup(cleanup)\n })\n\n destroyRef.onDestroy(cleanup)\n\n return {\n state,\n data,\n // Loosely typed so the impl return stays compatible with every overload\n // (the shared `isCollection` guard narrows the computed to `Collection | null`).\n collection: computed(() =>\n getPublicCollection(collection()),\n ) as Signal<any>,\n status,\n isLoading: computed(() => status() === `loading`),\n isReady: computed(() => status() === `ready` || status() === `disabled`),\n persistedStatus,\n isPersistedReady: computed(() => persistedStatus() === `ready`),\n persistedError,\n isIdle: computed(() => status() === `idle`),\n isError: computed(() => status() === `error`),\n isCleanedUp: computed(() => status() === `cleaned-up`),\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../src/index.ts"],"sourcesContent":["import {\n DestroyRef,\n assertInInjectionContext,\n computed,\n effect,\n inject,\n signal,\n} from '@angular/core'\nimport {\n BaseQueryBuilder,\n configPool,\n createLiveQueryObserver,\n getLiveQueryStatusFlags,\n getPublicCollection,\n isCollection,\n isSingleResultCollection,\n resolveLiveQueryValue,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n HealthNode,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryHookConfig,\n LiveQueryObserver,\n LiveQueryStatusFlags,\n NonSingleResult,\n PooledLiveQueries,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\nimport type { Signal } from '@angular/core'\n\nconst DEFAULT_GC_TIME_MS = 1\n\nexport * from '@tanstack/db'\n\n/**\n * The result of calling `injectLiveQuery`.\n * Contains reactive signals for the query state and data.\n */\n/** The status and flag signals every `injectLiveQuery` result has. */\nexport type InjectLiveQueryFlags = {\n /** A signal containing the current status of the collection */\n status: Signal<CollectionStatus | `disabled`>\n} & {\n [K in keyof LiveQueryStatusFlags]: Signal<LiveQueryStatusFlags[K]>\n}\n\nexport interface InjectLiveQueryResult<\n TContext extends Context,\n> extends InjectLiveQueryFlags {\n /** A signal containing the complete state map of results keyed by their ID */\n state: Signal<Map<string | number, GetResult<TContext>>>\n /** A signal containing the results as an array, or single result for findOne queries */\n data: Signal<InferResultType<TContext>>\n /** A signal containing the underlying collection instance (null for disabled queries) */\n collection: Signal<Collection<\n GetResult<TContext>,\n string | number,\n {}\n > | null>\n}\n\ntype InferConditionalResultType<TContext extends Context> =\n TContext extends SingleResult\n ? InferResultType<TContext> | []\n : InferResultType<TContext>\n\nexport type InjectConditionalLiveQueryResult<TContext extends Context> = Omit<\n InjectLiveQueryResult<TContext>,\n `data`\n> & {\n data: Signal<InferConditionalResultType<TContext>>\n}\n\nexport interface InjectLiveQueryResultWithCollection<\n TResult extends object = any,\n TKey extends string | number = string | number,\n TUtils extends Record<string, any> = {},\n> extends InjectLiveQueryFlags {\n state: Signal<Map<TKey, TResult>>\n data: Signal<Array<TResult>>\n collection: Signal<Collection<TResult, TKey, TUtils> | null>\n}\n\nexport interface InjectLiveQueryResultWithSingleResultCollection<\n TResult extends object = any,\n TKey extends string | number = string | number,\n TUtils extends Record<string, any> = {},\n> extends InjectLiveQueryFlags {\n state: Signal<Map<TKey, TResult>>\n data: Signal<TResult | undefined>\n collection: Signal<(Collection<TResult, TKey, TUtils> & SingleResult) | null>\n}\n\nexport function injectLiveQuery<\n TContext extends Context,\n TParams extends any,\n>(options: {\n params: () => TParams\n /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */\n pool?: PooledLiveQueries\n query: (args: {\n params: TParams\n q: InitialQueryBuilder\n }) => QueryBuilder<TContext>\n}): InjectLiveQueryResult<TContext>\nexport function injectLiveQuery<\n TContext extends Context,\n TParams extends any,\n>(options: {\n params: () => TParams\n /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */\n pool?: PooledLiveQueries\n query: (args: {\n params: TParams\n q: InitialQueryBuilder\n }) => QueryBuilder<TContext> | undefined | null\n}): InjectConditionalLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n): InjectLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n): InjectConditionalLiveQueryResult<TContext>\nexport function injectLiveQuery<TContext extends Context>(\n config: LiveQueryHookConfig<TContext>,\n): InjectLiveQueryResult<TContext>\n// Pre-created collection without singleResult\nexport function injectLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): InjectLiveQueryResultWithCollection<TResult, TKey, TUtils>\n// Pre-created collection with singleResult\nexport function injectLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): InjectLiveQueryResultWithSingleResultCollection<TResult, TKey, TUtils>\nexport function injectLiveQuery(opts: any) {\n assertInInjectionContext(injectLiveQuery)\n const destroyRef = inject(DestroyRef)\n\n const collection = computed(() => {\n // Check if it's an existing collection\n if (isCollection(opts)) {\n return opts\n }\n\n if (typeof opts === `function`) {\n // Check if query function returns null/undefined (disabled query)\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = opts(queryBuilder)\n\n if (result === undefined || result === null) {\n // Disabled query - return null\n return null\n }\n\n return resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS })\n }\n\n // Check if it's reactive query options\n const isReactiveQueryOptions =\n opts &&\n typeof opts === `object` &&\n typeof opts.query === `function` &&\n typeof opts.params === `function`\n\n if (isReactiveQueryOptions) {\n // The pool is checked first, so a disabled query rejects it too.\n const pool = configPool(opts)\n // A query function returning null or undefined disables the query.\n const result = opts.query({\n params: opts.params(),\n q: new BaseQueryBuilder() as InitialQueryBuilder,\n })\n return resolveLiveQueryValue(result, {\n gcTime: DEFAULT_GC_TIME_MS,\n pool,\n })\n }\n\n // Handle LiveQueryCollectionConfig objects. Default startSync/gcTime to\n // match the query-fn and reactive-options paths, but let an explicit value\n // in the config win — otherwise a bare `{ query }` never syncs.\n if (opts && typeof opts === `object` && typeof opts.query === `function`) {\n return resolveLiveQueryValue(opts, { gcTime: DEFAULT_GC_TIME_MS })\n }\n\n throw new Error(`Invalid options provided to injectLiveQuery`)\n })\n\n const state = signal(new Map<string | number, any>())\n const internalData = signal<Array<any>>([])\n const statusValue = signal<CollectionStatus | `disabled`>(`idle`)\n const status = computed(() => {\n const value = statusValue()\n return value === `idle` && !collection() ? `disabled` : value\n })\n const health = signal<HealthNode | undefined>(undefined)\n // Every status flag comes from the shared rule; a disabled query is ready.\n const flags = computed(() =>\n getLiveQueryStatusFlags(status() as CollectionStatus, health()),\n )\n\n // Returns single item for singleResult collections, array otherwise\n const data = computed(() => {\n const currentCollection = collection()\n if (!currentCollection) {\n return internalData()\n }\n return isSingleResultCollection(currentCollection)\n ? internalData()[0]\n : internalData()\n })\n\n const syncDataFromCollection = (\n currentCollection: Collection<any, any, any>,\n observer: LiveQueryObserver<any, any>,\n ) => {\n const newState = new Map(currentCollection.entries())\n const newData = Array.from(newState.values())\n\n state.set(newState)\n internalData.set(newData)\n const snapshot = observer.getSnapshot()\n statusValue.set(snapshot.status)\n health.set(snapshot.health)\n }\n\n let unsub: (() => void) | null = null\n const cleanup = () => {\n unsub?.()\n unsub = null\n }\n\n effect((onCleanup) => {\n const currentCollection = collection()\n\n // Handle null collection (disabled query)\n if (!currentCollection) {\n statusValue.set(`disabled` as const)\n health.set(undefined)\n state.set(new Map())\n internalData.set([])\n cleanup()\n return\n }\n\n cleanup()\n\n // The shared observer owns sync start, subscription, the ready-race, and\n // status transitions; Angular re-reads the whole collection on each notify\n // (wholesale) into its signals.\n // Angular re-reads the collection on notify; wholesale mode preserves its\n // pre-observer loading policy (no initial-state snapshot request).\n const observer = createLiveQueryObserver(currentCollection, {\n mode: `wholesale`,\n })\n\n const unsubscribe = observer.subscribe(() => {\n syncDataFromCollection(currentCollection, observer)\n })\n // Wholesale attach suppresses listener calls raised by synchronous sync\n // startup. Read once after subscribe returns to capture that final state.\n syncDataFromCollection(currentCollection, observer)\n unsub = () => {\n unsubscribe()\n observer.dispose()\n }\n\n onCleanup(cleanup)\n })\n\n destroyRef.onDestroy(cleanup)\n\n return {\n state,\n data,\n // Loosely typed so the impl return stays compatible with every overload\n // (the shared `isCollection` guard narrows the computed to `Collection | null`).\n collection: computed(() =>\n getPublicCollection(collection()),\n ) as Signal<any>,\n status,\n isLoading: computed(() => flags().isLoading),\n isReady: computed(() => flags().isReady || status() === `disabled`),\n isIdle: computed(() => flags().isIdle),\n isLoadingError: computed(() => flags().isLoadingError),\n isCrashed: computed(() => flags().isCrashed),\n isCleanedUp: computed(() => flags().isCleanedUp),\n isSyncError: computed(() => flags().isSyncError),\n health,\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/angular-db",
3
- "version": "0.2.11",
3
+ "version": "1.0.0-rc.1",
4
4
  "description": "Angular integration for @tanstack/db",
5
5
  "author": "Ethan McDaniel",
6
6
  "license": "MIT",
@@ -40,19 +40,19 @@
40
40
  "skills"
41
41
  ],
42
42
  "dependencies": {
43
- "@tanstack/db": "0.15.1"
43
+ "@tanstack/db": "1.0.0-rc.1"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@angular/core": ">=16.0.0",
47
47
  "rxjs": ">=6.0.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@angular/common": "^20.3.16",
51
- "@angular/compiler": "^20.3.16",
52
- "@angular/core": "^20.3.16",
53
- "@angular/platform-browser": "^20.3.16",
54
- "@angular/platform-browser-dynamic": "^20.3.16",
55
- "@vitest/coverage-istanbul": "^3.2.4",
50
+ "@angular/common": "^20.3.34",
51
+ "@angular/compiler": "^20.3.34",
52
+ "@angular/core": "^20.3.34",
53
+ "@angular/platform-browser": "^20.3.34",
54
+ "@angular/platform-browser-dynamic": "^20.3.34",
55
+ "@vitest/coverage-istanbul": "^4.1.12",
56
56
  "rxjs": "^7.8.2",
57
57
  "zone.js": "~0.15.0"
58
58
  },
@@ -66,7 +66,11 @@ Returns an object with Angular `Signal<T>` properties — call with `()` in temp
66
66
  const query = injectLiveQuery((q) => q.from({ todo: todoCollection }))
67
67
  // query.data() → Array<T>
68
68
  // query.status() → CollectionStatus | 'disabled'
69
- // query.isLoading(), query.isReady(), query.isError()
69
+ // query.isLoading(), query.isReady()
70
+ // query.isLoadingError() → the first load failed: nothing to show
71
+ // query.isCrashed() → status 'crashed': local storage broke
72
+ // query.isSyncError() → data is shown, but a sync problem is in health
73
+ // query.health() → health tree: one node for the query and one per source
70
74
  // query.isIdle(), query.isCleanedUp() (seldom used)
71
75
  // query.state() → Map<TKey, T>
72
76
  // query.collection() → Collection | null
@@ -188,7 +192,7 @@ Angular 16 structural directives:
188
192
 
189
193
  ## Includes (Hierarchical Data)
190
194
 
191
- When a query uses includes (subqueries in `select`), each child field is a live `Collection` by default. Subscribe to it with `injectLiveQuery` in a child component:
195
+ When a query uses `include()` for a subquery in `select`, each child field is a live `Collection`. Subscribe to it with `injectLiveQuery` in a child component:
192
196
 
193
197
  ```typescript
194
198
  @Component({
@@ -209,10 +213,12 @@ export class ProjectListComponent {
209
213
  q.from({ p: projectsCollection }).select(({ p }) => ({
210
214
  id: p.id,
211
215
  name: p.name,
212
- issues: q
213
- .from({ i: issuesCollection })
214
- .where(({ i }) => eq(i.projectId, p.id))
215
- .select(({ i }) => ({ id: i.id, title: i.title })),
216
+ issues: include(
217
+ q
218
+ .from({ i: issuesCollection })
219
+ .where(({ i }) => eq(i.projectId, p.id))
220
+ .select(({ i }) => ({ id: i.id, title: i.title })),
221
+ ),
216
222
  })),
217
223
  )
218
224
  }
package/src/index.ts CHANGED
@@ -8,7 +8,9 @@ import {
8
8
  } from '@angular/core'
9
9
  import {
10
10
  BaseQueryBuilder,
11
+ configPool,
11
12
  createLiveQueryObserver,
13
+ getLiveQueryStatusFlags,
12
14
  getPublicCollection,
13
15
  isCollection,
14
16
  isSingleResultCollection,
@@ -19,12 +21,14 @@ import type {
19
21
  CollectionStatus,
20
22
  Context,
21
23
  GetResult,
24
+ HealthNode,
22
25
  InferResultType,
23
26
  InitialQueryBuilder,
24
- LiveQueryCollectionConfig,
27
+ LiveQueryHookConfig,
25
28
  LiveQueryObserver,
26
- LiveQueryPersistedStatus,
29
+ LiveQueryStatusFlags,
27
30
  NonSingleResult,
31
+ PooledLiveQueries,
28
32
  QueryBuilder,
29
33
  SingleResult,
30
34
  } from '@tanstack/db'
@@ -38,7 +42,17 @@ export * from '@tanstack/db'
38
42
  * The result of calling `injectLiveQuery`.
39
43
  * Contains reactive signals for the query state and data.
40
44
  */
41
- export interface InjectLiveQueryResult<TContext extends Context> {
45
+ /** The status and flag signals every `injectLiveQuery` result has. */
46
+ export type InjectLiveQueryFlags = {
47
+ /** A signal containing the current status of the collection */
48
+ status: Signal<CollectionStatus | `disabled`>
49
+ } & {
50
+ [K in keyof LiveQueryStatusFlags]: Signal<LiveQueryStatusFlags[K]>
51
+ }
52
+
53
+ export interface InjectLiveQueryResult<
54
+ TContext extends Context,
55
+ > extends InjectLiveQueryFlags {
42
56
  /** A signal containing the complete state map of results keyed by their ID */
43
57
  state: Signal<Map<string | number, GetResult<TContext>>>
44
58
  /** A signal containing the results as an array, or single result for findOne queries */
@@ -49,21 +63,6 @@ export interface InjectLiveQueryResult<TContext extends Context> {
49
63
  string | number,
50
64
  {}
51
65
  > | null>
52
- /** A signal containing the current status of the collection */
53
- status: Signal<CollectionStatus | `disabled`>
54
- /** A signal indicating whether the collection is currently loading */
55
- isLoading: Signal<boolean>
56
- /** A signal indicating whether the collection is ready */
57
- isReady: Signal<boolean>
58
- persistedStatus: Signal<LiveQueryPersistedStatus>
59
- isPersistedReady: Signal<boolean>
60
- persistedError: Signal<unknown | undefined>
61
- /** A signal indicating whether the collection is idle */
62
- isIdle: Signal<boolean>
63
- /** A signal indicating whether the collection has an error */
64
- isError: Signal<boolean>
65
- /** A signal indicating whether the collection has been cleaned up */
66
- isCleanedUp: Signal<boolean>
67
66
  }
68
67
 
69
68
  type InferConditionalResultType<TContext extends Context> =
@@ -82,38 +81,20 @@ export interface InjectLiveQueryResultWithCollection<
82
81
  TResult extends object = any,
83
82
  TKey extends string | number = string | number,
84
83
  TUtils extends Record<string, any> = {},
85
- > {
84
+ > extends InjectLiveQueryFlags {
86
85
  state: Signal<Map<TKey, TResult>>
87
86
  data: Signal<Array<TResult>>
88
87
  collection: Signal<Collection<TResult, TKey, TUtils> | null>
89
- status: Signal<CollectionStatus | `disabled`>
90
- isLoading: Signal<boolean>
91
- isReady: Signal<boolean>
92
- persistedStatus: Signal<LiveQueryPersistedStatus>
93
- isPersistedReady: Signal<boolean>
94
- persistedError: Signal<unknown | undefined>
95
- isIdle: Signal<boolean>
96
- isError: Signal<boolean>
97
- isCleanedUp: Signal<boolean>
98
88
  }
99
89
 
100
90
  export interface InjectLiveQueryResultWithSingleResultCollection<
101
91
  TResult extends object = any,
102
92
  TKey extends string | number = string | number,
103
93
  TUtils extends Record<string, any> = {},
104
- > {
94
+ > extends InjectLiveQueryFlags {
105
95
  state: Signal<Map<TKey, TResult>>
106
96
  data: Signal<TResult | undefined>
107
97
  collection: Signal<(Collection<TResult, TKey, TUtils> & SingleResult) | null>
108
- status: Signal<CollectionStatus | `disabled`>
109
- isLoading: Signal<boolean>
110
- isReady: Signal<boolean>
111
- persistedStatus: Signal<LiveQueryPersistedStatus>
112
- isPersistedReady: Signal<boolean>
113
- persistedError: Signal<unknown | undefined>
114
- isIdle: Signal<boolean>
115
- isError: Signal<boolean>
116
- isCleanedUp: Signal<boolean>
117
98
  }
118
99
 
119
100
  export function injectLiveQuery<
@@ -121,6 +102,8 @@ export function injectLiveQuery<
121
102
  TParams extends any,
122
103
  >(options: {
123
104
  params: () => TParams
105
+ /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */
106
+ pool?: PooledLiveQueries
124
107
  query: (args: {
125
108
  params: TParams
126
109
  q: InitialQueryBuilder
@@ -131,6 +114,8 @@ export function injectLiveQuery<
131
114
  TParams extends any,
132
115
  >(options: {
133
116
  params: () => TParams
117
+ /** Pass `pooledLiveQueries` to share one partition among equal-shape queries. */
118
+ pool?: PooledLiveQueries
134
119
  query: (args: {
135
120
  params: TParams
136
121
  q: InitialQueryBuilder
@@ -145,7 +130,7 @@ export function injectLiveQuery<TContext extends Context>(
145
130
  ) => QueryBuilder<TContext> | undefined | null,
146
131
  ): InjectConditionalLiveQueryResult<TContext>
147
132
  export function injectLiveQuery<TContext extends Context>(
148
- config: LiveQueryCollectionConfig<TContext>,
133
+ config: LiveQueryHookConfig<TContext>,
149
134
  ): InjectLiveQueryResult<TContext>
150
135
  // Pre-created collection without singleResult
151
136
  export function injectLiveQuery<
@@ -194,19 +179,17 @@ export function injectLiveQuery(opts: any) {
194
179
  typeof opts.params === `function`
195
180
 
196
181
  if (isReactiveQueryOptions) {
197
- const { params, query } = opts
198
- const currentParams = params()
199
-
200
- // Check if query function returns null/undefined (disabled query)
201
- const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder
202
- const result = query({ params: currentParams, q: queryBuilder })
203
-
204
- if (result === undefined || result === null) {
205
- // Disabled query - return null
206
- return null
207
- }
208
-
209
- return resolveLiveQueryValue(result, { gcTime: DEFAULT_GC_TIME_MS })
182
+ // The pool is checked first, so a disabled query rejects it too.
183
+ const pool = configPool(opts)
184
+ // A query function returning null or undefined disables the query.
185
+ const result = opts.query({
186
+ params: opts.params(),
187
+ q: new BaseQueryBuilder() as InitialQueryBuilder,
188
+ })
189
+ return resolveLiveQueryValue(result, {
190
+ gcTime: DEFAULT_GC_TIME_MS,
191
+ pool,
192
+ })
210
193
  }
211
194
 
212
195
  // Handle LiveQueryCollectionConfig objects. Default startSync/gcTime to
@@ -226,8 +209,11 @@ export function injectLiveQuery(opts: any) {
226
209
  const value = statusValue()
227
210
  return value === `idle` && !collection() ? `disabled` : value
228
211
  })
229
- const persistedStatus = signal<LiveQueryPersistedStatus>(`unavailable`)
230
- const persistedError = signal<unknown>(undefined)
212
+ const health = signal<HealthNode | undefined>(undefined)
213
+ // Every status flag comes from the shared rule; a disabled query is ready.
214
+ const flags = computed(() =>
215
+ getLiveQueryStatusFlags(status() as CollectionStatus, health()),
216
+ )
231
217
 
232
218
  // Returns single item for singleResult collections, array otherwise
233
219
  const data = computed(() => {
@@ -251,8 +237,7 @@ export function injectLiveQuery(opts: any) {
251
237
  internalData.set(newData)
252
238
  const snapshot = observer.getSnapshot()
253
239
  statusValue.set(snapshot.status)
254
- persistedStatus.set(snapshot.persistedStatus)
255
- persistedError.set(snapshot.persistedError)
240
+ health.set(snapshot.health)
256
241
  }
257
242
 
258
243
  let unsub: (() => void) | null = null
@@ -267,8 +252,7 @@ export function injectLiveQuery(opts: any) {
267
252
  // Handle null collection (disabled query)
268
253
  if (!currentCollection) {
269
254
  statusValue.set(`disabled` as const)
270
- persistedStatus.set(`unavailable`)
271
- persistedError.set(undefined)
255
+ health.set(undefined)
272
256
  state.set(new Map())
273
257
  internalData.set([])
274
258
  cleanup()
@@ -311,13 +295,13 @@ export function injectLiveQuery(opts: any) {
311
295
  getPublicCollection(collection()),
312
296
  ) as Signal<any>,
313
297
  status,
314
- isLoading: computed(() => status() === `loading`),
315
- isReady: computed(() => status() === `ready` || status() === `disabled`),
316
- persistedStatus,
317
- isPersistedReady: computed(() => persistedStatus() === `ready`),
318
- persistedError,
319
- isIdle: computed(() => status() === `idle`),
320
- isError: computed(() => status() === `error`),
321
- isCleanedUp: computed(() => status() === `cleaned-up`),
298
+ isLoading: computed(() => flags().isLoading),
299
+ isReady: computed(() => flags().isReady || status() === `disabled`),
300
+ isIdle: computed(() => flags().isIdle),
301
+ isLoadingError: computed(() => flags().isLoadingError),
302
+ isCrashed: computed(() => flags().isCrashed),
303
+ isCleanedUp: computed(() => flags().isCleanedUp),
304
+ isSyncError: computed(() => flags().isSyncError),
305
+ health,
322
306
  }
323
307
  }