@averyyy/pi-coding-agent 0.82.1-piclient.6 → 0.82.1-piclient.7
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 +0 -12
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +0 -5
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -12
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +89 -317
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/pi-server-client.d.ts +2 -54
- package/dist/core/pi-server-client.d.ts.map +1 -1
- package/dist/core/pi-server-client.js +272 -2320
- package/dist/core/pi-server-client.js.map +1 -1
- package/dist/core/pi-server-protocol.d.ts +0 -2
- package/dist/core/pi-server-protocol.d.ts.map +1 -1
- package/dist/core/pi-server-protocol.js +1 -24
- package/dist/core/pi-server-protocol.js.map +1 -1
- package/dist/core/pi-server-request.d.ts +0 -28
- package/dist/core/pi-server-request.d.ts.map +1 -1
- package/dist/core/pi-server-request.js +75 -492
- package/dist/core/pi-server-request.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +53 -69
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -86
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +144 -737
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts +1 -3
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +26 -46
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +0 -13
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts +0 -15
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +29 -98
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/package.json +4 -4
- package/dist/cli/tool-effects.d.ts +0 -6
- package/dist/cli/tool-effects.d.ts.map +0 -1
- package/dist/cli/tool-effects.js +0 -164
- package/dist/cli/tool-effects.js.map +0 -1
- package/dist/core/pi-server-compact-state.d.ts +0 -81
- package/dist/core/pi-server-compact-state.d.ts.map +0 -1
- package/dist/core/pi-server-compact-state.js +0 -439
- package/dist/core/pi-server-compact-state.js.map +0 -1
- package/dist/core/pi-server-run-state.d.ts +0 -42
- package/dist/core/pi-server-run-state.d.ts.map +0 -1
- package/dist/core/pi-server-run-state.js +0 -320
- package/dist/core/pi-server-run-state.js.map +0 -1
- package/dist/core/tool-effect-journal.d.ts +0 -103
- package/dist/core/tool-effect-journal.d.ts.map +0 -1
- package/dist/core/tool-effect-journal.js +0 -1442
- package/dist/core/tool-effect-journal.js.map +0 -1
|
@@ -3,55 +3,6 @@ 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
|
-
}
|
|
55
6
|
function jsonByteLength(value) {
|
|
56
7
|
return Buffer.byteLength(JSON.stringify(value), "utf-8");
|
|
57
8
|
}
|
|
@@ -74,494 +25,126 @@ function makeHeaders(authToken) {
|
|
|
74
25
|
function sha256(value) {
|
|
75
26
|
return createHash("sha256").update(value).digest("hex");
|
|
76
27
|
}
|
|
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
|
-
}
|
|
96
28
|
function isRecord(value) {
|
|
97
29
|
return typeof value === "object" && value !== null;
|
|
98
30
|
}
|
|
99
31
|
function isChunkAckBody(value) {
|
|
100
32
|
return (isRecord(value) && "received" in value && "requestId" in value && "chunkIndex" in value && "totalChunks" in value);
|
|
101
33
|
}
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
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;
|
|
34
|
+
async function isChunkAck(response, requestId, chunkIndex, totalChunks) {
|
|
35
|
+
if (!response.headers.get("Content-Type")?.includes("application/json"))
|
|
36
|
+
return false;
|
|
215
37
|
try {
|
|
216
|
-
|
|
217
|
-
|
|
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) &&
|
|
38
|
+
const body = (await response.clone().json());
|
|
39
|
+
return (isChunkAckBody(body) &&
|
|
264
40
|
body.received === true &&
|
|
265
41
|
body.requestId === requestId &&
|
|
266
42
|
body.chunkIndex === chunkIndex &&
|
|
267
|
-
body.totalChunks === totalChunks;
|
|
268
|
-
return acknowledged ? { acknowledged: true } : { acknowledged: false, response: rebuilt };
|
|
43
|
+
body.totalChunks === totalChunks);
|
|
269
44
|
}
|
|
270
45
|
catch {
|
|
271
|
-
return
|
|
272
|
-
}
|
|
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;
|
|
273
60
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return { acknowledged: false, response };
|
|
61
|
+
function splitEncodedBody(target, requestId, encoded, maxBytes) {
|
|
62
|
+
if (!chunkBodyFits(target, requestId, encoded.length, 1, maxBytes)) {
|
|
63
|
+
throw new Error("PI_CLIENT_MAX_REQUEST_KB is too small for pi-server chunk envelopes");
|
|
278
64
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
body = JSON.parse(text);
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
throw new PiServerChunkAckProtocolError("pi-server chunk acknowledgement was not valid JSON");
|
|
65
|
+
let low = 1;
|
|
66
|
+
let high = encoded.length;
|
|
67
|
+
let best = 1;
|
|
68
|
+
while (low <= high) {
|
|
69
|
+
const mid = Math.floor((low + high) / 2);
|
|
70
|
+
if (chunkBodyFits(target, requestId, encoded.length, mid, maxBytes)) {
|
|
71
|
+
best = mid;
|
|
72
|
+
low = mid + 1;
|
|
291
73
|
}
|
|
292
|
-
|
|
293
|
-
|
|
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");
|
|
74
|
+
else {
|
|
75
|
+
high = mid - 1;
|
|
298
76
|
}
|
|
299
|
-
return { acknowledged: true };
|
|
300
77
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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;
|
|
78
|
+
const chunks = [];
|
|
79
|
+
for (let offset = 0; offset < encoded.length; offset += best) {
|
|
80
|
+
chunks.push(encoded.slice(offset, offset + best));
|
|
342
81
|
}
|
|
82
|
+
return chunks;
|
|
343
83
|
}
|
|
344
84
|
export class ChunkRequest {
|
|
345
85
|
options;
|
|
346
|
-
#chunkRequestMemos = new Map();
|
|
347
|
-
#maxChunkRequestMemos;
|
|
348
86
|
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
|
-
}
|
|
353
87
|
this.options = options;
|
|
354
|
-
this.#maxChunkRequestMemos = maxChunkRequestMemos;
|
|
355
88
|
}
|
|
356
89
|
async getJson(endpoint) {
|
|
357
|
-
return this
|
|
90
|
+
return fetch(`${this.options.serverUrl}${endpoint}`, {
|
|
358
91
|
method: "GET",
|
|
359
92
|
headers: makeHeaders(this.options.authToken),
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
async deleteJson(endpoint) {
|
|
363
|
-
return this.#fetchWithResponseHeaderTimeout(endpoint, {
|
|
364
|
-
method: "DELETE",
|
|
365
|
-
headers: makeHeaders(this.options.authToken),
|
|
93
|
+
signal: this.options.signal,
|
|
366
94
|
});
|
|
367
95
|
}
|
|
368
96
|
async postJson(endpoint, body) {
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
}
|
|
97
|
+
const rawJson = JSON.stringify(body);
|
|
98
|
+
const maxBytes = getMaxRequestBytes();
|
|
99
|
+
if (Buffer.byteLength(rawJson, "utf-8") <= maxBytes) {
|
|
100
|
+
return this.#postRawJson(endpoint, rawJson);
|
|
101
|
+
}
|
|
102
|
+
const requestId = randomUUID();
|
|
103
|
+
const encoded = Buffer.from(rawJson, "utf-8").toString("base64");
|
|
104
|
+
const chunks = splitEncodedBody(endpoint, requestId, encoded, maxBytes);
|
|
394
105
|
let nextIndex = 0;
|
|
395
106
|
let finalResponse;
|
|
396
|
-
let
|
|
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
|
-
};
|
|
107
|
+
let failureResponse;
|
|
491
108
|
const uploadWorker = async () => {
|
|
492
|
-
while (
|
|
109
|
+
while (nextIndex < chunks.length && finalResponse === undefined && failureResponse === undefined) {
|
|
493
110
|
const index = nextIndex;
|
|
494
|
-
if (index >= layout.totalChunks)
|
|
495
|
-
return;
|
|
496
111
|
nextIndex++;
|
|
497
|
-
|
|
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)
|
|
123
|
+
return;
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
failureResponse = response;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!(await isChunkAck(response, requestId, index, chunks.length))) {
|
|
129
|
+
finalResponse = response;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
498
132
|
}
|
|
499
133
|
};
|
|
500
|
-
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
}
|
|
134
|
+
await Promise.all(Array.from({ length: Math.min(CHUNK_UPLOAD_CONCURRENCY, chunks.length) }, () => uploadWorker()));
|
|
135
|
+
if (failureResponse)
|
|
136
|
+
return failureResponse;
|
|
137
|
+
if (finalResponse)
|
|
516
138
|
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
|
-
}
|
|
526
139
|
throw new Error("No final chunk response received from pi-server");
|
|
527
140
|
}
|
|
528
141
|
async #postRawJson(endpoint, rawJson) {
|
|
529
|
-
return this
|
|
142
|
+
return fetch(`${this.options.serverUrl}${endpoint}`, {
|
|
530
143
|
method: "POST",
|
|
531
144
|
headers: makeHeaders(this.options.authToken),
|
|
532
145
|
body: rawJson,
|
|
146
|
+
signal: this.options.signal,
|
|
533
147
|
});
|
|
534
148
|
}
|
|
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
|
-
}
|
|
566
149
|
}
|
|
567
150
|
//# sourceMappingURL=pi-server-request.js.map
|