@averyyy/pi-server 0.82.1-piclient.2 → 0.82.1-piclient.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +25 -0
- package/bin/update.js +156 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pi-server-protocol.d.ts +16 -0
- package/dist/pi-server-protocol.d.ts.map +1 -1
- package/dist/pi-server-protocol.js +30 -1
- package/dist/pi-server-protocol.js.map +1 -1
- package/dist/receive-upload.d.ts +5 -0
- package/dist/receive-upload.d.ts.map +1 -1
- package/dist/receive-upload.js +88 -6
- package/dist/receive-upload.js.map +1 -1
- package/dist/request-chunks.d.ts +33 -0
- package/dist/request-chunks.d.ts.map +1 -1
- package/dist/request-chunks.js +540 -48
- package/dist/request-chunks.js.map +1 -1
- package/dist/server.d.ts +41 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3088 -366
- package/dist/server.js.map +1 -1
- package/dist/session-persistence.d.ts +39 -1
- package/dist/session-persistence.d.ts.map +1 -1
- package/dist/session-persistence.js +1132 -116
- package/dist/session-persistence.js.map +1 -1
- package/dist/session-store.d.ts +112 -6
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +695 -120
- package/dist/session-store.js.map +1 -1
- package/dist/stream-run-persistence.d.ts +163 -0
- package/dist/stream-run-persistence.d.ts.map +1 -0
- package/dist/stream-run-persistence.js +1824 -0
- package/dist/stream-run-persistence.js.map +1 -0
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receive-upload.js","sourceRoot":"","sources":["../src/receive-upload.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\ninterface UploadEntry {\n\tpath: string;\n\ttype: \"file\" | \"directory\";\n\tdata?: Buffer;\n}\n\nexport class ReceiveUploadError extends Error {\n\tstatus: number;\n\n\tconstructor(status: number, message: string) {\n\t\tsuper(message);\n\t\tthis.status = status;\n\t}\n}\n\nfunction invalid(message: string): never {\n\tthrow new ReceiveUploadError(400, message);\n}\n\nfunction validatePath(path: unknown): string {\n\tif (typeof path !== \"string\") invalid(\"entry path must be a string\");\n\tif (path.includes(\"\\\\\") || path.includes(\"\\0\")) invalid(`invalid entry path: ${path}`);\n\tif (path !== \"\" && path.split(\"/\").some((part) => part === \"\" || part === \".\" || part === \"..\")) {\n\t\tinvalid(`invalid entry path: ${path}`);\n\t}\n\treturn path;\n}\n\nfunction validateBody(body: unknown): { name: string; entries: UploadEntry[] } {\n\tif (typeof body !== \"object\" || body === null) invalid(\"upload body must be an object\");\n\tconst upload = body as { name?: unknown; entries?: unknown };\n\tif (\n\t\ttypeof upload.name !== \"string\" ||\n\t\tupload.name === \"\" ||\n\t\tupload.name === \".\" ||\n\t\tupload.name === \"..\" ||\n\t\tupload.name.includes(\"/\") ||\n\t\tupload.name.includes(\"\\\\\") ||\n\t\tupload.name.includes(\"\\0\")\n\t) {\n\t\tinvalid(\"invalid upload name\");\n\t}\n\tif (!Array.isArray(upload.entries) || upload.entries.length === 0) invalid(\"entries must be a non-empty array\");\n\n\tconst entries: UploadEntry[] = [];\n\tconst entryTypes = new Map<string, \"file\" | \"directory\">();\n\tfor (const value of upload.entries) {\n\t\tif (typeof value !== \"object\" || value === null) invalid(\"each entry must be an object\");\n\t\tconst entry = value as { path?: unknown; type?: unknown; data?: unknown };\n\t\tconst path = validatePath(entry.path);\n\t\tif (entry.type !== \"file\" && entry.type !== \"directory\") invalid(`invalid entry type: ${String(entry.type)}`);\n\t\tif (entryTypes.has(path)) invalid(`duplicate entry path: ${path}`);\n\t\tentryTypes.set(path, entry.type);\n\t\tif (entry.type === \"directory\") {\n\t\t\tif (entry.data !== undefined) invalid(`directory entry must not contain data: ${path}`);\n\t\t\tentries.push({ path, type: \"directory\" });\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof entry.data !== \"string\") invalid(`file entry data must be base64: ${path}`);\n\t\tconst data = Buffer.from(entry.data, \"base64\");\n\t\tif (data.toString(\"base64\") !== entry.data) invalid(`file entry data must be valid base64: ${path}`);\n\t\tentries.push({ path, type: \"file\", data });\n\t}\n\n\tconst rootType = entryTypes.get(\"\");\n\tif (!rootType) invalid(\"entries must include the root path\");\n\tif (rootType === \"file\" && entries.length !== 1) invalid(\"a file upload cannot contain child entries\");\n\tfor (const entry of entries) {\n\t\tif (entry.path === \"\") continue;\n\t\tconst parent = entry.path.includes(\"/\") ? entry.path.slice(0, entry.path.lastIndexOf(\"/\")) : \"\";\n\t\tif (entryTypes.get(parent) !== \"directory\") invalid(`missing directory entry: ${parent}`);\n\t}\n\treturn { name: upload.name, entries };\n}\n\nexport function receiveUpload(uploadDir: string, body: unknown): { path: string; files: number } {\n\tconst upload = validateBody(body);\n\tmkdirSync(uploadDir, { recursive: true });\n\tconst destination = join(uploadDir, upload.name);\n\tif (existsSync(destination)) throw new ReceiveUploadError(409, `destination already exists: ${destination}`);\n\tconst temporaryDir = mkdtempSync(join(uploadDir, \".upload-\"));\n\tconst temporaryRoot = join(temporaryDir, upload.name);\n\ttry {\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"directory\")) {\n\t\t\tmkdirSync(join(temporaryRoot, entry.path), { recursive: true });\n\t\t}\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"file\")) {\n\t\t\tconst path = join(temporaryRoot, entry.path);\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, entry.data!);\n\t\t}\n\t\trenameSync(temporaryRoot, destination);\n\t} finally {\n\t\trmSync(temporaryDir, { recursive: true, force: true });\n\t}\n\treturn { path: destination, files: upload.entries.filter((entry) => entry.type === \"file\").length };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"receive-upload.js","sourceRoot":"","sources":["../src/receive-upload.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tcloseSync,\n\texistsSync,\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\topenSync,\n\treaddirSync,\n\treadSync,\n\trenameSync,\n\trmSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { dirname, join, relative, sep } from \"node:path\";\n\ninterface UploadEntry {\n\tpath: string;\n\ttype: \"file\" | \"directory\";\n\tdata?: Buffer;\n}\n\nexport const RECEIVE_UPLOAD_MAX_ENTRIES = 10_000;\nexport const RECEIVE_UPLOAD_MAX_NAME_CHARS = 255;\nexport const RECEIVE_UPLOAD_MAX_PATH_CHARS = 4096;\nexport const RECEIVE_UPLOAD_MAX_PATH_SEGMENT_CHARS = 255;\nexport const RECEIVE_UPLOAD_MAX_DECODED_BYTES = 64 * 1024 * 1024;\n\nexport class ReceiveUploadError extends Error {\n\tstatus: number;\n\n\tconstructor(status: number, message: string) {\n\t\tsuper(message);\n\t\tthis.status = status;\n\t}\n}\n\nfunction invalid(message: string): never {\n\tthrow new ReceiveUploadError(400, message);\n}\n\nfunction validatePath(path: unknown): string {\n\tif (typeof path !== \"string\") invalid(\"entry path must be a string\");\n\tif (path.length > RECEIVE_UPLOAD_MAX_PATH_CHARS) {\n\t\tinvalid(`entry path must not exceed ${RECEIVE_UPLOAD_MAX_PATH_CHARS} characters`);\n\t}\n\tif (path.includes(\"\\\\\") || path.includes(\"\\0\")) invalid(`invalid entry path: ${path}`);\n\tif (path !== \"\" && path.split(\"/\").some((part) => part === \"\" || part === \".\" || part === \"..\")) {\n\t\tinvalid(`invalid entry path: ${path}`);\n\t}\n\tif (path.split(\"/\").some((part) => part.length > RECEIVE_UPLOAD_MAX_PATH_SEGMENT_CHARS)) {\n\t\tinvalid(`entry path segment must not exceed ${RECEIVE_UPLOAD_MAX_PATH_SEGMENT_CHARS} characters`);\n\t}\n\treturn path;\n}\n\nfunction validateBody(body: unknown): { name: string; entries: UploadEntry[] } {\n\tif (typeof body !== \"object\" || body === null) invalid(\"upload body must be an object\");\n\tconst upload = body as { name?: unknown; entries?: unknown };\n\tif (\n\t\ttypeof upload.name !== \"string\" ||\n\t\tupload.name === \"\" ||\n\t\tupload.name === \".\" ||\n\t\tupload.name === \"..\" ||\n\t\tupload.name.includes(\"/\") ||\n\t\tupload.name.includes(\"\\\\\") ||\n\t\tupload.name.includes(\"\\0\") ||\n\t\tupload.name.length > RECEIVE_UPLOAD_MAX_NAME_CHARS\n\t) {\n\t\tinvalid(\"invalid upload name\");\n\t}\n\tif (!Array.isArray(upload.entries) || upload.entries.length === 0) invalid(\"entries must be a non-empty array\");\n\tif (upload.entries.length > RECEIVE_UPLOAD_MAX_ENTRIES) {\n\t\tinvalid(`entries must not exceed ${RECEIVE_UPLOAD_MAX_ENTRIES}`);\n\t}\n\n\tconst entries: UploadEntry[] = [];\n\tconst entryTypes = new Map<string, \"file\" | \"directory\">();\n\tlet decodedBytes = 0;\n\tfor (const value of upload.entries) {\n\t\tif (typeof value !== \"object\" || value === null) invalid(\"each entry must be an object\");\n\t\tconst entry = value as { path?: unknown; type?: unknown; data?: unknown };\n\t\tconst path = validatePath(entry.path);\n\t\tif (entry.type !== \"file\" && entry.type !== \"directory\") invalid(`invalid entry type: ${String(entry.type)}`);\n\t\tif (entryTypes.has(path)) invalid(`duplicate entry path: ${path}`);\n\t\tentryTypes.set(path, entry.type);\n\t\tif (entry.type === \"directory\") {\n\t\t\tif (entry.data !== undefined) invalid(`directory entry must not contain data: ${path}`);\n\t\t\tentries.push({ path, type: \"directory\" });\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof entry.data !== \"string\") invalid(`file entry data must be base64: ${path}`);\n\t\tconst data = Buffer.from(entry.data, \"base64\");\n\t\tif (data.toString(\"base64\") !== entry.data) invalid(`file entry data must be valid base64: ${path}`);\n\t\tdecodedBytes += data.byteLength;\n\t\tif (decodedBytes > RECEIVE_UPLOAD_MAX_DECODED_BYTES) {\n\t\t\tinvalid(`decoded upload data must not exceed ${RECEIVE_UPLOAD_MAX_DECODED_BYTES} bytes`);\n\t\t}\n\t\tentries.push({ path, type: \"file\", data });\n\t}\n\n\tconst rootType = entryTypes.get(\"\");\n\tif (!rootType) invalid(\"entries must include the root path\");\n\tif (rootType === \"file\" && entries.length !== 1) invalid(\"a file upload cannot contain child entries\");\n\tfor (const entry of entries) {\n\t\tif (entry.path === \"\") continue;\n\t\tconst parent = entry.path.includes(\"/\") ? entry.path.slice(0, entry.path.lastIndexOf(\"/\")) : \"\";\n\t\tif (entryTypes.get(parent) !== \"directory\") invalid(`missing directory entry: ${parent}`);\n\t}\n\treturn { name: upload.name, entries };\n}\n\nfunction fileMatches(path: string, expected: Buffer): boolean {\n\tconst stat = lstatSync(path);\n\tif (!stat.isFile() || stat.size !== expected.byteLength) return false;\n\tconst handle = openSync(path, \"r\");\n\tconst buffer = Buffer.allocUnsafe(Math.min(64 * 1024, Math.max(1, expected.byteLength)));\n\tlet offset = 0;\n\ttry {\n\t\twhile (offset < expected.byteLength) {\n\t\t\tconst bytesRead = readSync(\n\t\t\t\thandle,\n\t\t\t\tbuffer,\n\t\t\t\t0,\n\t\t\t\tMath.min(buffer.byteLength, expected.byteLength - offset),\n\t\t\t\toffset,\n\t\t\t);\n\t\t\tif (bytesRead === 0 || !buffer.subarray(0, bytesRead).equals(expected.subarray(offset, offset + bytesRead))) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\toffset += bytesRead;\n\t\t}\n\t\treturn true;\n\t} finally {\n\t\tcloseSync(handle);\n\t}\n}\n\nfunction existingDestinationMatches(destination: string, entries: UploadEntry[]): boolean {\n\tconst expected = new Map(entries.map((entry) => [entry.path, entry]));\n\tconst root = expected.get(\"\");\n\tif (!root) return false;\n\tconst destinationStat = lstatSync(destination);\n\tif (root.type === \"file\") {\n\t\treturn !destinationStat.isSymbolicLink() && fileMatches(destination, root.data!);\n\t}\n\tif (!destinationStat.isDirectory() || destinationStat.isSymbolicLink()) return false;\n\n\tlet visited = 1;\n\tconst visitDirectory = (directory: string): boolean => {\n\t\tfor (const child of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tif (child.isSymbolicLink()) return false;\n\t\t\tconst childPath = join(directory, child.name);\n\t\t\tconst entryPath = relative(destination, childPath).split(sep).join(\"/\");\n\t\t\tconst entry = expected.get(entryPath);\n\t\t\tif (!entry) return false;\n\t\t\tvisited++;\n\t\t\tif (visited > expected.size) return false;\n\t\t\tif (entry.type === \"directory\") {\n\t\t\t\tif (!child.isDirectory() || !visitDirectory(childPath)) return false;\n\t\t\t} else if (!child.isFile() || !fileMatches(childPath, entry.data!)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t};\n\n\treturn visitDirectory(destination) && visited === expected.size;\n}\n\nexport function receiveUpload(uploadDir: string, body: unknown): { path: string; files: number } {\n\tconst upload = validateBody(body);\n\tmkdirSync(uploadDir, { recursive: true });\n\tconst destination = join(uploadDir, upload.name);\n\tconst fileCount = upload.entries.filter((entry) => entry.type === \"file\").length;\n\tif (existsSync(destination)) {\n\t\tif (existingDestinationMatches(destination, upload.entries)) {\n\t\t\treturn { path: destination, files: fileCount };\n\t\t}\n\t\tthrow new ReceiveUploadError(409, `destination already exists with different contents: ${destination}`);\n\t}\n\tconst temporaryDir = mkdtempSync(join(uploadDir, \".upload-\"));\n\tconst temporaryRoot = join(temporaryDir, upload.name);\n\ttry {\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"directory\")) {\n\t\t\tmkdirSync(join(temporaryRoot, entry.path), { recursive: true });\n\t\t}\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"file\")) {\n\t\t\tconst path = join(temporaryRoot, entry.path);\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, entry.data!);\n\t\t}\n\t\trenameSync(temporaryRoot, destination);\n\t} finally {\n\t\trmSync(temporaryDir, { recursive: true, force: true });\n\t}\n\treturn { path: destination, files: fileCount };\n}\n"]}
|
package/dist/request-chunks.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const CHUNK_ENDPOINT = "/api/request/chunk";
|
|
2
|
+
export declare const REQUEST_CHUNK_SEGMENT_ENCODING = "base64-segment-v1";
|
|
2
3
|
export interface RequestChunkBody {
|
|
3
4
|
requestId: string;
|
|
4
5
|
target: string;
|
|
@@ -6,6 +7,9 @@ export interface RequestChunkBody {
|
|
|
6
7
|
totalChunks: number;
|
|
7
8
|
sha256: string;
|
|
8
9
|
chunk: string;
|
|
10
|
+
encoding?: string;
|
|
11
|
+
rawTotalBytes?: number;
|
|
12
|
+
rawChunkBytes?: number;
|
|
9
13
|
}
|
|
10
14
|
interface ChunkAck {
|
|
11
15
|
received: true;
|
|
@@ -19,19 +23,48 @@ interface PendingChunkResult {
|
|
|
19
23
|
}
|
|
20
24
|
interface CompleteChunkResult {
|
|
21
25
|
complete: true;
|
|
26
|
+
requestId: string;
|
|
27
|
+
replayed: boolean;
|
|
22
28
|
target: string;
|
|
23
29
|
bodyJson: string;
|
|
24
30
|
}
|
|
25
31
|
export declare const REQUEST_CHUNK_PENDING_TTL_MS: number;
|
|
26
32
|
export declare const REQUEST_CHUNK_MAX_PENDING_BYTES: number;
|
|
33
|
+
export declare const REQUEST_CHUNK_MAX_TOTAL_CHUNKS = 4096;
|
|
34
|
+
export declare const REQUEST_CHUNK_MAX_REQUEST_ID_CHARS = 128;
|
|
35
|
+
export declare const REQUEST_CHUNK_MAX_PENDING_COUNT = 128;
|
|
36
|
+
export declare const REQUEST_CHUNK_MAX_PENDING_METADATA_BYTES: number;
|
|
27
37
|
export declare const REQUEST_CHUNK_COMPLETED_TTL_MS: number;
|
|
38
|
+
export declare const REQUEST_CHUNK_MAX_COMPLETED_COUNT = 128;
|
|
39
|
+
export declare const REQUEST_CHUNK_MAX_COMPLETED_BYTES: number;
|
|
40
|
+
export declare const REQUEST_CHUNK_TOMBSTONE_TTL_MS: number;
|
|
41
|
+
export declare const REQUEST_CHUNK_MAX_TOMBSTONE_COUNT = 8192;
|
|
42
|
+
export declare const REQUEST_CHUNK_MAX_TOMBSTONE_BYTES: number;
|
|
28
43
|
interface ReceiveRequestChunkOptions {
|
|
29
44
|
nowMs?: number;
|
|
30
45
|
pendingTtlMs?: number;
|
|
31
46
|
maxPendingBytes?: number;
|
|
47
|
+
maxPendingCount?: number;
|
|
48
|
+
maxPendingMetadataBytes?: number;
|
|
32
49
|
completedTtlMs?: number;
|
|
50
|
+
maxCompletedCount?: number;
|
|
51
|
+
maxCompletedBytes?: number;
|
|
52
|
+
tombstoneTtlMs?: number;
|
|
53
|
+
maxTombstoneCount?: number;
|
|
54
|
+
maxTombstoneBytes?: number;
|
|
33
55
|
}
|
|
34
56
|
export declare function receiveRequestChunk(body: RequestChunkBody, options?: ReceiveRequestChunkOptions): PendingChunkResult | CompleteChunkResult;
|
|
57
|
+
export declare function getRequestChunkCacheStats(): {
|
|
58
|
+
pendingCount: number;
|
|
59
|
+
pendingBytes: number;
|
|
60
|
+
pendingMetadataBytes: number;
|
|
61
|
+
pendingPreallocatedBuffers: number;
|
|
62
|
+
pendingStoredChunks: number;
|
|
63
|
+
completedCount: number;
|
|
64
|
+
completedBytes: number;
|
|
65
|
+
tombstoneCount: number;
|
|
66
|
+
tombstoneBytes: number;
|
|
67
|
+
};
|
|
35
68
|
export declare function clearAllRequestChunks(): void;
|
|
36
69
|
export {};
|
|
37
70
|
//# sourceMappingURL=request-chunks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-chunks.d.ts","sourceRoot":"","sources":["../src/request-chunks.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,uBAAuB,CAAC;AAgBnD,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAwBD,UAAU,QAAQ;IACjB,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,kBAAkB;IAC3B,QAAQ,EAAE,KAAK,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACd;AAED,UAAU,mBAAmB;IAC5B,QAAQ,EAAE,IAAI,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAMD,eAAO,MAAM,4BAA4B,QAAgB,CAAC;AAC1D,eAAO,MAAM,+BAA+B,QAAmB,CAAC;AAChE,eAAO,MAAM,8BAA8B,QAAY,CAAC;AAExD,UAAU,0BAA0B;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAkED,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,0BAA+B,GACtC,kBAAkB,GAAG,mBAAmB,CAuF1C;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C","sourcesContent":["import { Buffer } from \"node:buffer\";\nimport { createHash } from \"node:crypto\";\n\nexport const CHUNK_ENDPOINT = \"/api/request/chunk\";\n\nconst ALLOWED_TARGETS = new Set([\n\t\"/api/session/init\",\n\t\"/api/session/update\",\n\t\"/api/session/sync\",\n\t\"/api/session/append\",\n\t\"/api/session/tree/sync\",\n\t\"/api/session/tree/append\",\n\t\"/api/session/tree/switch\",\n\t\"/api/session/drop-last-assistant-error\",\n\t\"/api/session/compact\",\n\t\"/api/stream\",\n\t\"/api/receive\",\n]);\n\nexport interface RequestChunkBody {\n\trequestId: string;\n\ttarget: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n\tsha256: string;\n\tchunk: string;\n}\n\ninterface RequestChunk {\n\tchunk: string;\n\tsha256: string;\n}\n\ninterface PendingRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tchunks: Map<number, RequestChunk>;\n\treceivedBytes: number;\n\tupdatedAtMs: number;\n}\n\ninterface CompletedRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tbodyJson: string;\n\tcompletedChunkIndex: number;\n\tcompletedAtMs: number;\n\tchunks: Map<number, RequestChunk>;\n}\n\ninterface ChunkAck {\n\treceived: true;\n\trequestId: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n}\n\ninterface PendingChunkResult {\n\tcomplete: false;\n\tack: ChunkAck;\n}\n\ninterface CompleteChunkResult {\n\tcomplete: true;\n\ttarget: string;\n\tbodyJson: string;\n}\n\nconst pendingRequests = new Map<string, PendingRequest>();\nconst completedRequests = new Map<string, CompletedRequest>();\nlet pendingRequestBytes = 0;\n\nexport const REQUEST_CHUNK_PENDING_TTL_MS = 5 * 60 * 1000;\nexport const REQUEST_CHUNK_MAX_PENDING_BYTES = 64 * 1024 * 1024;\nexport const REQUEST_CHUNK_COMPLETED_TTL_MS = 60 * 1000;\n\ninterface ReceiveRequestChunkOptions {\n\tnowMs?: number;\n\tpendingTtlMs?: number;\n\tmaxPendingBytes?: number;\n\tcompletedTtlMs?: number;\n}\n\nfunction sha256(value: string): string {\n\treturn createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction assertValidChunk(body: RequestChunkBody): void {\n\tif (!body.requestId) throw new Error(\"requestId is required\");\n\tif (!ALLOWED_TARGETS.has(body.target)) throw new Error(`Unsupported chunk target: ${body.target}`);\n\tif (!Number.isInteger(body.totalChunks) || body.totalChunks <= 0) {\n\t\tthrow new Error(\"totalChunks must be a positive integer\");\n\t}\n\tif (!Number.isInteger(body.chunkIndex) || body.chunkIndex < 0 || body.chunkIndex >= body.totalChunks) {\n\t\tthrow new Error(\"chunkIndex must be an integer within the chunk range\");\n\t}\n\tif (typeof body.chunk !== \"string\") throw new Error(\"chunk must be a string\");\n\tif (typeof body.sha256 !== \"string\" || !/^[a-f0-9]{64}$/i.test(body.sha256)) {\n\t\tthrow new Error(\"sha256 must be a 64-character hex string\");\n\t}\n\tif (sha256(body.chunk) !== body.sha256) {\n\t\tthrow new Error(`Chunk checksum mismatch: ${body.chunkIndex}`);\n\t}\n}\n\nfunction makeAck(body: RequestChunkBody): PendingChunkResult {\n\treturn {\n\t\tcomplete: false,\n\t\tack: {\n\t\t\treceived: true,\n\t\t\trequestId: body.requestId,\n\t\t\tchunkIndex: body.chunkIndex,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t},\n\t};\n}\n\nfunction deletePendingRequest(requestId: string, pending: PendingRequest): void {\n\tpendingRequests.delete(requestId);\n\tpendingRequestBytes -= pending.receivedBytes;\n}\n\nfunction cleanupExpiredRequests(nowMs: number, pendingTtlMs: number, completedTtlMs: number): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (nowMs - pending.updatedAtMs > pendingTtlMs) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tfor (const [requestId, completed] of completedRequests) {\n\t\tif (nowMs - completed.completedAtMs > completedTtlMs) {\n\t\t\tcompletedRequests.delete(requestId);\n\t\t}\n\t}\n}\n\nfunction cleanupForPendingBytes(extraBytes: number, maxPendingBytes: number, protectedRequestId: string): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (pendingRequestBytes + extraBytes <= maxPendingBytes) return;\n\t\tif (requestId !== protectedRequestId) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tif (pendingRequestBytes + extraBytes > maxPendingBytes) {\n\t\tthrow new Error(\"Request chunk pending bytes limit exceeded\");\n\t}\n}\n\nexport function receiveRequestChunk(\n\tbody: RequestChunkBody,\n\toptions: ReceiveRequestChunkOptions = {},\n): PendingChunkResult | CompleteChunkResult {\n\tassertValidChunk(body);\n\n\tconst nowMs = options.nowMs ?? Date.now();\n\tcleanupExpiredRequests(\n\t\tnowMs,\n\t\toptions.pendingTtlMs ?? REQUEST_CHUNK_PENDING_TTL_MS,\n\t\toptions.completedTtlMs ?? REQUEST_CHUNK_COMPLETED_TTL_MS,\n\t);\n\n\tconst completed = completedRequests.get(body.requestId);\n\tif (completed) {\n\t\tif (completed.target !== body.target || completed.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the completed request\");\n\t\t}\n\t\tconst existing = completed.chunks.get(body.chunkIndex);\n\t\tif (!existing || existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t}\n\t\tif (body.chunkIndex === completed.completedChunkIndex) {\n\t\t\treturn {\n\t\t\t\tcomplete: true,\n\t\t\t\ttarget: completed.target,\n\t\t\t\tbodyJson: completed.bodyJson,\n\t\t\t};\n\t\t}\n\t\treturn makeAck(body);\n\t}\n\n\tlet pending = pendingRequests.get(body.requestId);\n\tif (pending) {\n\t\tif (pending.target !== body.target || pending.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the pending request\");\n\t\t}\n\t\tconst existing = pending.chunks.get(body.chunkIndex);\n\t\tif (existing) {\n\t\t\tif (existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t\t}\n\t\t\tpending.updatedAtMs = nowMs;\n\t\t\treturn makeAck(body);\n\t\t}\n\t}\n\n\tconst chunkBytes = Buffer.byteLength(body.chunk, \"utf-8\");\n\tcleanupForPendingBytes(chunkBytes, options.maxPendingBytes ?? REQUEST_CHUNK_MAX_PENDING_BYTES, body.requestId);\n\tif (!pending) {\n\t\tpending = {\n\t\t\ttarget: body.target,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t\tchunks: new Map(),\n\t\t\treceivedBytes: 0,\n\t\t\tupdatedAtMs: nowMs,\n\t\t};\n\t\tpendingRequests.set(body.requestId, pending);\n\t}\n\tpending.chunks.set(body.chunkIndex, { chunk: body.chunk, sha256: body.sha256 });\n\tpending.receivedBytes += chunkBytes;\n\tpending.updatedAtMs = nowMs;\n\tpendingRequestBytes += chunkBytes;\n\n\tif (pending.chunks.size !== pending.totalChunks) {\n\t\treturn makeAck(body);\n\t}\n\n\tconst encodedChunks: string[] = [];\n\tfor (let index = 0; index < pending.totalChunks; index++) {\n\t\tconst chunk = pending.chunks.get(index);\n\t\tif (chunk === undefined) throw new Error(`Missing chunk index: ${index}`);\n\t\tencodedChunks.push(chunk.chunk);\n\t}\n\n\tdeletePendingRequest(body.requestId, pending);\n\tconst bodyJson = Buffer.from(encodedChunks.join(\"\"), \"base64\").toString(\"utf-8\");\n\tcompletedRequests.set(body.requestId, {\n\t\ttarget: pending.target,\n\t\ttotalChunks: pending.totalChunks,\n\t\tbodyJson,\n\t\tcompletedChunkIndex: body.chunkIndex,\n\t\tcompletedAtMs: nowMs,\n\t\tchunks: pending.chunks,\n\t});\n\treturn {\n\t\tcomplete: true,\n\t\ttarget: pending.target,\n\t\tbodyJson,\n\t};\n}\n\nexport function clearAllRequestChunks(): void {\n\tpendingRequests.clear();\n\tcompletedRequests.clear();\n\tpendingRequestBytes = 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"request-chunks.d.ts","sourceRoot":"","sources":["../src/request-chunks.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,uBAAuB,CAAC;AACnD,eAAO,MAAM,8BAA8B,sBAAsB,CAAC;AAoBlE,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAqED,UAAU,QAAQ;IACjB,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,kBAAkB;IAC3B,QAAQ,EAAE,KAAK,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACd;AAED,UAAU,mBAAmB;IAC5B,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAYD,eAAO,MAAM,4BAA4B,QAAgB,CAAC;AAG1D,eAAO,MAAM,+BAA+B,QAAoB,CAAC;AACjE,eAAO,MAAM,8BAA8B,OAAO,CAAC;AACnD,eAAO,MAAM,kCAAkC,MAAM,CAAC;AACtD,eAAO,MAAM,+BAA+B,MAAM,CAAC;AACnD,eAAO,MAAM,wCAAwC,QAAmB,CAAC;AACzE,eAAO,MAAM,8BAA8B,QAAY,CAAC;AACxD,eAAO,MAAM,iCAAiC,MAAM,CAAC;AACrD,eAAO,MAAM,iCAAiC,QAAoB,CAAC;AACnE,eAAO,MAAM,8BAA8B,QAAqB,CAAC;AACjE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD,eAAO,MAAM,iCAAiC,QAAkB,CAAC;AAKjE,UAAU,0BAA0B;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAidD,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,0BAA+B,GACtC,kBAAkB,GAAG,mBAAmB,CA+O1C;AAED,wBAAgB,yBAAyB,IAAI;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,0BAA0B,EAAE,MAAM,CAAC;IACnC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACvB,CAqBA;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CAa5C","sourcesContent":["import { Buffer } from \"node:buffer\";\nimport { createHash } from \"node:crypto\";\nimport { TextDecoder } from \"node:util\";\n\nexport const CHUNK_ENDPOINT = \"/api/request/chunk\";\nexport const REQUEST_CHUNK_SEGMENT_ENCODING = \"base64-segment-v1\";\nconst LEGACY_REQUEST_CHUNK_ENCODING = \"legacy-base64-stream\";\nconst UTF8_DECODER = new TextDecoder(\"utf-8\", { fatal: true });\n\nconst ALLOWED_TARGETS = new Set([\n\t\"/api/session/init\",\n\t\"/api/session/update\",\n\t\"/api/session/sync\",\n\t\"/api/session/append\",\n\t\"/api/session/tree/sync\",\n\t\"/api/session/tree/append\",\n\t\"/api/session/tree/switch\",\n\t\"/api/session/drop-last-assistant-error\",\n\t\"/api/session/run/cancel\",\n\t\"/api/session/compact\",\n\t\"/api/session/compact/cancel\",\n\t\"/api/stream\",\n\t\"/api/receive\",\n]);\n\nexport interface RequestChunkBody {\n\trequestId: string;\n\ttarget: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n\tsha256: string;\n\tchunk: string;\n\tencoding?: string;\n\trawTotalBytes?: number;\n\trawChunkBytes?: number;\n}\n\ntype RequestChunkEncoding = typeof LEGACY_REQUEST_CHUNK_ENCODING | typeof REQUEST_CHUNK_SEGMENT_ENCODING;\n\ntype RequestChunk =\n\t| {\n\t\t\tencoding: typeof LEGACY_REQUEST_CHUNK_ENCODING;\n\t\t\tencoded: string;\n\t\t\tsha256: string;\n\t\t\tretainedBytes: number;\n\t }\n\t| {\n\t\t\tencoding: typeof REQUEST_CHUNK_SEGMENT_ENCODING;\n\t\t\tdecoded: Buffer;\n\t\t\tsha256: string;\n\t\t\tretainedBytes: number;\n\t };\n\ninterface PendingRequestBase {\n\ttarget: string;\n\ttotalChunks: number;\n\tencoding: RequestChunkEncoding;\n\treceivedBytes: number;\n\tallocatedBytes: number;\n\tmetadataBytes: number;\n\ttombstoneBytes: number;\n\texpiresAtMs: number;\n}\n\ninterface StoredPendingRequest extends PendingRequestBase {\n\tlayout: \"stored\";\n\tchunks: Map<number, RequestChunk>;\n}\n\ninterface OffsetPendingRequest extends PendingRequestBase {\n\tlayout: \"offset\";\n\trawTotalBytes: number;\n\trawChunkBytes: number;\n\trawBody: Buffer;\n\treceivedChunks: Uint8Array;\n\tchunkHashes: Array<string | undefined>;\n\treceivedChunkCount: number;\n}\n\ntype PendingRequest = StoredPendingRequest | OffsetPendingRequest;\n\ninterface CompletedRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tencoding: RequestChunkEncoding;\n\trawTotalBytes?: number;\n\trawChunkBytes?: number;\n\tbodyJson: string;\n\tcompletedChunkIndex: number;\n\tchunkHashes: ReadonlyArray<string>;\n\tretainedBytes: number;\n\texpiresAtMs: number;\n}\n\ninterface CompletedRequestTombstone {\n\ttarget: string;\n\ttotalChunks: number;\n\tencoding: RequestChunkEncoding;\n\trawTotalBytes?: number;\n\trawChunkBytes?: number;\n\tretainedBytes: number;\n\texpiresAtMs: number;\n}\n\ninterface ChunkAck {\n\treceived: true;\n\trequestId: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n}\n\ninterface PendingChunkResult {\n\tcomplete: false;\n\tack: ChunkAck;\n}\n\ninterface CompleteChunkResult {\n\tcomplete: true;\n\trequestId: string;\n\treplayed: boolean;\n\ttarget: string;\n\tbodyJson: string;\n}\n\nconst pendingRequests = new Map<string, PendingRequest>();\nconst completedRequests = new Map<string, CompletedRequest>();\nconst completedRequestTombstones = new Map<string, CompletedRequestTombstone>();\nlet pendingRequestBytes = 0;\nlet pendingRequestMetadataBytes = 0;\nlet pendingRequestTombstoneReservationBytes = 0;\nlet completedRequestBytes = 0;\nlet completedRequestTombstoneBytes = 0;\nlet cleanupTimer: ReturnType<typeof setTimeout> | undefined;\n\nexport const REQUEST_CHUNK_PENDING_TTL_MS = 5 * 60 * 1000;\n// Chunk transport base64-encodes the JSON body, whose file payloads may already\n// be base64. Keep enough bounded headroom for the 64 MiB decoded receive limit.\nexport const REQUEST_CHUNK_MAX_PENDING_BYTES = 256 * 1024 * 1024;\nexport const REQUEST_CHUNK_MAX_TOTAL_CHUNKS = 4096;\nexport const REQUEST_CHUNK_MAX_REQUEST_ID_CHARS = 128;\nexport const REQUEST_CHUNK_MAX_PENDING_COUNT = 128;\nexport const REQUEST_CHUNK_MAX_PENDING_METADATA_BYTES = 32 * 1024 * 1024;\nexport const REQUEST_CHUNK_COMPLETED_TTL_MS = 60 * 1000;\nexport const REQUEST_CHUNK_MAX_COMPLETED_COUNT = 128;\nexport const REQUEST_CHUNK_MAX_COMPLETED_BYTES = 256 * 1024 * 1024;\nexport const REQUEST_CHUNK_TOMBSTONE_TTL_MS = 6 * 60 * 60 * 1000;\nexport const REQUEST_CHUNK_MAX_TOMBSTONE_COUNT = 8192;\nexport const REQUEST_CHUNK_MAX_TOMBSTONE_BYTES = 4 * 1024 * 1024;\n\nconst REQUEST_CHUNK_PENDING_BASE_METADATA_BYTES = 256;\nconst REQUEST_CHUNK_PENDING_PER_CHUNK_METADATA_BYTES = 192;\n\ninterface ReceiveRequestChunkOptions {\n\tnowMs?: number;\n\tpendingTtlMs?: number;\n\tmaxPendingBytes?: number;\n\tmaxPendingCount?: number;\n\tmaxPendingMetadataBytes?: number;\n\tcompletedTtlMs?: number;\n\tmaxCompletedCount?: number;\n\tmaxCompletedBytes?: number;\n\ttombstoneTtlMs?: number;\n\tmaxTombstoneCount?: number;\n\tmaxTombstoneBytes?: number;\n}\n\nfunction sha256(value: string): string {\n\treturn createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction base64Value(characterCode: number): number {\n\tif (characterCode >= 65 && characterCode <= 90) return characterCode - 65;\n\tif (characterCode >= 97 && characterCode <= 122) return characterCode - 71;\n\tif (characterCode >= 48 && characterCode <= 57) return characterCode + 4;\n\tif (characterCode === 43) return 62;\n\tif (characterCode === 47) return 63;\n\treturn -1;\n}\n\nfunction getCanonicalBase64DecodedBytes(value: string, label: string): number {\n\tif (value.length % 4 !== 0) {\n\t\tthrow new Error(`${label} must be canonical padded base64`);\n\t}\n\tlet padding = 0;\n\tif (value.endsWith(\"==\")) {\n\t\tpadding = 2;\n\t} else if (value.endsWith(\"=\")) {\n\t\tpadding = 1;\n\t}\n\tconst dataLength = value.length - padding;\n\tfor (let index = 0; index < dataLength; index++) {\n\t\tif (base64Value(value.charCodeAt(index)) < 0) {\n\t\t\tthrow new Error(`${label} must be canonical padded base64`);\n\t\t}\n\t}\n\tfor (let index = dataLength; index < value.length; index++) {\n\t\tif (value.charCodeAt(index) !== 61) {\n\t\t\tthrow new Error(`${label} must be canonical padded base64`);\n\t\t}\n\t}\n\tif (\n\t\t(padding === 0 && dataLength % 4 !== 0) ||\n\t\t(padding === 1 && (dataLength % 4 !== 3 || (base64Value(value.charCodeAt(dataLength - 1)) & 0b11) !== 0)) ||\n\t\t(padding === 2 && (dataLength % 4 !== 2 || (base64Value(value.charCodeAt(dataLength - 1)) & 0b1111) !== 0))\n\t) {\n\t\tthrow new Error(`${label} must be canonical padded base64`);\n\t}\n\treturn (value.length / 4) * 3 - padding;\n}\n\nfunction decodeCanonicalBase64(value: string, label: string): Buffer {\n\tgetCanonicalBase64DecodedBytes(value, label);\n\treturn Buffer.from(value, \"base64\");\n}\n\nfunction decodeRequestBody(rawBody: Buffer): string {\n\ttry {\n\t\treturn UTF8_DECODER.decode(rawBody);\n\t} catch {\n\t\tthrow new Error(\"Decoded request body must be valid UTF-8\");\n\t}\n}\n\ntype ValidatedChunk =\n\t| {\n\t\t\tencoding: typeof LEGACY_REQUEST_CHUNK_ENCODING;\n\t\t\tlayout: \"stored\";\n\t\t\tchunk: RequestChunk;\n\t }\n\t| {\n\t\t\tencoding: typeof REQUEST_CHUNK_SEGMENT_ENCODING;\n\t\t\tlayout: \"stored\";\n\t\t\tchunk: RequestChunk;\n\t }\n\t| {\n\t\t\tencoding: typeof REQUEST_CHUNK_SEGMENT_ENCODING;\n\t\t\tlayout: \"offset\";\n\t\t\tdecodedBytes: number;\n\t\t\trawTotalBytes: number;\n\t\t\trawChunkBytes: number;\n\t\t\toffset: number;\n\t\t\tsha256: string;\n\t\t\tencoded: string;\n\t };\n\nfunction assertValidChunk(body: RequestChunkBody): ValidatedChunk {\n\tif (!body.requestId) throw new Error(\"requestId is required\");\n\tif (typeof body.requestId !== \"string\" || body.requestId.length > REQUEST_CHUNK_MAX_REQUEST_ID_CHARS) {\n\t\tthrow new Error(`requestId must not exceed ${REQUEST_CHUNK_MAX_REQUEST_ID_CHARS} characters`);\n\t}\n\tif (!ALLOWED_TARGETS.has(body.target)) throw new Error(`Unsupported chunk target: ${body.target}`);\n\tif (\n\t\t!Number.isInteger(body.totalChunks) ||\n\t\tbody.totalChunks <= 0 ||\n\t\tbody.totalChunks > REQUEST_CHUNK_MAX_TOTAL_CHUNKS\n\t) {\n\t\tthrow new Error(`totalChunks must be an integer from 1 to ${REQUEST_CHUNK_MAX_TOTAL_CHUNKS}`);\n\t}\n\tif (!Number.isInteger(body.chunkIndex) || body.chunkIndex < 0 || body.chunkIndex >= body.totalChunks) {\n\t\tthrow new Error(\"chunkIndex must be an integer within the chunk range\");\n\t}\n\tif (typeof body.chunk !== \"string\") throw new Error(\"chunk must be a string\");\n\tif (body.chunk.length === 0) throw new Error(\"chunk must not be empty\");\n\tif (typeof body.sha256 !== \"string\" || !/^[a-f0-9]{64}$/.test(body.sha256)) {\n\t\tthrow new Error(\"sha256 must be a canonical lowercase 64-character hex string\");\n\t}\n\tif (sha256(body.chunk) !== body.sha256) {\n\t\tthrow new Error(`Chunk checksum mismatch: ${body.chunkIndex}`);\n\t}\n\tif (body.encoding === undefined) {\n\t\tif (body.rawTotalBytes !== undefined || body.rawChunkBytes !== undefined) {\n\t\t\tthrow new Error(\"raw chunk layout metadata requires base64-segment-v1 encoding\");\n\t\t}\n\t\treturn {\n\t\t\tencoding: LEGACY_REQUEST_CHUNK_ENCODING,\n\t\t\tlayout: \"stored\",\n\t\t\tchunk: {\n\t\t\t\tencoding: LEGACY_REQUEST_CHUNK_ENCODING,\n\t\t\t\tencoded: body.chunk,\n\t\t\t\tsha256: body.sha256,\n\t\t\t\tretainedBytes: Buffer.byteLength(body.chunk, \"utf-8\"),\n\t\t\t},\n\t\t};\n\t}\n\tif (body.encoding !== REQUEST_CHUNK_SEGMENT_ENCODING) {\n\t\tthrow new Error(`Unsupported chunk encoding: ${String(body.encoding)}`);\n\t}\n\tconst decodedBytes = getCanonicalBase64DecodedBytes(body.chunk, `Chunk ${body.chunkIndex}`);\n\tif (body.rawTotalBytes === undefined && body.rawChunkBytes === undefined) {\n\t\tconst decoded = Buffer.from(body.chunk, \"base64\");\n\t\treturn {\n\t\t\tencoding: REQUEST_CHUNK_SEGMENT_ENCODING,\n\t\t\tlayout: \"stored\",\n\t\t\tchunk: {\n\t\t\t\tencoding: REQUEST_CHUNK_SEGMENT_ENCODING,\n\t\t\t\tdecoded,\n\t\t\t\tsha256: body.sha256,\n\t\t\t\tretainedBytes: decoded.byteLength,\n\t\t\t},\n\t\t};\n\t}\n\tif (body.rawTotalBytes === undefined || body.rawChunkBytes === undefined) {\n\t\tthrow new Error(\"rawTotalBytes and rawChunkBytes must be supplied together\");\n\t}\n\tif (\n\t\t!Number.isSafeInteger(body.rawTotalBytes) ||\n\t\tbody.rawTotalBytes <= 0 ||\n\t\tbody.rawTotalBytes > REQUEST_CHUNK_MAX_PENDING_BYTES\n\t) {\n\t\tthrow new Error(`rawTotalBytes must be an integer from 1 to ${REQUEST_CHUNK_MAX_PENDING_BYTES}`);\n\t}\n\tif (\n\t\t!Number.isSafeInteger(body.rawChunkBytes) ||\n\t\tbody.rawChunkBytes <= 0 ||\n\t\tbody.rawChunkBytes > body.rawTotalBytes\n\t) {\n\t\tthrow new Error(\"rawChunkBytes must be an integer within the raw request body\");\n\t}\n\tif (Math.ceil(body.rawTotalBytes / body.rawChunkBytes) !== body.totalChunks) {\n\t\tthrow new Error(\"totalChunks does not match the raw chunk layout\");\n\t}\n\tconst offset = body.chunkIndex * body.rawChunkBytes;\n\tconst expectedBytes = Math.min(body.rawChunkBytes, body.rawTotalBytes - offset);\n\tif (expectedBytes <= 0 || decodedBytes !== expectedBytes) {\n\t\tthrow new Error(`Chunk ${body.chunkIndex} byte length does not match the raw chunk layout`);\n\t}\n\treturn {\n\t\tencoding: REQUEST_CHUNK_SEGMENT_ENCODING,\n\t\tlayout: \"offset\",\n\t\tdecodedBytes,\n\t\trawTotalBytes: body.rawTotalBytes,\n\t\trawChunkBytes: body.rawChunkBytes,\n\t\toffset,\n\t\tsha256: body.sha256,\n\t\tencoded: body.chunk,\n\t};\n}\n\nfunction makeAck(body: RequestChunkBody): PendingChunkResult {\n\treturn {\n\t\tcomplete: false,\n\t\tack: {\n\t\t\treceived: true,\n\t\t\trequestId: body.requestId,\n\t\t\tchunkIndex: body.chunkIndex,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t},\n\t};\n}\n\nfunction deletePendingRequest(requestId: string, pending: PendingRequest): void {\n\tpendingRequests.delete(requestId);\n\tpendingRequestBytes -= pending.allocatedBytes;\n\tpendingRequestMetadataBytes -= pending.metadataBytes;\n\tpendingRequestTombstoneReservationBytes -= pending.tombstoneBytes;\n}\n\nfunction deleteCompletedRequest(requestId: string, completed: CompletedRequest): void {\n\tcompletedRequests.delete(requestId);\n\tcompletedRequestBytes -= completed.retainedBytes;\n}\n\nfunction deleteCompletedRequestTombstone(requestId: string, tombstone: CompletedRequestTombstone): void {\n\tcompletedRequestTombstones.delete(requestId);\n\tcompletedRequestTombstoneBytes -= tombstone.retainedBytes;\n}\n\nfunction cleanupExpiredRequests(nowMs: number): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (nowMs >= pending.expiresAtMs) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tfor (const [requestId, completed] of completedRequests) {\n\t\tif (nowMs >= completed.expiresAtMs) {\n\t\t\tdeleteCompletedRequest(requestId, completed);\n\t\t}\n\t}\n\tfor (const [requestId, tombstone] of completedRequestTombstones) {\n\t\tif (nowMs >= tombstone.expiresAtMs) {\n\t\t\tdeleteCompletedRequestTombstone(requestId, tombstone);\n\t\t}\n\t}\n}\n\nfunction scheduleCleanupTimer(nowMs = Date.now()): void {\n\tif (cleanupTimer !== undefined) {\n\t\tclearTimeout(cleanupTimer);\n\t\tcleanupTimer = undefined;\n\t}\n\n\tlet nextExpiryMs = Number.POSITIVE_INFINITY;\n\tfor (const pending of pendingRequests.values()) {\n\t\tnextExpiryMs = Math.min(nextExpiryMs, pending.expiresAtMs);\n\t}\n\tfor (const completed of completedRequests.values()) {\n\t\tnextExpiryMs = Math.min(nextExpiryMs, completed.expiresAtMs);\n\t}\n\tfor (const tombstone of completedRequestTombstones.values()) {\n\t\tnextExpiryMs = Math.min(nextExpiryMs, tombstone.expiresAtMs);\n\t}\n\tif (!Number.isFinite(nextExpiryMs)) return;\n\n\tcleanupTimer = setTimeout(\n\t\t() => {\n\t\t\tcleanupTimer = undefined;\n\t\t\tcleanupExpiredRequests(Date.now());\n\t\t\tscheduleCleanupTimer();\n\t\t},\n\t\tMath.max(0, nextExpiryMs - nowMs),\n\t);\n\tcleanupTimer.unref();\n}\n\nfunction assertPositiveLimit(value: number, label: string): void {\n\tif (!Number.isSafeInteger(value) || value <= 0) {\n\t\tthrow new Error(`${label} must be a positive safe integer`);\n\t}\n}\n\nfunction calculatePendingMetadataBytes(body: RequestChunkBody): number {\n\treturn (\n\t\tREQUEST_CHUNK_PENDING_BASE_METADATA_BYTES +\n\t\tbody.totalChunks * REQUEST_CHUNK_PENDING_PER_CHUNK_METADATA_BYTES +\n\t\tBuffer.byteLength(body.requestId, \"utf-8\") +\n\t\tBuffer.byteLength(body.target, \"utf-8\") +\n\t\tBuffer.byteLength(body.encoding ?? LEGACY_REQUEST_CHUNK_ENCODING, \"utf-8\")\n\t);\n}\n\nfunction calculateTombstoneBytes(body: RequestChunkBody): number {\n\treturn (\n\t\t192 +\n\t\tBuffer.byteLength(body.requestId, \"utf-8\") +\n\t\tBuffer.byteLength(body.target, \"utf-8\") +\n\t\tBuffer.byteLength(body.encoding ?? LEGACY_REQUEST_CHUNK_ENCODING, \"utf-8\")\n\t);\n}\n\nfunction cleanupForPendingCapacity(\n\textraBytes: number,\n\textraMetadataBytes: number,\n\textraCount: number,\n\tmaxPendingBytes: number,\n\tmaxPendingMetadataBytes: number,\n\tmaxPendingCount: number,\n\tprotectedRequestId: string,\n\tprotectedBytesAfterAdmission: number,\n\tprotectedMetadataBytesAfterAdmission: number,\n): void {\n\tassertPositiveLimit(maxPendingBytes, \"maxPendingBytes\");\n\tassertPositiveLimit(maxPendingMetadataBytes, \"maxPendingMetadataBytes\");\n\tassertPositiveLimit(maxPendingCount, \"maxPendingCount\");\n\tif (protectedBytesAfterAdmission > maxPendingBytes) {\n\t\tthrow new Error(\"Request chunk pending bytes limit exceeded\");\n\t}\n\tif (protectedMetadataBytesAfterAdmission > maxPendingMetadataBytes) {\n\t\tthrow new Error(\"Request chunk pending metadata limit exceeded\");\n\t}\n\tif (extraCount > maxPendingCount) {\n\t\tthrow new Error(\"Request chunk pending count limit exceeded\");\n\t}\n\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (\n\t\t\tpendingRequestBytes + extraBytes <= maxPendingBytes &&\n\t\t\tpendingRequestMetadataBytes + extraMetadataBytes <= maxPendingMetadataBytes &&\n\t\t\tpendingRequests.size + extraCount <= maxPendingCount\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tif (requestId !== protectedRequestId) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tif (pendingRequestBytes + extraBytes > maxPendingBytes) {\n\t\tthrow new Error(\"Request chunk pending bytes limit exceeded\");\n\t}\n\tif (pendingRequestMetadataBytes + extraMetadataBytes > maxPendingMetadataBytes) {\n\t\tthrow new Error(\"Request chunk pending metadata limit exceeded\");\n\t}\n\tif (pendingRequests.size + extraCount > maxPendingCount) {\n\t\tthrow new Error(\"Request chunk pending count limit exceeded\");\n\t}\n}\n\nfunction cleanupForTombstoneReservation(\n\textraCount: number,\n\textraBytes: number,\n\tmaxTombstoneCount: number,\n\tmaxTombstoneBytes: number,\n\tprotectedRequestId: string,\n): void {\n\tassertPositiveLimit(maxTombstoneCount, \"maxTombstoneCount\");\n\tassertPositiveLimit(maxTombstoneBytes, \"maxTombstoneBytes\");\n\tif (extraCount > maxTombstoneCount || extraBytes > maxTombstoneBytes) {\n\t\tthrow new Error(\"Request chunk completed tombstone limit exceeded\");\n\t}\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (\n\t\t\tcompletedRequestTombstones.size + pendingRequests.size + extraCount <= maxTombstoneCount &&\n\t\t\tcompletedRequestTombstoneBytes + pendingRequestTombstoneReservationBytes + extraBytes <= maxTombstoneBytes\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tif (requestId !== protectedRequestId) deletePendingRequest(requestId, pending);\n\t}\n\tif (\n\t\tcompletedRequestTombstones.size + pendingRequests.size + extraCount > maxTombstoneCount ||\n\t\tcompletedRequestTombstoneBytes + pendingRequestTombstoneReservationBytes + extraBytes > maxTombstoneBytes\n\t) {\n\t\tthrow new Error(\"Request chunk completed tombstone limit exceeded\");\n\t}\n}\n\nfunction cacheCompletedRequest(\n\trequestId: string,\n\tcompleted: CompletedRequest,\n\tmaxCompletedCount: number,\n\tmaxCompletedBytes: number,\n): void {\n\tif (maxCompletedCount <= 0 || completed.retainedBytes > maxCompletedBytes) return;\n\n\tfor (const [completedRequestId, existing] of completedRequests) {\n\t\tif (\n\t\t\tcompletedRequests.size < maxCompletedCount &&\n\t\t\tcompletedRequestBytes + completed.retainedBytes <= maxCompletedBytes\n\t\t) {\n\t\t\tbreak;\n\t\t}\n\t\tdeleteCompletedRequest(completedRequestId, existing);\n\t}\n\tif (\n\t\tcompletedRequests.size >= maxCompletedCount ||\n\t\tcompletedRequestBytes + completed.retainedBytes > maxCompletedBytes\n\t) {\n\t\treturn;\n\t}\n\n\tcompletedRequests.set(requestId, completed);\n\tcompletedRequestBytes += completed.retainedBytes;\n}\n\nfunction cacheCompletedRequestTombstone(\n\trequestId: string,\n\ttombstone: CompletedRequestTombstone,\n\tmaxTombstoneCount: number,\n\tmaxTombstoneBytes: number,\n): void {\n\tassertPositiveLimit(maxTombstoneCount, \"maxTombstoneCount\");\n\tassertPositiveLimit(maxTombstoneBytes, \"maxTombstoneBytes\");\n\tif (\n\t\tcompletedRequestTombstones.size >= maxTombstoneCount ||\n\t\tcompletedRequestTombstoneBytes + tombstone.retainedBytes > maxTombstoneBytes\n\t) {\n\t\tthrow new Error(\"Request chunk completed tombstone limit exceeded\");\n\t}\n\tcompletedRequestTombstones.set(requestId, tombstone);\n\tcompletedRequestTombstoneBytes += tombstone.retainedBytes;\n}\n\nfunction requestChunksEqual(left: RequestChunk, right: RequestChunk): boolean {\n\tif (left.encoding !== right.encoding || left.sha256 !== right.sha256) return false;\n\tif (left.encoding === LEGACY_REQUEST_CHUNK_ENCODING && right.encoding === LEGACY_REQUEST_CHUNK_ENCODING) {\n\t\treturn left.encoded === right.encoded;\n\t}\n\tif (left.encoding === REQUEST_CHUNK_SEGMENT_ENCODING && right.encoding === REQUEST_CHUNK_SEGMENT_ENCODING) {\n\t\treturn left.decoded.equals(right.decoded);\n\t}\n\treturn false;\n}\n\nfunction requestLayoutMatches(\n\trequest: Pick<PendingRequest, \"encoding\" | \"layout\"> | CompletedRequest | CompletedRequestTombstone,\n\tvalidated: ValidatedChunk,\n): boolean {\n\tconst requestUsesOffsetLayout =\n\t\t\"rawTotalBytes\" in request && request.rawTotalBytes !== undefined && request.rawChunkBytes !== undefined;\n\tif (validated.layout === \"offset\") {\n\t\treturn (\n\t\t\trequest.encoding === validated.encoding &&\n\t\t\trequestUsesOffsetLayout &&\n\t\t\trequest.rawTotalBytes === validated.rawTotalBytes &&\n\t\t\trequest.rawChunkBytes === validated.rawChunkBytes\n\t\t);\n\t}\n\treturn request.encoding === validated.encoding && !requestUsesOffsetLayout;\n}\n\nfunction assembleBodyJson(pending: PendingRequest): string {\n\tif (pending.layout === \"offset\") {\n\t\tif (pending.receivedBytes !== pending.rawTotalBytes) {\n\t\t\tthrow new Error(\"Decoded request chunk byte count did not match the raw chunk layout\");\n\t\t}\n\t\tconst rawBody = pending.rawBody;\n\t\tpending.rawBody = Buffer.alloc(0);\n\t\treturn decodeRequestBody(rawBody);\n\t}\n\n\tif (pending.encoding === LEGACY_REQUEST_CHUNK_ENCODING) {\n\t\tconst encodedChunks: string[] = [];\n\t\tfor (let index = 0; index < pending.totalChunks; index++) {\n\t\t\tconst chunk = pending.chunks.get(index);\n\t\t\tif (!chunk || chunk.encoding !== LEGACY_REQUEST_CHUNK_ENCODING) {\n\t\t\t\tthrow new Error(`Missing legacy chunk index: ${index}`);\n\t\t\t}\n\t\t\tencodedChunks.push(chunk.encoded);\n\t\t\tpending.chunks.delete(index);\n\t\t}\n\t\treturn decodeRequestBody(decodeCanonicalBase64(encodedChunks.join(\"\"), \"Legacy request body\"));\n\t}\n\n\tconst rawBody = Buffer.allocUnsafe(pending.receivedBytes);\n\tlet offset = 0;\n\tfor (let index = 0; index < pending.totalChunks; index++) {\n\t\tconst chunk = pending.chunks.get(index);\n\t\tif (!chunk || chunk.encoding !== REQUEST_CHUNK_SEGMENT_ENCODING) {\n\t\t\tthrow new Error(`Missing ${REQUEST_CHUNK_SEGMENT_ENCODING} chunk index: ${index}`);\n\t\t}\n\t\tchunk.decoded.copy(rawBody, offset);\n\t\toffset += chunk.decoded.byteLength;\n\t\tpending.chunks.delete(index);\n\t}\n\tif (offset !== rawBody.byteLength) {\n\t\tthrow new Error(\"Decoded request chunk byte count did not match pending accounting\");\n\t}\n\treturn decodeRequestBody(rawBody);\n}\n\nexport function receiveRequestChunk(\n\tbody: RequestChunkBody,\n\toptions: ReceiveRequestChunkOptions = {},\n): PendingChunkResult | CompleteChunkResult {\n\tconst validated = assertValidChunk(body);\n\n\tconst nowMs = options.nowMs ?? Date.now();\n\tconst pendingTtlMs = options.pendingTtlMs ?? REQUEST_CHUNK_PENDING_TTL_MS;\n\tconst completedTtlMs = options.completedTtlMs ?? REQUEST_CHUNK_COMPLETED_TTL_MS;\n\tconst tombstoneTtlMs = options.tombstoneTtlMs ?? REQUEST_CHUNK_TOMBSTONE_TTL_MS;\n\tassertPositiveLimit(pendingTtlMs, \"pendingTtlMs\");\n\tassertPositiveLimit(completedTtlMs, \"completedTtlMs\");\n\tassertPositiveLimit(tombstoneTtlMs, \"tombstoneTtlMs\");\n\tcleanupExpiredRequests(nowMs);\n\n\tconst completed = completedRequests.get(body.requestId);\n\tif (completed) {\n\t\tif (\n\t\t\tcompleted.target !== body.target ||\n\t\t\tcompleted.totalChunks !== body.totalChunks ||\n\t\t\t!requestLayoutMatches(completed, validated)\n\t\t) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the completed request\");\n\t\t}\n\t\tconst existingHash = completed.chunkHashes[body.chunkIndex];\n\t\tif (existingHash !== body.sha256) {\n\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t}\n\t\tif (body.chunkIndex === completed.completedChunkIndex) {\n\t\t\treturn {\n\t\t\t\tcomplete: true,\n\t\t\t\trequestId: body.requestId,\n\t\t\t\treplayed: true,\n\t\t\t\ttarget: completed.target,\n\t\t\t\tbodyJson: completed.bodyJson,\n\t\t\t};\n\t\t}\n\t\treturn makeAck(body);\n\t}\n\n\tconst tombstone = completedRequestTombstones.get(body.requestId);\n\tif (tombstone) {\n\t\tif (\n\t\t\ttombstone.target !== body.target ||\n\t\t\ttombstone.totalChunks !== body.totalChunks ||\n\t\t\t!requestLayoutMatches(tombstone, validated)\n\t\t) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the completed request tombstone\");\n\t\t}\n\t\tthrow new Error(\"Chunk request already completed; target response is no longer replayable\");\n\t}\n\n\tlet pending = pendingRequests.get(body.requestId);\n\tif (pending) {\n\t\tif (\n\t\t\tpending.target !== body.target ||\n\t\t\tpending.totalChunks !== body.totalChunks ||\n\t\t\t!requestLayoutMatches(pending, validated)\n\t\t) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the pending request\");\n\t\t}\n\t\tif (pending.layout === \"offset\" && validated.layout === \"offset\") {\n\t\t\tconst existingHash = pending.chunkHashes[body.chunkIndex];\n\t\t\tif (existingHash !== undefined) {\n\t\t\t\tif (existingHash !== body.sha256) {\n\t\t\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t\t\t}\n\t\t\t\tpending.expiresAtMs = nowMs + pendingTtlMs;\n\t\t\t\tscheduleCleanupTimer(nowMs);\n\t\t\t\treturn makeAck(body);\n\t\t\t}\n\t\t} else if (pending.layout === \"stored\" && validated.layout === \"stored\") {\n\t\t\tconst existing = pending.chunks.get(body.chunkIndex);\n\t\t\tif (existing && !requestChunksEqual(existing, validated.chunk)) {\n\t\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t\t}\n\t\t\tif (existing) {\n\t\t\t\tpending.expiresAtMs = nowMs + pendingTtlMs;\n\t\t\t\tscheduleCleanupTimer(nowMs);\n\t\t\t\treturn makeAck(body);\n\t\t\t}\n\t\t} else {\n\t\t\tthrow new Error(\"Chunk layout does not match the pending request\");\n\t\t}\n\t}\n\n\tconst maxPendingBytes = options.maxPendingBytes ?? REQUEST_CHUNK_MAX_PENDING_BYTES;\n\tconst maxPendingMetadataBytes = options.maxPendingMetadataBytes ?? REQUEST_CHUNK_MAX_PENDING_METADATA_BYTES;\n\tconst maxPendingCount = options.maxPendingCount ?? REQUEST_CHUNK_MAX_PENDING_COUNT;\n\tconst maxTombstoneCount = options.maxTombstoneCount ?? REQUEST_CHUNK_MAX_TOMBSTONE_COUNT;\n\tconst maxTombstoneBytes = options.maxTombstoneBytes ?? REQUEST_CHUNK_MAX_TOMBSTONE_BYTES;\n\tconst metadataBytes = pending?.metadataBytes ?? calculatePendingMetadataBytes(body);\n\tconst tombstoneBytes = pending?.tombstoneBytes ?? calculateTombstoneBytes(body);\n\tconst chunkBytes = pending?.layout === \"offset\" || validated.layout === \"offset\" ? 0 : validated.chunk.retainedBytes;\n\tconst allocatedBytes = pending?.allocatedBytes ?? (validated.layout === \"offset\" ? validated.rawTotalBytes : 0);\n\tconst protectedBytesAfterAdmission = allocatedBytes + chunkBytes;\n\tcleanupForPendingCapacity(\n\t\tpending ? chunkBytes : allocatedBytes + chunkBytes,\n\t\tpending ? 0 : metadataBytes,\n\t\tpending ? 0 : 1,\n\t\tmaxPendingBytes,\n\t\tmaxPendingMetadataBytes,\n\t\tmaxPendingCount,\n\t\tbody.requestId,\n\t\tprotectedBytesAfterAdmission,\n\t\tmetadataBytes,\n\t);\n\tcleanupForTombstoneReservation(\n\t\tpending ? 0 : 1,\n\t\tpending ? 0 : tombstoneBytes,\n\t\tmaxTombstoneCount,\n\t\tmaxTombstoneBytes,\n\t\tbody.requestId,\n\t);\n\tif (!pending) {\n\t\tif (validated.layout === \"offset\") {\n\t\t\tpending = {\n\t\t\t\ttarget: body.target,\n\t\t\t\ttotalChunks: body.totalChunks,\n\t\t\t\tencoding: validated.encoding,\n\t\t\t\tlayout: \"offset\",\n\t\t\t\trawTotalBytes: validated.rawTotalBytes,\n\t\t\t\trawChunkBytes: validated.rawChunkBytes,\n\t\t\t\trawBody: Buffer.allocUnsafe(validated.rawTotalBytes),\n\t\t\t\treceivedChunks: new Uint8Array(body.totalChunks),\n\t\t\t\tchunkHashes: new Array<string | undefined>(body.totalChunks),\n\t\t\t\treceivedChunkCount: 0,\n\t\t\t\treceivedBytes: 0,\n\t\t\t\tallocatedBytes: validated.rawTotalBytes,\n\t\t\t\tmetadataBytes,\n\t\t\t\ttombstoneBytes,\n\t\t\t\texpiresAtMs: nowMs + pendingTtlMs,\n\t\t\t};\n\t\t} else {\n\t\t\tpending = {\n\t\t\t\ttarget: body.target,\n\t\t\t\ttotalChunks: body.totalChunks,\n\t\t\t\tencoding: validated.encoding,\n\t\t\t\tlayout: \"stored\",\n\t\t\t\tchunks: new Map(),\n\t\t\t\treceivedBytes: 0,\n\t\t\t\tallocatedBytes: 0,\n\t\t\t\tmetadataBytes,\n\t\t\t\ttombstoneBytes,\n\t\t\t\texpiresAtMs: nowMs + pendingTtlMs,\n\t\t\t};\n\t\t}\n\t\tpendingRequests.set(body.requestId, pending);\n\t\tpendingRequestBytes += pending.allocatedBytes;\n\t\tpendingRequestMetadataBytes += pending.metadataBytes;\n\t\tpendingRequestTombstoneReservationBytes += pending.tombstoneBytes;\n\t}\n\tif (pending.layout === \"offset\" && validated.layout === \"offset\") {\n\t\tconst written = pending.rawBody.write(validated.encoded, validated.offset, validated.decodedBytes, \"base64\");\n\t\tif (written !== validated.decodedBytes) {\n\t\t\tdeletePendingRequest(body.requestId, pending);\n\t\t\tthrow new Error(`Chunk ${body.chunkIndex} base64 decode did not fill its raw chunk range`);\n\t\t}\n\t\tpending.receivedChunks[body.chunkIndex] = 1;\n\t\tpending.chunkHashes[body.chunkIndex] = validated.sha256;\n\t\tpending.receivedChunkCount++;\n\t\tpending.receivedBytes += validated.decodedBytes;\n\t} else if (pending.layout === \"stored\" && validated.layout === \"stored\") {\n\t\tpending.chunks.set(body.chunkIndex, validated.chunk);\n\t\tpending.receivedBytes += validated.chunk.retainedBytes;\n\t\tpending.allocatedBytes += validated.chunk.retainedBytes;\n\t\tpendingRequestBytes += validated.chunk.retainedBytes;\n\t} else {\n\t\tdeletePendingRequest(body.requestId, pending);\n\t\tthrow new Error(\"Chunk layout changed while admitting the request\");\n\t}\n\tpending.expiresAtMs = nowMs + pendingTtlMs;\n\tscheduleCleanupTimer(nowMs);\n\n\tconst receivedChunkCount = pending.layout === \"offset\" ? pending.receivedChunkCount : pending.chunks.size;\n\tif (receivedChunkCount !== pending.totalChunks) {\n\t\treturn makeAck(body);\n\t}\n\n\tconst chunkHashes: string[] = [];\n\tif (pending.layout === \"offset\") {\n\t\tfor (let index = 0; index < pending.totalChunks; index++) {\n\t\t\tconst chunkHash = pending.chunkHashes[index];\n\t\t\tif (pending.receivedChunks[index] !== 1 || chunkHash === undefined) {\n\t\t\t\tthrow new Error(`Missing ${REQUEST_CHUNK_SEGMENT_ENCODING} chunk index: ${index}`);\n\t\t\t}\n\t\t\tchunkHashes.push(chunkHash);\n\t\t}\n\t} else {\n\t\tfor (let index = 0; index < pending.totalChunks; index++) {\n\t\t\tconst chunk = pending.chunks.get(index);\n\t\t\tif (!chunk) throw new Error(`Missing chunk index: ${index}`);\n\t\t\tchunkHashes.push(chunk.sha256);\n\t\t}\n\t}\n\tdeletePendingRequest(body.requestId, pending);\n\tconst bodyJson = assembleBodyJson(pending);\n\tconst retainedBytes =\n\t\tBuffer.byteLength(bodyJson, \"utf-8\") +\n\t\tBuffer.byteLength(pending.target, \"utf-8\") +\n\t\tBuffer.byteLength(pending.encoding, \"utf-8\") +\n\t\tBuffer.byteLength(body.requestId, \"utf-8\") +\n\t\tchunkHashes.length * 64;\n\tconst completedMetadata = {\n\t\ttarget: pending.target,\n\t\ttotalChunks: pending.totalChunks,\n\t\tencoding: pending.encoding,\n\t\t...(pending.layout === \"offset\"\n\t\t\t? { rawTotalBytes: pending.rawTotalBytes, rawChunkBytes: pending.rawChunkBytes }\n\t\t\t: {}),\n\t};\n\tcacheCompletedRequestTombstone(\n\t\tbody.requestId,\n\t\t{\n\t\t\t...completedMetadata,\n\t\t\tretainedBytes: tombstoneBytes,\n\t\t\texpiresAtMs: nowMs + tombstoneTtlMs,\n\t\t},\n\t\tmaxTombstoneCount,\n\t\tmaxTombstoneBytes,\n\t);\n\tcacheCompletedRequest(\n\t\tbody.requestId,\n\t\t{\n\t\t\t...completedMetadata,\n\t\t\tbodyJson,\n\t\t\tcompletedChunkIndex: body.chunkIndex,\n\t\t\tchunkHashes,\n\t\t\tretainedBytes,\n\t\t\texpiresAtMs: nowMs + completedTtlMs,\n\t\t},\n\t\toptions.maxCompletedCount ?? REQUEST_CHUNK_MAX_COMPLETED_COUNT,\n\t\toptions.maxCompletedBytes ?? REQUEST_CHUNK_MAX_COMPLETED_BYTES,\n\t);\n\tscheduleCleanupTimer(nowMs);\n\treturn {\n\t\tcomplete: true,\n\t\trequestId: body.requestId,\n\t\treplayed: false,\n\t\ttarget: pending.target,\n\t\tbodyJson,\n\t};\n}\n\nexport function getRequestChunkCacheStats(): {\n\tpendingCount: number;\n\tpendingBytes: number;\n\tpendingMetadataBytes: number;\n\tpendingPreallocatedBuffers: number;\n\tpendingStoredChunks: number;\n\tcompletedCount: number;\n\tcompletedBytes: number;\n\ttombstoneCount: number;\n\ttombstoneBytes: number;\n} {\n\tlet pendingPreallocatedBuffers = 0;\n\tlet pendingStoredChunks = 0;\n\tfor (const pending of pendingRequests.values()) {\n\t\tif (pending.layout === \"offset\") {\n\t\t\tpendingPreallocatedBuffers++;\n\t\t} else {\n\t\t\tpendingStoredChunks += pending.chunks.size;\n\t\t}\n\t}\n\treturn {\n\t\tpendingCount: pendingRequests.size,\n\t\tpendingBytes: pendingRequestBytes,\n\t\tpendingMetadataBytes: pendingRequestMetadataBytes,\n\t\tpendingPreallocatedBuffers,\n\t\tpendingStoredChunks,\n\t\tcompletedCount: completedRequests.size,\n\t\tcompletedBytes: completedRequestBytes,\n\t\ttombstoneCount: completedRequestTombstones.size,\n\t\ttombstoneBytes: completedRequestTombstoneBytes,\n\t};\n}\n\nexport function clearAllRequestChunks(): void {\n\tif (cleanupTimer !== undefined) {\n\t\tclearTimeout(cleanupTimer);\n\t\tcleanupTimer = undefined;\n\t}\n\tpendingRequests.clear();\n\tcompletedRequests.clear();\n\tcompletedRequestTombstones.clear();\n\tpendingRequestBytes = 0;\n\tpendingRequestMetadataBytes = 0;\n\tpendingRequestTombstoneReservationBytes = 0;\n\tcompletedRequestBytes = 0;\n\tcompletedRequestTombstoneBytes = 0;\n}\n"]}
|