@ethlete/query 6.0.0-next.21 → 6.0.0-next.23
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 +90 -7
- package/fesm2022/ethlete-query-testing.mjs +235 -5
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +7037 -5891
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/generators.json +10 -0
- package/generators/migrate-query-client-features/migration.js +142 -0
- package/generators/migrate-query-client-features/report.js +136 -0
- package/generators/migrate-query-client-features/schema.json +26 -0
- package/generators/migrate-query-opt-in-features/migration.js +182 -0
- package/generators/migrate-query-opt-in-features/report.js +143 -0
- package/generators/migrate-query-opt-in-features/schema.json +31 -0
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +3 -3
- package/generators/migrate-to-query-v3/cleanup-migration.js +2 -2
- package/generators/migrate-to-query-v3/http-client-check.js +5 -5
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +4 -3
- package/generators/migrate-to-query-v3/query-client-migration.js +6 -5
- package/generators/migrate-to-query-v3/rename-symbols.js +4 -4
- package/generators/migrate-to-query-v3/schema.json +1 -1
- package/generators/migrate-to-query-v3/shared.js +2 -0
- package/package.json +18 -1
- package/types/ethlete-query-testing.d.ts +98 -12
- package/types/ethlete-query.d.ts +4948 -3837
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
# @ethlete/query
|
|
2
2
|
|
|
3
|
+
## 6.0.0-next.23
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`53fcc97`](https://github.com/ethlete-io/ethdk/commit/53fcc97ef5b982fa8a3c5912c0800de1416feca6) Thanks [@github-actions](https://github.com/apps/github-actions)! - Bearer auth multi-tab sync and leader election are opt-in: `createBearerAuthProvider`'s `multiTabSync`
|
|
8
|
+
option is replaced by `withBearerAuthMultiTabSync()` in `features` (~1.4 kB gz off the secure entry
|
|
9
|
+
without it), read back as `provider.features.multiTabSync.isLeader()`. Election runs on the Web Locks
|
|
10
|
+
API now rather than a `localStorage` heartbeat, so a tab that closes or crashes hands over at once.
|
|
11
|
+
`setupAuthTest()` lost its `multiTabSync` option. Migrate with
|
|
12
|
+
`npx nx g @ethlete/query:migrate-query-opt-in-features`.
|
|
13
|
+
|
|
14
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`8b13854`](https://github.com/ethlete-io/ethdk/commit/8b13854aa079196c84fc08da0aee685f728a441d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Query client: multi-tab sync and response persistence are opt-in `features` instead of defaults - the
|
|
15
|
+
`multiTabSync` / `persistence` client options are gone and a client without the features ships neither
|
|
16
|
+
engine. Migrate with `nx g @ethlete/query:migrate-query-client-features`.
|
|
17
|
+
|
|
18
|
+
- `withMultiTabSync()` shares reads, polls a cache key in one tab for all of them and refreshes the
|
|
19
|
+
others after a mutation; per-query `multiTabSync: false` keeps a query tab-local.
|
|
20
|
+
- `withQueryPersistence()` keeps public reads in IndexedDB, so a reload renders the last known data
|
|
21
|
+
while it revalidates. Secure responses need an explicit opt-in and are removed on logout.
|
|
22
|
+
- `refreshQueriesInUse()` also refreshes GraphQL queries transported over POST; devtools gain Sync
|
|
23
|
+
and Disk columns.
|
|
24
|
+
|
|
25
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`53fcc97`](https://github.com/ethlete-io/ethdk/commit/53fcc97ef5b982fa8a3c5912c0800de1416feca6) Thanks [@github-actions](https://github.com/apps/github-actions)! - Error-response parsing and the default retry policy are opt-in query client features - without them a
|
|
26
|
+
client reads the plain `string` / `{ message }` / `{ detail }` / `string[]` ladder and retries
|
|
27
|
+
nothing, which takes ~1.4 kB gz out of an app that needs neither. Pick from `withHtmlErrorParsing()`,
|
|
28
|
+
`withSymfonyErrors()`, `withDefaultRetry()`, `withEthleteApiErrors()` (all three, the previous
|
|
29
|
+
behavior) and `registerQueryErrorParser()` for a body shape the SDK does not know. Run
|
|
30
|
+
`npx nx g @ethlete/query:migrate-query-opt-in-features` to keep the old behavior.
|
|
31
|
+
|
|
32
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`42ec970`](https://github.com/ethlete-io/ethdk/commit/42ec970d6963bb5a3aa3af4e207ef2cac801c915) Thanks [@github-actions](https://github.com/apps/github-actions)! - DI: `createProvider` / `createRootProvider` / `createStaticProvider` / `createStaticRootProvider` /
|
|
33
|
+
`createLabels` are replaced by `defineProvider` & co., which return a definition you read with
|
|
34
|
+
`toProvideFn` / `toInjectFn` / `toToken`; `createQueryClient`, `createBearerAuthProvider` and
|
|
35
|
+
`createWebSocketClient` return that definition instead of a tuple. Every `provideX` / `injectX` /
|
|
36
|
+
token export keeps its name - run `nx g @ethlete/core:migrate-provider-shape` for your own call sites.
|
|
37
|
+
Cuts the `@ethlete/components` import floor from 89.9 to 2.4 kB gz.
|
|
38
|
+
|
|
39
|
+
### Minor Changes
|
|
40
|
+
|
|
41
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`e040432`](https://github.com/ethlete-io/ethdk/commit/e040432bdfdce6b46073589bc66fd3a8bdf66111) Thanks [@github-actions](https://github.com/apps/github-actions)! - New `client.invalidateQueries()` re-runs the queries this client has on screen whose data went stale -
|
|
42
|
+
after a mutation, or a push message saying something changed server-side - and tells the user's other
|
|
43
|
+
tabs to do the same. Narrow it with `url` (resolved against `baseUrl`, matched on path boundaries) and
|
|
44
|
+
`filter`, or keep it local with `otherTabs: false`; reaching other tabs needs `withMultiTabSync()`.
|
|
45
|
+
See [Invalidating after a change](https://ethlete-sdk-docs.web.app/query/caching#invalidating-after-a-change).
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`a935328`](https://github.com/ethlete-io/ethdk/commit/a9353288d4e4ee41607005e8a1ab81921c21cf83) Thanks [@github-actions](https://github.com/apps/github-actions)! - `logout()` now abandons every unsaved-changes guard (`injectUnsavedChangesCoordinator().abandonAll('logout')`). Pressing logout with a dirty form used to leave a "discard your changes?" dialog floating over the login page the app had already redirected to, plus a tab still locked against closing - over edits that can no longer be saved. Guards created after a re-login work normally again.
|
|
50
|
+
|
|
51
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`353777b`](https://github.com/ethlete-io/ethdk/commit/353777bb56e42ad1c02058a2ccf694f69c94b025) Thanks [@github-actions](https://github.com/apps/github-actions)! - Mark build-tooling peer dependencies (`vite`, `typescript`, `ts-morph`, `@nx/devkit`, `@analogjs/*`) and feature-scoped runtime peers (`date-fns` in components) as optional via `peerDependenciesMeta`. They are only needed when running the Nx generators or using the date/time components - consumers no longer have to install them just to use the libraries.
|
|
52
|
+
|
|
53
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`53fcc97`](https://github.com/ethlete-io/ethdk/commit/53fcc97ef5b982fa8a3c5912c0800de1416feca6) Thanks [@github-actions](https://github.com/apps/github-actions)! - Devtools instrumentation is now registered through a hook that `provideQueryDevtools()` installs, so
|
|
54
|
+
an app without it drops the entry registry, the route stringifier and the client-name derivation
|
|
55
|
+
entirely - ~0.5 kB gz off every entry. No API change; `queryDevtoolsEntries` and the
|
|
56
|
+
`<et-query-devtools>` contract are unchanged.
|
|
57
|
+
|
|
58
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`42ec970`](https://github.com/ethlete-io/ethdk/commit/42ec970d6963bb5a3aa3af4e207ef2cac801c915) Thanks [@github-actions](https://github.com/apps/github-actions)! - Internal: the current generation no longer imports anything from the legacy V2 tree - `buildRoute`,
|
|
59
|
+
`buildQueryString`, `buildTimestampFromSeconds`, `decryptBearer`, `QueryError` and the query-string
|
|
60
|
+
types now live in `http/internal/request-route`, and `legacy` re-exports them. Every public name is
|
|
61
|
+
unchanged. Also closes the `http ↔ gql` cycle.
|
|
62
|
+
|
|
63
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`53fcc97`](https://github.com/ethlete-io/ethdk/commit/53fcc97ef5b982fa8a3c5912c0800de1416feca6) Thanks [@github-actions](https://github.com/apps/github-actions)! - `socket.io-client` is an optional peer dependency. It is only imported by `createWebSocketClient`,
|
|
64
|
+
and no non-`ws` public type surfaces it, so an app that does not use the realtime client no longer
|
|
65
|
+
has to install it.
|
|
66
|
+
|
|
67
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`353777b`](https://github.com/ethlete-io/ethdk/commit/353777bb56e42ad1c02058a2ccf694f69c94b025) Thanks [@github-actions](https://github.com/apps/github-actions)! - Adopt the `@ethlete/eslint-plugin` styleguide flat configs in `core`, `query`, `contentful` and
|
|
68
|
+
`types`, and apply the resulting auto-fixes. Runtime behavior is unchanged; three fixes are visible to
|
|
69
|
+
TypeScript consumers: exported `types` shapes are `type` aliases rather than `interface` declarations
|
|
70
|
+
(so they can no longer be extended by declaration merging), `core`'s `PropsDirective.destroyRef` and
|
|
71
|
+
`SeoDirective.parent` are `private`, and `ConsentHandler` is a `type`. The theme name registries stay
|
|
72
|
+
interfaces so consumers can keep augmenting them.
|
|
73
|
+
|
|
74
|
+
## 6.0.0-next.22
|
|
75
|
+
|
|
76
|
+
### Minor Changes
|
|
77
|
+
|
|
78
|
+
- [#3040](https://github.com/ethlete-io/ethdk/pull/3040) [`dcd8585`](https://github.com/ethlete-io/ethdk/commit/dcd8585423c4d0a66d968016a6e88b03c87b168a) Thanks [@github-actions](https://github.com/apps/github-actions)! - Error normalization: reduce an HTML error page (a proxy's `502`, a maintenance page) to its heading and message text instead of using the raw markup as the message. Exposes `isHtmlErrorPayload`, `htmlErrorPayload` and `extractHtmlErrorMessage`.
|
|
79
|
+
|
|
80
|
+
### Patch Changes
|
|
81
|
+
|
|
82
|
+
- [#3040](https://github.com/ethlete-io/ethdk/pull/3040) [`bc8aca7`](https://github.com/ethlete-io/ethdk/commit/bc8aca76900a47a3faed4ad2be2ce2ab7b70b27d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Filter overlay: new `provideFilterOverlay` / `injectFilterOverlay` - a filter panel that drafts the page's query
|
|
83
|
+
form, reports how many results the draft would return on its submit button, and applies on submit or discards on
|
|
84
|
+
dismiss. Replaces cdk's `FilterOverlayService`, rebuilt on signal forms and the current query client.
|
|
85
|
+
|
|
3
86
|
## 6.0.0-next.21
|
|
4
87
|
|
|
5
88
|
### Patch Changes
|
|
@@ -9,7 +92,7 @@
|
|
|
9
92
|
`legacyPatch*` / `legacyDelete*` executed without an explicit `skipCache: true` reached the repository with
|
|
10
93
|
`allowCache: true` and threw "This request is uncacheable, but allowCache is set to true".
|
|
11
94
|
|
|
12
|
-
`allowCache` is never read on the uncacheable path
|
|
95
|
+
`allowCache` is never read on the uncacheable path - there is no cache entry to reuse - so the throw was purely a
|
|
13
96
|
guard against a mistake in hand-written code. It stays for that, and gains a
|
|
14
97
|
`silenceUncacheableAllowCacheError` opt-out on `QueryConfig`, which `createLegacyQueryCreator` sets alongside the
|
|
15
98
|
`silenceMissingWithArgsFeatureError` it already passed. The cache-key guard is unaffected.
|
|
@@ -21,18 +104,18 @@
|
|
|
21
104
|
|
|
22
105
|
### Minor Changes
|
|
23
106
|
|
|
24
|
-
- [#3037](https://github.com/ethlete-io/ethdk/pull/3037) [`a83cfe4`](https://github.com/ethlete-io/ethdk/commit/a83cfe4b3e69c3e70ac20f81a5c287bdb6278a63) Thanks [@github-actions](https://github.com/apps/github-actions)! - Cache entries are now kept for `keepUnusedFor` (5 minutes by default) after their last consumer was destroyed, instead of being discarded immediately. A query that mounts again within that window
|
|
107
|
+
- [#3037](https://github.com/ethlete-io/ethdk/pull/3037) [`a83cfe4`](https://github.com/ethlete-io/ethdk/commit/a83cfe4b3e69c3e70ac20f81a5c287bdb6278a63) Thanks [@github-actions](https://github.com/apps/github-actions)! - Cache entries are now kept for `keepUnusedFor` (5 minutes by default) after their last consumer was destroyed, instead of being discarded immediately. A query that mounts again within that window - a list page reached via browser back navigation, for instance - binds to the existing entry and renders its previous response right away while revalidating in the background (`executionState()` reports `{ type: 'loading', hasCachedResponse: true }`).
|
|
25
108
|
|
|
26
109
|
- Configurable per client (`createQueryClient({ keepUnusedFor })`) and per query creator; `0` restores the previous release-immediately behavior.
|
|
27
110
|
- Independent of `cache-control`, so it also applies to private/authenticated responses.
|
|
28
|
-
- Only entries holding a response are kept
|
|
111
|
+
- Only entries holding a response are kept - in-flight and errored ones are still aborted immediately. At most 50 unused entries per client are retained, and retention is disabled on the server.
|
|
29
112
|
|
|
30
113
|
- [#3037](https://github.com/ethlete-io/ethdk/pull/3037) [`7ae9eaa`](https://github.com/ethlete-io/ethdk/commit/7ae9eaa878c6c3bfa80876c67ac1f9360a1c24ec) Thanks [@github-actions](https://github.com/apps/github-actions)! - Close the gaps found while migrating a real workspace from the legacy client to v3:
|
|
31
114
|
|
|
32
115
|
- **Client-level headers.** `createQueryClient({ headers })` takes `HttpHeaders` or a function
|
|
33
116
|
re-read on every request, so a signal can drive it. Per-query `args.headers` merge on top and win
|
|
34
117
|
per name. `client.refreshQueriesInUse()` re-runs every bound `GET`/`HEAD`/`OPTIONS` (in-flight ones
|
|
35
|
-
included)
|
|
118
|
+
included) - the replacement for v2's `setDefaultHeaders({ refreshQueriesInUse: true })`.
|
|
36
119
|
- **`provider.setTokens(access, refresh)`** on the public bearer auth provider, for tokens issued
|
|
37
120
|
outside it (SSO callbacks, native shells). Previously reachable only from inside a custom feature.
|
|
38
121
|
- **`logout()` now resets bound secure queries.** It already unbound their cache entries, but the
|
|
@@ -61,7 +144,7 @@
|
|
|
61
144
|
- Auth providers are scaffolded from the v2 `V2BearerAuthProvider` config instead of `queries: []`.
|
|
62
145
|
- Devtools are migrated rather than deleted: the per-client `provideQueryClientForDevtools` calls
|
|
63
146
|
collapse into one `provideQueryDevtools()`, `QueryDevtoolsComponent` is re-imported from
|
|
64
|
-
`@ethlete/components`, and `<et-query-devtools>` markup is left alone
|
|
147
|
+
`@ethlete/components`, and `<et-query-devtools>` markup is left alone - both versions use that selector.
|
|
65
148
|
- New report tasks for dropped v2 client options, missing `provideHttpClient()`, `setDefaultHeaders`
|
|
66
149
|
call sites and the auth import-cycle layout.
|
|
67
150
|
|
|
@@ -95,13 +178,13 @@
|
|
|
95
178
|
|
|
96
179
|
### Minor Changes
|
|
97
180
|
|
|
98
|
-
- [`5685798`](https://github.com/ethlete-io/ethdk/commit/56857988a9fb579a791950cd85d015dfd36921a3) Thanks [@TomTomB](https://github.com/TomTomB)! - Add `querySequence` for imperative waterfalls of dependent queries
|
|
181
|
+
- [`5685798`](https://github.com/ethlete-io/ethdk/commit/56857988a9fb579a791950cd85d015dfd36921a3) Thanks [@TomTomB](https://github.com/TomTomB)! - Add `querySequence` for imperative waterfalls of dependent queries - chain mutations with `.then()`, thread each response into the next, and `run()` to a typed, discriminated result that aborts on the first error. Exposes `status`/`running`/`currentStep`/`error` progress signals for driving UI.
|
|
99
182
|
|
|
100
183
|
## 6.0.0-next.17
|
|
101
184
|
|
|
102
185
|
### Minor Changes
|
|
103
186
|
|
|
104
|
-
- [#3030](https://github.com/ethlete-io/ethdk/pull/3030) [`47e4109`](https://github.com/ethlete-io/ethdk/commit/47e41097adf96788120840bdd03cad25306628f2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add `validateWithQuery` (v3) and `validateWithV2Query` (v2)
|
|
187
|
+
- [#3030](https://github.com/ethlete-io/ethdk/pull/3030) [`47e4109`](https://github.com/ethlete-io/ethdk/commit/47e41097adf96788120840bdd03cad25306628f2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add `validateWithQuery` (v3) and `validateWithV2Query` (v2) - query-backed async
|
|
105
188
|
validators for Angular signal forms. They adapt an `@ethlete/query` query into
|
|
106
189
|
`validateAsync`, so server-side validation runs through the query client (auth,
|
|
107
190
|
base route, caching, error normalization) instead of a raw `httpResource`. A
|
|
@@ -27,12 +27,12 @@ const setupAuthTest = (config) => {
|
|
|
27
27
|
}), extractRefreshTokens = (response) => ({
|
|
28
28
|
accessToken: response.accessToken,
|
|
29
29
|
refreshToken: response.refreshToken,
|
|
30
|
-
}), features, bearerDecryptFn,
|
|
30
|
+
}), features, bearerDecryptFn, refreshStrategy, minRefreshInterval, refreshIfExpired, expiresInPropertyName, } = config;
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
32
|
const login = querySetup.createPost(loginPath, {});
|
|
33
33
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
34
|
const refresh = querySetup.createPost(refreshPath, {});
|
|
35
|
-
const
|
|
35
|
+
const { inject: injectAuth } = createBearerAuthProvider({
|
|
36
36
|
name: 'test-auth',
|
|
37
37
|
queryClientRef: querySetup.queryClientRef,
|
|
38
38
|
queries: [
|
|
@@ -52,7 +52,6 @@ const setupAuthTest = (config) => {
|
|
|
52
52
|
],
|
|
53
53
|
features: (features ?? []),
|
|
54
54
|
bearerDecryptFn,
|
|
55
|
-
multiTabSync,
|
|
56
55
|
});
|
|
57
56
|
const auth = TestBed.runInInjectionContext(() => {
|
|
58
57
|
const provider = injectAuth();
|
|
@@ -96,6 +95,237 @@ const setupAuthTest = (config) => {
|
|
|
96
95
|
};
|
|
97
96
|
};
|
|
98
97
|
|
|
98
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
99
|
+
/**
|
|
100
|
+
* Replaces `globalThis.BroadcastChannel` with an in-memory bus.
|
|
101
|
+
*
|
|
102
|
+
* Messages are structured-cloned at post time (so a non-cloneable payload throws exactly like the
|
|
103
|
+
* real API does), delivered on a microtask (never synchronously), and never delivered back to the
|
|
104
|
+
* channel that posted them - all three matching browser behavior. `await Promise.resolve()` in a
|
|
105
|
+
* spec is enough to flush delivery.
|
|
106
|
+
*/
|
|
107
|
+
const installFakeBroadcastChannel = () => {
|
|
108
|
+
const original = globalThis.BroadcastChannel;
|
|
109
|
+
const instances = new Set();
|
|
110
|
+
const posted = [];
|
|
111
|
+
class FakeBroadcastChannel {
|
|
112
|
+
constructor(name) {
|
|
113
|
+
this.name = name;
|
|
114
|
+
this.closed = false;
|
|
115
|
+
this.onmessage = null;
|
|
116
|
+
instances.add(this);
|
|
117
|
+
}
|
|
118
|
+
postMessage(data) {
|
|
119
|
+
if (this.closed)
|
|
120
|
+
throw new DOMException('Channel is closed', 'InvalidStateError');
|
|
121
|
+
const clone = structuredClone(data);
|
|
122
|
+
posted.push({ channel: this.name, data: clone });
|
|
123
|
+
for (const instance of instances) {
|
|
124
|
+
if (instance === this || instance.closed || instance.name !== this.name)
|
|
125
|
+
continue;
|
|
126
|
+
queueMicrotask(() => {
|
|
127
|
+
if (instance.closed)
|
|
128
|
+
return;
|
|
129
|
+
instance.onmessage?.({ data: structuredClone(clone) });
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
close() {
|
|
134
|
+
this.closed = true;
|
|
135
|
+
instances.delete(this);
|
|
136
|
+
}
|
|
137
|
+
addEventListener() {
|
|
138
|
+
throw new Error('The fake BroadcastChannel only supports `onmessage`.');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
globalThis.BroadcastChannel = FakeBroadcastChannel;
|
|
142
|
+
return {
|
|
143
|
+
posted,
|
|
144
|
+
restore: () => {
|
|
145
|
+
for (const instance of [...instances]) {
|
|
146
|
+
instance.closed = true;
|
|
147
|
+
}
|
|
148
|
+
instances.clear();
|
|
149
|
+
globalThis.BroadcastChannel = original;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Installs an in-memory `navigator.locks` implementing exclusive locks: one holder per name, the
|
|
155
|
+
* rest queued FIFO, granted on a microtask (never synchronously), released when the callback's
|
|
156
|
+
* promise settles. `signal` is honored for queued requests and - as in the real API - ignored once a
|
|
157
|
+
* request has been granted.
|
|
158
|
+
*
|
|
159
|
+
* `mode: 'shared'`, `ifAvailable` and `steal` are not implemented; nothing in the library uses them.
|
|
160
|
+
*/
|
|
161
|
+
const installFakeWebLocks = () => {
|
|
162
|
+
const originalDescriptor = Object.getOwnPropertyDescriptor(navigator, 'locks');
|
|
163
|
+
const holders = new Map();
|
|
164
|
+
const queues = new Map();
|
|
165
|
+
const abortError = () => new DOMException('The lock request was aborted', 'AbortError');
|
|
166
|
+
const pump = (name) => {
|
|
167
|
+
if (holders.has(name))
|
|
168
|
+
return;
|
|
169
|
+
const waiter = queues.get(name)?.shift();
|
|
170
|
+
if (!waiter)
|
|
171
|
+
return;
|
|
172
|
+
waiter.granted = true;
|
|
173
|
+
holders.set(name, waiter);
|
|
174
|
+
const settle = (settleWaiter) => {
|
|
175
|
+
holders.delete(name);
|
|
176
|
+
settleWaiter();
|
|
177
|
+
pump(name);
|
|
178
|
+
};
|
|
179
|
+
// `pump` itself only ever runs from a microtask, so calling the callback right here keeps the
|
|
180
|
+
// grant exactly one microtask away from the `request()` call - which is what makes a single
|
|
181
|
+
// `flushMultiTabSync()` per hop enough in specs.
|
|
182
|
+
try {
|
|
183
|
+
Promise.resolve(waiter.callback({ name, mode: 'exclusive' })).then((value) => settle(() => waiter.resolve(value)), (error) => settle(() => waiter.reject(error)));
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
settle(() => waiter.reject(error));
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const request = (name, optionsOrCallback, maybeCallback) => {
|
|
190
|
+
const options = (typeof optionsOrCallback === 'function' ? {} : (optionsOrCallback ?? {}));
|
|
191
|
+
const callback = (typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback);
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
if (options.signal?.aborted) {
|
|
194
|
+
reject(abortError());
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const waiter = { callback, resolve, reject, granted: false };
|
|
198
|
+
options.signal?.addEventListener('abort', () => {
|
|
199
|
+
if (waiter.granted)
|
|
200
|
+
return;
|
|
201
|
+
const queue = queues.get(name);
|
|
202
|
+
if (queue)
|
|
203
|
+
queues.set(name, queue.filter((entry) => entry !== waiter));
|
|
204
|
+
reject(abortError());
|
|
205
|
+
});
|
|
206
|
+
queues.set(name, [...(queues.get(name) ?? []), waiter]);
|
|
207
|
+
queueMicrotask(() => pump(name));
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
// Both lists are reported, because a queued request is how a tab that is *not* the holder still
|
|
211
|
+
// shows up - which is what the auth leader election counts its tabs by.
|
|
212
|
+
const query = () => Promise.resolve({
|
|
213
|
+
held: [...holders.keys()].map((name) => ({ name, mode: 'exclusive' })),
|
|
214
|
+
pending: [...queues.entries()].flatMap(([name, waiters]) => waiters.map(() => ({ name, mode: 'exclusive' }))),
|
|
215
|
+
});
|
|
216
|
+
Object.defineProperty(navigator, 'locks', {
|
|
217
|
+
value: {
|
|
218
|
+
request,
|
|
219
|
+
query,
|
|
220
|
+
},
|
|
221
|
+
configurable: true,
|
|
222
|
+
writable: true,
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
heldNames: () => [...holders.keys()],
|
|
226
|
+
pendingNames: () => [...queues.entries()].filter(([, queue]) => queue.length > 0).map(([name]) => name),
|
|
227
|
+
restore: () => {
|
|
228
|
+
holders.clear();
|
|
229
|
+
queues.clear();
|
|
230
|
+
if (originalDescriptor) {
|
|
231
|
+
Object.defineProperty(navigator, 'locks', originalDescriptor);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
delete navigator.locks;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Flushes the microtask queue, which is where both fakes schedule their work - message delivery and
|
|
241
|
+
* lock grants. Awaiting it once is enough for a single hop; a grant that has to wait for another
|
|
242
|
+
* tab's release needs one await per hop.
|
|
243
|
+
*/
|
|
244
|
+
const flushMultiTabSync = () => Promise.resolve();
|
|
245
|
+
|
|
246
|
+
const quotaError = () => new DOMException('The quota has been exceeded.', 'QuotaExceededError');
|
|
247
|
+
const createFakeQueryPersistenceStore = () => {
|
|
248
|
+
const store = new Map();
|
|
249
|
+
const calls = { loadIndex: 0, read: 0, write: 0, remove: 0, clear: 0 };
|
|
250
|
+
const deferredReads = [];
|
|
251
|
+
let failingWrites = 0;
|
|
252
|
+
let writeError = null;
|
|
253
|
+
let loadIndexError = null;
|
|
254
|
+
let isDeferringReads = false;
|
|
255
|
+
// Cloning on the way in and on the way out is not pedantry: a real store hands back a copy, so a
|
|
256
|
+
// spec (or the engine) mutating a hydrated body must not be able to change what is "on disk".
|
|
257
|
+
const clone = (value) => structuredClone(value);
|
|
258
|
+
const loadIndex = async () => {
|
|
259
|
+
calls.loadIndex++;
|
|
260
|
+
if (loadIndexError !== null) {
|
|
261
|
+
const error = loadIndexError;
|
|
262
|
+
loadIndexError = null;
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
return Array.from(store.values()).map(({ body: _body, ...meta }) => clone(meta));
|
|
266
|
+
};
|
|
267
|
+
const read = async (key) => {
|
|
268
|
+
calls.read++;
|
|
269
|
+
if (isDeferringReads) {
|
|
270
|
+
await new Promise((resolve) => deferredReads.push(resolve));
|
|
271
|
+
}
|
|
272
|
+
const entry = store.get(key);
|
|
273
|
+
return entry ? { body: clone(entry.body) } : null;
|
|
274
|
+
};
|
|
275
|
+
const write = async (entries) => {
|
|
276
|
+
calls.write++;
|
|
277
|
+
if (failingWrites > 0) {
|
|
278
|
+
failingWrites--;
|
|
279
|
+
throw writeError ?? quotaError();
|
|
280
|
+
}
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
store.set(entry.key, clone(entry));
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
const remove = async (keys) => {
|
|
286
|
+
calls.remove++;
|
|
287
|
+
for (const key of keys) {
|
|
288
|
+
store.delete(key);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
const clear = async () => {
|
|
292
|
+
calls.clear++;
|
|
293
|
+
store.clear();
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
adapter: { loadIndex, read, write, remove, clear, isSupported: true },
|
|
297
|
+
seed: (entries) => {
|
|
298
|
+
for (const entry of entries) {
|
|
299
|
+
store.set(entry.key, clone(entry));
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
entries: () => Array.from(store.values()).map(clone),
|
|
303
|
+
entry: (key) => {
|
|
304
|
+
const entry = store.get(key);
|
|
305
|
+
return entry ? clone(entry) : undefined;
|
|
306
|
+
},
|
|
307
|
+
calls: () => ({ ...calls }),
|
|
308
|
+
failNextWrites: (count, error) => {
|
|
309
|
+
failingWrites = count;
|
|
310
|
+
writeError = error ?? null;
|
|
311
|
+
},
|
|
312
|
+
failNextLoadIndex: (error) => {
|
|
313
|
+
loadIndexError = error ?? quotaError();
|
|
314
|
+
},
|
|
315
|
+
deferReads: () => {
|
|
316
|
+
isDeferringReads = true;
|
|
317
|
+
},
|
|
318
|
+
flushReads: async () => {
|
|
319
|
+
isDeferringReads = false;
|
|
320
|
+
const pending = deferredReads.splice(0, deferredReads.length);
|
|
321
|
+
for (const resolve of pending) {
|
|
322
|
+
resolve();
|
|
323
|
+
}
|
|
324
|
+
await Promise.resolve();
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
|
|
99
329
|
const setupQueryTest = (config) => {
|
|
100
330
|
const baseUrl = config?.baseUrl ?? 'https://api.test.com';
|
|
101
331
|
const name = config?.name ?? 'test';
|
|
@@ -141,7 +371,7 @@ const setupQueryTest = (config) => {
|
|
|
141
371
|
TestBed.configureTestingModule({ providers });
|
|
142
372
|
const queryClientRef = createQueryClient({ baseUrl, name });
|
|
143
373
|
const setup = TestBed.runInInjectionContext(() => {
|
|
144
|
-
const
|
|
374
|
+
const { inject } = queryClientRef;
|
|
145
375
|
const queryClient = inject();
|
|
146
376
|
if (!queryClient) {
|
|
147
377
|
throw new Error('Failed to create query client in test setup');
|
|
@@ -174,5 +404,5 @@ const setupQueryTest = (config) => {
|
|
|
174
404
|
* Generated bundle index. Do not edit.
|
|
175
405
|
*/
|
|
176
406
|
|
|
177
|
-
export { expectAndFlush, expectFlushAndWait, setupAuthTest, setupQueryTest };
|
|
407
|
+
export { createFakeQueryPersistenceStore, expectAndFlush, expectFlushAndWait, flushMultiTabSync, installFakeBroadcastChannel, installFakeWebLocks, setupAuthTest, setupQueryTest };
|
|
178
408
|
//# 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/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 /** Whether to disable multi-tab sync. Default: false */\n multiTabSync?:\n | false\n | { enabled?: boolean; channelName?: string; syncTokens?: boolean; syncLogout?: boolean; leaderElection?: boolean };\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 multiTabSync = false,\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 [, 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 multiTabSync,\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","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[1]>>;\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/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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"}
|