@jskit-ai/shell-web 0.1.4
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 +165 -0
- package/package.json +23 -0
- package/src/client/components/ShellErrorHost.vue +208 -0
- package/src/client/components/ShellLayout.vue +191 -0
- package/src/client/components/ShellOutlet.vue +95 -0
- package/src/client/components/useShellLayout.js +93 -0
- package/src/client/error/index.js +2 -0
- package/src/client/error/inject.js +142 -0
- package/src/client/error/normalize.js +75 -0
- package/src/client/error/policy.js +50 -0
- package/src/client/error/presenters.js +89 -0
- package/src/client/error/runtime.js +418 -0
- package/src/client/error/store.js +176 -0
- package/src/client/error/tokens.js +14 -0
- package/src/client/index.js +17 -0
- package/src/client/navigation/linkResolver.js +117 -0
- package/src/client/placement/debug.js +52 -0
- package/src/client/placement/index.js +26 -0
- package/src/client/placement/inject.js +104 -0
- package/src/client/placement/pathname.js +14 -0
- package/src/client/placement/registry.js +41 -0
- package/src/client/placement/runtime.js +435 -0
- package/src/client/placement/surfaceContext.js +290 -0
- package/src/client/placement/tokens.js +29 -0
- package/src/client/placement/validators.js +210 -0
- package/src/client/providers/ShellWebClientProvider.js +352 -0
- package/templates/src/App.vue +11 -0
- package/templates/src/components/ShellLayout.vue +247 -0
- package/templates/src/error.js +13 -0
- package/templates/src/pages/console/index.vue +24 -0
- package/templates/src/pages/console.vue +20 -0
- package/templates/src/pages/home/index.vue +54 -0
- package/templates/src/pages/home.vue +20 -0
- package/templates/src/placement.js +12 -0
- package/test/errorRuntime.test.js +191 -0
- package/test/errorStore.test.js +26 -0
- package/test/linkResolver.test.js +112 -0
- package/test/placementRegistry.test.js +45 -0
- package/test/placementRuntime.test.js +374 -0
- package/test/provider.test.js +163 -0
- package/test/surfaceContext.test.js +184 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isRecord,
|
|
3
|
+
normalizeAction,
|
|
4
|
+
normalizeChannel,
|
|
5
|
+
normalizeNonNegativeInteger,
|
|
6
|
+
normalizeSeverity,
|
|
7
|
+
normalizeText
|
|
8
|
+
} from "./normalize.js";
|
|
9
|
+
import { createDefaultErrorPolicy } from "./policy.js";
|
|
10
|
+
|
|
11
|
+
function createRuntimeLogger(logger = null) {
|
|
12
|
+
const source = isRecord(logger) ? logger : null;
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
warn: typeof source?.warn === "function" ? source.warn.bind(source) : () => {},
|
|
15
|
+
error: typeof source?.error === "function" ? source.error.bind(source) : () => {}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeErrorEvent(rawEvent = {}) {
|
|
20
|
+
const source = isRecord(rawEvent) ? rawEvent : { message: rawEvent };
|
|
21
|
+
const cause = source.cause !== undefined ? source.cause : source.error;
|
|
22
|
+
|
|
23
|
+
const statusCandidate = Number(
|
|
24
|
+
source.status || source.statusCode || cause?.status || cause?.statusCode || 0
|
|
25
|
+
);
|
|
26
|
+
const status = Number.isFinite(statusCandidate) ? Math.trunc(statusCandidate) : 0;
|
|
27
|
+
|
|
28
|
+
const fieldErrors = isRecord(source.fieldErrors)
|
|
29
|
+
? Object.freeze({ ...source.fieldErrors })
|
|
30
|
+
: isRecord(source.details?.fieldErrors)
|
|
31
|
+
? Object.freeze({ ...source.details.fieldErrors })
|
|
32
|
+
: null;
|
|
33
|
+
|
|
34
|
+
const details = isRecord(source.details) ? Object.freeze({ ...source.details }) : null;
|
|
35
|
+
|
|
36
|
+
const userMessage = normalizeText(source.userMessage);
|
|
37
|
+
const runtimeMessage = normalizeText(source.message || cause?.message);
|
|
38
|
+
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
code: normalizeText(source.code || cause?.code),
|
|
41
|
+
status,
|
|
42
|
+
source: normalizeText(source.source, "app"),
|
|
43
|
+
message: normalizeText(userMessage || runtimeMessage, "Request failed."),
|
|
44
|
+
userMessage,
|
|
45
|
+
severity: normalizeSeverity(source.severity, "error"),
|
|
46
|
+
channel: normalizeChannel(source.channel),
|
|
47
|
+
presenterId: normalizeText(source.presenterId),
|
|
48
|
+
action: normalizeAction(source.action),
|
|
49
|
+
persist: typeof source.persist === "boolean" ? source.persist : null,
|
|
50
|
+
blocking: source.blocking === true,
|
|
51
|
+
dedupeKey: normalizeText(source.dedupeKey),
|
|
52
|
+
dedupeWindowMs: normalizeNonNegativeInteger(source.dedupeWindowMs, 0),
|
|
53
|
+
traceId: normalizeText(source.traceId),
|
|
54
|
+
fieldErrors,
|
|
55
|
+
details,
|
|
56
|
+
cause: cause || null,
|
|
57
|
+
timestamp: Number(Date.now())
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizePolicyDecision(policyDecision = {}, event = {}) {
|
|
62
|
+
const source = isRecord(policyDecision) ? policyDecision : {};
|
|
63
|
+
const channel = normalizeChannel(source.channel || event.channel, "snackbar") || "snackbar";
|
|
64
|
+
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
channel,
|
|
67
|
+
presenterId: normalizeText(source.presenterId || event.presenterId),
|
|
68
|
+
message: normalizeText(source.message || event.userMessage || event.message, "Request failed."),
|
|
69
|
+
severity: normalizeSeverity(source.severity || event.severity, "error"),
|
|
70
|
+
action: normalizeAction(source.action || event.action),
|
|
71
|
+
persist:
|
|
72
|
+
typeof source.persist === "boolean"
|
|
73
|
+
? source.persist
|
|
74
|
+
: typeof event.persist === "boolean"
|
|
75
|
+
? event.persist
|
|
76
|
+
: channel !== "snackbar",
|
|
77
|
+
dedupeKey: normalizeText(source.dedupeKey || event.dedupeKey),
|
|
78
|
+
dedupeWindowMs: normalizeNonNegativeInteger(source.dedupeWindowMs, event.dedupeWindowMs || 0)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizePresenter(candidate = {}) {
|
|
83
|
+
const source = isRecord(candidate) ? candidate : {};
|
|
84
|
+
const id = normalizeText(source.id);
|
|
85
|
+
if (!id) {
|
|
86
|
+
throw new Error("Error presenter requires id.");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof source.present !== "function") {
|
|
90
|
+
throw new Error(`Error presenter "${id}" requires present(payload).`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
id,
|
|
95
|
+
supports: typeof source.supports === "function" ? source.supports.bind(source) : () => true,
|
|
96
|
+
present: source.present.bind(source),
|
|
97
|
+
dismiss: typeof source.dismiss === "function" ? source.dismiss.bind(source) : null
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createErrorRuntime({
|
|
102
|
+
presenters = [],
|
|
103
|
+
policy = null,
|
|
104
|
+
defaultPresenterId = "",
|
|
105
|
+
moduleDefaultPresenterId = "",
|
|
106
|
+
logger = null
|
|
107
|
+
} = {}) {
|
|
108
|
+
const runtimeLogger = createRuntimeLogger(logger);
|
|
109
|
+
const byPresenterId = new Map();
|
|
110
|
+
const listeners = new Set();
|
|
111
|
+
const dedupeWindowByKey = new Map();
|
|
112
|
+
let activePolicy = typeof policy === "function" ? policy : createDefaultErrorPolicy();
|
|
113
|
+
let activeAppDefaultPresenterId = normalizeText(defaultPresenterId);
|
|
114
|
+
const activeModuleDefaultPresenterId = normalizeText(moduleDefaultPresenterId);
|
|
115
|
+
|
|
116
|
+
function getPresenterIds() {
|
|
117
|
+
return Object.freeze([...byPresenterId.keys()].sort((left, right) => left.localeCompare(right)));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function assertPresenterExists(presenterId, label) {
|
|
121
|
+
if (!byPresenterId.has(presenterId)) {
|
|
122
|
+
throw new Error(`${label} presenter "${presenterId}" is not registered.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveDefaultPresenterId() {
|
|
127
|
+
if (activeAppDefaultPresenterId) {
|
|
128
|
+
assertPresenterExists(activeAppDefaultPresenterId, "App default error");
|
|
129
|
+
return activeAppDefaultPresenterId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (activeModuleDefaultPresenterId) {
|
|
133
|
+
assertPresenterExists(activeModuleDefaultPresenterId, "Module default error");
|
|
134
|
+
return activeModuleDefaultPresenterId;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error("Error runtime requires app default presenter or module default presenter.");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function assertBootReady() {
|
|
141
|
+
resolveDefaultPresenterId();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function registerPresenter(presenter) {
|
|
145
|
+
const normalized = normalizePresenter(presenter);
|
|
146
|
+
if (byPresenterId.has(normalized.id)) {
|
|
147
|
+
throw new Error(`Error presenter "${normalized.id}" is already registered.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
byPresenterId.set(normalized.id, normalized);
|
|
151
|
+
return normalized.id;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function registerPresenters(nextPresenters = []) {
|
|
155
|
+
const source = Array.isArray(nextPresenters) ? nextPresenters : [];
|
|
156
|
+
const ids = [];
|
|
157
|
+
for (const presenter of source) {
|
|
158
|
+
ids.push(registerPresenter(presenter));
|
|
159
|
+
}
|
|
160
|
+
return Object.freeze(ids);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function setPolicy(nextPolicy) {
|
|
164
|
+
if (typeof nextPolicy !== "function") {
|
|
165
|
+
throw new TypeError("Error policy must be a function.");
|
|
166
|
+
}
|
|
167
|
+
activePolicy = nextPolicy;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function setAppDefaultPresenterId(nextDefaultPresenterId = "") {
|
|
171
|
+
const normalized = normalizeText(nextDefaultPresenterId);
|
|
172
|
+
if (normalized) {
|
|
173
|
+
assertPresenterExists(normalized, "App default error");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
activeAppDefaultPresenterId = normalized;
|
|
177
|
+
return activeAppDefaultPresenterId;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function subscribe(listener) {
|
|
181
|
+
if (typeof listener !== "function") {
|
|
182
|
+
return () => {};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
listeners.add(listener);
|
|
186
|
+
return () => {
|
|
187
|
+
listeners.delete(listener);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function notify(event = {}) {
|
|
192
|
+
const payload = Object.freeze({ ...event });
|
|
193
|
+
for (const listener of listeners) {
|
|
194
|
+
try {
|
|
195
|
+
listener(payload);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
runtimeLogger.warn(
|
|
198
|
+
{
|
|
199
|
+
error: String(error?.message || error || "unknown error")
|
|
200
|
+
},
|
|
201
|
+
"Error runtime subscriber failed."
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function resolveChannelCompatiblePresenter(channel = "") {
|
|
208
|
+
for (const presenter of byPresenterId.values()) {
|
|
209
|
+
if (presenter.supports(channel)) {
|
|
210
|
+
return presenter;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolvePresenterForDecision(decision) {
|
|
218
|
+
const explicitPresenterId = normalizeText(decision.presenterId);
|
|
219
|
+
if (explicitPresenterId) {
|
|
220
|
+
assertPresenterExists(explicitPresenterId, "Policy-selected error");
|
|
221
|
+
return byPresenterId.get(explicitPresenterId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const defaultPresenterId = resolveDefaultPresenterId();
|
|
225
|
+
const defaultPresenter = byPresenterId.get(defaultPresenterId);
|
|
226
|
+
if (defaultPresenter?.supports(decision.channel)) {
|
|
227
|
+
return defaultPresenter;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const compatiblePresenter = resolveChannelCompatiblePresenter(decision.channel);
|
|
231
|
+
if (!compatiblePresenter) {
|
|
232
|
+
throw new Error(`No error presenter supports channel "${decision.channel}".`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return compatiblePresenter;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function shouldSkipByDedupe(decision) {
|
|
239
|
+
if (!decision.dedupeKey || decision.dedupeWindowMs < 1) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const now = Number(Date.now());
|
|
244
|
+
const previousTimestamp = Number(dedupeWindowByKey.get(decision.dedupeKey) || 0);
|
|
245
|
+
if (now - previousTimestamp < decision.dedupeWindowMs) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
dedupeWindowByKey.set(decision.dedupeKey, now);
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function report(rawEvent = {}, rawContext = {}) {
|
|
254
|
+
const event = normalizeErrorEvent(rawEvent);
|
|
255
|
+
const context = isRecord(rawContext) ? Object.freeze({ ...rawContext }) : Object.freeze({});
|
|
256
|
+
|
|
257
|
+
let policyDecision;
|
|
258
|
+
try {
|
|
259
|
+
policyDecision = activePolicy(
|
|
260
|
+
event,
|
|
261
|
+
Object.freeze({
|
|
262
|
+
context,
|
|
263
|
+
runtime: Object.freeze({
|
|
264
|
+
presenterIds: getPresenterIds(),
|
|
265
|
+
appDefaultPresenterId: activeAppDefaultPresenterId,
|
|
266
|
+
moduleDefaultPresenterId: activeModuleDefaultPresenterId
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
runtimeLogger.error(
|
|
272
|
+
{
|
|
273
|
+
source: event.source,
|
|
274
|
+
error: String(error?.message || error || "unknown error")
|
|
275
|
+
},
|
|
276
|
+
"Error policy threw while evaluating error event."
|
|
277
|
+
);
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const decision = normalizePolicyDecision(policyDecision, event);
|
|
282
|
+
|
|
283
|
+
if (decision.channel === "silent") {
|
|
284
|
+
const silentResult = Object.freeze({
|
|
285
|
+
event,
|
|
286
|
+
decision,
|
|
287
|
+
skipped: true,
|
|
288
|
+
reason: "silent"
|
|
289
|
+
});
|
|
290
|
+
notify({
|
|
291
|
+
type: "reported.silent",
|
|
292
|
+
result: silentResult
|
|
293
|
+
});
|
|
294
|
+
return silentResult;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (shouldSkipByDedupe(decision)) {
|
|
298
|
+
const dedupedResult = Object.freeze({
|
|
299
|
+
event,
|
|
300
|
+
decision,
|
|
301
|
+
skipped: true,
|
|
302
|
+
reason: "dedupe"
|
|
303
|
+
});
|
|
304
|
+
notify({
|
|
305
|
+
type: "reported.deduped",
|
|
306
|
+
result: dedupedResult
|
|
307
|
+
});
|
|
308
|
+
return dedupedResult;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const presenter = resolvePresenterForDecision(decision);
|
|
312
|
+
if (!presenter.supports(decision.channel)) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Error presenter "${presenter.id}" does not support channel "${decision.channel}".`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const payload = Object.freeze({
|
|
319
|
+
...decision,
|
|
320
|
+
presenterId: presenter.id,
|
|
321
|
+
event,
|
|
322
|
+
context
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const presentationId = normalizeText(presenter.present(payload));
|
|
326
|
+
|
|
327
|
+
const result = Object.freeze({
|
|
328
|
+
event,
|
|
329
|
+
decision: Object.freeze({
|
|
330
|
+
...decision,
|
|
331
|
+
presenterId: presenter.id
|
|
332
|
+
}),
|
|
333
|
+
presentationId,
|
|
334
|
+
skipped: false
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
notify({
|
|
338
|
+
type: "reported",
|
|
339
|
+
result
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function dismiss(presentationId = "", options = {}) {
|
|
346
|
+
const normalizedPresentationId = normalizeText(presentationId);
|
|
347
|
+
const source = isRecord(options) ? options : {};
|
|
348
|
+
const presenterId = normalizeText(source.presenterId);
|
|
349
|
+
|
|
350
|
+
if (presenterId) {
|
|
351
|
+
const presenter = byPresenterId.get(presenterId);
|
|
352
|
+
if (!presenter || typeof presenter.dismiss !== "function") {
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
return Number(presenter.dismiss(normalizedPresentationId) || 0);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let total = 0;
|
|
359
|
+
for (const presenter of byPresenterId.values()) {
|
|
360
|
+
if (typeof presenter.dismiss !== "function") {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
total += Number(presenter.dismiss(normalizedPresentationId) || 0);
|
|
364
|
+
}
|
|
365
|
+
return total;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function configure(options = {}) {
|
|
369
|
+
const source = isRecord(options) ? options : {};
|
|
370
|
+
|
|
371
|
+
if (Array.isArray(source.presenters) && source.presenters.length > 0) {
|
|
372
|
+
registerPresenters(source.presenters);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (Object.prototype.hasOwnProperty.call(source, "policy")) {
|
|
376
|
+
setPolicy(source.policy);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (Object.prototype.hasOwnProperty.call(source, "defaultPresenterId")) {
|
|
380
|
+
setAppDefaultPresenterId(source.defaultPresenterId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
assertBootReady();
|
|
384
|
+
|
|
385
|
+
return getSnapshot();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function getSnapshot() {
|
|
389
|
+
return Object.freeze({
|
|
390
|
+
presenterIds: getPresenterIds(),
|
|
391
|
+
appDefaultPresenterId: activeAppDefaultPresenterId,
|
|
392
|
+
moduleDefaultPresenterId: activeModuleDefaultPresenterId,
|
|
393
|
+
resolvedDefaultPresenterId: resolveDefaultPresenterId()
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
registerPresenters(presenters);
|
|
398
|
+
assertBootReady();
|
|
399
|
+
|
|
400
|
+
return Object.freeze({
|
|
401
|
+
report,
|
|
402
|
+
dismiss,
|
|
403
|
+
configure,
|
|
404
|
+
registerPresenter,
|
|
405
|
+
registerPresenters,
|
|
406
|
+
setPolicy,
|
|
407
|
+
setAppDefaultPresenterId,
|
|
408
|
+
assertBootReady,
|
|
409
|
+
getSnapshot,
|
|
410
|
+
subscribe,
|
|
411
|
+
normalizeErrorEvent
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export {
|
|
416
|
+
createErrorRuntime,
|
|
417
|
+
normalizeErrorEvent
|
|
418
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeAction,
|
|
3
|
+
normalizeSeverity,
|
|
4
|
+
normalizeText
|
|
5
|
+
} from "./normalize.js";
|
|
6
|
+
|
|
7
|
+
const PRESENTATION_CHANNELS = Object.freeze(["snackbar", "banner", "dialog"]);
|
|
8
|
+
const SINGLETON_CHANNELS = new Set(["banner"]);
|
|
9
|
+
|
|
10
|
+
function createEmptyChannelState() {
|
|
11
|
+
return {
|
|
12
|
+
snackbar: [],
|
|
13
|
+
banner: [],
|
|
14
|
+
dialog: []
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cloneEntry(entry = {}) {
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
id: String(entry.id || "").trim(),
|
|
21
|
+
channel: String(entry.channel || "").trim(),
|
|
22
|
+
message: String(entry.message || "").trim(),
|
|
23
|
+
severity: String(entry.severity || "error").trim(),
|
|
24
|
+
persist: Boolean(entry.persist),
|
|
25
|
+
action: entry.action || null,
|
|
26
|
+
presenterId: String(entry.presenterId || "").trim(),
|
|
27
|
+
dedupeKey: String(entry.dedupeKey || "").trim(),
|
|
28
|
+
timestamp: Number(entry.timestamp || 0)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createErrorPresentationStore({
|
|
33
|
+
now = () => Date.now()
|
|
34
|
+
} = {}) {
|
|
35
|
+
const listeners = new Set();
|
|
36
|
+
const channelState = createEmptyChannelState();
|
|
37
|
+
let sequence = 0;
|
|
38
|
+
let revision = 0;
|
|
39
|
+
|
|
40
|
+
function getChannelEntries(channel) {
|
|
41
|
+
if (!PRESENTATION_CHANNELS.includes(channel)) {
|
|
42
|
+
throw new Error(`Unknown presentation channel "${channel}".`);
|
|
43
|
+
}
|
|
44
|
+
return channelState[channel];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getState() {
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
revision,
|
|
50
|
+
channels: Object.freeze({
|
|
51
|
+
snackbar: Object.freeze(channelState.snackbar.map(cloneEntry)),
|
|
52
|
+
banner: Object.freeze(channelState.banner.map(cloneEntry)),
|
|
53
|
+
dialog: Object.freeze(channelState.dialog.map(cloneEntry))
|
|
54
|
+
})
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function notify(event = {}) {
|
|
59
|
+
const snapshot = getState();
|
|
60
|
+
for (const listener of listeners) {
|
|
61
|
+
try {
|
|
62
|
+
listener(snapshot, Object.freeze({ ...event }));
|
|
63
|
+
} catch {
|
|
64
|
+
// Ignore store listener failures so one broken consumer does not break the runtime.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function subscribe(listener) {
|
|
70
|
+
if (typeof listener !== "function") {
|
|
71
|
+
return () => {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
listeners.add(listener);
|
|
75
|
+
return () => {
|
|
76
|
+
listeners.delete(listener);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function present(channel, payload = {}) {
|
|
81
|
+
const normalizedChannel = String(channel || "").trim();
|
|
82
|
+
const entries = getChannelEntries(normalizedChannel);
|
|
83
|
+
sequence += 1;
|
|
84
|
+
|
|
85
|
+
const entry = Object.freeze({
|
|
86
|
+
id: normalizeText(payload.id, `${normalizedChannel}-${sequence}`),
|
|
87
|
+
channel: normalizedChannel,
|
|
88
|
+
message: normalizeText(payload.message, "Request failed."),
|
|
89
|
+
severity: normalizeSeverity(payload.severity, "error"),
|
|
90
|
+
persist: typeof payload.persist === "boolean" ? payload.persist : normalizedChannel !== "snackbar",
|
|
91
|
+
action: normalizeAction(payload.action),
|
|
92
|
+
presenterId: normalizeText(payload.presenterId),
|
|
93
|
+
dedupeKey: normalizeText(payload.dedupeKey),
|
|
94
|
+
timestamp: Number(now())
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (SINGLETON_CHANNELS.has(normalizedChannel) && entries.length > 0) {
|
|
98
|
+
entries.splice(0, entries.length);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
entries.push(entry);
|
|
102
|
+
revision += 1;
|
|
103
|
+
|
|
104
|
+
notify({
|
|
105
|
+
type: "presented",
|
|
106
|
+
channel: normalizedChannel,
|
|
107
|
+
id: entry.id
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return entry.id;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function dismiss(channel, presentationId = "") {
|
|
114
|
+
const normalizedChannel = String(channel || "").trim();
|
|
115
|
+
const entries = getChannelEntries(normalizedChannel);
|
|
116
|
+
const normalizedPresentationId = normalizeText(presentationId);
|
|
117
|
+
|
|
118
|
+
if (!normalizedPresentationId) {
|
|
119
|
+
if (entries.length < 1) {
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
const removed = entries.length;
|
|
123
|
+
entries.splice(0, entries.length);
|
|
124
|
+
revision += 1;
|
|
125
|
+
notify({
|
|
126
|
+
type: "dismissed",
|
|
127
|
+
channel: normalizedChannel,
|
|
128
|
+
id: "",
|
|
129
|
+
count: removed
|
|
130
|
+
});
|
|
131
|
+
return removed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const index = entries.findIndex((entry) => entry.id === normalizedPresentationId);
|
|
135
|
+
if (index < 0) {
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
entries.splice(index, 1);
|
|
140
|
+
revision += 1;
|
|
141
|
+
notify({
|
|
142
|
+
type: "dismissed",
|
|
143
|
+
channel: normalizedChannel,
|
|
144
|
+
id: normalizedPresentationId,
|
|
145
|
+
count: 1
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function clear(channel = "") {
|
|
152
|
+
const normalizedChannel = normalizeText(channel).toLowerCase();
|
|
153
|
+
if (!normalizedChannel) {
|
|
154
|
+
let removedCount = 0;
|
|
155
|
+
for (const candidateChannel of PRESENTATION_CHANNELS) {
|
|
156
|
+
removedCount += dismiss(candidateChannel, "");
|
|
157
|
+
}
|
|
158
|
+
return removedCount;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return dismiss(normalizedChannel, "");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return Object.freeze({
|
|
165
|
+
getState,
|
|
166
|
+
subscribe,
|
|
167
|
+
present,
|
|
168
|
+
dismiss,
|
|
169
|
+
clear
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export {
|
|
174
|
+
PRESENTATION_CHANNELS,
|
|
175
|
+
createErrorPresentationStore
|
|
176
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const SHELL_WEB_ERROR_RUNTIME_CLIENT_TOKEN = "runtime.web-error.client";
|
|
2
|
+
const SHELL_WEB_ERROR_PRESENTATION_STORE_CLIENT_TOKEN = "runtime.web-error.presentation-store.client";
|
|
3
|
+
|
|
4
|
+
const SHELL_WEB_ERROR_RUNTIME_INJECTION_KEY = Symbol.for("jskit.shell-web.runtime.web-error.client");
|
|
5
|
+
const SHELL_WEB_ERROR_PRESENTATION_STORE_INJECTION_KEY = Symbol.for(
|
|
6
|
+
"jskit.shell-web.runtime.web-error.presentation-store.client"
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
SHELL_WEB_ERROR_RUNTIME_CLIENT_TOKEN,
|
|
11
|
+
SHELL_WEB_ERROR_PRESENTATION_STORE_CLIENT_TOKEN,
|
|
12
|
+
SHELL_WEB_ERROR_RUNTIME_INJECTION_KEY,
|
|
13
|
+
SHELL_WEB_ERROR_PRESENTATION_STORE_INJECTION_KEY
|
|
14
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ShellWebClientProvider,
|
|
3
|
+
SHELL_WEB_QUERY_CLIENT_TOKEN
|
|
4
|
+
} from "./providers/ShellWebClientProvider.js";
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
ShellWebClientProvider,
|
|
8
|
+
SHELL_WEB_QUERY_CLIENT_TOKEN
|
|
9
|
+
} from "./providers/ShellWebClientProvider.js";
|
|
10
|
+
|
|
11
|
+
export { default as ShellLayout } from "./components/ShellLayout.vue";
|
|
12
|
+
export { default as ShellOutlet } from "./components/ShellOutlet.vue";
|
|
13
|
+
export { default as ShellErrorHost } from "./components/ShellErrorHost.vue";
|
|
14
|
+
|
|
15
|
+
const clientProviders = Object.freeze([ShellWebClientProvider]);
|
|
16
|
+
|
|
17
|
+
export { clientProviders };
|