@a4anthony/proctorkit-sdk 0.1.1 → 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/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 CANARY_BYTES = 1024;
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(CANARY_BYTES)], {
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) !== CANARY_BYTES) {
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(CANARY_BYTES)], {
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) !== CANARY_BYTES) {
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.1.1";
4137
- const SDK_BUILD_SHA = "de98da0125619b3b2d4486f45c10d4a8f7a85113";
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
- if (upload < minUploadMbps) {
6174
- return {
6175
- kind: "connection",
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] : `Up ${result.uploadMbps} · Down ${result.downloadMbps} Mbps`
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
- const BROWSER_CLASS_KINDS = [
6775
- "browser",
6776
- "device",
6777
- "layout",
6778
- "monitor",
6779
- "connection"
6780
- ];
6781
- const MEDIA_KINDS = [
6782
- "microphone",
6783
- "speaker",
6784
- "camera",
6785
- "screen-share"
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
- (resolve) => {
6905
- resolveWaiter = resolve;
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
- const result = this.options.connectionTest ? applyConnectionQualityToResult(
7015
- await measureConnectionQuality(
7016
- createHttpConnectionProbe(this.options.connectionTest),
7017
- this.options.connectionTest
7018
- ),
7019
- this.options.thresholds.minBandwidthMbps,
7020
- this.options.thresholds.minUploadBandwidthMbps
7021
- ) : applySpeedToResult(
7022
- await measureSpeed(),
7023
- this.options.thresholds.minBandwidthMbps
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
- /** Minimum acceptable downlink speed in megabits/sec. */
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 shown to the candidate INSTEAD of a raw Mbps
95
- * figure. Below the floor is not a band that's the `slow-connection` fail —
96
- * so a passing connection is always one of these three.
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 floor.
101
- * Returns null below the floor (the fail path owns that) or for a non-finite /
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
- * measurement failure). The `minMbps` threshold defaults to the
180
- * module-level `MIN_SPEED_MBPS` so existing call sites work
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
- * Apply policy to the richer application-path result. Upload is evaluated
187
- * first because recordings must leave the browser faster than MediaRecorder
188
- * produces them. The existing `slow-connection` code remains the stable
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,OACrB,CAAC;AAEjC,qFAAqF;AACrF,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAExB,kCAAkC;AAClC,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,YAAY,GACZ,YAAY,GACZ,SAAS,GACT,QAAQ,GACR,cAAc,CAAC;AAEnB;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,UAAU,CAAC;IAClB,yFAAyF;IACzF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iEAAiE;IACjE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,sDAAsD;IACtD,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,yDAAyD;AACzD,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC;AAY3D;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,WAAW,MAAM,KAChB,cAAc,GAAG,IAYnB,CAAC;AASF,yFAAyF;AACzF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;EAMrB,CAAC;AAMX,+CAA+C;AAC/C,eAAO,MAAM,YAAY,GAAI,YAAY,cAAc,KAAG,QA6CzD,CAAC;AAMF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,GACtB,YAAY,cAAc,EAC1B,qBAAmB,EACnB,mCAAiC,KAChC,QAkDF,CAAC;AAeF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,YAAY,cAAc,KAAG,QAmB9D,CAAC;AAMF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,GAC/B,YAAY,cAAc,EAC1B,yBAAuB,EACvB,8BAA4B,KAC3B,QA4BF,CAAC;AA8FF;;;;GAIG;AACH,eAAO,MAAM,YAAY,QAAa,OAAO,CAAC,MAAM,GAAG,IAAI,CAmB1D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,YAAY,cAAc,KAAG,QAAQ,GAAG,IAYvE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,MAAM,GAAG,IAAI,EACpB,UAAS,MAAuB,KAC/B,QA6BF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GACzC,QAAQ,uBAAuB,EAC/B,kBAAiB,MAAuB,EACxC,gBAAe,MAAuB,KACrC,QA6DF,CAAC"}
1
+ {"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/preflight/checks.ts"],"names":[],"mappings":"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"}