@jskit-ai/shell-web 0.1.64 → 0.1.66

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 (37) hide show
  1. package/package.descriptor.mjs +200 -16
  2. package/package.json +8 -7
  3. package/src/client/components/ShellErrorHost.vue +88 -15
  4. package/src/client/components/ShellLayout.vue +551 -50
  5. package/src/client/components/ShellOutlet.vue +34 -4
  6. package/src/client/components/ShellOutletMenuWidget.vue +1 -8
  7. package/src/client/components/ShellRouteTransition.vue +480 -0
  8. package/src/client/components/ShellTabLinkItem.vue +22 -6
  9. package/src/client/composables/useShellLayoutState.js +12 -1
  10. package/src/client/error/normalize.js +17 -0
  11. package/src/client/error/policy.js +25 -11
  12. package/src/client/error/runtime.js +2 -0
  13. package/src/client/index.js +1 -0
  14. package/src/client/placement/index.js +5 -0
  15. package/src/client/placement/runtime.js +149 -16
  16. package/src/client/placement/validators.js +36 -8
  17. package/src/client/providers/ShellWebClientProvider.js +189 -24
  18. package/src/client/stores/useShellLayoutStore.js +21 -1
  19. package/src/test/adaptiveShellSmoke.js +121 -0
  20. package/templates/expected-existing/src/pages/home/index.vue +40 -10
  21. package/templates/src/components/ShellLayout.vue +10 -90
  22. package/templates/src/components/menus/TabLinkItem.vue +4 -0
  23. package/templates/src/error.js +7 -1
  24. package/templates/src/pages/home/index.vue +64 -23
  25. package/templates/src/pages/home/settings/general/index.vue +12 -9
  26. package/templates/src/pages/home/settings.vue +68 -24
  27. package/templates/src/placement.js +7 -6
  28. package/templates/src/placementTopology.js +149 -0
  29. package/templates/tests/e2e/adaptive-shell.spec.ts +4 -0
  30. package/test/errorRuntime.test.js +42 -0
  31. package/test/linkItemScaffoldContract.test.js +9 -2
  32. package/test/outletMenuWidgetContract.test.js +2 -2
  33. package/test/placementRegistry.test.js +3 -3
  34. package/test/placementRuntime.test.js +144 -14
  35. package/test/provider.test.js +97 -5
  36. package/test/settingsPlacementContract.test.js +234 -20
  37. package/test/useShellLayoutState.test.js +19 -0
@@ -2,6 +2,11 @@ export {
2
2
  createPlacementRegistry
3
3
  } from "./registry.js";
4
4
 
5
+ export {
6
+ definePlacement,
7
+ definePlacementTopology
8
+ } from "./validators.js";
9
+
5
10
  export {
6
11
  useWebPlacementContext
7
12
  } from "./inject.js";
@@ -1,6 +1,10 @@
1
1
  import { DEFAULT_DEBUG_DEPTH, explodePayload } from "./debug.js";
2
2
  import { createListenerSubscription } from "@jskit-ai/kernel/shared/support/listenerSet";
3
3
  import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import {
5
+ normalizePlacementLayoutClass,
6
+ normalizePlacementTopologyDefinition
7
+ } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
4
8
  import {
5
9
  isRenderableComponent,
6
10
  normalizePlacementDefinition,
@@ -100,6 +104,40 @@ function normalizePlacementList(placements, context = {}) {
100
104
  .map((entry) => entry.placement);
101
105
  }
102
106
 
107
+ function normalizeTopologyList(topology, context = {}) {
108
+ if (Array.isArray(topology)) {
109
+ const normalized = normalizePlacementTopologyDefinition(
110
+ { placements: topology },
111
+ {
112
+ context: context.source || "placement topology"
113
+ }
114
+ );
115
+ return normalizeTopologyList(normalized, context);
116
+ }
117
+
118
+ const candidates = ensureArray(topology);
119
+ const entries = [];
120
+ for (const candidate of candidates) {
121
+ const normalized = normalizePlacementTopologyDefinition(candidate, {
122
+ context: context.source || "placement topology"
123
+ });
124
+ entries.push(...normalized.placements);
125
+ }
126
+
127
+ const byKey = new Map();
128
+ for (const entry of entries) {
129
+ const key = `${entry.id}::${entry.owner || ""}`;
130
+ if (byKey.has(key)) {
131
+ throw new Error(
132
+ `Duplicate semantic placement "${entry.id}"${entry.owner ? ` for owner "${entry.owner}"` : ""} in ${context.source || "placement topology"}.`
133
+ );
134
+ }
135
+ byKey.set(key, entry);
136
+ }
137
+
138
+ return Object.freeze([...byKey.values()]);
139
+ }
140
+
103
141
  function matchesSurface(placementSurfaces, requestedSurface) {
104
142
  if (requestedSurface === WEB_PLACEMENT_SURFACE_ANY) {
105
143
  return true;
@@ -108,6 +146,67 @@ function matchesSurface(placementSurfaces, requestedSurface) {
108
146
  return surfaces.includes(WEB_PLACEMENT_SURFACE_ANY) || surfaces.includes(requestedSurface);
109
147
  }
110
148
 
149
+ function resolveTopologyPlacement(topologyEntries = [], placement = {}, requestedSurface = WEB_PLACEMENT_SURFACE_ANY) {
150
+ const semanticTarget = String(placement.target || "").trim();
151
+ const owner = String(placement.owner || "").trim();
152
+ const matches = topologyEntries.filter((entry) => {
153
+ if (entry.id !== semanticTarget) {
154
+ return false;
155
+ }
156
+ if (owner && entry.owner !== owner) {
157
+ return false;
158
+ }
159
+ if (!owner && entry.owner) {
160
+ return false;
161
+ }
162
+ return matchesSurface(entry.surfaces, requestedSurface);
163
+ });
164
+
165
+ if (matches.length === 1) {
166
+ return matches[0];
167
+ }
168
+ return null;
169
+ }
170
+
171
+ function resolveRenderablePlacement({
172
+ placement = {},
173
+ topologyEntries = [],
174
+ requestedSurface = WEB_PLACEMENT_SURFACE_ANY,
175
+ requestedTarget = "",
176
+ requestedLayoutClass = "compact"
177
+ } = {}) {
178
+ if (placement.targetType === "concrete") {
179
+ if (placement.target !== requestedTarget) {
180
+ return null;
181
+ }
182
+ return placement;
183
+ }
184
+
185
+ const topologyPlacement = resolveTopologyPlacement(topologyEntries, placement, requestedSurface);
186
+ if (!topologyPlacement) {
187
+ return null;
188
+ }
189
+
190
+ const variant = topologyPlacement.variants?.[requestedLayoutClass] || null;
191
+ if (!variant || variant.outlet !== requestedTarget) {
192
+ return null;
193
+ }
194
+
195
+ const componentToken = String(placement.componentToken || variant.renderers?.[placement.kind] || "").trim();
196
+ if (!componentToken) {
197
+ return null;
198
+ }
199
+
200
+ return Object.freeze({
201
+ ...placement,
202
+ target: requestedTarget,
203
+ semanticTarget: placement.target,
204
+ topologyOwner: topologyPlacement.owner || "",
205
+ layoutClass: requestedLayoutClass,
206
+ componentToken
207
+ });
208
+ }
209
+
111
210
  function resolveContextContributors(app, baseContext = {}, logger) {
112
211
  const contributors = app.resolveTag("web-placement.context.client");
113
212
  let merged = {};
@@ -232,6 +331,7 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
232
331
  const listeners = new Set();
233
332
  const subscribe = createListenerSubscription(listeners);
234
333
  let placementDefinitions = Object.freeze([]);
334
+ let placementTopology = Object.freeze([]);
235
335
  let sharedContext = Object.freeze({});
236
336
  let revision = 0;
237
337
 
@@ -276,6 +376,22 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
276
376
  });
277
377
  }
278
378
 
379
+ function replacePlacementTopology(topology = [], { source = "app placement topology" } = {}) {
380
+ missingTokens.clear();
381
+ invalidComponentTokens.clear();
382
+ failedTokens.clear();
383
+ placementTopology = normalizeTopologyList(topology, { source });
384
+ debugLog("replacePlacementTopology", {
385
+ source,
386
+ count: placementTopology.length,
387
+ ids: placementTopology.map((entry) => entry.owner ? `${entry.id}#${entry.owner}` : entry.id)
388
+ });
389
+ notify({
390
+ type: "placement-topology.replaced",
391
+ source
392
+ });
393
+ }
394
+
279
395
  function getContext() {
280
396
  return sharedContext;
281
397
  }
@@ -308,13 +424,17 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
308
424
  return revision;
309
425
  }
310
426
 
311
- function getPlacements({ surface = WEB_PLACEMENT_SURFACE_ANY, target = "", context = {} } = {}) {
427
+ function getPlacements({ surface = WEB_PLACEMENT_SURFACE_ANY, target = "", layoutClass = "", context = {} } = {}) {
312
428
  const normalizedTarget = normalizePlacementTarget(target, { strict: false });
313
429
  if (!normalizedTarget) {
314
430
  return Object.freeze([]);
315
431
  }
316
432
 
317
433
  const normalizedSurface = normalizeSurface(surface);
434
+ const normalizedLayoutClass =
435
+ normalizePlacementLayoutClass(layoutClass) ||
436
+ normalizePlacementLayoutClass(sharedContext?.layoutClass) ||
437
+ "compact";
318
438
  const baseContext = isRecord(context) ? { ...context } : {};
319
439
  const contextFromRuntime = isRecord(sharedContext) ? sharedContext : {};
320
440
  const contextFromContributors = resolveContextContributors(
@@ -323,6 +443,7 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
323
443
  app,
324
444
  surface: normalizedSurface,
325
445
  target: normalizedTarget,
446
+ layoutClass: normalizedLayoutClass,
326
447
  context: {
327
448
  ...contextFromRuntime,
328
449
  ...baseContext
@@ -336,44 +457,54 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
336
457
  ...baseContext,
337
458
  app,
338
459
  surface: normalizedSurface,
339
- target: normalizedTarget
460
+ target: normalizedTarget,
461
+ layoutClass: normalizedLayoutClass
340
462
  };
341
463
 
342
464
  debugLog("getPlacements:start", {
343
465
  surface: normalizedSurface,
344
466
  target: normalizedTarget,
467
+ layoutClass: normalizedLayoutClass,
345
468
  contextKeys: Object.keys(baseContext),
346
469
  sharedContextKeys: Object.keys(contextFromRuntime),
347
- placementCount: placementDefinitions.length
470
+ placementCount: placementDefinitions.length,
471
+ topologyCount: placementTopology.length
348
472
  });
349
473
 
350
474
  const matches = [];
351
475
  for (const placement of placementDefinitions) {
352
- if (placement.target !== normalizedTarget) {
476
+ const renderablePlacement = resolveRenderablePlacement({
477
+ placement,
478
+ topologyEntries: placementTopology,
479
+ requestedSurface: normalizedSurface,
480
+ requestedTarget: normalizedTarget,
481
+ requestedLayoutClass: normalizedLayoutClass
482
+ });
483
+ if (!renderablePlacement) {
353
484
  continue;
354
485
  }
355
- const placementSurfaces = Array.isArray(placement.surfaces)
356
- ? placement.surfaces
486
+ const placementSurfaces = Array.isArray(renderablePlacement.surfaces)
487
+ ? renderablePlacement.surfaces
357
488
  : [WEB_PLACEMENT_SURFACE_ANY];
358
489
 
359
490
  if (!matchesSurface(placementSurfaces, normalizedSurface)) {
360
491
  debugLog("getPlacements:skip-surfaces", {
361
- placementId: placement.id,
492
+ placementId: renderablePlacement.id,
362
493
  placementSurfaces,
363
494
  requestedSurface: normalizedSurface
364
495
  });
365
496
  continue;
366
497
  }
367
- if (!shouldIncludePlacement(placement, placementContext, runtimeLogger)) {
498
+ if (!shouldIncludePlacement(renderablePlacement, placementContext, runtimeLogger)) {
368
499
  debugLog("getPlacements:skip-when", {
369
- placementId: placement.id
500
+ placementId: renderablePlacement.id
370
501
  });
371
502
  continue;
372
503
  }
373
504
 
374
505
  const component = resolvePlacementComponent(
375
506
  app,
376
- placement,
507
+ renderablePlacement,
377
508
  runtimeLogger,
378
509
  missingTokens,
379
510
  invalidComponentTokens,
@@ -381,22 +512,22 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
381
512
  );
382
513
  if (!component) {
383
514
  debugLog("getPlacements:skip-component", {
384
- placementId: placement.id,
385
- componentToken: placement.componentToken
515
+ placementId: renderablePlacement.id,
516
+ componentToken: renderablePlacement.componentToken
386
517
  });
387
518
  continue;
388
519
  }
389
520
 
390
521
  debugLog("getPlacements:include", {
391
- placementId: placement.id,
392
- componentToken: placement.componentToken,
522
+ placementId: renderablePlacement.id,
523
+ componentToken: renderablePlacement.componentToken,
393
524
  placementSurfaces,
394
- order: placement.order
525
+ order: renderablePlacement.order
395
526
  });
396
527
 
397
528
  matches.push(
398
529
  Object.freeze({
399
- ...placement,
530
+ ...renderablePlacement,
400
531
  component
401
532
  })
402
533
  );
@@ -405,6 +536,7 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
405
536
  debugLog("getPlacements:done", {
406
537
  surface: normalizedSurface,
407
538
  target: normalizedTarget,
539
+ layoutClass: normalizedLayoutClass,
408
540
  resultIds: matches.map((entry) => entry.id)
409
541
  });
410
542
 
@@ -413,6 +545,7 @@ function createWebPlacementRuntime({ app, logger = null } = {}) {
413
545
 
414
546
  return Object.freeze({
415
547
  replacePlacements,
548
+ replacePlacementTopology,
416
549
  getPlacements,
417
550
  getContext,
418
551
  setContext,
@@ -1,5 +1,10 @@
1
1
  import { isRecord, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
- import { normalizeShellOutletTargetId } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
2
+ import {
3
+ normalizePlacementKind,
4
+ normalizePlacementTopologyDefinition,
5
+ normalizeSemanticPlacementId,
6
+ resolvePlacementTargetReference
7
+ } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
3
8
 
4
9
  const WEB_PLACEMENT_SURFACE_ANY = "*";
5
10
 
@@ -57,13 +62,13 @@ function toInteger(value, fallback = 1000) {
57
62
  }
58
63
 
59
64
  function normalizePlacementTarget(value, { strict = false, source = "placement" } = {}) {
60
- const normalized = normalizeShellOutletTargetId(String(value || "").toLowerCase());
61
- if (normalized) {
62
- return normalized;
65
+ const reference = resolvePlacementTargetReference(String(value || "").toLowerCase());
66
+ if (reference?.id) {
67
+ return reference.id;
63
68
  }
64
69
 
65
70
  if (strict) {
66
- throw new TypeError(`${source} requires target in "host:position" format.`);
71
+ throw new TypeError(`${source} requires semantic target in "area.slot" format or internal concrete target in "host:position" format.`);
67
72
  }
68
73
  return "";
69
74
  }
@@ -132,8 +137,19 @@ function normalizePlacementDefinition(value, { strict = false, source = "placeme
132
137
  return null;
133
138
  }
134
139
 
140
+ const targetReference = resolvePlacementTargetReference(target);
141
+ const internal = value.internal === true;
142
+ if (targetReference?.type === "concrete" && internal !== true) {
143
+ if (strict) {
144
+ throw new TypeError(`${source} "${id}" targets concrete outlet "${target}" without internal: true.`);
145
+ }
146
+ return null;
147
+ }
148
+
149
+ const owner = normalizeText(value.owner).toLowerCase();
150
+ const kind = normalizePlacementKind(value.kind) || (normalizeText(value.componentToken) ? "component" : "link");
135
151
  const componentToken = normalizeText(value.componentToken);
136
- if (!componentToken) {
152
+ if (kind === "component" && !componentToken) {
137
153
  if (strict) {
138
154
  throw new TypeError(`${source} "${id}" requires componentToken.`);
139
155
  }
@@ -150,12 +166,16 @@ function normalizePlacementDefinition(value, { strict = false, source = "placeme
150
166
  return Object.freeze({
151
167
  id,
152
168
  target,
169
+ targetType: targetReference?.type || "",
170
+ owner,
171
+ kind,
153
172
  surfaces,
154
173
  order: toInteger(value.order, 1000),
155
174
  componentToken,
156
175
  props,
157
176
  when,
158
- enabled: value.enabled !== false
177
+ enabled: value.enabled !== false,
178
+ internal
159
179
  });
160
180
  }
161
181
 
@@ -166,6 +186,12 @@ function definePlacement(value = {}) {
166
186
  });
167
187
  }
168
188
 
189
+ function definePlacementTopology(value = {}) {
190
+ return normalizePlacementTopologyDefinition(value, {
191
+ context: "placement topology"
192
+ });
193
+ }
194
+
169
195
  export {
170
196
  isRecord,
171
197
  isRenderableComponent,
@@ -174,5 +200,7 @@ export {
174
200
  normalizePlacementTarget,
175
201
  normalizePlacementSurfaces,
176
202
  normalizePlacementDefinition,
177
- definePlacement
203
+ definePlacement,
204
+ definePlacementTopology,
205
+ normalizeSemanticPlacementId
178
206
  };
@@ -1,11 +1,8 @@
1
1
  import { getClientAppConfig } from "@jskit-ai/kernel/client";
2
2
  import {
3
- isRecord,
4
- shouldRetryTransientQueryFailure,
5
- transientQueryRetryDelay
3
+ isRecord
6
4
  } from "@jskit-ai/kernel/shared/support";
7
5
  import { createProviderLogger as createSharedProviderLogger } from "@jskit-ai/kernel/shared/support/providerLogger";
8
- import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
9
6
  import {
10
7
  createDefaultErrorPolicy
11
8
  } from "../error/policy.js";
@@ -30,21 +27,9 @@ import { resolveBootstrapErrorStatusCode } from "../bootstrap/bootstrapErrorStat
30
27
 
31
28
  // Keep this constant for diagnostics, but keep import() below as a literal string so Vite can statically analyze it.
32
29
  const APP_PLACEMENT_MODULE_SPECIFIER = "/src/placement.js";
30
+ const APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER = "/src/placementTopology.js";
33
31
  const APP_ERROR_MODULE_SPECIFIER = "/src/error.js";
34
32
 
35
- function createShellWebQueryClient() {
36
- return new QueryClient({
37
- defaultOptions: {
38
- queries: {
39
- refetchOnWindowFocus: false,
40
- refetchOnReconnect: true,
41
- retry: shouldRetryTransientQueryFailure,
42
- retryDelay: transientQueryRetryDelay
43
- }
44
- }
45
- });
46
- }
47
-
48
33
  function isMissingDynamicModule(error, moduleSpecifier) {
49
34
  const message = String(error?.message || error || "");
50
35
  return (
@@ -88,6 +73,47 @@ async function loadAppPlacementDefinitions(logger) {
88
73
  return [];
89
74
  }
90
75
 
76
+ async function loadAppPlacementTopology(logger) {
77
+ try {
78
+ const moduleNamespace = await import("/src/placementTopology.js");
79
+ const exported = moduleNamespace?.default;
80
+ return resolveAppPlacementTopologyExport(exported, logger);
81
+ } catch (error) {
82
+ if (isMissingDynamicModule(error, APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER)) {
83
+ return [];
84
+ }
85
+
86
+ logger.warn(
87
+ {
88
+ module: APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER,
89
+ error: String(error?.message || error || "unknown error")
90
+ },
91
+ "Failed to load app placement topology module; using empty topology."
92
+ );
93
+ }
94
+
95
+ return [];
96
+ }
97
+
98
+ function resolveAppPlacementTopologyExport(exported, logger) {
99
+ const resolved = typeof exported === "function" ? exported() : exported;
100
+ if (Array.isArray(resolved)) {
101
+ return resolved;
102
+ }
103
+ if (resolved && typeof resolved === "object") {
104
+ return resolved;
105
+ }
106
+
107
+ logger.warn(
108
+ {
109
+ module: APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER,
110
+ exportedType: typeof exported
111
+ },
112
+ "App placement topology module default export did not resolve to an object or array; using empty topology."
113
+ );
114
+ return [];
115
+ }
116
+
91
117
  function createErrorConfigToolkit(errorRuntime) {
92
118
  return Object.freeze({
93
119
  createDefaultErrorPolicy,
@@ -160,6 +186,136 @@ function applyAppErrorConfig(errorRuntime, errorConfig = {}) {
160
186
  errorRuntime.assertBootReady();
161
187
  }
162
188
 
189
+ function isPullRefreshQuery(query = null) {
190
+ const meta = isRecord(query?.meta) ? query.meta : {};
191
+ if (meta.jskitRefresh === "pull") {
192
+ return true;
193
+ }
194
+ if (meta.jskitRefresh === false) {
195
+ return false;
196
+ }
197
+
198
+ const jskitMeta = isRecord(meta.jskit) ? meta.jskit : {};
199
+ return jskitMeta.refreshOnPull !== false;
200
+ }
201
+
202
+ function createShellRefreshRuntime({
203
+ app,
204
+ logger = null
205
+ } = {}) {
206
+ if (!app || typeof app.has !== "function" || typeof app.make !== "function") {
207
+ throw new Error("createShellRefreshRuntime requires application has()/make().");
208
+ }
209
+
210
+ const runtimeLogger = logger || createSharedProviderLogger(app);
211
+ let refreshQueue = Promise.resolve(null);
212
+
213
+ async function refreshBootstrap(reason) {
214
+ if (!app.has("runtime.web-bootstrap.client")) {
215
+ return false;
216
+ }
217
+
218
+ const bootstrapRuntime = app.make("runtime.web-bootstrap.client");
219
+ if (!bootstrapRuntime || typeof bootstrapRuntime.refresh !== "function") {
220
+ return false;
221
+ }
222
+
223
+ await bootstrapRuntime.refresh(reason);
224
+ return true;
225
+ }
226
+
227
+ async function refetchPullQueries() {
228
+ if (!app.has("jskit.client.query-client")) {
229
+ return false;
230
+ }
231
+
232
+ const queryClient = app.make("jskit.client.query-client");
233
+ if (!queryClient || typeof queryClient.refetchQueries !== "function") {
234
+ return false;
235
+ }
236
+
237
+ await queryClient.refetchQueries(
238
+ {
239
+ type: "active",
240
+ predicate: isPullRefreshQuery
241
+ },
242
+ {
243
+ throwOnError: false
244
+ }
245
+ );
246
+ return true;
247
+ }
248
+
249
+ function reportRefreshFailure(error) {
250
+ runtimeLogger.warn(
251
+ {
252
+ error: String(error?.message || error || "unknown error")
253
+ },
254
+ "Shell refresh failed."
255
+ );
256
+
257
+ if (!app.has("runtime.web-error.client")) {
258
+ return;
259
+ }
260
+
261
+ const errorRuntime = app.make("runtime.web-error.client");
262
+ if (!errorRuntime || typeof errorRuntime.report !== "function") {
263
+ return;
264
+ }
265
+
266
+ errorRuntime.report({
267
+ source: "shell-web.refresh",
268
+ message: "Unable to refresh. Check the connection and try again.",
269
+ intent: "app-recoverable",
270
+ severity: "error",
271
+ dedupeKey: "shell-web.refresh.failed",
272
+ dedupeWindowMs: 2000,
273
+ action: {
274
+ label: "Retry",
275
+ dismissOnRun: true,
276
+ handler() {
277
+ void refresh("retry");
278
+ }
279
+ }
280
+ });
281
+ }
282
+
283
+ async function performRefresh(reason = "manual") {
284
+ const normalizedReason = String(reason || "manual").trim() || "manual";
285
+ try {
286
+ const [bootstrapRefreshed, queriesRefetched] = await Promise.all([
287
+ refreshBootstrap(normalizedReason),
288
+ refetchPullQueries()
289
+ ]);
290
+
291
+ return Object.freeze({
292
+ reason: normalizedReason,
293
+ bootstrapRefreshed,
294
+ queriesRefetched
295
+ });
296
+ } catch (error) {
297
+ reportRefreshFailure(error);
298
+ return Object.freeze({
299
+ reason: normalizedReason,
300
+ bootstrapRefreshed: false,
301
+ queriesRefetched: false,
302
+ error
303
+ });
304
+ }
305
+ }
306
+
307
+ function refresh(reason = "manual") {
308
+ refreshQueue = refreshQueue
309
+ .catch(() => null)
310
+ .then(() => performRefresh(reason));
311
+ return refreshQueue;
312
+ }
313
+
314
+ return Object.freeze({
315
+ refresh
316
+ });
317
+ }
318
+
163
319
  function installVueErrorBridge(vueApp, errorRuntime, logger) {
164
320
  if (!vueApp || !isRecord(vueApp.config)) {
165
321
  return;
@@ -173,8 +329,8 @@ function installVueErrorBridge(vueApp, errorRuntime, logger) {
173
329
  source: "shell-web.vue.error-handler",
174
330
  message: String(error?.message || "Unexpected UI error."),
175
331
  cause: error,
332
+ intent: "blocking",
176
333
  severity: "error",
177
- channel: "dialog",
178
334
  details: {
179
335
  info: String(info || "")
180
336
  }
@@ -211,8 +367,8 @@ function installRouterErrorBridge(app, errorRuntime, logger) {
211
367
  source: "shell-web.router.on-error",
212
368
  message: String(error?.message || "Navigation failed."),
213
369
  cause: error,
370
+ intent: "app-recoverable",
214
371
  severity: "error",
215
- channel: "banner",
216
372
  dedupeKey: String(error?.message || "navigation-failed"),
217
373
  dedupeWindowMs: 2000
218
374
  });
@@ -275,7 +431,12 @@ class ShellWebClientProvider {
275
431
  logger
276
432
  })
277
433
  );
278
- app.singleton("shell.web.query-client", () => createShellWebQueryClient());
434
+ app.singleton("runtime.web-refresh.client", (scope) =>
435
+ createShellRefreshRuntime({
436
+ app: scope,
437
+ logger
438
+ })
439
+ );
279
440
  app.singleton("runtime.web-error.presentation-store.client", () => createErrorPresentationStore());
280
441
  app.singleton("runtime.web-error.client", (scope) =>
281
442
  createErrorRuntime({
@@ -297,6 +458,10 @@ class ShellWebClientProvider {
297
458
  const logger = createSharedProviderLogger(isRecord(app) ? app : null);
298
459
  const placementRuntime = app.make("runtime.web-placement.client");
299
460
  if (placementRuntime && typeof placementRuntime.replacePlacements === "function") {
461
+ const placementTopology = await loadAppPlacementTopology(logger);
462
+ if (typeof placementRuntime.replacePlacementTopology === "function") {
463
+ placementRuntime.replacePlacementTopology(placementTopology, { source: APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER });
464
+ }
300
465
  const placements = await loadAppPlacementDefinitions(logger);
301
466
  placementRuntime.replacePlacements(placements, { source: APP_PLACEMENT_MODULE_SPECIFIER });
302
467
  const appConfig = getClientAppConfig();
@@ -343,12 +508,11 @@ class ShellWebClientProvider {
343
508
  throw new Error("ShellWebClientProvider requires Pinia installed in the client app.");
344
509
  }
345
510
  const errorPresentationStore = app.make("runtime.web-error.presentation-store.client");
511
+ const refreshRuntime = app.make("runtime.web-refresh.client");
346
512
  useShellErrorPresentationStore(pinia).attachRuntimeStore(errorPresentationStore);
347
513
 
348
- vueApp.use(VueQueryPlugin, {
349
- queryClient: app.make("shell.web.query-client")
350
- });
351
514
  vueApp.provide("jskit.shell-web.runtime.web-placement.client", placementRuntime);
515
+ vueApp.provide("jskit.shell-web.runtime.web-refresh.client", refreshRuntime);
352
516
  vueApp.provide("jskit.shell-web.runtime.web-error.client", errorRuntime);
353
517
  vueApp.provide(
354
518
  "jskit.shell-web.runtime.web-error.presentation-store.client",
@@ -361,5 +525,6 @@ class ShellWebClientProvider {
361
525
  }
362
526
 
363
527
  export {
364
- ShellWebClientProvider
528
+ ShellWebClientProvider,
529
+ resolveAppPlacementTopologyExport
365
530
  };