@averyyy/pi-coding-agent 0.82.1-piclient.3 → 0.82.1-piclient.5
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 +12 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/tool-effects.d.ts +6 -0
- package/dist/cli/tool-effects.d.ts.map +1 -0
- package/dist/cli/tool-effects.js +164 -0
- package/dist/cli/tool-effects.js.map +1 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +5 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +12 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +314 -89
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/pi-server-client.d.ts +53 -2
- package/dist/core/pi-server-client.d.ts.map +1 -1
- package/dist/core/pi-server-client.js +2082 -290
- package/dist/core/pi-server-client.js.map +1 -1
- package/dist/core/pi-server-compact-state.d.ts +81 -0
- package/dist/core/pi-server-compact-state.d.ts.map +1 -0
- package/dist/core/pi-server-compact-state.js +438 -0
- package/dist/core/pi-server-compact-state.js.map +1 -0
- package/dist/core/pi-server-protocol.d.ts +2 -0
- package/dist/core/pi-server-protocol.d.ts.map +1 -1
- package/dist/core/pi-server-protocol.js +24 -1
- package/dist/core/pi-server-protocol.js.map +1 -1
- package/dist/core/pi-server-request.d.ts +28 -0
- package/dist/core/pi-server-request.d.ts.map +1 -1
- package/dist/core/pi-server-request.js +492 -75
- package/dist/core/pi-server-request.js.map +1 -1
- package/dist/core/pi-server-run-state.d.ts +41 -0
- package/dist/core/pi-server-run-state.d.ts.map +1 -0
- package/dist/core/pi-server-run-state.js +318 -0
- package/dist/core/pi-server-run-state.js.map +1 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +69 -53
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +86 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +737 -144
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/tool-effect-journal.d.ts +103 -0
- package/dist/core/tool-effect-journal.d.ts.map +1 -0
- package/dist/core/tool-effect-journal.js +1442 -0
- package/dist/core/tool-effect-journal.js.map +1 -0
- package/dist/core/tools/output-accumulator.d.ts +3 -1
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +46 -26
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +13 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts +15 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +98 -29
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/package.json +4 -4
|
@@ -3,6 +3,55 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
3
3
|
const DEFAULT_MAX_REQUEST_KB = 512;
|
|
4
4
|
const CHUNK_ENDPOINT = "/api/request/chunk";
|
|
5
5
|
const CHUNK_UPLOAD_CONCURRENCY = 4;
|
|
6
|
+
const CHUNK_MAX_TOTAL_CHUNKS = 4096;
|
|
7
|
+
const CHUNK_UPLOAD_RETRY_DELAYS_MS = [100, 250, 500];
|
|
8
|
+
const LEGACY_CHUNK_ACK_MAX_BYTES = 4096;
|
|
9
|
+
const DEFAULT_CHUNK_REQUEST_MEMO_LIMIT = 128;
|
|
10
|
+
const DEFAULT_RESPONSE_NO_PROGRESS_TIMEOUT_MS = 90_000;
|
|
11
|
+
export const PI_SERVER_CHUNK_ACK_HEADER = "X-Pi-Chunk-Ack";
|
|
12
|
+
export const PI_SERVER_CHUNK_ACK_VALUE = "1";
|
|
13
|
+
export const PI_SERVER_CHUNK_FINAL_VALUE = "0";
|
|
14
|
+
export const PI_SERVER_CHUNK_SEGMENT_ENCODING = "base64-segment-v1";
|
|
15
|
+
export const PI_SERVER_RESPONSE_HEADER_TIMEOUT_MS = 30_000;
|
|
16
|
+
export class PiServerTransportHeaderTimeoutError extends Error {
|
|
17
|
+
code = "PI_SERVER_TRANSPORT_HEADER_TIMEOUT";
|
|
18
|
+
timeoutMs;
|
|
19
|
+
constructor(method, endpoint, timeoutMs) {
|
|
20
|
+
super(`pi-server transport header timeout after ${timeoutMs} ms (ETIMEDOUT): ${method} ${endpoint}`);
|
|
21
|
+
this.name = "PiServerTransportHeaderTimeoutError";
|
|
22
|
+
this.timeoutMs = timeoutMs;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
class PiServerTransportBodyNoProgressError extends Error {
|
|
26
|
+
constructor(timeoutMs) {
|
|
27
|
+
super(`pi-server response made no progress for ${timeoutMs}ms`);
|
|
28
|
+
this.name = "PiServerTransportBodyNoProgressError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class PiServerChunkRequestMemoLimitError extends Error {
|
|
32
|
+
code = "PI_SERVER_CHUNK_REQUEST_MEMO_LIMIT";
|
|
33
|
+
limit;
|
|
34
|
+
constructor(limit) {
|
|
35
|
+
super(`pi-server chunk request identity limit (${limit}) reached; refusing to allocate a new requestId while prior requests remain uncertain`);
|
|
36
|
+
this.name = "PiServerChunkRequestMemoLimitError";
|
|
37
|
+
this.limit = limit;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class PiServerChunkAckProtocolError extends Error {
|
|
41
|
+
constructor(message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "PiServerChunkAckProtocolError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class PiServerTransportBodyLimitError extends Error {
|
|
47
|
+
code = "PI_SERVER_TRANSPORT_BODY_LIMIT";
|
|
48
|
+
maxBytes;
|
|
49
|
+
constructor(maxBytes) {
|
|
50
|
+
super(`pi-server response body exceeded ${maxBytes} bytes`);
|
|
51
|
+
this.name = "PiServerTransportBodyLimitError";
|
|
52
|
+
this.maxBytes = maxBytes;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
6
55
|
function jsonByteLength(value) {
|
|
7
56
|
return Buffer.byteLength(JSON.stringify(value), "utf-8");
|
|
8
57
|
}
|
|
@@ -25,126 +74,494 @@ function makeHeaders(authToken) {
|
|
|
25
74
|
function sha256(value) {
|
|
26
75
|
return createHash("sha256").update(value).digest("hex");
|
|
27
76
|
}
|
|
77
|
+
function isTransientChunkResponse(status) {
|
|
78
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
79
|
+
}
|
|
80
|
+
function waitForChunkRetry(delayMs, signal) {
|
|
81
|
+
if (signal?.aborted)
|
|
82
|
+
return Promise.reject(signal.reason);
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const timeout = setTimeout(() => {
|
|
85
|
+
signal?.removeEventListener("abort", onAbort);
|
|
86
|
+
resolve();
|
|
87
|
+
}, delayMs);
|
|
88
|
+
const onAbort = () => {
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
signal?.removeEventListener("abort", onAbort);
|
|
91
|
+
reject(signal?.reason ?? new DOMException("The operation was aborted", "AbortError"));
|
|
92
|
+
};
|
|
93
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
94
|
+
});
|
|
95
|
+
}
|
|
28
96
|
function isRecord(value) {
|
|
29
97
|
return typeof value === "object" && value !== null;
|
|
30
98
|
}
|
|
31
99
|
function isChunkAckBody(value) {
|
|
32
100
|
return (isRecord(value) && "received" in value && "requestId" in value && "chunkIndex" in value && "totalChunks" in value);
|
|
33
101
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
102
|
+
function getResponseNoProgressTimeoutMs() {
|
|
103
|
+
const raw = process.env.PI_SERVER_RESPONSE_NO_PROGRESS_TIMEOUT_MS;
|
|
104
|
+
const value = raw === undefined ? DEFAULT_RESPONSE_NO_PROGRESS_TIMEOUT_MS : Number(raw);
|
|
105
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
106
|
+
throw new Error("PI_SERVER_RESPONSE_NO_PROGRESS_TIMEOUT_MS must be positive finite milliseconds");
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function createBodyNoProgressState() {
|
|
111
|
+
const timeoutMs = getResponseNoProgressTimeoutMs();
|
|
112
|
+
return { timeoutMs, deadlineMs: Date.now() + timeoutMs };
|
|
113
|
+
}
|
|
114
|
+
async function readPiServerBodyChunk(reader, state) {
|
|
115
|
+
while (true) {
|
|
116
|
+
const remainingMs = state.deadlineMs - Date.now();
|
|
117
|
+
if (remainingMs <= 0) {
|
|
118
|
+
throw new PiServerTransportBodyNoProgressError(state.timeoutMs);
|
|
119
|
+
}
|
|
120
|
+
const result = await new Promise((resolve, reject) => {
|
|
121
|
+
let settled = false;
|
|
122
|
+
const timeout = setTimeout(() => {
|
|
123
|
+
if (settled)
|
|
124
|
+
return;
|
|
125
|
+
settled = true;
|
|
126
|
+
reject(new PiServerTransportBodyNoProgressError(state.timeoutMs));
|
|
127
|
+
}, remainingMs);
|
|
128
|
+
timeout.unref();
|
|
129
|
+
void reader.read().then((value) => {
|
|
130
|
+
if (settled)
|
|
131
|
+
return;
|
|
132
|
+
settled = true;
|
|
133
|
+
clearTimeout(timeout);
|
|
134
|
+
resolve(value);
|
|
135
|
+
}, (error) => {
|
|
136
|
+
if (settled)
|
|
137
|
+
return;
|
|
138
|
+
settled = true;
|
|
139
|
+
clearTimeout(timeout);
|
|
140
|
+
reject(error);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
if (result.done || result.value.byteLength > 0) {
|
|
144
|
+
if (!result.done)
|
|
145
|
+
state.deadlineMs = Date.now() + state.timeoutMs;
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
await new Promise((resolve) => {
|
|
149
|
+
setTimeout(resolve, 0);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function readPiServerResponseText(response, maxBytes = Number.POSITIVE_INFINITY) {
|
|
154
|
+
if ((maxBytes !== Number.POSITIVE_INFINITY && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) ||
|
|
155
|
+
maxBytes === Number.NEGATIVE_INFINITY) {
|
|
156
|
+
throw new Error("pi-server response body limit must be positive safe-integer bytes or Infinity");
|
|
157
|
+
}
|
|
158
|
+
if (!response.body)
|
|
159
|
+
throw new Error("pi-server response body was missing");
|
|
160
|
+
const reader = response.body.getReader();
|
|
161
|
+
const decoder = new TextDecoder();
|
|
162
|
+
const noProgress = createBodyNoProgressState();
|
|
163
|
+
let body = "";
|
|
164
|
+
let bodyBytes = 0;
|
|
165
|
+
try {
|
|
166
|
+
while (true) {
|
|
167
|
+
const result = await readPiServerBodyChunk(reader, noProgress);
|
|
168
|
+
if (result.done) {
|
|
169
|
+
body += decoder.decode();
|
|
170
|
+
return body;
|
|
171
|
+
}
|
|
172
|
+
bodyBytes += result.value.byteLength;
|
|
173
|
+
if (bodyBytes > maxBytes) {
|
|
174
|
+
throw new PiServerTransportBodyLimitError(maxBytes);
|
|
175
|
+
}
|
|
176
|
+
body += decoder.decode(result.value, { stream: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
void reader.cancel().catch(() => undefined);
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export async function readPiServerResponseJson(response, maxBytes = Number.POSITIVE_INFINITY) {
|
|
185
|
+
return JSON.parse(await readPiServerResponseText(response, maxBytes));
|
|
186
|
+
}
|
|
187
|
+
function rebuildResponse(response, body) {
|
|
188
|
+
return new Response(body, {
|
|
189
|
+
status: response.status,
|
|
190
|
+
statusText: response.statusText,
|
|
191
|
+
headers: response.headers,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async function discardResponse(response) {
|
|
195
|
+
if (!response?.body || response.bodyUsed || response.body.locked)
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
await response.body.cancel();
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// The response is already unusable and must not replace the selected final response.
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function inspectLegacyChunkAck(response, requestId, chunkIndex, totalChunks) {
|
|
205
|
+
const contentLength = Number(response.headers.get("Content-Length"));
|
|
206
|
+
if (Number.isFinite(contentLength) && contentLength > LEGACY_CHUNK_ACK_MAX_BYTES) {
|
|
207
|
+
return { acknowledged: false, response };
|
|
208
|
+
}
|
|
209
|
+
if (!response.body)
|
|
210
|
+
return { acknowledged: false, response };
|
|
211
|
+
const reader = response.body.getReader();
|
|
212
|
+
const noProgress = createBodyNoProgressState();
|
|
213
|
+
const chunks = [];
|
|
214
|
+
let bodyBytes = 0;
|
|
37
215
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
216
|
+
while (true) {
|
|
217
|
+
const result = await readPiServerBodyChunk(reader, noProgress);
|
|
218
|
+
if (result.done)
|
|
219
|
+
break;
|
|
220
|
+
chunks.push(result.value);
|
|
221
|
+
bodyBytes += result.value.byteLength;
|
|
222
|
+
if (bodyBytes > LEGACY_CHUNK_ACK_MAX_BYTES) {
|
|
223
|
+
let prefixIndex = 0;
|
|
224
|
+
const rebuiltBody = new ReadableStream({
|
|
225
|
+
async pull(controller) {
|
|
226
|
+
if (prefixIndex < chunks.length) {
|
|
227
|
+
const prefix = chunks[prefixIndex];
|
|
228
|
+
chunks[prefixIndex] = new Uint8Array();
|
|
229
|
+
prefixIndex++;
|
|
230
|
+
controller.enqueue(prefix);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const next = await readPiServerBodyChunk(reader, noProgress);
|
|
235
|
+
if (next.done) {
|
|
236
|
+
controller.close();
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
controller.enqueue(next.value);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
controller.error(error);
|
|
244
|
+
void reader.cancel(error).catch(() => undefined);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
cancel(reason) {
|
|
248
|
+
return reader.cancel(reason);
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
return { acknowledged: false, response: rebuildResponse(response, rebuiltBody) };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
void reader.cancel().catch(() => undefined);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
const encoded = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), bodyBytes);
|
|
260
|
+
const rebuilt = rebuildResponse(response, encoded);
|
|
261
|
+
try {
|
|
262
|
+
const body = JSON.parse(encoded.toString("utf-8"));
|
|
263
|
+
const acknowledged = isChunkAckBody(body) &&
|
|
40
264
|
body.received === true &&
|
|
41
265
|
body.requestId === requestId &&
|
|
42
266
|
body.chunkIndex === chunkIndex &&
|
|
43
|
-
body.totalChunks === totalChunks
|
|
267
|
+
body.totalChunks === totalChunks;
|
|
268
|
+
return acknowledged ? { acknowledged: true } : { acknowledged: false, response: rebuilt };
|
|
44
269
|
}
|
|
45
270
|
catch {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function chunkBodyFits(target, requestId, encodedLength, chunkLength, maxBytes) {
|
|
50
|
-
const total = Math.ceil(encodedLength / chunkLength);
|
|
51
|
-
const body = {
|
|
52
|
-
requestId,
|
|
53
|
-
target,
|
|
54
|
-
chunkIndex: Math.max(0, total - 1),
|
|
55
|
-
totalChunks: total,
|
|
56
|
-
sha256: "0".repeat(64),
|
|
57
|
-
chunk: "x".repeat(Math.min(chunkLength, encodedLength)),
|
|
58
|
-
};
|
|
59
|
-
return jsonByteLength(body) <= maxBytes;
|
|
271
|
+
return { acknowledged: false, response: rebuilt };
|
|
272
|
+
}
|
|
60
273
|
}
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
274
|
+
async function inspectChunkResponse(response, requestId, chunkIndex, totalChunks) {
|
|
275
|
+
const marker = response.headers.get(PI_SERVER_CHUNK_ACK_HEADER);
|
|
276
|
+
if (marker === PI_SERVER_CHUNK_FINAL_VALUE) {
|
|
277
|
+
return { acknowledged: false, response };
|
|
64
278
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
279
|
+
if (marker === PI_SERVER_CHUNK_ACK_VALUE) {
|
|
280
|
+
if (!response.headers.get("Content-Type")?.includes("application/json")) {
|
|
281
|
+
await discardResponse(response);
|
|
282
|
+
throw new PiServerChunkAckProtocolError("pi-server chunk acknowledgement must use application/json");
|
|
283
|
+
}
|
|
284
|
+
const text = await readPiServerResponseText(response, LEGACY_CHUNK_ACK_MAX_BYTES);
|
|
285
|
+
let body;
|
|
286
|
+
try {
|
|
287
|
+
body = JSON.parse(text);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
throw new PiServerChunkAckProtocolError("pi-server chunk acknowledgement was not valid JSON");
|
|
73
291
|
}
|
|
74
|
-
|
|
75
|
-
|
|
292
|
+
if (!isChunkAckBody(body) ||
|
|
293
|
+
body.received !== true ||
|
|
294
|
+
body.requestId !== requestId ||
|
|
295
|
+
body.chunkIndex !== chunkIndex ||
|
|
296
|
+
body.totalChunks !== totalChunks) {
|
|
297
|
+
throw new PiServerChunkAckProtocolError("pi-server chunk acknowledgement did not match its request envelope");
|
|
76
298
|
}
|
|
299
|
+
return { acknowledged: true };
|
|
77
300
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
301
|
+
if (marker !== null) {
|
|
302
|
+
await discardResponse(response);
|
|
303
|
+
throw new PiServerChunkAckProtocolError(`pi-server returned unsupported ${PI_SERVER_CHUNK_ACK_HEADER} value: ${marker}`);
|
|
304
|
+
}
|
|
305
|
+
if (!response.headers.get("Content-Type")?.includes("application/json")) {
|
|
306
|
+
return { acknowledged: false, response };
|
|
307
|
+
}
|
|
308
|
+
return inspectLegacyChunkAck(response, requestId, chunkIndex, totalChunks);
|
|
309
|
+
}
|
|
310
|
+
function getRawChunkLayout(target, requestId, rawBodyBytes, maxBytes) {
|
|
311
|
+
let totalChunks = 1;
|
|
312
|
+
while (true) {
|
|
313
|
+
const emptyEnvelopeBytes = jsonByteLength({
|
|
314
|
+
requestId,
|
|
315
|
+
target,
|
|
316
|
+
chunkIndex: totalChunks - 1,
|
|
317
|
+
totalChunks,
|
|
318
|
+
sha256: "0".repeat(64),
|
|
319
|
+
chunk: "",
|
|
320
|
+
encoding: PI_SERVER_CHUNK_SEGMENT_ENCODING,
|
|
321
|
+
rawTotalBytes: rawBodyBytes,
|
|
322
|
+
// Using the total body size here is conservative because every
|
|
323
|
+
// derived chunk size is no larger and therefore has no more digits.
|
|
324
|
+
rawChunkBytes: rawBodyBytes,
|
|
325
|
+
});
|
|
326
|
+
const encodedChunkBytes = Math.floor((maxBytes - emptyEnvelopeBytes) / 4) * 4;
|
|
327
|
+
const rawChunkBytes = Math.floor(encodedChunkBytes / 4) * 3;
|
|
328
|
+
if (rawChunkBytes <= 0) {
|
|
329
|
+
throw new Error("PI_CLIENT_MAX_REQUEST_KB is too small for pi-server chunk envelopes");
|
|
330
|
+
}
|
|
331
|
+
const nextTotalChunks = Math.ceil(rawBodyBytes / rawChunkBytes);
|
|
332
|
+
if (nextTotalChunks > CHUNK_MAX_TOTAL_CHUNKS) {
|
|
333
|
+
throw new Error(`pi-server chunk request exceeds the ${CHUNK_MAX_TOTAL_CHUNKS}-chunk protocol limit`);
|
|
334
|
+
}
|
|
335
|
+
if (nextTotalChunks === totalChunks) {
|
|
336
|
+
return { rawChunkBytes, totalChunks };
|
|
337
|
+
}
|
|
338
|
+
if (nextTotalChunks < totalChunks) {
|
|
339
|
+
throw new Error("pi-server chunk layout did not converge monotonically");
|
|
340
|
+
}
|
|
341
|
+
totalChunks = nextTotalChunks;
|
|
81
342
|
}
|
|
82
|
-
return chunks;
|
|
83
343
|
}
|
|
84
344
|
export class ChunkRequest {
|
|
85
345
|
options;
|
|
346
|
+
#chunkRequestMemos = new Map();
|
|
347
|
+
#maxChunkRequestMemos;
|
|
86
348
|
constructor(options) {
|
|
349
|
+
const maxChunkRequestMemos = options.maxChunkRequestMemos ?? DEFAULT_CHUNK_REQUEST_MEMO_LIMIT;
|
|
350
|
+
if (!Number.isSafeInteger(maxChunkRequestMemos) || maxChunkRequestMemos <= 0) {
|
|
351
|
+
throw new Error("maxChunkRequestMemos must be a positive safe integer");
|
|
352
|
+
}
|
|
87
353
|
this.options = options;
|
|
354
|
+
this.#maxChunkRequestMemos = maxChunkRequestMemos;
|
|
88
355
|
}
|
|
89
356
|
async getJson(endpoint) {
|
|
90
|
-
return
|
|
357
|
+
return this.#fetchWithResponseHeaderTimeout(endpoint, {
|
|
91
358
|
method: "GET",
|
|
92
359
|
headers: makeHeaders(this.options.authToken),
|
|
93
|
-
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async deleteJson(endpoint) {
|
|
363
|
+
return this.#fetchWithResponseHeaderTimeout(endpoint, {
|
|
364
|
+
method: "DELETE",
|
|
365
|
+
headers: makeHeaders(this.options.authToken),
|
|
94
366
|
});
|
|
95
367
|
}
|
|
96
368
|
async postJson(endpoint, body) {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
369
|
+
const rawBody = Buffer.from(JSON.stringify(body), "utf-8");
|
|
370
|
+
const memoKey = `${endpoint}\0${sha256(rawBody)}`;
|
|
371
|
+
let memo = this.#chunkRequestMemos.get(memoKey);
|
|
372
|
+
const maxBytes = memo?.maxBytes ?? getMaxRequestBytes();
|
|
373
|
+
if (memo === undefined && rawBody.byteLength <= maxBytes) {
|
|
374
|
+
return this.#postRawJson(endpoint, rawBody.toString("utf-8"));
|
|
375
|
+
}
|
|
376
|
+
if (memo === undefined) {
|
|
377
|
+
if (this.#chunkRequestMemos.size >= this.#maxChunkRequestMemos) {
|
|
378
|
+
throw new PiServerChunkRequestMemoLimitError(this.#maxChunkRequestMemos);
|
|
379
|
+
}
|
|
380
|
+
memo = { requestId: randomUUID(), maxBytes };
|
|
381
|
+
this.#chunkRequestMemos.set(memoKey, memo);
|
|
382
|
+
}
|
|
383
|
+
const { requestId } = memo;
|
|
384
|
+
let layout;
|
|
385
|
+
try {
|
|
386
|
+
layout = getRawChunkLayout(endpoint, requestId, rawBody.byteLength, memo.maxBytes);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
if (this.#chunkRequestMemos.get(memoKey) === memo) {
|
|
390
|
+
this.#chunkRequestMemos.delete(memoKey);
|
|
391
|
+
}
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
105
394
|
let nextIndex = 0;
|
|
106
395
|
let finalResponse;
|
|
107
|
-
let
|
|
396
|
+
let transientFailureResponse;
|
|
397
|
+
let fatalError;
|
|
398
|
+
const stopped = () => finalResponse !== undefined || fatalError !== undefined;
|
|
399
|
+
const replaceTransientFailureResponse = async (response) => {
|
|
400
|
+
const previous = transientFailureResponse;
|
|
401
|
+
transientFailureResponse = response;
|
|
402
|
+
if (previous !== response)
|
|
403
|
+
await discardResponse(previous);
|
|
404
|
+
};
|
|
405
|
+
const uploadChunk = async (index) => {
|
|
406
|
+
const start = index * layout.rawChunkBytes;
|
|
407
|
+
const chunk = rawBody
|
|
408
|
+
.subarray(start, Math.min(start + layout.rawChunkBytes, rawBody.byteLength))
|
|
409
|
+
.toString("base64");
|
|
410
|
+
let lastError;
|
|
411
|
+
let lastTransientResponse;
|
|
412
|
+
try {
|
|
413
|
+
for (let attempt = 0; attempt <= CHUNK_UPLOAD_RETRY_DELAYS_MS.length; attempt++) {
|
|
414
|
+
if (stopped())
|
|
415
|
+
return;
|
|
416
|
+
if (attempt > 0) {
|
|
417
|
+
await waitForChunkRetry(CHUNK_UPLOAD_RETRY_DELAYS_MS[attempt - 1], this.options.signal);
|
|
418
|
+
if (stopped())
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const chunkBody = {
|
|
422
|
+
requestId,
|
|
423
|
+
target: endpoint,
|
|
424
|
+
chunkIndex: index,
|
|
425
|
+
totalChunks: layout.totalChunks,
|
|
426
|
+
sha256: sha256(chunk),
|
|
427
|
+
chunk,
|
|
428
|
+
encoding: PI_SERVER_CHUNK_SEGMENT_ENCODING,
|
|
429
|
+
rawTotalBytes: rawBody.byteLength,
|
|
430
|
+
rawChunkBytes: layout.rawChunkBytes,
|
|
431
|
+
};
|
|
432
|
+
try {
|
|
433
|
+
let response = await this.#postRawJson(CHUNK_ENDPOINT, JSON.stringify(chunkBody));
|
|
434
|
+
if (stopped()) {
|
|
435
|
+
await discardResponse(response);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (response.ok) {
|
|
439
|
+
const inspection = await inspectChunkResponse(response, requestId, index, layout.totalChunks);
|
|
440
|
+
if (inspection.acknowledged)
|
|
441
|
+
return;
|
|
442
|
+
if (!inspection.response) {
|
|
443
|
+
throw new PiServerChunkAckProtocolError("pi-server chunk response inspection lost its response");
|
|
444
|
+
}
|
|
445
|
+
response = inspection.response;
|
|
446
|
+
if (stopped()) {
|
|
447
|
+
await discardResponse(response);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (isTransientChunkResponse(response.status)) {
|
|
452
|
+
await discardResponse(lastTransientResponse);
|
|
453
|
+
lastTransientResponse = response;
|
|
454
|
+
lastError = new Error(`pi-server chunk ${index + 1}/${layout.totalChunks} returned HTTP ${response.status}`);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (finalResponse === undefined && fatalError === undefined) {
|
|
458
|
+
finalResponse = response;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
await discardResponse(response);
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
if (this.options.signal?.aborted) {
|
|
467
|
+
throw this.options.signal.reason ?? error;
|
|
468
|
+
}
|
|
469
|
+
if (error instanceof PiServerChunkAckProtocolError ||
|
|
470
|
+
error instanceof PiServerTransportBodyLimitError) {
|
|
471
|
+
fatalError = error;
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
lastError = error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (lastTransientResponse !== undefined) {
|
|
478
|
+
await replaceTransientFailureResponse(lastTransientResponse);
|
|
479
|
+
lastTransientResponse = undefined;
|
|
480
|
+
}
|
|
481
|
+
if (lastError instanceof Error) {
|
|
482
|
+
throw lastError;
|
|
483
|
+
}
|
|
484
|
+
throw new Error(`pi-server chunk ${index + 1}/${layout.totalChunks} failed after bounded retries`);
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
if (stopped())
|
|
488
|
+
await discardResponse(lastTransientResponse);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
108
491
|
const uploadWorker = async () => {
|
|
109
|
-
while (
|
|
492
|
+
while (!stopped()) {
|
|
110
493
|
const index = nextIndex;
|
|
111
|
-
|
|
112
|
-
const chunk = chunks[index];
|
|
113
|
-
const chunkBody = {
|
|
114
|
-
requestId,
|
|
115
|
-
target: endpoint,
|
|
116
|
-
chunkIndex: index,
|
|
117
|
-
totalChunks: chunks.length,
|
|
118
|
-
sha256: sha256(chunk),
|
|
119
|
-
chunk,
|
|
120
|
-
};
|
|
121
|
-
const response = await this.#postRawJson(CHUNK_ENDPOINT, JSON.stringify(chunkBody));
|
|
122
|
-
if (finalResponse !== undefined || failureResponse !== undefined)
|
|
494
|
+
if (index >= layout.totalChunks)
|
|
123
495
|
return;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
if (!(await isChunkAck(response, requestId, index, chunks.length))) {
|
|
129
|
-
finalResponse = response;
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
496
|
+
nextIndex++;
|
|
497
|
+
await uploadChunk(index);
|
|
132
498
|
}
|
|
133
499
|
};
|
|
134
|
-
await Promise.
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
500
|
+
const results = await Promise.allSettled(Array.from({ length: Math.min(CHUNK_UPLOAD_CONCURRENCY, layout.totalChunks) }, () => uploadWorker()));
|
|
501
|
+
if (fatalError !== undefined) {
|
|
502
|
+
await discardResponse(finalResponse);
|
|
503
|
+
await discardResponse(transientFailureResponse);
|
|
504
|
+
throw fatalError;
|
|
505
|
+
}
|
|
506
|
+
if (this.options.signal?.aborted) {
|
|
507
|
+
await discardResponse(finalResponse);
|
|
508
|
+
await discardResponse(transientFailureResponse);
|
|
509
|
+
throw this.options.signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
510
|
+
}
|
|
511
|
+
if (finalResponse !== undefined) {
|
|
512
|
+
await discardResponse(transientFailureResponse);
|
|
513
|
+
if (this.#chunkRequestMemos.get(memoKey) === memo) {
|
|
514
|
+
this.#chunkRequestMemos.delete(memoKey);
|
|
515
|
+
}
|
|
138
516
|
return finalResponse;
|
|
517
|
+
}
|
|
518
|
+
if (transientFailureResponse !== undefined) {
|
|
519
|
+
return transientFailureResponse;
|
|
520
|
+
}
|
|
521
|
+
for (const result of results) {
|
|
522
|
+
if (result.status === "rejected") {
|
|
523
|
+
throw result.reason;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
139
526
|
throw new Error("No final chunk response received from pi-server");
|
|
140
527
|
}
|
|
141
528
|
async #postRawJson(endpoint, rawJson) {
|
|
142
|
-
return
|
|
529
|
+
return this.#fetchWithResponseHeaderTimeout(endpoint, {
|
|
143
530
|
method: "POST",
|
|
144
531
|
headers: makeHeaders(this.options.authToken),
|
|
145
532
|
body: rawJson,
|
|
146
|
-
signal: this.options.signal,
|
|
147
533
|
});
|
|
148
534
|
}
|
|
535
|
+
async #fetchWithResponseHeaderTimeout(endpoint, init) {
|
|
536
|
+
const timeoutMs = this.options.responseHeaderTimeoutMs ?? PI_SERVER_RESPONSE_HEADER_TIMEOUT_MS;
|
|
537
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
538
|
+
throw new Error("responseHeaderTimeoutMs must be a positive finite number");
|
|
539
|
+
}
|
|
540
|
+
const method = init.method ?? "GET";
|
|
541
|
+
const timeoutError = new PiServerTransportHeaderTimeoutError(method, endpoint, timeoutMs);
|
|
542
|
+
const timeoutController = new AbortController();
|
|
543
|
+
const signal = this.options.signal
|
|
544
|
+
? AbortSignal.any([this.options.signal, timeoutController.signal])
|
|
545
|
+
: timeoutController.signal;
|
|
546
|
+
const timeout = setTimeout(() => {
|
|
547
|
+
timeoutController.abort(timeoutError);
|
|
548
|
+
}, timeoutMs);
|
|
549
|
+
timeout.unref();
|
|
550
|
+
try {
|
|
551
|
+
return await fetch(`${this.options.serverUrl}${endpoint}`, {
|
|
552
|
+
...init,
|
|
553
|
+
signal,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
if (timeoutController.signal.aborted && !this.options.signal?.aborted) {
|
|
558
|
+
throw timeoutError;
|
|
559
|
+
}
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
clearTimeout(timeout);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
149
566
|
}
|
|
150
567
|
//# sourceMappingURL=pi-server-request.js.map
|