@design.estate/wcctools 6.1.0 → 7.0.0
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/changelog.md +41 -0
- package/dist_shell/bundle.js +405 -368
- package/dist_shell/bundle.js.map +1 -1
- package/dist_shell/bundle.js.third-party-notices.json +52 -0
- package/dist_shell/bundle.js.third-party-notices.txt +107 -1
- package/dist_shell/index.html +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/capture/classes.capturebrowser.d.ts +65 -0
- package/dist_ts/capture/classes.capturebrowser.js +251 -0
- package/dist_ts/capture/classes.captureservice.d.ts +39 -0
- package/dist_ts/capture/classes.captureservice.js +119 -0
- package/dist_ts/capture/classes.screencast.d.ts +47 -0
- package/dist_ts/capture/classes.screencast.js +127 -0
- package/dist_ts/capture/errors.d.ts +14 -0
- package/dist_ts/capture/errors.js +23 -0
- package/dist_ts/capture/images.d.ts +3 -0
- package/dist_ts/capture/images.js +58 -0
- package/dist_ts/capture/index.d.ts +4 -0
- package/dist_ts/capture/index.js +5 -0
- package/dist_ts/capture/interaction.d.ts +11 -0
- package/dist_ts/capture/interaction.js +138 -0
- package/dist_ts/capture/navigationguard.d.ts +44 -0
- package/dist_ts/capture/navigationguard.js +129 -0
- package/dist_ts/capture/previewpage.d.ts +29 -0
- package/dist_ts/capture/previewpage.js +96 -0
- package/dist_ts/capture/request.d.ts +69 -0
- package/dist_ts/capture/request.js +239 -0
- package/dist_ts/classes.bundlestatus.d.ts +36 -0
- package/dist_ts/classes.bundlestatus.js +110 -0
- package/dist_ts/classes.devapi.d.ts +3 -0
- package/dist_ts/classes.devapi.js +8 -1
- package/dist_ts/classes.devserver.d.ts +65 -14
- package/dist_ts/classes.devserver.js +157 -52
- package/dist_ts/classes.hostpolicy.d.ts +63 -0
- package/dist_ts/classes.hostpolicy.js +171 -0
- package/dist_ts/cli.js +28 -3
- package/dist_ts/cli.screenshot.d.ts +6 -0
- package/dist_ts/cli.screenshot.js +89 -0
- package/dist_ts/index.d.ts +0 -2
- package/dist_ts/index.js +1 -3
- package/dist_ts/plugins.d.ts +6 -2
- package/dist_ts/plugins.js +8 -3
- package/dist_ts_interfaces/capture.d.ts +151 -0
- package/dist_ts_interfaces/capture.js +2 -0
- package/dist_ts_interfaces/index.d.ts +1 -0
- package/dist_ts_interfaces/index.js +2 -1
- package/dist_ts_interfaces/requests.d.ts +59 -0
- package/dist_ts_shell/bundlestatus.d.ts +15 -0
- package/dist_ts_shell/bundlestatus.js +64 -0
- package/dist_ts_shell/elements/wcc-contextmenu.d.ts +3 -0
- package/dist_ts_shell/elements/wcc-contextmenu.js +18 -3
- package/dist_ts_shell/elements/wcc-preview-frame.d.ts +6 -0
- package/dist_ts_shell/elements/wcc-preview-frame.js +50 -2
- package/dist_ts_shell/elements/wcc-recording-panel.d.ts +9 -0
- package/dist_ts_shell/elements/wcc-recording-panel.js +12 -1
- package/dist_ts_shell/elements/wcc-shell.d.ts +15 -0
- package/dist_ts_shell/elements/wcc-shell.js +45 -3
- package/dist_ts_shell/plugins.d.ts +2 -1
- package/dist_ts_shell/plugins.js +3 -2
- package/dist_ts_shell/services/framesampler.service.d.ts +23 -0
- package/dist_ts_shell/services/framesampler.service.js +101 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +6 -2
- package/readme.md +108 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/capture/classes.capturebrowser.ts +290 -0
- package/ts/capture/classes.captureservice.ts +152 -0
- package/ts/capture/classes.screencast.ts +152 -0
- package/ts/capture/errors.ts +25 -0
- package/ts/capture/images.ts +61 -0
- package/ts/capture/index.ts +4 -0
- package/ts/capture/interaction.ts +146 -0
- package/ts/capture/navigationguard.ts +147 -0
- package/ts/capture/previewpage.ts +130 -0
- package/ts/capture/request.ts +304 -0
- package/ts/classes.bundlestatus.ts +123 -0
- package/ts/classes.devapi.ts +9 -0
- package/ts/classes.devserver.ts +172 -50
- package/ts/classes.hostpolicy.ts +201 -0
- package/ts/cli.screenshot.ts +95 -0
- package/ts/cli.ts +28 -2
- package/ts/index.ts +0 -2
- package/ts/plugins.ts +9 -2
- package/ts_interfaces/bridge.ts +95 -0
- package/ts_interfaces/capture.ts +144 -0
- package/ts_interfaces/catalog.ts +37 -0
- package/ts_interfaces/index.ts +5 -0
- package/ts_interfaces/plugins.ts +3 -0
- package/ts_interfaces/requests.ts +104 -0
- package/ts_interfaces/standard.ts +103 -0
- package/ts_shared/index.ts +1 -0
- package/ts_shared/plugins.ts +3 -0
- package/ts_shared/previewroute.ts +58 -0
- package/ts_shell/bundlestatus.ts +68 -0
- package/ts_shell/elements/wcc-contextmenu.ts +306 -0
- package/ts_shell/elements/wcc-preview-frame.ts +167 -0
- package/ts_shell/elements/wcc-properties.ts +998 -0
- package/ts_shell/elements/wcc-record-button.ts +108 -0
- package/ts_shell/elements/wcc-recording-panel.ts +1017 -0
- package/ts_shell/elements/wcc-shell.ts +485 -0
- package/ts_shell/elements/wcc-sidebar.ts +1419 -0
- package/ts_shell/index.html +21 -0
- package/ts_shell/index.ts +4 -0
- package/ts_shell/plugins.ts +12 -0
- package/ts_shell/previewconnection.ts +103 -0
- package/ts_shell/services/framesampler.service.ts +112 -0
- package/ts_shell/services/recorder.service.ts +451 -0
- package/ts_shell/types/dom-mediacapture-stub/index.d.ts +12 -0
- package/ts_shell/types/dom-mediacapture-stub/package.json +6 -0
- package/ts_shell/types/dom-webcodecs-stub/index.d.ts +2 -0
- package/ts_shell/types/dom-webcodecs-stub/package.json +6 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { WccCaptureRequestError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
type IWccCaptureSubject = plugins.interfaces.IWccCaptureSubject;
|
|
5
|
+
type TWccCaptureFraming = plugins.interfaces.TWccCaptureFraming;
|
|
6
|
+
type TWccInteractionStep = plugins.interfaces.TWccInteractionStep;
|
|
7
|
+
|
|
8
|
+
/** The limits every capture request is held to. */
|
|
9
|
+
export const captureLimits = {
|
|
10
|
+
minWidth: 200,
|
|
11
|
+
maxWidth: 3840,
|
|
12
|
+
minHeight: 200,
|
|
13
|
+
maxHeight: 4320,
|
|
14
|
+
defaultHeight: 800,
|
|
15
|
+
/** Full-page captures are cut at this height in CSS pixels. */
|
|
16
|
+
maxFullPageHeight: 10_000,
|
|
17
|
+
defaultJpegQuality: 80,
|
|
18
|
+
maxSteps: 20,
|
|
19
|
+
stepTimeoutMs: 5_000,
|
|
20
|
+
maxWaitMs: 3_000,
|
|
21
|
+
defaultObserveMs: 500,
|
|
22
|
+
maxObserveMs: 5_000,
|
|
23
|
+
defaultMaxFrames: 12,
|
|
24
|
+
maxMaxFrames: 40,
|
|
25
|
+
defaultFrameQuality: 70,
|
|
26
|
+
maxSelectorLength: 500,
|
|
27
|
+
maxTextLength: 2_000,
|
|
28
|
+
maxKeyLength: 32,
|
|
29
|
+
maxSubjectLength: 200,
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
/** The capture widths of the named viewports: the dees-domtools breakpoints. */
|
|
33
|
+
export const captureViewportWidths: Readonly<Record<plugins.interfaces.TWccCaptureViewportPreset, number>> = {
|
|
34
|
+
phone: plugins.deesDomtools.breakpoints.phone,
|
|
35
|
+
phablet: plugins.deesDomtools.breakpoints.phablet,
|
|
36
|
+
tablet: plugins.deesDomtools.breakpoints.tablet,
|
|
37
|
+
desktop: plugins.deesDomtools.breakpoints.desktop,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const isViewportPreset = (valueArg: unknown): valueArg is plugins.interfaces.TWccCaptureViewportPreset => {
|
|
41
|
+
return typeof valueArg === 'string' && Object.hasOwn(captureViewportWidths, valueArg);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const framings: readonly TWccCaptureFraming[] = ['element', 'viewport', 'fullpage'];
|
|
45
|
+
|
|
46
|
+
/** The preview set-up of a capture, validated and with its defaults applied. */
|
|
47
|
+
export interface IWccResolvedView {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
theme: plugins.interfaces.TWccTheme;
|
|
51
|
+
scale: 1 | 2;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface IWccResolvedImageOptions {
|
|
55
|
+
/** Undefined: the default framing of what the demo rendered. */
|
|
56
|
+
framing?: TWccCaptureFraming;
|
|
57
|
+
format: 'png' | 'jpeg';
|
|
58
|
+
/** JPEG quality; undefined for PNG. */
|
|
59
|
+
quality?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface IWccResolvedScreenshotRequest {
|
|
63
|
+
subject: IWccCaptureSubject;
|
|
64
|
+
view: IWccResolvedView;
|
|
65
|
+
image: IWccResolvedImageOptions;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface IWccResolvedInteractionRequest {
|
|
69
|
+
subject: IWccCaptureSubject;
|
|
70
|
+
view: IWccResolvedView;
|
|
71
|
+
steps: TWccInteractionStep[];
|
|
72
|
+
observeMs: number;
|
|
73
|
+
maxFrames: number;
|
|
74
|
+
frameQuality: number;
|
|
75
|
+
finalScreenshot: IWccResolvedImageOptions | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const isInteger = (valueArg: unknown): valueArg is number => typeof valueArg === 'number' && Number.isInteger(valueArg);
|
|
79
|
+
|
|
80
|
+
const requireIntegerIn = (valueArg: unknown, minArg: number, maxArg: number, nameArg: string): number => {
|
|
81
|
+
if (!isInteger(valueArg) || valueArg < minArg || valueArg > maxArg) {
|
|
82
|
+
throw new WccCaptureRequestError(`wcctools: ${nameArg} must be a whole number from ${minArg} to ${maxArg}.`);
|
|
83
|
+
}
|
|
84
|
+
return valueArg;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const requireText = (valueArg: unknown, maxLengthArg: number, nameArg: string, allowEmptyArg = false): string => {
|
|
88
|
+
if (typeof valueArg !== 'string' || (!allowEmptyArg && valueArg.trim() === '') || valueArg.length > maxLengthArg) {
|
|
89
|
+
throw new WccCaptureRequestError(`wcctools: ${nameArg} must be ${allowEmptyArg ? 'a' : 'a non-empty'} string of at most ${maxLengthArg} characters.`);
|
|
90
|
+
}
|
|
91
|
+
return valueArg;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const resolveSubject = (subjectArg: unknown): IWccCaptureSubject => {
|
|
95
|
+
if (typeof subjectArg !== 'object' || subjectArg === null) {
|
|
96
|
+
throw new WccCaptureRequestError('wcctools: a capture needs a subject: the catalog entry to render.');
|
|
97
|
+
}
|
|
98
|
+
const subject = subjectArg as Partial<IWccCaptureSubject>;
|
|
99
|
+
const resolved: IWccCaptureSubject = {
|
|
100
|
+
itemName: requireText(subject.itemName, captureLimits.maxSubjectLength, 'the subject\'s itemName'),
|
|
101
|
+
demoIndex: subject.demoIndex === undefined ? 0 : requireIntegerIn(subject.demoIndex, 0, 1_000, 'the demo index'),
|
|
102
|
+
};
|
|
103
|
+
if (subject.sectionName !== undefined) {
|
|
104
|
+
resolved.sectionName = requireText(subject.sectionName, captureLimits.maxSubjectLength, 'the subject\'s sectionName');
|
|
105
|
+
}
|
|
106
|
+
return resolved;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Reads a viewport: a named one, or a width in CSS pixels. */
|
|
110
|
+
export const resolveViewportWidth = (viewportArg: unknown): number => {
|
|
111
|
+
if (viewportArg === undefined) {
|
|
112
|
+
return captureViewportWidths.desktop;
|
|
113
|
+
}
|
|
114
|
+
if (isViewportPreset(viewportArg)) {
|
|
115
|
+
return captureViewportWidths[viewportArg];
|
|
116
|
+
}
|
|
117
|
+
if (isInteger(viewportArg)) {
|
|
118
|
+
return requireIntegerIn(viewportArg, captureLimits.minWidth, captureLimits.maxWidth, 'the viewport width');
|
|
119
|
+
}
|
|
120
|
+
throw new WccCaptureRequestError(
|
|
121
|
+
`wcctools: the viewport must be one of ${Object.keys(captureViewportWidths).join(', ')} or a width from ${captureLimits.minWidth} to ${captureLimits.maxWidth}.`,
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const resolveView = (requestArg: plugins.interfaces.IWccCaptureView): IWccResolvedView => {
|
|
126
|
+
const theme = requestArg.theme ?? 'dark';
|
|
127
|
+
if (theme !== 'dark' && theme !== 'bright') {
|
|
128
|
+
throw new WccCaptureRequestError('wcctools: the theme must be dark or bright.');
|
|
129
|
+
}
|
|
130
|
+
const scale = requestArg.scale ?? 1;
|
|
131
|
+
if (scale !== 1 && scale !== 2) {
|
|
132
|
+
throw new WccCaptureRequestError('wcctools: the scale must be 1 or 2.');
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
width: resolveViewportWidth(requestArg.viewport),
|
|
136
|
+
height: requestArg.height === undefined
|
|
137
|
+
? captureLimits.defaultHeight
|
|
138
|
+
: requireIntegerIn(requestArg.height, captureLimits.minHeight, captureLimits.maxHeight, 'the height'),
|
|
139
|
+
theme,
|
|
140
|
+
scale,
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const resolveImageOptions = (
|
|
145
|
+
optionsArg: { framing?: unknown; format?: unknown; quality?: unknown },
|
|
146
|
+
): IWccResolvedImageOptions => {
|
|
147
|
+
const framing = optionsArg.framing;
|
|
148
|
+
if (framing !== undefined && !framings.includes(framing as TWccCaptureFraming)) {
|
|
149
|
+
throw new WccCaptureRequestError(`wcctools: the framing must be one of ${framings.join(', ')}.`);
|
|
150
|
+
}
|
|
151
|
+
const format = optionsArg.format ?? 'png';
|
|
152
|
+
if (format !== 'png' && format !== 'jpeg') {
|
|
153
|
+
throw new WccCaptureRequestError('wcctools: the format must be png or jpeg.');
|
|
154
|
+
}
|
|
155
|
+
if (format === 'png' && optionsArg.quality !== undefined) {
|
|
156
|
+
throw new WccCaptureRequestError('wcctools: a quality applies to jpeg only.');
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
framing: framing as TWccCaptureFraming | undefined,
|
|
160
|
+
format,
|
|
161
|
+
quality: format === 'jpeg'
|
|
162
|
+
? (optionsArg.quality === undefined ? captureLimits.defaultJpegQuality : requireIntegerIn(optionsArg.quality, 1, 100, 'the quality'))
|
|
163
|
+
: undefined,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** Validates a screenshot request and applies its defaults. */
|
|
168
|
+
export const resolveScreenshotRequest = (requestArg: plugins.interfaces.IWccScreenshotRequest): IWccResolvedScreenshotRequest => {
|
|
169
|
+
if (typeof requestArg !== 'object' || requestArg === null) {
|
|
170
|
+
throw new WccCaptureRequestError('wcctools: the screenshot request is missing.');
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
subject: resolveSubject(requestArg.subject),
|
|
174
|
+
view: resolveView(requestArg),
|
|
175
|
+
image: resolveImageOptions(requestArg),
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const resolveStep = (stepArg: unknown, indexArg: number): TWccInteractionStep => {
|
|
180
|
+
const name = `step ${indexArg}`;
|
|
181
|
+
if (typeof stepArg !== 'object' || stepArg === null) {
|
|
182
|
+
throw new WccCaptureRequestError(`wcctools: ${name} must be an object with an action.`);
|
|
183
|
+
}
|
|
184
|
+
const step = stepArg as Record<string, unknown>;
|
|
185
|
+
const selector = (requiredArg: boolean) => {
|
|
186
|
+
if (!requiredArg && step.selector === undefined) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
return requireText(step.selector, captureLimits.maxSelectorLength, `the selector of ${name}`);
|
|
190
|
+
};
|
|
191
|
+
switch (step.action) {
|
|
192
|
+
case 'click':
|
|
193
|
+
case 'hover':
|
|
194
|
+
return { action: step.action, selector: selector(true) };
|
|
195
|
+
case 'fill':
|
|
196
|
+
return {
|
|
197
|
+
action: 'fill',
|
|
198
|
+
selector: selector(true),
|
|
199
|
+
text: requireText(step.text, captureLimits.maxTextLength, `the text of ${name}`, true),
|
|
200
|
+
};
|
|
201
|
+
case 'press': {
|
|
202
|
+
const pressStep: TWccInteractionStep = {
|
|
203
|
+
action: 'press',
|
|
204
|
+
key: requireText(step.key, captureLimits.maxKeyLength, `the key of ${name}`),
|
|
205
|
+
};
|
|
206
|
+
const pressSelector = selector(false);
|
|
207
|
+
return pressSelector === undefined ? pressStep : { ...pressStep, selector: pressSelector };
|
|
208
|
+
}
|
|
209
|
+
case 'waitFor': {
|
|
210
|
+
const state = step.state ?? 'visible';
|
|
211
|
+
if (state !== 'visible' && state !== 'hidden') {
|
|
212
|
+
throw new WccCaptureRequestError(`wcctools: the state of ${name} must be visible or hidden.`);
|
|
213
|
+
}
|
|
214
|
+
return { action: 'waitFor', selector: selector(true), state };
|
|
215
|
+
}
|
|
216
|
+
case 'wait':
|
|
217
|
+
return { action: 'wait', ms: requireIntegerIn(step.ms, 0, captureLimits.maxWaitMs, `the time of ${name}`) };
|
|
218
|
+
default:
|
|
219
|
+
throw new WccCaptureRequestError(`wcctools: the action of ${name} must be one of click, hover, fill, press, waitFor, wait.`);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** Validates an interaction request and applies its defaults. */
|
|
224
|
+
export const resolveInteractionRequest = (requestArg: plugins.interfaces.IWccInteractionRequest): IWccResolvedInteractionRequest => {
|
|
225
|
+
if (typeof requestArg !== 'object' || requestArg === null) {
|
|
226
|
+
throw new WccCaptureRequestError('wcctools: the interaction request is missing.');
|
|
227
|
+
}
|
|
228
|
+
if (!Array.isArray(requestArg.steps) || requestArg.steps.length > captureLimits.maxSteps) {
|
|
229
|
+
throw new WccCaptureRequestError(`wcctools: an interaction has a list of at most ${captureLimits.maxSteps} steps.`);
|
|
230
|
+
}
|
|
231
|
+
if (requestArg.finalScreenshot !== undefined && (typeof requestArg.finalScreenshot !== 'object' || requestArg.finalScreenshot === null)) {
|
|
232
|
+
throw new WccCaptureRequestError('wcctools: finalScreenshot must be an object.');
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
subject: resolveSubject(requestArg.subject),
|
|
236
|
+
view: resolveView(requestArg),
|
|
237
|
+
steps: requestArg.steps.map((stepArg, indexArg) => resolveStep(stepArg, indexArg)),
|
|
238
|
+
observeMs: requestArg.observeMs === undefined
|
|
239
|
+
? captureLimits.defaultObserveMs
|
|
240
|
+
: requireIntegerIn(requestArg.observeMs, 0, captureLimits.maxObserveMs, 'observeMs'),
|
|
241
|
+
maxFrames: requestArg.maxFrames === undefined
|
|
242
|
+
? captureLimits.defaultMaxFrames
|
|
243
|
+
: requireIntegerIn(requestArg.maxFrames, 1, captureLimits.maxMaxFrames, 'maxFrames'),
|
|
244
|
+
frameQuality: requestArg.frameQuality === undefined
|
|
245
|
+
? captureLimits.defaultFrameQuality
|
|
246
|
+
: requireIntegerIn(requestArg.frameQuality, 1, 100, 'frameQuality'),
|
|
247
|
+
finalScreenshot: requestArg.finalScreenshot ? resolveImageOptions(requestArg.finalScreenshot) : null,
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const describeEntry = (sectionNameArg: string, entryNameArg: string) => `${sectionNameArg}/${entryNameArg}`;
|
|
252
|
+
|
|
253
|
+
const listSome = (namesArg: string[]): string => {
|
|
254
|
+
const shown = namesArg.slice(0, 20).join(', ');
|
|
255
|
+
return namesArg.length > 20 ? `${shown}, … (${namesArg.length} in all)` : shown;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolves a subject to one demo of the catalog: an entry named by its name or its element's tag,
|
|
260
|
+
* in the named section or, without one, in exactly one section.
|
|
261
|
+
*/
|
|
262
|
+
export const resolveSelection = (
|
|
263
|
+
manifestArg: plugins.interfaces.IWccCatalogManifest,
|
|
264
|
+
subjectArg: IWccCaptureSubject,
|
|
265
|
+
): plugins.interfaces.IWccSelection => {
|
|
266
|
+
const sections = subjectArg.sectionName === undefined
|
|
267
|
+
? manifestArg.sections
|
|
268
|
+
: manifestArg.sections.filter((sectionArg) => sectionArg.name === subjectArg.sectionName);
|
|
269
|
+
if (sections.length === 0) {
|
|
270
|
+
throw new WccCaptureRequestError(
|
|
271
|
+
`wcctools: the catalog has no section "${subjectArg.sectionName}". Sections: ${listSome(manifestArg.sections.map((sectionArg) => sectionArg.name))}.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const wanted = subjectArg.itemName;
|
|
275
|
+
const matchesName = (entryArg: { name: string; tagName: string | null }) => {
|
|
276
|
+
return entryArg.name === wanted || (entryArg.tagName !== null && entryArg.tagName === wanted.toLowerCase());
|
|
277
|
+
};
|
|
278
|
+
const matches = sections.flatMap((sectionArg) => sectionArg.entries
|
|
279
|
+
.filter(matchesName)
|
|
280
|
+
.map((entryArg) => ({ section: sectionArg, entry: entryArg })));
|
|
281
|
+
if (matches.length === 0) {
|
|
282
|
+
const withoutDemo = sections.flatMap((sectionArg) => sectionArg.entriesWithoutDemo
|
|
283
|
+
.filter(matchesName)
|
|
284
|
+
.map((entryArg) => describeEntry(sectionArg.name, entryArg.name)));
|
|
285
|
+
if (withoutDemo.length > 0) {
|
|
286
|
+
throw new WccCaptureRequestError(`wcctools: ${listSome(withoutDemo)} has no demo to capture.`);
|
|
287
|
+
}
|
|
288
|
+
const known = sections.flatMap((sectionArg) => sectionArg.entries.map((entryArg) => describeEntry(sectionArg.name, entryArg.name)));
|
|
289
|
+
throw new WccCaptureRequestError(`wcctools: the catalog has no entry "${wanted}". Entries: ${listSome(known)}.`);
|
|
290
|
+
}
|
|
291
|
+
if (matches.length > 1) {
|
|
292
|
+
throw new WccCaptureRequestError(
|
|
293
|
+
`wcctools: "${wanted}" names ${matches.length} entries (${listSome(matches.map((matchArg) => describeEntry(matchArg.section.name, matchArg.entry.name)))}); name its section.`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const [{ section, entry }] = matches;
|
|
297
|
+
const demoIndex = subjectArg.demoIndex ?? 0;
|
|
298
|
+
if (demoIndex >= entry.demoCount) {
|
|
299
|
+
throw new WccCaptureRequestError(
|
|
300
|
+
`wcctools: ${describeEntry(section.name, entry.name)} has ${entry.demoCount} demo${entry.demoCount === 1 ? '' : 's'}; demo ${demoIndex} does not exist (demos count from 0).`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return { sectionName: section.name, itemName: entry.name, demoIndex };
|
|
304
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
/** The longest time a status request waits for a change before it answers the unchanged status. */
|
|
4
|
+
export const bundleStatusWaitLimitMs = 25_000;
|
|
5
|
+
|
|
6
|
+
/** What a status request answers while the board is closed: the server is not serving. */
|
|
7
|
+
export const bundleStatusClosedMessage = 'wcctools: the dev server is not serving the bundle status.';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The status of the catalog bundles, as tswatch's bundle events report it, for the shell's
|
|
11
|
+
* `getBundleStatus` requests. Every event raises the revision and answers the requests waiting for
|
|
12
|
+
* a change. While closed (the server is not serving), requests are refused with a typed error, so
|
|
13
|
+
* a client backs off as it does for any failed request.
|
|
14
|
+
*/
|
|
15
|
+
export class WccBundleStatusBoard {
|
|
16
|
+
private readonly bundles = new Map<string, plugins.interfaces.IWccBundleStatus>();
|
|
17
|
+
private revision = 0;
|
|
18
|
+
private isOpen = false;
|
|
19
|
+
/** Settles one waiting request: answers the current snapshot, or refuses; each removes itself. */
|
|
20
|
+
private readonly waiters = new Set<(refusalArg?: Error) => void>();
|
|
21
|
+
|
|
22
|
+
/** Starts a run of the server: the bundles of an earlier run are forgotten. */
|
|
23
|
+
public open() {
|
|
24
|
+
this.bundles.clear();
|
|
25
|
+
this.isOpen = true;
|
|
26
|
+
this.changed();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Ends the run: every waiting request is refused now, later ones at once. */
|
|
30
|
+
public close() {
|
|
31
|
+
this.isOpen = false;
|
|
32
|
+
const refusal = this.createClosedError();
|
|
33
|
+
for (const settle of [...this.waiters]) {
|
|
34
|
+
settle(refusal);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The number of requests waiting for a change. */
|
|
39
|
+
public getWaiterCount(): number {
|
|
40
|
+
return this.waiters.size;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public record(eventArg: plugins.tswatch.TBundleEvent) {
|
|
44
|
+
const previous = this.bundles.get(eventArg.name);
|
|
45
|
+
const status: plugins.interfaces.IWccBundleStatus = { name: eventArg.name, state: eventArg.type };
|
|
46
|
+
switch (eventArg.type) {
|
|
47
|
+
case 'started':
|
|
48
|
+
// The latest completed run stays reported while the next one runs
|
|
49
|
+
if (previous?.durationMs !== undefined) {
|
|
50
|
+
status.durationMs = previous.durationMs;
|
|
51
|
+
}
|
|
52
|
+
if (previous?.errorMessage !== undefined) {
|
|
53
|
+
status.errorMessage = previous.errorMessage;
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
case 'finished':
|
|
57
|
+
status.durationMs = eventArg.durationMs;
|
|
58
|
+
break;
|
|
59
|
+
case 'failed':
|
|
60
|
+
status.durationMs = eventArg.durationMs;
|
|
61
|
+
status.errorMessage = eventArg.errorMessage;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
this.bundles.set(eventArg.name, status);
|
|
65
|
+
this.changed();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public getSnapshot(): plugins.interfaces.IWccBundleStatusSnapshot {
|
|
69
|
+
return {
|
|
70
|
+
revision: this.revision,
|
|
71
|
+
bundles: [...this.bundles.values()].map((statusArg) => ({ ...statusArg })),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The snapshot once it differs from `afterRevisionArg`, or after the wait limit; at once without
|
|
77
|
+
* a revision or for a revision that is no longer current. Refused while closed. When
|
|
78
|
+
* `abortSignalArg` aborts (the client went away), the wait ends at once and releases its timer;
|
|
79
|
+
* it answers the current snapshot, which the transport drops.
|
|
80
|
+
*/
|
|
81
|
+
public waitForChange(
|
|
82
|
+
afterRevisionArg: number | undefined,
|
|
83
|
+
abortSignalArg?: AbortSignal,
|
|
84
|
+
): Promise<plugins.interfaces.IWccBundleStatusSnapshot> {
|
|
85
|
+
if (!this.isOpen) {
|
|
86
|
+
return Promise.reject(this.createClosedError());
|
|
87
|
+
}
|
|
88
|
+
if (afterRevisionArg === undefined || afterRevisionArg !== this.revision || abortSignalArg?.aborted) {
|
|
89
|
+
return Promise.resolve(this.getSnapshot());
|
|
90
|
+
}
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const settle = (refusalArg?: Error) => {
|
|
93
|
+
clearTimeout(waitLimit);
|
|
94
|
+
abortSignalArg?.removeEventListener('abort', onAbort);
|
|
95
|
+
this.waiters.delete(settle);
|
|
96
|
+
if (refusalArg) {
|
|
97
|
+
reject(refusalArg);
|
|
98
|
+
} else {
|
|
99
|
+
resolve(this.getSnapshot());
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const onAbort = () => settle();
|
|
103
|
+
const waitLimit = setTimeout(() => settle(), bundleStatusWaitLimitMs);
|
|
104
|
+
abortSignalArg?.addEventListener('abort', onAbort, { once: true });
|
|
105
|
+
this.waiters.add(settle);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private changed() {
|
|
110
|
+
this.revision++;
|
|
111
|
+
this.answerWaiters();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private answerWaiters() {
|
|
115
|
+
for (const settle of [...this.waiters]) {
|
|
116
|
+
settle();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private createClosedError() {
|
|
121
|
+
return new plugins.typedrequest.TypedResponseError(bundleStatusClosedMessage);
|
|
122
|
+
}
|
|
123
|
+
}
|
package/ts/classes.devapi.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { WccBundleStatusBoard } from './classes.bundlestatus.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The typed API the shell reaches on `/wcctools/typedrequest`. Later features register their
|
|
@@ -6,6 +7,8 @@ import * as plugins from './plugins.js';
|
|
|
6
7
|
*/
|
|
7
8
|
export class WccDevApi {
|
|
8
9
|
public readonly typedrouter = new plugins.typedrequest.TypedRouter();
|
|
10
|
+
/** The catalog bundles' status, fed with tswatch's bundle events while the server runs. */
|
|
11
|
+
public readonly bundleStatus = new WccBundleStatusBoard();
|
|
9
12
|
private serverInfo: plugins.interfaces.IWccDevServerInfo | null = null;
|
|
10
13
|
|
|
11
14
|
constructor() {
|
|
@@ -17,6 +20,12 @@ export class WccDevApi {
|
|
|
17
20
|
return { ...this.serverInfo };
|
|
18
21
|
}),
|
|
19
22
|
);
|
|
23
|
+
this.typedrouter.addTypedHandler(
|
|
24
|
+
new plugins.typedrequest.TypedHandler<plugins.interfaces.IReq_GetBundleStatus>('getBundleStatus', async (requestArg, typedToolsArg) => {
|
|
25
|
+
// A client that goes away ends its wait, so no waiter or timer outlives the request
|
|
26
|
+
return this.bundleStatus.waitForChange(requestArg.afterRevision, typedToolsArg?.abortSignal);
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
public setServerInfo(infoArg: plugins.interfaces.IWccDevServerInfo) {
|