@ethlete/query 6.0.0-next.24 → 6.0.0-next.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/fesm2022/ethlete-query-testing.mjs +46 -1
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +42 -22
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/package.json +1 -5
- package/types/ethlete-query-testing.d.ts +42 -3
- package/types/ethlete-query.d.ts +68 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @ethlete/query
|
|
2
2
|
|
|
3
|
+
## 6.0.0-next.25
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- [`e9fbdab`](https://github.com/ethlete-io/ethdk/commit/e9fbdab00dfe50a20c1b7fe284ab29fd91a25e77) Thanks [@TomTomB](https://github.com/TomTomB)! - `createWebSocketClient` now takes socket.io's `io` as a required option, so `@ethlete/query` no longer
|
|
8
|
+
pulls `socket.io-client` (~13 kB gz) into apps that never open a socket. New:
|
|
9
|
+
`createWebSocketTestDouble()` in `@ethlete/query/testing`.
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [`fb32936`](https://github.com/ethlete-io/ethdk/commit/fb32936caa749895fc77348a6213d0d3ab6bd056) Thanks [@TomTomB](https://github.com/TomTomB)! - Web socket devtools now record outgoing traffic and can emit test messages, and a request reports how long its last execution took via `subtle.lastDurationMs`.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [`a50a6a0`](https://github.com/ethlete-io/ethdk/commit/a50a6a01e722746cf74fe6181d2da301912528ab) Thanks [@TomTomB](https://github.com/TomTomB)! - Fix `HttpRequestLoadingProgressState.speed`, which reported 1000x the actual rate (and
|
|
18
|
+
`Infinity` on a stalled or re-executed request), and show it in the query devtools.
|
|
19
|
+
|
|
3
20
|
## 6.0.0-next.24
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
|
@@ -400,9 +400,54 @@ const setupQueryTest = (config) => {
|
|
|
400
400
|
return setup;
|
|
401
401
|
};
|
|
402
402
|
|
|
403
|
+
/**
|
|
404
|
+
* A socket io test double, for specs around `createWebSocketClient` - which takes its `io` factory as
|
|
405
|
+
* an option precisely so a spec (or an app that never opens a socket) needs no `socket.io-client`.
|
|
406
|
+
*
|
|
407
|
+
* Nothing here connects on its own: the client's own `connect()` only flips
|
|
408
|
+
* {@link WebSocketTestDouble.state}, and it is {@link WebSocketTestDouble.serverConnect} that fires
|
|
409
|
+
* the `connect` listener. That split is what makes "joined a room while disconnected, and the join
|
|
410
|
+
* was re-sent on connect" testable.
|
|
411
|
+
*/
|
|
412
|
+
const createWebSocketTestDouble = () => {
|
|
413
|
+
const sent = [];
|
|
414
|
+
const listeners = new Map();
|
|
415
|
+
const anyListeners = [];
|
|
416
|
+
let connection = null;
|
|
417
|
+
let connectRequested = false;
|
|
418
|
+
let disconnected = false;
|
|
419
|
+
const socket = {
|
|
420
|
+
connect: () => void (connectRequested = true),
|
|
421
|
+
disconnect: () => void (disconnected = true),
|
|
422
|
+
emit: (event, data) => void sent.push({ event, data }),
|
|
423
|
+
on: (event, listener) => void listeners.set(event, listener),
|
|
424
|
+
onAny: (listener) => void anyListeners.push(listener),
|
|
425
|
+
};
|
|
426
|
+
return {
|
|
427
|
+
io: (url, options) => {
|
|
428
|
+
connection = { url, transports: options.transports };
|
|
429
|
+
return socket;
|
|
430
|
+
},
|
|
431
|
+
connection: () => connection,
|
|
432
|
+
sent: () => [...sent],
|
|
433
|
+
state: () => ({ connectRequested, disconnected }),
|
|
434
|
+
serverConnect: () => listeners.get('connect')?.(),
|
|
435
|
+
serverDisconnect: () => listeners.get('disconnect')?.(),
|
|
436
|
+
serverSend: (message) => {
|
|
437
|
+
const frame = JSON.stringify(message);
|
|
438
|
+
for (const listener of anyListeners)
|
|
439
|
+
listener(frame);
|
|
440
|
+
},
|
|
441
|
+
serverSendRaw: (frame) => {
|
|
442
|
+
for (const listener of anyListeners)
|
|
443
|
+
listener(frame);
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
|
|
403
448
|
/**
|
|
404
449
|
* Generated bundle index. Do not edit.
|
|
405
450
|
*/
|
|
406
451
|
|
|
407
|
-
export { createFakeQueryPersistenceStore, expectAndFlush, expectFlushAndWait, flushMultiTabSync, installFakeBroadcastChannel, installFakeWebLocks, setupAuthTest, setupQueryTest };
|
|
452
|
+
export { createFakeQueryPersistenceStore, createWebSocketTestDouble, expectAndFlush, expectFlushAndWait, flushMultiTabSync, installFakeBroadcastChannel, installFakeWebLocks, setupAuthTest, setupQueryTest };
|
|
408
453
|
//# sourceMappingURL=ethlete-query-testing.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ethlete-query-testing.mjs","sources":["../../../../libs/query/testing/query-test-utils.ts","../../../../libs/query/testing/auth-test-utils.ts","../../../../libs/query/testing/multi-tab-test-utils.ts","../../../../libs/query/testing/persistence-test-utils.ts","../../../../libs/query/testing/query-test-setup.ts","../../../../libs/query/testing/ethlete-query-testing.ts"],"sourcesContent":["import { HttpTestingController } from '@angular/common/http/testing';\nimport { TestBed } from '@angular/core/testing';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\nexport const expectAndFlush = <T = unknown>(\n httpTesting: HttpTestingController,\n url: string,\n response: T,\n statusOpts?: { status: number; statusText: string },\n): void => {\n const req = httpTesting.expectOne(url);\n if (statusOpts) {\n req.flush(response as any, statusOpts);\n } else {\n req.flush(response as any);\n }\n};\n\nexport const expectFlushAndWait = <T = unknown>(\n httpTesting: HttpTestingController,\n url: string,\n response: T,\n statusOpts?: { status: number; statusText: string },\n): void => {\n expectAndFlush(httpTesting, url, response, statusOpts);\n TestBed.tick();\n};\n","import { DestroyRef } from '@angular/core';\nimport { TestBed } from '@angular/core/testing';\nimport {\n AnyQueryBuilder,\n AuthQueryBuilder,\n BearerAuthProvider,\n BearerAuthProviderFeatureContext,\n createBearerAuthProvider,\n TokenRefreshQueryBuilder,\n TokenRefreshQueryConfig,\n withAuthenticationQuery,\n withRefreshQuery,\n} from '@ethlete/query';\nimport { QueryTestSetup } from './query-test-setup';\nimport { expectFlushAndWait } from './query-test-utils';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyFeatureBuilder = (context: BearerAuthProviderFeatureContext<any, readonly AnyQueryBuilder[]>) => {\n type: string;\n instance: unknown;\n};\n\nexport type AuthTestSetupConfig<\n TLoginArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TRefreshArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TFeatures extends readonly AnyFeatureBuilder[] = [],\n TBearerData = unknown,\n> = {\n /** The query test setup instance */\n querySetup: QueryTestSetup;\n /** Login endpoint path. Default: '/auth/login' */\n loginPath?: string;\n /** Refresh endpoint path. Default: '/auth/refresh' */\n refreshPath?: string;\n /** Whether to enable auto-retry on 401. Default: false */\n autoRetryOn401?: boolean;\n /** Function to extract tokens from login response */\n extractLoginTokens?: (response: TLoginArgs['response']) => { accessToken: string; refreshToken: string };\n /** Function to extract tokens from refresh response */\n extractRefreshTokens?: (response: TRefreshArgs['response']) => { accessToken: string; refreshToken: string };\n /** Feature builders for additional auth functionality */\n features?: [...TFeatures];\n /** Custom bearer decrypt function for testing */\n bearerDecryptFn?: (token: string) => TBearerData;\n /** Refresh strategy configuration. See TokenRefreshQueryConfig for details */\n refreshStrategy?: TokenRefreshQueryConfig<TRefreshArgs>['refreshStrategy'];\n /** Minimum refresh interval in milliseconds */\n minRefreshInterval?: number;\n /** Whether to refresh if token is already expired */\n refreshIfExpired?: boolean;\n /** Property name for expiration in JWT */\n expiresInPropertyName?: string;\n};\n\nexport type AuthTestSetup<\n TLoginArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TRefreshArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TFeatures extends readonly AnyFeatureBuilder[] = [],\n TBearerData = unknown,\n> = {\n /** The bearer auth provider instance */\n auth: BearerAuthProvider<\n [AuthQueryBuilder<'login', TLoginArgs>, TokenRefreshQueryBuilder<'refresh', TRefreshArgs>],\n TFeatures,\n TBearerData\n >;\n /** Helper to login a user and flush the HTTP request */\n login: (credentials: TLoginArgs['body'], response: TLoginArgs['response']) => void;\n /** Helper to trigger a refresh and flush the HTTP request */\n refresh: (token: string, response: TRefreshArgs['response']) => void;\n /** Helper to make a secure request that requires authentication */\n makeSecureRequest: (route: string) => void;\n};\n\nexport const setupAuthTest = <\n TLoginArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TRefreshArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TFeatures extends readonly AnyFeatureBuilder[] = readonly AnyFeatureBuilder[],\n TBearerData = unknown,\n>(\n config: AuthTestSetupConfig<TLoginArgs, TRefreshArgs, TFeatures, TBearerData>,\n): AuthTestSetup<TLoginArgs, TRefreshArgs, TFeatures, TBearerData> => {\n const {\n querySetup,\n loginPath = '/auth/login',\n refreshPath = '/auth/refresh',\n autoRetryOn401 = false,\n extractLoginTokens = (response) => ({\n accessToken: response.accessToken,\n refreshToken: response.refreshToken,\n }),\n extractRefreshTokens = (response) => ({\n accessToken: response.accessToken,\n refreshToken: response.refreshToken,\n }),\n features,\n bearerDecryptFn,\n refreshStrategy,\n minRefreshInterval,\n refreshIfExpired,\n expiresInPropertyName,\n } = config;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const login = querySetup.createPost<TLoginArgs>(loginPath as any, {} as any) as any;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const refresh = querySetup.createPost<TRefreshArgs>(refreshPath as any, {} as any) as any;\n\n const { inject: injectAuth } = createBearerAuthProvider({\n name: 'test-auth',\n queryClientRef: querySetup.queryClientRef,\n queries: [\n withAuthenticationQuery('login', {\n queryCreator: login,\n extractTokens: extractLoginTokens,\n }),\n withRefreshQuery('refresh', {\n queryCreator: refresh,\n extractTokens: extractRefreshTokens,\n autoRetryOn401,\n refreshStrategy,\n minRefreshInterval,\n refreshIfExpired,\n expiresInPropertyName,\n }),\n ] as [AuthQueryBuilder<'login', TLoginArgs>, TokenRefreshQueryBuilder<'refresh', TRefreshArgs>],\n features: (features ?? ([] as const)) as unknown as TFeatures,\n bearerDecryptFn,\n });\n\n const auth = TestBed.runInInjectionContext(() => {\n const provider = injectAuth();\n if (!provider) {\n throw new Error('Failed to create auth provider in test setup');\n }\n return provider;\n });\n\n const loginHelper = (credentials: TLoginArgs['body'], response: TLoginArgs['response']) => {\n TestBed.runInInjectionContext(() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n auth.queries.login.execute({ body: credentials } as any);\n });\n const fullUrl = `${querySetup.baseUrl}${loginPath}`;\n expectFlushAndWait(querySetup.httpTesting, fullUrl, response);\n };\n\n const refreshHelper = (token: string, response: TRefreshArgs['response']) => {\n TestBed.runInInjectionContext(() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n auth.queries.refresh.execute({ body: { token } as any } as any);\n });\n const fullUrl = `${querySetup.baseUrl}${refreshPath}`;\n expectFlushAndWait(querySetup.httpTesting, fullUrl, response);\n };\n\n const makeSecureRequest = (route: string) => {\n TestBed.runInInjectionContext(() => {\n querySetup.queryClient.repository.request({\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n route: route as any,\n method: 'GET',\n isSecure: true,\n consumerDestroyRef: TestBed.inject(DestroyRef),\n });\n });\n };\n\n return {\n auth,\n login: loginHelper,\n refresh: refreshHelper,\n makeSecureRequest,\n };\n};\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/**\n * Test doubles for the two browser APIs the query client's multi-tab sync is built on:\n * `BroadcastChannel` and the Web Locks API (`navigator.locks`).\n *\n * Both fakes are process-wide and shared between every consumer created while they are installed,\n * which is exactly what makes \"two tabs\" testable: create two query clients in one test and they\n * talk to each other over the fake bus and contend for the same fake locks, going through the real\n * production code path on both sides.\n *\n * Neither jsdom nor happy-dom can stand in here - jsdom ships a `BroadcastChannel` that never\n * delivers to other instances, and no `navigator.locks` at all.\n */\n\n/** Something posted on the fake bus, as seen by a receiver. */\nexport type FakeBroadcastMessage = {\n /** The channel name it was posted on. */\n channel: string;\n\n /** The structured-cloned payload. */\n data: unknown;\n};\n\nexport type FakeBroadcastChannelHandle = {\n /** Every message posted on the bus, in order, across all channel names. */\n posted: FakeBroadcastMessage[];\n\n /** Uninstalls the fake and restores whatever `BroadcastChannel` was there before. */\n restore: () => void;\n};\n\ntype FakeChannelInstance = {\n name: string;\n closed: boolean;\n onmessage: ((event: MessageEvent<unknown>) => void) | null;\n};\n\n/**\n * Replaces `globalThis.BroadcastChannel` with an in-memory bus.\n *\n * Messages are structured-cloned at post time (so a non-cloneable payload throws exactly like the\n * real API does), delivered on a microtask (never synchronously), and never delivered back to the\n * channel that posted them - all three matching browser behavior. `await Promise.resolve()` in a\n * spec is enough to flush delivery.\n */\nexport const installFakeBroadcastChannel = (): FakeBroadcastChannelHandle => {\n const original = (globalThis as any).BroadcastChannel;\n const instances = new Set<FakeChannelInstance>();\n const posted: FakeBroadcastMessage[] = [];\n\n class FakeBroadcastChannel implements FakeChannelInstance {\n closed = false;\n onmessage: ((event: MessageEvent<unknown>) => void) | null = null;\n\n constructor(public name: string) {\n instances.add(this);\n }\n\n postMessage(data: unknown) {\n if (this.closed) throw new DOMException('Channel is closed', 'InvalidStateError');\n\n const clone = structuredClone(data);\n\n posted.push({ channel: this.name, data: clone });\n\n for (const instance of instances) {\n if (instance === this || instance.closed || instance.name !== this.name) continue;\n\n queueMicrotask(() => {\n if (instance.closed) return;\n\n instance.onmessage?.({ data: structuredClone(clone) } as MessageEvent<unknown>);\n });\n }\n }\n\n close() {\n this.closed = true;\n instances.delete(this);\n }\n\n addEventListener() {\n throw new Error('The fake BroadcastChannel only supports `onmessage`.');\n }\n }\n\n (globalThis as any).BroadcastChannel = FakeBroadcastChannel;\n\n return {\n posted,\n restore: () => {\n for (const instance of [...instances]) {\n instance.closed = true;\n }\n\n instances.clear();\n (globalThis as any).BroadcastChannel = original;\n },\n };\n};\n\nexport type FakeWebLocksHandle = {\n /** The names currently held, in no particular order. */\n heldNames: () => string[];\n\n /** The names with at least one request queued behind the holder. */\n pendingNames: () => string[];\n\n /** Uninstalls the fake and restores whatever `navigator.locks` was there before. */\n restore: () => void;\n};\n\ntype FakeLockWaiter = {\n callback: (lock: Lock) => unknown;\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n granted: boolean;\n};\n\n/**\n * Installs an in-memory `navigator.locks` implementing exclusive locks: one holder per name, the\n * rest queued FIFO, granted on a microtask (never synchronously), released when the callback's\n * promise settles. `signal` is honored for queued requests and - as in the real API - ignored once a\n * request has been granted.\n *\n * `mode: 'shared'`, `ifAvailable` and `steal` are not implemented; nothing in the library uses them.\n */\nexport const installFakeWebLocks = (): FakeWebLocksHandle => {\n const originalDescriptor = Object.getOwnPropertyDescriptor(navigator, 'locks');\n const holders = new Map<string, FakeLockWaiter>();\n const queues = new Map<string, FakeLockWaiter[]>();\n\n const abortError = () => new DOMException('The lock request was aborted', 'AbortError');\n\n const pump = (name: string) => {\n if (holders.has(name)) return;\n\n const waiter = queues.get(name)?.shift();\n\n if (!waiter) return;\n\n waiter.granted = true;\n holders.set(name, waiter);\n\n const settle = (settleWaiter: () => void) => {\n holders.delete(name);\n settleWaiter();\n pump(name);\n };\n\n // `pump` itself only ever runs from a microtask, so calling the callback right here keeps the\n // grant exactly one microtask away from the `request()` call - which is what makes a single\n // `flushMultiTabSync()` per hop enough in specs.\n try {\n Promise.resolve(waiter.callback({ name, mode: 'exclusive' } as Lock)).then(\n (value) => settle(() => waiter.resolve(value)),\n (error) => settle(() => waiter.reject(error)),\n );\n } catch (error) {\n settle(() => waiter.reject(error));\n }\n };\n\n const request = (name: string, optionsOrCallback: unknown, maybeCallback?: unknown) => {\n const options = (typeof optionsOrCallback === 'function' ? {} : (optionsOrCallback ?? {})) as LockOptions;\n const callback = (typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback) as (\n lock: Lock,\n ) => unknown;\n\n return new Promise((resolve, reject) => {\n if (options.signal?.aborted) {\n reject(abortError());\n\n return;\n }\n\n const waiter: FakeLockWaiter = { callback, resolve, reject, granted: false };\n\n options.signal?.addEventListener('abort', () => {\n if (waiter.granted) return;\n\n const queue = queues.get(name);\n\n if (queue)\n queues.set(\n name,\n queue.filter((entry) => entry !== waiter),\n );\n\n reject(abortError());\n });\n\n queues.set(name, [...(queues.get(name) ?? []), waiter]);\n queueMicrotask(() => pump(name));\n });\n };\n\n // Both lists are reported, because a queued request is how a tab that is *not* the holder still\n // shows up - which is what the auth leader election counts its tabs by.\n const query = () =>\n Promise.resolve({\n held: [...holders.keys()].map((name) => ({ name, mode: 'exclusive' as const })),\n pending: [...queues.entries()].flatMap(([name, waiters]) =>\n waiters.map(() => ({ name, mode: 'exclusive' as const })),\n ),\n });\n\n Object.defineProperty(navigator, 'locks', {\n value: {\n request,\n query,\n },\n configurable: true,\n writable: true,\n });\n\n return {\n heldNames: () => [...holders.keys()],\n pendingNames: () => [...queues.entries()].filter(([, queue]) => queue.length > 0).map(([name]) => name),\n restore: () => {\n holders.clear();\n queues.clear();\n\n if (originalDescriptor) {\n Object.defineProperty(navigator, 'locks', originalDescriptor);\n } else {\n delete (navigator as any).locks;\n }\n },\n };\n};\n\n/**\n * Flushes the microtask queue, which is where both fakes schedule their work - message delivery and\n * lock grants. Awaiting it once is enough for a single hop; a grant that has to wait for another\n * tab's release needs one await per hop.\n */\nexport const flushMultiTabSync = () => Promise.resolve();\n","import type { PersistedQueryEntry, PersistedQueryEntryMeta, QueryPersistenceAdapter } from '@ethlete/query';\n\n/**\n * An in-memory stand-in for the query client's persisted response store.\n *\n * jsdom has no IndexedDB at all, so this is how persistence is tested - and because the storage\n * adapter is a documented seam rather than a mock of one, a spec running against this exercises the\n * real engine, repository and request code on both the write and the read side.\n *\n * Everything resolves on a microtask, never synchronously, matching the real thing: `await\n * Promise.resolve()` (or `flushMicrotasks`) is what lets a hydration land in a spec.\n */\nexport type FakeQueryPersistenceStoreHandle = {\n /** Pass this as `persistence: { adapter }`. Two clients sharing one handle share one store. */\n adapter: QueryPersistenceAdapter;\n\n /** Pre-populates the store, as if a previous session had written these entries. */\n seed: (entries: PersistedQueryEntry[]) => void;\n\n /** Every entry currently held, in insertion order. */\n entries: () => PersistedQueryEntry[];\n\n /** The entry held under `key`, or `undefined`. */\n entry: (key: string) => PersistedQueryEntry | undefined;\n\n /** How often each adapter method has been called. */\n calls: () => Record<'loadIndex' | 'read' | 'write' | 'remove' | 'clear', number>;\n\n /** Makes the next `count` writes reject - a full disk, by default. */\n failNextWrites: (count: number, error?: unknown) => void;\n\n /** Makes the next `loadIndex` reject. */\n failNextLoadIndex: (error?: unknown) => void;\n\n /**\n * Holds every subsequent `read` pending until {@link flushReads} is called, for testing the window\n * between a cold mount and its hydration (a slow disk, or a mount that happened before startup\n * finished).\n */\n deferReads: () => void;\n\n /** Settles the reads held by {@link deferReads} and stops deferring. */\n flushReads: () => Promise<void>;\n};\n\nconst quotaError = () => new DOMException('The quota has been exceeded.', 'QuotaExceededError');\n\nexport const createFakeQueryPersistenceStore = (): FakeQueryPersistenceStoreHandle => {\n const store = new Map<string, PersistedQueryEntry>();\n const calls = { loadIndex: 0, read: 0, write: 0, remove: 0, clear: 0 };\n const deferredReads: Array<() => void> = [];\n\n let failingWrites = 0;\n let writeError: unknown = null;\n let loadIndexError: unknown = null;\n let isDeferringReads = false;\n\n // Cloning on the way in and on the way out is not pedantry: a real store hands back a copy, so a\n // spec (or the engine) mutating a hydrated body must not be able to change what is \"on disk\".\n const clone = <T>(value: T): T => structuredClone(value);\n\n const loadIndex = async () => {\n calls.loadIndex++;\n\n if (loadIndexError !== null) {\n const error = loadIndexError;\n loadIndexError = null;\n\n throw error;\n }\n\n return Array.from(store.values()).map(({ body: _body, ...meta }) => clone(meta) satisfies PersistedQueryEntryMeta);\n };\n\n const read = async (key: string) => {\n calls.read++;\n\n if (isDeferringReads) {\n await new Promise<void>((resolve) => deferredReads.push(resolve));\n }\n\n const entry = store.get(key);\n\n return entry ? { body: clone(entry.body) } : null;\n };\n\n const write = async (entries: PersistedQueryEntry[]) => {\n calls.write++;\n\n if (failingWrites > 0) {\n failingWrites--;\n\n throw writeError ?? quotaError();\n }\n\n for (const entry of entries) {\n store.set(entry.key, clone(entry));\n }\n };\n\n const remove = async (keys: string[]) => {\n calls.remove++;\n\n for (const key of keys) {\n store.delete(key);\n }\n };\n\n const clear = async () => {\n calls.clear++;\n store.clear();\n };\n\n return {\n adapter: { loadIndex, read, write, remove, clear, isSupported: true },\n seed: (entries) => {\n for (const entry of entries) {\n store.set(entry.key, clone(entry));\n }\n },\n entries: () => Array.from(store.values()).map(clone),\n entry: (key) => {\n const entry = store.get(key);\n\n return entry ? clone(entry) : undefined;\n },\n calls: () => ({ ...calls }),\n failNextWrites: (count, error) => {\n failingWrites = count;\n writeError = error ?? null;\n },\n failNextLoadIndex: (error) => {\n loadIndexError = error ?? quotaError();\n },\n deferReads: () => {\n isDeferringReads = true;\n },\n flushReads: async () => {\n isDeferringReads = false;\n\n const pending = deferredReads.splice(0, deferredReads.length);\n\n for (const resolve of pending) {\n resolve();\n }\n\n await Promise.resolve();\n },\n };\n};\n","import { HttpClient, provideHttpClient } from '@angular/common/http';\nimport { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';\nimport { EnvironmentProviders, ErrorHandler, Injector } from '@angular/core';\nimport { TestBed } from '@angular/core/testing';\nimport { provideRouter } from '@angular/router';\nimport {\n AnyCreateQueryClientResult,\n createDeleteQuery,\n createGetQuery,\n createPatchQuery,\n createPostQuery,\n createPutQuery,\n createQueryClient,\n QueryClientRef,\n} from '@ethlete/query';\n\nexport type QueryTestSetup = {\n queryClient: NonNullable<ReturnType<AnyCreateQueryClientResult['inject']>>;\n queryClientRef: QueryClientRef;\n httpClient: HttpClient;\n httpTesting: HttpTestingController;\n injector: Injector;\n baseUrl: string;\n createGet: ReturnType<typeof createGetQuery>;\n createPost: ReturnType<typeof createPostQuery>;\n createPut: ReturnType<typeof createPutQuery>;\n createPatch: ReturnType<typeof createPatchQuery>;\n createDelete: ReturnType<typeof createDeleteQuery>;\n};\n\nexport type QueryTestSetupConfig = {\n baseUrl?: string;\n name?: string;\n mockErrorHandler?: boolean;\n};\n\nexport const setupQueryTest = (config?: QueryTestSetupConfig): QueryTestSetup => {\n const baseUrl = config?.baseUrl ?? 'https://api.test.com';\n const name = config?.name ?? 'test';\n const mockErrorHandler = config?.mockErrorHandler !== false;\n\n // Suppress console.warn for auth-related warnings during tests\n const originalWarn = console.warn;\n console.warn = (...args: unknown[]) => {\n const message = args[0];\n if (typeof message === 'string' && message.includes('auto-refresh')) {\n return; // Suppress auto-refresh warnings\n }\n originalWarn(...args);\n };\n\n // Suppress console.error for expected error scenarios during tests\n const originalError = console.error;\n console.error = (...args: unknown[]) => {\n const message = args[0];\n // Suppress HttpErrorResponse logs\n if (message && typeof message === 'object' && 'name' in message && message.name === 'HttpErrorResponse') {\n return;\n }\n // Suppress bearer token decryption errors\n if (typeof message === 'string' && message.includes('Failed to decrypt bearer token')) {\n return;\n }\n // Suppress token extraction errors\n if (typeof message === 'string' && message.includes('Failed to extract tokens from')) {\n return;\n }\n originalError(...args);\n };\n\n const providers: (EnvironmentProviders | object)[] = [\n provideHttpClient(),\n provideHttpClientTesting(),\n provideRouter([]),\n ];\n\n if (mockErrorHandler) {\n providers.push({\n provide: ErrorHandler,\n useValue: { handleError: () => undefined },\n });\n }\n\n TestBed.configureTestingModule({ providers });\n\n const queryClientRef = createQueryClient({ baseUrl, name });\n\n const setup = TestBed.runInInjectionContext(() => {\n const { inject } = queryClientRef;\n const queryClient = inject();\n\n if (!queryClient) {\n throw new Error('Failed to create query client in test setup');\n }\n\n return {\n queryClient,\n queryClientRef,\n httpClient: TestBed.inject(HttpClient),\n httpTesting: TestBed.inject(HttpTestingController),\n injector: TestBed.inject(Injector),\n baseUrl,\n createGet: createGetQuery(queryClientRef),\n createPost: createPostQuery(queryClientRef),\n createPut: createPutQuery(queryClientRef),\n createPatch: createPatchQuery(queryClientRef),\n createDelete: createDeleteQuery(queryClientRef),\n };\n });\n\n // Restore console.warn and console.error when tests are done\n const restoreConsole = () => {\n console.warn = originalWarn;\n console.error = originalError;\n };\n\n // Store the restore function for cleanup\n (setup as typeof setup & { _restoreConsole: () => void })._restoreConsole = restoreConsole;\n\n return setup;\n};\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ethlete-query-testing.mjs","sources":["../../../../libs/query/testing/query-test-utils.ts","../../../../libs/query/testing/auth-test-utils.ts","../../../../libs/query/testing/multi-tab-test-utils.ts","../../../../libs/query/testing/persistence-test-utils.ts","../../../../libs/query/testing/query-test-setup.ts","../../../../libs/query/testing/web-socket-test-utils.ts","../../../../libs/query/testing/ethlete-query-testing.ts"],"sourcesContent":["import { HttpTestingController } from '@angular/common/http/testing';\nimport { TestBed } from '@angular/core/testing';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\nexport const expectAndFlush = <T = unknown>(\n httpTesting: HttpTestingController,\n url: string,\n response: T,\n statusOpts?: { status: number; statusText: string },\n): void => {\n const req = httpTesting.expectOne(url);\n if (statusOpts) {\n req.flush(response as any, statusOpts);\n } else {\n req.flush(response as any);\n }\n};\n\nexport const expectFlushAndWait = <T = unknown>(\n httpTesting: HttpTestingController,\n url: string,\n response: T,\n statusOpts?: { status: number; statusText: string },\n): void => {\n expectAndFlush(httpTesting, url, response, statusOpts);\n TestBed.tick();\n};\n","import { DestroyRef } from '@angular/core';\nimport { TestBed } from '@angular/core/testing';\nimport {\n AnyQueryBuilder,\n AuthQueryBuilder,\n BearerAuthProvider,\n BearerAuthProviderFeatureContext,\n createBearerAuthProvider,\n TokenRefreshQueryBuilder,\n TokenRefreshQueryConfig,\n withAuthenticationQuery,\n withRefreshQuery,\n} from '@ethlete/query';\nimport { QueryTestSetup } from './query-test-setup';\nimport { expectFlushAndWait } from './query-test-utils';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyFeatureBuilder = (context: BearerAuthProviderFeatureContext<any, readonly AnyQueryBuilder[]>) => {\n type: string;\n instance: unknown;\n};\n\nexport type AuthTestSetupConfig<\n TLoginArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TRefreshArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TFeatures extends readonly AnyFeatureBuilder[] = [],\n TBearerData = unknown,\n> = {\n /** The query test setup instance */\n querySetup: QueryTestSetup;\n /** Login endpoint path. Default: '/auth/login' */\n loginPath?: string;\n /** Refresh endpoint path. Default: '/auth/refresh' */\n refreshPath?: string;\n /** Whether to enable auto-retry on 401. Default: false */\n autoRetryOn401?: boolean;\n /** Function to extract tokens from login response */\n extractLoginTokens?: (response: TLoginArgs['response']) => { accessToken: string; refreshToken: string };\n /** Function to extract tokens from refresh response */\n extractRefreshTokens?: (response: TRefreshArgs['response']) => { accessToken: string; refreshToken: string };\n /** Feature builders for additional auth functionality */\n features?: [...TFeatures];\n /** Custom bearer decrypt function for testing */\n bearerDecryptFn?: (token: string) => TBearerData;\n /** Refresh strategy configuration. See TokenRefreshQueryConfig for details */\n refreshStrategy?: TokenRefreshQueryConfig<TRefreshArgs>['refreshStrategy'];\n /** Minimum refresh interval in milliseconds */\n minRefreshInterval?: number;\n /** Whether to refresh if token is already expired */\n refreshIfExpired?: boolean;\n /** Property name for expiration in JWT */\n expiresInPropertyName?: string;\n};\n\nexport type AuthTestSetup<\n TLoginArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TRefreshArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TFeatures extends readonly AnyFeatureBuilder[] = [],\n TBearerData = unknown,\n> = {\n /** The bearer auth provider instance */\n auth: BearerAuthProvider<\n [AuthQueryBuilder<'login', TLoginArgs>, TokenRefreshQueryBuilder<'refresh', TRefreshArgs>],\n TFeatures,\n TBearerData\n >;\n /** Helper to login a user and flush the HTTP request */\n login: (credentials: TLoginArgs['body'], response: TLoginArgs['response']) => void;\n /** Helper to trigger a refresh and flush the HTTP request */\n refresh: (token: string, response: TRefreshArgs['response']) => void;\n /** Helper to make a secure request that requires authentication */\n makeSecureRequest: (route: string) => void;\n};\n\nexport const setupAuthTest = <\n TLoginArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TRefreshArgs extends { body: Record<string, unknown>; response: { accessToken: string; refreshToken: string } } = {\n body: Record<string, unknown>;\n response: { accessToken: string; refreshToken: string };\n },\n TFeatures extends readonly AnyFeatureBuilder[] = readonly AnyFeatureBuilder[],\n TBearerData = unknown,\n>(\n config: AuthTestSetupConfig<TLoginArgs, TRefreshArgs, TFeatures, TBearerData>,\n): AuthTestSetup<TLoginArgs, TRefreshArgs, TFeatures, TBearerData> => {\n const {\n querySetup,\n loginPath = '/auth/login',\n refreshPath = '/auth/refresh',\n autoRetryOn401 = false,\n extractLoginTokens = (response) => ({\n accessToken: response.accessToken,\n refreshToken: response.refreshToken,\n }),\n extractRefreshTokens = (response) => ({\n accessToken: response.accessToken,\n refreshToken: response.refreshToken,\n }),\n features,\n bearerDecryptFn,\n refreshStrategy,\n minRefreshInterval,\n refreshIfExpired,\n expiresInPropertyName,\n } = config;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const login = querySetup.createPost<TLoginArgs>(loginPath as any, {} as any) as any;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const refresh = querySetup.createPost<TRefreshArgs>(refreshPath as any, {} as any) as any;\n\n const { inject: injectAuth } = createBearerAuthProvider({\n name: 'test-auth',\n queryClientRef: querySetup.queryClientRef,\n queries: [\n withAuthenticationQuery('login', {\n queryCreator: login,\n extractTokens: extractLoginTokens,\n }),\n withRefreshQuery('refresh', {\n queryCreator: refresh,\n extractTokens: extractRefreshTokens,\n autoRetryOn401,\n refreshStrategy,\n minRefreshInterval,\n refreshIfExpired,\n expiresInPropertyName,\n }),\n ] as [AuthQueryBuilder<'login', TLoginArgs>, TokenRefreshQueryBuilder<'refresh', TRefreshArgs>],\n features: (features ?? ([] as const)) as unknown as TFeatures,\n bearerDecryptFn,\n });\n\n const auth = TestBed.runInInjectionContext(() => {\n const provider = injectAuth();\n if (!provider) {\n throw new Error('Failed to create auth provider in test setup');\n }\n return provider;\n });\n\n const loginHelper = (credentials: TLoginArgs['body'], response: TLoginArgs['response']) => {\n TestBed.runInInjectionContext(() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n auth.queries.login.execute({ body: credentials } as any);\n });\n const fullUrl = `${querySetup.baseUrl}${loginPath}`;\n expectFlushAndWait(querySetup.httpTesting, fullUrl, response);\n };\n\n const refreshHelper = (token: string, response: TRefreshArgs['response']) => {\n TestBed.runInInjectionContext(() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n auth.queries.refresh.execute({ body: { token } as any } as any);\n });\n const fullUrl = `${querySetup.baseUrl}${refreshPath}`;\n expectFlushAndWait(querySetup.httpTesting, fullUrl, response);\n };\n\n const makeSecureRequest = (route: string) => {\n TestBed.runInInjectionContext(() => {\n querySetup.queryClient.repository.request({\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n route: route as any,\n method: 'GET',\n isSecure: true,\n consumerDestroyRef: TestBed.inject(DestroyRef),\n });\n });\n };\n\n return {\n auth,\n login: loginHelper,\n refresh: refreshHelper,\n makeSecureRequest,\n };\n};\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/**\n * Test doubles for the two browser APIs the query client's multi-tab sync is built on:\n * `BroadcastChannel` and the Web Locks API (`navigator.locks`).\n *\n * Both fakes are process-wide and shared between every consumer created while they are installed,\n * which is exactly what makes \"two tabs\" testable: create two query clients in one test and they\n * talk to each other over the fake bus and contend for the same fake locks, going through the real\n * production code path on both sides.\n *\n * Neither jsdom nor happy-dom can stand in here - jsdom ships a `BroadcastChannel` that never\n * delivers to other instances, and no `navigator.locks` at all.\n */\n\n/** Something posted on the fake bus, as seen by a receiver. */\nexport type FakeBroadcastMessage = {\n /** The channel name it was posted on. */\n channel: string;\n\n /** The structured-cloned payload. */\n data: unknown;\n};\n\nexport type FakeBroadcastChannelHandle = {\n /** Every message posted on the bus, in order, across all channel names. */\n posted: FakeBroadcastMessage[];\n\n /** Uninstalls the fake and restores whatever `BroadcastChannel` was there before. */\n restore: () => void;\n};\n\ntype FakeChannelInstance = {\n name: string;\n closed: boolean;\n onmessage: ((event: MessageEvent<unknown>) => void) | null;\n};\n\n/**\n * Replaces `globalThis.BroadcastChannel` with an in-memory bus.\n *\n * Messages are structured-cloned at post time (so a non-cloneable payload throws exactly like the\n * real API does), delivered on a microtask (never synchronously), and never delivered back to the\n * channel that posted them - all three matching browser behavior. `await Promise.resolve()` in a\n * spec is enough to flush delivery.\n */\nexport const installFakeBroadcastChannel = (): FakeBroadcastChannelHandle => {\n const original = (globalThis as any).BroadcastChannel;\n const instances = new Set<FakeChannelInstance>();\n const posted: FakeBroadcastMessage[] = [];\n\n class FakeBroadcastChannel implements FakeChannelInstance {\n closed = false;\n onmessage: ((event: MessageEvent<unknown>) => void) | null = null;\n\n constructor(public name: string) {\n instances.add(this);\n }\n\n postMessage(data: unknown) {\n if (this.closed) throw new DOMException('Channel is closed', 'InvalidStateError');\n\n const clone = structuredClone(data);\n\n posted.push({ channel: this.name, data: clone });\n\n for (const instance of instances) {\n if (instance === this || instance.closed || instance.name !== this.name) continue;\n\n queueMicrotask(() => {\n if (instance.closed) return;\n\n instance.onmessage?.({ data: structuredClone(clone) } as MessageEvent<unknown>);\n });\n }\n }\n\n close() {\n this.closed = true;\n instances.delete(this);\n }\n\n addEventListener() {\n throw new Error('The fake BroadcastChannel only supports `onmessage`.');\n }\n }\n\n (globalThis as any).BroadcastChannel = FakeBroadcastChannel;\n\n return {\n posted,\n restore: () => {\n for (const instance of [...instances]) {\n instance.closed = true;\n }\n\n instances.clear();\n (globalThis as any).BroadcastChannel = original;\n },\n };\n};\n\nexport type FakeWebLocksHandle = {\n /** The names currently held, in no particular order. */\n heldNames: () => string[];\n\n /** The names with at least one request queued behind the holder. */\n pendingNames: () => string[];\n\n /** Uninstalls the fake and restores whatever `navigator.locks` was there before. */\n restore: () => void;\n};\n\ntype FakeLockWaiter = {\n callback: (lock: Lock) => unknown;\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n granted: boolean;\n};\n\n/**\n * Installs an in-memory `navigator.locks` implementing exclusive locks: one holder per name, the\n * rest queued FIFO, granted on a microtask (never synchronously), released when the callback's\n * promise settles. `signal` is honored for queued requests and - as in the real API - ignored once a\n * request has been granted.\n *\n * `mode: 'shared'`, `ifAvailable` and `steal` are not implemented; nothing in the library uses them.\n */\nexport const installFakeWebLocks = (): FakeWebLocksHandle => {\n const originalDescriptor = Object.getOwnPropertyDescriptor(navigator, 'locks');\n const holders = new Map<string, FakeLockWaiter>();\n const queues = new Map<string, FakeLockWaiter[]>();\n\n const abortError = () => new DOMException('The lock request was aborted', 'AbortError');\n\n const pump = (name: string) => {\n if (holders.has(name)) return;\n\n const waiter = queues.get(name)?.shift();\n\n if (!waiter) return;\n\n waiter.granted = true;\n holders.set(name, waiter);\n\n const settle = (settleWaiter: () => void) => {\n holders.delete(name);\n settleWaiter();\n pump(name);\n };\n\n // `pump` itself only ever runs from a microtask, so calling the callback right here keeps the\n // grant exactly one microtask away from the `request()` call - which is what makes a single\n // `flushMultiTabSync()` per hop enough in specs.\n try {\n Promise.resolve(waiter.callback({ name, mode: 'exclusive' } as Lock)).then(\n (value) => settle(() => waiter.resolve(value)),\n (error) => settle(() => waiter.reject(error)),\n );\n } catch (error) {\n settle(() => waiter.reject(error));\n }\n };\n\n const request = (name: string, optionsOrCallback: unknown, maybeCallback?: unknown) => {\n const options = (typeof optionsOrCallback === 'function' ? {} : (optionsOrCallback ?? {})) as LockOptions;\n const callback = (typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback) as (\n lock: Lock,\n ) => unknown;\n\n return new Promise((resolve, reject) => {\n if (options.signal?.aborted) {\n reject(abortError());\n\n return;\n }\n\n const waiter: FakeLockWaiter = { callback, resolve, reject, granted: false };\n\n options.signal?.addEventListener('abort', () => {\n if (waiter.granted) return;\n\n const queue = queues.get(name);\n\n if (queue)\n queues.set(\n name,\n queue.filter((entry) => entry !== waiter),\n );\n\n reject(abortError());\n });\n\n queues.set(name, [...(queues.get(name) ?? []), waiter]);\n queueMicrotask(() => pump(name));\n });\n };\n\n // Both lists are reported, because a queued request is how a tab that is *not* the holder still\n // shows up - which is what the auth leader election counts its tabs by.\n const query = () =>\n Promise.resolve({\n held: [...holders.keys()].map((name) => ({ name, mode: 'exclusive' as const })),\n pending: [...queues.entries()].flatMap(([name, waiters]) =>\n waiters.map(() => ({ name, mode: 'exclusive' as const })),\n ),\n });\n\n Object.defineProperty(navigator, 'locks', {\n value: {\n request,\n query,\n },\n configurable: true,\n writable: true,\n });\n\n return {\n heldNames: () => [...holders.keys()],\n pendingNames: () => [...queues.entries()].filter(([, queue]) => queue.length > 0).map(([name]) => name),\n restore: () => {\n holders.clear();\n queues.clear();\n\n if (originalDescriptor) {\n Object.defineProperty(navigator, 'locks', originalDescriptor);\n } else {\n delete (navigator as any).locks;\n }\n },\n };\n};\n\n/**\n * Flushes the microtask queue, which is where both fakes schedule their work - message delivery and\n * lock grants. Awaiting it once is enough for a single hop; a grant that has to wait for another\n * tab's release needs one await per hop.\n */\nexport const flushMultiTabSync = () => Promise.resolve();\n","import type { PersistedQueryEntry, PersistedQueryEntryMeta, QueryPersistenceAdapter } from '@ethlete/query';\n\n/**\n * An in-memory stand-in for the query client's persisted response store.\n *\n * jsdom has no IndexedDB at all, so this is how persistence is tested - and because the storage\n * adapter is a documented seam rather than a mock of one, a spec running against this exercises the\n * real engine, repository and request code on both the write and the read side.\n *\n * Everything resolves on a microtask, never synchronously, matching the real thing: `await\n * Promise.resolve()` (or `flushMicrotasks`) is what lets a hydration land in a spec.\n */\nexport type FakeQueryPersistenceStoreHandle = {\n /** Pass this as `persistence: { adapter }`. Two clients sharing one handle share one store. */\n adapter: QueryPersistenceAdapter;\n\n /** Pre-populates the store, as if a previous session had written these entries. */\n seed: (entries: PersistedQueryEntry[]) => void;\n\n /** Every entry currently held, in insertion order. */\n entries: () => PersistedQueryEntry[];\n\n /** The entry held under `key`, or `undefined`. */\n entry: (key: string) => PersistedQueryEntry | undefined;\n\n /** How often each adapter method has been called. */\n calls: () => Record<'loadIndex' | 'read' | 'write' | 'remove' | 'clear', number>;\n\n /** Makes the next `count` writes reject - a full disk, by default. */\n failNextWrites: (count: number, error?: unknown) => void;\n\n /** Makes the next `loadIndex` reject. */\n failNextLoadIndex: (error?: unknown) => void;\n\n /**\n * Holds every subsequent `read` pending until {@link flushReads} is called, for testing the window\n * between a cold mount and its hydration (a slow disk, or a mount that happened before startup\n * finished).\n */\n deferReads: () => void;\n\n /** Settles the reads held by {@link deferReads} and stops deferring. */\n flushReads: () => Promise<void>;\n};\n\nconst quotaError = () => new DOMException('The quota has been exceeded.', 'QuotaExceededError');\n\nexport const createFakeQueryPersistenceStore = (): FakeQueryPersistenceStoreHandle => {\n const store = new Map<string, PersistedQueryEntry>();\n const calls = { loadIndex: 0, read: 0, write: 0, remove: 0, clear: 0 };\n const deferredReads: Array<() => void> = [];\n\n let failingWrites = 0;\n let writeError: unknown = null;\n let loadIndexError: unknown = null;\n let isDeferringReads = false;\n\n // Cloning on the way in and on the way out is not pedantry: a real store hands back a copy, so a\n // spec (or the engine) mutating a hydrated body must not be able to change what is \"on disk\".\n const clone = <T>(value: T): T => structuredClone(value);\n\n const loadIndex = async () => {\n calls.loadIndex++;\n\n if (loadIndexError !== null) {\n const error = loadIndexError;\n loadIndexError = null;\n\n throw error;\n }\n\n return Array.from(store.values()).map(({ body: _body, ...meta }) => clone(meta) satisfies PersistedQueryEntryMeta);\n };\n\n const read = async (key: string) => {\n calls.read++;\n\n if (isDeferringReads) {\n await new Promise<void>((resolve) => deferredReads.push(resolve));\n }\n\n const entry = store.get(key);\n\n return entry ? { body: clone(entry.body) } : null;\n };\n\n const write = async (entries: PersistedQueryEntry[]) => {\n calls.write++;\n\n if (failingWrites > 0) {\n failingWrites--;\n\n throw writeError ?? quotaError();\n }\n\n for (const entry of entries) {\n store.set(entry.key, clone(entry));\n }\n };\n\n const remove = async (keys: string[]) => {\n calls.remove++;\n\n for (const key of keys) {\n store.delete(key);\n }\n };\n\n const clear = async () => {\n calls.clear++;\n store.clear();\n };\n\n return {\n adapter: { loadIndex, read, write, remove, clear, isSupported: true },\n seed: (entries) => {\n for (const entry of entries) {\n store.set(entry.key, clone(entry));\n }\n },\n entries: () => Array.from(store.values()).map(clone),\n entry: (key) => {\n const entry = store.get(key);\n\n return entry ? clone(entry) : undefined;\n },\n calls: () => ({ ...calls }),\n failNextWrites: (count, error) => {\n failingWrites = count;\n writeError = error ?? null;\n },\n failNextLoadIndex: (error) => {\n loadIndexError = error ?? quotaError();\n },\n deferReads: () => {\n isDeferringReads = true;\n },\n flushReads: async () => {\n isDeferringReads = false;\n\n const pending = deferredReads.splice(0, deferredReads.length);\n\n for (const resolve of pending) {\n resolve();\n }\n\n await Promise.resolve();\n },\n };\n};\n","import { HttpClient, provideHttpClient } from '@angular/common/http';\nimport { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';\nimport { EnvironmentProviders, ErrorHandler, Injector } from '@angular/core';\nimport { TestBed } from '@angular/core/testing';\nimport { provideRouter } from '@angular/router';\nimport {\n AnyCreateQueryClientResult,\n createDeleteQuery,\n createGetQuery,\n createPatchQuery,\n createPostQuery,\n createPutQuery,\n createQueryClient,\n QueryClientRef,\n} from '@ethlete/query';\n\nexport type QueryTestSetup = {\n queryClient: NonNullable<ReturnType<AnyCreateQueryClientResult['inject']>>;\n queryClientRef: QueryClientRef;\n httpClient: HttpClient;\n httpTesting: HttpTestingController;\n injector: Injector;\n baseUrl: string;\n createGet: ReturnType<typeof createGetQuery>;\n createPost: ReturnType<typeof createPostQuery>;\n createPut: ReturnType<typeof createPutQuery>;\n createPatch: ReturnType<typeof createPatchQuery>;\n createDelete: ReturnType<typeof createDeleteQuery>;\n};\n\nexport type QueryTestSetupConfig = {\n baseUrl?: string;\n name?: string;\n mockErrorHandler?: boolean;\n};\n\nexport const setupQueryTest = (config?: QueryTestSetupConfig): QueryTestSetup => {\n const baseUrl = config?.baseUrl ?? 'https://api.test.com';\n const name = config?.name ?? 'test';\n const mockErrorHandler = config?.mockErrorHandler !== false;\n\n // Suppress console.warn for auth-related warnings during tests\n const originalWarn = console.warn;\n console.warn = (...args: unknown[]) => {\n const message = args[0];\n if (typeof message === 'string' && message.includes('auto-refresh')) {\n return; // Suppress auto-refresh warnings\n }\n originalWarn(...args);\n };\n\n // Suppress console.error for expected error scenarios during tests\n const originalError = console.error;\n console.error = (...args: unknown[]) => {\n const message = args[0];\n // Suppress HttpErrorResponse logs\n if (message && typeof message === 'object' && 'name' in message && message.name === 'HttpErrorResponse') {\n return;\n }\n // Suppress bearer token decryption errors\n if (typeof message === 'string' && message.includes('Failed to decrypt bearer token')) {\n return;\n }\n // Suppress token extraction errors\n if (typeof message === 'string' && message.includes('Failed to extract tokens from')) {\n return;\n }\n originalError(...args);\n };\n\n const providers: (EnvironmentProviders | object)[] = [\n provideHttpClient(),\n provideHttpClientTesting(),\n provideRouter([]),\n ];\n\n if (mockErrorHandler) {\n providers.push({\n provide: ErrorHandler,\n useValue: { handleError: () => undefined },\n });\n }\n\n TestBed.configureTestingModule({ providers });\n\n const queryClientRef = createQueryClient({ baseUrl, name });\n\n const setup = TestBed.runInInjectionContext(() => {\n const { inject } = queryClientRef;\n const queryClient = inject();\n\n if (!queryClient) {\n throw new Error('Failed to create query client in test setup');\n }\n\n return {\n queryClient,\n queryClientRef,\n httpClient: TestBed.inject(HttpClient),\n httpTesting: TestBed.inject(HttpTestingController),\n injector: TestBed.inject(Injector),\n baseUrl,\n createGet: createGetQuery(queryClientRef),\n createPost: createPostQuery(queryClientRef),\n createPut: createPutQuery(queryClientRef),\n createPatch: createPatchQuery(queryClientRef),\n createDelete: createDeleteQuery(queryClientRef),\n };\n });\n\n // Restore console.warn and console.error when tests are done\n const restoreConsole = () => {\n console.warn = originalWarn;\n console.error = originalError;\n };\n\n // Store the restore function for cleanup\n (setup as typeof setup & { _restoreConsole: () => void })._restoreConsole = restoreConsole;\n\n return setup;\n};\n","import type { SocketMessageView, WebSocketClientIo, WebSocketClientSocket } from '@ethlete/query';\n\n/** A scripted stand-in for `socket.io-client`'s `io`, driven from a spec. */\nexport type WebSocketTestDouble = {\n /** Pass this as `createWebSocketClient({ io })`. */\n io: WebSocketClientIo;\n\n /** The url and options the client called the factory with, or `null` until it did. */\n connection: () => { url: string; transports: string[] | undefined } | null;\n\n /** Every message the client sent, newest last - room joins and leaves included. */\n sent: () => { event: string; data: unknown }[];\n\n /** Whether the client asked the socket to connect, and whether it has since disconnected it. */\n state: () => { connectRequested: boolean; disconnected: boolean };\n\n /** Complete the handshake, so the client's `isConnected` turns true and queued rooms re-join. */\n serverConnect: () => void;\n\n /** Drop the connection, so `isConnected` turns false. */\n serverDisconnect: () => void;\n\n /** Deliver a message the way the server would. */\n serverSend: (message: SocketMessageView) => void;\n\n /** Deliver a raw frame, for the malformed-payload path. */\n serverSendRaw: (frame: string) => void;\n};\n\n/**\n * A socket io test double, for specs around `createWebSocketClient` - which takes its `io` factory as\n * an option precisely so a spec (or an app that never opens a socket) needs no `socket.io-client`.\n *\n * Nothing here connects on its own: the client's own `connect()` only flips\n * {@link WebSocketTestDouble.state}, and it is {@link WebSocketTestDouble.serverConnect} that fires\n * the `connect` listener. That split is what makes \"joined a room while disconnected, and the join\n * was re-sent on connect\" testable.\n */\nexport const createWebSocketTestDouble = (): WebSocketTestDouble => {\n const sent: { event: string; data: unknown }[] = [];\n const listeners = new Map<'connect' | 'disconnect', () => void>();\n const anyListeners: ((frame: string) => void)[] = [];\n\n let connection: { url: string; transports: string[] | undefined } | null = null;\n let connectRequested = false;\n let disconnected = false;\n\n const socket: WebSocketClientSocket = {\n connect: () => void (connectRequested = true),\n disconnect: () => void (disconnected = true),\n emit: (event, data) => void sent.push({ event, data }),\n on: (event, listener) => void listeners.set(event, listener),\n onAny: (listener) => void anyListeners.push(listener),\n };\n\n return {\n io: (url, options) => {\n connection = { url, transports: options.transports };\n return socket;\n },\n connection: () => connection,\n sent: () => [...sent],\n state: () => ({ connectRequested, disconnected }),\n serverConnect: () => listeners.get('connect')?.(),\n serverDisconnect: () => listeners.get('disconnect')?.(),\n serverSend: (message) => {\n const frame = JSON.stringify(message);\n for (const listener of anyListeners) listener(frame);\n },\n serverSendRaw: (frame) => {\n for (const listener of anyListeners) listener(frame);\n },\n };\n};\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -10,7 +10,6 @@ import { validateAsync, form } from '@angular/forms/signals';
|
|
|
10
10
|
import * as i1 from '@angular/forms';
|
|
11
11
|
import { FormControl, ReactiveFormsModule, FormGroup } from '@angular/forms';
|
|
12
12
|
import { Router, ActivatedRoute } from '@angular/router';
|
|
13
|
-
import { io } from 'socket.io-client';
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Turns the features applied at creation into their devtools representation.
|
|
@@ -1524,6 +1523,8 @@ const createHttpRequest = (options) => {
|
|
|
1524
1523
|
...(ngDevMode ? [{ debugName: "attempts" }] : /* istanbul ignore next */ []));
|
|
1525
1524
|
const retryState = signal(null, /* @ts-ignore */
|
|
1526
1525
|
...(ngDevMode ? [{ debugName: "retryState" }] : /* istanbul ignore next */ []));
|
|
1526
|
+
const lastDurationMs = signal(null, /* @ts-ignore */
|
|
1527
|
+
...(ngDevMode ? [{ debugName: "lastDurationMs" }] : /* istanbul ignore next */ []));
|
|
1527
1528
|
// NOTE: This must be a plain function, not a `computed`. The freshness check compares against
|
|
1528
1529
|
// `Date.now()`, which is not reactive, so a memoized computed would only ever recompute when
|
|
1529
1530
|
// `expiresIn` changes - once it evaluated to `false` (fresh) it would stay `false` forever, even
|
|
@@ -1621,6 +1622,7 @@ const createHttpRequest = (options) => {
|
|
|
1621
1622
|
currentStreamSubscription.unsubscribe();
|
|
1622
1623
|
lastExecuteTime.set(Date.now());
|
|
1623
1624
|
lastLoadEventTime.set(lastExecuteTime());
|
|
1625
|
+
lastLoadEventAmount.set(0);
|
|
1624
1626
|
loading.set({
|
|
1625
1627
|
executeTime: lastExecuteTime(),
|
|
1626
1628
|
progress: null,
|
|
@@ -1653,6 +1655,7 @@ const createHttpRequest = (options) => {
|
|
|
1653
1655
|
{
|
|
1654
1656
|
loading.set(null);
|
|
1655
1657
|
response.set(event.body);
|
|
1658
|
+
lastDurationMs.set(Date.now() - lastExecuteTime());
|
|
1656
1659
|
const expiresInSeconds = options.cacheAdapter
|
|
1657
1660
|
? options.cacheAdapter(event.headers)
|
|
1658
1661
|
: extractExpiresInSeconds(event.headers);
|
|
@@ -1681,6 +1684,7 @@ const createHttpRequest = (options) => {
|
|
|
1681
1684
|
const errorRes = createQueryErrorResponse(errorResponse);
|
|
1682
1685
|
error.set(errorRes);
|
|
1683
1686
|
loading.set(null);
|
|
1687
|
+
lastDurationMs.set(Date.now() - lastExecuteTime());
|
|
1684
1688
|
const errorEvent = { type: 'error', error: errorRes };
|
|
1685
1689
|
currentEvent.set(errorEvent);
|
|
1686
1690
|
event$.next(errorEvent);
|
|
@@ -1705,15 +1709,21 @@ const createHttpRequest = (options) => {
|
|
|
1705
1709
|
const elapsedTimeSinceLastEvent = currentTime - lastLoadEventTime();
|
|
1706
1710
|
const elapsedTimeSinceLastExecute = currentTime - lastExecuteTime();
|
|
1707
1711
|
const loadedAmount = event.loaded - lastLoadEventAmount();
|
|
1712
|
+
// An event in the same millisecond as the last one, or one that carries no new bytes, has no rate
|
|
1713
|
+
// to report - dividing by either zero yields Infinity or NaN. Such an event also keeps the
|
|
1714
|
+
// baseline where it is, so the next one measures across the wider window instead of restarting.
|
|
1715
|
+
const isMeasurable = elapsedTimeSinceLastEvent > 0 && loadedAmount > 0;
|
|
1708
1716
|
// We only want to calculate speed and remaining time after 2 seconds of the execution
|
|
1709
1717
|
// This is to avoid showing incorrect speed and remaining time when the request is very fast
|
|
1710
|
-
if (elapsedTimeSinceLastExecute > SPEED_BUFFER_TIME_IN_MS) {
|
|
1711
|
-
const
|
|
1712
|
-
progress.speed =
|
|
1713
|
-
progress.remainingTime = Math.round((event.total - event.loaded) /
|
|
1718
|
+
if (isMeasurable && elapsedTimeSinceLastExecute > SPEED_BUFFER_TIME_IN_MS) {
|
|
1719
|
+
const bytesPerSecond = (loadedAmount / elapsedTimeSinceLastEvent) * 1000;
|
|
1720
|
+
progress.speed = bytesPerSecond;
|
|
1721
|
+
progress.remainingTime = Math.round((event.total - event.loaded) / bytesPerSecond) * 1000;
|
|
1722
|
+
}
|
|
1723
|
+
if (isMeasurable) {
|
|
1724
|
+
lastLoadEventTime.set(currentTime);
|
|
1725
|
+
lastLoadEventAmount.set(event.loaded);
|
|
1714
1726
|
}
|
|
1715
|
-
lastLoadEventTime.set(currentTime);
|
|
1716
|
-
lastLoadEventAmount.set(event.loaded);
|
|
1717
1727
|
loading.set(state);
|
|
1718
1728
|
};
|
|
1719
1729
|
const lastExternalResponseAt = signal(null, /* @ts-ignore */
|
|
@@ -1752,6 +1762,7 @@ const createHttpRequest = (options) => {
|
|
|
1752
1762
|
resolveHeaders,
|
|
1753
1763
|
attempts: attempts.asReadonly(),
|
|
1754
1764
|
retryState: retryState.asReadonly(),
|
|
1765
|
+
lastDurationMs: lastDurationMs.asReadonly(),
|
|
1755
1766
|
},
|
|
1756
1767
|
};
|
|
1757
1768
|
return httpRequest;
|
|
@@ -5474,7 +5485,9 @@ const trimRetainedResponses = (runs) => {
|
|
|
5474
5485
|
/**
|
|
5475
5486
|
* The size of a payload in bytes, and whether that size is the one that went over the wire. Only a
|
|
5476
5487
|
* `content-length` header is exact; everything else is measured by serializing the body.
|
|
5477
|
-
*
|
|
5488
|
+
*
|
|
5489
|
+
* Part of the devtools contract - the panel measures the cache and the event log with it. **Not part of
|
|
5490
|
+
* the general public contract.**
|
|
5478
5491
|
*/
|
|
5479
5492
|
const measureQueryDevtoolsPayload = (payload) => {
|
|
5480
5493
|
const contentLength = Number(payload.headers?.get('content-length'));
|
|
@@ -11145,7 +11158,7 @@ const messageMalformed = () => {
|
|
|
11145
11158
|
const MAX_DEVTOOLS_MESSAGES = 100;
|
|
11146
11159
|
const createWebSocketClient = (options) => {
|
|
11147
11160
|
return defineRootProvider(() => {
|
|
11148
|
-
const socket = io(options.url, {
|
|
11161
|
+
const socket = options.io(options.url, {
|
|
11149
11162
|
withCredentials: true,
|
|
11150
11163
|
autoConnect: false,
|
|
11151
11164
|
transports: options.transports,
|
|
@@ -11164,13 +11177,28 @@ const createWebSocketClient = (options) => {
|
|
|
11164
11177
|
if (devtoolsEnabled)
|
|
11165
11178
|
devtoolsRooms.set([...rooms.keys()]);
|
|
11166
11179
|
};
|
|
11180
|
+
const recordDevtoolsMessage = (message) => {
|
|
11181
|
+
if (!devtoolsEnabled)
|
|
11182
|
+
return;
|
|
11183
|
+
devtoolsMessages.update((log) => [{ ...message, id: devtoolsMessageId++, timestamp: Date.now() }, ...log].slice(0, MAX_DEVTOOLS_MESSAGES));
|
|
11184
|
+
};
|
|
11185
|
+
/** Every outgoing message goes through here, so the devtools log covers both directions. */
|
|
11186
|
+
const emit = (message) => {
|
|
11187
|
+
socket.emit(message.event, message.data);
|
|
11188
|
+
recordDevtoolsMessage({
|
|
11189
|
+
room: message.room ?? '',
|
|
11190
|
+
event: message.event,
|
|
11191
|
+
data: message.data,
|
|
11192
|
+
direction: 'out',
|
|
11193
|
+
});
|
|
11194
|
+
};
|
|
11167
11195
|
const joinRoom = (room) => {
|
|
11168
11196
|
const roomFn = typeof room === 'function' ? room : () => room;
|
|
11169
11197
|
const pre = previousSignalValue(computed(() => roomFn()));
|
|
11170
11198
|
const roomData = signal(null, /* @ts-ignore */
|
|
11171
11199
|
...(ngDevMode ? [{ debugName: "roomData" }] : /* istanbul ignore next */ []));
|
|
11172
11200
|
const join = (name) => {
|
|
11173
|
-
|
|
11201
|
+
emit({ event: 'join-room', data: name, room: name });
|
|
11174
11202
|
const existingRoom = rooms.get(name);
|
|
11175
11203
|
if (existingRoom)
|
|
11176
11204
|
return existingRoom;
|
|
@@ -11215,7 +11243,7 @@ const createWebSocketClient = (options) => {
|
|
|
11215
11243
|
if (isDevMode())
|
|
11216
11244
|
throw roomNotJoined(room);
|
|
11217
11245
|
}
|
|
11218
|
-
|
|
11246
|
+
emit({ event: 'leave-room', data: room, room });
|
|
11219
11247
|
rooms.delete(room);
|
|
11220
11248
|
syncDevtoolsRooms();
|
|
11221
11249
|
};
|
|
@@ -11223,7 +11251,7 @@ const createWebSocketClient = (options) => {
|
|
|
11223
11251
|
socket.on('connect', () => {
|
|
11224
11252
|
isConnected.set(true);
|
|
11225
11253
|
for (const room of rooms.keys()) {
|
|
11226
|
-
|
|
11254
|
+
emit({ event: 'join-room', data: room, room });
|
|
11227
11255
|
}
|
|
11228
11256
|
});
|
|
11229
11257
|
socket.on('disconnect', () => isConnected.set(false));
|
|
@@ -11232,16 +11260,7 @@ const createWebSocketClient = (options) => {
|
|
|
11232
11260
|
socket.onAny((data) => {
|
|
11233
11261
|
try {
|
|
11234
11262
|
const json = JSON.parse(data);
|
|
11235
|
-
|
|
11236
|
-
const message = {
|
|
11237
|
-
id: devtoolsMessageId++,
|
|
11238
|
-
timestamp: Date.now(),
|
|
11239
|
-
room: json.room,
|
|
11240
|
-
event: json.event,
|
|
11241
|
-
data: json.data,
|
|
11242
|
-
};
|
|
11243
|
-
devtoolsMessages.update((log) => [message, ...log].slice(0, MAX_DEVTOOLS_MESSAGES));
|
|
11244
|
-
}
|
|
11263
|
+
recordDevtoolsMessage({ room: json.room, event: json.event, data: json.data, direction: 'in' });
|
|
11245
11264
|
const room = rooms.get(json.room);
|
|
11246
11265
|
if (room)
|
|
11247
11266
|
room.latestMessage.set(json);
|
|
@@ -11269,6 +11288,7 @@ const createWebSocketClient = (options) => {
|
|
|
11269
11288
|
connected: isConnected.asReadonly(),
|
|
11270
11289
|
rooms: devtoolsRooms.asReadonly(),
|
|
11271
11290
|
messages: devtoolsMessages.asReadonly(),
|
|
11291
|
+
emit,
|
|
11272
11292
|
};
|
|
11273
11293
|
const unregister = registerQueryDevtoolsEntry({
|
|
11274
11294
|
kind: 'ws-client',
|