@a4anthony/proctorkit-sdk 0.1.0 → 0.2.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/README.md +13 -3
- package/dist/delivery-readiness.d.ts +3 -0
- package/dist/delivery-readiness.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +258 -95
- package/dist/preflight/checks.d.ts +22 -17
- package/dist/preflight/checks.d.ts.map +1 -1
- package/dist/preflight/system-check.d.ts +16 -5
- package/dist/preflight/system-check.d.ts.map +1 -1
- package/dist/recording-delivery-readiness.d.ts +24 -2
- package/dist/recording-delivery-readiness.d.ts.map +1 -1
- package/dist/worker.js +5 -2
- package/package.json +2 -2
- package/src/delivery-readiness.ts +8 -1
- package/src/index.ts +7 -0
- package/src/preflight/checks.ts +36 -87
- package/src/preflight/system-check.ts +149 -85
- package/src/recording-delivery-readiness.ts +231 -12
package/dist/index.js
CHANGED
|
@@ -2,15 +2,20 @@ import { openDB, deleteDB } from "idb";
|
|
|
2
2
|
class DeliveryReadinessError extends Error {
|
|
3
3
|
code;
|
|
4
4
|
status;
|
|
5
|
+
/** Structured operator diagnostics. Never render this object in candidate UI. */
|
|
6
|
+
details;
|
|
5
7
|
constructor(code, message, options) {
|
|
6
8
|
super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
7
9
|
this.name = "DeliveryReadinessError";
|
|
8
10
|
this.code = code;
|
|
9
11
|
this.status = options?.status;
|
|
12
|
+
this.details = options?.details;
|
|
10
13
|
}
|
|
11
14
|
}
|
|
12
15
|
const RECORDING_READINESS_TIMEOUT_MS = 1e4;
|
|
13
|
-
const
|
|
16
|
+
const DEFAULT_CANARY_BYTES = 1024;
|
|
17
|
+
const MAX_CANARY_BYTES = 1e7;
|
|
18
|
+
const DEFAULT_MEASUREMENT_SAMPLES = 3;
|
|
14
19
|
async function verifyRecordingDelivery(config) {
|
|
15
20
|
if (typeof fetch === "undefined") {
|
|
16
21
|
throw new DeliveryReadinessError(
|
|
@@ -32,9 +37,9 @@ async function verifyRecordingDelivery(config) {
|
|
|
32
37
|
const mode = await resolveUploadMode(baseUrl, controller.signal);
|
|
33
38
|
phase = "media";
|
|
34
39
|
if (mode === "direct" || mode === "segments") {
|
|
35
|
-
await verifyDirectPath(baseUrl, config, controller.signal);
|
|
40
|
+
await verifyDirectPath(baseUrl, config, DEFAULT_CANARY_BYTES, controller.signal, defaultNow$1);
|
|
36
41
|
} else {
|
|
37
|
-
await verifyPostPath(baseUrl, config, controller.signal);
|
|
42
|
+
await verifyPostPath(baseUrl, config, DEFAULT_CANARY_BYTES, controller.signal, defaultNow$1);
|
|
38
43
|
}
|
|
39
44
|
return mode;
|
|
40
45
|
} catch (error) {
|
|
@@ -55,6 +60,76 @@ async function verifyRecordingDelivery(config) {
|
|
|
55
60
|
clearTimeout(timeout);
|
|
56
61
|
}
|
|
57
62
|
}
|
|
63
|
+
async function measureRecordingDeliveryReadiness(config) {
|
|
64
|
+
const payloadBytes = normaliseCanaryBytes(config.payloadBytes);
|
|
65
|
+
const completionWindowMs = normaliseCompletionWindow(config.completionWindowMs);
|
|
66
|
+
const sampleCount = Math.min(
|
|
67
|
+
5,
|
|
68
|
+
Math.max(1, Math.floor(config.samples ?? DEFAULT_MEASUREMENT_SAMPLES))
|
|
69
|
+
);
|
|
70
|
+
const now2 = config.now ?? defaultNow$1;
|
|
71
|
+
const baseUrl = originOf$1(config.ingestUrl);
|
|
72
|
+
if (!baseUrl) {
|
|
73
|
+
throw new DeliveryReadinessError(
|
|
74
|
+
"upload-config-invalid",
|
|
75
|
+
"The ingest URL does not provide a valid recording upload origin."
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const mode = await withReadinessTimeout(
|
|
79
|
+
RECORDING_READINESS_TIMEOUT_MS,
|
|
80
|
+
"config",
|
|
81
|
+
(signal) => resolveUploadMode(baseUrl, signal)
|
|
82
|
+
);
|
|
83
|
+
await runPathProbe(baseUrl, config, mode, DEFAULT_CANARY_BYTES, now2).catch(() => void 0);
|
|
84
|
+
const durations = [];
|
|
85
|
+
let lastError;
|
|
86
|
+
for (let attempt = 0; attempt < sampleCount; attempt += 1) {
|
|
87
|
+
try {
|
|
88
|
+
durations.push(await runPathProbe(baseUrl, config, mode, payloadBytes, now2));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
lastError = error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const requiredSuccesses = Math.ceil(sampleCount / 2);
|
|
94
|
+
if (durations.length < requiredSuccesses) {
|
|
95
|
+
const details = measurementDetails(mode, payloadBytes, completionWindowMs, durations.length);
|
|
96
|
+
if (lastError instanceof DeliveryReadinessError) {
|
|
97
|
+
throw new DeliveryReadinessError(lastError.code, lastError.message, {
|
|
98
|
+
cause: lastError,
|
|
99
|
+
...lastError.status !== void 0 ? { status: lastError.status } : {},
|
|
100
|
+
details
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
throw new DeliveryReadinessError(
|
|
104
|
+
"media-storage-unreachable",
|
|
105
|
+
"The recording storage path could not complete enough readiness uploads.",
|
|
106
|
+
{ cause: lastError, details }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const medianCompletionMs = median(durations);
|
|
110
|
+
if (medianCompletionMs > completionWindowMs) {
|
|
111
|
+
throw new DeliveryReadinessError(
|
|
112
|
+
"media-storage-too-slow",
|
|
113
|
+
`The representative recording payload took ${Math.round(medianCompletionMs)} ms; the workload window is ${Math.round(completionWindowMs)} ms.`,
|
|
114
|
+
{
|
|
115
|
+
details: measurementDetails(
|
|
116
|
+
mode,
|
|
117
|
+
payloadBytes,
|
|
118
|
+
completionWindowMs,
|
|
119
|
+
durations.length,
|
|
120
|
+
medianCompletionMs
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
mode,
|
|
127
|
+
payloadBytes,
|
|
128
|
+
completionWindowMs,
|
|
129
|
+
sampleDurationsMs: durations.map((duration) => Math.round(duration)),
|
|
130
|
+
medianCompletionMs: Math.round(medianCompletionMs)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
58
133
|
async function resolveUploadMode(baseUrl, signal) {
|
|
59
134
|
const response = await fetch(`${baseUrl}/public/upload-config`, {
|
|
60
135
|
method: "GET",
|
|
@@ -87,29 +162,31 @@ async function resolveUploadMode(baseUrl, signal) {
|
|
|
87
162
|
}
|
|
88
163
|
return body.recordingUpload;
|
|
89
164
|
}
|
|
90
|
-
async function verifyDirectPath(baseUrl, config, signal) {
|
|
165
|
+
async function verifyDirectPath(baseUrl, config, canaryBytes, signal, now2) {
|
|
91
166
|
let canary = null;
|
|
92
167
|
try {
|
|
93
168
|
canary = await postJson(
|
|
94
169
|
`${baseUrl}/uploads/readiness/direct/start`,
|
|
95
|
-
{ sessionId: config.sessionId },
|
|
170
|
+
{ sessionId: config.sessionId, bytes: canaryBytes },
|
|
96
171
|
config.appId,
|
|
97
172
|
signal
|
|
98
173
|
);
|
|
99
|
-
if (!canary.canaryId || !canary.uploadId || !canary.url) {
|
|
174
|
+
if (!canary.canaryId || !canary.uploadId || !canary.url || canary.expectedBytes !== canaryBytes) {
|
|
100
175
|
throw new DeliveryReadinessError(
|
|
101
176
|
"media-storage-invalid-response",
|
|
102
177
|
"The recording storage canary did not return an upload target."
|
|
103
178
|
);
|
|
104
179
|
}
|
|
180
|
+
const uploadStartedAt = now2();
|
|
105
181
|
const put = await fetch(canary.url, {
|
|
106
182
|
method: "PUT",
|
|
107
|
-
body: new Blob([new Uint8Array(
|
|
183
|
+
body: new Blob([new Uint8Array(canaryBytes)], {
|
|
108
184
|
type: "application/octet-stream"
|
|
109
185
|
}),
|
|
110
186
|
credentials: "omit",
|
|
111
187
|
signal
|
|
112
188
|
});
|
|
189
|
+
const uploadDurationMs = Math.max(0.1, now2() - uploadStartedAt);
|
|
113
190
|
if (!put.ok) {
|
|
114
191
|
throw mediaHttpError(put.status, "The recording storage rejected the canary upload.");
|
|
115
192
|
}
|
|
@@ -118,39 +195,43 @@ async function verifyDirectPath(baseUrl, config, signal) {
|
|
|
118
195
|
{
|
|
119
196
|
sessionId: config.sessionId,
|
|
120
197
|
canaryId: canary.canaryId,
|
|
121
|
-
uploadId: canary.uploadId
|
|
198
|
+
uploadId: canary.uploadId,
|
|
199
|
+
expectedBytes: canaryBytes
|
|
122
200
|
},
|
|
123
201
|
config.appId,
|
|
124
202
|
signal
|
|
125
203
|
);
|
|
126
|
-
if (completed.ok !== true || Number(completed.receivedBytes) !==
|
|
204
|
+
if (completed.ok !== true || Number(completed.receivedBytes) !== canaryBytes) {
|
|
127
205
|
throw new DeliveryReadinessError(
|
|
128
206
|
"media-storage-invalid-response",
|
|
129
207
|
"The recording storage did not acknowledge the complete canary."
|
|
130
208
|
);
|
|
131
209
|
}
|
|
132
210
|
canary = null;
|
|
211
|
+
return uploadDurationMs;
|
|
133
212
|
} finally {
|
|
134
213
|
if (canary) void abortDirectCanary(baseUrl, config, canary);
|
|
135
214
|
}
|
|
136
215
|
}
|
|
137
|
-
async function verifyPostPath(baseUrl, config, signal) {
|
|
216
|
+
async function verifyPostPath(baseUrl, config, canaryBytes, signal, now2) {
|
|
138
217
|
const headers = {
|
|
139
218
|
"content-type": "application/octet-stream"
|
|
140
219
|
};
|
|
141
220
|
if (config.appId) headers["x-app-id"] = config.appId;
|
|
221
|
+
const uploadStartedAt = now2();
|
|
142
222
|
const response = await fetch(
|
|
143
223
|
`${baseUrl}/uploads/readiness/post/${encodeURIComponent(config.sessionId)}`,
|
|
144
224
|
{
|
|
145
225
|
method: "POST",
|
|
146
226
|
headers,
|
|
147
|
-
body: new Blob([new Uint8Array(
|
|
227
|
+
body: new Blob([new Uint8Array(canaryBytes)], {
|
|
148
228
|
type: "application/octet-stream"
|
|
149
229
|
}),
|
|
150
230
|
credentials: "omit",
|
|
151
231
|
signal
|
|
152
232
|
}
|
|
153
233
|
);
|
|
234
|
+
const uploadDurationMs = Math.max(0.1, now2() - uploadStartedAt);
|
|
154
235
|
if (!response.ok) {
|
|
155
236
|
throw mediaHttpError(response.status, "The recording relay rejected the canary upload.");
|
|
156
237
|
}
|
|
@@ -164,12 +245,13 @@ async function verifyPostPath(baseUrl, config, signal) {
|
|
|
164
245
|
{ cause: error }
|
|
165
246
|
);
|
|
166
247
|
}
|
|
167
|
-
if (body.ok !== true || Number(body.receivedBytes) !==
|
|
248
|
+
if (body.ok !== true || Number(body.receivedBytes) !== canaryBytes) {
|
|
168
249
|
throw new DeliveryReadinessError(
|
|
169
250
|
"media-storage-invalid-response",
|
|
170
251
|
"The recording relay did not acknowledge the complete canary."
|
|
171
252
|
);
|
|
172
253
|
}
|
|
254
|
+
return uploadDurationMs;
|
|
173
255
|
}
|
|
174
256
|
async function postJson(url, body, appId, signal) {
|
|
175
257
|
const headers = { "content-type": "application/json" };
|
|
@@ -203,7 +285,8 @@ async function abortDirectCanary(baseUrl, config, canary) {
|
|
|
203
285
|
{
|
|
204
286
|
sessionId: config.sessionId,
|
|
205
287
|
canaryId: canary.canaryId,
|
|
206
|
-
uploadId: canary.uploadId
|
|
288
|
+
uploadId: canary.uploadId,
|
|
289
|
+
expectedBytes: canary.expectedBytes
|
|
207
290
|
},
|
|
208
291
|
config.appId,
|
|
209
292
|
controller.signal
|
|
@@ -213,6 +296,72 @@ async function abortDirectCanary(baseUrl, config, canary) {
|
|
|
213
296
|
clearTimeout(timeout);
|
|
214
297
|
}
|
|
215
298
|
}
|
|
299
|
+
async function runPathProbe(baseUrl, config, mode, bytes, now2) {
|
|
300
|
+
return withReadinessTimeout(
|
|
301
|
+
RECORDING_READINESS_TIMEOUT_MS,
|
|
302
|
+
"media",
|
|
303
|
+
(signal) => mode === "direct" || mode === "segments" ? verifyDirectPath(baseUrl, config, bytes, signal, now2) : verifyPostPath(baseUrl, config, bytes, signal, now2)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
async function withReadinessTimeout(timeoutMs, phase, run) {
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
309
|
+
try {
|
|
310
|
+
return await run(controller.signal);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (error instanceof DeliveryReadinessError) throw error;
|
|
313
|
+
if (controller.signal.aborted) {
|
|
314
|
+
throw new DeliveryReadinessError(
|
|
315
|
+
phase === "config" ? "upload-config-timeout" : "media-storage-timeout",
|
|
316
|
+
phase === "config" ? "Timed out while resolving the recording upload path." : "Timed out while verifying recording storage.",
|
|
317
|
+
{ cause: error }
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
throw new DeliveryReadinessError(
|
|
321
|
+
phase === "config" ? "upload-config-unreachable" : "media-storage-unreachable",
|
|
322
|
+
phase === "config" ? "The recording upload configuration could not be reached." : "The recording storage path could not be reached.",
|
|
323
|
+
{ cause: error }
|
|
324
|
+
);
|
|
325
|
+
} finally {
|
|
326
|
+
clearTimeout(timeout);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function normaliseCanaryBytes(value) {
|
|
330
|
+
if (!Number.isFinite(value) || value < 1) {
|
|
331
|
+
throw new DeliveryReadinessError(
|
|
332
|
+
"media-storage-invalid-response",
|
|
333
|
+
"The recording readiness payload size must be a positive number."
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return Math.min(MAX_CANARY_BYTES, Math.max(1, Math.round(value)));
|
|
337
|
+
}
|
|
338
|
+
function normaliseCompletionWindow(value) {
|
|
339
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
340
|
+
throw new DeliveryReadinessError(
|
|
341
|
+
"media-storage-invalid-response",
|
|
342
|
+
"The recording readiness completion window must be positive."
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return Math.max(100, Math.round(value));
|
|
346
|
+
}
|
|
347
|
+
function median(values) {
|
|
348
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
349
|
+
const middle = Math.floor(sorted.length / 2);
|
|
350
|
+
if (sorted.length % 2 === 1) return sorted[middle];
|
|
351
|
+
return (sorted[middle - 1] + sorted[middle]) / 2;
|
|
352
|
+
}
|
|
353
|
+
function measurementDetails(mode, payloadBytes, completionWindowMs, successfulSamples, medianCompletionMs) {
|
|
354
|
+
return {
|
|
355
|
+
recordingUploadMode: mode,
|
|
356
|
+
readinessPayloadBytes: payloadBytes,
|
|
357
|
+
readinessWindowMs: Math.round(completionWindowMs),
|
|
358
|
+
readinessSampleCount: successfulSamples,
|
|
359
|
+
...medianCompletionMs !== void 0 ? { readinessMedianMs: Math.round(medianCompletionMs) } : {}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function defaultNow$1() {
|
|
363
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
364
|
+
}
|
|
216
365
|
function mediaHttpError(status, message) {
|
|
217
366
|
return new DeliveryReadinessError(
|
|
218
367
|
status >= 400 && status < 500 ? "media-storage-rejected" : "media-storage-unreachable",
|
|
@@ -3595,9 +3744,9 @@ class MediaFilePlayer {
|
|
|
3595
3744
|
return {
|
|
3596
3745
|
url: this.safeUrl,
|
|
3597
3746
|
...this.label ? { label: this.label } : {},
|
|
3598
|
-
currentTime: finiteNumber(this.element.currentTime),
|
|
3599
|
-
duration: finiteNumber(this.element.duration),
|
|
3600
|
-
playbackRate: finiteNumber(this.element.playbackRate),
|
|
3747
|
+
currentTime: finiteNumber$1(this.element.currentTime),
|
|
3748
|
+
duration: finiteNumber$1(this.element.duration),
|
|
3749
|
+
playbackRate: finiteNumber$1(this.element.playbackRate),
|
|
3601
3750
|
...extra
|
|
3602
3751
|
};
|
|
3603
3752
|
}
|
|
@@ -3610,7 +3759,7 @@ function getMediaDevices() {
|
|
|
3610
3759
|
}
|
|
3611
3760
|
return mediaDevices;
|
|
3612
3761
|
}
|
|
3613
|
-
function finiteNumber(value) {
|
|
3762
|
+
function finiteNumber$1(value) {
|
|
3614
3763
|
return Number.isFinite(value) ? value : null;
|
|
3615
3764
|
}
|
|
3616
3765
|
function safeMediaUrl(input) {
|
|
@@ -4133,8 +4282,8 @@ async function captureBaseline(selected, dependsOn) {
|
|
|
4133
4282
|
if (dependsOn.camera) permissions.camera = await queryPermission("camera");
|
|
4134
4283
|
return { permissions, devices: selected, display: probeDisplay() };
|
|
4135
4284
|
}
|
|
4136
|
-
const SDK_VERSION = "0.
|
|
4137
|
-
const SDK_BUILD_SHA = "
|
|
4285
|
+
const SDK_VERSION = "0.2.0";
|
|
4286
|
+
const SDK_BUILD_SHA = "0e08674057f5d4f2e5c5fac34cd5feeaa40d97f4";
|
|
4138
4287
|
const SDK_BUILD_INFO = Object.freeze({
|
|
4139
4288
|
sdkVersion: SDK_VERSION,
|
|
4140
4289
|
buildSha: SDK_BUILD_SHA
|
|
@@ -6131,17 +6280,6 @@ const applySpeedToResult = (speed, minMbps = MIN_SPEED_MBPS) => {
|
|
|
6131
6280
|
}
|
|
6132
6281
|
};
|
|
6133
6282
|
}
|
|
6134
|
-
if (speed < minMbps) {
|
|
6135
|
-
return {
|
|
6136
|
-
kind: "connection",
|
|
6137
|
-
state: {
|
|
6138
|
-
kind: "fail",
|
|
6139
|
-
code: "slow-connection",
|
|
6140
|
-
detail: `${speed} Mbps`
|
|
6141
|
-
},
|
|
6142
|
-
mbps: speed
|
|
6143
|
-
};
|
|
6144
|
-
}
|
|
6145
6283
|
const band = connectionQualityBand(speed, minMbps);
|
|
6146
6284
|
return {
|
|
6147
6285
|
kind: "connection",
|
|
@@ -6170,37 +6308,14 @@ const applyConnectionQualityToResult = (result, minDownloadMbps = MIN_SPEED_MBPS
|
|
|
6170
6308
|
...fields
|
|
6171
6309
|
};
|
|
6172
6310
|
}
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
state: {
|
|
6177
|
-
kind: "fail",
|
|
6178
|
-
code: "slow-connection",
|
|
6179
|
-
detail: `Upload ${result.uploadMbps ?? upload} Mbps`
|
|
6180
|
-
},
|
|
6181
|
-
...fields
|
|
6182
|
-
};
|
|
6183
|
-
}
|
|
6184
|
-
if (download < minDownloadMbps) {
|
|
6185
|
-
return {
|
|
6186
|
-
kind: "connection",
|
|
6187
|
-
state: {
|
|
6188
|
-
kind: "fail",
|
|
6189
|
-
code: "slow-connection",
|
|
6190
|
-
detail: `Download ${result.downloadMbps ?? download} Mbps`
|
|
6191
|
-
},
|
|
6192
|
-
...fields
|
|
6193
|
-
};
|
|
6194
|
-
}
|
|
6195
|
-
const band = worseBand(
|
|
6196
|
-
connectionQualityBand(upload, minUploadMbps),
|
|
6197
|
-
connectionQualityBand(download, minDownloadMbps)
|
|
6198
|
-
);
|
|
6311
|
+
const uploadBand = connectionQualityBand(upload, minUploadMbps);
|
|
6312
|
+
const downloadBand = connectionQualityBand(download, minDownloadMbps);
|
|
6313
|
+
const band = uploadBand && downloadBand ? worseBand(uploadBand, downloadBand) : null;
|
|
6199
6314
|
return {
|
|
6200
6315
|
kind: "connection",
|
|
6201
6316
|
state: {
|
|
6202
6317
|
kind: "pass",
|
|
6203
|
-
detail: band ? BAND_LABELS[band] :
|
|
6318
|
+
detail: band ? BAND_LABELS[band] : "Connection available"
|
|
6204
6319
|
},
|
|
6205
6320
|
...fields,
|
|
6206
6321
|
...band ? { band } : {}
|
|
@@ -6771,19 +6886,29 @@ const cameraReadyDetail = (video) => {
|
|
|
6771
6886
|
return label && label.length > 0 ? label : "Ready";
|
|
6772
6887
|
};
|
|
6773
6888
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6774
|
-
|
|
6775
|
-
"
|
|
6776
|
-
|
|
6777
|
-
"
|
|
6778
|
-
|
|
6779
|
-
"
|
|
6780
|
-
];
|
|
6781
|
-
const
|
|
6782
|
-
"
|
|
6783
|
-
"
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6889
|
+
function readinessErrorDetails(error) {
|
|
6890
|
+
if (!error || typeof error !== "object" || !("details" in error)) return {};
|
|
6891
|
+
const details = error.details;
|
|
6892
|
+
if (!details || typeof details !== "object") return {};
|
|
6893
|
+
const values = details;
|
|
6894
|
+
const mode = values["recordingUploadMode"];
|
|
6895
|
+
const payloadBytes = finiteNumber(values["readinessPayloadBytes"]);
|
|
6896
|
+
const medianMs = finiteNumber(values["readinessMedianMs"]);
|
|
6897
|
+
const windowMs = finiteNumber(values["readinessWindowMs"]);
|
|
6898
|
+
const sampleCount = finiteNumber(values["readinessSampleCount"]);
|
|
6899
|
+
return {
|
|
6900
|
+
...mode === "direct" || mode === "segments" || mode === "post" ? { recordingUploadMode: mode } : {},
|
|
6901
|
+
...payloadBytes !== void 0 ? { readinessPayloadBytes: payloadBytes } : {},
|
|
6902
|
+
...medianMs !== void 0 ? { readinessMedianMs: medianMs } : {},
|
|
6903
|
+
...windowMs !== void 0 ? { readinessWindowMs: windowMs } : {},
|
|
6904
|
+
...sampleCount !== void 0 ? { readinessSampleCount: Math.max(0, Math.floor(sampleCount)) } : {}
|
|
6905
|
+
};
|
|
6906
|
+
}
|
|
6907
|
+
function finiteNumber(value) {
|
|
6908
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
6909
|
+
}
|
|
6910
|
+
const BROWSER_CLASS_KINDS = ["browser", "device", "layout", "monitor", "connection"];
|
|
6911
|
+
const MEDIA_KINDS = ["microphone", "speaker", "camera", "screen-share"];
|
|
6787
6912
|
const GATE_KINDS = ["browser", "device"];
|
|
6788
6913
|
const GATED_SYSTEM_KINDS = ["layout", "monitor", "connection"];
|
|
6789
6914
|
const THRESHOLD_DEFAULTS = {
|
|
@@ -6823,7 +6948,8 @@ class SystemCheck {
|
|
|
6823
6948
|
thresholds: { ...THRESHOLD_DEFAULTS, ...options.thresholds ?? {} },
|
|
6824
6949
|
overrides: options.overrides,
|
|
6825
6950
|
deepCamera: options.deepCamera,
|
|
6826
|
-
connectionTest: options.connectionTest
|
|
6951
|
+
connectionTest: options.connectionTest,
|
|
6952
|
+
connectionReadiness: options.connectionReadiness
|
|
6827
6953
|
};
|
|
6828
6954
|
this.rows = this.buildInitialRows();
|
|
6829
6955
|
}
|
|
@@ -6900,11 +7026,9 @@ class SystemCheck {
|
|
|
6900
7026
|
continue;
|
|
6901
7027
|
}
|
|
6902
7028
|
if (done) break;
|
|
6903
|
-
const next = await new Promise(
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
}
|
|
6907
|
-
);
|
|
7029
|
+
const next = await new Promise((resolve) => {
|
|
7030
|
+
resolveWaiter = resolve;
|
|
7031
|
+
});
|
|
6908
7032
|
if (next === null) break;
|
|
6909
7033
|
yield next;
|
|
6910
7034
|
}
|
|
@@ -7011,17 +7135,46 @@ class SystemCheck {
|
|
|
7011
7135
|
message: "Testing connection..."
|
|
7012
7136
|
});
|
|
7013
7137
|
const start = performance.now();
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
this.options.
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7138
|
+
let result;
|
|
7139
|
+
if (this.options.connectionReadiness) {
|
|
7140
|
+
try {
|
|
7141
|
+
const readiness = await this.options.connectionReadiness();
|
|
7142
|
+
result = {
|
|
7143
|
+
kind: "connection",
|
|
7144
|
+
state: { kind: "pass", detail: "Connection available" },
|
|
7145
|
+
recordingUploadMode: readiness.mode,
|
|
7146
|
+
readinessPayloadBytes: readiness.payloadBytes,
|
|
7147
|
+
readinessMedianMs: readiness.medianCompletionMs,
|
|
7148
|
+
readinessWindowMs: readiness.completionWindowMs,
|
|
7149
|
+
readinessSampleCount: readiness.sampleDurationsMs.length
|
|
7150
|
+
};
|
|
7151
|
+
} catch (error) {
|
|
7152
|
+
const diagnostic = error && typeof error === "object" && "code" in error ? String(error.code) : "unavailable";
|
|
7153
|
+
const details = readinessErrorDetails(error);
|
|
7154
|
+
result = {
|
|
7155
|
+
kind: "connection",
|
|
7156
|
+
state: {
|
|
7157
|
+
kind: "fail",
|
|
7158
|
+
code: "speed-test-failed",
|
|
7159
|
+
detail: `Connection readiness failed: ${diagnostic}`
|
|
7160
|
+
},
|
|
7161
|
+
...details.recordingUploadMode !== void 0 ? { recordingUploadMode: details.recordingUploadMode } : {},
|
|
7162
|
+
...details.readinessPayloadBytes !== void 0 ? { readinessPayloadBytes: details.readinessPayloadBytes } : {},
|
|
7163
|
+
...details.readinessMedianMs !== void 0 ? { readinessMedianMs: details.readinessMedianMs } : {},
|
|
7164
|
+
...details.readinessWindowMs !== void 0 ? { readinessWindowMs: details.readinessWindowMs } : {},
|
|
7165
|
+
...details.readinessSampleCount !== void 0 ? { readinessSampleCount: details.readinessSampleCount } : {}
|
|
7166
|
+
};
|
|
7167
|
+
}
|
|
7168
|
+
} else {
|
|
7169
|
+
result = this.options.connectionTest ? applyConnectionQualityToResult(
|
|
7170
|
+
await measureConnectionQuality(
|
|
7171
|
+
createHttpConnectionProbe(this.options.connectionTest),
|
|
7172
|
+
this.options.connectionTest
|
|
7173
|
+
),
|
|
7174
|
+
this.options.thresholds.minBandwidthMbps,
|
|
7175
|
+
this.options.thresholds.minUploadBandwidthMbps
|
|
7176
|
+
) : applySpeedToResult(await measureSpeed(), this.options.thresholds.minBandwidthMbps);
|
|
7177
|
+
}
|
|
7025
7178
|
const elapsed = performance.now() - start;
|
|
7026
7179
|
if (elapsed < this.options.minSpeedTestMs) {
|
|
7027
7180
|
await this.delay(this.options.minSpeedTestMs - elapsed);
|
|
@@ -7042,6 +7195,21 @@ class SystemCheck {
|
|
|
7042
7195
|
if (result.band !== void 0) {
|
|
7043
7196
|
this.setRowField("connection", "band", result.band);
|
|
7044
7197
|
}
|
|
7198
|
+
if (result.recordingUploadMode !== void 0) {
|
|
7199
|
+
this.setRowField("connection", "recordingUploadMode", result.recordingUploadMode);
|
|
7200
|
+
}
|
|
7201
|
+
if (result.readinessPayloadBytes !== void 0) {
|
|
7202
|
+
this.setRowField("connection", "readinessPayloadBytes", result.readinessPayloadBytes);
|
|
7203
|
+
}
|
|
7204
|
+
if (result.readinessMedianMs !== void 0) {
|
|
7205
|
+
this.setRowField("connection", "readinessMedianMs", result.readinessMedianMs);
|
|
7206
|
+
}
|
|
7207
|
+
if (result.readinessWindowMs !== void 0) {
|
|
7208
|
+
this.setRowField("connection", "readinessWindowMs", result.readinessWindowMs);
|
|
7209
|
+
}
|
|
7210
|
+
if (result.readinessSampleCount !== void 0) {
|
|
7211
|
+
this.setRowField("connection", "readinessSampleCount", result.readinessSampleCount);
|
|
7212
|
+
}
|
|
7045
7213
|
}
|
|
7046
7214
|
/**
|
|
7047
7215
|
* Run just the system-class checks (browser → device → layout →
|
|
@@ -7091,9 +7259,7 @@ class SystemCheck {
|
|
|
7091
7259
|
}
|
|
7092
7260
|
/** True once a non-retryable gate check (browser/device) has failed. */
|
|
7093
7261
|
gateTripped() {
|
|
7094
|
-
return this.rows.some(
|
|
7095
|
-
(r) => GATE_KINDS.includes(r.kind) && r.state.kind === "fail"
|
|
7096
|
-
);
|
|
7262
|
+
return this.rows.some((r) => GATE_KINDS.includes(r.kind) && r.state.kind === "fail");
|
|
7097
7263
|
}
|
|
7098
7264
|
/** Mark every still-pending row of the given kinds as skipped. */
|
|
7099
7265
|
skipPending(kinds) {
|
|
@@ -7146,11 +7312,7 @@ class SystemCheck {
|
|
|
7146
7312
|
message: "Checking permission..."
|
|
7147
7313
|
});
|
|
7148
7314
|
const perms = await queryPermissions({ mic: false, camera: true });
|
|
7149
|
-
const camState = await this.resolveCameraState(
|
|
7150
|
-
perms.camera,
|
|
7151
|
-
deviceId,
|
|
7152
|
-
existingStream
|
|
7153
|
-
);
|
|
7315
|
+
const camState = await this.resolveCameraState(perms.camera, deviceId, existingStream);
|
|
7154
7316
|
this.setState("camera", camState.state);
|
|
7155
7317
|
if (camState.photo) this.setRowField("camera", "photo", camState.photo);
|
|
7156
7318
|
return { attempts: camState.attempts ?? [] };
|
|
@@ -10132,6 +10294,7 @@ export {
|
|
|
10132
10294
|
isSafari,
|
|
10133
10295
|
isSamsungBrowser,
|
|
10134
10296
|
measureConnectionQuality,
|
|
10297
|
+
measureRecordingDeliveryReadiness,
|
|
10135
10298
|
measureSpeed,
|
|
10136
10299
|
normalizeClipDropCode,
|
|
10137
10300
|
normalizeClipStartErrorCode,
|
|
@@ -64,6 +64,16 @@ export interface CheckRow {
|
|
|
64
64
|
latencyMs?: number;
|
|
65
65
|
/** Median variation between latency samples in milliseconds. Connection row only. */
|
|
66
66
|
jitterMs?: number;
|
|
67
|
+
/** Server-selected recording upload mode verified by the readiness canary. */
|
|
68
|
+
recordingUploadMode?: "direct" | "segments" | "post";
|
|
69
|
+
/** Representative readiness payload size. Internal telemetry only. */
|
|
70
|
+
readinessPayloadBytes?: number;
|
|
71
|
+
/** Median representative upload completion time. Internal telemetry only. */
|
|
72
|
+
readinessMedianMs?: number;
|
|
73
|
+
/** Workload-derived completion window. Internal telemetry only. */
|
|
74
|
+
readinessWindowMs?: number;
|
|
75
|
+
/** Number of successful representative readiness samples. Internal telemetry only. */
|
|
76
|
+
readinessSampleCount?: number;
|
|
67
77
|
/**
|
|
68
78
|
* Plain-language quality bucket for a PASSING connection row (Fair / Good /
|
|
69
79
|
* Excellent), relative to the configured floor. Absent on fail rows and on
|
|
@@ -88,19 +98,17 @@ export interface CheckOverrides {
|
|
|
88
98
|
/** Override browser support for `getDisplayMedia`. */
|
|
89
99
|
screenShareSupported?: boolean;
|
|
90
100
|
}
|
|
91
|
-
/**
|
|
101
|
+
/** Default reference floor for downlink quality telemetry in megabits/sec. */
|
|
92
102
|
export declare const MIN_SPEED_MBPS = 2;
|
|
93
103
|
/**
|
|
94
|
-
* Plain-language quality bucket
|
|
95
|
-
*
|
|
96
|
-
*
|
|
104
|
+
* Plain-language quality bucket for measured connection telemetry. A value
|
|
105
|
+
* below the reference floor has no band; measured Mbps never decides candidate
|
|
106
|
+
* eligibility.
|
|
97
107
|
*/
|
|
98
108
|
export type ConnectionBand = "fair" | "good" | "excellent";
|
|
99
109
|
/**
|
|
100
|
-
* Bucket a measured speed into a {@link ConnectionBand} relative to the
|
|
101
|
-
* Returns null below the floor
|
|
102
|
-
* non-positive input or floor. Pure — the single source of truth for both the
|
|
103
|
-
* application-path and legacy download-only rows.
|
|
110
|
+
* Bucket a measured speed into a {@link ConnectionBand} relative to the
|
|
111
|
+
* reference floor. Returns null below the floor or for invalid inputs.
|
|
104
112
|
*/
|
|
105
113
|
export declare const connectionQualityBand: (mbps: number, floorMbps: number) => ConnectionBand | null;
|
|
106
114
|
/** Supported browsers, surfaced on the wizard's "your browser isn't supported" alert. */
|
|
@@ -175,18 +183,15 @@ export declare const measureSpeed: () => Promise<number | null>;
|
|
|
175
183
|
*/
|
|
176
184
|
export declare const checkConnection: (overrides?: CheckOverrides) => CheckRow | null;
|
|
177
185
|
/**
|
|
178
|
-
* Build the connection row from a measured speed (or null on
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* unchanged; the engine threads the policy override through when
|
|
182
|
-
* present.
|
|
186
|
+
* Build the connection row from a measured speed (or null on measurement
|
|
187
|
+
* failure). Numeric speed is retained as telemetry, but never gates the
|
|
188
|
+
* candidate because short browser probes are dominated by request latency.
|
|
183
189
|
*/
|
|
184
190
|
export declare const applySpeedToResult: (speed: number | null, minMbps?: number) => CheckRow;
|
|
185
191
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* public contract; direction-specific values are carried on the row.
|
|
192
|
+
* Preserve application-path measurements as internal diagnostics. A completed
|
|
193
|
+
* measurement passes regardless of the estimated Mbps; candidate eligibility
|
|
194
|
+
* is decided separately by the workload-aware recording-storage canary.
|
|
190
195
|
*/
|
|
191
196
|
export declare const applyConnectionQualityToResult: (result: ConnectionQualityResult, minDownloadMbps?: number, minUploadMbps?: number) => CheckRow;
|
|
192
197
|
//# sourceMappingURL=checks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/preflight/checks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,KAAK,WAAW,EASjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAEvE,oFAAoF;AACpF,MAAM,MAAM,QAAQ,GAChB,qBAAqB,GACrB,kBAAkB,GAClB,YAAY,GACZ,YAAY,GACZ,qBAAqB,GACrB,YAAY,GACZ,kBAAkB,GAClB,SAAS,GACT,iBAAiB,GACjB,mBAAmB,GACnB,mBAAmB,GACnB,qBAAqB,GACrB,qBAAqB,GACrB,iBAAiB,GACjB,SAAS,GACT,gBAAgB,GAChB,mBAAmB,GACnB,uBAAuB,GACvB,4BAA4B,GAC5B,4BAA4B,CAAC;AAEjC;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,QAAQ,CAMrD,CAAC;AAEH,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,GAAI,MAAM,QAAQ,KAAG,
|
|
1
|
+
{"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/preflight/checks.ts"],"names":[],"mappings":"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"}
|