@ethlete/query 6.0.0-next.22 → 6.0.0-next.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +124 -8
  2. package/fesm2022/ethlete-query-testing.mjs +235 -5
  3. package/fesm2022/ethlete-query-testing.mjs.map +1 -1
  4. package/fesm2022/ethlete-query.mjs +7657 -5448
  5. package/fesm2022/ethlete-query.mjs.map +1 -1
  6. package/generators/deprecate-legacy-queries/migration.js +77 -0
  7. package/generators/deprecate-legacy-queries/schema.json +26 -0
  8. package/generators/generators.json +15 -0
  9. package/generators/migrate-query-client-features/migration.js +142 -0
  10. package/generators/migrate-query-client-features/report.js +136 -0
  11. package/generators/migrate-query-client-features/schema.json +26 -0
  12. package/generators/migrate-query-opt-in-features/migration.js +182 -0
  13. package/generators/migrate-query-opt-in-features/report.js +143 -0
  14. package/generators/migrate-query-opt-in-features/schema.json +31 -0
  15. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +3 -3
  16. package/generators/migrate-to-query-v3/cleanup-migration.js +2 -2
  17. package/generators/migrate-to-query-v3/http-client-check.js +5 -5
  18. package/generators/migrate-to-query-v3/legacy-prepare-migration.js +111 -18
  19. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +5 -4
  20. package/generators/migrate-to-query-v3/query-client-migration.js +6 -5
  21. package/generators/migrate-to-query-v3/rename-symbols.js +4 -4
  22. package/generators/migrate-to-query-v3/schema.json +1 -1
  23. package/generators/migrate-to-query-v3/shared.js +45 -0
  24. package/package.json +18 -1
  25. package/types/ethlete-query-testing.d.ts +98 -12
  26. package/types/ethlete-query.d.ts +6055 -3819
package/CHANGELOG.md CHANGED
@@ -1,5 +1,121 @@
1
1
  # @ethlete/query
2
2
 
3
+ ## 6.0.0-next.24
4
+
5
+ ### Minor Changes
6
+
7
+ - [`41354e7`](https://github.com/ethlete-io/ethdk/commit/41354e7c64ad713caa2393e6a3df592a410b53be) Thanks [@TomTomB](https://github.com/TomTomB)! - Legacy interop: containers accept `{ injector }` like `prepare()` does and default their cleanup by
8
+ request method again, and `provideLegacyPrepareFallback()` lets `prepare()` use the root injector instead
9
+ of throwing ET950 (browser only).
10
+
11
+ - [#3043](https://github.com/ethlete-io/ethdk/pull/3043) [`cf41b4e`](https://github.com/ethlete-io/ethdk/commit/cf41b4e64268eef05bbd69b501e9d08581d09697) Thanks [@github-actions](https://github.com/apps/github-actions)! - Mark every legacy (v2) query export as `@deprecated`, including the `createLegacyQueryCreator`
12
+ interop, and add `nx g @ethlete/query:deprecate-legacy-queries` to tag wrappers already in source.
13
+ Removal is v7.
14
+
15
+ - [#3043](https://github.com/ethlete-io/ethdk/pull/3043) [`156659d`](https://github.com/ethlete-io/ethdk/commit/156659de868ff8a66f9b0b6acbb59c637979500d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Query devtools: count what every query cost - how often it refreshed, how many refreshes reached
16
+ the network, and how much payload they moved - as activity tiles per query plus totals per client.
17
+
18
+ - [#3043](https://github.com/ethlete-io/ethdk/pull/3043) [`ebc94d4`](https://github.com/ethlete-io/ethdk/commit/ebc94d456176f350a02a29d8b23c1c6fbb573cd5) Thanks [@github-actions](https://github.com/apps/github-actions)! - Query devtools: a new **Faults** tab arms real request failures per query client - latency before
19
+ every attempt, fail-the-next-N, fail-N%, and the response status - resolved per attempt inside the
20
+ pipeline, so the retry policy re-rolls it.
21
+
22
+ - [#3043](https://github.com/ethlete-io/ethdk/pull/3043) [`156659d`](https://github.com/ethlete-io/ethdk/commit/156659de868ff8a66f9b0b6acbb59c637979500d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Query devtools: list every query, auth and client feature with the options it was configured with,
23
+ not just its name.
24
+
25
+ - [`6c6213b`](https://github.com/ethlete-io/ethdk/commit/6c6213b48f8e5c5ccf0fcb526dd50884e768fb82) Thanks [@TomTomB](https://github.com/TomTomB)! - Query devtools: add a **Forms** tab covering `createQueryForm` - its fields, URL params and the query it drives - and log each invalidation with every query it refetched.
26
+
27
+ - [#3043](https://github.com/ethlete-io/ethdk/pull/3043) [`9f5f9ec`](https://github.com/ethlete-io/ethdk/commit/9f5f9ecc64a0290f753b9979e2667fbe3de238a0) Thanks [@github-actions](https://github.com/apps/github-actions)! - Query devtools: the Insomnia export now includes the auth provider's token refresh, and secure
28
+ requests read their bearer token out of its response instead of shipping one that goes stale.
29
+
30
+ - [#3043](https://github.com/ethlete-io/ethdk/pull/3043) [`156659d`](https://github.com/ethlete-io/ethdk/commit/156659de868ff8a66f9b0b6acbb59c637979500d) Thanks [@github-actions](https://github.com/apps/github-actions)! - Query devtools: list queries with their path params filled in from args (`/post/12` instead of
31
+ `/post/:param`), and export requests as an Insomnia v4 collection.
32
+
33
+ - [#3043](https://github.com/ethlete-io/ethdk/pull/3043) [`8b665a3`](https://github.com/ethlete-io/ethdk/commit/8b665a39bccf09e51fac34c6d05b9b9d2793d4e5) Thanks [@github-actions](https://github.com/apps/github-actions)! - Surface retries and transfer progress in the query devtools: `request.subtle.attempts()` and
34
+ `request.subtle.retryState()` report what a retry policy is doing, and the panel shows backoff
35
+ countdowns, attempt counts and a progress bar for `reportProgress` requests.
36
+
37
+ - [#3043](https://github.com/ethlete-io/ethdk/pull/3043) [`37a74ae`](https://github.com/ethlete-io/ethdk/commit/37a74aec9c149a00f493894a75499c7e86d83cf7) Thanks [@github-actions](https://github.com/apps/github-actions)! - Record what a query did per run: its last 25 runs land on the devtools stats handle, feeding a new
38
+ **Timeline** tab that draws every request on one axis and a **History** section that diffs responses.
39
+
40
+ ### Patch Changes
41
+
42
+ - [`41354e7`](https://github.com/ethlete-io/ethdk/commit/41354e7c64ad713caa2393e6a3df592a410b53be) Thanks [@TomTomB](https://github.com/TomTomB)! - Legacy interop fixes: `destroyOnResponse` no longer strands its watcher on an aborted query, an `entity`
43
+ config no longer syncs a `null` response on `prepare()`, and a falsy `body` is sent instead of dropped.
44
+
45
+ - [`41354e7`](https://github.com/ethlete-io/ethdk/commit/41354e7c64ad713caa2393e6a3df592a410b53be) Thanks [@TomTomB](https://github.com/TomTomB)! - `migrate-to-query-v3` now threads an injector into `prepare()` calls inside callbacks (`computed`,
46
+ `effect`, RxJS operators) and adds `destroyOnResponse` when the prepared query is discarded.
47
+
48
+ ## 6.0.0-next.23
49
+
50
+ ### Major Changes
51
+
52
+ - [#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`
53
+ option is replaced by `withBearerAuthMultiTabSync()` in `features` (~1.4 kB gz off the secure entry
54
+ without it), read back as `provider.features.multiTabSync.isLeader()`. Election runs on the Web Locks
55
+ API now rather than a `localStorage` heartbeat, so a tab that closes or crashes hands over at once.
56
+ `setupAuthTest()` lost its `multiTabSync` option. Migrate with
57
+ `npx nx g @ethlete/query:migrate-query-opt-in-features`.
58
+
59
+ - [#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
60
+ `multiTabSync` / `persistence` client options are gone and a client without the features ships neither
61
+ engine. Migrate with `nx g @ethlete/query:migrate-query-client-features`.
62
+
63
+ - `withMultiTabSync()` shares reads, polls a cache key in one tab for all of them and refreshes the
64
+ others after a mutation; per-query `multiTabSync: false` keeps a query tab-local.
65
+ - `withQueryPersistence()` keeps public reads in IndexedDB, so a reload renders the last known data
66
+ while it revalidates. Secure responses need an explicit opt-in and are removed on logout.
67
+ - `refreshQueriesInUse()` also refreshes GraphQL queries transported over POST; devtools gain Sync
68
+ and Disk columns.
69
+
70
+ - [#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
71
+ client reads the plain `string` / `{ message }` / `{ detail }` / `string[]` ladder and retries
72
+ nothing, which takes ~1.4 kB gz out of an app that needs neither. Pick from `withHtmlErrorParsing()`,
73
+ `withSymfonyErrors()`, `withDefaultRetry()`, `withEthleteApiErrors()` (all three, the previous
74
+ behavior) and `registerQueryErrorParser()` for a body shape the SDK does not know. Run
75
+ `npx nx g @ethlete/query:migrate-query-opt-in-features` to keep the old behavior.
76
+
77
+ - [#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` /
78
+ `createLabels` are replaced by `defineProvider` & co., which return a definition you read with
79
+ `toProvideFn` / `toInjectFn` / `toToken`; `createQueryClient`, `createBearerAuthProvider` and
80
+ `createWebSocketClient` return that definition instead of a tuple. Every `provideX` / `injectX` /
81
+ token export keeps its name - run `nx g @ethlete/core:migrate-provider-shape` for your own call sites.
82
+ Cuts the `@ethlete/components` import floor from 89.9 to 2.4 kB gz.
83
+
84
+ ### Minor Changes
85
+
86
+ - [#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 -
87
+ after a mutation, or a push message saying something changed server-side - and tells the user's other
88
+ tabs to do the same. Narrow it with `url` (resolved against `baseUrl`, matched on path boundaries) and
89
+ `filter`, or keep it local with `otherTabs: false`; reaching other tabs needs `withMultiTabSync()`.
90
+ See [Invalidating after a change](https://ethlete-sdk-docs.web.app/query/caching#invalidating-after-a-change).
91
+
92
+ ### Patch Changes
93
+
94
+ - [#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.
95
+
96
+ - [#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.
97
+
98
+ - [#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
99
+ an app without it drops the entry registry, the route stringifier and the client-name derivation
100
+ entirely - ~0.5 kB gz off every entry. No API change; `queryDevtoolsEntries` and the
101
+ `<et-query-devtools>` contract are unchanged.
102
+
103
+ - [#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`,
104
+ `buildQueryString`, `buildTimestampFromSeconds`, `decryptBearer`, `QueryError` and the query-string
105
+ types now live in `http/internal/request-route`, and `legacy` re-exports them. Every public name is
106
+ unchanged. Also closes the `http ↔ gql` cycle.
107
+
108
+ - [#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`,
109
+ and no non-`ws` public type surfaces it, so an app that does not use the realtime client no longer
110
+ has to install it.
111
+
112
+ - [#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
113
+ `types`, and apply the resulting auto-fixes. Runtime behavior is unchanged; three fixes are visible to
114
+ TypeScript consumers: exported `types` shapes are `type` aliases rather than `interface` declarations
115
+ (so they can no longer be extended by declaration merging), `core`'s `PropsDirective.destroyRef` and
116
+ `SeoDirective.parent` are `private`, and `ConsentHandler` is a `type`. The theme name registries stay
117
+ interfaces so consumers can keep augmenting them.
118
+
3
119
  ## 6.0.0-next.22
4
120
 
5
121
  ### Minor Changes
@@ -8,7 +124,7 @@
8
124
 
9
125
  ### Patch Changes
10
126
 
11
- - [#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
127
+ - [#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
12
128
  form, reports how many results the draft would return on its submit button, and applies on submit or discards on
13
129
  dismiss. Replaces cdk's `FilterOverlayService`, rebuilt on signal forms and the current query client.
14
130
 
@@ -21,7 +137,7 @@
21
137
  `legacyPatch*` / `legacyDelete*` executed without an explicit `skipCache: true` reached the repository with
22
138
  `allowCache: true` and threw "This request is uncacheable, but allowCache is set to true".
23
139
 
24
- `allowCache` is never read on the uncacheable path — there is no cache entry to reuse — so the throw was purely a
140
+ `allowCache` is never read on the uncacheable path - there is no cache entry to reuse - so the throw was purely a
25
141
  guard against a mistake in hand-written code. It stays for that, and gains a
26
142
  `silenceUncacheableAllowCacheError` opt-out on `QueryConfig`, which `createLegacyQueryCreator` sets alongside the
27
143
  `silenceMissingWithArgsFeatureError` it already passed. The cache-key guard is unaffected.
@@ -33,18 +149,18 @@
33
149
 
34
150
  ### Minor Changes
35
151
 
36
- - [#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 }`).
152
+ - [#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 }`).
37
153
 
38
154
  - Configurable per client (`createQueryClient({ keepUnusedFor })`) and per query creator; `0` restores the previous release-immediately behavior.
39
155
  - Independent of `cache-control`, so it also applies to private/authenticated responses.
40
- - 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.
156
+ - 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.
41
157
 
42
158
  - [#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:
43
159
 
44
160
  - **Client-level headers.** `createQueryClient({ headers })` takes `HttpHeaders` or a function
45
161
  re-read on every request, so a signal can drive it. Per-query `args.headers` merge on top and win
46
162
  per name. `client.refreshQueriesInUse()` re-runs every bound `GET`/`HEAD`/`OPTIONS` (in-flight ones
47
- included) — the replacement for v2's `setDefaultHeaders({ refreshQueriesInUse: true })`.
163
+ included) - the replacement for v2's `setDefaultHeaders({ refreshQueriesInUse: true })`.
48
164
  - **`provider.setTokens(access, refresh)`** on the public bearer auth provider, for tokens issued
49
165
  outside it (SSO callbacks, native shells). Previously reachable only from inside a custom feature.
50
166
  - **`logout()` now resets bound secure queries.** It already unbound their cache entries, but the
@@ -73,7 +189,7 @@
73
189
  - Auth providers are scaffolded from the v2 `V2BearerAuthProvider` config instead of `queries: []`.
74
190
  - Devtools are migrated rather than deleted: the per-client `provideQueryClientForDevtools` calls
75
191
  collapse into one `provideQueryDevtools()`, `QueryDevtoolsComponent` is re-imported from
76
- `@ethlete/components`, and `<et-query-devtools>` markup is left alone — both versions use that selector.
192
+ `@ethlete/components`, and `<et-query-devtools>` markup is left alone - both versions use that selector.
77
193
  - New report tasks for dropped v2 client options, missing `provideHttpClient()`, `setDefaultHeaders`
78
194
  call sites and the auth import-cycle layout.
79
195
 
@@ -107,13 +223,13 @@
107
223
 
108
224
  ### Minor Changes
109
225
 
110
- - [`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.
226
+ - [`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.
111
227
 
112
228
  ## 6.0.0-next.17
113
229
 
114
230
  ### Minor Changes
115
231
 
116
- - [#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
232
+ - [#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
117
233
  validators for Angular signal forms. They adapt an `@ethlete/query` query into
118
234
  `validateAsync`, so server-side validation runs through the query client (auth,
119
235
  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, multiTabSync = false, refreshStrategy, minRefreshInterval, refreshIfExpired, expiresInPropertyName, } = config;
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 [, injectAuth] = createBearerAuthProvider({
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 [, inject] = queryClientRef;
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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"}