diff --git a/examples/angular/basic-persister/.devcontainer/devcontainer.json b/examples/angular/basic-persister/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..365adf8f4c --- /dev/null +++ b/examples/angular/basic-persister/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22" +} diff --git a/examples/angular/basic-persister/.eslintrc.cjs b/examples/angular/basic-persister/.eslintrc.cjs new file mode 100644 index 0000000000..cca134ce16 --- /dev/null +++ b/examples/angular/basic-persister/.eslintrc.cjs @@ -0,0 +1,6 @@ +// @ts-check + +/** @type {import('eslint').Linter.Config} */ +const config = {} + +module.exports = config diff --git a/examples/angular/basic-persister/README.md b/examples/angular/basic-persister/README.md new file mode 100644 index 0000000000..47d5931979 --- /dev/null +++ b/examples/angular/basic-persister/README.md @@ -0,0 +1,6 @@ +# TanStack Query Angular basic persister example + +To run this example: + +- `npm install` or `yarn` or `pnpm i` or `bun i` +- `npm run start` or `yarn start` or `pnpm start` or `bun start` diff --git a/examples/angular/basic-persister/angular.json b/examples/angular/basic-persister/angular.json new file mode 100644 index 0000000000..64adfea7c4 --- /dev/null +++ b/examples/angular/basic-persister/angular.json @@ -0,0 +1,104 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "basic-persister": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "outputPath": "dist/basic-persister", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico", "src/assets"], + "styles": [], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "basic-persister:build:production" + }, + "development": { + "buildTarget": "basic-persister:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n", + "options": { + "buildTarget": "basic-persister:build" + } + } + } + } + } +} diff --git a/examples/angular/basic-persister/package.json b/examples/angular/basic-persister/package.json new file mode 100644 index 0000000000..aadaabaf0e --- /dev/null +++ b/examples/angular/basic-persister/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tanstack/query-example-angular-basic-persister", + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "private": true, + "dependencies": { + "@angular/common": "^19.1.0-next.0", + "@angular/compiler": "^19.1.0-next.0", + "@angular/core": "^19.1.0-next.0", + "@angular/platform-browser": "^19.1.0-next.0", + "@angular/platform-browser-dynamic": "^19.1.0-next.0", + "@tanstack/angular-query-experimental": "^5.62.7", + "@tanstack/angular-query-persist-client-experimental": "^5.62.7", + "@tanstack/query-sync-storage-persister": "^5.62.3", + "rxjs": "^7.8.1", + "tslib": "^2.6.3", + "zone.js": "^0.15.0" + }, + "devDependencies": { + "@angular/build": "^19.0.2", + "@angular/cli": "^19.0.2", + "@angular/compiler-cli": "^19.1.0-next.0", + "typescript": "5.7.2" + } +} diff --git a/examples/angular/basic-persister/src/app/app.component.html b/examples/angular/basic-persister/src/app/app.component.html new file mode 100644 index 0000000000..4c21f9e879 --- /dev/null +++ b/examples/angular/basic-persister/src/app/app.component.html @@ -0,0 +1,10 @@ +

+ Try to mock offline behavior with the button in the devtools. You can navigate + around as long as there is already data in the cache. You'll get a refetch as + soon as you go "online" again. +

+@if (postId() > -1) { + +} @else { + +} diff --git a/examples/angular/basic-persister/src/app/app.component.ts b/examples/angular/basic-persister/src/app/app.component.ts new file mode 100644 index 0000000000..5958a1b5e7 --- /dev/null +++ b/examples/angular/basic-persister/src/app/app.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { PostComponent } from './components/post.component' +import { PostsComponent } from './components/posts.component' + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'basic-example', + templateUrl: './app.component.html', + imports: [PostComponent, PostsComponent], +}) +export class BasicExampleComponent { + postId = signal(-1) +} diff --git a/examples/angular/basic-persister/src/app/app.config.ts b/examples/angular/basic-persister/src/app/app.config.ts new file mode 100644 index 0000000000..9d7b34fbd8 --- /dev/null +++ b/examples/angular/basic-persister/src/app/app.config.ts @@ -0,0 +1,37 @@ +import { provideHttpClient, withFetch } from '@angular/common/http' +import { + QueryClient, + provideTanStackQuery, + withDevtools, +} from '@tanstack/angular-query-experimental' +import { withPersistQueryClient } from '@tanstack/angular-query-persist-client-experimental' +import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' +import type { ApplicationConfig } from '@angular/core' + +const localStoragePersister = createSyncStoragePersister({ + storage: window.localStorage, +}) + +export const appConfig: ApplicationConfig = { + providers: [ + provideHttpClient(withFetch()), + provideTanStackQuery( + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60, + gcTime: 1000 * 60 * 60 * 24, // 24 hours + }, + }, + }), + withDevtools(), + withPersistQueryClient([ + { + persistOptions: { + persister: localStoragePersister, + }, + }, + ]), + ), + ], +} diff --git a/examples/angular/basic-persister/src/app/components/post.component.html b/examples/angular/basic-persister/src/app/components/post.component.html new file mode 100644 index 0000000000..34b36e94fc --- /dev/null +++ b/examples/angular/basic-persister/src/app/components/post.component.html @@ -0,0 +1,19 @@ +
+
+ Back +
+ @if (postQuery.isPending()) { + Loading... + } @else if (postQuery.isError()) { + Error: {{ postQuery.error().message }} + } + @if (postQuery.data(); as post) { +

{{ post.title }}

+
+

{{ post.body }}

+
+ @if (postQuery.isFetching()) { + Background Updating... + } + } +
diff --git a/examples/angular/basic-persister/src/app/components/post.component.ts b/examples/angular/basic-persister/src/app/components/post.component.ts new file mode 100644 index 0000000000..d77e707e99 --- /dev/null +++ b/examples/angular/basic-persister/src/app/components/post.component.ts @@ -0,0 +1,39 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Output, + inject, + input, +} from '@angular/core' +import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental' +import { fromEvent, lastValueFrom, takeUntil } from 'rxjs' +import { PostsService } from '../services/posts-service' + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'post', + standalone: true, + templateUrl: './post.component.html', +}) +export class PostComponent { + #postsService = inject(PostsService) + + @Output() setPostId = new EventEmitter() + + postId = input(0) + + postQuery = injectQuery(() => ({ + enabled: this.postId() > 0, + queryKey: ['post', this.postId()], + queryFn: async (context) => { + // Cancels the request when component is destroyed before the request finishes + const abort$ = fromEvent(context.signal, 'abort') + return lastValueFrom( + this.#postsService.postById$(this.postId()).pipe(takeUntil(abort$)), + ) + }, + })) + + queryClient = inject(QueryClient) +} diff --git a/examples/angular/basic-persister/src/app/components/posts.component.html b/examples/angular/basic-persister/src/app/components/posts.component.html new file mode 100644 index 0000000000..568a6a4df4 --- /dev/null +++ b/examples/angular/basic-persister/src/app/components/posts.component.html @@ -0,0 +1,39 @@ +
+

Posts

+ @switch (postsQuery.status()) { + @case ('pending') { + Loading... + } + @case ('error') { + Error: {{ postsQuery.error()?.message }} + } + @default { +
+ @for (post of postsQuery.data(); track post.id) { +

+ + + {{ post.title }} +

+ } +
+ } + } +
+ @if (postsQuery.isFetching()) { + Background Updating... + } +
+
diff --git a/examples/angular/basic-persister/src/app/components/posts.component.ts b/examples/angular/basic-persister/src/app/components/posts.component.ts new file mode 100644 index 0000000000..3c8bf7c79d --- /dev/null +++ b/examples/angular/basic-persister/src/app/components/posts.component.ts @@ -0,0 +1,28 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Output, + inject, +} from '@angular/core' +import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental' +import { lastValueFrom } from 'rxjs' +import { PostsService } from '../services/posts-service' + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'posts', + standalone: true, + templateUrl: './posts.component.html', +}) +export class PostsComponent { + queryClient = inject(QueryClient) + #postsService = inject(PostsService) + + @Output() setPostId = new EventEmitter() + + postsQuery = injectQuery(() => ({ + queryKey: ['posts'], + queryFn: () => lastValueFrom(this.#postsService.allPosts$()), + })) +} diff --git a/examples/angular/basic-persister/src/app/services/posts-service.ts b/examples/angular/basic-persister/src/app/services/posts-service.ts new file mode 100644 index 0000000000..fed2a1b11c --- /dev/null +++ b/examples/angular/basic-persister/src/app/services/posts-service.ts @@ -0,0 +1,21 @@ +import { HttpClient } from '@angular/common/http' +import { Injectable, inject } from '@angular/core' + +@Injectable({ + providedIn: 'root', +}) +export class PostsService { + #http = inject(HttpClient) + + postById$ = (postId: number) => + this.#http.get(`https://jsonplaceholder.typicode.com/posts/${postId}`) + + allPosts$ = () => + this.#http.get>('https://jsonplaceholder.typicode.com/posts') +} + +export interface Post { + id: number + title: string + body: string +} diff --git a/examples/angular/basic-persister/src/favicon.ico b/examples/angular/basic-persister/src/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/basic-persister/src/favicon.ico differ diff --git a/examples/angular/basic-persister/src/index.html b/examples/angular/basic-persister/src/index.html new file mode 100644 index 0000000000..2e262442e1 --- /dev/null +++ b/examples/angular/basic-persister/src/index.html @@ -0,0 +1,13 @@ + + + + + TanStack Query Angular basic persister example + + + + + + + + diff --git a/examples/angular/basic-persister/src/main.ts b/examples/angular/basic-persister/src/main.ts new file mode 100644 index 0000000000..aa33a0b9ff --- /dev/null +++ b/examples/angular/basic-persister/src/main.ts @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { BasicExampleComponent } from './app/app.component' + +bootstrapApplication(BasicExampleComponent, appConfig).catch((err) => + console.error(err), +) diff --git a/examples/angular/basic-persister/tsconfig.app.json b/examples/angular/basic-persister/tsconfig.app.json new file mode 100644 index 0000000000..5b9d3c5ecb --- /dev/null +++ b/examples/angular/basic-persister/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/basic-persister/tsconfig.json b/examples/angular/basic-persister/tsconfig.json new file mode 100644 index 0000000000..d0d73c8beb --- /dev/null +++ b/examples/angular/basic-persister/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictStandalone": true, + "strictTemplates": true + } +} diff --git a/examples/angular/multiple-persisters/.devcontainer/devcontainer.json b/examples/angular/multiple-persisters/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..365adf8f4c --- /dev/null +++ b/examples/angular/multiple-persisters/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22" +} diff --git a/examples/angular/multiple-persisters/.eslintrc.cjs b/examples/angular/multiple-persisters/.eslintrc.cjs new file mode 100644 index 0000000000..cca134ce16 --- /dev/null +++ b/examples/angular/multiple-persisters/.eslintrc.cjs @@ -0,0 +1,6 @@ +// @ts-check + +/** @type {import('eslint').Linter.Config} */ +const config = {} + +module.exports = config diff --git a/examples/angular/multiple-persisters/README.md b/examples/angular/multiple-persisters/README.md new file mode 100644 index 0000000000..d0551cf9de --- /dev/null +++ b/examples/angular/multiple-persisters/README.md @@ -0,0 +1,6 @@ +# TanStack Query Angular multiple persisters example + +To run this example: + +- `npm install` or `yarn` or `pnpm i` or `bun i` +- `npm run start` or `yarn start` or `pnpm start` or `bun start` diff --git a/examples/angular/multiple-persisters/angular.json b/examples/angular/multiple-persisters/angular.json new file mode 100644 index 0000000000..e9ada92785 --- /dev/null +++ b/examples/angular/multiple-persisters/angular.json @@ -0,0 +1,104 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "multiple-persisters": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "outputPath": "dist/multiple-persisters", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "multiple-persisters:build:production" + }, + "development": { + "buildTarget": "multiple-persisters:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n", + "options": { + "buildTarget": "multiple-persisters:build" + } + } + } + } + } +} diff --git a/examples/angular/multiple-persisters/package.json b/examples/angular/multiple-persisters/package.json new file mode 100644 index 0000000000..4a1d978ba4 --- /dev/null +++ b/examples/angular/multiple-persisters/package.json @@ -0,0 +1,33 @@ +{ + "name": "@tanstack/query-example-angular-multiple-persisters", + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "private": true, + "dependencies": { + "@angular/common": "^19.1.0-next.0", + "@angular/compiler": "^19.1.0-next.0", + "@angular/core": "^19.1.0-next.0", + "@angular/platform-browser": "^19.1.0-next.0", + "@angular/platform-browser-dynamic": "^19.1.0-next.0", + "@tanstack/angular-query-experimental": "^5.62.7", + "@tanstack/angular-query-persist-client-experimental": "^5.62.7", + "@tanstack/query-sync-storage-persister": "^5.62.3", + "rxjs": "^7.8.1", + "tslib": "^2.6.3", + "zone.js": "^0.15.0" + }, + "devDependencies": { + "@angular/build": "^19.0.2", + "@angular/cli": "^19.0.2", + "@angular/compiler-cli": "^19.1.0-next.0", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.7", + "typescript": "5.7.2" + } +} diff --git a/examples/angular/multiple-persisters/src/app/app.component.ts b/examples/angular/multiple-persisters/src/app/app.component.ts new file mode 100644 index 0000000000..9ad189c91b --- /dev/null +++ b/examples/angular/multiple-persisters/src/app/app.component.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { UserPreferencesComponent } from './components/user-preferences.component' +import { SessionDataComponent } from './components/session-data.component' + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-root', + standalone: true, + template: ` +
+
+

+ TanStack Query Persistence Demo +

+

+ This demo illustrates how to selectively persist queries to different + persisters. By leveraging shouldDehydrateQuery, it is possible to + strategically cache data in multiple persisters based on specific + query requirements. +

+
+ + +
+
+
+ `, + imports: [UserPreferencesComponent, SessionDataComponent], +}) +export class AppComponent {} diff --git a/examples/angular/multiple-persisters/src/app/app.config.ts b/examples/angular/multiple-persisters/src/app/app.config.ts new file mode 100644 index 0000000000..544da92c30 --- /dev/null +++ b/examples/angular/multiple-persisters/src/app/app.config.ts @@ -0,0 +1,60 @@ +import { + provideHttpClient, + withFetch, + withInterceptors, +} from '@angular/common/http' +import { + QueryClient, + provideTanStackQuery, + withDevtools, +} from '@tanstack/angular-query-experimental' +import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' +import { withPersistQueryClient } from '@tanstack/angular-query-persist-client-experimental' +import { mockInterceptor } from './interceptor/mock-api.interceptor' +import type { ApplicationConfig } from '@angular/core' + +const localStoragePersister = createSyncStoragePersister({ + storage: window.localStorage, +}) + +const sessionStoragePersister = createSyncStoragePersister({ + storage: window.sessionStorage, +}) + +export const appConfig: ApplicationConfig = { + providers: [ + provideHttpClient(withFetch(), withInterceptors([mockInterceptor])), + provideTanStackQuery( + new QueryClient({ + defaultOptions: { + queries: { + gcTime: 1000 * 60 * 60 * 24, // 24 hours + }, + }, + }), + withDevtools(), + withPersistQueryClient([ + { + persistOptions: { + persister: localStoragePersister, + dehydrateOptions: { + shouldDehydrateQuery: (query) => + query.state.status === 'success' && + query.queryKey[0] === 'preferences', + }, + }, + }, + { + persistOptions: { + persister: sessionStoragePersister, + dehydrateOptions: { + shouldDehydrateQuery: (query) => + query.state.status === 'success' && + query.queryKey[0] === 'session', + }, + }, + }, + ]), + ), + ], +} diff --git a/examples/angular/multiple-persisters/src/app/components/session-data.component.ts b/examples/angular/multiple-persisters/src/app/components/session-data.component.ts new file mode 100644 index 0000000000..88e9f85b6e --- /dev/null +++ b/examples/angular/multiple-persisters/src/app/components/session-data.component.ts @@ -0,0 +1,75 @@ +import { Component, inject } from '@angular/core' +import { injectQuery } from '@tanstack/angular-query-experimental' +import { HttpClient } from '@angular/common/http' +import { firstValueFrom } from 'rxjs' +import { DatePipe } from '@angular/common' + +interface SessionData { + lastActive: string + currentView: string + activeFilters: Array + temporaryNotes: string +} + +@Component({ + selector: 'session-data', + template: ` + @if (sessionData.isLoading()) { +
+
+
+
+ } @else if (sessionData.isError()) { +
+ Error loading session data: {{ sessionData.error() }} +
+ } @else { +
+
+ 🔑 +

+ Session Data + (stored in sessionStorage) +

+
+
+
+ Last Active: + + {{ sessionData.data()?.lastActive | date }} + +
+
+ Current View: + {{ + sessionData.data()?.currentView + }} +
+
+ Active Filters: + + {{ sessionData.data()?.activeFilters?.join(', ') }} + +
+
+ Temporary Notes: + {{ + sessionData.data()?.temporaryNotes + }} +
+
+
+ } + `, + standalone: true, + imports: [DatePipe], +}) +export class SessionDataComponent { + #http = inject(HttpClient) + + sessionData = injectQuery(() => ({ + queryKey: ['session'], + queryFn: () => firstValueFrom(this.#http.get('/session')), + staleTime: 1000 * 60 * 60, // 1 hour + })) +} diff --git a/examples/angular/multiple-persisters/src/app/components/user-preferences.component.ts b/examples/angular/multiple-persisters/src/app/components/user-preferences.component.ts new file mode 100644 index 0000000000..c499ccd27e --- /dev/null +++ b/examples/angular/multiple-persisters/src/app/components/user-preferences.component.ts @@ -0,0 +1,73 @@ +import { Component, inject } from '@angular/core' +import { injectQuery } from '@tanstack/angular-query-experimental' +import { HttpClient } from '@angular/common/http' +import { firstValueFrom } from 'rxjs' + +interface UserPreferences { + theme: string + language: string + notifications: boolean + fontSize: string +} + +@Component({ + selector: 'user-preferences', + template: ` + @if (userPreferences.isLoading()) { +
+
+
+
+ } @else if (userPreferences.isError()) { +
+ Error loading preferences: {{ userPreferences.error() }} +
+ } @else { +
+
+ ⚙️ +

+ User Preferences + (stored in localStorage) +

+
+
+
+ Theme: + {{ userPreferences.data()?.theme }} +
+
+ Language: + {{ + userPreferences.data()?.language + }} +
+
+ Notifications: + {{ + userPreferences.data()?.notifications ? 'Enabled' : 'Disabled' + }} +
+
+ Font Size: + {{ + userPreferences.data()?.fontSize + }} +
+
+
+ } + `, + standalone: true, + imports: [], +}) +export class UserPreferencesComponent { + #http = inject(HttpClient) + + userPreferences = injectQuery(() => ({ + queryKey: ['preferences'], + queryFn: () => + firstValueFrom(this.#http.get('/preferences')), + staleTime: 1000 * 60 * 60, // 1 hour + })) +} diff --git a/examples/angular/multiple-persisters/src/app/interceptor/mock-api.interceptor.ts b/examples/angular/multiple-persisters/src/app/interceptor/mock-api.interceptor.ts new file mode 100644 index 0000000000..74b3ee0e50 --- /dev/null +++ b/examples/angular/multiple-persisters/src/app/interceptor/mock-api.interceptor.ts @@ -0,0 +1,43 @@ +/** + * MockApiInterceptor is used to simulate API responses for `/api/tasks` endpoints. + * It handles the following operations: + * - GET: Fetches all tasks from localStorage. + * - POST: Adds a new task to localStorage. + * - DELETE: Clears all tasks from localStorage. + * Simulated responses include a delay to mimic network latency. + */ +import { HttpResponse } from '@angular/common/http' +import { delay, of } from 'rxjs' +import type { + HttpEvent, + HttpHandlerFn, + HttpInterceptorFn, + HttpRequest, +} from '@angular/common/http' +import type { Observable } from 'rxjs' + +export const mockInterceptor: HttpInterceptorFn = ( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> => { + const respondWith = (status: number, body: any) => + of(new HttpResponse({ status, body })).pipe(delay(100)) + if (req.url === '/preferences') { + return respondWith(200, { + theme: 'dark', + language: 'en', + notifications: true, + fontSize: 'medium', + }) + } + + if (req.url === '/session') { + return respondWith(200, { + lastActive: '2024-02-28T12:00:00Z', + currentView: 'dashboard', + activeFilters: ['recent', 'important'], + temporaryNotes: 'Meeting at 3PM', + }) + } + return next(req) +} diff --git a/examples/angular/multiple-persisters/src/favicon.ico b/examples/angular/multiple-persisters/src/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/multiple-persisters/src/favicon.ico differ diff --git a/examples/angular/multiple-persisters/src/index.html b/examples/angular/multiple-persisters/src/index.html new file mode 100644 index 0000000000..94e60f2df0 --- /dev/null +++ b/examples/angular/multiple-persisters/src/index.html @@ -0,0 +1,13 @@ + + + + + TanStack Query Angular multiple persisters example + + + + + + + + diff --git a/examples/angular/multiple-persisters/src/main.ts b/examples/angular/multiple-persisters/src/main.ts new file mode 100644 index 0000000000..c3d8f9af99 --- /dev/null +++ b/examples/angular/multiple-persisters/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/multiple-persisters/src/styles.css b/examples/angular/multiple-persisters/src/styles.css new file mode 100644 index 0000000000..b5c61c9567 --- /dev/null +++ b/examples/angular/multiple-persisters/src/styles.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/examples/angular/multiple-persisters/tailwind.config.js b/examples/angular/multiple-persisters/tailwind.config.js new file mode 100644 index 0000000000..7d52777e05 --- /dev/null +++ b/examples/angular/multiple-persisters/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{html,ts}'], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/examples/angular/multiple-persisters/tsconfig.app.json b/examples/angular/multiple-persisters/tsconfig.app.json new file mode 100644 index 0000000000..5b9d3c5ecb --- /dev/null +++ b/examples/angular/multiple-persisters/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/multiple-persisters/tsconfig.json b/examples/angular/multiple-persisters/tsconfig.json new file mode 100644 index 0000000000..d0d73c8beb --- /dev/null +++ b/examples/angular/multiple-persisters/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictStandalone": true, + "strictTemplates": true + } +} diff --git a/packages/angular-persist-query-client-experimental/.attw.json b/packages/angular-persist-query-client-experimental/.attw.json new file mode 100644 index 0000000000..ce409e67a8 --- /dev/null +++ b/packages/angular-persist-query-client-experimental/.attw.json @@ -0,0 +1,3 @@ +{ + "ignoreRules": ["cjs-resolves-to-esm", "no-resolution"] +} diff --git a/packages/angular-persist-query-client-experimental/config/api-extractor.json b/packages/angular-persist-query-client-experimental/config/api-extractor.json new file mode 100644 index 0000000000..dd9a7a854f --- /dev/null +++ b/packages/angular-persist-query-client-experimental/config/api-extractor.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/build/index.d.ts", + + "newlineKind": "lf", + + "apiReport": { + "enabled": true + }, + + "docModel": { + "enabled": false + }, + + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/build/rollup.d.ts" + }, + + "tsdocMetadata": { + "enabled": false + } +} diff --git a/packages/angular-persist-query-client-experimental/eslint.config.js b/packages/angular-persist-query-client-experimental/eslint.config.js new file mode 100644 index 0000000000..b0ec381452 --- /dev/null +++ b/packages/angular-persist-query-client-experimental/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check + +import pluginJsdoc from 'eslint-plugin-jsdoc' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + pluginJsdoc.configs['flat/recommended-typescript'], + { + rules: { + 'cspell/spellchecker': [ + 'warn', + { + cspell: { + ignoreRegExpList: ['\\ɵ.+'], + }, + }, + ], + 'jsdoc/require-hyphen-before-param-description': 1, + 'jsdoc/sort-tags': 1, + 'jsdoc/require-throws': 1, + 'jsdoc/check-tag-names': [ + 'warn', + { + // Not compatible with Api Extractor @public + typed: false, + }, + ], + }, + }, +] diff --git a/packages/angular-persist-query-client-experimental/package.json b/packages/angular-persist-query-client-experimental/package.json new file mode 100644 index 0000000000..9c1a1c2e87 --- /dev/null +++ b/packages/angular-persist-query-client-experimental/package.json @@ -0,0 +1,73 @@ +{ + "name": "@tanstack/angular-query-persist-client-experimental", + "version": "5.62.7", + "description": "Angular bindings to work with persisters in TanStack/angular-query", + "author": "Omer Gronich", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/query.git", + "directory": "packages/angular-query-persist-client-experimental" + }, + "homepage": "https://tanstack.com/query", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "scripts": { + "clean": "rimraf ./build ./coverage", + "test:eslint": "eslint ./src", + "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", + "test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js", + "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js", + "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js", + "test:types:ts53": "tsc", + "test:lib": "vitest", + "test:lib:dev": "pnpm run test:lib --watch", + "test:build": "publint --strict && attw --pack", + "build": "pnpm build:tsup", + "build:tsup": "tsup" + }, + "type": "module", + "types": "build/index.d.ts", + "module": "build/index.js", + "exports": { + ".": { + "import": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + } + }, + "./package.json": { + "default": "./package.json" + } + }, + "sideEffects": false, + "files": [ + "build", + "src", + "!src/__tests__" + ], + "dependencies": { + "@tanstack/query-persist-client-core": "workspace:*" + }, + "devDependencies": { + "@analogjs/vite-plugin-angular": "^1.6.4", + "@angular/compiler": "^19.1.0-next.0", + "@angular/core": "^19.1.0-next.0", + "@angular/platform-browser": "^19.1.0-next.0", + "@angular/platform-browser-dynamic": "^19.1.0-next.0", + "@microsoft/api-extractor": "^7.48.0", + "@tanstack/angular-query-experimental": "workspace:*", + "@testing-library/angular": "^17.3.2", + "@testing-library/dom": "^10.4.0", + "eslint-plugin-jsdoc": "^50.5.0", + "tsup": "8.0.2", + "typescript": "5.7.2" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@tanstack/angular-query-experimental": "workspace:*" + } +} diff --git a/packages/angular-persist-query-client-experimental/src/__tests__/utils.ts b/packages/angular-persist-query-client-experimental/src/__tests__/utils.ts new file mode 100644 index 0000000000..73fb0ed26f --- /dev/null +++ b/packages/angular-persist-query-client-experimental/src/__tests__/utils.ts @@ -0,0 +1,12 @@ +let queryKeyCount = 0 + +export function queryKey(): Array { + queryKeyCount++ + return [`query_${queryKeyCount}`] +} + +export function sleep(timeout: number): Promise { + return new Promise((resolve, _reject) => { + setTimeout(resolve, timeout) + }) +} diff --git a/packages/angular-persist-query-client-experimental/src/__tests__/with-persist-query-client.test.ts b/packages/angular-persist-query-client-experimental/src/__tests__/with-persist-query-client.test.ts new file mode 100644 index 0000000000..8ffd261988 --- /dev/null +++ b/packages/angular-persist-query-client-experimental/src/__tests__/with-persist-query-client.test.ts @@ -0,0 +1,501 @@ +import { describe, expect, test, vi } from 'vitest' +import { + QueryClient, + injectQuery, + provideTanStackQuery, +} from '@tanstack/angular-query-experimental' +import { persistQueryClientSave } from '@tanstack/query-persist-client-core' +import { Component, effect } from '@angular/core' +import { render, screen, waitFor } from '@testing-library/angular' +import { withPersistQueryClient } from '../with-persist-query-client' +import { queryKey, sleep } from './utils' +import type { + PersistedClient, + Persister, +} from '@tanstack/query-persist-client-core' + +const createMockPersister = (): Persister => { + let storedState: PersistedClient | undefined + + return { + persistClient(persistClient: PersistedClient) { + storedState = persistClient + }, + async restoreClient() { + await sleep(10) + return storedState + }, + removeClient() { + storedState = undefined + }, + } +} + +const createMockErrorPersister = ( + removeClient: Persister['removeClient'], +): [Error, Persister] => { + const error = new Error('restore failed') + return [ + error, + { + async persistClient() { + // noop + }, + async restoreClient() { + await sleep(10) + throw error + }, + removeClient, + }, + ] +} + +describe('withPersistQueryClient', () => { + test('restores cache from persister', async () => { + const key = queryKey() + const states: Array<{ + status: string + fetchStatus: string + data: string | undefined + }> = [] + + const queryClient = new QueryClient() + await queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => Promise.resolve('hydrated'), + }) + + const persister = createMockPersister() + + await persistQueryClientSave({ queryClient, persister }) + + queryClient.clear() + + @Component({ + template: ` +
+

{{ state.data() }}

+

fetchStatus: {{ state.fetchStatus() }}

+
+ `, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + queryFn: async () => { + await sleep(10) + return 'fetched' + }, + })) + _ = effect(() => { + states.push({ + status: this.state.status(), + fetchStatus: this.state.fetchStatus(), + data: this.state.data(), + }) + }) + } + + render(Page, { + providers: [ + provideTanStackQuery( + queryClient, + withPersistQueryClient([{ persistOptions: { persister } }]), + ), + ], + }) + + await waitFor(() => screen.getByText('fetchStatus: idle')) + await waitFor(() => screen.getByText('hydrated')) + await waitFor(() => screen.getByText('fetched')) + + expect(states).toHaveLength(3) + + expect(states[0]).toMatchObject({ + status: 'pending', + fetchStatus: 'idle', + data: undefined, + }) + + expect(states[1]).toMatchObject({ + status: 'success', + fetchStatus: 'fetching', + data: 'hydrated', + }) + + expect(states[2]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'fetched', + }) + }) + + test.todo( + '(Write this test after injectQueries is working) should also put injectQueries into idle state', + ) + + test('should show initialData while restoring', async () => { + const key = queryKey() + const states: Array<{ + status: string + fetchStatus: string + data: string | undefined + }> = [] + + const queryClient = new QueryClient() + await queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => Promise.resolve('hydrated'), + }) + + const persister = createMockPersister() + + await persistQueryClientSave({ queryClient, persister }) + + queryClient.clear() + + @Component({ + template: ` +
+

{{ state.data() }}

+

fetchStatus: {{ state.fetchStatus() }}

+
+ `, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + queryFn: async () => { + await sleep(10) + return 'fetched' + }, + initialData: 'initial', + // make sure that initial data is older than the hydration data + // otherwise initialData would be newer and takes precedence + initialDataUpdatedAt: 1, + })) + _ = effect(() => { + states.push({ + status: this.state.status(), + fetchStatus: this.state.fetchStatus(), + data: this.state.data(), + }) + }) + } + + render(Page, { + providers: [ + provideTanStackQuery( + queryClient, + withPersistQueryClient([{ persistOptions: { persister } }]), + ), + ], + }) + + await waitFor(() => screen.getByText('fetched')) + + expect(states).toHaveLength(3) + + expect(states[0]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'initial', + }) + + expect(states[1]).toMatchObject({ + status: 'success', + fetchStatus: 'fetching', + data: 'hydrated', + }) + + expect(states[2]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'fetched', + }) + }) + + test('should not refetch after restoring when data is fresh', async () => { + const key = queryKey() + const states: Array<{ + status: string + fetchStatus: string + data: string | undefined + }> = [] + + const queryClient = new QueryClient() + await queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => Promise.resolve('hydrated'), + }) + + const persister = createMockPersister() + + await persistQueryClientSave({ queryClient, persister }) + + queryClient.clear() + + let fetched = false + + @Component({ + template: ` +
+

data: {{ state.data() ?? 'null' }}

+

fetchStatus: {{ state.fetchStatus() }}

+
+ `, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + queryFn: async () => { + fetched = true + await sleep(10) + return 'fetched' + }, + staleTime: Infinity, + })) + _ = effect(() => { + states.push({ + status: this.state.status(), + fetchStatus: this.state.fetchStatus(), + data: this.state.data(), + }) + }) + } + + render(Page, { + providers: [ + provideTanStackQuery( + queryClient, + withPersistQueryClient([{ persistOptions: { persister } }]), + ), + ], + }) + + await waitFor(() => screen.getByText('data: null')) + await waitFor(() => screen.getByText('data: hydrated')) + + expect(states).toHaveLength(2) + + expect(fetched).toBe(false) + + expect(states[0]).toMatchObject({ + status: 'pending', + fetchStatus: 'idle', + data: undefined, + }) + + expect(states[1]).toMatchObject({ + status: 'success', + fetchStatus: 'idle', + data: 'hydrated', + }) + }) + + test('should call onSuccess after successful restoring', async () => { + const key = queryKey() + const queryClient = new QueryClient() + await queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => Promise.resolve('hydrated'), + }) + + const persister = createMockPersister() + await persistQueryClientSave({ queryClient, persister }) + + queryClient.clear() + + @Component({ + template: ` +
+

{{ state.data() }}

+

fetchStatus: {{ state.fetchStatus() }}

+
+ `, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + queryFn: async () => { + await sleep(10) + return 'fetched' + }, + })) + } + + const onSuccess = vi.fn() + + render(Page, { + providers: [ + provideTanStackQuery( + queryClient, + withPersistQueryClient([ + { + persistOptions: { persister }, + onSuccess, + }, + ]), + ), + ], + }) + + expect(onSuccess).toHaveBeenCalledTimes(0) + await waitFor(() => screen.getByText('fetched')) + expect(onSuccess).toHaveBeenCalledTimes(1) + }) + + test('should remove cache after non-successful restoring', async () => { + const key = queryKey() + const onErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined) + const queryClient = new QueryClient() + const removeClient = vi.fn() + const [error, persister] = createMockErrorPersister(removeClient) + + @Component({ + template: ` +
+

{{ state.data() }}

+

fetchStatus: {{ state.fetchStatus() }}

+
+ `, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + queryFn: async () => { + await sleep(10) + return 'fetched' + }, + })) + } + + render(Page, { + providers: [ + provideTanStackQuery( + queryClient, + withPersistQueryClient([ + { + persistOptions: { persister }, + }, + ]), + ), + ], + }) + + await waitFor(() => screen.getByText('fetched')) + expect(removeClient).toHaveBeenCalledTimes(1) + expect(onErrorMock).toHaveBeenCalledTimes(1) + expect(onErrorMock).toHaveBeenNthCalledWith(1, error) + onErrorMock.mockRestore() + }) + + test('should be able to support multiple persisters', async () => { + const key1 = queryKey() + const key2 = queryKey() + const states1: Array<{ + status: string + fetchStatus: string + data: string | undefined + }> = [] + const states2: Array<{ + status: string + fetchStatus: string + data: string | undefined + }> = [] + + const queryClient = new QueryClient() + await queryClient.prefetchQuery({ + queryKey: key1, + queryFn: () => Promise.resolve('hydrated 1'), + }) + + const persister1 = createMockPersister() + await persistQueryClientSave({ queryClient, persister: persister1 }) + queryClient.clear() + + const persister2 = createMockPersister() + await queryClient.prefetchQuery({ + queryKey: key2, + queryFn: () => Promise.resolve('hydrated 2'), + }) + await persistQueryClientSave({ queryClient, persister: persister2 }) + queryClient.clear() + + @Component({ + template: ` +
+

{{ query1.data() }}

+

fetchStatus: {{ query1.fetchStatus() }}

+
+
+

{{ query2.data() }}

+

fetchStatus: {{ query2.fetchStatus() }}

+
+ `, + }) + class Page { + query1 = injectQuery(() => ({ + queryKey: key1, + queryFn: async () => { + await sleep(10) + return 'fetched 1' + }, + })) + query2 = injectQuery(() => ({ + queryKey: key2, + queryFn: async () => { + await sleep(10) + return 'fetched 2' + }, + })) + + _ = effect(() => { + states1.push({ + status: this.query1.status(), + fetchStatus: this.query1.fetchStatus(), + data: this.query1.data(), + }) + states2.push({ + status: this.query2.status(), + fetchStatus: this.query2.fetchStatus(), + data: this.query2.data(), + }) + }) + } + + const onSuccess1 = vi.fn() + const onSuccess2 = vi.fn() + + render(Page, { + providers: [ + provideTanStackQuery( + queryClient, + withPersistQueryClient([ + { + persistOptions: { + persister: persister1, + }, + onSuccess: onSuccess1, + }, + { + persistOptions: { + persister: persister2, + }, + onSuccess: onSuccess2, + }, + ]), + ), + ], + }) + + expect(onSuccess1).toHaveBeenCalledTimes(0) + expect(onSuccess2).toHaveBeenCalledTimes(0) + await waitFor(() => screen.getByText('fetched 1')) + await waitFor(() => screen.getByText('fetched 2')) + expect(onSuccess1).toHaveBeenCalledTimes(1) + expect(onSuccess2).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/angular-persist-query-client-experimental/src/index.ts b/packages/angular-persist-query-client-experimental/src/index.ts new file mode 100644 index 0000000000..2f7546d196 --- /dev/null +++ b/packages/angular-persist-query-client-experimental/src/index.ts @@ -0,0 +1,4 @@ +// Re-export core +export * from '@tanstack/query-persist-client-core' + +export * from './with-persist-query-client' diff --git a/packages/angular-persist-query-client-experimental/src/test-setup.ts b/packages/angular-persist-query-client-experimental/src/test-setup.ts new file mode 100644 index 0000000000..cb5fd340f3 --- /dev/null +++ b/packages/angular-persist-query-client-experimental/src/test-setup.ts @@ -0,0 +1,12 @@ +import '@analogjs/vite-plugin-angular/setup-vitest' + +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing' +import { getTestBed } from '@angular/core/testing' + +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), +) diff --git a/packages/angular-persist-query-client-experimental/src/with-persist-query-client.ts b/packages/angular-persist-query-client-experimental/src/with-persist-query-client.ts new file mode 100644 index 0000000000..049c05abf5 --- /dev/null +++ b/packages/angular-persist-query-client-experimental/src/with-persist-query-client.ts @@ -0,0 +1,93 @@ +import { + injectQueryClient, + provideIsRestoring, + queryFeature, +} from '@tanstack/angular-query-experimental' +import { + DestroyRef, + ENVIRONMENT_INITIALIZER, + PLATFORM_ID, + inject, + signal, +} from '@angular/core' +import { isPlatformBrowser } from '@angular/common' +import { + persistQueryClientRestore, + persistQueryClientSubscribe, +} from '@tanstack/query-persist-client-core' +import type { PersistQueryClientOptions as PersistQueryClientOptionsCore } from '@tanstack/query-persist-client-core' +import type { PersistQueryClientFeature } from '@tanstack/angular-query-experimental' + +type PersistQueryClientOptions = { + persistOptions: Omit + onSuccess?: () => Promise | unknown +} + +/** + * Enables persistence. + * + * **Example** + * + * ```ts + * const localStoragePersister = createSyncStoragePersister({ + * storage: window.localStorage, + * }) + * + * export const appConfig: ApplicationConfig = { + * providers: [ + * provideTanStackQuery( + * new QueryClient(), + * withPersistQueryClient([ + * { + * persistOptions: { + * persister: localStoragePersister, + * }, + * onSuccess: () => console.log('Restoration completed successfully.'), + * }, + * ]) + * ) + * ] + * } + * ``` + * @param persistQueryClientOptions - An array of objects containing persistOptions and an onSuccess callback which gets called when the restoration process is complete. + * @returns A set of providers for use with `provideTanStackQuery`. + * @public + */ +export function withPersistQueryClient( + persistQueryClientOptions: Array, +): PersistQueryClientFeature { + const isRestoring = signal(false) + const providers = [ + provideIsRestoring(isRestoring.asReadonly()), + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: () => { + if (!isPlatformBrowser(inject(PLATFORM_ID))) return + const destroyRef = inject(DestroyRef) + const queryClient = injectQueryClient() + + isRestoring.set(true) + const restorations = persistQueryClientOptions.map( + ({ onSuccess, persistOptions }) => { + const options = { queryClient, ...persistOptions } + return persistQueryClientRestore(options).then(async () => { + try { + if (onSuccess) { + await onSuccess() + } + } finally { + const cleanup = persistQueryClientSubscribe(options) + destroyRef.onDestroy(cleanup) + } + }) + }, + ) + Promise.all(restorations).finally(() => { + isRestoring.set(false) + }) + }, + }, + ] + return queryFeature('PersistQueryClient', providers) +} diff --git a/packages/angular-persist-query-client-experimental/tsconfig.json b/packages/angular-persist-query-client-experimental/tsconfig.json new file mode 100644 index 0000000000..c7fce5ae9d --- /dev/null +++ b/packages/angular-persist-query-client-experimental/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "moduleResolution": "Bundler", + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noFallthroughCasesInSwitch": true, + "useDefineForClassFields": false, + "target": "ES2022", + "types": ["vitest/globals"] + }, + "include": ["src", "eslint.config.js", "tsup.config.js", "vite.config.ts"] +} diff --git a/packages/angular-persist-query-client-experimental/tsup.config.js b/packages/angular-persist-query-client-experimental/tsup.config.js new file mode 100644 index 0000000000..eadb7ad63d --- /dev/null +++ b/packages/angular-persist-query-client-experimental/tsup.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + sourcemap: true, + clean: true, + format: ['esm'], + dts: true, + outDir: 'build', +}) diff --git a/packages/angular-persist-query-client-experimental/vite.config.ts b/packages/angular-persist-query-client-experimental/vite.config.ts new file mode 100644 index 0000000000..3793a2137c --- /dev/null +++ b/packages/angular-persist-query-client-experimental/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' +import packageJson from './package.json' + +export default defineConfig({ + test: { + name: packageJson.name, + dir: './src', + watch: false, + environment: 'jsdom', + setupFiles: ['src/test-setup.ts'], + coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, + typecheck: { enabled: true }, + globals: true, + restoreMocks: true, + }, +}) diff --git a/packages/angular-query-experimental/src/create-base-query.ts b/packages/angular-query-experimental/src/create-base-query.ts index d53a387087..a78a8eb0c1 100644 --- a/packages/angular-query-experimental/src/create-base-query.ts +++ b/packages/angular-query-experimental/src/create-base-query.ts @@ -1,5 +1,4 @@ import { - DestroyRef, Injector, NgZone, VERSION, @@ -13,6 +12,7 @@ import { import { QueryClient, notifyManager } from '@tanstack/query-core' import { signalProxy } from './signal-proxy' import { shouldThrowError } from './util' +import { injectIsRestoring } from './inject-is-restoring' import type { QueryKey, QueryObserver, @@ -41,8 +41,8 @@ export function createBaseQuery< ) { const injector = inject(Injector) const ngZone = injector.get(NgZone) - const destroyRef = injector.get(DestroyRef) const queryClient = injector.get(QueryClient) + const isRestoring = injectIsRestoring(injector) /** * Signal that has the default options from query client applied @@ -53,7 +53,9 @@ export function createBaseQuery< const defaultedOptionsSignal = computed(() => { const options = runInInjectionContext(injector, () => optionsFn()) const defaultedOptions = queryClient.defaultQueryOptions(options) - defaultedOptions._optimisticResults = 'optimistic' + defaultedOptions._optimisticResults = isRestoring() + ? 'isRestoring' + : 'optimistic' return defaultedOptions }) @@ -104,33 +106,33 @@ export function createBaseQuery< }, ) - effect(() => { + effect((onCleanup) => { // observer.trackResult is not used as this optimization is not needed for Angular const observer = observerSignal() - - untracked(() => { - const unsubscribe = ngZone.runOutsideAngular(() => - observer.subscribe( - notifyManager.batchCalls((state) => { - ngZone.run(() => { - if ( - state.isError && - !state.isFetching && - // !isRestoring() && // todo: enable when client persistence is implemented - shouldThrowError(observer.options.throwOnError, [ - state.error, - observer.getCurrentQuery(), - ]) - ) { - throw state.error - } - resultFromSubscriberSignal.set(state) - }) - }), - ), - ) - destroyRef.onDestroy(unsubscribe) - }) + const unsubscribe = isRestoring() + ? () => undefined + : untracked(() => + ngZone.runOutsideAngular(() => + observer.subscribe( + notifyManager.batchCalls((state) => { + ngZone.run(() => { + if ( + state.isError && + !state.isFetching && + shouldThrowError(observer.options.throwOnError, [ + state.error, + observer.getCurrentQuery(), + ]) + ) { + throw state.error + } + resultFromSubscriberSignal.set(state) + }) + }), + ), + ), + ) + onCleanup(unsubscribe) }) return signalProxy( diff --git a/packages/angular-query-experimental/src/index.ts b/packages/angular-query-experimental/src/index.ts index aa6292d4b5..da68067d33 100644 --- a/packages/angular-query-experimental/src/index.ts +++ b/packages/angular-query-experimental/src/index.ts @@ -22,6 +22,7 @@ export { infiniteQueryOptions } from './infinite-query-options' export * from './inject-infinite-query' export * from './inject-is-fetching' export * from './inject-is-mutating' +export * from './inject-is-restoring' export * from './inject-mutation' export * from './inject-mutation-state' export * from './inject-queries' diff --git a/packages/angular-query-experimental/src/inject-is-restoring.ts b/packages/angular-query-experimental/src/inject-is-restoring.ts new file mode 100644 index 0000000000..56753a5485 --- /dev/null +++ b/packages/angular-query-experimental/src/inject-is-restoring.ts @@ -0,0 +1,32 @@ +import { InjectionToken, computed, inject } from '@angular/core' +import { assertInjector } from './util/assert-injector/assert-injector' +import type { Injector, Provider, Signal } from '@angular/core' + +const IsRestoring = new InjectionToken>('IsRestoring') + +/** + * Injects a signal that tracks whether a restore is currently in progress. {@link injectQuery} and friends also check this internally to avoid race conditions between the restore and mounting queries. + * @param injector - The Angular injector to use. + * @returns signal with boolean that indicates whether a restore is in progress. + * @public + */ +export function injectIsRestoring(injector?: Injector): Signal { + return assertInjector( + injectIsRestoring, + injector, + () => inject(IsRestoring, { optional: true }) ?? computed(() => false), + ) +} + +/** + * Used by TanStack Query Angular persist client plugin to provide the signal that tracks the restore state + * @param isRestoring - a readonly signal that returns a boolean + * @returns Provider for the `isRestoring` signal + * @public + */ +export function provideIsRestoring(isRestoring: Signal): Provider { + return { + provide: IsRestoring, + useValue: isRestoring, + } +} diff --git a/packages/angular-query-experimental/src/inject-queries.ts b/packages/angular-query-experimental/src/inject-queries.ts index 8f969993d6..345d511924 100644 --- a/packages/angular-query-experimental/src/inject-queries.ts +++ b/packages/angular-query-experimental/src/inject-queries.ts @@ -12,6 +12,7 @@ import { signal, } from '@angular/core' import { assertInjector } from './util/assert-injector/assert-injector' +import { injectIsRestoring } from './inject-is-restoring' import type { Injector, Signal } from '@angular/core' import type { DefaultError, @@ -212,12 +213,15 @@ export function injectQueries< const destroyRef = inject(DestroyRef) const ngZone = inject(NgZone) const queryClient = inject(QueryClient) + const isRestoring = injectIsRestoring(injector) const defaultedQueries = computed(() => { return queries().map((opts) => { const defaultedOptions = queryClient.defaultQueryOptions(opts) // Make sure the results are already in fetching state before subscribing or updating options - defaultedOptions._optimisticResults = 'optimistic' + defaultedOptions._optimisticResults = isRestoring() + ? 'isRestoring' + : 'optimistic' return defaultedOptions as QueryObserverOptions }) @@ -246,10 +250,14 @@ export function injectQueries< const result = signal(getCombinedResult() as any) - const unsubscribe = ngZone.runOutsideAngular(() => - observer.subscribe(notifyManager.batchCalls(result.set)), - ) - destroyRef.onDestroy(unsubscribe) + effect(() => { + const unsubscribe = isRestoring() + ? () => undefined + : ngZone.runOutsideAngular(() => + observer.subscribe(notifyManager.batchCalls(result.set)), + ) + destroyRef.onDestroy(unsubscribe) + }) return result }) diff --git a/packages/angular-query-experimental/src/providers.ts b/packages/angular-query-experimental/src/providers.ts index 42645b1b85..87adfa581e 100644 --- a/packages/angular-query-experimental/src/providers.ts +++ b/packages/angular-query-experimental/src/providers.ts @@ -142,7 +142,7 @@ export interface QueryFeature { * @param providers - * @returns A Query feature. */ -function queryFeature( +export function queryFeature( kind: TFeatureKind, providers: Array, ): QueryFeature { @@ -157,6 +157,13 @@ function queryFeature( */ export type DeveloperToolsFeature = QueryFeature<'DeveloperTools'> +/** + * A type alias that represents a feature which enables persistence. + * The type is used to describe the return value of the `withPersistQueryClient` function. + * @public + */ +export type PersistQueryClientFeature = QueryFeature<'PersistQueryClient'> + /** * Options for configuring the TanStack Query devtools. * @public @@ -346,8 +353,8 @@ export function withDevtools( * @public * @see {@link provideTanStackQuery} */ -export type QueryFeatures = DeveloperToolsFeature // Union type of features but just one now +export type QueryFeatures = DeveloperToolsFeature | PersistQueryClientFeature -export const queryFeatures = ['DeveloperTools'] as const +export const queryFeatures = ['DeveloperTools', 'PersistQueryClient'] as const export type QueryFeatureKind = (typeof queryFeatures)[number] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6f76d18bb..9f38072b75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,6 +208,55 @@ importers: specifier: 5.7.2 version: 5.7.2 + examples/angular/basic-persister: + dependencies: + '@angular/common': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) + '@angular/compiler': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + '@angular/core': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) + '@angular/platform-browser': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + '@angular/platform-browser-dynamic': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))) + '@tanstack/angular-query-experimental': + specifier: ^5.62.7 + version: link:../../../packages/angular-query-experimental + '@tanstack/angular-query-persist-client-experimental': + specifier: ^5.62.7 + version: link:../../../packages/angular-persist-query-client-experimental + '@tanstack/query-sync-storage-persister': + specifier: ^5.62.3 + version: link:../../../packages/query-sync-storage-persister + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + tslib: + specifier: ^2.6.3 + version: 2.8.1 + zone.js: + specifier: ^0.15.0 + version: 0.15.0 + devDependencies: + '@angular/build': + specifier: ^19.0.2 + version: 19.0.2(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.9.3)(chokidar@4.0.1)(less@4.2.1)(lightningcss@1.27.0)(postcss@8.4.49)(tailwindcss@3.4.7)(terser@5.31.6)(typescript@5.7.2) + '@angular/cli': + specifier: ^19.0.2 + version: 19.0.2(@types/node@22.9.3)(chokidar@4.0.1) + '@angular/compiler-cli': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2) + typescript: + specifier: 5.7.2 + version: 5.7.2 + examples/angular/devtools-panel: dependencies: '@angular/common': @@ -300,6 +349,64 @@ importers: specifier: 5.7.2 version: 5.7.2 + examples/angular/multiple-persisters: + dependencies: + '@angular/common': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) + '@angular/compiler': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + '@angular/core': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) + '@angular/platform-browser': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + '@angular/platform-browser-dynamic': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))) + '@tanstack/angular-query-experimental': + specifier: ^5.62.7 + version: link:../../../packages/angular-query-experimental + '@tanstack/angular-query-persist-client-experimental': + specifier: ^5.62.7 + version: link:../../../packages/angular-persist-query-client-experimental + '@tanstack/query-sync-storage-persister': + specifier: ^5.62.3 + version: link:../../../packages/query-sync-storage-persister + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + tslib: + specifier: ^2.6.3 + version: 2.8.1 + zone.js: + specifier: ^0.15.0 + version: 0.15.0 + devDependencies: + '@angular/build': + specifier: ^19.0.2 + version: 19.0.2(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@types/node@22.9.3)(chokidar@4.0.1)(less@4.2.1)(lightningcss@1.27.0)(postcss@8.4.49)(tailwindcss@3.4.7)(terser@5.31.6)(typescript@5.7.2) + '@angular/cli': + specifier: ^19.0.2 + version: 19.0.2(@types/node@22.9.3)(chokidar@4.0.1) + '@angular/compiler-cli': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.49) + postcss: + specifier: ^8.4.49 + version: 8.4.49 + tailwindcss: + specifier: ^3.4.7 + version: 3.4.7 + typescript: + specifier: 5.7.2 + version: 5.7.2 + examples/angular/pagination: dependencies: '@angular/common': @@ -2085,6 +2192,52 @@ importers: specifier: ^2.1.10 version: 2.1.10(typescript@5.6.3) + packages/angular-persist-query-client-experimental: + dependencies: + '@angular/common': + specifier: '>=16.0.0' + version: 17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) + '@tanstack/query-persist-client-core': + specifier: workspace:* + version: link:../query-persist-client-core + devDependencies: + '@analogjs/vite-plugin-angular': + specifier: ^1.6.4 + version: 1.6.4(@angular-devkit/build-angular@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@types/node@22.9.3)(chokidar@3.6.0)(html-webpack-plugin@5.6.3(webpack@5.94.0(esbuild@0.19.12)))(lightningcss@1.27.0)(tailwindcss@3.4.7)(typescript@5.7.2))(@ngtools/webpack@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(typescript@5.7.2)(webpack@5.94.0(esbuild@0.23.0))) + '@angular/compiler': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + '@angular/core': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) + '@angular/platform-browser': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + '@angular/platform-browser-dynamic': + specifier: ^19.1.0-next.0 + version: 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))) + '@microsoft/api-extractor': + specifier: ^7.48.0 + version: 7.48.0(@types/node@22.9.3) + '@tanstack/angular-query-experimental': + specifier: workspace:* + version: link:../angular-query-experimental + '@testing-library/angular': + specifier: ^17.3.2 + version: 17.3.4(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/router@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(rxjs@7.8.1))(@testing-library/dom@10.4.0) + '@testing-library/dom': + specifier: ^10.4.0 + version: 10.4.0 + eslint-plugin-jsdoc: + specifier: ^50.5.0 + version: 50.5.0(eslint@9.15.0(jiti@2.4.0)) + tsup: + specifier: 8.0.2 + version: 8.0.2(@microsoft/api-extractor@7.48.0(@types/node@22.9.3))(postcss@8.4.41)(typescript@5.7.2) + typescript: + specifier: 5.7.2 + version: 5.7.2 + packages/angular-query-devtools-experimental: dependencies: '@angular/common': @@ -2096,7 +2249,7 @@ importers: devDependencies: '@analogjs/vite-plugin-angular': specifier: ^1.6.4 - version: 1.6.4(@angular-devkit/build-angular@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@types/node@22.9.3)(chokidar@3.6.0)(html-webpack-plugin@5.6.3(webpack@5.94.0(esbuild@0.19.12)))(lightningcss@1.27.0)(tailwindcss@3.4.7)(typescript@5.7.2))(@ngtools/webpack@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(typescript@5.7.2)(webpack@5.94.0(esbuild@0.23.0))) + version: 1.6.4(@angular-devkit/build-angular@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(@types/node@22.9.3)(chokidar@3.6.0)(html-webpack-plugin@5.6.3(webpack@5.96.1(esbuild@0.24.0)))(lightningcss@1.27.0)(tailwindcss@3.4.7)(typescript@5.7.2))(@ngtools/webpack@18.2.12(@angular/compiler-cli@19.1.0-next.0(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.2))(typescript@5.7.2)(webpack@5.96.1(esbuild@0.24.0))) '@angular/core': specifier: ^19.1.0-next.0 version: 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) @@ -2111,7 +2264,7 @@ importers: version: 50.5.0(eslint@9.15.0(jiti@2.4.0)) tsup: specifier: 8.0.2 - version: 8.0.2(@microsoft/api-extractor@7.48.0(@types/node@22.9.3))(postcss@8.4.41)(typescript@5.7.2) + version: 8.0.2(@microsoft/api-extractor@7.48.0(@types/node@22.9.3))(postcss@8.4.49)(typescript@5.7.2) typescript: specifier: 5.7.2 version: 5.7.2 @@ -7039,6 +7192,15 @@ packages: react: '>=16' react-dom: '>=16' + '@testing-library/angular@17.3.4': + resolution: {integrity: sha512-QqBcRaVb4VJO66/5oboJXaME1PugM+y/tOpSTpgB7QwjcWgvcW63CQsX8JbSJQPgthk7gwhhgiHJAqyDUITp6Q==} + peerDependencies: + '@angular/common': '>= 17.0.0' + '@angular/core': '>= 17.0.0' + '@angular/platform-browser': '>= 17.0.0' + '@angular/router': '>= 17.0.0' + '@testing-library/dom': ^10.0.0 + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -17201,6 +17363,12 @@ snapshots: rxjs: 7.8.1 tslib: 2.8.1 + '@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)': + dependencies: + '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) + rxjs: 7.8.1 + tslib: 2.8.1 + '@angular/common@19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)': dependencies: '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) @@ -17283,6 +17451,14 @@ snapshots: '@angular/platform-browser': 17.3.12(@angular/animations@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(@angular/common@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8))(rxjs@7.8.1))(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)) tslib: 2.8.1 + '@angular/platform-browser-dynamic@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))': + dependencies: + '@angular/common': 17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) + '@angular/compiler': 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) + '@angular/platform-browser': 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + tslib: 2.8.1 + '@angular/platform-browser-dynamic@19.1.0-next.0(@angular/common@19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))': dependencies: '@angular/common': 19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) @@ -17307,6 +17483,12 @@ snapshots: optionalDependencies: '@angular/animations': 17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)) + '@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))': + dependencies: + '@angular/common': 17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) + '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) + tslib: 2.8.1 + '@angular/platform-browser@19.1.0-next.0(@angular/common@19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))': dependencies: '@angular/common': 19.0.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) @@ -17319,6 +17501,14 @@ snapshots: '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) tslib: 2.8.1 + '@angular/router@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(rxjs@7.8.1)': + dependencies: + '@angular/common': 17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) + '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) + '@angular/platform-browser': 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + rxjs: 7.8.1 + tslib: 2.8.1 + '@angular/router@19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(rxjs@7.8.1)': dependencies: '@angular/common': 19.1.0-next.0(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) @@ -23015,6 +23205,15 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@testing-library/angular@17.3.4(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/router@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(rxjs@7.8.1))(@testing-library/dom@10.4.0)': + dependencies: + '@angular/common': 17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) + '@angular/core': 19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0) + '@angular/platform-browser': 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)) + '@angular/router': 19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.1.0-next.0(@angular/common@17.3.12(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.1.0-next.0(rxjs@7.8.1)(zone.js@0.15.0)))(rxjs@7.8.1) + '@testing-library/dom': 10.4.0 + tslib: 2.8.1 + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.26.2 diff --git a/scripts/publish.js b/scripts/publish.js index 9bfa1e5a35..3b6d770f1f 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -92,6 +92,10 @@ await publish({ name: '@tanstack/angular-query-experimental', packageDir: 'packages/angular-query-experimental', }, + { + name: '@tanstack/angular-query-persist-client-experimental', + packageDir: 'packages/angular-persist-query-client-experimental', + }, ], branchConfigs: { main: {