@ethlete/query 6.0.0-next.22 → 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 +79 -8
- package/fesm2022/ethlete-query-testing.mjs +235 -5
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +7081 -6088
- 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 +4919 -3855
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "migrate-query-opt-in-features",
|
|
4
|
+
"title": "Migrate query clients and auth providers to opt-in features",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"projects": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": { "type": "string" },
|
|
10
|
+
"description": "Nx projects to migrate. Defaults to every project in the workspace.",
|
|
11
|
+
"alias": "project"
|
|
12
|
+
},
|
|
13
|
+
"include": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": { "type": "string" },
|
|
16
|
+
"description": "Path prefixes to migrate, relative to the workspace root (e.g. 'libs/queries'). Unioned with `projects`. Defaults to the whole workspace.",
|
|
17
|
+
"alias": "includePath"
|
|
18
|
+
},
|
|
19
|
+
"reportOnly": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"description": "Only list the affected call sites in the report file, without changing any code.",
|
|
22
|
+
"default": false
|
|
23
|
+
},
|
|
24
|
+
"skipFormat": {
|
|
25
|
+
"type": "boolean",
|
|
26
|
+
"description": "Skip formatting files after migration",
|
|
27
|
+
"default": false
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"required": []
|
|
31
|
+
}
|
|
@@ -69,8 +69,8 @@ const REFRESH_QUERY_KEY = 'tokenRefresh';
|
|
|
69
69
|
/**
|
|
70
70
|
* Renders the `queries` / `features` arguments of a v3 provider from a v2 config.
|
|
71
71
|
*
|
|
72
|
-
* The mapping is close to 1:1
|
|
73
|
-
* `extractTokens`, `cookieName` → `withPersistentAuth({ cookie })`
|
|
72
|
+
* The mapping is close to 1:1 - `queryCreator` → `withRefreshQuery`, `responseAdapter` →
|
|
73
|
+
* `extractTokens`, `cookieName` → `withPersistentAuth({ cookie })` - but the adapters are copied
|
|
74
74
|
* verbatim from v2 and their parameter shapes differ, so each one is emitted with a TODO rather
|
|
75
75
|
* than presented as finished.
|
|
76
76
|
*/
|
|
@@ -85,7 +85,7 @@ export const renderAuthProviderBody = (config) => {
|
|
|
85
85
|
}
|
|
86
86
|
const refreshEntries = [` queryCreator: ${config.refreshCreatorName},`];
|
|
87
87
|
if (config.responseAdapter) {
|
|
88
|
-
refreshEntries.push(` // TODO(query-v3): v2 responseAdapter. It must return { accessToken, refreshToken }
|
|
88
|
+
refreshEntries.push(` // TODO(query-v3): v2 responseAdapter. It must return { accessToken, refreshToken } - v2 returned { token, refreshToken }.`, ` extractTokens: ${config.responseAdapter},`);
|
|
89
89
|
}
|
|
90
90
|
if (config.requestArgsAdapter) {
|
|
91
91
|
refreshEntries.push(` // TODO(query-v3): v2 requestArgsAdapter took { token, refreshToken }; buildArgs takes the refresh token alone.`, ` buildArgs: ${config.requestArgsAdapter},`);
|
|
@@ -3,7 +3,7 @@ import { createSourceFile, ensureImportFromQuery, ensureNamedImports, getLineNum
|
|
|
3
3
|
/**
|
|
4
4
|
* Points existing devtools usage at the v3 components instead of deleting it.
|
|
5
5
|
*
|
|
6
|
-
* Both versions render `<et-query-devtools>`, so templates need no change at all
|
|
6
|
+
* Both versions render `<et-query-devtools>`, so templates need no change at all - only the provider
|
|
7
7
|
* call and the component's import move. Stripping the markup (as this phase used to) threw away
|
|
8
8
|
* something that would have kept working, and left every migrated app without devtools until someone
|
|
9
9
|
* noticed they were gone.
|
|
@@ -119,7 +119,7 @@ const importsFromQuery = (content, name) => {
|
|
|
119
119
|
* `provideQueryDevtools()`.
|
|
120
120
|
*
|
|
121
121
|
* v3 registers every client and auth provider at once, so N per-client calls collapse to one. The
|
|
122
|
-
* first call site keeps its position
|
|
122
|
+
* first call site keeps its position - it is already where the app wanted its devtools - and the
|
|
123
123
|
* rest are removed along with the comma that separated them.
|
|
124
124
|
*/
|
|
125
125
|
const replaceDevtoolsProviderCalls = (content) => {
|
|
@@ -3,7 +3,7 @@ const APP_CONFIG_MARKERS = ['bootstrapApplication(', 'ApplicationConfig', 'provi
|
|
|
3
3
|
/**
|
|
4
4
|
* Flags the v2 default-header APIs, which the client migration drops on the floor.
|
|
5
5
|
*
|
|
6
|
-
* `setDefaultHeaders` is not cosmetic
|
|
6
|
+
* `setDefaultHeaders` is not cosmetic - an API token or a preview credential lives there, so losing
|
|
7
7
|
* it means every request goes out unauthenticated. v3 has a home for it (`headers` on
|
|
8
8
|
* `createQueryClient`), but the call sites are imperative and scattered, so this is a pointer rather
|
|
9
9
|
* than a rewrite.
|
|
@@ -31,7 +31,7 @@ export const reportDefaultHeaderUsages = (tree, report, scope) => {
|
|
|
31
31
|
report.addWarning({
|
|
32
32
|
title: 'Move default headers onto the query client',
|
|
33
33
|
summary: 'This workspace sets client-wide headers through the v2 `setDefaultHeaders` / `globalHeaders` API, which `createQueryClient` does not have. Left as is, those headers are simply not sent.',
|
|
34
|
-
action: 'Pass `headers` to `createQueryClient`
|
|
34
|
+
action: 'Pass `headers` to `createQueryClient` - a function form re-reads on every request, so a signal can drive it. `refreshQueriesInUse: true` becomes `injectMyClient().refreshQueriesInUse()` after the value changes.',
|
|
35
35
|
locations,
|
|
36
36
|
source: 'http-client-check',
|
|
37
37
|
dedupeKey: 'default-headers',
|
|
@@ -41,7 +41,7 @@ export const reportDefaultHeaderUsages = (tree, report, scope) => {
|
|
|
41
41
|
* Flags applications that will build cleanly and then fail on their first request.
|
|
42
42
|
*
|
|
43
43
|
* The v2 client shipped its own transport; v3 does `inject(HttpClient)`. An app that never needed
|
|
44
|
-
* `provideHttpClient()` therefore keeps compiling after the migration and dies at runtime instead
|
|
44
|
+
* `provideHttpClient()` therefore keeps compiling after the migration and dies at runtime instead -
|
|
45
45
|
* the one failure mode a codemod should never leave behind silently.
|
|
46
46
|
*/
|
|
47
47
|
export const reportMissingHttpClientProviders = (tree, report, scope) => {
|
|
@@ -71,8 +71,8 @@ export const reportMissingHttpClientProviders = (tree, report, scope) => {
|
|
|
71
71
|
if (appConfigFiles.length > 0 && filesWithProvider.length === 0) {
|
|
72
72
|
report.addWarning({
|
|
73
73
|
title: 'Add provideHttpClient() to the application providers',
|
|
74
|
-
summary: 'v3 queries run on Angular’s `HttpClient`, but no `provideHttpClient()` was found. The app compiles and then throws on the first request
|
|
75
|
-
action: 'Add `provideHttpClient()` to every app config that uses a query client. If anything relies on upload progress, use `provideHttpClient(withXhr())`
|
|
74
|
+
summary: 'v3 queries run on Angular’s `HttpClient`, but no `provideHttpClient()` was found. The app compiles and then throws on the first request - `@ethlete/query` never provides it, that is the application’s job.',
|
|
75
|
+
action: 'Add `provideHttpClient()` to every app config that uses a query client. If anything relies on upload progress, use `provideHttpClient(withXhr())` - on Angular ≥ 22 the default is fetch, which emits no upload progress events.',
|
|
76
76
|
locations: appConfigFiles,
|
|
77
77
|
source: 'http-client-check',
|
|
78
78
|
dedupeKey: 'missing-provide-http-client',
|
|
@@ -2,7 +2,7 @@ import * as ts from 'typescript';
|
|
|
2
2
|
import { collectV2BearerAuthConfigs, renderAuthProviderBody } from './auth-provider-scaffold.js';
|
|
3
3
|
import { createModuleGraph } from './module-graph.js';
|
|
4
4
|
import { pruneUnusedNamedImports, renameImportedReferences } from './rename-symbols.js';
|
|
5
|
-
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromQuery } from './shared.js';
|
|
5
|
+
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, } from './shared.js';
|
|
6
6
|
const LEGACY_HTTP_METHOD = {
|
|
7
7
|
GET: 'get',
|
|
8
8
|
POST: 'post',
|
|
@@ -504,7 +504,7 @@ const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, cre
|
|
|
504
504
|
}
|
|
505
505
|
};
|
|
506
506
|
const addAuthProviderToFile = (content, clientName, body) => {
|
|
507
|
-
const nextContent = ensureImportFromQuery(content, ['createBearerAuthProvider', ...body.importsNeeded]);
|
|
507
|
+
const nextContent = ensureImportFromEthleteCore(ensureImportFromQuery(content, ['createBearerAuthProvider', ...body.importsNeeded]), ['toInjectFn', 'toProvideFn']);
|
|
508
508
|
const sourceFile = createSourceFile(nextContent);
|
|
509
509
|
const configName = ensureConfigSuffix(clientName);
|
|
510
510
|
const configPosition = findVariableStatementEnd(sourceFile, configName, 'createQueryClient');
|
|
@@ -522,7 +522,8 @@ const addAuthProviderToFile = (content, clientName, body) => {
|
|
|
522
522
|
...(body.features ? [body.features] : []),
|
|
523
523
|
'});',
|
|
524
524
|
'',
|
|
525
|
-
`export const
|
|
525
|
+
`export const provide${capitalizedClientName}AuthProvider = toProvideFn(${authProviderName});`,
|
|
526
|
+
`export const inject${capitalizedClientName}AuthProvider = toInjectFn(${authProviderName});`,
|
|
526
527
|
].join('\n');
|
|
527
528
|
return nextContent.slice(0, configPosition) + authProviderBlock + nextContent.slice(configPosition);
|
|
528
529
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromQuery, getVariableStatementEnd, } from './shared.js';
|
|
2
|
+
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, getVariableStatementEnd, } from './shared.js';
|
|
3
3
|
export const migrateQueryClients = (tree, report, scope) => {
|
|
4
4
|
const queryClientFiles = new Map();
|
|
5
5
|
const variableRenames = new Map();
|
|
@@ -249,7 +249,7 @@ const renameVariables = (content, renames) => {
|
|
|
249
249
|
* report from production.
|
|
250
250
|
*/
|
|
251
251
|
const DROPPED_CLIENT_OPTIONS = {
|
|
252
|
-
'request.autoRefreshQueriesOnWindowFocus': 'No direct equivalent. Re-run the affected queries yourself
|
|
252
|
+
'request.autoRefreshQueriesOnWindowFocus': 'No direct equivalent. Re-run the affected queries yourself - `withAutoRefresh({ onSignalChanges: [windowFocusSignal] })` per query, or `injectMyClient().refreshQueriesInUse()` from a focus listener.',
|
|
253
253
|
'request.enableSmartPolling': 'No direct equivalent. `withPolling({ interval })` polls unconditionally; pause it yourself when the tab is hidden if that matters.',
|
|
254
254
|
'logging.preparedQuerySubscriptions': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
255
255
|
'logging.queryStateChanges': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
@@ -374,19 +374,20 @@ const addProviderAliasesToFile = (content, configNames) => {
|
|
|
374
374
|
return;
|
|
375
375
|
}
|
|
376
376
|
const { injectName, provideName } = getProviderAliasNames(variableName);
|
|
377
|
-
if (result.includes(`export const
|
|
377
|
+
if (result.includes(`export const ${injectName} = toInjectFn(${variableName});`)) {
|
|
378
378
|
return;
|
|
379
379
|
}
|
|
380
380
|
insertions.push({
|
|
381
381
|
position,
|
|
382
|
-
text: `\n\nexport const
|
|
382
|
+
text: `\n\nexport const ${provideName} = toProvideFn(${variableName});` +
|
|
383
|
+
`\nexport const ${injectName} = toInjectFn(${variableName});`,
|
|
383
384
|
});
|
|
384
385
|
});
|
|
385
386
|
insertions.sort((left, right) => right.position - left.position);
|
|
386
387
|
insertions.forEach(({ position, text }) => {
|
|
387
388
|
result = result.slice(0, position) + text + result.slice(position);
|
|
388
389
|
});
|
|
389
|
-
return result;
|
|
390
|
+
return insertions.length > 0 ? ensureImportFromEthleteCore(result, ['toInjectFn', 'toProvideFn']) : result;
|
|
390
391
|
};
|
|
391
392
|
const getProviderAliasNames = (configVariableName) => {
|
|
392
393
|
const baseName = configVariableName.replace(/Config$/, '');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { createSourceFile } from './shared.js';
|
|
3
3
|
/**
|
|
4
|
-
* Whether an identifier is a *name* rather than a *reference*
|
|
4
|
+
* Whether an identifier is a *name* rather than a *reference* - a declaration, a property key, the
|
|
5
5
|
* right-hand side of a member access. Renaming those is what turned `PeopleDetailDataSource.getPerson`
|
|
6
6
|
* and the `postLogin` property of a config type into `legacy…` during the first migration run.
|
|
7
7
|
*/
|
|
@@ -9,7 +9,7 @@ const isNamePosition = (node) => {
|
|
|
9
9
|
const parent = node.parent;
|
|
10
10
|
if (!parent)
|
|
11
11
|
return false;
|
|
12
|
-
// `foo.bar` / `Foo.Bar`
|
|
12
|
+
// `foo.bar` / `Foo.Bar` - the part after the dot is never the imported binding.
|
|
13
13
|
if (ts.isPropertyAccessExpression(parent) && parent.name === node)
|
|
14
14
|
return true;
|
|
15
15
|
if (ts.isQualifiedName(parent) && parent.right === node)
|
|
@@ -63,7 +63,7 @@ const isNamePosition = (node) => {
|
|
|
63
63
|
return true;
|
|
64
64
|
return false;
|
|
65
65
|
};
|
|
66
|
-
/** Every name the file declares itself
|
|
66
|
+
/** Every name the file declares itself - a local by the same name means the import is shadowed. */
|
|
67
67
|
const collectLocalDeclarations = (sourceFile) => {
|
|
68
68
|
const declared = new Set();
|
|
69
69
|
const add = (name) => {
|
|
@@ -134,7 +134,7 @@ export const renameImportedReferences = (content, renames) => {
|
|
|
134
134
|
* The migration rewrites away the constructs that needed `def`, `AnyLegacyQueryCreator` and friends;
|
|
135
135
|
* leaving the imports behind turns every touched file into a lint error, and `formatFiles` runs in
|
|
136
136
|
* the same generator invocation so they land that way. Identifiers inside template strings count as
|
|
137
|
-
* usage too
|
|
137
|
+
* usage too - a component referenced only from an inline template must not be pruned.
|
|
138
138
|
*/
|
|
139
139
|
export const pruneUnusedNamedImports = (content, moduleSpecifiers) => {
|
|
140
140
|
const sourceFile = createSourceFile(content);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"projects": {
|
|
8
8
|
"type": "array",
|
|
9
9
|
"items": { "type": "string" },
|
|
10
|
-
"description": "Nx projects to migrate. Defaults to every project in the workspace. Use this to adopt v3 one app or library at a time instead of rewriting the whole monorepo in one run
|
|
10
|
+
"description": "Nx projects to migrate. Defaults to every project in the workspace. Use this to adopt v3 one app or library at a time instead of rewriting the whole monorepo in one run - but keep a query client and the creators built from it in the same run, since they are migrated together.",
|
|
11
11
|
"alias": "project"
|
|
12
12
|
},
|
|
13
13
|
"include": {
|
|
@@ -55,6 +55,8 @@ export const ensureNamedImports = ({ content, importsNeeded, moduleSpecifier })
|
|
|
55
55
|
};
|
|
56
56
|
export const ensureImportFromQuery = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@ethlete/query' });
|
|
57
57
|
export const ensureAngularCoreImports = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@angular/core' });
|
|
58
|
+
/** `toProvideFn` / `toInjectFn` - the provider-definition extractors every generated ref pair needs. */
|
|
59
|
+
export const ensureImportFromEthleteCore = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@ethlete/core' });
|
|
58
60
|
export const getVariableStatementEnd = (sourceFile, variableName, initializerName) => {
|
|
59
61
|
let variableStatementEnd;
|
|
60
62
|
const visit = (node) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethlete/query",
|
|
3
|
-
"version": "6.0.0-next.
|
|
3
|
+
"version": "6.0.0-next.23",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"peerDependencies": {
|
|
@@ -17,6 +17,23 @@
|
|
|
17
17
|
"typescript": "6.0.3",
|
|
18
18
|
"vite": "8.1.5"
|
|
19
19
|
},
|
|
20
|
+
"peerDependenciesMeta": {
|
|
21
|
+
"@analogjs/vite-plugin-angular": {
|
|
22
|
+
"optional": true
|
|
23
|
+
},
|
|
24
|
+
"@nx/devkit": {
|
|
25
|
+
"optional": true
|
|
26
|
+
},
|
|
27
|
+
"socket.io-client": {
|
|
28
|
+
"optional": true
|
|
29
|
+
},
|
|
30
|
+
"typescript": {
|
|
31
|
+
"optional": true
|
|
32
|
+
},
|
|
33
|
+
"vite": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
20
37
|
"generators": "./generators/generators.json",
|
|
21
38
|
"module": "fesm2022/ethlete-query.mjs",
|
|
22
39
|
"typings": "types/ethlete-query.d.ts",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { AnyCreateQueryClientResult, QueryClientRef, createGetQuery, createPostQuery, createPutQuery, createPatchQuery, createDeleteQuery, BearerAuthProviderFeatureContext, AnyQueryBuilder, BearerAuthProvider, AuthQueryBuilder, TokenRefreshQueryBuilder, TokenRefreshQueryConfig } from '@ethlete/query';
|
|
1
|
+
import { AnyCreateQueryClientResult, QueryClientRef, createGetQuery, createPostQuery, createPutQuery, createPatchQuery, createDeleteQuery, BearerAuthProviderFeatureContext, AnyQueryBuilder, BearerAuthProvider, AuthQueryBuilder, TokenRefreshQueryBuilder, TokenRefreshQueryConfig, QueryPersistenceAdapter, PersistedQueryEntry } from '@ethlete/query';
|
|
2
2
|
import { HttpClient } from '@angular/common/http';
|
|
3
3
|
import { HttpTestingController } from '@angular/common/http/testing';
|
|
4
4
|
import { Injector } from '@angular/core';
|
|
5
5
|
|
|
6
6
|
type QueryTestSetup = {
|
|
7
|
-
queryClient: NonNullable<ReturnType<AnyCreateQueryClientResult[
|
|
7
|
+
queryClient: NonNullable<ReturnType<AnyCreateQueryClientResult['inject']>>;
|
|
8
8
|
queryClientRef: QueryClientRef;
|
|
9
9
|
httpClient: HttpClient;
|
|
10
10
|
httpTesting: HttpTestingController;
|
|
@@ -74,14 +74,6 @@ type AuthTestSetupConfig<TLoginArgs extends {
|
|
|
74
74
|
features?: [...TFeatures];
|
|
75
75
|
/** Custom bearer decrypt function for testing */
|
|
76
76
|
bearerDecryptFn?: (token: string) => TBearerData;
|
|
77
|
-
/** Whether to disable multi-tab sync. Default: false */
|
|
78
|
-
multiTabSync?: false | {
|
|
79
|
-
enabled?: boolean;
|
|
80
|
-
channelName?: string;
|
|
81
|
-
syncTokens?: boolean;
|
|
82
|
-
syncLogout?: boolean;
|
|
83
|
-
leaderElection?: boolean;
|
|
84
|
-
};
|
|
85
77
|
/** Refresh strategy configuration. See TokenRefreshQueryConfig for details */
|
|
86
78
|
refreshStrategy?: TokenRefreshQueryConfig<TRefreshArgs>['refreshStrategy'];
|
|
87
79
|
/** Minimum refresh interval in milliseconds */
|
|
@@ -154,6 +146,100 @@ declare const setupAuthTest: <TLoginArgs extends {
|
|
|
154
146
|
};
|
|
155
147
|
}, TFeatures extends readonly AnyFeatureBuilder[] = readonly AnyFeatureBuilder[], TBearerData = unknown>(config: AuthTestSetupConfig<TLoginArgs, TRefreshArgs, TFeatures, TBearerData>) => AuthTestSetup<TLoginArgs, TRefreshArgs, TFeatures, TBearerData>;
|
|
156
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Test doubles for the two browser APIs the query client's multi-tab sync is built on:
|
|
151
|
+
* `BroadcastChannel` and the Web Locks API (`navigator.locks`).
|
|
152
|
+
*
|
|
153
|
+
* Both fakes are process-wide and shared between every consumer created while they are installed,
|
|
154
|
+
* which is exactly what makes "two tabs" testable: create two query clients in one test and they
|
|
155
|
+
* talk to each other over the fake bus and contend for the same fake locks, going through the real
|
|
156
|
+
* production code path on both sides.
|
|
157
|
+
*
|
|
158
|
+
* Neither jsdom nor happy-dom can stand in here - jsdom ships a `BroadcastChannel` that never
|
|
159
|
+
* delivers to other instances, and no `navigator.locks` at all.
|
|
160
|
+
*/
|
|
161
|
+
/** Something posted on the fake bus, as seen by a receiver. */
|
|
162
|
+
type FakeBroadcastMessage = {
|
|
163
|
+
/** The channel name it was posted on. */
|
|
164
|
+
channel: string;
|
|
165
|
+
/** The structured-cloned payload. */
|
|
166
|
+
data: unknown;
|
|
167
|
+
};
|
|
168
|
+
type FakeBroadcastChannelHandle = {
|
|
169
|
+
/** Every message posted on the bus, in order, across all channel names. */
|
|
170
|
+
posted: FakeBroadcastMessage[];
|
|
171
|
+
/** Uninstalls the fake and restores whatever `BroadcastChannel` was there before. */
|
|
172
|
+
restore: () => void;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Replaces `globalThis.BroadcastChannel` with an in-memory bus.
|
|
176
|
+
*
|
|
177
|
+
* Messages are structured-cloned at post time (so a non-cloneable payload throws exactly like the
|
|
178
|
+
* real API does), delivered on a microtask (never synchronously), and never delivered back to the
|
|
179
|
+
* channel that posted them - all three matching browser behavior. `await Promise.resolve()` in a
|
|
180
|
+
* spec is enough to flush delivery.
|
|
181
|
+
*/
|
|
182
|
+
declare const installFakeBroadcastChannel: () => FakeBroadcastChannelHandle;
|
|
183
|
+
type FakeWebLocksHandle = {
|
|
184
|
+
/** The names currently held, in no particular order. */
|
|
185
|
+
heldNames: () => string[];
|
|
186
|
+
/** The names with at least one request queued behind the holder. */
|
|
187
|
+
pendingNames: () => string[];
|
|
188
|
+
/** Uninstalls the fake and restores whatever `navigator.locks` was there before. */
|
|
189
|
+
restore: () => void;
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Installs an in-memory `navigator.locks` implementing exclusive locks: one holder per name, the
|
|
193
|
+
* rest queued FIFO, granted on a microtask (never synchronously), released when the callback's
|
|
194
|
+
* promise settles. `signal` is honored for queued requests and - as in the real API - ignored once a
|
|
195
|
+
* request has been granted.
|
|
196
|
+
*
|
|
197
|
+
* `mode: 'shared'`, `ifAvailable` and `steal` are not implemented; nothing in the library uses them.
|
|
198
|
+
*/
|
|
199
|
+
declare const installFakeWebLocks: () => FakeWebLocksHandle;
|
|
200
|
+
/**
|
|
201
|
+
* Flushes the microtask queue, which is where both fakes schedule their work - message delivery and
|
|
202
|
+
* lock grants. Awaiting it once is enough for a single hop; a grant that has to wait for another
|
|
203
|
+
* tab's release needs one await per hop.
|
|
204
|
+
*/
|
|
205
|
+
declare const flushMultiTabSync: () => Promise<void>;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* An in-memory stand-in for the query client's persisted response store.
|
|
209
|
+
*
|
|
210
|
+
* jsdom has no IndexedDB at all, so this is how persistence is tested - and because the storage
|
|
211
|
+
* adapter is a documented seam rather than a mock of one, a spec running against this exercises the
|
|
212
|
+
* real engine, repository and request code on both the write and the read side.
|
|
213
|
+
*
|
|
214
|
+
* Everything resolves on a microtask, never synchronously, matching the real thing: `await
|
|
215
|
+
* Promise.resolve()` (or `flushMicrotasks`) is what lets a hydration land in a spec.
|
|
216
|
+
*/
|
|
217
|
+
type FakeQueryPersistenceStoreHandle = {
|
|
218
|
+
/** Pass this as `persistence: { adapter }`. Two clients sharing one handle share one store. */
|
|
219
|
+
adapter: QueryPersistenceAdapter;
|
|
220
|
+
/** Pre-populates the store, as if a previous session had written these entries. */
|
|
221
|
+
seed: (entries: PersistedQueryEntry[]) => void;
|
|
222
|
+
/** Every entry currently held, in insertion order. */
|
|
223
|
+
entries: () => PersistedQueryEntry[];
|
|
224
|
+
/** The entry held under `key`, or `undefined`. */
|
|
225
|
+
entry: (key: string) => PersistedQueryEntry | undefined;
|
|
226
|
+
/** How often each adapter method has been called. */
|
|
227
|
+
calls: () => Record<'loadIndex' | 'read' | 'write' | 'remove' | 'clear', number>;
|
|
228
|
+
/** Makes the next `count` writes reject - a full disk, by default. */
|
|
229
|
+
failNextWrites: (count: number, error?: unknown) => void;
|
|
230
|
+
/** Makes the next `loadIndex` reject. */
|
|
231
|
+
failNextLoadIndex: (error?: unknown) => void;
|
|
232
|
+
/**
|
|
233
|
+
* Holds every subsequent `read` pending until {@link flushReads} is called, for testing the window
|
|
234
|
+
* between a cold mount and its hydration (a slow disk, or a mount that happened before startup
|
|
235
|
+
* finished).
|
|
236
|
+
*/
|
|
237
|
+
deferReads: () => void;
|
|
238
|
+
/** Settles the reads held by {@link deferReads} and stops deferring. */
|
|
239
|
+
flushReads: () => Promise<void>;
|
|
240
|
+
};
|
|
241
|
+
declare const createFakeQueryPersistenceStore: () => FakeQueryPersistenceStoreHandle;
|
|
242
|
+
|
|
157
243
|
declare const expectAndFlush: <T = unknown>(httpTesting: HttpTestingController, url: string, response: T, statusOpts?: {
|
|
158
244
|
status: number;
|
|
159
245
|
statusText: string;
|
|
@@ -163,5 +249,5 @@ declare const expectFlushAndWait: <T = unknown>(httpTesting: HttpTestingControll
|
|
|
163
249
|
statusText: string;
|
|
164
250
|
}) => void;
|
|
165
251
|
|
|
166
|
-
export { expectAndFlush, expectFlushAndWait, setupAuthTest, setupQueryTest };
|
|
167
|
-
export type { AnyFeatureBuilder, AuthTestSetup, AuthTestSetupConfig, QueryTestSetup, QueryTestSetupConfig };
|
|
252
|
+
export { createFakeQueryPersistenceStore, expectAndFlush, expectFlushAndWait, flushMultiTabSync, installFakeBroadcastChannel, installFakeWebLocks, setupAuthTest, setupQueryTest };
|
|
253
|
+
export type { AnyFeatureBuilder, AuthTestSetup, AuthTestSetupConfig, FakeBroadcastChannelHandle, FakeBroadcastMessage, FakeQueryPersistenceStoreHandle, FakeWebLocksHandle, QueryTestSetup, QueryTestSetupConfig };
|