@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.
- package/package.descriptor.mjs +200 -16
- package/package.json +8 -7
- package/src/client/components/ShellErrorHost.vue +88 -15
- package/src/client/components/ShellLayout.vue +551 -50
- package/src/client/components/ShellOutlet.vue +34 -4
- package/src/client/components/ShellOutletMenuWidget.vue +1 -8
- package/src/client/components/ShellRouteTransition.vue +480 -0
- package/src/client/components/ShellTabLinkItem.vue +22 -6
- package/src/client/composables/useShellLayoutState.js +12 -1
- package/src/client/error/normalize.js +17 -0
- package/src/client/error/policy.js +25 -11
- package/src/client/error/runtime.js +2 -0
- package/src/client/index.js +1 -0
- package/src/client/placement/index.js +5 -0
- package/src/client/placement/runtime.js +149 -16
- package/src/client/placement/validators.js +36 -8
- package/src/client/providers/ShellWebClientProvider.js +189 -24
- package/src/client/stores/useShellLayoutStore.js +21 -1
- package/src/test/adaptiveShellSmoke.js +121 -0
- package/templates/expected-existing/src/pages/home/index.vue +40 -10
- package/templates/src/components/ShellLayout.vue +10 -90
- package/templates/src/components/menus/TabLinkItem.vue +4 -0
- package/templates/src/error.js +7 -1
- package/templates/src/pages/home/index.vue +64 -23
- package/templates/src/pages/home/settings/general/index.vue +12 -9
- package/templates/src/pages/home/settings.vue +68 -24
- package/templates/src/placement.js +7 -6
- package/templates/src/placementTopology.js +149 -0
- package/templates/tests/e2e/adaptive-shell.spec.ts +4 -0
- package/test/errorRuntime.test.js +42 -0
- package/test/linkItemScaffoldContract.test.js +9 -2
- package/test/outletMenuWidgetContract.test.js +2 -2
- package/test/placementRegistry.test.js +3 -3
- package/test/placementRuntime.test.js +144 -14
- package/test/provider.test.js +97 -5
- package/test/settingsPlacementContract.test.js +234 -20
- package/test/useShellLayoutState.test.js +19 -0
|
@@ -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
|
-
|
|
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(
|
|
356
|
-
?
|
|
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:
|
|
492
|
+
placementId: renderablePlacement.id,
|
|
362
493
|
placementSurfaces,
|
|
363
494
|
requestedSurface: normalizedSurface
|
|
364
495
|
});
|
|
365
496
|
continue;
|
|
366
497
|
}
|
|
367
|
-
if (!shouldIncludePlacement(
|
|
498
|
+
if (!shouldIncludePlacement(renderablePlacement, placementContext, runtimeLogger)) {
|
|
368
499
|
debugLog("getPlacements:skip-when", {
|
|
369
|
-
placementId:
|
|
500
|
+
placementId: renderablePlacement.id
|
|
370
501
|
});
|
|
371
502
|
continue;
|
|
372
503
|
}
|
|
373
504
|
|
|
374
505
|
const component = resolvePlacementComponent(
|
|
375
506
|
app,
|
|
376
|
-
|
|
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:
|
|
385
|
-
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:
|
|
392
|
-
componentToken:
|
|
522
|
+
placementId: renderablePlacement.id,
|
|
523
|
+
componentToken: renderablePlacement.componentToken,
|
|
393
524
|
placementSurfaces,
|
|
394
|
-
order:
|
|
525
|
+
order: renderablePlacement.order
|
|
395
526
|
});
|
|
396
527
|
|
|
397
528
|
matches.push(
|
|
398
529
|
Object.freeze({
|
|
399
|
-
...
|
|
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 {
|
|
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
|
|
61
|
-
if (
|
|
62
|
-
return
|
|
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("
|
|
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
|
};
|