@ethlete/query 6.0.0-next.59 → 6.0.0-next.60

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.
@@ -1,644 +1,557 @@
1
- import { DestroyRef, ErrorHandler, Injector } from '@angular/core';
2
- import { TestBed } from '@angular/core/testing';
3
- import { createBearerAuthProvider, withAuthenticationQuery, withRefreshQuery, isQueryErrorResponse, createQueryClient, createDeleteQuery, createPatchQuery, createPutQuery, createPostQuery, createGetQuery } from '@ethlete/query';
4
- import { HttpErrorResponse, provideHttpClient, HttpClient } from '@angular/common/http';
5
- import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
6
- import { provideRouter } from '@angular/router';
7
-
8
- /* eslint-disable @typescript-eslint/no-explicit-any */
1
+ import { DestroyRef, ErrorHandler, Injector } from "@angular/core";
2
+ import { TestBed } from "@angular/core/testing";
3
+ import { createBearerAuthProvider, createDeleteQuery, createGetQuery, createPatchQuery, createPostQuery, createPutQuery, createQueryClient, isQueryErrorResponse, withAuthenticationQuery, withRefreshQuery } from "@ethlete/query";
4
+ import { HttpClient, HttpErrorResponse, provideHttpClient } from "@angular/common/http";
5
+ import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing";
6
+ import { provideRouter } from "@angular/router";
9
7
  const expectAndFlush = (httpTesting, url, response, statusOpts) => {
10
- const req = httpTesting.expectOne(url);
11
- if (statusOpts) {
12
- req.flush(response, statusOpts);
13
- }
14
- else {
15
- req.flush(response);
16
- }
8
+ const req = httpTesting.expectOne(url);
9
+ if (statusOpts) req.flush(response, statusOpts);
10
+ else req.flush(response);
17
11
  };
18
12
  const expectFlushAndWait = (httpTesting, url, response, statusOpts) => {
19
- expectAndFlush(httpTesting, url, response, statusOpts);
20
- TestBed.tick();
13
+ expectAndFlush(httpTesting, url, response, statusOpts);
14
+ TestBed.tick();
21
15
  };
22
-
23
16
  const setupAuthTest = (config) => {
24
- const { querySetup, loginPath = '/auth/login', refreshPath = '/auth/refresh', autoRetryOn401 = true, extractLoginTokens = (response) => ({
25
- accessToken: response.accessToken,
26
- refreshToken: response.refreshToken,
27
- }), extractRefreshTokens = (response) => ({
28
- accessToken: response.accessToken,
29
- refreshToken: response.refreshToken,
30
- }), buildRefreshArgs, features, bearerDecryptFn, refreshStrategy, minRefreshInterval, refreshIfExpired, expiresInPropertyName, onRefreshFailure, } = config;
31
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
- const login = querySetup.createPost(loginPath, {});
33
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
- const refresh = querySetup.createPost(refreshPath, {});
35
- const { inject: injectAuth } = createBearerAuthProvider({
36
- name: 'test-auth',
37
- queryClientRef: querySetup.queryClientRef,
38
- queries: [
39
- withAuthenticationQuery('login', {
40
- queryCreator: login,
41
- extractTokens: extractLoginTokens,
42
- }),
43
- withRefreshQuery('refresh', {
44
- queryCreator: refresh,
45
- extractTokens: extractRefreshTokens,
46
- buildArgs: buildRefreshArgs,
47
- autoRetryOn401,
48
- refreshStrategy,
49
- minRefreshInterval,
50
- refreshIfExpired,
51
- expiresInPropertyName,
52
- onRefreshFailure,
53
- }),
54
- ],
55
- features: (features ?? []),
56
- bearerDecryptFn,
57
- });
58
- const auth = TestBed.runInInjectionContext(() => {
59
- const provider = injectAuth();
60
- if (!provider) {
61
- throw new Error('Failed to create auth provider in test setup');
62
- }
63
- return provider;
64
- });
65
- const loginHelper = (credentials, response) => {
66
- TestBed.runInInjectionContext(() => {
67
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
- auth.queries.login.execute({ body: credentials });
69
- });
70
- const fullUrl = `${querySetup.baseUrl}${loginPath}`;
71
- expectFlushAndWait(querySetup.httpTesting, fullUrl, response);
72
- };
73
- const refreshHelper = (token, response) => {
74
- TestBed.runInInjectionContext(() => {
75
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- auth.queries.refresh.execute((buildRefreshArgs?.(token) ?? { body: { token } }));
77
- });
78
- const fullUrl = `${querySetup.baseUrl}${refreshPath}`;
79
- expectFlushAndWait(querySetup.httpTesting, fullUrl, response);
80
- };
81
- const makeSecureRequest = (route) => {
82
- TestBed.runInInjectionContext(() => {
83
- querySetup.queryClient.repository.request({
84
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
- route: route,
86
- method: 'GET',
87
- isSecure: true,
88
- consumerDestroyRef: TestBed.inject(DestroyRef),
89
- });
90
- });
91
- };
92
- return {
93
- auth: auth,
94
- login: loginHelper,
95
- refresh: refreshHelper,
96
- makeSecureRequest,
97
- };
17
+ const { querySetup, loginPath = "/auth/login", refreshPath = "/auth/refresh", autoRetryOn401 = true, extractLoginTokens = (response) => ({
18
+ accessToken: response.accessToken,
19
+ refreshToken: response.refreshToken
20
+ }), extractRefreshTokens = (response) => ({
21
+ accessToken: response.accessToken,
22
+ refreshToken: response.refreshToken
23
+ }), buildRefreshArgs, features, bearerDecryptFn, refreshStrategy, minRefreshInterval, refreshIfExpired, expiresInPropertyName, onRefreshFailure } = config;
24
+ const login = querySetup.createPost(loginPath, {});
25
+ const refresh = querySetup.createPost(refreshPath, {});
26
+ const { inject: injectAuth } = createBearerAuthProvider({
27
+ name: "test-auth",
28
+ queryClientRef: querySetup.queryClientRef,
29
+ queries: [withAuthenticationQuery("login", {
30
+ queryCreator: login,
31
+ extractTokens: extractLoginTokens
32
+ }), withRefreshQuery("refresh", {
33
+ queryCreator: refresh,
34
+ extractTokens: extractRefreshTokens,
35
+ buildArgs: buildRefreshArgs,
36
+ autoRetryOn401,
37
+ refreshStrategy,
38
+ minRefreshInterval,
39
+ refreshIfExpired,
40
+ expiresInPropertyName,
41
+ onRefreshFailure
42
+ })],
43
+ features: features ?? [],
44
+ bearerDecryptFn
45
+ });
46
+ const auth = TestBed.runInInjectionContext(() => {
47
+ const provider = injectAuth();
48
+ if (!provider) throw new Error("Failed to create auth provider in test setup");
49
+ return provider;
50
+ });
51
+ const loginHelper = (credentials, response) => {
52
+ TestBed.runInInjectionContext(() => {
53
+ auth.queries.login.execute({ body: credentials });
54
+ });
55
+ const fullUrl = `${querySetup.baseUrl}${loginPath}`;
56
+ expectFlushAndWait(querySetup.httpTesting, fullUrl, response);
57
+ };
58
+ const refreshHelper = (token, response) => {
59
+ TestBed.runInInjectionContext(() => {
60
+ auth.queries.refresh.execute(buildRefreshArgs?.(token) ?? { body: { token } });
61
+ });
62
+ const fullUrl = `${querySetup.baseUrl}${refreshPath}`;
63
+ expectFlushAndWait(querySetup.httpTesting, fullUrl, response);
64
+ };
65
+ const makeSecureRequest = (route) => {
66
+ TestBed.runInInjectionContext(() => {
67
+ querySetup.queryClient.repository.request({
68
+ route,
69
+ method: "GET",
70
+ isSecure: true,
71
+ consumerDestroyRef: TestBed.inject(DestroyRef)
72
+ });
73
+ });
74
+ };
75
+ return {
76
+ auth,
77
+ login: loginHelper,
78
+ refresh: refreshHelper,
79
+ makeSecureRequest
80
+ };
98
81
  };
99
-
100
- /* eslint-disable @typescript-eslint/no-explicit-any */
101
- // Captured at import, before a spec can call `vi.useFakeTimers()`: delivery must not wait on a faked clock.
102
82
  const nodeSetImmediate = globalThis.setImmediate;
103
- const scheduleTask = nodeSetImmediate
104
- ? nodeSetImmediate.bind(globalThis)
105
- : setTimeout.bind(globalThis);
106
- /**
107
- * Replaces `globalThis.BroadcastChannel` with an in-memory bus.
108
- *
109
- * Messages are structured-cloned at post time (so a non-cloneable payload throws exactly like the
110
- * real API does), delivered as a task (never synchronously, and after every pending microtask), and
111
- * never delivered back to the channel that posted them - all three matching browser behavior. Delivery
112
- * does not depend on fake timers; `await flushMultiTabSync()` in a spec flushes it.
113
- */
83
+ const scheduleTask = nodeSetImmediate ? nodeSetImmediate.bind(globalThis) : setTimeout.bind(globalThis);
114
84
  const installFakeBroadcastChannel = () => {
115
- const original = globalThis.BroadcastChannel;
116
- const instances = new Set();
117
- const posted = [];
118
- class FakeBroadcastChannel {
119
- constructor(name) {
120
- this.name = name;
121
- this.closed = false;
122
- this.onmessage = null;
123
- instances.add(this);
124
- }
125
- postMessage(data) {
126
- if (this.closed)
127
- throw new DOMException('Channel is closed', 'InvalidStateError');
128
- const clone = structuredClone(data);
129
- posted.push({ channel: this.name, data: clone });
130
- for (const instance of instances) {
131
- if (instance === this || instance.closed || instance.name !== this.name)
132
- continue;
133
- scheduleTask(() => {
134
- if (instance.closed)
135
- return;
136
- instance.onmessage?.({ data: structuredClone(clone) });
137
- });
138
- }
139
- }
140
- close() {
141
- this.closed = true;
142
- instances.delete(this);
143
- }
144
- addEventListener() {
145
- throw new Error('The fake BroadcastChannel only supports `onmessage`.');
146
- }
147
- }
148
- globalThis.BroadcastChannel = FakeBroadcastChannel;
149
- return {
150
- posted,
151
- restore: () => {
152
- for (const instance of [...instances]) {
153
- instance.closed = true;
154
- }
155
- instances.clear();
156
- globalThis.BroadcastChannel = original;
157
- },
158
- };
85
+ const original = globalThis.BroadcastChannel;
86
+ const instances = /* @__PURE__ */ new Set();
87
+ const posted = [];
88
+ class FakeBroadcastChannel {
89
+ constructor(name) {
90
+ this.name = name;
91
+ this.closed = false;
92
+ this.onmessage = null;
93
+ instances.add(this);
94
+ }
95
+ postMessage(data) {
96
+ if (this.closed) throw new DOMException("Channel is closed", "InvalidStateError");
97
+ const clone = structuredClone(data);
98
+ posted.push({
99
+ channel: this.name,
100
+ data: clone
101
+ });
102
+ for (const instance of instances) {
103
+ if (instance === this || instance.closed || instance.name !== this.name) continue;
104
+ scheduleTask(() => {
105
+ if (instance.closed) return;
106
+ instance.onmessage?.({ data: structuredClone(clone) });
107
+ });
108
+ }
109
+ }
110
+ close() {
111
+ this.closed = true;
112
+ instances.delete(this);
113
+ }
114
+ addEventListener() {
115
+ throw new Error("The fake BroadcastChannel only supports `onmessage`.");
116
+ }
117
+ }
118
+ globalThis.BroadcastChannel = FakeBroadcastChannel;
119
+ return {
120
+ posted,
121
+ restore: () => {
122
+ for (const instance of [...instances]) instance.closed = true;
123
+ instances.clear();
124
+ globalThis.BroadcastChannel = original;
125
+ }
126
+ };
159
127
  };
160
- /**
161
- * Installs an in-memory `navigator.locks` implementing exclusive locks: one holder per name, the
162
- * rest queued FIFO, granted on a microtask (never synchronously), released when the callback's
163
- * promise settles. `signal` is honored for queued requests and - as in the real API - ignored once a
164
- * request has been granted.
165
- *
166
- * `ifAvailable` is a try-lock: a request that cannot be granted straight away runs its callback with
167
- * `null` instead of queueing, exactly as the real API does. `steal` releases the current holder -
168
- * rejecting its request with an `AbortError` - and is granted ahead of everything queued.
169
- *
170
- * `mode: 'shared'` is not implemented; nothing in the library uses it.
171
- */
172
128
  const installFakeWebLocks = () => {
173
- const originalDescriptor = Object.getOwnPropertyDescriptor(navigator, 'locks');
174
- const holders = new Map();
175
- const queues = new Map();
176
- const abortError = () => new DOMException('The lock request was aborted', 'AbortError');
177
- const pump = (name) => {
178
- if (holders.has(name))
179
- return;
180
- const waiter = queues.get(name)?.shift();
181
- if (!waiter)
182
- return;
183
- waiter.granted = true;
184
- holders.set(name, waiter);
185
- const settle = (settleWaiter) => {
186
- // Only if this waiter is still the holder: a stolen lock already has a new one, and the callback
187
- // of the tab it was taken from settles afterwards.
188
- if (holders.get(name) === waiter)
189
- holders.delete(name);
190
- waiter.releaseSignal();
191
- settleWaiter();
192
- pump(name);
193
- };
194
- // `pump` itself only ever runs from a microtask, so calling the callback right here keeps the
195
- // grant exactly one microtask away from the `request()` call - which is what makes a single
196
- // `flushMultiTabSync()` per hop enough in specs.
197
- try {
198
- Promise.resolve(waiter.callback({ name, mode: 'exclusive' })).then((value) => settle(() => waiter.resolve(value)), (error) => settle(() => waiter.reject(error)));
199
- }
200
- catch (error) {
201
- settle(() => waiter.reject(error));
202
- }
203
- };
204
- const request = (name, optionsOrCallback, maybeCallback) => {
205
- const options = (typeof optionsOrCallback === 'function' ? {} : (optionsOrCallback ?? {}));
206
- const callback = (typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback);
207
- return new Promise((resolve, reject) => {
208
- if (options.signal?.aborted) {
209
- reject(abortError());
210
- return;
211
- }
212
- const waiter = { callback, resolve, reject, granted: false, releaseSignal: () => undefined };
213
- const signal = options.signal;
214
- if (signal) {
215
- const onAbort = () => {
216
- waiter.releaseSignal();
217
- if (waiter.granted)
218
- return;
219
- const queue = queues.get(name);
220
- if (queue)
221
- queues.set(name, queue.filter((entry) => entry !== waiter));
222
- reject(abortError());
223
- };
224
- signal.addEventListener('abort', onAbort);
225
- waiter.releaseSignal = () => {
226
- waiter.releaseSignal = () => undefined;
227
- signal.removeEventListener('abort', onAbort);
228
- };
229
- }
230
- if (options.steal) {
231
- queueMicrotask(() => {
232
- const holder = holders.get(name);
233
- if (holder) {
234
- holders.delete(name);
235
- holder.reject(abortError());
236
- }
237
- queues.set(name, [waiter, ...(queues.get(name) ?? [])]);
238
- pump(name);
239
- });
240
- return;
241
- }
242
- // Availability is decided a microtask out, not here: that is when the real API decides it too, so
243
- // two try-locks requested in the same tick still resolve to one winner and one `null`.
244
- if (options.ifAvailable) {
245
- queueMicrotask(() => {
246
- if (holders.has(name) || (queues.get(name)?.length ?? 0) > 0) {
247
- waiter.releaseSignal();
248
- Promise.resolve(callback(null)).then(resolve, reject);
249
- return;
250
- }
251
- queues.set(name, [waiter]);
252
- pump(name);
253
- });
254
- return;
255
- }
256
- queues.set(name, [...(queues.get(name) ?? []), waiter]);
257
- queueMicrotask(() => pump(name));
258
- });
259
- };
260
- // Both lists are reported, because a queued request is how a tab that is *not* the holder still
261
- // shows up - which is what the auth leader election counts its tabs by.
262
- const query = () => Promise.resolve({
263
- held: [...holders.keys()].map((name) => ({ name, mode: 'exclusive' })),
264
- pending: [...queues.entries()].flatMap(([name, waiters]) => waiters.map(() => ({ name, mode: 'exclusive' }))),
265
- });
266
- Object.defineProperty(navigator, 'locks', {
267
- value: {
268
- request,
269
- query,
270
- },
271
- configurable: true,
272
- writable: true,
273
- });
274
- return {
275
- heldNames: () => [...holders.keys()],
276
- pendingNames: () => [...queues.entries()].filter(([, queue]) => queue.length > 0).map(([name]) => name),
277
- restore: () => {
278
- for (const waiter of [...holders.values(), ...[...queues.values()].flat()])
279
- waiter.releaseSignal();
280
- holders.clear();
281
- queues.clear();
282
- if (originalDescriptor) {
283
- Object.defineProperty(navigator, 'locks', originalDescriptor);
284
- }
285
- else {
286
- delete navigator.locks;
287
- }
288
- },
289
- };
129
+ const originalDescriptor = Object.getOwnPropertyDescriptor(navigator, "locks");
130
+ const holders = /* @__PURE__ */ new Map();
131
+ const queues = /* @__PURE__ */ new Map();
132
+ const abortError = () => new DOMException("The lock request was aborted", "AbortError");
133
+ const pump = (name) => {
134
+ if (holders.has(name)) return;
135
+ const waiter = queues.get(name)?.shift();
136
+ if (!waiter) return;
137
+ waiter.granted = true;
138
+ holders.set(name, waiter);
139
+ const settle = (settleWaiter) => {
140
+ if (holders.get(name) === waiter) holders.delete(name);
141
+ waiter.releaseSignal();
142
+ settleWaiter();
143
+ pump(name);
144
+ };
145
+ try {
146
+ Promise.resolve(waiter.callback({
147
+ name,
148
+ mode: "exclusive"
149
+ })).then((value) => settle(() => waiter.resolve(value)), (error) => settle(() => waiter.reject(error)));
150
+ } catch (error) {
151
+ settle(() => waiter.reject(error));
152
+ }
153
+ };
154
+ const request = (name, optionsOrCallback, maybeCallback) => {
155
+ const options = typeof optionsOrCallback === "function" ? {} : optionsOrCallback ?? {};
156
+ const callback = typeof optionsOrCallback === "function" ? optionsOrCallback : maybeCallback;
157
+ return new Promise((resolve, reject) => {
158
+ if (options.signal?.aborted) {
159
+ reject(abortError());
160
+ return;
161
+ }
162
+ const waiter = {
163
+ callback,
164
+ resolve,
165
+ reject,
166
+ granted: false,
167
+ releaseSignal: () => void 0
168
+ };
169
+ const signal = options.signal;
170
+ if (signal) {
171
+ const onAbort = () => {
172
+ waiter.releaseSignal();
173
+ if (waiter.granted) return;
174
+ const queue = queues.get(name);
175
+ if (queue) queues.set(name, queue.filter((entry) => entry !== waiter));
176
+ reject(abortError());
177
+ };
178
+ signal.addEventListener("abort", onAbort);
179
+ waiter.releaseSignal = () => {
180
+ waiter.releaseSignal = () => void 0;
181
+ signal.removeEventListener("abort", onAbort);
182
+ };
183
+ }
184
+ if (options.steal) {
185
+ queueMicrotask(() => {
186
+ const holder = holders.get(name);
187
+ if (holder) {
188
+ holders.delete(name);
189
+ holder.reject(abortError());
190
+ }
191
+ queues.set(name, [waiter, ...queues.get(name) ?? []]);
192
+ pump(name);
193
+ });
194
+ return;
195
+ }
196
+ if (options.ifAvailable) {
197
+ queueMicrotask(() => {
198
+ if (holders.has(name) || (queues.get(name)?.length ?? 0) > 0) {
199
+ waiter.releaseSignal();
200
+ Promise.resolve(callback(null)).then(resolve, reject);
201
+ return;
202
+ }
203
+ queues.set(name, [waiter]);
204
+ pump(name);
205
+ });
206
+ return;
207
+ }
208
+ queues.set(name, [...queues.get(name) ?? [], waiter]);
209
+ queueMicrotask(() => pump(name));
210
+ });
211
+ };
212
+ const query = () => Promise.resolve({
213
+ held: [...holders.keys()].map((name) => ({
214
+ name,
215
+ mode: "exclusive"
216
+ })),
217
+ pending: [...queues.entries()].flatMap(([name, waiters]) => waiters.map(() => ({
218
+ name,
219
+ mode: "exclusive"
220
+ })))
221
+ });
222
+ Object.defineProperty(navigator, "locks", {
223
+ value: {
224
+ request,
225
+ query
226
+ },
227
+ configurable: true,
228
+ writable: true
229
+ });
230
+ return {
231
+ heldNames: () => [...holders.keys()],
232
+ pendingNames: () => [...queues.entries()].filter(([, queue]) => queue.length > 0).map(([name]) => name),
233
+ restore: () => {
234
+ for (const waiter of [...holders.values(), ...[...queues.values()].flat()]) waiter.releaseSignal();
235
+ holders.clear();
236
+ queues.clear();
237
+ if (originalDescriptor) Object.defineProperty(navigator, "locks", originalDescriptor);
238
+ else delete navigator.locks;
239
+ }
240
+ };
290
241
  };
291
- /**
292
- * Waits for the fakes' pending work - message delivery (a task) and lock grants (a microtask) - even
293
- * under fake timers. Awaiting it once is enough for a single hop; a grant that has to wait for another
294
- * tab's release, or a reply to a delivered message, needs one await per hop.
295
- */
296
242
  const flushMultiTabSync = () => new Promise((resolve) => scheduleTask(resolve));
297
-
298
- const quotaError = () => new DOMException('The quota has been exceeded.', 'QuotaExceededError');
243
+ const quotaError = () => new DOMException("The quota has been exceeded.", "QuotaExceededError");
299
244
  const createFakeQueryPersistenceStore = () => {
300
- const store = new Map();
301
- const calls = { loadIndex: 0, read: 0, write: 0, remove: 0, clear: 0 };
302
- const deferredReads = [];
303
- const deferredLoadIndexes = [];
304
- let failingWrites = 0;
305
- let writeError = null;
306
- let failingRemoves = 0;
307
- let removeError = null;
308
- let loadIndexError = null;
309
- let isDeferringReads = false;
310
- let isDeferringLoadIndex = false;
311
- // Cloning on the way in and on the way out is not pedantry: a real store hands back a copy, so a
312
- // spec (or the engine) mutating a hydrated body must not be able to change what is "on disk".
313
- const clone = (value) => structuredClone(value);
314
- const loadIndex = async () => {
315
- calls.loadIndex++;
316
- if (loadIndexError !== null) {
317
- const error = loadIndexError;
318
- loadIndexError = null;
319
- throw error;
320
- }
321
- // Taken before the gate, not after: a real store answers with what it held when the read started,
322
- // so anything that empties it while a deferred load is held must not change what that load returns.
323
- const snapshot = Array.from(store.values()).map(({ body: _body, ...meta }) => clone(meta));
324
- if (isDeferringLoadIndex) {
325
- await new Promise((resolve) => deferredLoadIndexes.push(resolve));
326
- }
327
- return snapshot;
328
- };
329
- const read = async (key) => {
330
- calls.read++;
331
- const entry = store.get(key);
332
- const snapshot = entry ? { body: clone(entry.body), version: entry.version } : null;
333
- if (isDeferringReads) {
334
- await new Promise((resolve) => deferredReads.push(resolve));
335
- }
336
- return snapshot;
337
- };
338
- const write = async (entries) => {
339
- calls.write++;
340
- if (failingWrites > 0) {
341
- failingWrites--;
342
- throw writeError ?? quotaError();
343
- }
344
- for (const entry of entries) {
345
- store.set(entry.key, clone(entry));
346
- }
347
- };
348
- const remove = async (keys) => {
349
- calls.remove++;
350
- if (failingRemoves > 0) {
351
- failingRemoves--;
352
- throw removeError ?? new Error('The removal failed.');
353
- }
354
- for (const key of keys) {
355
- store.delete(key);
356
- }
357
- };
358
- const clear = async () => {
359
- calls.clear++;
360
- store.clear();
361
- };
362
- return {
363
- adapter: { loadIndex, read, write, remove, clear, isSupported: true },
364
- seed: (entries) => {
365
- for (const entry of entries) {
366
- store.set(entry.key, clone(entry));
367
- }
368
- },
369
- entries: () => Array.from(store.values()).map(clone),
370
- entry: (key) => {
371
- const entry = store.get(key);
372
- return entry ? clone(entry) : undefined;
373
- },
374
- calls: () => ({ ...calls }),
375
- failNextWrites: (count, error) => {
376
- failingWrites = count;
377
- writeError = error ?? null;
378
- },
379
- failNextRemoves: (count, error) => {
380
- failingRemoves = count;
381
- removeError = error ?? null;
382
- },
383
- failNextLoadIndex: (error) => {
384
- loadIndexError = error ?? quotaError();
385
- },
386
- deferReads: () => {
387
- isDeferringReads = true;
388
- },
389
- flushReads: async () => {
390
- isDeferringReads = false;
391
- const pending = deferredReads.splice(0, deferredReads.length);
392
- for (const resolve of pending) {
393
- resolve();
394
- }
395
- await Promise.resolve();
396
- },
397
- deferLoadIndex: () => {
398
- isDeferringLoadIndex = true;
399
- },
400
- flushLoadIndex: async () => {
401
- isDeferringLoadIndex = false;
402
- const pending = deferredLoadIndexes.splice(0, deferredLoadIndexes.length);
403
- for (const resolve of pending) {
404
- resolve();
405
- }
406
- await Promise.resolve();
407
- },
408
- };
245
+ const store = /* @__PURE__ */ new Map();
246
+ const calls = {
247
+ loadIndex: 0,
248
+ read: 0,
249
+ write: 0,
250
+ remove: 0,
251
+ clear: 0
252
+ };
253
+ const deferredReads = [];
254
+ const deferredLoadIndexes = [];
255
+ let failingWrites = 0;
256
+ let writeError = null;
257
+ let failingRemoves = 0;
258
+ let removeError = null;
259
+ let loadIndexError = null;
260
+ let isDeferringReads = false;
261
+ let isDeferringLoadIndex = false;
262
+ const clone = (value) => structuredClone(value);
263
+ const loadIndex = async () => {
264
+ calls.loadIndex++;
265
+ if (loadIndexError !== null) {
266
+ const error = loadIndexError;
267
+ loadIndexError = null;
268
+ throw error;
269
+ }
270
+ const snapshot = Array.from(store.values()).map(({ body: _body, ...meta }) => clone(meta));
271
+ if (isDeferringLoadIndex) await new Promise((resolve) => deferredLoadIndexes.push(resolve));
272
+ return snapshot;
273
+ };
274
+ const read = async (key) => {
275
+ calls.read++;
276
+ const entry = store.get(key);
277
+ const snapshot = entry ? {
278
+ body: clone(entry.body),
279
+ version: entry.version
280
+ } : null;
281
+ if (isDeferringReads) await new Promise((resolve) => deferredReads.push(resolve));
282
+ return snapshot;
283
+ };
284
+ const write = async (entries) => {
285
+ calls.write++;
286
+ if (failingWrites > 0) {
287
+ failingWrites--;
288
+ throw writeError ?? quotaError();
289
+ }
290
+ for (const entry of entries) store.set(entry.key, clone(entry));
291
+ };
292
+ const remove = async (keys) => {
293
+ calls.remove++;
294
+ if (failingRemoves > 0) {
295
+ failingRemoves--;
296
+ throw removeError ?? /* @__PURE__ */ new Error("The removal failed.");
297
+ }
298
+ for (const key of keys) store.delete(key);
299
+ };
300
+ const clear = async () => {
301
+ calls.clear++;
302
+ store.clear();
303
+ };
304
+ return {
305
+ adapter: {
306
+ loadIndex,
307
+ read,
308
+ write,
309
+ remove,
310
+ clear,
311
+ isSupported: true
312
+ },
313
+ seed: (entries) => {
314
+ for (const entry of entries) store.set(entry.key, clone(entry));
315
+ },
316
+ entries: () => Array.from(store.values()).map(clone),
317
+ entry: (key) => {
318
+ const entry = store.get(key);
319
+ return entry ? clone(entry) : void 0;
320
+ },
321
+ calls: () => ({ ...calls }),
322
+ failNextWrites: (count, error) => {
323
+ failingWrites = count;
324
+ writeError = error ?? null;
325
+ },
326
+ failNextRemoves: (count, error) => {
327
+ failingRemoves = count;
328
+ removeError = error ?? null;
329
+ },
330
+ failNextLoadIndex: (error) => {
331
+ loadIndexError = error ?? quotaError();
332
+ },
333
+ deferReads: () => {
334
+ isDeferringReads = true;
335
+ },
336
+ flushReads: async () => {
337
+ isDeferringReads = false;
338
+ const pending = deferredReads.splice(0, deferredReads.length);
339
+ for (const resolve of pending) resolve();
340
+ await Promise.resolve();
341
+ },
342
+ deferLoadIndex: () => {
343
+ isDeferringLoadIndex = true;
344
+ },
345
+ flushLoadIndex: async () => {
346
+ isDeferringLoadIndex = false;
347
+ const pending = deferredLoadIndexes.splice(0, deferredLoadIndexes.length);
348
+ for (const resolve of pending) resolve();
349
+ await Promise.resolve();
350
+ }
351
+ };
409
352
  };
410
-
411
353
  let originalError = null;
412
354
  const filteredError = (...args) => {
413
- const message = args[0];
414
- if (message && typeof message === 'object' && 'name' in message && message.name === 'HttpErrorResponse') {
415
- return;
416
- }
417
- if (typeof message === 'string' && message.includes('Failed to decrypt bearer token')) {
418
- return;
419
- }
420
- if (typeof message === 'string' && message.includes('Failed to extract tokens from')) {
421
- return;
422
- }
423
- originalError?.(...args);
355
+ const message = args[0];
356
+ if (message && typeof message === "object" && "name" in message && message.name === "HttpErrorResponse") return;
357
+ if (typeof message === "string" && message.includes("Failed to decrypt bearer token")) return;
358
+ if (typeof message === "string" && message.includes("Failed to extract tokens from")) return;
359
+ originalError?.(...args);
424
360
  };
425
- // Capture only what is not already the wrapper, so repeated calls reinstall the one filter instead
426
- // of nesting a new closure over the previous one and stranding the pristine handlers.
427
361
  const installConsoleFilters = () => {
428
- if (console.error !== filteredError)
429
- originalError = console.error;
430
- console.error = filteredError;
362
+ if (console.error !== filteredError) originalError = console.error;
363
+ console.error = filteredError;
431
364
  };
432
365
  const restoreConsole = () => {
433
- if (originalError && console.error === filteredError)
434
- console.error = originalError;
435
- originalError = null;
366
+ if (originalError && console.error === filteredError) console.error = originalError;
367
+ originalError = null;
436
368
  };
437
369
  const isFailedRequestError = (error) => error instanceof HttpErrorResponse || isQueryErrorResponse(error);
438
- const requestErrorHandler = {
439
- handleError: (error) => {
440
- if (isFailedRequestError(error))
441
- return;
442
- throw error;
443
- },
444
- };
370
+ const requestErrorHandler = { handleError: (error) => {
371
+ if (isFailedRequestError(error)) return;
372
+ throw error;
373
+ } };
445
374
  const setupQueryTest = (config) => {
446
- const baseUrl = config?.baseUrl ?? 'https://api.test.com';
447
- const name = config?.name ?? 'test';
448
- const mockErrorHandler = config?.mockErrorHandler ?? true;
449
- installConsoleFilters();
450
- const providers = [
451
- provideHttpClient(),
452
- provideHttpClientTesting(),
453
- provideRouter([]),
454
- ];
455
- if (mockErrorHandler) {
456
- providers.push({
457
- provide: ErrorHandler,
458
- useValue: mockErrorHandler === 'all' ? { handleError: () => undefined } : requestErrorHandler,
459
- });
460
- }
461
- TestBed.configureTestingModule({ providers });
462
- const queryClientRef = createQueryClient({ baseUrl, name });
463
- return TestBed.runInInjectionContext(() => {
464
- const { inject } = queryClientRef;
465
- const queryClient = inject();
466
- TestBed.inject(DestroyRef).onDestroy(restoreConsole);
467
- if (!queryClient) {
468
- throw new Error('Failed to create query client in test setup');
469
- }
470
- return {
471
- queryClient,
472
- queryClientRef,
473
- httpClient: TestBed.inject(HttpClient),
474
- httpTesting: TestBed.inject(HttpTestingController),
475
- injector: TestBed.inject(Injector),
476
- baseUrl,
477
- createGet: createGetQuery(queryClientRef),
478
- createPost: createPostQuery(queryClientRef),
479
- createPut: createPutQuery(queryClientRef),
480
- createPatch: createPatchQuery(queryClientRef),
481
- createDelete: createDeleteQuery(queryClientRef),
482
- restoreConsole,
483
- };
484
- });
375
+ const baseUrl = config?.baseUrl ?? "https://api.test.com";
376
+ const name = config?.name ?? "test";
377
+ const mockErrorHandler = config?.mockErrorHandler ?? true;
378
+ installConsoleFilters();
379
+ const providers = [
380
+ provideHttpClient(),
381
+ provideHttpClientTesting(),
382
+ provideRouter([])
383
+ ];
384
+ if (mockErrorHandler) providers.push({
385
+ provide: ErrorHandler,
386
+ useValue: mockErrorHandler === "all" ? { handleError: () => void 0 } : requestErrorHandler
387
+ });
388
+ TestBed.configureTestingModule({ providers });
389
+ const queryClientRef = createQueryClient({
390
+ baseUrl,
391
+ name
392
+ });
393
+ return TestBed.runInInjectionContext(() => {
394
+ const { inject } = queryClientRef;
395
+ const queryClient = inject();
396
+ TestBed.inject(DestroyRef).onDestroy(restoreConsole);
397
+ if (!queryClient) throw new Error("Failed to create query client in test setup");
398
+ return {
399
+ queryClient,
400
+ queryClientRef,
401
+ httpClient: TestBed.inject(HttpClient),
402
+ httpTesting: TestBed.inject(HttpTestingController),
403
+ injector: TestBed.inject(Injector),
404
+ baseUrl,
405
+ createGet: createGetQuery(queryClientRef),
406
+ createPost: createPostQuery(queryClientRef),
407
+ createPut: createPutQuery(queryClientRef),
408
+ createPatch: createPatchQuery(queryClientRef),
409
+ createDelete: createDeleteQuery(queryClientRef),
410
+ restoreConsole
411
+ };
412
+ });
485
413
  };
486
-
487
- /**
488
- * A socket io test double, for specs around `createWebSocketClient` - which takes its `io` factory as
489
- * an option precisely so a spec (or an app that never opens a socket) needs no `socket.io-client`.
490
- *
491
- * Nothing here connects on its own: the client's own `connect()` only flips
492
- * {@link WebSocketTestDouble.state}, and it is {@link WebSocketTestDouble.serverConnect} that fires
493
- * the `connect` listener. That split is what makes "joined a room while disconnected, and it was
494
- * re-joined after a reconnect" testable.
495
- */
496
414
  const createWebSocketTestDouble = () => {
497
- const sent = [];
498
- const delivered = [];
499
- const buffered = [];
500
- const listeners = {
501
- connect: [],
502
- disconnect: [],
503
- connect_error: [],
504
- };
505
- const anyListeners = [];
506
- const outgoingListeners = [];
507
- let connection = null;
508
- let withCredentials = null;
509
- let auth;
510
- const handshakes = [];
511
- let connectRequested = false;
512
- let disconnected = false;
513
- let connected = false;
514
- let active = false;
515
- let connectCalls = 0;
516
- let pingExpired = false;
517
- let recovered = false;
518
- const deliver = (message) => {
519
- for (const listener of outgoingListeners)
520
- listener(message.event, message.data);
521
- delivered.push(message);
522
- };
523
- const recordHandshake = () => {
524
- if (auth)
525
- auth((data) => handshakes.push(data));
526
- else
527
- handshakes.push(null);
528
- };
529
- const disconnect = (reason) => {
530
- connected = false;
531
- pingExpired = false;
532
- for (const listener of listeners.disconnect)
533
- listener(reason);
534
- };
535
- const socket = {
536
- connect: () => {
537
- connectRequested = true;
538
- active = true;
539
- connectCalls++;
540
- },
541
- disconnect: () => {
542
- disconnected = true;
543
- active = false;
544
- if (connected)
545
- disconnect('io client disconnect');
546
- },
547
- emit: (event, data) => {
548
- sent.push({ event, data });
549
- if (connected && !pingExpired)
550
- deliver({ event, data });
551
- else
552
- buffered.push({ event, data });
553
- },
554
- on: (event, listener) => void listeners[event].push(listener),
555
- onAny: (listener) => void anyListeners.push(listener),
556
- onAnyOutgoing: (listener) => void outgoingListeners.push(listener),
557
- get recovered() {
558
- return recovered;
559
- },
560
- get active() {
561
- return active;
562
- },
563
- get sendBuffer() {
564
- return buffered;
565
- },
566
- };
567
- return {
568
- io: (url, options) => {
569
- connection = { url, transports: options.transports };
570
- withCredentials = options.withCredentials;
571
- auth = options.auth;
572
- return socket;
573
- },
574
- connection: () => connection,
575
- withCredentials: () => withCredentials,
576
- handshakes: () => [...handshakes],
577
- sent: () => [...sent],
578
- delivered: () => [...delivered],
579
- state: () => ({ connectRequested, disconnected, active, connectCalls }),
580
- serverConnect: (options) => {
581
- recordHandshake();
582
- active = true;
583
- recovered = options?.recovered ?? false;
584
- connected = true;
585
- pingExpired = false;
586
- for (const message of buffered.splice(0))
587
- deliver(message);
588
- for (const listener of listeners.connect)
589
- listener();
590
- },
591
- serverDisconnect: (options) => {
592
- const reason = options?.reason ?? 'transport close';
593
- if (reason === 'io server disconnect')
594
- active = false;
595
- disconnect(reason);
596
- },
597
- serverRejectHandshake: (message) => {
598
- recordHandshake();
599
- active = false;
600
- for (const listener of listeners.connect_error)
601
- listener(new Error(message ?? 'unauthorized'));
602
- },
603
- serverPingExpire: () => void (pingExpired = true),
604
- serverSend: (message) => {
605
- const frame = JSON.stringify(message);
606
- for (const listener of anyListeners)
607
- listener('message', frame);
608
- },
609
- serverSendRaw: (frame) => {
610
- for (const listener of anyListeners)
611
- listener('message', frame);
612
- },
613
- };
415
+ const sent = [];
416
+ const delivered = [];
417
+ const buffered = [];
418
+ const listeners = {
419
+ connect: [],
420
+ disconnect: [],
421
+ connect_error: []
422
+ };
423
+ const anyListeners = [];
424
+ const outgoingListeners = [];
425
+ let connection = null;
426
+ let withCredentials = null;
427
+ let auth;
428
+ const handshakes = [];
429
+ let connectRequested = false;
430
+ let disconnected = false;
431
+ let connected = false;
432
+ let active = false;
433
+ let connectCalls = 0;
434
+ let pingExpired = false;
435
+ let recovered = false;
436
+ const deliver = (message) => {
437
+ for (const listener of outgoingListeners) listener(message.event, message.data);
438
+ delivered.push(message);
439
+ };
440
+ const recordHandshake = () => {
441
+ if (auth) auth((data) => handshakes.push(data));
442
+ else handshakes.push(null);
443
+ };
444
+ const disconnect = (reason) => {
445
+ connected = false;
446
+ pingExpired = false;
447
+ for (const listener of listeners.disconnect) listener(reason);
448
+ };
449
+ const socket = {
450
+ connect: () => {
451
+ connectRequested = true;
452
+ active = true;
453
+ connectCalls++;
454
+ },
455
+ disconnect: () => {
456
+ disconnected = true;
457
+ active = false;
458
+ if (connected) disconnect("io client disconnect");
459
+ },
460
+ emit: (event, data) => {
461
+ sent.push({
462
+ event,
463
+ data
464
+ });
465
+ if (connected && !pingExpired) deliver({
466
+ event,
467
+ data
468
+ });
469
+ else buffered.push({
470
+ event,
471
+ data
472
+ });
473
+ },
474
+ on: (event, listener) => void listeners[event].push(listener),
475
+ onAny: (listener) => void anyListeners.push(listener),
476
+ onAnyOutgoing: (listener) => void outgoingListeners.push(listener),
477
+ get recovered() {
478
+ return recovered;
479
+ },
480
+ get active() {
481
+ return active;
482
+ },
483
+ get sendBuffer() {
484
+ return buffered;
485
+ }
486
+ };
487
+ return {
488
+ io: (url, options) => {
489
+ connection = {
490
+ url,
491
+ transports: options.transports
492
+ };
493
+ withCredentials = options.withCredentials;
494
+ auth = options.auth;
495
+ return socket;
496
+ },
497
+ connection: () => connection,
498
+ withCredentials: () => withCredentials,
499
+ handshakes: () => [...handshakes],
500
+ sent: () => [...sent],
501
+ delivered: () => [...delivered],
502
+ state: () => ({
503
+ connectRequested,
504
+ disconnected,
505
+ active,
506
+ connectCalls
507
+ }),
508
+ serverConnect: (options) => {
509
+ recordHandshake();
510
+ active = true;
511
+ recovered = options?.recovered ?? false;
512
+ connected = true;
513
+ pingExpired = false;
514
+ for (const message of buffered.splice(0)) deliver(message);
515
+ for (const listener of listeners.connect) listener();
516
+ },
517
+ serverDisconnect: (options) => {
518
+ const reason = options?.reason ?? "transport close";
519
+ if (reason === "io server disconnect") active = false;
520
+ disconnect(reason);
521
+ },
522
+ serverRejectHandshake: (message) => {
523
+ recordHandshake();
524
+ active = false;
525
+ for (const listener of listeners.connect_error) listener(new Error(message ?? "unauthorized"));
526
+ },
527
+ serverPingExpire: () => void (pingExpired = true),
528
+ serverSend: (message) => {
529
+ const frame = JSON.stringify(message);
530
+ for (const listener of anyListeners) listener("message", frame);
531
+ },
532
+ serverSendRaw: (frame) => {
533
+ for (const listener of anyListeners) listener("message", frame);
534
+ }
535
+ };
614
536
  };
615
-
616
537
  const toBase64Url = (json) => {
617
- const encoded = btoa(encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))));
618
- return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
538
+ return btoa(encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
619
539
  };
620
540
  let mintCount = 0;
621
- /**
622
- * Mints an unsigned JWT the bearer auth provider can decode, with `iat`, `exp` and a unique `jti`.
623
- * Hand it to `setTokens()` to put the app's own provider into a session.
624
- *
625
- * @example
626
- * ```ts
627
- * TestBed.runInInjectionContext(() => authProviderRef.inject().setTokens(mintTestToken({ claims: { sub: '1' } }), 'refresh'));
628
- * ```
629
- */
630
541
  const mintTestToken = (options = {}) => {
631
- const { expiresInMs = 15 * 60 * 1000, claims = {} } = options;
632
- const iat = Math.floor(Date.now() / 1000);
633
- const exp = Math.floor((Date.now() + expiresInMs) / 1000);
634
- const header = toBase64Url(JSON.stringify({ alg: 'none', typ: 'JWT' }));
635
- const payload = toBase64Url(JSON.stringify({ iat, exp, jti: ++mintCount, ...claims }));
636
- return `${header}.${payload}.`;
542
+ const { expiresInMs = 9e5, claims = {} } = options;
543
+ const iat = Math.floor(Date.now() / 1e3);
544
+ const exp = Math.floor((Date.now() + expiresInMs) / 1e3);
545
+ return `${toBase64Url(JSON.stringify({
546
+ alg: "none",
547
+ typ: "JWT"
548
+ }))}.${toBase64Url(JSON.stringify({
549
+ iat,
550
+ exp,
551
+ jti: ++mintCount,
552
+ ...claims
553
+ }))}.`;
637
554
  };
638
-
639
- /**
640
- * Generated bundle index. Do not edit.
641
- */
642
-
643
555
  export { createFakeQueryPersistenceStore, createWebSocketTestDouble, expectAndFlush, expectFlushAndWait, flushMultiTabSync, installFakeBroadcastChannel, installFakeWebLocks, mintTestToken, setupAuthTest, setupQueryTest };
644
- //# sourceMappingURL=ethlete-query-testing.mjs.map
556
+
557
+ //# sourceMappingURL=ethlete-query-testing.mjs.map