@ttt-productions/media-processing-core 0.8.17 → 0.8.18
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/audio/audio-processor.d.ts.map +1 -1
- package/dist/audio/audio-processor.js +1 -2
- package/dist/audio/audio-processor.js.map +1 -1
- package/dist/audio/probe.d.ts.map +1 -1
- package/dist/audio/probe.js +0 -2
- package/dist/audio/probe.js.map +1 -1
- package/dist/duration/media-duration.d.ts +0 -11
- package/dist/duration/media-duration.d.ts.map +1 -1
- package/dist/duration/media-duration.js +1 -28
- package/dist/duration/media-duration.js.map +1 -1
- package/dist/image/formats.d.ts +0 -11
- package/dist/image/formats.d.ts.map +1 -1
- package/dist/image/formats.js +0 -10
- package/dist/image/formats.js.map +1 -1
- package/dist/image/image-processor.d.ts.map +1 -1
- package/dist/image/image-processor.js +1 -20
- package/dist/image/image-processor.js.map +1 -1
- package/dist/image/resize.d.ts +0 -14
- package/dist/image/resize.d.ts.map +1 -1
- package/dist/image/resize.js +0 -15
- package/dist/image/resize.js.map +1 -1
- package/dist/inspection/inspect-media.d.ts +0 -7
- package/dist/inspection/inspect-media.d.ts.map +1 -1
- package/dist/inspection/inspect-media.js +4 -64
- package/dist/inspection/inspect-media.js.map +1 -1
- package/dist/io/fs.d.ts.map +1 -1
- package/dist/io/fs.js +0 -1
- package/dist/io/fs.js.map +1 -1
- package/dist/io/local-input.d.ts +0 -20
- package/dist/io/local-input.d.ts.map +1 -1
- package/dist/io/local-input.js +0 -20
- package/dist/io/local-input.js.map +1 -1
- package/dist/io/types.d.ts +0 -3
- package/dist/io/types.d.ts.map +1 -1
- package/dist/io/types.js.map +1 -1
- package/dist/moderation/merge.d.ts.map +1 -1
- package/dist/moderation/merge.js +0 -1
- package/dist/moderation/merge.js.map +1 -1
- package/dist/moderation/types.d.ts +0 -2
- package/dist/moderation/types.d.ts.map +1 -1
- package/dist/moderation/types.js.map +1 -1
- package/dist/run-pipeline.d.ts +0 -17
- package/dist/run-pipeline.d.ts.map +1 -1
- package/dist/run-pipeline.js +2 -27
- package/dist/run-pipeline.js.map +1 -1
- package/dist/server/download-token.d.ts +0 -6
- package/dist/server/download-token.d.ts.map +1 -1
- package/dist/server/download-token.js +0 -10
- package/dist/server/download-token.js.map +1 -1
- package/dist/server/firebase-media-io.d.ts +1 -11
- package/dist/server/firebase-media-io.d.ts.map +1 -1
- package/dist/server/firebase-media-io.js +0 -5
- package/dist/server/firebase-media-io.js.map +1 -1
- package/dist/server/gcs-errors.d.ts +0 -8
- package/dist/server/gcs-errors.d.ts.map +1 -1
- package/dist/server/gcs-errors.js +0 -8
- package/dist/server/gcs-errors.js.map +1 -1
- package/dist/server/pending-media.d.ts +1 -32
- package/dist/server/pending-media.d.ts.map +1 -1
- package/dist/server/pending-media.js +1 -24
- package/dist/server/pending-media.js.map +1 -1
- package/dist/server/r2-retry.d.ts +0 -48
- package/dist/server/r2-retry.d.ts.map +1 -1
- package/dist/server/r2-retry.js +4 -90
- package/dist/server/r2-retry.js.map +1 -1
- package/dist/server/staged-object.d.ts +0 -44
- package/dist/server/staged-object.d.ts.map +1 -1
- package/dist/server/staged-object.js +0 -44
- package/dist/server/staged-object.js.map +1 -1
- package/dist/server/storage-ops.d.ts +2 -62
- package/dist/server/storage-ops.d.ts.map +1 -1
- package/dist/server/storage-ops.js +9 -88
- package/dist/server/storage-ops.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/safe-path.d.ts.map +1 -1
- package/dist/utils/safe-path.js +1 -2
- package/dist/utils/safe-path.js.map +1 -1
- package/dist/video/ffmpeg.d.ts +0 -22
- package/dist/video/ffmpeg.d.ts.map +1 -1
- package/dist/video/ffmpeg.js +6 -21
- package/dist/video/ffmpeg.js.map +1 -1
- package/dist/video/probe.d.ts.map +1 -1
- package/dist/video/probe.js +0 -1
- package/dist/video/probe.js.map +1 -1
- package/dist/video/video-processor.d.ts.map +1 -1
- package/dist/video/video-processor.js +2 -15
- package/dist/video/video-processor.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,71 +1,29 @@
|
|
|
1
|
-
/** TOTAL attempts (initial + retries) — three totals means at most TWO delays. */
|
|
2
1
|
export declare const R2_REQUEST_MAX_ATTEMPTS = 3;
|
|
3
|
-
/** Base delay for full-jitter exponential backoff. */
|
|
4
2
|
export declare const R2_RETRY_BASE_DELAY_MS = 250;
|
|
5
|
-
/** Ceiling on the exponential backoff (before jitter). */
|
|
6
3
|
export declare const R2_RETRY_MAX_BACKOFF_MS = 5000;
|
|
7
|
-
/** Hard cap on an honored server `Retry-After` so one response can't eat the
|
|
8
|
-
* caller's whole timeout budget. */
|
|
9
4
|
export declare const R2_RETRY_AFTER_CAP_MS = 30000;
|
|
10
|
-
/** HTTP statuses treated as transient and safe to retry. */
|
|
11
5
|
export declare const R2_RETRYABLE_STATUSES: ReadonlySet<number>;
|
|
12
6
|
export type R2RetryableOperation = 'putFile' | 'copy' | 'readToFile' | 'deleteCopy';
|
|
13
|
-
/** Every R2 store operation an `R2StorageError` can name; `delete` never retries. */
|
|
14
7
|
export type R2Operation = R2RetryableOperation | 'delete';
|
|
15
|
-
/** Deterministic seams for tests; production defaults are internal (see below). */
|
|
16
8
|
export interface R2RetrySeams {
|
|
17
|
-
/** Resolve after `ms` milliseconds. */
|
|
18
9
|
sleep: (ms: number) => Promise<void>;
|
|
19
|
-
/** Uniform random in [0, 1). */
|
|
20
10
|
random: () => number;
|
|
21
|
-
/** Epoch-millisecond clock — keeps HTTP-date `Retry-After` tests off wall-clock. */
|
|
22
11
|
now: () => number;
|
|
23
12
|
}
|
|
24
|
-
/** Production seam defaults. */
|
|
25
13
|
export declare function defaultR2RetrySeams(): R2RetrySeams;
|
|
26
|
-
/**
|
|
27
|
-
* True only for recognized transient transport failures. An explicit
|
|
28
|
-
* `AbortError` and deterministic local-filesystem errors always return false —
|
|
29
|
-
* an abort must never become another attempt.
|
|
30
|
-
*/
|
|
31
14
|
export declare function isRetryableTransportError(err: unknown): boolean;
|
|
32
|
-
/**
|
|
33
|
-
* Parse a `Retry-After` header value into a non-negative millisecond delay.
|
|
34
|
-
* Supports both the delta-seconds form (`"120"`) and the HTTP-date form
|
|
35
|
-
* (`"Wed, 21 Oct 2015 07:28:00 GMT"`). Returns null when absent/unparseable.
|
|
36
|
-
* An HTTP-date in the past clamps to 0.
|
|
37
|
-
*/
|
|
38
15
|
export declare function parseRetryAfterMs(headerValue: string | null | undefined, nowMs: number): number | null;
|
|
39
|
-
/**
|
|
40
|
-
* Compute the pre-next-attempt delay. If the failed response carries a valid
|
|
41
|
-
* `Retry-After`, honor it (capped, no backoff added on top). Otherwise use
|
|
42
|
-
* full-jitter exponential backoff: `random(0, min(maxBackoff, base * 2^retryIndex))`.
|
|
43
|
-
*/
|
|
44
16
|
export declare function computeR2DelayMs(params: {
|
|
45
17
|
response: Response | null;
|
|
46
|
-
/** 0-based index of the retry about to happen (0 for the first retry). */
|
|
47
18
|
retryIndex: number;
|
|
48
19
|
seams: R2RetrySeams;
|
|
49
20
|
}): number;
|
|
50
|
-
/**
|
|
51
|
-
* Thrown when an R2 operation fails: a retried operation exhausts its attempts or
|
|
52
|
-
* hits a non-retryable response, or a one-shot delete fails. Carries
|
|
53
|
-
* operation/bucket/key/attempt/status context, the S3 error code parsed from the
|
|
54
|
-
* response body, and the bounded body itself as properties — never credentials,
|
|
55
|
-
* authorization headers, or full signed URLs. Callers classify it from those
|
|
56
|
-
* properties, never from the message.
|
|
57
|
-
*
|
|
58
|
-
* The message names only the operation, the attempts, the status, and the S3 error
|
|
59
|
-
* code. The bucket, the key, and the raw body — which can echo either — stay out of
|
|
60
|
-
* it, so an escaping error never carries an object key to monitoring.
|
|
61
|
-
*/
|
|
62
21
|
export declare class R2StorageError extends Error {
|
|
63
22
|
readonly operation: R2Operation;
|
|
64
23
|
readonly bucket: string;
|
|
65
24
|
readonly key: string;
|
|
66
25
|
readonly attempts: number;
|
|
67
26
|
readonly status?: number;
|
|
68
|
-
/** The S3 `<Code>` of the response body (`NoSuchKey`, `PreconditionFailed`, …), when it had one. */
|
|
69
27
|
readonly s3ErrorCode?: string;
|
|
70
28
|
readonly responseText?: string;
|
|
71
29
|
constructor(params: {
|
|
@@ -85,12 +43,6 @@ export type R2AttemptResult<T> = {
|
|
|
85
43
|
kind: 'response';
|
|
86
44
|
response: Response;
|
|
87
45
|
};
|
|
88
|
-
/**
|
|
89
|
-
* Drive up to `R2_REQUEST_MAX_ATTEMPTS` attempts of a single storage operation.
|
|
90
|
-
* `runAttempt` builds a FRESH signed request each call and returns either a
|
|
91
|
-
* success value or the raw `Response` for the driver to classify. Thrown errors
|
|
92
|
-
* are classified: only recognized transient transport failures are retried.
|
|
93
|
-
*/
|
|
94
46
|
export declare function fetchWithR2Retry<T>(params: {
|
|
95
47
|
operation: R2RetryableOperation;
|
|
96
48
|
bucket: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"r2-retry.d.ts","sourceRoot":"","sources":["../../src/server/r2-retry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"r2-retry.d.ts","sourceRoot":"","sources":["../../src/server/r2-retry.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAC7C,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAA2C,CAAC;AAElG,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,YAAY,CAAC;AAEpF,MAAM,MAAM,WAAW,GAAG,oBAAoB,GAAG,QAAQ,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,EAAE,MAAM,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,wBAAgB,mBAAmB,IAAI,YAAY,CAMlD;AA8CD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAmB/D;AAED,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAef;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;CACrB,GAAG,MAAM,CAYT;AAED,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEnB,MAAM,EAAE;QAClB,SAAS,EAAE,WAAW,CAAC;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CAkBF;AAMD,MAAM,MAAM,eAAe,CAAC,CAAC,IAC3B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAqB3E,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE;IAChD,SAAS,EAAE,oBAAoB,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,YAAY,CAAC;IACpB,UAAU,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;CACpE,GAAG,OAAO,CAAC,CAAC,CAAC,CA+Bb"}
|
package/dist/server/r2-retry.js
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
|
-
// Bounded transient-retry policy for the R2 object store.
|
|
2
|
-
//
|
|
3
|
-
// PRIVATE to the server folder. The server entry (`server/index.ts`) forwards
|
|
4
|
-
// only the `R2StorageError` type, so callers can recognize an exhausted storage
|
|
5
|
-
// operation; the retry helper itself is never public.
|
|
6
|
-
//
|
|
7
|
-
// The policy applies to the idempotent-against-a-deterministic-key requests
|
|
8
|
-
// only (putFile, copy, readToFile, and deleteCopy's read of the destination).
|
|
9
|
-
// Every DELETE request is intentionally one-shot and never routes through here.
|
|
10
|
-
// Signing/config failures, local filesystem errors, and deterministic HTTP 4xx
|
|
11
|
-
// responses are never retried.
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Fixed internal policy — never exposed as tunables on the public factory.
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
/** TOTAL attempts (initial + retries) — three totals means at most TWO delays. */
|
|
16
1
|
export const R2_REQUEST_MAX_ATTEMPTS = 3;
|
|
17
|
-
/** Base delay for full-jitter exponential backoff. */
|
|
18
2
|
export const R2_RETRY_BASE_DELAY_MS = 250;
|
|
19
|
-
/** Ceiling on the exponential backoff (before jitter). */
|
|
20
3
|
export const R2_RETRY_MAX_BACKOFF_MS = 5_000;
|
|
21
|
-
/** Hard cap on an honored server `Retry-After` so one response can't eat the
|
|
22
|
-
* caller's whole timeout budget. */
|
|
23
4
|
export const R2_RETRY_AFTER_CAP_MS = 30_000;
|
|
24
|
-
/** HTTP statuses treated as transient and safe to retry. */
|
|
25
5
|
export const R2_RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
|
|
26
|
-
/** Production seam defaults. */
|
|
27
6
|
export function defaultR2RetrySeams() {
|
|
28
7
|
return {
|
|
29
8
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
@@ -31,11 +10,7 @@ export function defaultR2RetrySeams() {
|
|
|
31
10
|
now: Date.now,
|
|
32
11
|
};
|
|
33
12
|
}
|
|
34
|
-
//
|
|
35
|
-
// Transient-error classification
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// Recognized transient transport failure codes (undici/fetch surface these on
|
|
38
|
-
// the error or its `.cause`). A broken connection to R2 lands here.
|
|
13
|
+
// undici/fetch surface these codes on the error or its `.cause`.
|
|
39
14
|
const RETRYABLE_NETWORK_ERROR_CODES = new Set([
|
|
40
15
|
'ECONNRESET',
|
|
41
16
|
'ECONNREFUSED',
|
|
@@ -53,9 +28,6 @@ const RETRYABLE_NETWORK_ERROR_CODES = new Set([
|
|
|
53
28
|
'UND_ERR_BODY_TIMEOUT',
|
|
54
29
|
'UND_ERR_SOCKET',
|
|
55
30
|
]);
|
|
56
|
-
// Deterministic local filesystem errors are NEVER retried (disk full, perms,
|
|
57
|
-
// missing dir, etc.). This guard wins over any transport classification so a
|
|
58
|
-
// local write failure during readToFile piping can't be mistaken for transient.
|
|
59
31
|
const LOCAL_FS_ERROR_CODES = new Set([
|
|
60
32
|
'ENOSPC',
|
|
61
33
|
'EACCES',
|
|
@@ -80,71 +52,45 @@ function codeOf(err) {
|
|
|
80
52
|
}
|
|
81
53
|
return undefined;
|
|
82
54
|
}
|
|
83
|
-
/**
|
|
84
|
-
* True only for recognized transient transport failures. An explicit
|
|
85
|
-
* `AbortError` and deterministic local-filesystem errors always return false —
|
|
86
|
-
* an abort must never become another attempt.
|
|
87
|
-
*/
|
|
88
55
|
export function isRetryableTransportError(err) {
|
|
89
56
|
if (!err || typeof err !== 'object')
|
|
90
57
|
return false;
|
|
91
58
|
const e = err;
|
|
92
|
-
// NEVER convert an explicit abort into another attempt.
|
|
93
59
|
if (e.name === 'AbortError')
|
|
94
60
|
return false;
|
|
95
61
|
const selfCode = codeOf(err);
|
|
96
62
|
const causeCode = codeOf(e.cause);
|
|
97
|
-
// Deterministic local filesystem errors are not transient — decided first so a
|
|
98
|
-
// local write error can never be reclassified as a transport failure.
|
|
99
63
|
if (selfCode && LOCAL_FS_ERROR_CODES.has(selfCode))
|
|
100
64
|
return false;
|
|
101
65
|
if (causeCode && LOCAL_FS_ERROR_CODES.has(causeCode))
|
|
102
66
|
return false;
|
|
103
|
-
// Recognized transient network codes on the error or its cause.
|
|
104
67
|
if (selfCode && RETRYABLE_NETWORK_ERROR_CODES.has(selfCode))
|
|
105
68
|
return true;
|
|
106
69
|
if (causeCode && RETRYABLE_NETWORK_ERROR_CODES.has(causeCode))
|
|
107
70
|
return true;
|
|
108
|
-
// fetch()/undici surface transport failures as `TypeError: fetch failed
|
|
109
|
-
// (often with an unrecognized or absent cause) — treat those as transient.
|
|
71
|
+
// fetch()/undici surface transport failures as `TypeError: fetch failed`, often with no recognizable cause.
|
|
110
72
|
if (err instanceof TypeError || e.name === 'TypeError')
|
|
111
73
|
return true;
|
|
112
74
|
return false;
|
|
113
75
|
}
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
// Retry-After parsing + delay computation
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
/**
|
|
118
|
-
* Parse a `Retry-After` header value into a non-negative millisecond delay.
|
|
119
|
-
* Supports both the delta-seconds form (`"120"`) and the HTTP-date form
|
|
120
|
-
* (`"Wed, 21 Oct 2015 07:28:00 GMT"`). Returns null when absent/unparseable.
|
|
121
|
-
* An HTTP-date in the past clamps to 0.
|
|
122
|
-
*/
|
|
123
76
|
export function parseRetryAfterMs(headerValue, nowMs) {
|
|
124
77
|
if (headerValue == null)
|
|
125
78
|
return null;
|
|
126
79
|
const trimmed = headerValue.trim();
|
|
127
80
|
if (trimmed === '')
|
|
128
81
|
return null;
|
|
129
|
-
// delta-seconds form
|
|
130
82
|
if (/^\d+$/.test(trimmed)) {
|
|
131
83
|
const seconds = Number(trimmed);
|
|
132
84
|
if (!Number.isFinite(seconds) || seconds < 0)
|
|
133
85
|
return null;
|
|
134
86
|
return seconds * 1000;
|
|
135
87
|
}
|
|
136
|
-
// HTTP-date form
|
|
137
88
|
const dateMs = Date.parse(trimmed);
|
|
138
89
|
if (Number.isNaN(dateMs))
|
|
139
90
|
return null;
|
|
140
91
|
const delta = dateMs - nowMs;
|
|
141
92
|
return delta > 0 ? delta : 0;
|
|
142
93
|
}
|
|
143
|
-
/**
|
|
144
|
-
* Compute the pre-next-attempt delay. If the failed response carries a valid
|
|
145
|
-
* `Retry-After`, honor it (capped, no backoff added on top). Otherwise use
|
|
146
|
-
* full-jitter exponential backoff: `random(0, min(maxBackoff, base * 2^retryIndex))`.
|
|
147
|
-
*/
|
|
148
94
|
export function computeR2DelayMs(params) {
|
|
149
95
|
const { response, retryIndex, seams } = params;
|
|
150
96
|
if (response) {
|
|
@@ -156,28 +102,12 @@ export function computeR2DelayMs(params) {
|
|
|
156
102
|
const ceiling = Math.min(R2_RETRY_MAX_BACKOFF_MS, R2_RETRY_BASE_DELAY_MS * 2 ** retryIndex);
|
|
157
103
|
return Math.floor(seams.random() * ceiling);
|
|
158
104
|
}
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
// Exhaustion error
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
/**
|
|
163
|
-
* Thrown when an R2 operation fails: a retried operation exhausts its attempts or
|
|
164
|
-
* hits a non-retryable response, or a one-shot delete fails. Carries
|
|
165
|
-
* operation/bucket/key/attempt/status context, the S3 error code parsed from the
|
|
166
|
-
* response body, and the bounded body itself as properties — never credentials,
|
|
167
|
-
* authorization headers, or full signed URLs. Callers classify it from those
|
|
168
|
-
* properties, never from the message.
|
|
169
|
-
*
|
|
170
|
-
* The message names only the operation, the attempts, the status, and the S3 error
|
|
171
|
-
* code. The bucket, the key, and the raw body — which can echo either — stay out of
|
|
172
|
-
* it, so an escaping error never carries an object key to monitoring.
|
|
173
|
-
*/
|
|
174
105
|
export class R2StorageError extends Error {
|
|
175
106
|
operation;
|
|
176
107
|
bucket;
|
|
177
108
|
key;
|
|
178
109
|
attempts;
|
|
179
110
|
status;
|
|
180
|
-
/** The S3 `<Code>` of the response body (`NoSuchKey`, `PreconditionFailed`, …), when it had one. */
|
|
181
111
|
s3ErrorCode;
|
|
182
112
|
responseText;
|
|
183
113
|
constructor(params) {
|
|
@@ -196,13 +126,10 @@ export class R2StorageError extends Error {
|
|
|
196
126
|
this.responseText = params.responseText;
|
|
197
127
|
}
|
|
198
128
|
}
|
|
199
|
-
/** The `<Code>` of an S3 XML error body; only a plain alphanumeric token is taken. */
|
|
200
129
|
function parseS3ErrorCode(responseText) {
|
|
201
130
|
return responseText ? /<Code>([A-Za-z0-9]{1,64})<\/Code>/.exec(responseText)?.[1] : undefined;
|
|
202
131
|
}
|
|
203
|
-
//
|
|
204
|
-
// code-point filter rather than a control-character regex (which `no-control-regex`
|
|
205
|
-
// forbids and which would embed literal control bytes in source).
|
|
132
|
+
// A code-point filter, not a control-character regex, which `no-control-regex` forbids.
|
|
206
133
|
function sanitizeBodyText(text) {
|
|
207
134
|
let out = '';
|
|
208
135
|
for (const ch of text.slice(0, 1000)) {
|
|
@@ -211,9 +138,7 @@ function sanitizeBodyText(text) {
|
|
|
211
138
|
}
|
|
212
139
|
return out.trim().slice(0, 500);
|
|
213
140
|
}
|
|
214
|
-
//
|
|
215
|
-
// the underlying socket can be reused before the next attempt. R2 error bodies
|
|
216
|
-
// are short XML and never contain our credentials; sanitize defensively regardless.
|
|
141
|
+
// Draining the body lets the socket be reused before the next attempt.
|
|
217
142
|
async function safeReadBoundedBody(res) {
|
|
218
143
|
try {
|
|
219
144
|
return sanitizeBodyText(await res.text());
|
|
@@ -222,12 +147,6 @@ async function safeReadBoundedBody(res) {
|
|
|
222
147
|
return '<no body>';
|
|
223
148
|
}
|
|
224
149
|
}
|
|
225
|
-
/**
|
|
226
|
-
* Drive up to `R2_REQUEST_MAX_ATTEMPTS` attempts of a single storage operation.
|
|
227
|
-
* `runAttempt` builds a FRESH signed request each call and returns either a
|
|
228
|
-
* success value or the raw `Response` for the driver to classify. Thrown errors
|
|
229
|
-
* are classified: only recognized transient transport failures are retried.
|
|
230
|
-
*/
|
|
231
150
|
export async function fetchWithR2Retry(params) {
|
|
232
151
|
const { operation, bucket, key, seams, runAttempt } = params;
|
|
233
152
|
for (let attempt = 1; attempt <= R2_REQUEST_MAX_ATTEMPTS; attempt++) {
|
|
@@ -236,8 +155,6 @@ export async function fetchWithR2Retry(params) {
|
|
|
236
155
|
result = await runAttempt(attempt);
|
|
237
156
|
}
|
|
238
157
|
catch (err) {
|
|
239
|
-
// Signing failures, explicit aborts, and deterministic local-fs errors are
|
|
240
|
-
// never retried; only recognized transient transport failures are.
|
|
241
158
|
if (!isRetryableTransportError(err))
|
|
242
159
|
throw err;
|
|
243
160
|
if (attempt >= R2_REQUEST_MAX_ATTEMPTS) {
|
|
@@ -250,16 +167,13 @@ export async function fetchWithR2Retry(params) {
|
|
|
250
167
|
return result.value;
|
|
251
168
|
const response = result.response;
|
|
252
169
|
const status = response.status;
|
|
253
|
-
// Consume the failed body before the next attempt.
|
|
254
170
|
const responseText = await safeReadBoundedBody(response);
|
|
255
171
|
if (R2_RETRYABLE_STATUSES.has(status) && attempt < R2_REQUEST_MAX_ATTEMPTS) {
|
|
256
172
|
await seams.sleep(computeR2DelayMs({ response, retryIndex: attempt - 1, seams }));
|
|
257
173
|
continue;
|
|
258
174
|
}
|
|
259
|
-
// Non-retryable status, or a retryable status with attempts exhausted.
|
|
260
175
|
throw new R2StorageError({ operation, bucket, key, attempts: attempt, status, responseText });
|
|
261
176
|
}
|
|
262
|
-
// Unreachable: the loop always returns or throws.
|
|
263
177
|
throw new R2StorageError({ operation, bucket, key, attempts: R2_REQUEST_MAX_ATTEMPTS });
|
|
264
178
|
}
|
|
265
179
|
//# sourceMappingURL=r2-retry.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"r2-retry.js","sourceRoot":"","sources":["../../src/server/r2-retry.ts"],"names":[],"mappings":"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","sourcesContent":["// Bounded transient-retry policy for the R2 object store.\n//\n// PRIVATE to the server folder. The server entry (`server/index.ts`) forwards\n// only the `R2StorageError` type, so callers can recognize an exhausted storage\n// operation; the retry helper itself is never public.\n//\n// The policy applies to the idempotent-against-a-deterministic-key requests\n// only (putFile, copy, readToFile, and deleteCopy's read of the destination).\n// Every DELETE request is intentionally one-shot and never routes through here.\n// Signing/config failures, local filesystem errors, and deterministic HTTP 4xx\n// responses are never retried.\n\n// ---------------------------------------------------------------------------\n// Fixed internal policy — never exposed as tunables on the public factory.\n// ---------------------------------------------------------------------------\n\n/** TOTAL attempts (initial + retries) — three totals means at most TWO delays. */\nexport const R2_REQUEST_MAX_ATTEMPTS = 3;\n/** Base delay for full-jitter exponential backoff. */\nexport const R2_RETRY_BASE_DELAY_MS = 250;\n/** Ceiling on the exponential backoff (before jitter). */\nexport const R2_RETRY_MAX_BACKOFF_MS = 5_000;\n/** Hard cap on an honored server `Retry-After` so one response can't eat the\n * caller's whole timeout budget. */\nexport const R2_RETRY_AFTER_CAP_MS = 30_000;\n\n/** HTTP statuses treated as transient and safe to retry. */\nexport const R2_RETRYABLE_STATUSES: ReadonlySet<number> = new Set([408, 429, 500, 502, 503, 504]);\n\nexport type R2RetryableOperation = 'putFile' | 'copy' | 'readToFile' | 'deleteCopy';\n\n/** Every R2 store operation an `R2StorageError` can name; `delete` never retries. */\nexport type R2Operation = R2RetryableOperation | 'delete';\n\n/** Deterministic seams for tests; production defaults are internal (see below). */\nexport interface R2RetrySeams {\n /** Resolve after `ms` milliseconds. */\n sleep: (ms: number) => Promise<void>;\n /** Uniform random in [0, 1). */\n random: () => number;\n /** Epoch-millisecond clock — keeps HTTP-date `Retry-After` tests off wall-clock. */\n now: () => number;\n}\n\n/** Production seam defaults. */\nexport function defaultR2RetrySeams(): R2RetrySeams {\n return {\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n random: Math.random,\n now: Date.now,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Transient-error classification\n// ---------------------------------------------------------------------------\n\n// Recognized transient transport failure codes (undici/fetch surface these on\n// the error or its `.cause`). A broken connection to R2 lands here.\nconst RETRYABLE_NETWORK_ERROR_CODES: ReadonlySet<string> = new Set([\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ECONNABORTED',\n 'ETIMEDOUT',\n 'EPIPE',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n 'ENETUNREACH',\n 'ENETDOWN',\n 'EHOSTUNREACH',\n 'EHOSTDOWN',\n 'UND_ERR_CONNECT_TIMEOUT',\n 'UND_ERR_HEADERS_TIMEOUT',\n 'UND_ERR_BODY_TIMEOUT',\n 'UND_ERR_SOCKET',\n]);\n\n// Deterministic local filesystem errors are NEVER retried (disk full, perms,\n// missing dir, etc.). This guard wins over any transport classification so a\n// local write failure during readToFile piping can't be mistaken for transient.\nconst LOCAL_FS_ERROR_CODES: ReadonlySet<string> = new Set([\n 'ENOSPC',\n 'EACCES',\n 'EISDIR',\n 'ENOENT',\n 'EROFS',\n 'EMFILE',\n 'ENFILE',\n 'EBADF',\n 'EEXIST',\n 'EPERM',\n 'ENAMETOOLONG',\n 'ELOOP',\n 'ENOTDIR',\n 'EDQUOT',\n]);\n\nfunction codeOf(err: unknown): string | undefined {\n if (err && typeof err === 'object') {\n const c = (err as { code?: unknown }).code;\n if (typeof c === 'string') return c;\n }\n return undefined;\n}\n\n/**\n * True only for recognized transient transport failures. An explicit\n * `AbortError` and deterministic local-filesystem errors always return false —\n * an abort must never become another attempt.\n */\nexport function isRetryableTransportError(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false;\n const e = err as { name?: unknown; cause?: unknown };\n\n // NEVER convert an explicit abort into another attempt.\n if (e.name === 'AbortError') return false;\n\n const selfCode = codeOf(err);\n const causeCode = codeOf(e.cause);\n\n // Deterministic local filesystem errors are not transient — decided first so a\n // local write error can never be reclassified as a transport failure.\n if (selfCode && LOCAL_FS_ERROR_CODES.has(selfCode)) return false;\n if (causeCode && LOCAL_FS_ERROR_CODES.has(causeCode)) return false;\n\n // Recognized transient network codes on the error or its cause.\n if (selfCode && RETRYABLE_NETWORK_ERROR_CODES.has(selfCode)) return true;\n if (causeCode && RETRYABLE_NETWORK_ERROR_CODES.has(causeCode)) return true;\n\n // fetch()/undici surface transport failures as `TypeError: fetch failed`\n // (often with an unrecognized or absent cause) — treat those as transient.\n if (err instanceof TypeError || e.name === 'TypeError') return true;\n\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Retry-After parsing + delay computation\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a `Retry-After` header value into a non-negative millisecond delay.\n * Supports both the delta-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2015 07:28:00 GMT\"`). Returns null when absent/unparseable.\n * An HTTP-date in the past clamps to 0.\n */\nexport function parseRetryAfterMs(\n headerValue: string | null | undefined,\n nowMs: number,\n): number | null {\n if (headerValue == null) return null;\n const trimmed = headerValue.trim();\n if (trimmed === '') return null;\n\n // delta-seconds form\n if (/^\\d+$/.test(trimmed)) {\n const seconds = Number(trimmed);\n if (!Number.isFinite(seconds) || seconds < 0) return null;\n return seconds * 1000;\n }\n\n // HTTP-date form\n const dateMs = Date.parse(trimmed);\n if (Number.isNaN(dateMs)) return null;\n const delta = dateMs - nowMs;\n return delta > 0 ? delta : 0;\n}\n\n/**\n * Compute the pre-next-attempt delay. If the failed response carries a valid\n * `Retry-After`, honor it (capped, no backoff added on top). Otherwise use\n * full-jitter exponential backoff: `random(0, min(maxBackoff, base * 2^retryIndex))`.\n */\nexport function computeR2DelayMs(params: {\n response: Response | null;\n /** 0-based index of the retry about to happen (0 for the first retry). */\n retryIndex: number;\n seams: R2RetrySeams;\n}): number {\n const { response, retryIndex, seams } = params;\n\n if (response) {\n const retryAfterMs = parseRetryAfterMs(response.headers.get('retry-after'), seams.now());\n if (retryAfterMs !== null) {\n return Math.min(retryAfterMs, R2_RETRY_AFTER_CAP_MS);\n }\n }\n\n const ceiling = Math.min(R2_RETRY_MAX_BACKOFF_MS, R2_RETRY_BASE_DELAY_MS * 2 ** retryIndex);\n return Math.floor(seams.random() * ceiling);\n}\n\n// ---------------------------------------------------------------------------\n// Exhaustion error\n// ---------------------------------------------------------------------------\n\n/**\n * Thrown when an R2 operation fails: a retried operation exhausts its attempts or\n * hits a non-retryable response, or a one-shot delete fails. Carries\n * operation/bucket/key/attempt/status context, the S3 error code parsed from the\n * response body, and the bounded body itself as properties — never credentials,\n * authorization headers, or full signed URLs. Callers classify it from those\n * properties, never from the message.\n *\n * The message names only the operation, the attempts, the status, and the S3 error\n * code. The bucket, the key, and the raw body — which can echo either — stay out of\n * it, so an escaping error never carries an object key to monitoring.\n */\nexport class R2StorageError extends Error {\n readonly operation: R2Operation;\n readonly bucket: string;\n readonly key: string;\n readonly attempts: number;\n readonly status?: number;\n /** The S3 `<Code>` of the response body (`NoSuchKey`, `PreconditionFailed`, …), when it had one. */\n readonly s3ErrorCode?: string;\n readonly responseText?: string;\n\n constructor(params: {\n operation: R2Operation;\n bucket: string;\n key: string;\n attempts: number;\n status?: number;\n responseText?: string;\n cause?: unknown;\n }) {\n const code = parseS3ErrorCode(params.responseText);\n const detail = [params.status !== undefined ? `status ${params.status}` : undefined, code]\n .filter(Boolean)\n .join(' ');\n super(\n `R2 ${params.operation} failed after ${params.attempts} attempt(s)${detail ? `: ${detail}` : ''}`,\n params.cause !== undefined ? { cause: params.cause } : undefined,\n );\n this.name = 'R2StorageError';\n this.operation = params.operation;\n this.bucket = params.bucket;\n this.key = params.key;\n this.attempts = params.attempts;\n this.status = params.status;\n this.s3ErrorCode = code;\n this.responseText = params.responseText;\n }\n}\n\n/** The `<Code>` of an S3 XML error body; only a plain alphanumeric token is taken. */\nfunction parseS3ErrorCode(responseText: string | undefined): string | undefined {\n return responseText ? /<Code>([A-Za-z0-9]{1,64})<\\/Code>/.exec(responseText)?.[1] : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Retry driver\n// ---------------------------------------------------------------------------\n\nexport type R2AttemptResult<T> =\n { kind: 'success'; value: T } | { kind: 'response'; response: Response };\n\n// Strip ASCII control characters (0x00-0x1F, 0x7F) and bound length. Done with a\n// code-point filter rather than a control-character regex (which `no-control-regex`\n// forbids and which would embed literal control bytes in source).\nfunction sanitizeBodyText(text: string): string {\n let out = '';\n for (const ch of text.slice(0, 1000)) {\n const code = ch.codePointAt(0) ?? 0;\n out += code < 0x20 || code === 0x7f ? ' ' : ch;\n }\n return out.trim().slice(0, 500);\n}\n\n// Bounded, sanitized read of a failed response body. Also drains the body so\n// the underlying socket can be reused before the next attempt. R2 error bodies\n// are short XML and never contain our credentials; sanitize defensively regardless.\nasync function safeReadBoundedBody(res: Response): Promise<string> {\n try {\n return sanitizeBodyText(await res.text());\n } catch {\n return '<no body>';\n }\n}\n\n/**\n * Drive up to `R2_REQUEST_MAX_ATTEMPTS` attempts of a single storage operation.\n * `runAttempt` builds a FRESH signed request each call and returns either a\n * success value or the raw `Response` for the driver to classify. Thrown errors\n * are classified: only recognized transient transport failures are retried.\n */\nexport async function fetchWithR2Retry<T>(params: {\n operation: R2RetryableOperation;\n bucket: string;\n key: string;\n seams: R2RetrySeams;\n runAttempt: (attemptNumber: number) => Promise<R2AttemptResult<T>>;\n}): Promise<T> {\n const { operation, bucket, key, seams, runAttempt } = params;\n\n for (let attempt = 1; attempt <= R2_REQUEST_MAX_ATTEMPTS; attempt++) {\n let result: R2AttemptResult<T>;\n try {\n result = await runAttempt(attempt);\n } catch (err) {\n // Signing failures, explicit aborts, and deterministic local-fs errors are\n // never retried; only recognized transient transport failures are.\n if (!isRetryableTransportError(err)) throw err;\n if (attempt >= R2_REQUEST_MAX_ATTEMPTS) {\n throw new R2StorageError({ operation, bucket, key, attempts: attempt, cause: err });\n }\n await seams.sleep(computeR2DelayMs({ response: null, retryIndex: attempt - 1, seams }));\n continue;\n }\n\n if (result.kind === 'success') return result.value;\n\n const response = result.response;\n const status = response.status;\n // Consume the failed body before the next attempt.\n const responseText = await safeReadBoundedBody(response);\n\n if (R2_RETRYABLE_STATUSES.has(status) && attempt < R2_REQUEST_MAX_ATTEMPTS) {\n await seams.sleep(computeR2DelayMs({ response, retryIndex: attempt - 1, seams }));\n continue;\n }\n\n // Non-retryable status, or a retryable status with attempts exhausted.\n throw new R2StorageError({ operation, bucket, key, attempts: attempt, status, responseText });\n }\n\n // Unreachable: the loop always returns or throws.\n throw new R2StorageError({ operation, bucket, key, attempts: R2_REQUEST_MAX_ATTEMPTS });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"r2-retry.js","sourceRoot":"","sources":["../../src/server/r2-retry.ts"],"names":[],"mappings":"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","sourcesContent":["export const R2_REQUEST_MAX_ATTEMPTS = 3;\nexport const R2_RETRY_BASE_DELAY_MS = 250;\nexport const R2_RETRY_MAX_BACKOFF_MS = 5_000;\nexport const R2_RETRY_AFTER_CAP_MS = 30_000;\n\nexport const R2_RETRYABLE_STATUSES: ReadonlySet<number> = new Set([408, 429, 500, 502, 503, 504]);\n\nexport type R2RetryableOperation = 'putFile' | 'copy' | 'readToFile' | 'deleteCopy';\n\nexport type R2Operation = R2RetryableOperation | 'delete';\n\nexport interface R2RetrySeams {\n sleep: (ms: number) => Promise<void>;\n random: () => number;\n now: () => number;\n}\n\nexport function defaultR2RetrySeams(): R2RetrySeams {\n return {\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n random: Math.random,\n now: Date.now,\n };\n}\n\n// undici/fetch surface these codes on the error or its `.cause`.\nconst RETRYABLE_NETWORK_ERROR_CODES: ReadonlySet<string> = new Set([\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ECONNABORTED',\n 'ETIMEDOUT',\n 'EPIPE',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n 'ENETUNREACH',\n 'ENETDOWN',\n 'EHOSTUNREACH',\n 'EHOSTDOWN',\n 'UND_ERR_CONNECT_TIMEOUT',\n 'UND_ERR_HEADERS_TIMEOUT',\n 'UND_ERR_BODY_TIMEOUT',\n 'UND_ERR_SOCKET',\n]);\n\nconst LOCAL_FS_ERROR_CODES: ReadonlySet<string> = new Set([\n 'ENOSPC',\n 'EACCES',\n 'EISDIR',\n 'ENOENT',\n 'EROFS',\n 'EMFILE',\n 'ENFILE',\n 'EBADF',\n 'EEXIST',\n 'EPERM',\n 'ENAMETOOLONG',\n 'ELOOP',\n 'ENOTDIR',\n 'EDQUOT',\n]);\n\nfunction codeOf(err: unknown): string | undefined {\n if (err && typeof err === 'object') {\n const c = (err as { code?: unknown }).code;\n if (typeof c === 'string') return c;\n }\n return undefined;\n}\n\nexport function isRetryableTransportError(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false;\n const e = err as { name?: unknown; cause?: unknown };\n\n if (e.name === 'AbortError') return false;\n\n const selfCode = codeOf(err);\n const causeCode = codeOf(e.cause);\n\n if (selfCode && LOCAL_FS_ERROR_CODES.has(selfCode)) return false;\n if (causeCode && LOCAL_FS_ERROR_CODES.has(causeCode)) return false;\n\n if (selfCode && RETRYABLE_NETWORK_ERROR_CODES.has(selfCode)) return true;\n if (causeCode && RETRYABLE_NETWORK_ERROR_CODES.has(causeCode)) return true;\n\n // fetch()/undici surface transport failures as `TypeError: fetch failed`, often with no recognizable cause.\n if (err instanceof TypeError || e.name === 'TypeError') return true;\n\n return false;\n}\n\nexport function parseRetryAfterMs(\n headerValue: string | null | undefined,\n nowMs: number,\n): number | null {\n if (headerValue == null) return null;\n const trimmed = headerValue.trim();\n if (trimmed === '') return null;\n\n if (/^\\d+$/.test(trimmed)) {\n const seconds = Number(trimmed);\n if (!Number.isFinite(seconds) || seconds < 0) return null;\n return seconds * 1000;\n }\n\n const dateMs = Date.parse(trimmed);\n if (Number.isNaN(dateMs)) return null;\n const delta = dateMs - nowMs;\n return delta > 0 ? delta : 0;\n}\n\nexport function computeR2DelayMs(params: {\n response: Response | null;\n retryIndex: number;\n seams: R2RetrySeams;\n}): number {\n const { response, retryIndex, seams } = params;\n\n if (response) {\n const retryAfterMs = parseRetryAfterMs(response.headers.get('retry-after'), seams.now());\n if (retryAfterMs !== null) {\n return Math.min(retryAfterMs, R2_RETRY_AFTER_CAP_MS);\n }\n }\n\n const ceiling = Math.min(R2_RETRY_MAX_BACKOFF_MS, R2_RETRY_BASE_DELAY_MS * 2 ** retryIndex);\n return Math.floor(seams.random() * ceiling);\n}\n\nexport class R2StorageError extends Error {\n readonly operation: R2Operation;\n readonly bucket: string;\n readonly key: string;\n readonly attempts: number;\n readonly status?: number;\n readonly s3ErrorCode?: string;\n readonly responseText?: string;\n\n constructor(params: {\n operation: R2Operation;\n bucket: string;\n key: string;\n attempts: number;\n status?: number;\n responseText?: string;\n cause?: unknown;\n }) {\n const code = parseS3ErrorCode(params.responseText);\n const detail = [params.status !== undefined ? `status ${params.status}` : undefined, code]\n .filter(Boolean)\n .join(' ');\n super(\n `R2 ${params.operation} failed after ${params.attempts} attempt(s)${detail ? `: ${detail}` : ''}`,\n params.cause !== undefined ? { cause: params.cause } : undefined,\n );\n this.name = 'R2StorageError';\n this.operation = params.operation;\n this.bucket = params.bucket;\n this.key = params.key;\n this.attempts = params.attempts;\n this.status = params.status;\n this.s3ErrorCode = code;\n this.responseText = params.responseText;\n }\n}\n\nfunction parseS3ErrorCode(responseText: string | undefined): string | undefined {\n return responseText ? /<Code>([A-Za-z0-9]{1,64})<\\/Code>/.exec(responseText)?.[1] : undefined;\n}\n\nexport type R2AttemptResult<T> =\n { kind: 'success'; value: T } | { kind: 'response'; response: Response };\n\n// A code-point filter, not a control-character regex, which `no-control-regex` forbids.\nfunction sanitizeBodyText(text: string): string {\n let out = '';\n for (const ch of text.slice(0, 1000)) {\n const code = ch.codePointAt(0) ?? 0;\n out += code < 0x20 || code === 0x7f ? ' ' : ch;\n }\n return out.trim().slice(0, 500);\n}\n\n// Draining the body lets the socket be reused before the next attempt.\nasync function safeReadBoundedBody(res: Response): Promise<string> {\n try {\n return sanitizeBodyText(await res.text());\n } catch {\n return '<no body>';\n }\n}\n\nexport async function fetchWithR2Retry<T>(params: {\n operation: R2RetryableOperation;\n bucket: string;\n key: string;\n seams: R2RetrySeams;\n runAttempt: (attemptNumber: number) => Promise<R2AttemptResult<T>>;\n}): Promise<T> {\n const { operation, bucket, key, seams, runAttempt } = params;\n\n for (let attempt = 1; attempt <= R2_REQUEST_MAX_ATTEMPTS; attempt++) {\n let result: R2AttemptResult<T>;\n try {\n result = await runAttempt(attempt);\n } catch (err) {\n if (!isRetryableTransportError(err)) throw err;\n if (attempt >= R2_REQUEST_MAX_ATTEMPTS) {\n throw new R2StorageError({ operation, bucket, key, attempts: attempt, cause: err });\n }\n await seams.sleep(computeR2DelayMs({ response: null, retryIndex: attempt - 1, seams }));\n continue;\n }\n\n if (result.kind === 'success') return result.value;\n\n const response = result.response;\n const status = response.status;\n const responseText = await safeReadBoundedBody(response);\n\n if (R2_RETRYABLE_STATUSES.has(status) && attempt < R2_REQUEST_MAX_ATTEMPTS) {\n await seams.sleep(computeR2DelayMs({ response, retryIndex: attempt - 1, seams }));\n continue;\n }\n\n throw new R2StorageError({ operation, bucket, key, attempts: attempt, status, responseText });\n }\n\n throw new R2StorageError({ operation, bucket, key, attempts: R2_REQUEST_MAX_ATTEMPTS });\n}\n"]}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Bucket } from './storage-ops.js';
|
|
2
|
-
/** The stored-object metadata fields these reads use (the Admin SDK returns more). */
|
|
3
2
|
export interface StagedObjectMetadata {
|
|
4
3
|
contentType?: string;
|
|
5
4
|
size?: string | number;
|
|
@@ -12,43 +11,17 @@ export type StagedUploadMetadataRead = {
|
|
|
12
11
|
} | {
|
|
13
12
|
found: false;
|
|
14
13
|
};
|
|
15
|
-
/**
|
|
16
|
-
* Read a staged object's stored metadata (`contentType`, `size`, `generation`, …) for
|
|
17
|
-
* `verifyStagedUploadMetadata`. A missing object is `{ found: false }`; any other read
|
|
18
|
-
* failure throws.
|
|
19
|
-
*/
|
|
20
14
|
export declare function readStagedUploadMetadata(bucket: Bucket, storagePath: string): Promise<StagedUploadMetadataRead>;
|
|
21
|
-
/**
|
|
22
|
-
* The staged object's generation from metadata the caller already fetched, as a string
|
|
23
|
-
* (GCS generations exceed 2^53) — the one normalization `readStagedObjectGeneration`
|
|
24
|
-
* applies, for a processor that already holds the object's metadata and must not read
|
|
25
|
-
* it again. `undefined` when the metadata carries no generation (the emulator may not):
|
|
26
|
-
* absent, `null`, or an empty string, which names no generation.
|
|
27
|
-
*/
|
|
28
15
|
export declare function stagedObjectGenerationFromMetadata(metadata: {
|
|
29
16
|
generation?: string | number | null;
|
|
30
17
|
}): string | undefined;
|
|
31
|
-
/** The stored-object metadata field an `UnreadableStorageObjectMetadataError` names. */
|
|
32
18
|
export type StorageObjectMetadataField = 'generation' | 'size' | 'etag' | 'lastModified';
|
|
33
|
-
/**
|
|
34
|
-
* A storage object whose metadata lacks a field its backend always reports: a Cloud Storage
|
|
35
|
-
* generation or byte size (GCS, the Storage emulator, and a Storage finalize event carry
|
|
36
|
-
* both), or an R2 object's ETag, last-modified time, or size. This is an anomaly to surface,
|
|
37
|
-
* never to paper over with a placeholder. The bucket and key are PROPERTIES, never message
|
|
38
|
-
* text: an escaping error reaches monitoring, and an object key must not.
|
|
39
|
-
*/
|
|
40
19
|
export declare class UnreadableStorageObjectMetadataError extends Error {
|
|
41
20
|
readonly bucket: string;
|
|
42
21
|
readonly key: string;
|
|
43
22
|
readonly field: StorageObjectMetadataField;
|
|
44
23
|
constructor(bucket: string, key: string, field: StorageObjectMetadataField);
|
|
45
24
|
}
|
|
46
|
-
/**
|
|
47
|
-
* The object's generation from metadata the caller already holds, for a site that cannot
|
|
48
|
-
* proceed without it (a hold key, an evidence record, a pinned read). Throws
|
|
49
|
-
* `UnreadableStorageObjectMetadataError` (field `generation`) when the metadata names none —
|
|
50
|
-
* the same normalization as `stagedObjectGenerationFromMetadata`, with no fallback value.
|
|
51
|
-
*/
|
|
52
25
|
export declare function requireStorageObjectGeneration(metadata: {
|
|
53
26
|
generation?: string | number | null;
|
|
54
27
|
}, object: {
|
|
@@ -56,25 +29,8 @@ export declare function requireStorageObjectGeneration(metadata: {
|
|
|
56
29
|
key: string;
|
|
57
30
|
}): string;
|
|
58
31
|
export interface ReadStagedObjectGenerationOptions {
|
|
59
|
-
/** Called when the generation cannot be read (the caller logs it); reads then stay unpinned. */
|
|
60
32
|
onReadFailure?: (error: unknown) => void;
|
|
61
33
|
}
|
|
62
|
-
/**
|
|
63
|
-
* The staged object's current generation, as a string (GCS generations exceed 2^53).
|
|
64
|
-
*
|
|
65
|
-
* This is the one value that pins every later read of the staged bytes — the processing
|
|
66
|
-
* download, a moderation scan, a hash — to the SAME immutable object, so "processed
|
|
67
|
-
* bytes == scanned bytes == hashed bytes" holds even if the staging path were
|
|
68
|
-
* overwritten (a pinned read resolves exactly the captured generation). Returns
|
|
69
|
-
* `undefined` when no path is given, the object reports no generation (the emulator may
|
|
70
|
-
* not), or the read fails — best effort: callers fall back to unpinned reads, and a
|
|
71
|
-
* create-only staging rule stays the primary defense.
|
|
72
|
-
*/
|
|
73
34
|
export declare function readStagedObjectGeneration(bucket: Bucket, storagePath: string | undefined, options?: ReadStagedObjectGenerationOptions): Promise<string | undefined>;
|
|
74
|
-
/**
|
|
75
|
-
* A `gs://` URI pinned to a captured generation (`gs://bucket/path#<generation>`), so a
|
|
76
|
-
* service reading by URI (a moderation scan) reads the same bytes the processor did.
|
|
77
|
-
* Without a generation it is the bare URI.
|
|
78
|
-
*/
|
|
79
35
|
export declare function pinnedGcsUri(gcsUri: string, generation: string | undefined): string;
|
|
80
36
|
//# sourceMappingURL=staged-object.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staged-object.d.ts","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"staged-object.d.ts","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,MAAM,wBAAwB,GAClC;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,oBAAoB,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAErE,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,wBAAwB,CAAC,CAQnC;AAED,wBAAgB,kCAAkC,CAAC,QAAQ,EAAE;IAC3D,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACrC,GAAG,MAAM,GAAG,SAAS,CAKrB;AAED,MAAM,MAAM,0BAA0B,GAAG,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC;AAEzF,qBAAa,oCAAqC,SAAQ,KAAK;aAE3C,MAAM,EAAE,MAAM;aACd,GAAG,EAAE,MAAM;aACX,KAAK,EAAE,0BAA0B;gBAFjC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,0BAA0B;CAKpD;AAED,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,EACjD,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACtC,MAAM,CAMR;AAED,MAAM,WAAW,iCAAiC;IAChD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C;AAED,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS7B;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEnF"}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
// Admin-SDK reads of a STAGED upload object (the client-written staging copy a
|
|
2
|
-
// processor reads from). The accept-time verdict over the metadata is the pure
|
|
3
|
-
// `verifyStagedUploadMetadata` in `@ttt-productions/media-schemas`.
|
|
4
1
|
import { isObjectNotFoundError } from './gcs-errors.js';
|
|
5
|
-
/**
|
|
6
|
-
* Read a staged object's stored metadata (`contentType`, `size`, `generation`, …) for
|
|
7
|
-
* `verifyStagedUploadMetadata`. A missing object is `{ found: false }`; any other read
|
|
8
|
-
* failure throws.
|
|
9
|
-
*/
|
|
10
2
|
export async function readStagedUploadMetadata(bucket, storagePath) {
|
|
11
3
|
try {
|
|
12
4
|
const [metadata] = await bucket.file(storagePath).getMetadata();
|
|
@@ -18,13 +10,6 @@ export async function readStagedUploadMetadata(bucket, storagePath) {
|
|
|
18
10
|
throw e;
|
|
19
11
|
}
|
|
20
12
|
}
|
|
21
|
-
/**
|
|
22
|
-
* The staged object's generation from metadata the caller already fetched, as a string
|
|
23
|
-
* (GCS generations exceed 2^53) — the one normalization `readStagedObjectGeneration`
|
|
24
|
-
* applies, for a processor that already holds the object's metadata and must not read
|
|
25
|
-
* it again. `undefined` when the metadata carries no generation (the emulator may not):
|
|
26
|
-
* absent, `null`, or an empty string, which names no generation.
|
|
27
|
-
*/
|
|
28
13
|
export function stagedObjectGenerationFromMetadata(metadata) {
|
|
29
14
|
const { generation } = metadata;
|
|
30
15
|
if (generation === undefined || generation === null)
|
|
@@ -32,13 +17,6 @@ export function stagedObjectGenerationFromMetadata(metadata) {
|
|
|
32
17
|
const value = String(generation);
|
|
33
18
|
return value === '' ? undefined : value;
|
|
34
19
|
}
|
|
35
|
-
/**
|
|
36
|
-
* A storage object whose metadata lacks a field its backend always reports: a Cloud Storage
|
|
37
|
-
* generation or byte size (GCS, the Storage emulator, and a Storage finalize event carry
|
|
38
|
-
* both), or an R2 object's ETag, last-modified time, or size. This is an anomaly to surface,
|
|
39
|
-
* never to paper over with a placeholder. The bucket and key are PROPERTIES, never message
|
|
40
|
-
* text: an escaping error reaches monitoring, and an object key must not.
|
|
41
|
-
*/
|
|
42
20
|
export class UnreadableStorageObjectMetadataError extends Error {
|
|
43
21
|
bucket;
|
|
44
22
|
key;
|
|
@@ -51,12 +29,6 @@ export class UnreadableStorageObjectMetadataError extends Error {
|
|
|
51
29
|
this.name = 'UnreadableStorageObjectMetadataError';
|
|
52
30
|
}
|
|
53
31
|
}
|
|
54
|
-
/**
|
|
55
|
-
* The object's generation from metadata the caller already holds, for a site that cannot
|
|
56
|
-
* proceed without it (a hold key, an evidence record, a pinned read). Throws
|
|
57
|
-
* `UnreadableStorageObjectMetadataError` (field `generation`) when the metadata names none —
|
|
58
|
-
* the same normalization as `stagedObjectGenerationFromMetadata`, with no fallback value.
|
|
59
|
-
*/
|
|
60
32
|
export function requireStorageObjectGeneration(metadata, object) {
|
|
61
33
|
const generation = stagedObjectGenerationFromMetadata(metadata);
|
|
62
34
|
if (generation === undefined) {
|
|
@@ -64,17 +36,6 @@ export function requireStorageObjectGeneration(metadata, object) {
|
|
|
64
36
|
}
|
|
65
37
|
return generation;
|
|
66
38
|
}
|
|
67
|
-
/**
|
|
68
|
-
* The staged object's current generation, as a string (GCS generations exceed 2^53).
|
|
69
|
-
*
|
|
70
|
-
* This is the one value that pins every later read of the staged bytes — the processing
|
|
71
|
-
* download, a moderation scan, a hash — to the SAME immutable object, so "processed
|
|
72
|
-
* bytes == scanned bytes == hashed bytes" holds even if the staging path were
|
|
73
|
-
* overwritten (a pinned read resolves exactly the captured generation). Returns
|
|
74
|
-
* `undefined` when no path is given, the object reports no generation (the emulator may
|
|
75
|
-
* not), or the read fails — best effort: callers fall back to unpinned reads, and a
|
|
76
|
-
* create-only staging rule stays the primary defense.
|
|
77
|
-
*/
|
|
78
39
|
export async function readStagedObjectGeneration(bucket, storagePath, options = {}) {
|
|
79
40
|
if (!storagePath)
|
|
80
41
|
return undefined;
|
|
@@ -87,11 +48,6 @@ export async function readStagedObjectGeneration(bucket, storagePath, options =
|
|
|
87
48
|
return undefined;
|
|
88
49
|
}
|
|
89
50
|
}
|
|
90
|
-
/**
|
|
91
|
-
* A `gs://` URI pinned to a captured generation (`gs://bucket/path#<generation>`), so a
|
|
92
|
-
* service reading by URI (a moderation scan) reads the same bytes the processor did.
|
|
93
|
-
* Without a generation it is the bare URI.
|
|
94
|
-
*/
|
|
95
51
|
export function pinnedGcsUri(gcsUri, generation) {
|
|
96
52
|
return generation ? `${gcsUri}#${generation}` : gcsUri;
|
|
97
53
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staged-object.js","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"staged-object.js","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAaxD,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,MAAc,EACd,WAAmB;IAEnB,IAAI,CAAC;QACH,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;QAChE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IACnC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,qBAAqB,CAAC,CAAC,CAAC;YAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACtD,MAAM,CAAC,CAAC;IACV,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kCAAkC,CAAC,QAElD;IACC,MAAM,EAAE,UAAU,EAAE,GAAG,QAAQ,CAAC;IAChC,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACtE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IACjC,OAAO,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AAC1C,CAAC;AAID,MAAM,OAAO,oCAAqC,SAAQ,KAAK;IAE3C;IACA;IACA;IAHlB,YACkB,MAAc,EACd,GAAW,EACX,KAAiC;QAEjD,KAAK,CAAC,2CAA2C,KAAK,EAAE,CAAC,CAAC;QAJ1C,WAAM,GAAN,MAAM,CAAQ;QACd,QAAG,GAAH,GAAG,CAAQ;QACX,UAAK,GAAL,KAAK,CAA4B;QAGjD,IAAI,CAAC,IAAI,GAAG,sCAAsC,CAAC;IACrD,CAAC;CACF;AAED,MAAM,UAAU,8BAA8B,CAC5C,QAAiD,EACjD,MAAuC;IAEvC,MAAM,UAAU,GAAG,kCAAkC,CAAC,QAAQ,CAAC,CAAC;IAChE,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,oCAAoC,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAMD,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,MAAc,EACd,WAA+B,EAC/B,UAA6C,EAAE;IAE/C,IAAI,CAAC,WAAW;QAAE,OAAO,SAAS,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;QAChE,OAAO,kCAAkC,CAAC,QAAQ,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3B,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,UAA8B;IACzE,OAAO,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;AACzD,CAAC","sourcesContent":["import { isObjectNotFoundError } from './gcs-errors.js';\nimport type { Bucket } from './storage-ops.js';\n\nexport interface StagedObjectMetadata {\n contentType?: string;\n size?: string | number;\n generation?: string | number;\n [field: string]: unknown;\n}\n\nexport type StagedUploadMetadataRead =\n { found: true; metadata: StagedObjectMetadata } | { found: false };\n\nexport async function readStagedUploadMetadata(\n bucket: Bucket,\n storagePath: string,\n): Promise<StagedUploadMetadataRead> {\n try {\n const [metadata] = await bucket.file(storagePath).getMetadata();\n return { found: true, metadata };\n } catch (e) {\n if (isObjectNotFoundError(e)) return { found: false };\n throw e;\n }\n}\n\nexport function stagedObjectGenerationFromMetadata(metadata: {\n generation?: string | number | null;\n}): string | undefined {\n const { generation } = metadata;\n if (generation === undefined || generation === null) return undefined;\n const value = String(generation);\n return value === '' ? undefined : value;\n}\n\nexport type StorageObjectMetadataField = 'generation' | 'size' | 'etag' | 'lastModified';\n\nexport class UnreadableStorageObjectMetadataError extends Error {\n constructor(\n public readonly bucket: string,\n public readonly key: string,\n public readonly field: StorageObjectMetadataField,\n ) {\n super(`storage object metadata has no readable ${field}`);\n this.name = 'UnreadableStorageObjectMetadataError';\n }\n}\n\nexport function requireStorageObjectGeneration(\n metadata: { generation?: string | number | null },\n object: { bucket: string; key: string },\n): string {\n const generation = stagedObjectGenerationFromMetadata(metadata);\n if (generation === undefined) {\n throw new UnreadableStorageObjectMetadataError(object.bucket, object.key, 'generation');\n }\n return generation;\n}\n\nexport interface ReadStagedObjectGenerationOptions {\n onReadFailure?: (error: unknown) => void;\n}\n\nexport async function readStagedObjectGeneration(\n bucket: Bucket,\n storagePath: string | undefined,\n options: ReadStagedObjectGenerationOptions = {},\n): Promise<string | undefined> {\n if (!storagePath) return undefined;\n try {\n const [metadata] = await bucket.file(storagePath).getMetadata();\n return stagedObjectGenerationFromMetadata(metadata);\n } catch (e) {\n options.onReadFailure?.(e);\n return undefined;\n }\n}\n\nexport function pinnedGcsUri(gcsUri: string, generation: string | undefined): string {\n return generation ? `${gcsUri}#${generation}` : gcsUri;\n}\n"]}
|