@cloudpdf/engine 3.0.0-next.6 → 3.0.0-next.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +67 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +67 -8
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.cts
CHANGED
|
@@ -21,6 +21,23 @@ interface HttpClientOptions {
|
|
|
21
21
|
sessionId?: string;
|
|
22
22
|
/** Replace the global fetch (e.g. in Node tests with undici). */
|
|
23
23
|
fetch?: typeof globalThis.fetch;
|
|
24
|
+
/**
|
|
25
|
+
* Document affinity (opt-in; requires a server whose CORS
|
|
26
|
+
* allowlist includes the header): send `X-CloudPDF-Doc` on
|
|
27
|
+
* origin-bound doc requests so consistent-hash load balancers pin a
|
|
28
|
+
* document's traffic to one warm replica. Derived from the request
|
|
29
|
+
* path (`/v1/docs/:docId/…` — the same extraction the Helm chart's
|
|
30
|
+
* uri-mode fallback uses); never sent on CDN-rewritten requests.
|
|
31
|
+
*/
|
|
32
|
+
docAffinityHeader?: boolean;
|
|
33
|
+
/** Observability: fired once per transport-level retry (engine busy /
|
|
34
|
+
* restarting). Lets apps count sheds without wrapping every call. */
|
|
35
|
+
onRetry?: (info: {
|
|
36
|
+
path: string;
|
|
37
|
+
code: string;
|
|
38
|
+
attempt: number;
|
|
39
|
+
waitMs: number;
|
|
40
|
+
}) => void;
|
|
24
41
|
}
|
|
25
42
|
/**
|
|
26
43
|
* Per-document binding for CDN URL rewriting. `CloudDocumentHandle`
|
|
@@ -51,6 +68,11 @@ declare class HttpClient {
|
|
|
51
68
|
private readonly base;
|
|
52
69
|
private readonly tokenFn;
|
|
53
70
|
private readonly fetchFn;
|
|
71
|
+
private readonly docAffinityHeader;
|
|
72
|
+
private readonly onRetry;
|
|
73
|
+
private static readonly MAX_RETRIES;
|
|
74
|
+
private static readonly RETRY_AFTER_CAP_MS;
|
|
75
|
+
private static readonly RETRY_AFTER_DEFAULT_MS;
|
|
54
76
|
private readonly sessionId;
|
|
55
77
|
private cdnBinding;
|
|
56
78
|
constructor(opts: HttpClientOptions);
|
|
@@ -158,6 +180,7 @@ declare class HttpClient {
|
|
|
158
180
|
postEmpty(path: string, body: unknown, signal: AbortSignal): Promise<void>;
|
|
159
181
|
private requestJson;
|
|
160
182
|
private request;
|
|
183
|
+
private execute;
|
|
161
184
|
private parseJsonResponse;
|
|
162
185
|
/**
|
|
163
186
|
* Decide where this request actually goes. With no CDN binding (or
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,23 @@ interface HttpClientOptions {
|
|
|
21
21
|
sessionId?: string;
|
|
22
22
|
/** Replace the global fetch (e.g. in Node tests with undici). */
|
|
23
23
|
fetch?: typeof globalThis.fetch;
|
|
24
|
+
/**
|
|
25
|
+
* Document affinity (opt-in; requires a server whose CORS
|
|
26
|
+
* allowlist includes the header): send `X-CloudPDF-Doc` on
|
|
27
|
+
* origin-bound doc requests so consistent-hash load balancers pin a
|
|
28
|
+
* document's traffic to one warm replica. Derived from the request
|
|
29
|
+
* path (`/v1/docs/:docId/…` — the same extraction the Helm chart's
|
|
30
|
+
* uri-mode fallback uses); never sent on CDN-rewritten requests.
|
|
31
|
+
*/
|
|
32
|
+
docAffinityHeader?: boolean;
|
|
33
|
+
/** Observability: fired once per transport-level retry (engine busy /
|
|
34
|
+
* restarting). Lets apps count sheds without wrapping every call. */
|
|
35
|
+
onRetry?: (info: {
|
|
36
|
+
path: string;
|
|
37
|
+
code: string;
|
|
38
|
+
attempt: number;
|
|
39
|
+
waitMs: number;
|
|
40
|
+
}) => void;
|
|
24
41
|
}
|
|
25
42
|
/**
|
|
26
43
|
* Per-document binding for CDN URL rewriting. `CloudDocumentHandle`
|
|
@@ -51,6 +68,11 @@ declare class HttpClient {
|
|
|
51
68
|
private readonly base;
|
|
52
69
|
private readonly tokenFn;
|
|
53
70
|
private readonly fetchFn;
|
|
71
|
+
private readonly docAffinityHeader;
|
|
72
|
+
private readonly onRetry;
|
|
73
|
+
private static readonly MAX_RETRIES;
|
|
74
|
+
private static readonly RETRY_AFTER_CAP_MS;
|
|
75
|
+
private static readonly RETRY_AFTER_DEFAULT_MS;
|
|
54
76
|
private readonly sessionId;
|
|
55
77
|
private cdnBinding;
|
|
56
78
|
constructor(opts: HttpClientOptions);
|
|
@@ -158,6 +180,7 @@ declare class HttpClient {
|
|
|
158
180
|
postEmpty(path: string, body: unknown, signal: AbortSignal): Promise<void>;
|
|
159
181
|
private requestJson;
|
|
160
182
|
private request;
|
|
183
|
+
private execute;
|
|
161
184
|
private parseJsonResponse;
|
|
162
185
|
/**
|
|
163
186
|
* Decide where this request actually goes. With no CDN binding (or
|
package/dist/index.js
CHANGED
|
@@ -809,8 +809,7 @@ var CloudDocumentSecurityService = class {
|
|
|
809
809
|
* and `applyCdnAccess` short-circuits to origin; safe to call always.
|
|
810
810
|
*/
|
|
811
811
|
async postAccess(signal, body) {
|
|
812
|
-
const response = await this.http.postJson(wirePaths.access, {
|
|
813
|
-
docId: this.docId,
|
|
812
|
+
const response = await this.http.postJson(wirePaths.access(this.docId), {
|
|
814
813
|
layerName: this.layerName,
|
|
815
814
|
...body.password ? { password: body.password } : {},
|
|
816
815
|
mode: body.mode
|
|
@@ -1934,7 +1933,7 @@ function fallbackUnknownHead(id) {
|
|
|
1934
1933
|
access: {
|
|
1935
1934
|
required: true,
|
|
1936
1935
|
reasons: ["permissions-unknown"],
|
|
1937
|
-
endpoint: wirePaths.access
|
|
1936
|
+
endpoint: wirePaths.access(id)
|
|
1938
1937
|
}
|
|
1939
1938
|
};
|
|
1940
1939
|
}
|
|
@@ -2042,6 +2041,8 @@ var HttpClient = class HttpClient {
|
|
|
2042
2041
|
_defineProperty(this, "base", void 0);
|
|
2043
2042
|
_defineProperty(this, "tokenFn", void 0);
|
|
2044
2043
|
_defineProperty(this, "fetchFn", void 0);
|
|
2044
|
+
_defineProperty(this, "docAffinityHeader", void 0);
|
|
2045
|
+
_defineProperty(this, "onRetry", void 0);
|
|
2045
2046
|
_defineProperty(this, "sessionId", void 0);
|
|
2046
2047
|
_defineProperty(this, "cdnBinding", null);
|
|
2047
2048
|
this.base = opts.baseUrl.replace(/\/$/, "");
|
|
@@ -2049,6 +2050,8 @@ var HttpClient = class HttpClient {
|
|
|
2049
2050
|
this.tokenFn = token === void 0 ? null : typeof token === "function" ? token : () => token;
|
|
2050
2051
|
this.sessionId = opts.sessionId ?? null;
|
|
2051
2052
|
this.fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
2053
|
+
this.docAffinityHeader = opts.docAffinityHeader ?? false;
|
|
2054
|
+
this.onRetry = opts.onRetry;
|
|
2052
2055
|
}
|
|
2053
2056
|
/** Normalized server base URL (no trailing slash). */
|
|
2054
2057
|
get baseUrl() {
|
|
@@ -2325,7 +2328,7 @@ var HttpClient = class HttpClient {
|
|
|
2325
2328
|
});
|
|
2326
2329
|
}
|
|
2327
2330
|
async request(path, init) {
|
|
2328
|
-
const { url, extraHeader } = this.resolveOutgoing(path);
|
|
2331
|
+
const { url, extraHeader, routedToCdn } = this.resolveOutgoing(path);
|
|
2329
2332
|
const headers = new Headers(init.headers ?? {});
|
|
2330
2333
|
if (this.tokenFn) {
|
|
2331
2334
|
const token = await this.tokenFn();
|
|
@@ -2334,11 +2337,32 @@ var HttpClient = class HttpClient {
|
|
|
2334
2337
|
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
2335
2338
|
if (this.sessionId) headers.set("X-Engine-Session-Id", this.sessionId);
|
|
2336
2339
|
if (extraHeader) headers.set(extraHeader.name, extraHeader.value);
|
|
2337
|
-
|
|
2338
|
-
|
|
2340
|
+
if (this.docAffinityHeader && !routedToCdn) {
|
|
2341
|
+
const doc = /^\/v1\/docs\/([^/]+)\//.exec(path);
|
|
2342
|
+
if (doc) headers.set("X-CloudPDF-Doc", decodeURIComponent(doc[1]));
|
|
2343
|
+
}
|
|
2344
|
+
for (let attempt = 0;; attempt++) {
|
|
2345
|
+
const res = await this.execute(url, {
|
|
2339
2346
|
...init,
|
|
2340
2347
|
headers
|
|
2341
2348
|
});
|
|
2349
|
+
if (res.status !== 503 || attempt >= HttpClient.MAX_RETRIES || isStreamBody(init.body ?? null)) return res;
|
|
2350
|
+
const code = await peekErrorCode(res);
|
|
2351
|
+
if (code === null || !RETRYABLE_503.has(code)) return res;
|
|
2352
|
+
const hinted = Number(res.headers.get("retry-after"));
|
|
2353
|
+
const waitMs = (Number.isFinite(hinted) ? Math.min(Math.max(hinted, 0) * 1e3, HttpClient.RETRY_AFTER_CAP_MS) : HttpClient.RETRY_AFTER_DEFAULT_MS) * (.9 + .2 * Math.random());
|
|
2354
|
+
this.onRetry?.({
|
|
2355
|
+
path,
|
|
2356
|
+
code,
|
|
2357
|
+
attempt,
|
|
2358
|
+
waitMs
|
|
2359
|
+
});
|
|
2360
|
+
await sleepRacingAbort(waitMs, init.signal ?? null);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
async execute(url, init) {
|
|
2364
|
+
try {
|
|
2365
|
+
return await this.fetchFn(url, init);
|
|
2342
2366
|
} catch (err) {
|
|
2343
2367
|
if (init.signal?.aborted) throw new AbortError$1(init.signal.reason ?? err);
|
|
2344
2368
|
if (err?.name === "AbortError") throw new AbortError$1(err.message);
|
|
@@ -2364,7 +2388,8 @@ var HttpClient = class HttpClient {
|
|
|
2364
2388
|
resolveOutgoing(path) {
|
|
2365
2389
|
if (!this.cdnBinding || !this.cdnBinding.cdn) return {
|
|
2366
2390
|
url: `${this.baseUrl}${path}`,
|
|
2367
|
-
extraHeader: null
|
|
2391
|
+
extraHeader: null,
|
|
2392
|
+
routedToCdn: false
|
|
2368
2393
|
};
|
|
2369
2394
|
const applied = applyCdnAccess({
|
|
2370
2395
|
path,
|
|
@@ -2375,7 +2400,8 @@ var HttpClient = class HttpClient {
|
|
|
2375
2400
|
});
|
|
2376
2401
|
return {
|
|
2377
2402
|
url: applied.url,
|
|
2378
|
-
extraHeader: applied.routedToCdn && applied.authHeader ? applied.authHeader : null
|
|
2403
|
+
extraHeader: applied.routedToCdn && applied.authHeader ? applied.authHeader : null,
|
|
2404
|
+
routedToCdn: applied.routedToCdn
|
|
2379
2405
|
};
|
|
2380
2406
|
}
|
|
2381
2407
|
async throwFromBody(res) {
|
|
@@ -2395,6 +2421,9 @@ var HttpClient = class HttpClient {
|
|
|
2395
2421
|
throw new EngineError$1(code, `HTTP ${res.status}`, { details: { status: res.status } });
|
|
2396
2422
|
}
|
|
2397
2423
|
};
|
|
2424
|
+
_defineProperty(HttpClient, "MAX_RETRIES", 2);
|
|
2425
|
+
_defineProperty(HttpClient, "RETRY_AFTER_CAP_MS", 5e3);
|
|
2426
|
+
_defineProperty(HttpClient, "RETRY_AFTER_DEFAULT_MS", 1e3);
|
|
2398
2427
|
function mapStatusToCode(status) {
|
|
2399
2428
|
if (status === 401) return EngineErrorCode$1.Unauthenticated;
|
|
2400
2429
|
if (status === 403) return EngineErrorCode$1.Forbidden;
|
|
@@ -2409,6 +2438,36 @@ function trySerialized(value) {
|
|
|
2409
2438
|
const result = EngineErrorPayloadSchema.safeParse(value);
|
|
2410
2439
|
return result.success ? result.data : null;
|
|
2411
2440
|
}
|
|
2441
|
+
const RETRYABLE_503 = /* @__PURE__ */ new Set(["EngineBusy", "EngineRestarting"]);
|
|
2442
|
+
function isStreamBody(body) {
|
|
2443
|
+
return typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
|
|
2444
|
+
}
|
|
2445
|
+
/** Read the error code from a 503 body without consuming the response. */
|
|
2446
|
+
async function peekErrorCode(res) {
|
|
2447
|
+
try {
|
|
2448
|
+
const code = (await res.clone().json())?.error?.code;
|
|
2449
|
+
return typeof code === "string" ? code : null;
|
|
2450
|
+
} catch {
|
|
2451
|
+
return null;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
function sleepRacingAbort(ms, signal) {
|
|
2455
|
+
return new Promise((resolve, reject) => {
|
|
2456
|
+
if (signal?.aborted) {
|
|
2457
|
+
reject(new AbortError$1(signal.reason ?? "aborted"));
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
const timer = setTimeout(() => {
|
|
2461
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2462
|
+
resolve();
|
|
2463
|
+
}, ms);
|
|
2464
|
+
const onAbort = () => {
|
|
2465
|
+
clearTimeout(timer);
|
|
2466
|
+
reject(new AbortError$1(signal?.reason ?? "aborted"));
|
|
2467
|
+
};
|
|
2468
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2412
2471
|
//#endregion
|
|
2413
2472
|
//#region src/CloudEngine.ts
|
|
2414
2473
|
/**
|