@ttt-productions/media-processing-core 0.8.15 → 0.8.17
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 +2 -2
- package/dist/audio/audio-processor.d.ts.map +1 -1
- package/dist/audio/audio-processor.js +62 -48
- package/dist/audio/audio-processor.js.map +1 -1
- package/dist/audio/probe.d.ts +1 -1
- package/dist/audio/probe.d.ts.map +1 -1
- package/dist/audio/probe.js +6 -3
- package/dist/audio/probe.js.map +1 -1
- package/dist/duration/media-duration.d.ts +2 -2
- package/dist/duration/media-duration.d.ts.map +1 -1
- package/dist/duration/media-duration.js +23 -21
- package/dist/duration/media-duration.js.map +1 -1
- package/dist/image/formats.d.ts +2 -2
- package/dist/image/formats.js +25 -25
- package/dist/image/formats.js.map +1 -1
- package/dist/image/image-processor.d.ts +2 -2
- package/dist/image/image-processor.d.ts.map +1 -1
- package/dist/image/image-processor.js +47 -37
- package/dist/image/image-processor.js.map +1 -1
- package/dist/image/resize.d.ts +2 -2
- package/dist/image/resize.d.ts.map +1 -1
- package/dist/image/resize.js +13 -13
- package/dist/image/resize.js.map +1 -1
- package/dist/index.d.ts +19 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -16
- package/dist/index.js.map +1 -1
- package/dist/inspection/inspect-media.d.ts +13 -13
- package/dist/inspection/inspect-media.d.ts.map +1 -1
- package/dist/inspection/inspect-media.js +196 -140
- package/dist/inspection/inspect-media.js.map +1 -1
- package/dist/io/fs.d.ts +1 -1
- package/dist/io/fs.js +4 -4
- package/dist/io/fs.js.map +1 -1
- package/dist/io/local-input.d.ts +1 -1
- package/dist/io/local-input.d.ts.map +1 -1
- package/dist/io/local-input.js +15 -15
- package/dist/io/local-input.js.map +1 -1
- package/dist/io/types.d.ts.map +1 -1
- package/dist/io/types.js.map +1 -1
- package/dist/moderation/merge.d.ts +1 -1
- package/dist/moderation/merge.d.ts.map +1 -1
- package/dist/moderation/merge.js +1 -1
- package/dist/moderation/merge.js.map +1 -1
- package/dist/moderation/types.d.ts +3 -3
- package/dist/moderation/types.js.map +1 -1
- package/dist/process-media.d.ts +2 -3
- package/dist/process-media.d.ts.map +1 -1
- package/dist/process-media.js +9 -9
- package/dist/process-media.js.map +1 -1
- package/dist/processing/errors.d.ts.map +1 -1
- package/dist/processing/errors.js +1 -1
- package/dist/processing/errors.js.map +1 -1
- package/dist/processing/result.d.ts +2 -2
- package/dist/processing/result.js +1 -1
- package/dist/processing/result.js.map +1 -1
- package/dist/run-pipeline.d.ts +5 -5
- package/dist/run-pipeline.d.ts.map +1 -1
- package/dist/run-pipeline.js +70 -54
- package/dist/run-pipeline.js.map +1 -1
- package/dist/server/download-token.d.ts +14 -0
- package/dist/server/download-token.d.ts.map +1 -0
- package/dist/server/download-token.js +19 -0
- package/dist/server/download-token.js.map +1 -0
- package/dist/server/firebase-media-io.d.ts +2 -2
- package/dist/server/firebase-media-io.js +1 -1
- package/dist/server/firebase-media-io.js.map +1 -1
- package/dist/server/gcs-errors.js +2 -2
- package/dist/server/gcs-errors.js.map +1 -1
- package/dist/server/index.d.ts +7 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/pending-media.d.ts +2 -2
- package/dist/server/pending-media.d.ts.map +1 -1
- package/dist/server/pending-media.js +12 -6
- package/dist/server/pending-media.js.map +1 -1
- package/dist/server/r2-retry.d.ts +4 -4
- package/dist/server/r2-retry.d.ts.map +1 -1
- package/dist/server/r2-retry.js +48 -47
- package/dist/server/r2-retry.js.map +1 -1
- package/dist/server/staged-object.d.ts +2 -2
- package/dist/server/staged-object.d.ts.map +1 -1
- package/dist/server/staged-object.js +4 -4
- package/dist/server/staged-object.js.map +1 -1
- package/dist/server/storage-ops.d.ts +3 -5
- package/dist/server/storage-ops.d.ts.map +1 -1
- package/dist/server/storage-ops.js +101 -97
- package/dist/server/storage-ops.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js.map +1 -1
- package/dist/utils/log.d.ts.map +1 -1
- package/dist/utils/log.js +3 -3
- package/dist/utils/log.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +1 -4
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/safe-path.js +8 -8
- package/dist/utils/safe-path.js.map +1 -1
- package/dist/validation/validate-duration.d.ts.map +1 -1
- package/dist/validation/validate-duration.js.map +1 -1
- package/dist/validation/validate-mime.d.ts.map +1 -1
- package/dist/validation/validate-mime.js.map +1 -1
- package/dist/validation/validate-size.d.ts.map +1 -1
- package/dist/validation/validate-size.js.map +1 -1
- package/dist/video/ffmpeg.d.ts +1 -1
- package/dist/video/ffmpeg.d.ts.map +1 -1
- package/dist/video/ffmpeg.js +51 -43
- package/dist/video/ffmpeg.js.map +1 -1
- package/dist/video/probe.d.ts +1 -1
- package/dist/video/probe.d.ts.map +1 -1
- package/dist/video/probe.js +10 -7
- package/dist/video/probe.js.map +1 -1
- package/dist/video/video-processor.d.ts +2 -2
- package/dist/video/video-processor.d.ts.map +1 -1
- package/dist/video/video-processor.js +123 -81
- package/dist/video/video-processor.js.map +1 -1
- package/dist/workspace/temp.js +4 -4
- package/dist/workspace/temp.js.map +1 -1
- package/package.json +2 -2
- package/dist/audio/index.d.ts +0 -3
- package/dist/audio/index.d.ts.map +0 -1
- package/dist/audio/index.js +0 -3
- package/dist/audio/index.js.map +0 -1
- package/dist/image/index.d.ts +0 -4
- package/dist/image/index.d.ts.map +0 -1
- package/dist/image/index.js +0 -4
- package/dist/image/index.js.map +0 -1
- package/dist/video/index.d.ts +0 -4
- package/dist/video/index.d.ts.map +0 -1
- package/dist/video/index.js +0 -4
- package/dist/video/index.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pending-media.js","sourceRoot":"","sources":["../../src/server/pending-media.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,4EAA4E;AAC5E,kFAAkF;AAClF,wFAAwF;AACxF,kFAAkF;AAClF,uDAAuD;
|
|
1
|
+
{"version":3,"file":"pending-media.js","sourceRoot":"","sources":["../../src/server/pending-media.ts"],"names":[],"mappings":"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","sourcesContent":["// Admin-SDK transaction wrappers for the pending-media lifecycle. The decisions are the\n// pure ones in `@ttt-productions/media-schemas` (`decidePendingMediaClaim`,\n// `decidePendingMediaFinalize`); these read the CURRENT row inside a transaction,\n// decide, and write. Everything app-specific is injected: the row's `DocumentReference`\n// (so no collection name lives here), the row schema, the claim policy, the extra\n// writes a terminal transition carries, and the clock.\n\nimport type {\n DocumentReference,\n DocumentSnapshot,\n Firestore,\n Transaction,\n} from 'firebase-admin/firestore';\nimport {\n buildPendingMediaTerminalFields,\n decidePendingMediaClaim,\n decidePendingMediaFinalize,\n isPendingMediaNonTerminalStatus,\n PendingMediaStatusSchema,\n type ClaimablePendingMedia,\n type PendingMediaClaimOutcome,\n type PendingMediaClaimPolicy,\n type PendingMediaFinalizeResult,\n type PendingMediaTerminalStatus,\n} from '@ttt-productions/media-schemas';\n\n// ---------------------------------------------------------------------------\n// Claim\n// ---------------------------------------------------------------------------\n\n/** The app's pending-media row schema — anything with zod's `safeParse` shape. */\nexport interface PendingMediaRowParser<TRow> {\n safeParse(data: unknown): { success: true; data: TRow } | { success: false };\n}\n\nexport interface ClaimPendingMediaOptions<TRow> {\n schema: PendingMediaRowParser<TRow>;\n policy: PendingMediaClaimPolicy;\n /** Epoch ms; defaults to `Date.now()`. */\n now?: number;\n}\n\n/**\n * Claim a row for processing inside `transaction`, driven by the CURRENT row (never a\n * frozen event snapshot). Reads before any write. A row that no longer parses is\n * `missing` and never dispatched: a corrupt row must not drive the processor.\n */\nexport async function claimPendingMediaForProcessing<TRow extends ClaimablePendingMedia>(\n transaction: Transaction,\n pendingMediaRef: DocumentReference,\n options: ClaimPendingMediaOptions<TRow>,\n): Promise<PendingMediaClaimOutcome<TRow>> {\n const snap = await transaction.get(pendingMediaRef);\n if (!snap.exists) return { kind: 'missing' };\n\n const parsed = options.schema.safeParse(snap.data());\n if (!parsed.success) return { kind: 'missing' };\n\n const { outcome, write } = decidePendingMediaClaim(\n parsed.data,\n options.now ?? Date.now(),\n options.policy,\n );\n // Spread into a literal: the Admin SDK's update() wants an UpdateData-compatible object.\n if (write) transaction.update(pendingMediaRef, { ...write });\n return outcome;\n}\n\n// ---------------------------------------------------------------------------\n// Finalize\n// ---------------------------------------------------------------------------\n\nexport interface PendingMediaFinalizeContext {\n targetStatus: PendingMediaTerminalStatus;\n /** True when the row failed schema validation and is being failed as unparsable. */\n unparsable: boolean;\n}\n\n/**\n * Caller-owned writes that must commit atomically with a terminal transition, and only\n * when it applies (releasing a quota reservation, for example — exactly once, because\n * the compare-and-set gates it). It runs in the transaction's READ phase and may\n * `transaction.get`; it returns the WRITE-phase step, or undefined when there is nothing\n * to write. The step runs after the terminal write, and its return value is surfaced as\n * the outcome's `extra`.\n */\nexport type PendingMediaExtraWrites<TExtra> = (\n transaction: Transaction,\n snapshot: DocumentSnapshot,\n context: PendingMediaFinalizeContext,\n) => Promise<(() => TExtra) | undefined>;\n\nexport interface FinalizePendingMediaOptions<TExtra> {\n /** Merged into the terminal fields (`result`, `errorCategory`, `errorMessage`, …). */\n extraFields?: Record<string, unknown>;\n extraWrites?: PendingMediaExtraWrites<TExtra>;\n /** Epoch ms; defaults to `Date.now()`. */\n now?: number;\n}\n\nexport interface PendingMediaFinalizeOutcome<TExtra> extends PendingMediaFinalizeResult {\n /** What the extra writes returned — present only on `applied`, when they wrote. */\n extra?: TExtra;\n}\n\n/**\n * Compare-and-set finalize inside the caller's transaction: re-read the row, decide,\n * and write the terminal fields (plus any extra writes) only on `applied`. An\n * already-terminal row is never overwritten. Reads-before-writes holds as long as the\n * caller has issued no write on `transaction` yet.\n */\nexport async function finalizePendingMediaInTransaction<TExtra = never>(\n transaction: Transaction,\n ref: DocumentReference,\n targetStatus: PendingMediaTerminalStatus,\n options: FinalizePendingMediaOptions<TExtra> = {},\n): Promise<PendingMediaFinalizeOutcome<TExtra>> {\n const snap = await transaction.get(ref);\n const result = decidePendingMediaFinalize(\n snap.exists ? (snap.data() ?? {}) : undefined,\n targetStatus,\n );\n if (result.decision !== 'applied') return result;\n\n const applyExtra = await options.extraWrites?.(transaction, snap, {\n targetStatus,\n unparsable: false,\n });\n transaction.update(\n ref,\n buildPendingMediaTerminalFields(\n targetStatus,\n options.extraFields ?? {},\n options.now ?? Date.now(),\n ),\n );\n return applyExtra ? { ...result, extra: applyExtra() } : result;\n}\n\n/** Standalone compare-and-set finalize, in its own transaction. */\nexport function finalizePendingMedia<TExtra = never>(\n db: Firestore,\n ref: DocumentReference,\n targetStatus: PendingMediaTerminalStatus,\n options: FinalizePendingMediaOptions<TExtra> = {},\n): Promise<PendingMediaFinalizeOutcome<TExtra>> {\n return db.runTransaction((transaction) =>\n finalizePendingMediaInTransaction(transaction, ref, targetStatus, options),\n );\n}\n\n/**\n * Fail a row that no longer passes schema validation, so the transition table cannot\n * be applied (it may carry no readable `status`). One transaction: the row is failed —\n * with any extra writes — only while it has no status or a `pending` / `processing`\n * one, so an already-terminal row is never touched and extra writes run exactly once.\n */\nexport async function finalizeUnparsablePendingMedia<TExtra = never>(\n db: Firestore,\n ref: DocumentReference,\n options: FinalizePendingMediaOptions<TExtra> = {},\n): Promise<void> {\n await db.runTransaction(async (transaction) => {\n const snap = await transaction.get(ref);\n if (!snap.exists) return;\n const status = (snap.data() as { status?: unknown } | undefined)?.status;\n if (status !== undefined && !isPendingMediaNonTerminalStatus(status)) return;\n\n const failed = PendingMediaStatusSchema.enum.failed;\n const applyExtra = await options.extraWrites?.(transaction, snap, {\n targetStatus: failed,\n unparsable: true,\n });\n transaction.update(\n ref,\n buildPendingMediaTerminalFields(failed, options.extraFields ?? {}, options.now ?? Date.now()),\n );\n applyExtra?.();\n });\n}\n"]}
|
|
@@ -9,9 +9,9 @@ export declare const R2_RETRY_MAX_BACKOFF_MS = 5000;
|
|
|
9
9
|
export declare const R2_RETRY_AFTER_CAP_MS = 30000;
|
|
10
10
|
/** HTTP statuses treated as transient and safe to retry. */
|
|
11
11
|
export declare const R2_RETRYABLE_STATUSES: ReadonlySet<number>;
|
|
12
|
-
export type R2RetryableOperation =
|
|
12
|
+
export type R2RetryableOperation = 'putFile' | 'copy' | 'readToFile' | 'deleteCopy';
|
|
13
13
|
/** Every R2 store operation an `R2StorageError` can name; `delete` never retries. */
|
|
14
|
-
export type R2Operation = R2RetryableOperation |
|
|
14
|
+
export type R2Operation = R2RetryableOperation | 'delete';
|
|
15
15
|
/** Deterministic seams for tests; production defaults are internal (see below). */
|
|
16
16
|
export interface R2RetrySeams {
|
|
17
17
|
/** Resolve after `ms` milliseconds. */
|
|
@@ -79,10 +79,10 @@ export declare class R2StorageError extends Error {
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
export type R2AttemptResult<T> = {
|
|
82
|
-
kind:
|
|
82
|
+
kind: 'success';
|
|
83
83
|
value: T;
|
|
84
84
|
} | {
|
|
85
|
-
kind:
|
|
85
|
+
kind: 'response';
|
|
86
86
|
response: Response;
|
|
87
87
|
};
|
|
88
88
|
/**
|
|
@@ -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":"AAgBA,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,sDAAsD;AACtD,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,0DAA0D;AAC1D,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAC7C;qCACqC;AACrC,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C,4DAA4D;AAC5D,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAA2C,CAAC;AAElG,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,YAAY,CAAC;AAEpF,qFAAqF;AACrF,MAAM,MAAM,WAAW,GAAG,oBAAoB,GAAG,QAAQ,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,gCAAgC;IAChC,MAAM,EAAE,MAAM,MAAM,CAAC;IACrB,oFAAoF;IACpF,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,gCAAgC;AAChC,wBAAgB,mBAAmB,IAAI,YAAY,CAMlD;AAsDD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAwB/D;AAMD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAiBf;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;CACrB,GAAG,MAAM,CAYT;AAMD;;;;;;;;;;;GAWG;AACH,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,oGAAoG;IACpG,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;AAWD,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;AAyB3E;;;;;GAKG;AACH,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,CAoCb"}
|
package/dist/server/r2-retry.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// Bounded transient-retry policy for the R2 object store.
|
|
2
2
|
//
|
|
3
|
-
// PRIVATE
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// so callers can recognize an exhausted storage operation.
|
|
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.
|
|
7
6
|
//
|
|
8
7
|
// The policy applies to the idempotent-against-a-deterministic-key requests
|
|
9
8
|
// only (putFile, copy, readToFile, and deleteCopy's read of the destination).
|
|
@@ -38,45 +37,45 @@ export function defaultR2RetrySeams() {
|
|
|
38
37
|
// Recognized transient transport failure codes (undici/fetch surface these on
|
|
39
38
|
// the error or its `.cause`). A broken connection to R2 lands here.
|
|
40
39
|
const RETRYABLE_NETWORK_ERROR_CODES = new Set([
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
'ECONNRESET',
|
|
41
|
+
'ECONNREFUSED',
|
|
42
|
+
'ECONNABORTED',
|
|
43
|
+
'ETIMEDOUT',
|
|
44
|
+
'EPIPE',
|
|
45
|
+
'ENOTFOUND',
|
|
46
|
+
'EAI_AGAIN',
|
|
47
|
+
'ENETUNREACH',
|
|
48
|
+
'ENETDOWN',
|
|
49
|
+
'EHOSTUNREACH',
|
|
50
|
+
'EHOSTDOWN',
|
|
51
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
52
|
+
'UND_ERR_HEADERS_TIMEOUT',
|
|
53
|
+
'UND_ERR_BODY_TIMEOUT',
|
|
54
|
+
'UND_ERR_SOCKET',
|
|
56
55
|
]);
|
|
57
56
|
// Deterministic local filesystem errors are NEVER retried (disk full, perms,
|
|
58
57
|
// missing dir, etc.). This guard wins over any transport classification so a
|
|
59
58
|
// local write failure during readToFile piping can't be mistaken for transient.
|
|
60
59
|
const LOCAL_FS_ERROR_CODES = new Set([
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
60
|
+
'ENOSPC',
|
|
61
|
+
'EACCES',
|
|
62
|
+
'EISDIR',
|
|
63
|
+
'ENOENT',
|
|
64
|
+
'EROFS',
|
|
65
|
+
'EMFILE',
|
|
66
|
+
'ENFILE',
|
|
67
|
+
'EBADF',
|
|
68
|
+
'EEXIST',
|
|
69
|
+
'EPERM',
|
|
70
|
+
'ENAMETOOLONG',
|
|
71
|
+
'ELOOP',
|
|
72
|
+
'ENOTDIR',
|
|
73
|
+
'EDQUOT',
|
|
75
74
|
]);
|
|
76
75
|
function codeOf(err) {
|
|
77
|
-
if (err && typeof err ===
|
|
76
|
+
if (err && typeof err === 'object') {
|
|
78
77
|
const c = err.code;
|
|
79
|
-
if (typeof c ===
|
|
78
|
+
if (typeof c === 'string')
|
|
80
79
|
return c;
|
|
81
80
|
}
|
|
82
81
|
return undefined;
|
|
@@ -87,11 +86,11 @@ function codeOf(err) {
|
|
|
87
86
|
* an abort must never become another attempt.
|
|
88
87
|
*/
|
|
89
88
|
export function isRetryableTransportError(err) {
|
|
90
|
-
if (!err || typeof err !==
|
|
89
|
+
if (!err || typeof err !== 'object')
|
|
91
90
|
return false;
|
|
92
91
|
const e = err;
|
|
93
92
|
// NEVER convert an explicit abort into another attempt.
|
|
94
|
-
if (e.name ===
|
|
93
|
+
if (e.name === 'AbortError')
|
|
95
94
|
return false;
|
|
96
95
|
const selfCode = codeOf(err);
|
|
97
96
|
const causeCode = codeOf(e.cause);
|
|
@@ -108,7 +107,7 @@ export function isRetryableTransportError(err) {
|
|
|
108
107
|
return true;
|
|
109
108
|
// fetch()/undici surface transport failures as `TypeError: fetch failed`
|
|
110
109
|
// (often with an unrecognized or absent cause) — treat those as transient.
|
|
111
|
-
if (err instanceof TypeError || e.name ===
|
|
110
|
+
if (err instanceof TypeError || e.name === 'TypeError')
|
|
112
111
|
return true;
|
|
113
112
|
return false;
|
|
114
113
|
}
|
|
@@ -125,7 +124,7 @@ export function parseRetryAfterMs(headerValue, nowMs) {
|
|
|
125
124
|
if (headerValue == null)
|
|
126
125
|
return null;
|
|
127
126
|
const trimmed = headerValue.trim();
|
|
128
|
-
if (trimmed ===
|
|
127
|
+
if (trimmed === '')
|
|
129
128
|
return null;
|
|
130
129
|
// delta-seconds form
|
|
131
130
|
if (/^\d+$/.test(trimmed)) {
|
|
@@ -149,7 +148,7 @@ export function parseRetryAfterMs(headerValue, nowMs) {
|
|
|
149
148
|
export function computeR2DelayMs(params) {
|
|
150
149
|
const { response, retryIndex, seams } = params;
|
|
151
150
|
if (response) {
|
|
152
|
-
const retryAfterMs = parseRetryAfterMs(response.headers.get(
|
|
151
|
+
const retryAfterMs = parseRetryAfterMs(response.headers.get('retry-after'), seams.now());
|
|
153
152
|
if (retryAfterMs !== null) {
|
|
154
153
|
return Math.min(retryAfterMs, R2_RETRY_AFTER_CAP_MS);
|
|
155
154
|
}
|
|
@@ -183,9 +182,11 @@ export class R2StorageError extends Error {
|
|
|
183
182
|
responseText;
|
|
184
183
|
constructor(params) {
|
|
185
184
|
const code = parseS3ErrorCode(params.responseText);
|
|
186
|
-
const detail = [params.status !== undefined ? `status ${params.status}` : undefined, code]
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
const detail = [params.status !== undefined ? `status ${params.status}` : undefined, code]
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join(' ');
|
|
188
|
+
super(`R2 ${params.operation} failed after ${params.attempts} attempt(s)${detail ? `: ${detail}` : ''}`, params.cause !== undefined ? { cause: params.cause } : undefined);
|
|
189
|
+
this.name = 'R2StorageError';
|
|
189
190
|
this.operation = params.operation;
|
|
190
191
|
this.bucket = params.bucket;
|
|
191
192
|
this.key = params.key;
|
|
@@ -203,10 +204,10 @@ function parseS3ErrorCode(responseText) {
|
|
|
203
204
|
// code-point filter rather than a control-character regex (which `no-control-regex`
|
|
204
205
|
// forbids and which would embed literal control bytes in source).
|
|
205
206
|
function sanitizeBodyText(text) {
|
|
206
|
-
let out =
|
|
207
|
+
let out = '';
|
|
207
208
|
for (const ch of text.slice(0, 1000)) {
|
|
208
209
|
const code = ch.codePointAt(0) ?? 0;
|
|
209
|
-
out += code < 0x20 || code === 0x7f ?
|
|
210
|
+
out += code < 0x20 || code === 0x7f ? ' ' : ch;
|
|
210
211
|
}
|
|
211
212
|
return out.trim().slice(0, 500);
|
|
212
213
|
}
|
|
@@ -218,7 +219,7 @@ async function safeReadBoundedBody(res) {
|
|
|
218
219
|
return sanitizeBodyText(await res.text());
|
|
219
220
|
}
|
|
220
221
|
catch {
|
|
221
|
-
return
|
|
222
|
+
return '<no body>';
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
225
|
/**
|
|
@@ -245,7 +246,7 @@ export async function fetchWithR2Retry(params) {
|
|
|
245
246
|
await seams.sleep(computeR2DelayMs({ response: null, retryIndex: attempt - 1, seams }));
|
|
246
247
|
continue;
|
|
247
248
|
}
|
|
248
|
-
if (result.kind ===
|
|
249
|
+
if (result.kind === 'success')
|
|
249
250
|
return result.value;
|
|
250
251
|
const response = result.response;
|
|
251
252
|
const status = response.status;
|
|
@@ -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 sibling of storage-ops.ts — imported ONLY by that module. It is\n// deliberately NOT re-exported from the server barrel (`server/index.ts`); the\n// only public surface is the `R2StorageError` type, which storage-ops re-exports\n// so callers can recognize an exhausted storage operation.\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(headerValue: string | null | undefined, nowMs: number): 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].filter(Boolean).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 }\n | { 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":["// 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,4 +1,4 @@
|
|
|
1
|
-
import type { Bucket } from
|
|
1
|
+
import type { Bucket } from './storage-ops.js';
|
|
2
2
|
/** The stored-object metadata fields these reads use (the Admin SDK returns more). */
|
|
3
3
|
export interface StagedObjectMetadata {
|
|
4
4
|
contentType?: string;
|
|
@@ -29,7 +29,7 @@ export declare function stagedObjectGenerationFromMetadata(metadata: {
|
|
|
29
29
|
generation?: string | number | null;
|
|
30
30
|
}): string | undefined;
|
|
31
31
|
/** The stored-object metadata field an `UnreadableStorageObjectMetadataError` names. */
|
|
32
|
-
export type StorageObjectMetadataField =
|
|
32
|
+
export type StorageObjectMetadataField = 'generation' | 'size' | 'etag' | 'lastModified';
|
|
33
33
|
/**
|
|
34
34
|
* A storage object whose metadata lacks a field its backend always reports: a Cloud Storage
|
|
35
35
|
* generation or byte size (GCS, the Storage emulator, and a Storage finalize event carry
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staged-object.d.ts","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C,sFAAsF;AACtF,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,
|
|
1
|
+
{"version":3,"file":"staged-object.d.ts","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C,sFAAsF;AACtF,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;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,wBAAwB,CAAC,CAQnC;AAED;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAAC,QAAQ,EAAE;IAC3D,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACrC,GAAG,MAAM,GAAG,SAAS,CAKrB;AAED,wFAAwF;AACxF,MAAM,MAAM,0BAA0B,GAAG,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC;AAEzF;;;;;;GAMG;AACH,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;;;;;GAKG;AACH,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,gGAAgG;IAChG,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C;AAED;;;;;;;;;;GAUG;AACH,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;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEnF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Admin-SDK reads of a STAGED upload object (the client-written staging copy a
|
|
2
2
|
// processor reads from). The accept-time verdict over the metadata is the pure
|
|
3
3
|
// `verifyStagedUploadMetadata` in `@ttt-productions/media-schemas`.
|
|
4
|
-
import { isObjectNotFoundError } from
|
|
4
|
+
import { isObjectNotFoundError } from './gcs-errors.js';
|
|
5
5
|
/**
|
|
6
6
|
* Read a staged object's stored metadata (`contentType`, `size`, `generation`, …) for
|
|
7
7
|
* `verifyStagedUploadMetadata`. A missing object is `{ found: false }`; any other read
|
|
@@ -30,7 +30,7 @@ export function stagedObjectGenerationFromMetadata(metadata) {
|
|
|
30
30
|
if (generation === undefined || generation === null)
|
|
31
31
|
return undefined;
|
|
32
32
|
const value = String(generation);
|
|
33
|
-
return value ===
|
|
33
|
+
return value === '' ? undefined : value;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* A storage object whose metadata lacks a field its backend always reports: a Cloud Storage
|
|
@@ -48,7 +48,7 @@ export class UnreadableStorageObjectMetadataError extends Error {
|
|
|
48
48
|
this.bucket = bucket;
|
|
49
49
|
this.key = key;
|
|
50
50
|
this.field = field;
|
|
51
|
-
this.name =
|
|
51
|
+
this.name = 'UnreadableStorageObjectMetadataError';
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
@@ -60,7 +60,7 @@ export class UnreadableStorageObjectMetadataError extends Error {
|
|
|
60
60
|
export function requireStorageObjectGeneration(metadata, object) {
|
|
61
61
|
const generation = stagedObjectGenerationFromMetadata(metadata);
|
|
62
62
|
if (generation === undefined) {
|
|
63
|
-
throw new UnreadableStorageObjectMetadataError(object.bucket, object.key,
|
|
63
|
+
throw new UnreadableStorageObjectMetadataError(object.bucket, object.key, 'generation');
|
|
64
64
|
}
|
|
65
65
|
return generation;
|
|
66
66
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staged-object.js","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,oEAAoE;AAEpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"staged-object.js","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,oEAAoE;AAEpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAcxD;;;;GAIG;AACH,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;;;;;;GAMG;AACH,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;AAKD;;;;;;GAMG;AACH,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;;;;;GAKG;AACH,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;AAOD;;;;;;;;;;GAUG;AACH,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;;;;GAIG;AACH,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":["// Admin-SDK reads of a STAGED upload object (the client-written staging copy a\n// processor reads from). The accept-time verdict over the metadata is the pure\n// `verifyStagedUploadMetadata` in `@ttt-productions/media-schemas`.\n\nimport { isObjectNotFoundError } from './gcs-errors.js';\nimport type { Bucket } from './storage-ops.js';\n\n/** The stored-object metadata fields these reads use (the Admin SDK returns more). */\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\n/**\n * Read a staged object's stored metadata (`contentType`, `size`, `generation`, …) for\n * `verifyStagedUploadMetadata`. A missing object is `{ found: false }`; any other read\n * failure throws.\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\n/**\n * The staged object's generation from metadata the caller already fetched, as a string\n * (GCS generations exceed 2^53) — the one normalization `readStagedObjectGeneration`\n * applies, for a processor that already holds the object's metadata and must not read\n * it again. `undefined` when the metadata carries no generation (the emulator may not):\n * absent, `null`, or an empty string, which names no generation.\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\n/** The stored-object metadata field an `UnreadableStorageObjectMetadataError` names. */\nexport type StorageObjectMetadataField = 'generation' | 'size' | 'etag' | 'lastModified';\n\n/**\n * A storage object whose metadata lacks a field its backend always reports: a Cloud Storage\n * generation or byte size (GCS, the Storage emulator, and a Storage finalize event carry\n * both), or an R2 object's ETag, last-modified time, or size. This is an anomaly to surface,\n * never to paper over with a placeholder. The bucket and key are PROPERTIES, never message\n * text: an escaping error reaches monitoring, and an object key must not.\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\n/**\n * The object's generation from metadata the caller already holds, for a site that cannot\n * proceed without it (a hold key, an evidence record, a pinned read). Throws\n * `UnreadableStorageObjectMetadataError` (field `generation`) when the metadata names none —\n * the same normalization as `stagedObjectGenerationFromMetadata`, with no fallback value.\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 /** Called when the generation cannot be read (the caller logs it); reads then stay unpinned. */\n onReadFailure?: (error: unknown) => void;\n}\n\n/**\n * The staged object's current generation, as a string (GCS generations exceed 2^53).\n *\n * This is the one value that pins every later read of the staged bytes — the processing\n * download, a moderation scan, a hash — to the SAME immutable object, so \"processed\n * bytes == scanned bytes == hashed bytes\" holds even if the staging path were\n * overwritten (a pinned read resolves exactly the captured generation). Returns\n * `undefined` when no path is given, the object reports no generation (the emulator may\n * not), or the read fails — best effort: callers fall back to unpinned reads, and a\n * create-only staging rule stays the primary defense.\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\n/**\n * A `gs://` URI pinned to a captured generation (`gs://bucket/path#<generation>`), so a\n * service reading by URI (a moderation scan) reads the same bytes the processor did.\n * Without a generation it is the bare URI.\n */\nexport function pinnedGcsUri(gcsUri: string, generation: string | undefined): string {\n return generation ? `${gcsUri}#${generation}` : gcsUri;\n}\n"]}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type { getStorage } from
|
|
2
|
-
import { R2StorageError } from "./r2-retry.js";
|
|
3
|
-
export { R2StorageError };
|
|
1
|
+
import type { getStorage } from 'firebase-admin/storage';
|
|
4
2
|
/**
|
|
5
3
|
* A copy found an object at its destination key that is not this copy's result, and
|
|
6
4
|
* wrote nothing; that object is untouched. The key is a PROPERTY, never message text, so
|
|
@@ -11,7 +9,7 @@ export declare class ObjectAlreadyExistsError extends Error {
|
|
|
11
9
|
constructor(key: string);
|
|
12
10
|
}
|
|
13
11
|
/** Firebase Admin Storage Bucket — inferred to avoid @google-cloud/storage dep. */
|
|
14
|
-
export type Bucket = ReturnType<ReturnType<typeof getStorage>[
|
|
12
|
+
export type Bucket = ReturnType<ReturnType<typeof getStorage>['bucket']>;
|
|
15
13
|
export interface ObjectWriteResult {
|
|
16
14
|
key: string;
|
|
17
15
|
sizeBytes: number;
|
|
@@ -35,7 +33,7 @@ export interface ObjectCopyResult {
|
|
|
35
33
|
* `absent` — no object; `notThisCopy` — an object this package did not copy from that
|
|
36
34
|
* source, left untouched.
|
|
37
35
|
*/
|
|
38
|
-
export type DeleteCopyResult =
|
|
36
|
+
export type DeleteCopyResult = 'deleted' | 'absent' | 'notThisCopy';
|
|
39
37
|
export interface MediaObjectStore {
|
|
40
38
|
/**
|
|
41
39
|
* Upload a local file to the store at `key`. Objects are written with a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-ops.d.ts","sourceRoot":"","sources":["../../src/server/storage-ops.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"storage-ops.d.ts","sourceRoot":"","sources":["../../src/server/storage-ops.ts"],"names":[],"mappings":"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"}
|