@jskit-ai/shell-web 0.1.89 → 0.1.91

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.
@@ -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.89",
4
+ version: "0.1.91",
5
5
  kind: "runtime",
6
6
  description: "Web shell layout runtime with outlet-based placement contributions.",
7
7
  dependsOn: [],
@@ -300,7 +300,7 @@ export default Object.freeze({
300
300
  dependencies: {
301
301
  runtime: {
302
302
  "@mdi/js": "^7.4.47",
303
- "@jskit-ai/kernel": "0.1.90"
303
+ "@jskit-ai/kernel": "0.1.92"
304
304
  },
305
305
  dev: {}
306
306
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/shell-web",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@mdi/js": "^7.4.47",
31
- "@jskit-ai/kernel": "0.1.90"
31
+ "@jskit-ai/kernel": "0.1.92"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "pinia": "^3.0.4",
@@ -31,6 +31,11 @@ const CANCELED_REQUEST_ERROR_CODES = Object.freeze(new Set([
31
31
  "ERR_CANCELED"
32
32
  ]));
33
33
 
34
+ const SAFE_REQUEST_RECOVERY_METHODS = Object.freeze(new Set([
35
+ "GET",
36
+ "HEAD"
37
+ ]));
38
+
34
39
  function normalizeText(value, fallback = "") {
35
40
  const normalized = String(value || "").trim();
36
41
  return normalized || String(fallback || "").trim();
@@ -142,6 +147,56 @@ function resolveQueryRecoveryLabel(query = null) {
142
147
  );
143
148
  }
144
149
 
150
+ function resolveQueryRecoverySource(query = null) {
151
+ const meta = resolveQueryMeta(query);
152
+ const jskitMeta = isRecord(meta.jskit) ? meta.jskit : {};
153
+
154
+ return normalizeText(
155
+ jskitMeta.requestRecoverySource ||
156
+ meta.jskitRequestRecoverySource ||
157
+ meta.requestRecoverySource,
158
+ "shell-web.request-recovery.query"
159
+ );
160
+ }
161
+
162
+ function resolveQueryRecoveryDedupeKey(query = null, fallback = "") {
163
+ const meta = resolveQueryMeta(query);
164
+ const jskitMeta = isRecord(meta.jskit) ? meta.jskit : {};
165
+
166
+ return normalizeText(
167
+ jskitMeta.requestRecoveryDedupeKey ||
168
+ meta.jskitRequestRecoveryDedupeKey ||
169
+ meta.requestRecoveryDedupeKey,
170
+ fallback
171
+ );
172
+ }
173
+
174
+ function resolveQueryRecoveryDedupeWindowMs(query = null) {
175
+ const meta = resolveQueryMeta(query);
176
+ const jskitMeta = isRecord(meta.jskit) ? meta.jskit : {};
177
+ const value =
178
+ jskitMeta.requestRecoveryDedupeWindowMs ??
179
+ meta.jskitRequestRecoveryDedupeWindowMs ??
180
+ meta.requestRecoveryDedupeWindowMs;
181
+
182
+ return Number.isFinite(Number(value)) ? Math.max(0, Number(value)) : null;
183
+ }
184
+
185
+ function resolveQueryRecoveryMethod(query = null) {
186
+ const meta = resolveQueryMeta(query);
187
+ const jskitMeta = isRecord(meta.jskit) ? meta.jskit : {};
188
+
189
+ return normalizeText(
190
+ jskitMeta.requestRecoveryMethod ||
191
+ meta.jskitRequestRecoveryMethod ||
192
+ meta.requestRecoveryMethod
193
+ ).toUpperCase();
194
+ }
195
+
196
+ function isSafeQueryRecoveryMethod(query = null) {
197
+ return SAFE_REQUEST_RECOVERY_METHODS.has(resolveQueryRecoveryMethod(query));
198
+ }
199
+
145
200
  function isActiveQuery(query = null) {
146
201
  if (typeof query?.isActive === "function") {
147
202
  return Boolean(query.isActive());
@@ -223,7 +278,12 @@ function installRecoverableQueryObserver({
223
278
  const reportedErrorUpdateByQueryHash = new Map();
224
279
 
225
280
  function inspectQuery(query = null) {
226
- if (!query || isRequestRecoveryDisabled(query) || !isActiveQuery(query)) {
281
+ if (
282
+ !query ||
283
+ isRequestRecoveryDisabled(query) ||
284
+ !isSafeQueryRecoveryMethod(query) ||
285
+ !isActiveQuery(query)
286
+ ) {
227
287
  return;
228
288
  }
229
289
 
@@ -241,12 +301,19 @@ function installRecoverableQueryObserver({
241
301
  }
242
302
  reportedErrorUpdateByQueryHash.set(queryHash, reportKey);
243
303
 
304
+ const source = resolveQueryRecoverySource(query);
305
+ const dedupeKey = resolveQueryRecoveryDedupeKey(
306
+ query,
307
+ `shell-web.request-recovery.query.${queryHash}.${errorUpdateCount}`
308
+ );
309
+ const dedupeWindowMs = resolveQueryRecoveryDedupeWindowMs(query);
244
310
  runtime.report(error, {
245
311
  label: resolveQueryRecoveryLabel(query),
246
312
  retry: createQueryRetry(queryClient, query),
247
- source: "shell-web.request-recovery.query",
313
+ source,
248
314
  stale: query?.state?.data !== undefined,
249
- dedupeKey: `shell-web.request-recovery.query.${queryHash}.${errorUpdateCount}`
315
+ dedupeKey,
316
+ ...(dedupeWindowMs !== null ? { dedupeWindowMs } : {})
250
317
  });
251
318
  }
252
319
 
@@ -371,13 +371,21 @@ test("shell request recovery reports active query transport failures with retry
371
371
  const provider = new ShellWebClientProvider();
372
372
  provider.register(app);
373
373
  await provider.boot(app);
374
+ const reportEvents = [];
375
+ app.make("runtime.web-error.client").subscribe((event = {}) => {
376
+ reportEvents.push(event);
377
+ });
374
378
 
375
379
  const failedQuery = {
376
380
  queryHash: "[\"project-access\"]",
377
381
  queryKey: ["project-access"],
378
382
  meta: {
379
383
  jskit: {
380
- requestRecoveryLabel: "Project access"
384
+ requestRecoveryLabel: "Project access",
385
+ requestRecoverySource: "project-access.panel",
386
+ requestRecoveryDedupeKey: "project-access",
387
+ requestRecoveryMethod: "GET",
388
+ requestRecoveryDedupeWindowMs: 100
381
389
  }
382
390
  },
383
391
  state: {
@@ -403,6 +411,9 @@ test("shell request recovery reports active query transport failures with retry
403
411
  "Project access could not reach the server or network. Check the connection and try again."
404
412
  );
405
413
  assert.equal(state.channels.banner[0].action.label, "Retry");
414
+ assert.equal(state.channels.banner[0].dedupeKey, "project-access");
415
+ assert.equal(reportEvents[0]?.result?.event?.source, "project-access.panel");
416
+ assert.equal(reportEvents[0]?.result?.decision?.dedupeWindowMs, 100);
406
417
 
407
418
  await state.channels.banner[0].action.handler();
408
419
  assert.equal(refetchCalls.length, 1);
@@ -417,6 +428,55 @@ test("shell request recovery reports active query transport failures with retry
417
428
  });
418
429
  });
419
430
 
431
+ test("shell request recovery ignores query transport failures without a safe read method", async () => {
432
+ await withFetchStub({ surfaceAccess: {} }, async () => {
433
+ const queryCache = createQueryCacheDouble();
434
+ const queryClient = {
435
+ getQueryCache() {
436
+ return queryCache;
437
+ },
438
+ async refetchQueries() {}
439
+ };
440
+ const app = createAppDouble({ queryClient });
441
+ const provider = new ShellWebClientProvider();
442
+ provider.register(app);
443
+ await provider.boot(app);
444
+
445
+ for (const [queryKey, meta] of [
446
+ [["unmarked"], {}],
447
+ [["unsafe"], {
448
+ jskit: {
449
+ requestRecoveryMethod: "POST"
450
+ }
451
+ }]
452
+ ]) {
453
+ queryCache.emit({
454
+ type: "updated",
455
+ query: {
456
+ queryHash: JSON.stringify(queryKey),
457
+ queryKey,
458
+ meta,
459
+ state: {
460
+ status: "error",
461
+ fetchStatus: "idle",
462
+ error: {
463
+ status: 0,
464
+ message: "Network request failed."
465
+ },
466
+ errorUpdateCount: 1
467
+ },
468
+ isActive() {
469
+ return true;
470
+ }
471
+ }
472
+ });
473
+ }
474
+
475
+ const errorStore = app.make("runtime.web-error.presentation-store.client");
476
+ assert.equal(errorStore.getState().channels.banner.length, 0);
477
+ });
478
+ });
479
+
420
480
  test("shell request recovery ignores ordinary active query validation failures", async () => {
421
481
  await withFetchStub({ surfaceAccess: {} }, async () => {
422
482
  const queryCache = createQueryCacheDouble();
@@ -433,10 +493,15 @@ test("shell request recovery ignores ordinary active query validation failures",
433
493
 
434
494
  queryCache.emit({
435
495
  type: "updated",
436
- query: {
437
- queryHash: "[\"project-access\"]",
438
- queryKey: ["project-access"],
439
- state: {
496
+ query: {
497
+ queryHash: "[\"project-access\"]",
498
+ queryKey: ["project-access"],
499
+ meta: {
500
+ jskit: {
501
+ requestRecoveryMethod: "GET"
502
+ }
503
+ },
504
+ state: {
440
505
  status: "error",
441
506
  fetchStatus: "idle",
442
507
  error: {