@tanstack/angular-query-experimental 5.103.2 → 5.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/dist/create-base-query.d.mts +168 -0
  2. package/dist/create-base-query.d.mts.map +1 -0
  3. package/dist/create-base-query.mjs +91 -0
  4. package/dist/create-base-query.mjs.map +1 -0
  5. package/dist/devtools/index.d.mts +3 -0
  6. package/dist/devtools/index.mjs +2 -0
  7. package/dist/devtools/production/index.d.mts +4 -0
  8. package/dist/devtools/production/index.mjs +2 -0
  9. package/dist/devtools/stub.d.mts +5 -0
  10. package/dist/devtools/stub.d.mts.map +1 -0
  11. package/dist/devtools/stub.mjs +9 -0
  12. package/dist/devtools/stub.mjs.map +1 -0
  13. package/dist/devtools/types.d.mts +106 -0
  14. package/dist/devtools/types.d.mts.map +1 -0
  15. package/dist/devtools/types.mjs +1 -0
  16. package/{devtools/with-devtools.d.ts → dist/devtools/with-devtools.d.mts} +4 -1
  17. package/dist/devtools/with-devtools.d.mts.map +1 -0
  18. package/dist/devtools/with-devtools.mjs +110 -0
  19. package/dist/devtools/with-devtools.mjs.map +1 -0
  20. package/dist/devtools-panel/index.d.mts +3 -0
  21. package/dist/devtools-panel/index.mjs +2 -0
  22. package/{devtools-panel/inject-devtools-panel.d.ts → dist/devtools-panel/inject-devtools-panel.d.mts} +4 -1
  23. package/dist/devtools-panel/inject-devtools-panel.d.mts.map +1 -0
  24. package/dist/devtools-panel/inject-devtools-panel.mjs +67 -0
  25. package/dist/devtools-panel/inject-devtools-panel.mjs.map +1 -0
  26. package/dist/devtools-panel/production/index.d.mts +4 -0
  27. package/dist/devtools-panel/production/index.mjs +2 -0
  28. package/dist/devtools-panel/stub.d.mts +5 -0
  29. package/dist/devtools-panel/stub.d.mts.map +1 -0
  30. package/dist/devtools-panel/stub.mjs +7 -0
  31. package/dist/devtools-panel/stub.mjs.map +1 -0
  32. package/dist/devtools-panel/types.d.mts +51 -0
  33. package/dist/devtools-panel/types.d.mts.map +1 -0
  34. package/dist/devtools-panel/types.mjs +1 -0
  35. package/dist/index.d.mts +16 -0
  36. package/{index.mjs → dist/index.mjs} +8 -21
  37. package/dist/index.mjs.map +1 -0
  38. package/{infinite-query-options.d.ts → dist/infinite-query-options.d.mts} +28 -25
  39. package/dist/infinite-query-options.d.mts.map +1 -0
  40. package/dist/infinite-query-options.mjs +8 -0
  41. package/dist/infinite-query-options.mjs.map +1 -0
  42. package/{inject-infinite-query.d.ts → dist/inject-infinite-query.d.mts} +13 -10
  43. package/dist/inject-infinite-query.d.mts.map +1 -0
  44. package/dist/inject-infinite-query.mjs +13 -0
  45. package/dist/inject-infinite-query.mjs.map +1 -0
  46. package/{inject-is-fetching.d.ts → dist/inject-is-fetching.d.mts} +11 -8
  47. package/dist/inject-is-fetching.d.mts.map +1 -0
  48. package/dist/inject-is-fetching.mjs +69 -0
  49. package/dist/inject-is-fetching.mjs.map +1 -0
  50. package/{inject-is-mutating.d.ts → dist/inject-is-mutating.d.mts} +11 -8
  51. package/dist/inject-is-mutating.d.mts.map +1 -0
  52. package/dist/inject-is-mutating.mjs +52 -0
  53. package/dist/inject-is-mutating.mjs.map +1 -0
  54. package/{inject-is-restoring.d.ts → dist/inject-is-restoring.d.mts} +10 -8
  55. package/dist/inject-is-restoring.d.mts.map +1 -0
  56. package/dist/inject-is-restoring.mjs +35 -0
  57. package/dist/inject-is-restoring.mjs.map +1 -0
  58. package/{inject-mutation-state.d.ts → dist/inject-mutation-state.d.mts} +13 -11
  59. package/dist/inject-mutation-state.d.mts.map +1 -0
  60. package/dist/inject-mutation-state.mjs +103 -0
  61. package/dist/inject-mutation-state.mjs.map +1 -0
  62. package/{inject-mutation.d.ts → dist/inject-mutation.d.mts} +12 -9
  63. package/dist/inject-mutation.d.mts.map +1 -0
  64. package/dist/inject-mutation.mjs +226 -0
  65. package/dist/inject-mutation.mjs.map +1 -0
  66. package/dist/inject-queries-experimental/index.d.mts +2 -0
  67. package/dist/inject-queries-experimental/index.mjs +2 -0
  68. package/{inject-queries.d.ts → dist/inject-queries.d.mts} +33 -53
  69. package/dist/inject-queries.d.mts.map +1 -0
  70. package/dist/inject-queries.mjs +203 -0
  71. package/dist/inject-queries.mjs.map +1 -0
  72. package/{inject-query-client.d.ts → dist/inject-query-client.d.mts} +6 -3
  73. package/dist/inject-query-client.d.mts.map +1 -0
  74. package/dist/inject-query-client.mjs +23 -0
  75. package/dist/inject-query-client.mjs.map +1 -0
  76. package/{inject-query.d.ts → dist/inject-query.d.mts} +13 -10
  77. package/dist/inject-query.d.mts.map +1 -0
  78. package/dist/inject-query.mjs +12 -0
  79. package/dist/inject-query.mjs.map +1 -0
  80. package/{mutation-options.d.ts → dist/mutation-options.d.mts} +5 -2
  81. package/dist/mutation-options.d.mts.map +1 -0
  82. package/dist/mutation-options.mjs +8 -0
  83. package/dist/mutation-options.mjs.map +1 -0
  84. package/dist/pending-tasks-compat.d.mts +9 -0
  85. package/dist/pending-tasks-compat.d.mts.map +1 -0
  86. package/dist/pending-tasks-compat.mjs +13 -0
  87. package/dist/pending-tasks-compat.mjs.map +1 -0
  88. package/{providers.d.ts → dist/providers.d.mts} +7 -5
  89. package/dist/providers.d.mts.map +1 -0
  90. package/dist/providers.mjs +124 -0
  91. package/dist/providers.mjs.map +1 -0
  92. package/{query-options.d.ts → dist/query-options.d.mts} +34 -31
  93. package/dist/query-options.d.mts.map +1 -0
  94. package/dist/query-options.mjs +8 -0
  95. package/dist/query-options.mjs.map +1 -0
  96. package/{signal-proxy.d.ts → dist/signal-proxy.d.mts} +5 -4
  97. package/dist/signal-proxy.d.mts.map +1 -0
  98. package/dist/signal-proxy.mjs +35 -0
  99. package/dist/signal-proxy.mjs.map +1 -0
  100. package/{types.d.ts → dist/types.d.mts} +24 -26
  101. package/dist/types.d.mts.map +1 -0
  102. package/dist/types.mjs +6 -0
  103. package/dist/types.mjs.map +1 -0
  104. package/package.json +50 -20
  105. package/src/__tests__/infinite-query-options.test-d.ts +314 -0
  106. package/src/__tests__/infinite-query-options.test.ts +19 -0
  107. package/src/__tests__/inject-devtools-panel.test.ts +196 -0
  108. package/src/__tests__/inject-infinite-query.test-d.ts +66 -0
  109. package/src/__tests__/inject-infinite-query.test.ts +227 -0
  110. package/src/__tests__/inject-is-fetching.test.ts +108 -0
  111. package/src/__tests__/inject-is-mutating.test.ts +113 -0
  112. package/src/__tests__/inject-is-restoring.test.ts +93 -0
  113. package/src/__tests__/inject-mutation-state.test-d.ts +22 -0
  114. package/src/__tests__/inject-mutation-state.test.ts +198 -0
  115. package/src/__tests__/inject-mutation.test-d.ts +158 -0
  116. package/src/__tests__/inject-mutation.test.ts +1161 -0
  117. package/src/__tests__/inject-queries.test-d.ts +196 -0
  118. package/src/__tests__/inject-queries.test.ts +240 -0
  119. package/src/__tests__/inject-query.test-d.ts +357 -0
  120. package/src/__tests__/inject-query.test.ts +700 -0
  121. package/src/__tests__/mutation-options.test-d.ts +234 -0
  122. package/src/__tests__/mutation-options.test.ts +344 -0
  123. package/src/__tests__/pending-tasks.test.ts +644 -0
  124. package/src/__tests__/provide-query-client.test.ts +38 -0
  125. package/src/__tests__/provide-tanstack-query.test.ts +38 -0
  126. package/src/__tests__/query-options.test-d.ts +262 -0
  127. package/src/__tests__/query-options.test.ts +14 -0
  128. package/src/__tests__/signal-proxy.test.ts +27 -0
  129. package/src/__tests__/test-utils.ts +77 -0
  130. package/src/__tests__/with-devtools.test.ts +674 -0
  131. package/src/create-base-query.ts +176 -0
  132. package/src/devtools/index.ts +8 -0
  133. package/src/devtools/production/index.ts +1 -0
  134. package/src/devtools/stub.ts +7 -0
  135. package/src/devtools/types.ts +116 -0
  136. package/src/devtools/with-devtools.ts +182 -0
  137. package/src/devtools-panel/index.ts +8 -0
  138. package/src/devtools-panel/inject-devtools-panel.ts +110 -0
  139. package/src/devtools-panel/production/index.ts +1 -0
  140. package/src/devtools-panel/stub.ts +7 -0
  141. package/src/devtools-panel/types.ts +57 -0
  142. package/src/index.ts +60 -0
  143. package/src/infinite-query-options.ts +354 -0
  144. package/src/inject-infinite-query.ts +296 -0
  145. package/src/inject-is-fetching.ts +97 -0
  146. package/src/inject-is-mutating.ts +80 -0
  147. package/src/inject-is-restoring.ts +53 -0
  148. package/src/inject-mutation-state.ts +167 -0
  149. package/src/inject-mutation.ts +328 -0
  150. package/src/inject-queries-experimental/index.ts +1 -0
  151. package/src/inject-queries.ts +480 -0
  152. package/src/inject-query-client.ts +22 -0
  153. package/src/inject-query.ts +208 -0
  154. package/src/mutation-options.ts +121 -0
  155. package/src/pending-tasks-compat.ts +27 -0
  156. package/src/providers.ts +170 -0
  157. package/src/query-options.ts +292 -0
  158. package/src/signal-proxy.ts +46 -0
  159. package/src/types.ts +429 -0
  160. package/create-base-query.d.ts +0 -164
  161. package/create-base-query.mjs +0 -99
  162. package/create-base-query.mjs.map +0 -1
  163. package/devtools/index.d.ts +0 -2
  164. package/devtools/index.mjs +0 -5
  165. package/devtools/index.mjs.map +0 -1
  166. package/devtools/production/index.d.ts +0 -1
  167. package/devtools/stub.d.ts +0 -2
  168. package/devtools/stub.mjs +0 -8
  169. package/devtools/stub.mjs.map +0 -1
  170. package/devtools/types.d.ts +0 -103
  171. package/devtools/with-devtools.mjs +0 -106
  172. package/devtools/with-devtools.mjs.map +0 -1
  173. package/devtools-panel/index.d.ts +0 -2
  174. package/devtools-panel/index.mjs +0 -5
  175. package/devtools-panel/index.mjs.map +0 -1
  176. package/devtools-panel/inject-devtools-panel.mjs +0 -72
  177. package/devtools-panel/inject-devtools-panel.mjs.map +0 -1
  178. package/devtools-panel/production/index.d.ts +0 -1
  179. package/devtools-panel/stub.d.ts +0 -2
  180. package/devtools-panel/stub.mjs +0 -8
  181. package/devtools-panel/stub.mjs.map +0 -1
  182. package/devtools-panel/types.d.ts +0 -48
  183. package/index.d.ts +0 -25
  184. package/index.mjs.map +0 -1
  185. package/infinite-query-options.mjs +0 -7
  186. package/infinite-query-options.mjs.map +0 -1
  187. package/inject-infinite-query.mjs +0 -18
  188. package/inject-infinite-query.mjs.map +0 -1
  189. package/inject-is-fetching.mjs +0 -31
  190. package/inject-is-fetching.mjs.map +0 -1
  191. package/inject-is-mutating.mjs +0 -31
  192. package/inject-is-mutating.mjs.map +0 -1
  193. package/inject-is-restoring.mjs +0 -21
  194. package/inject-is-restoring.mjs.map +0 -1
  195. package/inject-mutation-state.mjs +0 -51
  196. package/inject-mutation-state.mjs.map +0 -1
  197. package/inject-mutation.mjs +0 -94
  198. package/inject-mutation.mjs.map +0 -1
  199. package/inject-queries-experimental/index.d.ts +0 -1
  200. package/inject-queries-experimental/index.mjs +0 -5
  201. package/inject-queries-experimental/index.mjs.map +0 -1
  202. package/inject-queries.mjs +0 -82
  203. package/inject-queries.mjs.map +0 -1
  204. package/inject-query-client.mjs +0 -9
  205. package/inject-query-client.mjs.map +0 -1
  206. package/inject-query.mjs +0 -14
  207. package/inject-query.mjs.map +0 -1
  208. package/mutation-options.mjs +0 -7
  209. package/mutation-options.mjs.map +0 -1
  210. package/pending-tasks-compat.d.ts +0 -7
  211. package/pending-tasks-compat.mjs +0 -19
  212. package/pending-tasks-compat.mjs.map +0 -1
  213. package/providers.mjs +0 -32
  214. package/providers.mjs.map +0 -1
  215. package/query-options.mjs +0 -7
  216. package/query-options.mjs.map +0 -1
  217. package/signal-proxy.mjs +0 -29
  218. package/signal-proxy.mjs.map +0 -1
@@ -0,0 +1,644 @@
1
+ import {
2
+ ApplicationRef,
3
+ Component,
4
+ provideZonelessChangeDetection,
5
+ } from '@angular/core'
6
+ import { TestBed } from '@angular/core/testing'
7
+ import { HttpClient, provideHttpClient } from '@angular/common/http'
8
+ import {
9
+ HttpTestingController,
10
+ provideHttpClientTesting,
11
+ } from '@angular/common/http/testing'
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
13
+ import { queryKey, sleep } from '@tanstack/query-test-utils'
14
+ import { lastValueFrom } from 'rxjs'
15
+ import {
16
+ QueryClient,
17
+ injectMutation,
18
+ injectQuery,
19
+ onlineManager,
20
+ provideTanStackQuery,
21
+ } from '..'
22
+
23
+ describe('PendingTasks Integration', () => {
24
+ let queryClient: QueryClient
25
+
26
+ beforeEach(() => {
27
+ vi.useFakeTimers()
28
+
29
+ queryClient = new QueryClient({
30
+ defaultOptions: {
31
+ queries: {
32
+ retry: false,
33
+ },
34
+ mutations: {
35
+ retry: false,
36
+ },
37
+ },
38
+ })
39
+
40
+ TestBed.configureTestingModule({
41
+ providers: [
42
+ provideZonelessChangeDetection(),
43
+ provideTanStackQuery(queryClient),
44
+ ],
45
+ })
46
+ })
47
+
48
+ afterEach(() => {
49
+ onlineManager.setOnline(true)
50
+ queryClient.clear()
51
+ vi.useRealTimers()
52
+ })
53
+
54
+ describe('Synchronous Resolution', () => {
55
+ it('should handle synchronous queryFn with whenStable()', async () => {
56
+ const app = TestBed.inject(ApplicationRef)
57
+
58
+ const key = queryKey()
59
+ const query = TestBed.runInInjectionContext(() =>
60
+ injectQuery(() => ({
61
+ queryKey: key,
62
+ queryFn: () => 'instant-data', // Resolves synchronously
63
+ })),
64
+ )
65
+
66
+ // Should start as pending even with synchronous data
67
+ expect(query.status()).toBe('pending')
68
+ expect(query.data()).toBeUndefined()
69
+
70
+ const stablePromise = app.whenStable()
71
+ // Flush microtasks to allow TanStack Query's scheduled notifications to process
72
+ await Promise.resolve()
73
+ await vi.advanceTimersByTimeAsync(10)
74
+ await stablePromise
75
+
76
+ // Should work correctly even though queryFn was synchronous
77
+ expect(query.status()).toBe('success')
78
+ expect(query.data()).toBe('instant-data')
79
+ })
80
+
81
+ it('should handle synchronous error with whenStable()', async () => {
82
+ const app = TestBed.inject(ApplicationRef)
83
+
84
+ const key = queryKey()
85
+ const query = TestBed.runInInjectionContext(() =>
86
+ injectQuery(() => ({
87
+ queryKey: key,
88
+ queryFn: () => {
89
+ throw new Error('instant-error')
90
+ }, // Throws synchronously
91
+ })),
92
+ )
93
+
94
+ const stablePromise = app.whenStable()
95
+ // Flush microtasks to allow TanStack Query's scheduled notifications to process
96
+ await Promise.resolve()
97
+ await vi.advanceTimersByTimeAsync(10)
98
+ await stablePromise
99
+
100
+ expect(query.status()).toBe('error')
101
+ expect(query.error()).toEqual(new Error('instant-error'))
102
+ })
103
+
104
+ it('should handle synchronous mutationFn with whenStable()', async () => {
105
+ const app = TestBed.inject(ApplicationRef)
106
+ let mutationFnCalled = false
107
+
108
+ const mutation = TestBed.runInInjectionContext(() =>
109
+ injectMutation(() => ({
110
+ mutationFn: async (data: string) => {
111
+ mutationFnCalled = true
112
+ await Promise.resolve()
113
+ return `processed: ${data}`
114
+ },
115
+ })),
116
+ )
117
+
118
+ mutation.mutate('test')
119
+
120
+ TestBed.tick()
121
+
122
+ const stablePromise = app.whenStable()
123
+ await Promise.resolve()
124
+ await vi.advanceTimersByTimeAsync(10)
125
+ await stablePromise
126
+
127
+ expect(mutationFnCalled).toBe(true)
128
+ expect(mutation.isSuccess()).toBe(true)
129
+ expect(mutation.data()).toBe('processed: test')
130
+ })
131
+
132
+ it('should handle synchronous mutation error with whenStable()', async () => {
133
+ const app = TestBed.inject(ApplicationRef)
134
+
135
+ const mutation = TestBed.runInInjectionContext(() =>
136
+ injectMutation(() => ({
137
+ mutationFn: async () => {
138
+ await Promise.resolve()
139
+ throw new Error('sync-mutation-error')
140
+ },
141
+ })),
142
+ )
143
+
144
+ mutation.mutate()
145
+
146
+ TestBed.tick()
147
+
148
+ const stablePromise = app.whenStable()
149
+ await Promise.resolve()
150
+ await vi.advanceTimersByTimeAsync(10)
151
+ await stablePromise
152
+
153
+ expect(mutation.isError()).toBe(true)
154
+ expect(mutation.error()).toEqual(new Error('sync-mutation-error'))
155
+ })
156
+ })
157
+
158
+ describe('Race Conditions', () => {
159
+ it('should handle query that completes during initial subscription', async () => {
160
+ const key = queryKey()
161
+ const app = TestBed.inject(ApplicationRef)
162
+ let resolveQuery: (value: string) => void
163
+
164
+ const queryPromise = new Promise<string>((resolve) => {
165
+ resolveQuery = resolve
166
+ })
167
+
168
+ const query = TestBed.runInInjectionContext(() =>
169
+ injectQuery(() => ({
170
+ queryKey: key,
171
+ queryFn: () => queryPromise,
172
+ })),
173
+ )
174
+
175
+ // Resolve immediately to create potential race condition
176
+ resolveQuery!('race-data')
177
+
178
+ const stablePromise = app.whenStable()
179
+ await Promise.resolve()
180
+ await vi.advanceTimersByTimeAsync(10)
181
+ await stablePromise
182
+
183
+ expect(query.status()).toBe('success')
184
+ expect(query.data()).toBe('race-data')
185
+ })
186
+
187
+ it('should handle rapid refetches without task leaks', async () => {
188
+ const app = TestBed.inject(ApplicationRef)
189
+ let callCount = 0
190
+
191
+ const key = queryKey()
192
+ const query = TestBed.runInInjectionContext(() =>
193
+ injectQuery(() => ({
194
+ queryKey: key,
195
+ queryFn: async () => {
196
+ callCount++
197
+ await sleep(10)
198
+ return `data-${callCount}`
199
+ },
200
+ })),
201
+ )
202
+
203
+ // Trigger multiple rapid refetches
204
+ query.refetch()
205
+ query.refetch()
206
+ query.refetch()
207
+
208
+ const stablePromise = app.whenStable()
209
+ await vi.advanceTimersByTimeAsync(20)
210
+ await stablePromise
211
+
212
+ expect(query.status()).toBe('success')
213
+ expect(query.data()).toMatch(/^data-\d+$/)
214
+ })
215
+
216
+ it('should keep PendingTasks active while query retry is paused offline', async () => {
217
+ const app = TestBed.inject(ApplicationRef)
218
+ let attempt = 0
219
+
220
+ const key = queryKey()
221
+ const query = TestBed.runInInjectionContext(() =>
222
+ injectQuery(() => ({
223
+ queryKey: key,
224
+ retry: 1,
225
+ retryDelay: 50, // Longer delay to ensure we can go offline before retry
226
+ queryFn: async () => {
227
+ attempt++
228
+ if (attempt === 1) {
229
+ throw new Error('offline-fail')
230
+ }
231
+ await sleep(10)
232
+ return 'final-data'
233
+ },
234
+ })),
235
+ )
236
+
237
+ // Allow the initial attempt to start and fail
238
+ await vi.advanceTimersByTimeAsync(0)
239
+ await Promise.resolve()
240
+
241
+ // Wait for the first attempt to complete and start retry delay
242
+ await vi.advanceTimersByTimeAsync(10)
243
+ await Promise.resolve()
244
+
245
+ expect(query.status()).toBe('pending')
246
+ expect(query.fetchStatus()).toBe('fetching')
247
+
248
+ // Simulate the app going offline during retry delay
249
+ onlineManager.setOnline(false)
250
+
251
+ // Advance past the retry delay to trigger the pause
252
+ await vi.advanceTimersByTimeAsync(50)
253
+ await Promise.resolve()
254
+
255
+ expect(query.fetchStatus()).toBe('paused')
256
+
257
+ const stablePromise = app.whenStable()
258
+ let stableResolved = false
259
+ void stablePromise.then(() => {
260
+ stableResolved = true
261
+ })
262
+
263
+ await Promise.resolve()
264
+
265
+ // PendingTasks should continue blocking stability while the fetch is paused
266
+ expect(stableResolved).toBe(false)
267
+ expect(query.status()).toBe('pending')
268
+
269
+ // Bring the app back online so the retry can continue
270
+ onlineManager.setOnline(true)
271
+
272
+ // Give time for the retry to resume and complete
273
+ await vi.advanceTimersByTimeAsync(20)
274
+ await Promise.resolve()
275
+
276
+ await stablePromise
277
+
278
+ expect(stableResolved).toBe(true)
279
+ expect(query.status()).toBe('success')
280
+ expect(query.data()).toBe('final-data')
281
+ })
282
+ })
283
+
284
+ describe('Component Destruction', () => {
285
+ @Component({
286
+ template: '',
287
+ })
288
+ class TestComponent {
289
+ query = injectQuery(() => ({
290
+ queryKey: ['component-query'],
291
+ queryFn: () => sleep(100).then(() => 'component-data'),
292
+ }))
293
+
294
+ mutation = injectMutation(() => ({
295
+ mutationFn: (data: string) =>
296
+ sleep(100).then(() => `processed: ${data}`),
297
+ }))
298
+ }
299
+
300
+ it('should cleanup pending tasks when component with active query is destroyed', async () => {
301
+ const app = TestBed.inject(ApplicationRef)
302
+ const fixture = TestBed.createComponent(TestComponent)
303
+
304
+ // Start the query
305
+ expect(fixture.componentInstance.query.status()).toBe('pending')
306
+
307
+ // Destroy component while query is running
308
+ fixture.destroy()
309
+
310
+ // Angular should become stable even though component was destroyed
311
+ const stablePromise = app.whenStable()
312
+ await vi.advanceTimersByTimeAsync(150)
313
+
314
+ await expect(stablePromise).resolves.toEqual(undefined)
315
+ })
316
+
317
+ it('should cleanup pending tasks when component with active mutation is destroyed', async () => {
318
+ const app = TestBed.inject(ApplicationRef)
319
+ const fixture = TestBed.createComponent(TestComponent)
320
+
321
+ fixture.componentInstance.mutation.mutate('test')
322
+
323
+ // Destroy component while mutation is running
324
+ fixture.destroy()
325
+
326
+ // Angular should become stable even though component was destroyed
327
+ const stablePromise = app.whenStable()
328
+ await vi.advanceTimersByTimeAsync(150)
329
+
330
+ await expect(stablePromise).resolves.toEqual(undefined)
331
+ })
332
+ })
333
+
334
+ describe('Concurrent Operations', () => {
335
+ it('should handle multiple queries running simultaneously', async () => {
336
+ const key1 = queryKey()
337
+ const key2 = queryKey()
338
+ const key3 = queryKey()
339
+ const app = TestBed.inject(ApplicationRef)
340
+
341
+ const query1 = TestBed.runInInjectionContext(() =>
342
+ injectQuery(() => ({
343
+ queryKey: key1,
344
+ queryFn: () => sleep(30).then(() => 'data-1'),
345
+ })),
346
+ )
347
+
348
+ const query2 = TestBed.runInInjectionContext(() =>
349
+ injectQuery(() => ({
350
+ queryKey: key2,
351
+ queryFn: () => sleep(50).then(() => 'data-2'),
352
+ })),
353
+ )
354
+
355
+ const query3 = TestBed.runInInjectionContext(() =>
356
+ injectQuery(() => ({
357
+ queryKey: key3,
358
+ queryFn: () => 'instant-data', // Synchronous
359
+ })),
360
+ )
361
+
362
+ // All queries should start
363
+ expect(query1.status()).toBe('pending')
364
+ expect(query2.status()).toBe('pending')
365
+ expect(query3.status()).toBe('pending')
366
+
367
+ const stablePromise = app.whenStable()
368
+ await vi.advanceTimersByTimeAsync(60)
369
+ await stablePromise
370
+
371
+ // All queries should be complete
372
+ expect(query1.status()).toBe('success')
373
+ expect(query1.data()).toBe('data-1')
374
+ expect(query2.status()).toBe('success')
375
+ expect(query2.data()).toBe('data-2')
376
+ expect(query3.status()).toBe('success')
377
+ expect(query3.data()).toBe('instant-data')
378
+ })
379
+
380
+ it('should handle multiple mutations running simultaneously', async () => {
381
+ const app = TestBed.inject(ApplicationRef)
382
+
383
+ const mutation1 = TestBed.runInInjectionContext(() =>
384
+ injectMutation(() => ({
385
+ mutationFn: (data: string) =>
386
+ sleep(30).then(() => `processed-1: ${data}`),
387
+ })),
388
+ )
389
+
390
+ const mutation2 = TestBed.runInInjectionContext(() =>
391
+ injectMutation(() => ({
392
+ mutationFn: (data: string) =>
393
+ sleep(50).then(() => `processed-2: ${data}`),
394
+ })),
395
+ )
396
+
397
+ const mutation3 = TestBed.runInInjectionContext(() =>
398
+ injectMutation(() => ({
399
+ mutationFn: async (data: string) => {
400
+ await Promise.resolve()
401
+ return `processed-3: ${data}`
402
+ },
403
+ })),
404
+ )
405
+
406
+ // Start all mutations
407
+ mutation1.mutate('test1')
408
+ mutation2.mutate('test2')
409
+ mutation3.mutate('test3')
410
+
411
+ TestBed.tick()
412
+
413
+ const stablePromise = app.whenStable()
414
+ await vi.advanceTimersByTimeAsync(60)
415
+ await stablePromise
416
+
417
+ // All mutations should be complete
418
+ expect(mutation1.isSuccess()).toBe(true)
419
+ expect(mutation1.data()).toBe('processed-1: test1')
420
+ expect(mutation2.isSuccess()).toBe(true)
421
+ expect(mutation2.data()).toBe('processed-2: test2')
422
+ expect(mutation3.isSuccess()).toBe(true)
423
+ expect(mutation3.data()).toBe('processed-3: test3')
424
+ })
425
+
426
+ it('should handle mixed queries and mutations', async () => {
427
+ const app = TestBed.inject(ApplicationRef)
428
+
429
+ const key = queryKey()
430
+ const query = TestBed.runInInjectionContext(() =>
431
+ injectQuery(() => ({
432
+ queryKey: key,
433
+ queryFn: () => sleep(40).then(() => 'query-data'),
434
+ })),
435
+ )
436
+
437
+ const mutation = TestBed.runInInjectionContext(() =>
438
+ injectMutation(() => ({
439
+ mutationFn: (data: string) =>
440
+ sleep(60).then(() => `mutation: ${data}`),
441
+ })),
442
+ )
443
+
444
+ // Start both operations
445
+ mutation.mutate('test')
446
+
447
+ const stablePromise = app.whenStable()
448
+ await vi.advanceTimersByTimeAsync(70)
449
+ await stablePromise
450
+
451
+ // Both should be complete
452
+ expect(query.status()).toBe('success')
453
+ expect(query.data()).toBe('query-data')
454
+ expect(mutation.isSuccess()).toBe(true)
455
+ expect(mutation.data()).toBe('mutation: test')
456
+ })
457
+ })
458
+
459
+ describe('HttpClient Integration', () => {
460
+ beforeEach(() => {
461
+ TestBed.configureTestingModule({
462
+ providers: [provideHttpClient(), provideHttpClientTesting()],
463
+ })
464
+ })
465
+
466
+ it('should handle multiple HttpClient requests with lastValueFrom', async () => {
467
+ const app = TestBed.inject(ApplicationRef)
468
+ const httpClient = TestBed.inject(HttpClient)
469
+ const httpTestingController = TestBed.inject(HttpTestingController)
470
+
471
+ const key1 = queryKey()
472
+ const key2 = queryKey()
473
+
474
+ const query1 = TestBed.runInInjectionContext(() =>
475
+ injectQuery(() => ({
476
+ queryKey: key1,
477
+ queryFn: () =>
478
+ lastValueFrom(httpClient.get<{ id: number }>('/api/1')),
479
+ })),
480
+ )
481
+
482
+ const query2 = TestBed.runInInjectionContext(() =>
483
+ injectQuery(() => ({
484
+ queryKey: key2,
485
+ queryFn: () =>
486
+ lastValueFrom(httpClient.get<{ id: number }>('/api/2')),
487
+ })),
488
+ )
489
+
490
+ // Schedule HTTP responses
491
+ setTimeout(() => {
492
+ const req1 = httpTestingController.expectOne('/api/1')
493
+ req1.flush({ id: 1 })
494
+
495
+ const req2 = httpTestingController.expectOne('/api/2')
496
+ req2.flush({ id: 2 })
497
+ }, 10)
498
+
499
+ const stablePromise = app.whenStable()
500
+ await vi.advanceTimersByTimeAsync(20)
501
+ await stablePromise
502
+
503
+ expect(query1.status()).toBe('success')
504
+ expect(query1.data()).toEqual({ id: 1 })
505
+ expect(query2.status()).toBe('success')
506
+ expect(query2.data()).toEqual({ id: 2 })
507
+
508
+ httpTestingController.verify()
509
+ })
510
+
511
+ it('should handle HttpClient request cancellation', async () => {
512
+ const app = TestBed.inject(ApplicationRef)
513
+ const httpClient = TestBed.inject(HttpClient)
514
+ const httpTestingController = TestBed.inject(HttpTestingController)
515
+
516
+ const key = queryKey()
517
+ const query = TestBed.runInInjectionContext(() =>
518
+ injectQuery(() => ({
519
+ queryKey: key,
520
+ queryFn: () =>
521
+ lastValueFrom(httpClient.get<{ data: string }>('/api/cancel')),
522
+ })),
523
+ )
524
+
525
+ // Cancel the request before it completes
526
+ setTimeout(() => {
527
+ const req = httpTestingController.expectOne('/api/cancel')
528
+ req.error(new ProgressEvent('error'), {
529
+ status: 0,
530
+ statusText: 'Unknown Error',
531
+ })
532
+ }, 10)
533
+
534
+ const stablePromise = app.whenStable()
535
+ await vi.advanceTimersByTimeAsync(20)
536
+ await stablePromise
537
+
538
+ expect(query.status()).toBe('error')
539
+
540
+ httpTestingController.verify()
541
+ })
542
+ })
543
+
544
+ describe('Edge Cases', () => {
545
+ it('should handle query cancellation mid-flight', async () => {
546
+ const key = queryKey()
547
+ const app = TestBed.inject(ApplicationRef)
548
+
549
+ const query = TestBed.runInInjectionContext(() =>
550
+ injectQuery(() => ({
551
+ queryKey: key,
552
+ queryFn: () => sleep(100).then(() => 'data'),
553
+ })),
554
+ )
555
+
556
+ // Cancel the query after a short delay
557
+ setTimeout(() => {
558
+ queryClient.cancelQueries({ queryKey: key })
559
+ }, 20)
560
+
561
+ // Advance to the cancellation point
562
+ await vi.advanceTimersByTimeAsync(20)
563
+
564
+ TestBed.tick()
565
+
566
+ const stablePromise = app.whenStable()
567
+ await vi.advanceTimersByTimeAsync(130)
568
+ await stablePromise
569
+
570
+ // Cancellation should restore the pre-fetch state
571
+ expect(query.status()).toBe('pending')
572
+ expect(query.fetchStatus()).toBe('idle')
573
+ })
574
+
575
+ it('should handle query retry and pending task tracking', async () => {
576
+ const app = TestBed.inject(ApplicationRef)
577
+ let attemptCount = 0
578
+
579
+ const key = queryKey()
580
+ const query = TestBed.runInInjectionContext(() =>
581
+ injectQuery(() => ({
582
+ queryKey: key,
583
+ retry: 2,
584
+ retryDelay: 10,
585
+ queryFn: async () => {
586
+ attemptCount++
587
+ if (attemptCount <= 2) {
588
+ throw new Error(`Attempt ${attemptCount} failed`)
589
+ }
590
+ return 'success-data'
591
+ },
592
+ })),
593
+ )
594
+
595
+ const stablePromise = app.whenStable()
596
+ await vi.advanceTimersByTimeAsync(50)
597
+ await stablePromise
598
+
599
+ expect(query.status()).toBe('success')
600
+ expect(query.data()).toBe('success-data')
601
+ expect(attemptCount).toBe(3) // Initial + 2 retries
602
+ })
603
+
604
+ it('should handle mutation with optimistic updates', async () => {
605
+ const app = TestBed.inject(ApplicationRef)
606
+ const testQueryKey = queryKey()
607
+
608
+ queryClient.setQueryData(testQueryKey, 'initial-data')
609
+
610
+ const mutation = TestBed.runInInjectionContext(() =>
611
+ injectMutation(() => ({
612
+ mutationFn: (newData: string) => sleep(50).then(() => newData),
613
+ onMutate: async (newData) => {
614
+ // Optimistic update
615
+ const previousData = queryClient.getQueryData(testQueryKey)
616
+ queryClient.setQueryData(testQueryKey, newData)
617
+ return { previousData }
618
+ },
619
+ onError: (_err, _newData, context) => {
620
+ // Rollback on error
621
+ if (context?.previousData) {
622
+ queryClient.setQueryData(testQueryKey, context.previousData)
623
+ }
624
+ },
625
+ })),
626
+ )
627
+
628
+ mutation.mutate('optimistic-data')
629
+
630
+ await Promise.resolve()
631
+
632
+ // Data should be optimistically updated immediately
633
+ expect(queryClient.getQueryData(testQueryKey)).toBe('optimistic-data')
634
+
635
+ const stablePromise = app.whenStable()
636
+ await vi.advanceTimersByTimeAsync(60)
637
+ await stablePromise
638
+
639
+ expect(mutation.isSuccess()).toBe(true)
640
+ expect(mutation.data()).toBe('optimistic-data')
641
+ expect(queryClient.getQueryData(testQueryKey)).toBe('optimistic-data')
642
+ })
643
+ })
644
+ })
@@ -0,0 +1,38 @@
1
+ import { TestBed } from '@angular/core/testing'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { InjectionToken, provideZonelessChangeDetection } from '@angular/core'
4
+ import { QueryClient } from '@tanstack/query-core'
5
+ import { provideQueryClient } from '../providers'
6
+
7
+ describe('provideQueryClient', () => {
8
+ it('should provide a QueryClient instance directly', () => {
9
+ const queryClient = new QueryClient()
10
+
11
+ TestBed.configureTestingModule({
12
+ providers: [
13
+ provideZonelessChangeDetection(),
14
+ provideQueryClient(queryClient),
15
+ ],
16
+ })
17
+
18
+ const providedQueryClient = TestBed.inject(QueryClient)
19
+ expect(providedQueryClient).toBe(queryClient)
20
+ })
21
+
22
+ it('should provide a QueryClient instance using an InjectionToken', () => {
23
+ const queryClient = new QueryClient()
24
+ const CUSTOM_QUERY_CLIENT = new InjectionToken<QueryClient>('', {
25
+ factory: () => queryClient,
26
+ })
27
+
28
+ TestBed.configureTestingModule({
29
+ providers: [
30
+ provideZonelessChangeDetection(),
31
+ provideQueryClient(CUSTOM_QUERY_CLIENT),
32
+ ],
33
+ })
34
+
35
+ const providedQueryClient = TestBed.inject(QueryClient)
36
+ expect(providedQueryClient).toBe(queryClient)
37
+ })
38
+ })