@jskit-ai/shell-web 0.1.87 → 0.1.89
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/package.descriptor.mjs +9 -3
- package/package.json +3 -2
- package/src/client/index.js +6 -0
- package/src/client/providers/ShellWebClientProvider.js +15 -0
- package/src/client/requestRecovery/index.js +8 -0
- package/src/client/requestRecovery/inject.js +30 -0
- package/src/client/requestRecovery/runtime.js +446 -0
- package/src/client/runtime/bootstrapRuntime.js +14 -0
- package/test/provider.test.js +162 -0
- package/test/requestRecovery.test.js +72 -0
- package/test/settingsPlacementContract.test.js +24 -0
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/shell-web",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.89",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Web shell layout runtime with outlet-based placement contributions.",
|
|
7
7
|
dependsOn: [],
|
|
@@ -9,7 +9,8 @@ export default Object.freeze({
|
|
|
9
9
|
provides: [
|
|
10
10
|
"runtime.web-placement",
|
|
11
11
|
"runtime.web-error",
|
|
12
|
-
"runtime.web-async-module-recovery"
|
|
12
|
+
"runtime.web-async-module-recovery",
|
|
13
|
+
"runtime.web-request-recovery"
|
|
13
14
|
],
|
|
14
15
|
requires: []
|
|
15
16
|
},
|
|
@@ -41,6 +42,10 @@ export default Object.freeze({
|
|
|
41
42
|
subpath: "./client/error",
|
|
42
43
|
summary: "Exports default error policy and runtime error reporter hook."
|
|
43
44
|
},
|
|
45
|
+
{
|
|
46
|
+
subpath: "./client/requestRecovery",
|
|
47
|
+
summary: "Exports request connectivity recovery classification and runtime access for app-caught request failures."
|
|
48
|
+
},
|
|
44
49
|
{
|
|
45
50
|
subpath: "./client/bootstrap",
|
|
46
51
|
summary: "Exports the shared client bootstrap handler registry used to extend /api/bootstrap handling."
|
|
@@ -57,6 +62,7 @@ export default Object.freeze({
|
|
|
57
62
|
"runtime.web-bootstrap.client",
|
|
58
63
|
"runtime.web-refresh.client",
|
|
59
64
|
"runtime.web-async-module-recovery.client",
|
|
65
|
+
"runtime.web-request-recovery.client",
|
|
60
66
|
"runtime.web-error.client",
|
|
61
67
|
"runtime.web-error.presentation-store.client"
|
|
62
68
|
]
|
|
@@ -294,7 +300,7 @@ export default Object.freeze({
|
|
|
294
300
|
dependencies: {
|
|
295
301
|
runtime: {
|
|
296
302
|
"@mdi/js": "^7.4.47",
|
|
297
|
-
"@jskit-ai/kernel": "0.1.
|
|
303
|
+
"@jskit-ai/kernel": "0.1.90"
|
|
298
304
|
},
|
|
299
305
|
dev: {}
|
|
300
306
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/shell-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.89",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"./client/error": "./src/client/error/index.js",
|
|
11
11
|
"./client/placement": "./src/client/placement/index.js",
|
|
12
12
|
"./client/asyncModuleRecovery": "./src/client/asyncModuleRecovery/index.js",
|
|
13
|
+
"./client/requestRecovery": "./src/client/requestRecovery/index.js",
|
|
13
14
|
"./client/bootstrap": "./src/client/bootstrap/index.js",
|
|
14
15
|
"./server/support/localLinkItemScaffolds": "./src/server/support/localLinkItemScaffolds.js",
|
|
15
16
|
"./client/navigation/linkResolver": "./src/client/navigation/linkResolver.js",
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
30
|
"@mdi/js": "^7.4.47",
|
|
30
|
-
"@jskit-ai/kernel": "0.1.
|
|
31
|
+
"@jskit-ai/kernel": "0.1.90"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"pinia": "^3.0.4",
|
package/src/client/index.js
CHANGED
|
@@ -21,6 +21,12 @@ export {
|
|
|
21
21
|
SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY,
|
|
22
22
|
useShellAsyncModuleRecoveryRuntime
|
|
23
23
|
} from "./asyncModuleRecovery/index.js";
|
|
24
|
+
export {
|
|
25
|
+
SHELL_REQUEST_RECOVERY_RUNTIME_KEY,
|
|
26
|
+
isRecoverableRequestError,
|
|
27
|
+
requestRecoveryMessage,
|
|
28
|
+
useShellRequestRecoveryRuntime
|
|
29
|
+
} from "./requestRecovery/index.js";
|
|
24
30
|
export {
|
|
25
31
|
BOOTSTRAP_PAYLOAD_HANDLER_TAG,
|
|
26
32
|
registerBootstrapPayloadHandler,
|
|
@@ -13,6 +13,12 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY
|
|
15
15
|
} from "../asyncModuleRecovery/inject.js";
|
|
16
|
+
import {
|
|
17
|
+
SHELL_REQUEST_RECOVERY_RUNTIME_KEY
|
|
18
|
+
} from "../requestRecovery/inject.js";
|
|
19
|
+
import {
|
|
20
|
+
createShellRequestRecoveryRuntime
|
|
21
|
+
} from "../requestRecovery/runtime.js";
|
|
16
22
|
import {
|
|
17
23
|
isRecord
|
|
18
24
|
} from "@jskit-ai/kernel/shared/support";
|
|
@@ -577,6 +583,12 @@ class ShellWebClientProvider {
|
|
|
577
583
|
logger
|
|
578
584
|
})
|
|
579
585
|
);
|
|
586
|
+
app.singleton("runtime.web-request-recovery.client", (scope) =>
|
|
587
|
+
createShellRequestRecoveryRuntime({
|
|
588
|
+
app: scope,
|
|
589
|
+
logger
|
|
590
|
+
})
|
|
591
|
+
);
|
|
580
592
|
app.singleton("runtime.web-error.presentation-store.client", () => createErrorPresentationStore());
|
|
581
593
|
app.singleton("runtime.web-error.client", (scope) =>
|
|
582
594
|
createErrorRuntime({
|
|
@@ -598,6 +610,7 @@ class ShellWebClientProvider {
|
|
|
598
610
|
const logger = createSharedProviderLogger(isRecord(app) ? app : null);
|
|
599
611
|
const errorRuntime = app.make("runtime.web-error.client");
|
|
600
612
|
const asyncModuleRecoveryRuntime = app.make("runtime.web-async-module-recovery.client");
|
|
613
|
+
const requestRecoveryRuntime = app.make("runtime.web-request-recovery.client");
|
|
601
614
|
asyncModuleRecoveryRuntime.install();
|
|
602
615
|
|
|
603
616
|
const placementRuntime = app.make("runtime.web-placement.client");
|
|
@@ -632,6 +645,7 @@ class ShellWebClientProvider {
|
|
|
632
645
|
|
|
633
646
|
const errorConfig = await loadAppErrorConfig(logger, errorRuntime, asyncModuleRecoveryRuntime);
|
|
634
647
|
applyAppErrorConfig(errorRuntime, errorConfig);
|
|
648
|
+
requestRecoveryRuntime.install();
|
|
635
649
|
|
|
636
650
|
const bootstrapRuntime = app.make("runtime.web-bootstrap.client");
|
|
637
651
|
if (bootstrapRuntime && typeof bootstrapRuntime.initialize === "function") {
|
|
@@ -658,6 +672,7 @@ class ShellWebClientProvider {
|
|
|
658
672
|
vueApp.provide("jskit.shell-web.runtime.web-refresh.client", refreshRuntime);
|
|
659
673
|
vueApp.provide("jskit.shell-web.runtime.web-error.client", errorRuntime);
|
|
660
674
|
vueApp.provide(SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY, asyncModuleRecoveryRuntime);
|
|
675
|
+
vueApp.provide(SHELL_REQUEST_RECOVERY_RUNTIME_KEY, requestRecoveryRuntime);
|
|
661
676
|
vueApp.provide(
|
|
662
677
|
"jskit.shell-web.runtime.web-error.presentation-store.client",
|
|
663
678
|
errorPresentationStore
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasInjectionContext,
|
|
3
|
+
inject
|
|
4
|
+
} from "vue";
|
|
5
|
+
|
|
6
|
+
const SHELL_REQUEST_RECOVERY_RUNTIME_KEY =
|
|
7
|
+
"jskit.shell-web.runtime.web-request-recovery.client";
|
|
8
|
+
|
|
9
|
+
function isShellRequestRecoveryRuntime(value) {
|
|
10
|
+
return Boolean(
|
|
11
|
+
value &&
|
|
12
|
+
typeof value.report === "function" &&
|
|
13
|
+
typeof value.reload === "function"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function useShellRequestRecoveryRuntime() {
|
|
18
|
+
if (!hasInjectionContext()) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const runtime = inject(SHELL_REQUEST_RECOVERY_RUNTIME_KEY, null);
|
|
23
|
+
return isShellRequestRecoveryRuntime(runtime) ? runtime : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
SHELL_REQUEST_RECOVERY_RUNTIME_KEY,
|
|
28
|
+
isShellRequestRecoveryRuntime,
|
|
29
|
+
useShellRequestRecoveryRuntime
|
|
30
|
+
};
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { guardedReloadApp } from "@jskit-ai/kernel/client/asyncModuleRecovery";
|
|
2
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support";
|
|
3
|
+
import { createProviderLogger as createSharedProviderLogger } from "@jskit-ai/kernel/shared/support/providerLogger";
|
|
4
|
+
|
|
5
|
+
const REQUEST_RECOVERY_RELOAD_FAILURE_MESSAGE =
|
|
6
|
+
"The app cannot reload because the app server is not reachable. Restart the server, then click Reload.";
|
|
7
|
+
|
|
8
|
+
const RECOVERABLE_REQUEST_ERROR_MESSAGES = Object.freeze([
|
|
9
|
+
/Failed to fetch/iu,
|
|
10
|
+
/Load failed/iu,
|
|
11
|
+
/Network request failed/iu,
|
|
12
|
+
/NetworkError when attempting to fetch resource/iu,
|
|
13
|
+
/The Internet connection appears to be offline/iu
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const RECOVERABLE_REQUEST_ERROR_CODES = Object.freeze(new Set([
|
|
17
|
+
"EAI_AGAIN",
|
|
18
|
+
"ECONNABORTED",
|
|
19
|
+
"ECONNREFUSED",
|
|
20
|
+
"ECONNRESET",
|
|
21
|
+
"ENETDOWN",
|
|
22
|
+
"ENETUNREACH",
|
|
23
|
+
"ENOTFOUND",
|
|
24
|
+
"ERR_NETWORK",
|
|
25
|
+
"ETIMEDOUT"
|
|
26
|
+
]));
|
|
27
|
+
|
|
28
|
+
const CANCELED_REQUEST_ERROR_CODES = Object.freeze(new Set([
|
|
29
|
+
"ABORT_ERR",
|
|
30
|
+
"ERR_ABORTED",
|
|
31
|
+
"ERR_CANCELED"
|
|
32
|
+
]));
|
|
33
|
+
|
|
34
|
+
function normalizeText(value, fallback = "") {
|
|
35
|
+
const normalized = String(value || "").trim();
|
|
36
|
+
return normalized || String(fallback || "").trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function errorMessage(error = null) {
|
|
40
|
+
return normalizeText(error?.message || error?.cause?.message || error);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function errorCode(error = null) {
|
|
44
|
+
return normalizeText(error?.code || error?.cause?.code).toUpperCase();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function errorName(error = null) {
|
|
48
|
+
return normalizeText(error?.name || error?.cause?.name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeRequestErrorStatus(error = null) {
|
|
52
|
+
if (!isRecord(error)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const hasStatus = Object.prototype.hasOwnProperty.call(error, "status");
|
|
57
|
+
const hasStatusCode = Object.prototype.hasOwnProperty.call(error, "statusCode");
|
|
58
|
+
if (!hasStatus && !hasStatusCode) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const status = Number(hasStatus ? error.status : error.statusCode);
|
|
63
|
+
return Number.isInteger(status) ? status : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isCanceledRequestError(error = null) {
|
|
67
|
+
const name = errorName(error).toLowerCase();
|
|
68
|
+
if (name === "aborterror" || name === "cancelederror") {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return CANCELED_REQUEST_ERROR_CODES.has(errorCode(error));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isRecoverableRequestError(error = null) {
|
|
75
|
+
if (!error || isCanceledRequestError(error)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const status = normalizeRequestErrorStatus(error);
|
|
80
|
+
if (status === 0) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (RECOVERABLE_REQUEST_ERROR_CODES.has(errorCode(error))) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const message = errorMessage(error);
|
|
89
|
+
return Boolean(
|
|
90
|
+
message &&
|
|
91
|
+
RECOVERABLE_REQUEST_ERROR_MESSAGES.some((pattern) => pattern.test(message))
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function requestRecoveryMessage(error = null, {
|
|
96
|
+
label = "Request",
|
|
97
|
+
message = ""
|
|
98
|
+
} = {}) {
|
|
99
|
+
const explicitMessage = normalizeText(message);
|
|
100
|
+
if (explicitMessage) {
|
|
101
|
+
return explicitMessage;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const requestLabel = normalizeText(label, "Request");
|
|
105
|
+
if (requestLabel === "Request") {
|
|
106
|
+
return "The app could not reach the server or network. Check the connection and try again.";
|
|
107
|
+
}
|
|
108
|
+
return `${requestLabel} could not reach the server or network. Check the connection and try again.`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveQueryMeta(query = null) {
|
|
112
|
+
if (isRecord(query?.meta)) {
|
|
113
|
+
return query.meta;
|
|
114
|
+
}
|
|
115
|
+
if (isRecord(query?.options?.meta)) {
|
|
116
|
+
return query.options.meta;
|
|
117
|
+
}
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isRequestRecoveryDisabled(query = null) {
|
|
122
|
+
const meta = resolveQueryMeta(query);
|
|
123
|
+
if (meta.jskitRequestRecovery === false || meta.requestRecovery === false) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const jskitMeta = isRecord(meta.jskit) ? meta.jskit : {};
|
|
128
|
+
return jskitMeta.requestRecovery === false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveQueryRecoveryLabel(query = null) {
|
|
132
|
+
const meta = resolveQueryMeta(query);
|
|
133
|
+
const jskitMeta = isRecord(meta.jskit) ? meta.jskit : {};
|
|
134
|
+
|
|
135
|
+
return normalizeText(
|
|
136
|
+
jskitMeta.requestRecoveryLabel ||
|
|
137
|
+
jskitMeta.label ||
|
|
138
|
+
meta.jskitRequestRecoveryLabel ||
|
|
139
|
+
meta.requestRecoveryLabel ||
|
|
140
|
+
meta.label,
|
|
141
|
+
"Request"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isActiveQuery(query = null) {
|
|
146
|
+
if (typeof query?.isActive === "function") {
|
|
147
|
+
return Boolean(query.isActive());
|
|
148
|
+
}
|
|
149
|
+
if (typeof query?.getObserversCount === "function") {
|
|
150
|
+
return Number(query.getObserversCount()) > 0;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function recoverableQueryError(query = null) {
|
|
156
|
+
const state = isRecord(query?.state) ? query.state : {};
|
|
157
|
+
if (state.status !== "error" || state.fetchStatus !== "idle") {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const error = state.error || state.fetchFailureReason || null;
|
|
162
|
+
return isRecoverableRequestError(error) ? error : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function createQueryRetry(queryClient, query = null) {
|
|
166
|
+
if (!queryClient || typeof queryClient.refetchQueries !== "function") {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const queryKey = query?.queryKey;
|
|
171
|
+
if (!Array.isArray(queryKey)) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return () =>
|
|
176
|
+
queryClient.refetchQueries(
|
|
177
|
+
{
|
|
178
|
+
queryKey,
|
|
179
|
+
exact: true,
|
|
180
|
+
type: "active"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
throwOnError: false
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resolveQueryHash(query = null) {
|
|
189
|
+
const explicitHash = normalizeText(query?.queryHash);
|
|
190
|
+
if (explicitHash) {
|
|
191
|
+
return explicitHash;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
return normalizeText(JSON.stringify(query?.queryKey || []));
|
|
196
|
+
} catch {
|
|
197
|
+
return normalizeText(String(query?.queryKey || ""));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function installRecoverableQueryObserver({
|
|
202
|
+
app,
|
|
203
|
+
runtime,
|
|
204
|
+
logger
|
|
205
|
+
} = {}) {
|
|
206
|
+
if (!app?.has?.("jskit.client.query-client")) {
|
|
207
|
+
return Object.freeze({
|
|
208
|
+
dispose() {}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const queryClient = app.make("jskit.client.query-client");
|
|
213
|
+
const queryCache =
|
|
214
|
+
queryClient && typeof queryClient.getQueryCache === "function"
|
|
215
|
+
? queryClient.getQueryCache()
|
|
216
|
+
: null;
|
|
217
|
+
if (!queryCache || typeof queryCache.subscribe !== "function") {
|
|
218
|
+
return Object.freeze({
|
|
219
|
+
dispose() {}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const reportedErrorUpdateByQueryHash = new Map();
|
|
224
|
+
|
|
225
|
+
function inspectQuery(query = null) {
|
|
226
|
+
if (!query || isRequestRecoveryDisabled(query) || !isActiveQuery(query)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const error = recoverableQueryError(query);
|
|
231
|
+
const queryHash = resolveQueryHash(query);
|
|
232
|
+
if (!error || !queryHash) {
|
|
233
|
+
reportedErrorUpdateByQueryHash.delete(queryHash);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const errorUpdateCount = Number(query?.state?.errorUpdateCount || 0);
|
|
238
|
+
const reportKey = `${errorUpdateCount}:${errorMessage(error)}`;
|
|
239
|
+
if (reportedErrorUpdateByQueryHash.get(queryHash) === reportKey) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
reportedErrorUpdateByQueryHash.set(queryHash, reportKey);
|
|
243
|
+
|
|
244
|
+
runtime.report(error, {
|
|
245
|
+
label: resolveQueryRecoveryLabel(query),
|
|
246
|
+
retry: createQueryRetry(queryClient, query),
|
|
247
|
+
source: "shell-web.request-recovery.query",
|
|
248
|
+
stale: query?.state?.data !== undefined,
|
|
249
|
+
dedupeKey: `shell-web.request-recovery.query.${queryHash}.${errorUpdateCount}`
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
if (typeof queryCache.getAll === "function") {
|
|
255
|
+
for (const query of queryCache.getAll()) {
|
|
256
|
+
inspectQuery(query);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
logger.warn(
|
|
261
|
+
{
|
|
262
|
+
error: errorMessage(error) || "unknown error"
|
|
263
|
+
},
|
|
264
|
+
"Shell request recovery could not inspect existing queries."
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const unsubscribe = queryCache.subscribe((event = {}) => {
|
|
269
|
+
try {
|
|
270
|
+
inspectQuery(event?.query);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
logger.warn(
|
|
273
|
+
{
|
|
274
|
+
error: errorMessage(error) || "unknown error"
|
|
275
|
+
},
|
|
276
|
+
"Shell request recovery query observer failed."
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return Object.freeze({
|
|
282
|
+
dispose() {
|
|
283
|
+
reportedErrorUpdateByQueryHash.clear();
|
|
284
|
+
if (typeof unsubscribe === "function") {
|
|
285
|
+
unsubscribe();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function createShellRequestRecoveryRuntime({
|
|
292
|
+
app,
|
|
293
|
+
logger = null
|
|
294
|
+
} = {}) {
|
|
295
|
+
if (!app || typeof app.has !== "function" || typeof app.make !== "function") {
|
|
296
|
+
throw new Error("createShellRequestRecoveryRuntime requires application has()/make().");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const runtimeLogger = logger || createSharedProviderLogger(app);
|
|
300
|
+
let installedQueryObserver = null;
|
|
301
|
+
let reloadAttempt = 0;
|
|
302
|
+
|
|
303
|
+
function errorRuntime() {
|
|
304
|
+
if (!app.has("runtime.web-error.client")) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const runtime = app.make("runtime.web-error.client");
|
|
308
|
+
return runtime && typeof runtime.report === "function" ? runtime : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function reload({
|
|
312
|
+
label = "App",
|
|
313
|
+
message = REQUEST_RECOVERY_RELOAD_FAILURE_MESSAGE
|
|
314
|
+
} = {}) {
|
|
315
|
+
const reloaded = await guardedReloadApp({
|
|
316
|
+
label,
|
|
317
|
+
message
|
|
318
|
+
});
|
|
319
|
+
if (!reloaded) {
|
|
320
|
+
reloadAttempt += 1;
|
|
321
|
+
report(new Error("Network request failed."), {
|
|
322
|
+
label,
|
|
323
|
+
message,
|
|
324
|
+
reload: true,
|
|
325
|
+
force: true,
|
|
326
|
+
source: "shell-web.request-recovery.reload",
|
|
327
|
+
dedupeKey: `shell-web.request-recovery.reload.${reloadAttempt}`
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return reloaded;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function retryAction(error, options, retry) {
|
|
334
|
+
return {
|
|
335
|
+
label: normalizeText(options.actionLabel, "Retry"),
|
|
336
|
+
dismissOnRun: true,
|
|
337
|
+
async handler() {
|
|
338
|
+
try {
|
|
339
|
+
return await retry();
|
|
340
|
+
} catch (retryError) {
|
|
341
|
+
if (isRecoverableRequestError(retryError)) {
|
|
342
|
+
return report(retryError, options);
|
|
343
|
+
}
|
|
344
|
+
throw retryError;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function reloadAction(options) {
|
|
351
|
+
return {
|
|
352
|
+
label: normalizeText(options.actionLabel, "Reload"),
|
|
353
|
+
dismissOnRun: true,
|
|
354
|
+
handler() {
|
|
355
|
+
return reload({
|
|
356
|
+
label: options.label,
|
|
357
|
+
message: options.reloadFailureMessage || REQUEST_RECOVERY_RELOAD_FAILURE_MESSAGE
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function report(error = null, options = {}) {
|
|
364
|
+
if (!isRecoverableRequestError(error) && options.force !== true) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const runtime = errorRuntime();
|
|
369
|
+
const source = normalizeText(options.source, "shell-web.request-recovery");
|
|
370
|
+
if (!runtime) {
|
|
371
|
+
runtimeLogger.warn(
|
|
372
|
+
{
|
|
373
|
+
source,
|
|
374
|
+
error: errorMessage(error) || "unknown error"
|
|
375
|
+
},
|
|
376
|
+
"Shell request recovery could not report because the shell error runtime is unavailable."
|
|
377
|
+
);
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const retry = typeof options.retry === "function" ? options.retry : null;
|
|
382
|
+
const action = retry
|
|
383
|
+
? retryAction(error, options, retry)
|
|
384
|
+
: options.reload === true
|
|
385
|
+
? reloadAction(options)
|
|
386
|
+
: null;
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
return runtime.report({
|
|
390
|
+
source,
|
|
391
|
+
message: requestRecoveryMessage(error, options),
|
|
392
|
+
cause: error || null,
|
|
393
|
+
intent: "app-recoverable",
|
|
394
|
+
severity: normalizeText(options.severity, options.stale ? "warning" : "error"),
|
|
395
|
+
dedupeKey: normalizeText(options.dedupeKey),
|
|
396
|
+
dedupeWindowMs: Number.isFinite(Number(options.dedupeWindowMs))
|
|
397
|
+
? Math.max(0, Number(options.dedupeWindowMs))
|
|
398
|
+
: 2000,
|
|
399
|
+
action
|
|
400
|
+
});
|
|
401
|
+
} catch (reportError) {
|
|
402
|
+
runtimeLogger.error(
|
|
403
|
+
{
|
|
404
|
+
source,
|
|
405
|
+
error: errorMessage(reportError) || "unknown error"
|
|
406
|
+
},
|
|
407
|
+
"Shell request recovery could not report through the shell error runtime."
|
|
408
|
+
);
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function install() {
|
|
414
|
+
if (installedQueryObserver) {
|
|
415
|
+
return installedQueryObserver;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
installedQueryObserver = installRecoverableQueryObserver({
|
|
419
|
+
app,
|
|
420
|
+
runtime: api,
|
|
421
|
+
logger: runtimeLogger
|
|
422
|
+
});
|
|
423
|
+
return installedQueryObserver;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function dispose() {
|
|
427
|
+
installedQueryObserver?.dispose?.();
|
|
428
|
+
installedQueryObserver = null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const api = Object.freeze({
|
|
432
|
+
dispose,
|
|
433
|
+
install,
|
|
434
|
+
isRecoverableRequestError,
|
|
435
|
+
reload,
|
|
436
|
+
report
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
return api;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export {
|
|
443
|
+
createShellRequestRecoveryRuntime,
|
|
444
|
+
isRecoverableRequestError,
|
|
445
|
+
requestRecoveryMessage
|
|
446
|
+
};
|
|
@@ -50,6 +50,14 @@ function createShellBootstrapRuntime({
|
|
|
50
50
|
let initialized = false;
|
|
51
51
|
let refreshQueue = Promise.resolve();
|
|
52
52
|
|
|
53
|
+
function requestRecoveryRuntime() {
|
|
54
|
+
if (!app.has("runtime.web-request-recovery.client")) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const runtime = app.make("runtime.web-request-recovery.client");
|
|
58
|
+
return runtime && typeof runtime.report === "function" ? runtime : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
async function resolveBootstrapRequest(reason = "manual") {
|
|
54
62
|
const handlers = resolveBootstrapPayloadHandlers(app);
|
|
55
63
|
let request = {
|
|
@@ -162,6 +170,12 @@ function createShellBootstrapRuntime({
|
|
|
162
170
|
return applyBootstrapPayload(payload, reason, request);
|
|
163
171
|
} catch (error) {
|
|
164
172
|
await applyBootstrapError(error, reason, request);
|
|
173
|
+
requestRecoveryRuntime()?.report?.(error, {
|
|
174
|
+
label: "App data",
|
|
175
|
+
retry: () => refresh(reason),
|
|
176
|
+
source: "shell-web.bootstrap",
|
|
177
|
+
dedupeKey: `shell-web.bootstrap.${String(reason || "manual").trim() || "manual"}.request-failed`
|
|
178
|
+
});
|
|
165
179
|
runtimeLogger.warn(
|
|
166
180
|
{
|
|
167
181
|
reason,
|
package/test/provider.test.js
CHANGED
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY
|
|
13
13
|
} from "../src/client/asyncModuleRecovery/index.js";
|
|
14
|
+
import {
|
|
15
|
+
SHELL_REQUEST_RECOVERY_RUNTIME_KEY
|
|
16
|
+
} from "../src/client/requestRecovery/index.js";
|
|
14
17
|
import { useShellErrorPresentationStore } from "../src/client/stores/useShellErrorPresentationStore.js";
|
|
15
18
|
const CLIENT_APP_CONFIG_GLOBAL_KEY = "__JSKIT_CLIENT_APP_CONFIG__";
|
|
16
19
|
|
|
@@ -169,6 +172,32 @@ function createWindowDouble({ href = "https://example.test/home" } = {}) {
|
|
|
169
172
|
};
|
|
170
173
|
}
|
|
171
174
|
|
|
175
|
+
function createQueryCacheDouble(initialQueries = []) {
|
|
176
|
+
const listeners = [];
|
|
177
|
+
const queries = [...initialQueries];
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
queries,
|
|
181
|
+
getAll() {
|
|
182
|
+
return queries;
|
|
183
|
+
},
|
|
184
|
+
subscribe(listener) {
|
|
185
|
+
listeners.push(listener);
|
|
186
|
+
return () => {
|
|
187
|
+
const index = listeners.indexOf(listener);
|
|
188
|
+
if (index >= 0) {
|
|
189
|
+
listeners.splice(index, 1);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
emit(event) {
|
|
194
|
+
for (const listener of [...listeners]) {
|
|
195
|
+
listener(event);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
172
201
|
test("shell web client provider preserves append-only placement topology object exports", () => {
|
|
173
202
|
const warnings = [];
|
|
174
203
|
const topology = {
|
|
@@ -223,6 +252,7 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
223
252
|
assert.equal(app.singletons.has("runtime.web-error.presentation-store.client"), true);
|
|
224
253
|
assert.equal(app.singletons.has("runtime.web-refresh.client"), true);
|
|
225
254
|
assert.equal(app.singletons.has("runtime.web-async-module-recovery.client"), true);
|
|
255
|
+
assert.equal(app.singletons.has("runtime.web-request-recovery.client"), true);
|
|
226
256
|
|
|
227
257
|
await provider.boot(app);
|
|
228
258
|
assert.equal(app.plugins.length, 0);
|
|
@@ -233,6 +263,7 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
233
263
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-refresh.client"), true);
|
|
234
264
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.client"), true);
|
|
235
265
|
assert.equal(providedByKey.has(SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY), true);
|
|
266
|
+
assert.equal(providedByKey.has(SHELL_REQUEST_RECOVERY_RUNTIME_KEY), true);
|
|
236
267
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.presentation-store.client"), true);
|
|
237
268
|
|
|
238
269
|
const placementRuntime = providedByKey.get("jskit.shell-web.runtime.web-placement.client");
|
|
@@ -253,6 +284,11 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
253
284
|
assert.equal(typeof asyncModuleRecoveryRuntime.notify, "function");
|
|
254
285
|
assert.equal(typeof asyncModuleRecoveryRuntime.reload, "function");
|
|
255
286
|
|
|
287
|
+
const requestRecoveryRuntime = providedByKey.get(SHELL_REQUEST_RECOVERY_RUNTIME_KEY);
|
|
288
|
+
assert.equal(typeof requestRecoveryRuntime.install, "function");
|
|
289
|
+
assert.equal(typeof requestRecoveryRuntime.report, "function");
|
|
290
|
+
assert.equal(typeof requestRecoveryRuntime.reload, "function");
|
|
291
|
+
|
|
256
292
|
const errorStore = providedByKey.get("jskit.shell-web.runtime.web-error.presentation-store.client");
|
|
257
293
|
assert.equal(typeof errorStore.getState, "function");
|
|
258
294
|
assert.equal(typeof errorStore.present, "function");
|
|
@@ -319,6 +355,132 @@ test("shell refresh runtime reports recoverable retry errors as banners", async
|
|
|
319
355
|
});
|
|
320
356
|
});
|
|
321
357
|
|
|
358
|
+
test("shell request recovery reports active query transport failures with retry actions", async () => {
|
|
359
|
+
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
360
|
+
const refetchCalls = [];
|
|
361
|
+
const queryCache = createQueryCacheDouble();
|
|
362
|
+
const queryClient = {
|
|
363
|
+
getQueryCache() {
|
|
364
|
+
return queryCache;
|
|
365
|
+
},
|
|
366
|
+
async refetchQueries(filters = {}, options = {}) {
|
|
367
|
+
refetchCalls.push({ filters, options });
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
const app = createAppDouble({ queryClient });
|
|
371
|
+
const provider = new ShellWebClientProvider();
|
|
372
|
+
provider.register(app);
|
|
373
|
+
await provider.boot(app);
|
|
374
|
+
|
|
375
|
+
const failedQuery = {
|
|
376
|
+
queryHash: "[\"project-access\"]",
|
|
377
|
+
queryKey: ["project-access"],
|
|
378
|
+
meta: {
|
|
379
|
+
jskit: {
|
|
380
|
+
requestRecoveryLabel: "Project access"
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
state: {
|
|
384
|
+
status: "error",
|
|
385
|
+
fetchStatus: "idle",
|
|
386
|
+
error: {
|
|
387
|
+
status: 0,
|
|
388
|
+
message: "Network request failed."
|
|
389
|
+
},
|
|
390
|
+
errorUpdateCount: 1
|
|
391
|
+
},
|
|
392
|
+
isActive() {
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
queryCache.emit({ type: "updated", query: failedQuery });
|
|
397
|
+
|
|
398
|
+
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
399
|
+
const state = errorStore.getState();
|
|
400
|
+
assert.equal(state.channels.banner.length, 1);
|
|
401
|
+
assert.equal(
|
|
402
|
+
state.channels.banner[0].message,
|
|
403
|
+
"Project access could not reach the server or network. Check the connection and try again."
|
|
404
|
+
);
|
|
405
|
+
assert.equal(state.channels.banner[0].action.label, "Retry");
|
|
406
|
+
|
|
407
|
+
await state.channels.banner[0].action.handler();
|
|
408
|
+
assert.equal(refetchCalls.length, 1);
|
|
409
|
+
assert.deepEqual(refetchCalls[0].filters, {
|
|
410
|
+
queryKey: ["project-access"],
|
|
411
|
+
exact: true,
|
|
412
|
+
type: "active"
|
|
413
|
+
});
|
|
414
|
+
assert.deepEqual(refetchCalls[0].options, {
|
|
415
|
+
throwOnError: false
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("shell request recovery ignores ordinary active query validation failures", async () => {
|
|
421
|
+
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
422
|
+
const queryCache = createQueryCacheDouble();
|
|
423
|
+
const queryClient = {
|
|
424
|
+
getQueryCache() {
|
|
425
|
+
return queryCache;
|
|
426
|
+
},
|
|
427
|
+
async refetchQueries() {}
|
|
428
|
+
};
|
|
429
|
+
const app = createAppDouble({ queryClient });
|
|
430
|
+
const provider = new ShellWebClientProvider();
|
|
431
|
+
provider.register(app);
|
|
432
|
+
await provider.boot(app);
|
|
433
|
+
|
|
434
|
+
queryCache.emit({
|
|
435
|
+
type: "updated",
|
|
436
|
+
query: {
|
|
437
|
+
queryHash: "[\"project-access\"]",
|
|
438
|
+
queryKey: ["project-access"],
|
|
439
|
+
state: {
|
|
440
|
+
status: "error",
|
|
441
|
+
fetchStatus: "idle",
|
|
442
|
+
error: {
|
|
443
|
+
status: 422,
|
|
444
|
+
message: "Invalid input."
|
|
445
|
+
},
|
|
446
|
+
errorUpdateCount: 1
|
|
447
|
+
},
|
|
448
|
+
isActive() {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
455
|
+
assert.equal(errorStore.getState().channels.banner.length, 0);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("shell bootstrap transport failures report through request recovery", async () => {
|
|
460
|
+
let fetchCalls = 0;
|
|
461
|
+
await withFetchImplementation(async () => {
|
|
462
|
+
fetchCalls += 1;
|
|
463
|
+
throw new TypeError("Failed to fetch");
|
|
464
|
+
}, async () => {
|
|
465
|
+
const app = createAppDouble();
|
|
466
|
+
const provider = new ShellWebClientProvider();
|
|
467
|
+
provider.register(app);
|
|
468
|
+
await provider.boot(app);
|
|
469
|
+
|
|
470
|
+
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
471
|
+
const state = errorStore.getState();
|
|
472
|
+
assert.equal(state.channels.banner.length, 1);
|
|
473
|
+
assert.equal(
|
|
474
|
+
state.channels.banner[0].message,
|
|
475
|
+
"App data could not reach the server or network. Check the connection and try again."
|
|
476
|
+
);
|
|
477
|
+
assert.equal(state.channels.banner[0].action.label, "Retry");
|
|
478
|
+
|
|
479
|
+
await state.channels.banner[0].action.handler();
|
|
480
|
+
assert.equal(fetchCalls, 2);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
322
484
|
test("shell web client provider reports dynamic import failures through async module recovery", async () => {
|
|
323
485
|
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
324
486
|
const routerErrorHandlers = [];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createApp } from "vue";
|
|
4
|
+
import {
|
|
5
|
+
SHELL_REQUEST_RECOVERY_RUNTIME_KEY,
|
|
6
|
+
isRecoverableRequestError,
|
|
7
|
+
requestRecoveryMessage,
|
|
8
|
+
useShellRequestRecoveryRuntime
|
|
9
|
+
} from "../src/client/requestRecovery/index.js";
|
|
10
|
+
|
|
11
|
+
test("shell request recovery classifier accepts transport failures only", () => {
|
|
12
|
+
assert.equal(isRecoverableRequestError({ status: 0, message: "Network request failed." }), true);
|
|
13
|
+
assert.equal(isRecoverableRequestError(new TypeError("Failed to fetch")), true);
|
|
14
|
+
assert.equal(isRecoverableRequestError({ code: "ECONNREFUSED", message: "fetch failed" }), true);
|
|
15
|
+
assert.equal(isRecoverableRequestError({ status: 422, message: "Invalid input." }), false);
|
|
16
|
+
assert.equal(isRecoverableRequestError(new Error("Business rule failed.")), false);
|
|
17
|
+
assert.equal(isRecoverableRequestError({ name: "AbortError", message: "The operation was aborted." }), false);
|
|
18
|
+
assert.equal(isRecoverableRequestError({ code: "ERR_CANCELED", message: "canceled" }), false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("shell request recovery message uses app-facing labels", () => {
|
|
22
|
+
assert.equal(
|
|
23
|
+
requestRecoveryMessage(null, { label: "Project access" }),
|
|
24
|
+
"Project access could not reach the server or network. Check the connection and try again."
|
|
25
|
+
);
|
|
26
|
+
assert.equal(
|
|
27
|
+
requestRecoveryMessage(null, { message: "Custom recovery copy." }),
|
|
28
|
+
"Custom recovery copy."
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("shell request recovery runtime composable returns null outside Vue injection context", () => {
|
|
33
|
+
assert.equal(useShellRequestRecoveryRuntime(), null);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("shell request recovery runtime composable resolves the provided public runtime", () => {
|
|
37
|
+
const runtime = {
|
|
38
|
+
report(error, options) {
|
|
39
|
+
return { error, options };
|
|
40
|
+
},
|
|
41
|
+
async reload() {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const app = createApp({
|
|
46
|
+
render() {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
app.provide(SHELL_REQUEST_RECOVERY_RUNTIME_KEY, runtime);
|
|
51
|
+
|
|
52
|
+
assert.equal(
|
|
53
|
+
app.runWithContext(() => useShellRequestRecoveryRuntime()),
|
|
54
|
+
runtime
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("shell request recovery runtime composable rejects incomplete runtimes", () => {
|
|
59
|
+
const app = createApp({
|
|
60
|
+
render() {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
app.provide(SHELL_REQUEST_RECOVERY_RUNTIME_KEY, {
|
|
65
|
+
report() {}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
assert.equal(
|
|
69
|
+
app.runWithContext(() => useShellRequestRecoveryRuntime()),
|
|
70
|
+
null
|
|
71
|
+
);
|
|
72
|
+
});
|
|
@@ -171,6 +171,29 @@ test("shell-web exports async module recovery runtime access as a public client
|
|
|
171
171
|
assert.match(providerSource, /SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY/);
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
test("shell-web exports request recovery runtime access as a public client API", async () => {
|
|
175
|
+
const packageJson = JSON.parse(await readFile(path.join(PACKAGE_DIR, "package.json"), "utf8"));
|
|
176
|
+
const clientIndex = await readFile(path.join(PACKAGE_DIR, "src", "client", "index.js"), "utf8");
|
|
177
|
+
const recoveryIndex = await readFile(
|
|
178
|
+
path.join(PACKAGE_DIR, "src", "client", "requestRecovery", "index.js"),
|
|
179
|
+
"utf8"
|
|
180
|
+
);
|
|
181
|
+
const providerSource = await readFile(
|
|
182
|
+
path.join(PACKAGE_DIR, "src", "client", "providers", "ShellWebClientProvider.js"),
|
|
183
|
+
"utf8"
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
assert.equal(
|
|
187
|
+
packageJson?.exports?.["./client/requestRecovery"],
|
|
188
|
+
"./src/client/requestRecovery/index.js"
|
|
189
|
+
);
|
|
190
|
+
assert.match(clientIndex, /useShellRequestRecoveryRuntime/);
|
|
191
|
+
assert.match(clientIndex, /SHELL_REQUEST_RECOVERY_RUNTIME_KEY/);
|
|
192
|
+
assert.match(recoveryIndex, /useShellRequestRecoveryRuntime/);
|
|
193
|
+
assert.match(recoveryIndex, /SHELL_REQUEST_RECOVERY_RUNTIME_KEY/);
|
|
194
|
+
assert.match(providerSource, /SHELL_REQUEST_RECOVERY_RUNTIME_KEY/);
|
|
195
|
+
});
|
|
196
|
+
|
|
174
197
|
test("shell-web route transition keeps mobile route motion placement-driven", async () => {
|
|
175
198
|
const source = await readFile(
|
|
176
199
|
path.join(PACKAGE_DIR, "src", "client", "components", "ShellRouteTransition.vue"),
|
|
@@ -298,6 +321,7 @@ test("shell-web descriptor metadata advertises adaptive shell outlets, default l
|
|
|
298
321
|
"runtime.web-bootstrap.client",
|
|
299
322
|
"runtime.web-refresh.client",
|
|
300
323
|
"runtime.web-async-module-recovery.client",
|
|
324
|
+
"runtime.web-request-recovery.client",
|
|
301
325
|
"runtime.web-error.client",
|
|
302
326
|
"runtime.web-error.presentation-store.client"
|
|
303
327
|
]);
|