diff --git a/packages/angular-query-experimental/package.json b/packages/angular-query-experimental/package.json index 3c3a36734e..7efa7eb76d 100644 --- a/packages/angular-query-experimental/package.json +++ b/packages/angular-query-experimental/package.json @@ -54,11 +54,13 @@ "@angular/platform-browser": "^17.0.8", "@angular/platform-browser-dynamic": "^17.0.8", "ng-packagr": "^17.0.3", + "rxjs": "^7.8.1", "typescript": "5.2.2", "zone.js": "^0.14.2" }, "peerDependencies": { - "@angular/core": "^17" + "@angular/core": "^17", + "rxjs": "^7" }, "module": "build/fesm2022/tanstack-angular-query-experimental.mjs", "types": "build/index.d.ts", diff --git a/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts b/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts index 4f8bcdcad7..963e766637 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf } from 'vitest' import { injectQuery } from '../inject-query' -import { simpleFetcher } from './test-utils' +import { simpleFetcher, simpleObservable } from './test-utils' import type { Signal } from '@angular/core' describe('Discriminated union return type', () => { @@ -56,4 +56,24 @@ describe('Discriminated union return type', () => { expectTypeOf(query.error).toEqualTypeOf>() } }) + + test('data should be infered from a passed in observable', () => { + const query = injectQuery(() => ({ + queryKey: ['key'], + query$: () => simpleObservable(), + })) + + expectTypeOf(query.data).toEqualTypeOf>() + }) + + test('data should still be defined when query is successful', () => { + const query = injectQuery(() => ({ + queryKey: ['key'], + query$: () => simpleObservable(), + })) + + if (query.isSuccess()) { + expectTypeOf(query.data).toEqualTypeOf>() + } + }) }) diff --git a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts index 8d317452f4..2ca064f966 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts @@ -2,6 +2,7 @@ import { computed, signal } from '@angular/core' import { TestBed, fakeAsync, flush, tick } from '@angular/core/testing' import { QueryClient } from '@tanstack/query-core' import { expect, vi } from 'vitest' +import { Subject } from 'rxjs' import { injectQuery } from '../inject-query' import { provideAngularQuery } from '../providers' import { @@ -215,4 +216,24 @@ describe('injectQuery', () => { expect(query.status()).toBe('error') })) + + test('should allow an observable to be passed as queryFn', fakeAsync(() => { + const subject$ = new Subject() + const query = TestBed.runInInjectionContext(() => { + return injectQuery(() => ({ + queryKey: ['key14'], + query$: () => subject$.asObservable(), + })) + }) + + expect(query.status()).toBe('pending') + + flush() + + subject$.next('Some data') + + flush() + expect(query.status()).toBe('success') + expect(query.data()).toBe('Some data') + })) }) diff --git a/packages/angular-query-experimental/src/__tests__/test-utils.ts b/packages/angular-query-experimental/src/__tests__/test-utils.ts index 1b3055c955..1751ffc3c9 100644 --- a/packages/angular-query-experimental/src/__tests__/test-utils.ts +++ b/packages/angular-query-experimental/src/__tests__/test-utils.ts @@ -1,4 +1,5 @@ import { isSignal, untracked } from '@angular/core' +import { interval, map, take } from 'rxjs' export function simpleFetcher(): Promise { return new Promise((resolve) => { @@ -8,6 +9,13 @@ export function simpleFetcher(): Promise { }) } +export function simpleObservable() { + return interval(1000).pipe( + map((_, i) => `Some data ${i}`), + take(5), + ) +} + export function delayedFetcher(timeout = 0): () => Promise { return () => new Promise((resolve) => { diff --git a/packages/angular-query-experimental/src/create-base-query.ts b/packages/angular-query-experimental/src/create-base-query.ts index c5a0e11839..2c69537db3 100644 --- a/packages/angular-query-experimental/src/create-base-query.ts +++ b/packages/angular-query-experimental/src/create-base-query.ts @@ -7,9 +7,26 @@ import { signal, } from '@angular/core' import { notifyManager } from '@tanstack/query-core' +import { + Subject, + fromEvent, + lastValueFrom, + shareReplay, + skip, + switchMap, + take, + takeUntil, +} from 'rxjs' +import { takeUntilDestroyed } from '@angular/core/rxjs-interop' import { signalProxy } from './signal-proxy' -import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core' +import type { + QueryClient, + QueryFunctionContext, + QueryKey, + QueryObserver, +} from '@tanstack/query-core' import type { CreateBaseQueryOptions, CreateBaseQueryResult } from './types' +import type { Subscription } from 'rxjs' /** * Base implementation for `injectQuery` and `injectInfiniteQuery`. @@ -20,6 +37,7 @@ export function createBaseQuery< TData, TQueryData, TQueryKey extends QueryKey, + TPageParam = never, >( options: ( client: QueryClient, @@ -28,7 +46,8 @@ export function createBaseQuery< TError, TData, TQueryData, - TQueryKey + TQueryKey, + TPageParam >, Observer: typeof QueryObserver, queryClient: QueryClient, @@ -36,6 +55,11 @@ export function createBaseQuery< assertInInjectionContext(createBaseQuery) const destroyRef = inject(DestroyRef) + /** + * Subscription to the query$ observable. + */ + let subscription: Subscription | undefined + /** * Signal that has the default options from query client applied * computed() is used so signals can be inserted into the options @@ -43,9 +67,55 @@ export function createBaseQuery< * are preserved and can keep being applied after signal changes */ const defaultedOptionsSignal = computed(() => { - const defaultedOptions = queryClient.defaultQueryOptions( - options(queryClient), - ) + const { query$, ...opts } = options(queryClient) + + // If there is a subscription, unsubscribe from it this is to prevent + // multiple subscriptions on the same computed type + if (subscription) subscription.unsubscribe() + + /** + * Subscribe to the query$ observable and set the query data + * when the observable emits a value. This creates a promise + * on the queryFn and on each new emit it will update the client + * side. + */ + if (query$) { + const trigger$ = new Subject< + QueryFunctionContext + >() + + const obs$ = trigger$.pipe( + switchMap((context) => + query$(context).pipe( + takeUntil( + // If the signal is aborted, abort the observable + fromEvent(context.signal, 'abort'), + ), + ), + ), + shareReplay(1), + takeUntilDestroyed(destroyRef), + ) + + subscription = obs$.pipe(skip(1)).subscribe({ + next: (value) => + queryClient.setQueryData(opts.queryKey, value), + }) + + const queryFn = ( + context: QueryFunctionContext, + ) => { + // Trigger the observable with the new context. + const promise = lastValueFrom(obs$.pipe(take(1))) + trigger$.next(context) + return promise + } + + opts.queryFn = queryFn + } + + const defaultedOptions = queryClient.defaultQueryOptions(opts) + defaultedOptions._optimisticResults = 'optimistic' return defaultedOptions }) diff --git a/packages/angular-query-experimental/src/types.ts b/packages/angular-query-experimental/src/types.ts index 77f828e173..b1c0c6a1b2 100644 --- a/packages/angular-query-experimental/src/types.ts +++ b/packages/angular-query-experimental/src/types.ts @@ -1,4 +1,5 @@ import type { Signal } from '@angular/core' +import type { Observable } from 'rxjs' import type { DefaultError, @@ -8,6 +9,7 @@ import type { MutateFunction, MutationObserverOptions, MutationObserverResult, + QueryFunctionContext, QueryKey, QueryObserverOptions, QueryObserverResult, @@ -21,10 +23,22 @@ export interface CreateBaseQueryOptions< TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, + TPageParam = never, > extends WithRequired< - QueryObserverOptions, + QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + >, 'queryKey' - > {} + > { + query$?: ( + context: QueryFunctionContext, + ) => Observable +} type CreateStatusBasedQueryResult< TStatus extends QueryObserverResult['status'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6244ea6656..05a96a20ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1439,7 +1439,7 @@ importers: devDependencies: '@angular-devkit/build-angular': specifier: ^17.0.8 - version: 17.0.8(@angular/compiler-cli@17.0.8)(@types/node@18.19.3)(ng-packagr@17.0.3)(typescript@5.2.2) + version: 17.0.8(@angular/compiler-cli@17.0.8)(@types/node@18.19.3)(typescript@5.2.2) '@angular/cli': specifier: ^17.0.8 version: 17.0.8 @@ -1678,6 +1678,9 @@ importers: ng-packagr: specifier: ^17.0.3 version: 17.0.3(@angular/compiler-cli@17.0.8)(tslib@2.6.2)(typescript@5.2.2) + rxjs: + specifier: ^7.8.1 + version: 7.8.1 typescript: specifier: 5.2.2 version: 5.2.2 @@ -2283,6 +2286,129 @@ packages: typescript: 5.2.2 undici: 5.27.2 vite: 4.5.1(@types/node@18.19.3)(less@4.2.0)(sass@1.69.5)(terser@5.24.0) + webpack: 5.89.0(esbuild@0.19.5) + webpack-dev-middleware: 6.1.1(webpack@5.89.0) + webpack-dev-server: 4.15.1(webpack@5.89.0) + webpack-merge: 5.10.0 + webpack-subresource-integrity: 5.1.0(webpack@5.89.0) + optionalDependencies: + esbuild: 0.19.5 + transitivePeerDependencies: + - '@swc/core' + - '@types/express' + - '@types/node' + - bufferutil + - debug + - fibers + - html-webpack-plugin + - lightningcss + - node-sass + - sass-embedded + - stylus + - sugarss + - supports-color + - uglify-js + - utf-8-validate + - webpack-cli + dev: true + + /@angular-devkit/build-angular@17.0.8(@angular/compiler-cli@17.0.8)(@types/node@18.19.3)(typescript@5.2.2): + resolution: {integrity: sha512-u7R5yX92ZxOL/LfxiKGGqlBo86100sJ5Rabavn8DeGtYP8N0qgwCcNwlW2zaMoUlkw2geMnxcxIX5VJI4iFPUA==} + engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + '@angular/compiler-cli': ^17.0.0 + '@angular/localize': ^17.0.0 + '@angular/platform-server': ^17.0.0 + '@angular/service-worker': ^17.0.0 + jest: ^29.5.0 + jest-environment-jsdom: ^29.5.0 + karma: ^6.3.0 + ng-packagr: ^17.0.0 + protractor: ^7.0.0 + tailwindcss: ^2.0.0 || ^3.0.0 + typescript: '>=5.2 <5.3' + peerDependenciesMeta: + '@angular/localize': + optional: true + '@angular/platform-server': + optional: true + '@angular/service-worker': + optional: true + jest: + optional: true + jest-environment-jsdom: + optional: true + karma: + optional: true + ng-packagr: + optional: true + protractor: + optional: true + tailwindcss: + optional: true + dependencies: + '@ampproject/remapping': 2.2.1 + '@angular-devkit/architect': 0.1700.8(chokidar@3.5.3) + '@angular-devkit/build-webpack': 0.1700.8(chokidar@3.5.3)(webpack-dev-server@4.15.1)(webpack@5.89.0) + '@angular-devkit/core': 17.0.8(chokidar@3.5.3) + '@angular/compiler-cli': 17.0.8(@angular/compiler@17.0.8)(typescript@5.2.2) + '@babel/core': 7.23.2 + '@babel/generator': 7.23.0 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/plugin-transform-async-generator-functions': 7.23.2(@babel/core@7.23.2) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.23.2) + '@babel/plugin-transform-runtime': 7.23.2(@babel/core@7.23.2) + '@babel/preset-env': 7.23.2(@babel/core@7.23.2) + '@babel/runtime': 7.23.2 + '@discoveryjs/json-ext': 0.5.7 + '@ngtools/webpack': 17.0.8(@angular/compiler-cli@17.0.8)(typescript@5.2.2)(webpack@5.89.0) + '@vitejs/plugin-basic-ssl': 1.0.1(vite@4.5.1) + ansi-colors: 4.1.3 + autoprefixer: 10.4.16(postcss@8.4.31) + babel-loader: 9.1.3(@babel/core@7.23.2)(webpack@5.89.0) + babel-plugin-istanbul: 6.1.1 + browser-sync: 2.29.3 + browserslist: 4.22.2 + chokidar: 3.5.3 + copy-webpack-plugin: 11.0.0(webpack@5.89.0) + critters: 0.0.20 + css-loader: 6.8.1(webpack@5.89.0) + esbuild-wasm: 0.19.5 + fast-glob: 3.3.1 + http-proxy-middleware: 2.0.6(@types/express@4.17.20) + https-proxy-agent: 7.0.2 + inquirer: 9.2.11 + jsonc-parser: 3.2.0 + karma-source-map-support: 1.4.0 + less: 4.2.0 + less-loader: 11.1.0(less@4.2.0)(webpack@5.89.0) + license-webpack-plugin: 4.0.2(webpack@5.89.0) + loader-utils: 3.2.1 + magic-string: 0.30.5 + mini-css-extract-plugin: 2.7.6(webpack@5.89.0) + mrmime: 1.0.1 + open: 8.4.2 + ora: 5.4.1 + parse5-html-rewriting-stream: 7.0.0 + picomatch: 3.0.1 + piscina: 4.1.0 + postcss: 8.4.31 + postcss-loader: 7.3.3(postcss@8.4.31)(typescript@5.2.2)(webpack@5.89.0) + resolve-url-loader: 5.0.0 + rxjs: 7.8.1 + sass: 1.69.5 + sass-loader: 13.3.2(sass@1.69.5)(webpack@5.89.0) + semver: 7.5.4 + source-map-loader: 4.0.1(webpack@5.89.0) + source-map-support: 0.5.21 + terser: 5.24.0 + text-table: 0.2.0 + tree-kill: 1.2.2 + tslib: 2.6.2 + typescript: 5.2.2 + undici: 5.27.2 + vite: 4.5.1(@types/node@18.19.3)(less@4.2.0)(sass@1.69.5)(terser@5.24.0) webpack: 5.89.0(esbuild@0.19.10) webpack-dev-middleware: 6.1.1(webpack@5.89.0) webpack-dev-server: 4.15.1(webpack@5.89.0) @@ -2318,7 +2444,7 @@ packages: dependencies: '@angular-devkit/architect': 0.1700.8(chokidar@3.5.3) rxjs: 7.8.1 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) webpack-dev-server: 4.15.1(webpack@5.89.0) transitivePeerDependencies: - chokidar @@ -7566,7 +7692,7 @@ packages: dependencies: '@angular/compiler-cli': 17.0.8(@angular/compiler@17.0.8)(typescript@5.2.2) typescript: 5.2.2 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) dev: true /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1: @@ -11678,7 +11804,7 @@ packages: '@babel/core': 7.23.2 find-cache-dir: 4.0.0 schema-utils: 4.2.0 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) dev: true /babel-plugin-add-module-exports@0.2.1: @@ -13392,7 +13518,7 @@ packages: normalize-path: 3.0.0 schema-utils: 4.2.0 serialize-javascript: 6.0.1 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) dev: true /core-js-compat@3.33.0: @@ -13733,7 +13859,7 @@ packages: postcss-modules-values: 4.0.0(postcss@8.4.32) postcss-value-parser: 4.2.0 semver: 7.5.4 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) /css-minimizer-webpack-plugin@3.4.1(esbuild@0.19.10)(webpack@5.89.0): resolution: {integrity: sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==} @@ -20462,7 +20588,7 @@ packages: dependencies: klona: 2.0.6 less: 4.2.0 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) dev: true /less@4.2.0: @@ -20513,7 +20639,7 @@ packages: webpack-sources: optional: true dependencies: - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) webpack-sources: 3.2.3 dev: true @@ -21567,7 +21693,7 @@ packages: webpack: ^5.0.0 dependencies: schema-utils: 4.2.0 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) /minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -24093,7 +24219,7 @@ packages: jiti: 1.21.0 postcss: 8.4.31 semver: 7.5.4 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) transitivePeerDependencies: - typescript dev: true @@ -26831,7 +26957,7 @@ packages: dependencies: neo-async: 2.6.2 sass: 1.69.5 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) dev: true /sass@1.69.5: @@ -27541,7 +27667,7 @@ packages: abab: 2.0.6 iconv-lite: 0.6.3 source-map-js: 1.0.2 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) dev: true /source-map-resolve@0.5.3: @@ -28590,6 +28716,30 @@ packages: terser: 5.24.0 webpack: 5.89.0(esbuild@0.19.10) + /terser-webpack-plugin@5.3.9(esbuild@0.19.5)(webpack@5.89.0): + resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.20 + esbuild: 0.19.5 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.1 + terser: 5.24.0 + webpack: 5.89.0(esbuild@0.19.5) + /terser@4.8.1: resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==} engines: {node: '>=6.0.0'} @@ -30300,7 +30450,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.2.0 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) /webpack-dev-middleware@6.1.1(webpack@5.89.0): resolution: {integrity: sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==} @@ -30316,7 +30466,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.2.0 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) dev: true /webpack-dev-server@3.11.1(webpack@4.44.2): @@ -30410,7 +30560,7 @@ packages: serve-index: 1.9.1(supports-color@6.1.0) sockjs: 0.3.24 spdy: 4.0.2(supports-color@6.1.0) - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) webpack-dev-middleware: 5.3.3(webpack@5.89.0) ws: 8.14.2 transitivePeerDependencies: @@ -30490,7 +30640,7 @@ packages: optional: true dependencies: typed-assert: 1.0.9 - webpack: 5.89.0(esbuild@0.19.10) + webpack: 5.89.0(esbuild@0.19.5) dev: true /webpack-virtual-modules@0.6.1: @@ -30576,6 +30726,45 @@ packages: - esbuild - uglify-js + /webpack@5.89.0(esbuild@0.19.5): + resolution: {integrity: sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.6 + '@types/estree': 1.0.3 + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/wasm-edit': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + acorn: 8.10.0 + acorn-import-assertions: 1.9.0(acorn@8.10.0) + browserslist: 4.22.2 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.15.0 + es-module-lexer: 1.3.1 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.9(esbuild@0.19.5)(webpack@5.89.0) + watchpack: 2.4.0 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + /websocket-driver@0.7.4: resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} engines: {node: '>=0.8.0'}