deepline 0.3.231 → 0.3.233

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.
@@ -0,0 +1,419 @@
1
+ import type { SafeFetchImplementation } from '../security/safe-fetch';
2
+ import type { RuntimeResourceGovernor } from './resource-governor';
3
+ import { CTX_FETCH_EGRESS_TOOL_ID } from './builtin-pacing';
4
+ import {
5
+ positiveTimeoutMs,
6
+ RUNTIME_RELIABILITY_POLICY,
7
+ } from './runtime-reliability-policy';
8
+ import {
9
+ TOOL_EXECUTE_BARE_RATE_LIMIT_MAX_ATTEMPTS,
10
+ TOOL_EXECUTE_RATE_LIMIT_MAX_ATTEMPTS,
11
+ TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS,
12
+ TOOL_EXECUTE_RATE_LIMIT_RETRY_DELAY_MAX_MS,
13
+ parseToolExecuteRetryAfterMs,
14
+ } from './tool-execute-retry-policy';
15
+
16
+ const FETCH_TRANSPORT_MAX_ATTEMPTS =
17
+ RUNTIME_RELIABILITY_POLICY.egress.fetchMaxAttempts;
18
+ const FETCH_TRANSPORT_RETRY_DELAY_MS = 100;
19
+ const CTX_FETCH_HEADERS_TIMEOUT_MS =
20
+ RUNTIME_RELIABILITY_POLICY.egress.fetchHeadersTimeoutMs;
21
+ const CTX_FETCH_BODY_TIMEOUT_MS =
22
+ RUNTIME_RELIABILITY_POLICY.egress.fetchBodyTimeoutMs;
23
+ const CTX_FETCH_TOTAL_TIMEOUT_MS =
24
+ RUNTIME_RELIABILITY_POLICY.egress.fetchTotalTimeoutMs;
25
+
26
+ type SafeFetchModule = typeof import('../security/safe-fetch');
27
+ let safeFetchModule: Promise<SafeFetchModule> | null = null;
28
+ function loadSafeFetch(): Promise<SafeFetchModule> {
29
+ safeFetchModule ??= import('../security/safe-fetch');
30
+ return safeFetchModule;
31
+ }
32
+
33
+ function isUnsafeOutboundUrlError(error: unknown): boolean {
34
+ return error instanceof Error && error.name === 'UnsafeOutboundUrlError';
35
+ }
36
+
37
+ export class CtxFetchTimeoutError extends Error {
38
+ readonly code = 'CTX_FETCH_TIMEOUT';
39
+
40
+ constructor(
41
+ readonly phase: 'headers' | 'body' | 'total',
42
+ readonly timeoutMs: number,
43
+ ) {
44
+ super(
45
+ `[CTX_FETCH_TIMEOUT phase=${phase} timeout_ms=${timeoutMs}] ` +
46
+ `ctx.fetch exceeded its ${phase} deadline after ${timeoutMs}ms.`,
47
+ );
48
+ this.name = 'CtxFetchTimeoutError';
49
+ }
50
+ }
51
+
52
+ type CtxFetchDeadline = {
53
+ bodyMs: number;
54
+ headersMs: number;
55
+ startedAt: number;
56
+ totalMs: number;
57
+ };
58
+
59
+ function remainingCtxFetchMs(deadline: CtxFetchDeadline): number {
60
+ return Math.max(0, deadline.totalMs - (Date.now() - deadline.startedAt));
61
+ }
62
+
63
+ async function runCtxFetchPhase<T>(input: {
64
+ callerSignal?: AbortSignal | null;
65
+ controller: AbortController;
66
+ deadline: CtxFetchDeadline;
67
+ phase: 'headers' | 'body' | 'total';
68
+ run: () => Promise<T>;
69
+ }): Promise<T> {
70
+ const remainingMs = remainingCtxFetchMs(input.deadline);
71
+ if (remainingMs <= 0) {
72
+ throw new CtxFetchTimeoutError('total', input.deadline.totalMs);
73
+ }
74
+ if (input.callerSignal?.aborted) {
75
+ throw (
76
+ input.callerSignal.reason ?? new DOMException('Aborted', 'AbortError')
77
+ );
78
+ }
79
+ const phaseLimitMs =
80
+ input.phase === 'headers'
81
+ ? input.deadline.headersMs
82
+ : input.phase === 'body'
83
+ ? input.deadline.bodyMs
84
+ : remainingMs;
85
+ const timeoutMs = Math.min(phaseLimitMs, remainingMs);
86
+ const timeoutPhase = timeoutMs >= remainingMs ? 'total' : input.phase;
87
+ let timeout: ReturnType<typeof setTimeout> | null = null;
88
+ let onCallerAbort: (() => void) | null = null;
89
+ const phaseTimeout = new Promise<never>((_resolve, reject) => {
90
+ timeout = setTimeout(() => {
91
+ const error = new CtxFetchTimeoutError(
92
+ timeoutPhase,
93
+ timeoutPhase === 'total' ? input.deadline.totalMs : phaseLimitMs,
94
+ );
95
+ reject(error);
96
+ input.controller.abort(error);
97
+ }, timeoutMs);
98
+ });
99
+ const callerAbort = input.callerSignal
100
+ ? new Promise<never>((_resolve, reject) => {
101
+ onCallerAbort = () => {
102
+ const reason =
103
+ input.callerSignal?.reason ??
104
+ new DOMException('Aborted', 'AbortError');
105
+ reject(reason);
106
+ input.controller.abort(reason);
107
+ };
108
+ input.callerSignal!.addEventListener('abort', onCallerAbort, {
109
+ once: true,
110
+ });
111
+ })
112
+ : null;
113
+ try {
114
+ return await Promise.race([
115
+ input.run(),
116
+ phaseTimeout,
117
+ ...(callerAbort ? [callerAbort] : []),
118
+ ]);
119
+ } finally {
120
+ if (timeout) clearTimeout(timeout);
121
+ if (onCallerAbort) {
122
+ input.callerSignal?.removeEventListener('abort', onCallerAbort);
123
+ }
124
+ }
125
+ }
126
+
127
+ async function sleepWithinCtxFetchDeadline(input: {
128
+ callerSignal?: AbortSignal | null;
129
+ deadline: CtxFetchDeadline;
130
+ delayMs: number;
131
+ }): Promise<void> {
132
+ const controller = new AbortController();
133
+ let timer: ReturnType<typeof setTimeout> | undefined;
134
+ try {
135
+ await runCtxFetchPhase({
136
+ callerSignal: input.callerSignal,
137
+ controller,
138
+ deadline: input.deadline,
139
+ phase: 'total',
140
+ run: () =>
141
+ new Promise<void>((resolve) => {
142
+ timer = setTimeout(resolve, input.delayMs);
143
+ }),
144
+ });
145
+ } finally {
146
+ if (timer !== undefined) clearTimeout(timer);
147
+ }
148
+ }
149
+
150
+ function isCtxFetchBackpressure(response: Response, bodyText: string): boolean {
151
+ if (response.status === 429) return true;
152
+ if (response.status !== 503) return false;
153
+ // This Salesforce endpoint reports a rejected burst as 503. Match the final
154
+ // destination and exact response contract; arbitrary 503s remain failures.
155
+ try {
156
+ const destination = new URL(response.url);
157
+ const body: unknown = JSON.parse(bodyText);
158
+ return (
159
+ destination.protocol === 'https:' &&
160
+ destination.hostname.endsWith('.salesforce-sites.com') &&
161
+ body !== null &&
162
+ typeof body === 'object' &&
163
+ !Array.isArray(body) &&
164
+ (body as Record<string, unknown>).status === 'burst_circuit_open'
165
+ );
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+
171
+ async function readCtxFetchBody(input: {
172
+ callerSignal?: AbortSignal | null;
173
+ controller: AbortController;
174
+ deadline: CtxFetchDeadline;
175
+ response: Response;
176
+ }): Promise<string> {
177
+ if (!input.response.body) return '';
178
+ const reader = input.response.body.getReader();
179
+ const decoder = new TextDecoder();
180
+ try {
181
+ return await runCtxFetchPhase({
182
+ callerSignal: input.callerSignal,
183
+ controller: input.controller,
184
+ deadline: input.deadline,
185
+ phase: 'body',
186
+ run: async () => {
187
+ let body = '';
188
+ for (;;) {
189
+ const chunk = await reader.read();
190
+ if (chunk.done) return body + decoder.decode();
191
+ body += decoder.decode(chunk.value, { stream: true });
192
+ }
193
+ },
194
+ });
195
+ } catch (error) {
196
+ // Cancellation is socket hygiene, not part of the timeout contract. A
197
+ // hostile/custom stream may never resolve its cancel hook; awaiting it
198
+ // here would recreate the exact forever-pending ctx.fetch this deadline
199
+ // is meant to eliminate.
200
+ void reader.cancel(error).catch(() => {});
201
+ throw error;
202
+ } finally {
203
+ reader.releaseLock();
204
+ }
205
+ }
206
+
207
+ /** Sends one logical Play HTTP request within its permit and I/O budgets.
208
+ * Durable receipt policy and secret redaction belong to the caller.
209
+ */
210
+ export async function sendPlayHttpRequest(input: {
211
+ key: string;
212
+ url: string;
213
+ init: RequestInit;
214
+ governor: Pick<RuntimeResourceGovernor, 'acquireProviderPermit'>;
215
+ sensitiveHeaders: string[];
216
+ timeouts?: { headersMs?: number; bodyMs?: number; totalMs?: number };
217
+ fetchImpl?: SafeFetchImplementation;
218
+ log: (message: string) => void;
219
+ }): Promise<{ response: Response; bodyText: string }> {
220
+ const { url, init } = input;
221
+ const method = String(init.method ?? 'GET').toUpperCase();
222
+ const fetchDeadline: CtxFetchDeadline = {
223
+ startedAt: Date.now(),
224
+ headersMs: positiveTimeoutMs(
225
+ input.timeouts?.headersMs,
226
+ CTX_FETCH_HEADERS_TIMEOUT_MS,
227
+ ),
228
+ bodyMs: positiveTimeoutMs(
229
+ input.timeouts?.bodyMs,
230
+ CTX_FETCH_BODY_TIMEOUT_MS,
231
+ ),
232
+ totalMs: positiveTimeoutMs(
233
+ input.timeouts?.totalMs,
234
+ CTX_FETCH_TOTAL_TIMEOUT_MS,
235
+ ),
236
+ };
237
+ let response: Response | null = null;
238
+ let bodyText: string | null = null;
239
+ // Only replay a body whose bytes are stable. In particular, a
240
+ // consumed ReadableStream cannot be retried even with a write key.
241
+ const replayableBody = init.body == null || typeof init.body === 'string';
242
+ const canRetryTransport =
243
+ replayableBody && ['GET', 'HEAD', 'OPTIONS'].includes(method);
244
+ const { safePublicFetch } = await loadSafeFetch();
245
+ let transportAttempt = 1;
246
+ let rateLimitAttempt = 0;
247
+ while (true) {
248
+ const attemptController = new AbortController();
249
+ let permit: Awaited<
250
+ ReturnType<RuntimeResourceGovernor['acquireProviderPermit']>
251
+ > | null = null;
252
+ let dispatched = false;
253
+ const releasePhysicalPermit = async () => {
254
+ const current = permit;
255
+ permit = null;
256
+ if (!current) return;
257
+ if (dispatched) current.release();
258
+ else await current.cancel();
259
+ dispatched = false;
260
+ };
261
+ let transportRetry = false;
262
+ try {
263
+ const attemptInit = {
264
+ ...init,
265
+ signal: init.signal
266
+ ? AbortSignal.any([init.signal, attemptController.signal])
267
+ : attemptController.signal,
268
+ };
269
+ response = await runCtxFetchPhase({
270
+ callerSignal: init.signal,
271
+ controller: attemptController,
272
+ deadline: fetchDeadline,
273
+ phase: 'total',
274
+ run: () =>
275
+ safePublicFetch(url, attemptInit, {
276
+ // safePublicFetch validates each URL before invoking this
277
+ // wrapper. Redirect hops are physical dispatches too.
278
+ fetchImpl: async (physicalUrl, physicalInit) => {
279
+ await releasePhysicalPermit();
280
+ const signal = attemptInit.signal;
281
+ permit = await runCtxFetchPhase({
282
+ callerSignal: init.signal,
283
+ controller: attemptController,
284
+ deadline: fetchDeadline,
285
+ phase: 'total',
286
+ run: async () => {
287
+ for (;;) {
288
+ const acquired = await input.governor.acquireProviderPermit(
289
+ { toolId: CTX_FETCH_EGRESS_TOOL_ID, signal },
290
+ );
291
+ try {
292
+ signal.throwIfAborted();
293
+ const valid =
294
+ !acquired.validate || (await acquired.validate());
295
+ signal.throwIfAborted();
296
+ if (valid) return acquired;
297
+ } catch (error) {
298
+ await acquired.cancel();
299
+ throw error;
300
+ }
301
+ await acquired.cancel();
302
+ }
303
+ },
304
+ });
305
+ return await runCtxFetchPhase({
306
+ callerSignal: init.signal,
307
+ controller: attemptController,
308
+ deadline: fetchDeadline,
309
+ phase: 'headers',
310
+ run: () => {
311
+ dispatched = true;
312
+ return (input.fetchImpl ?? globalThis.fetch)(
313
+ physicalUrl,
314
+ physicalInit,
315
+ );
316
+ },
317
+ });
318
+ },
319
+ sensitiveHeaders: input.sensitiveHeaders,
320
+ stripHeadersOnCrossOriginRedirect: true,
321
+ headersOnCrossOriginRedirect: {
322
+ 'user-agent': 'Deepline Play Runtime',
323
+ },
324
+ }),
325
+ });
326
+ bodyText = await readCtxFetchBody({
327
+ callerSignal: init.signal,
328
+ controller: attemptController,
329
+ deadline: fetchDeadline,
330
+ response,
331
+ });
332
+ } catch (error) {
333
+ void response?.body?.cancel(error).catch(() => undefined);
334
+ response = null;
335
+ bodyText = null;
336
+ if (error instanceof CtxFetchTimeoutError) throw error;
337
+ if (init.signal?.aborted) {
338
+ throw init.signal.reason ?? error;
339
+ }
340
+ if (isUnsafeOutboundUrlError(error)) {
341
+ throw error;
342
+ }
343
+ const message = error instanceof Error ? error.message : String(error);
344
+ if (
345
+ canRetryTransport &&
346
+ transportAttempt < FETCH_TRANSPORT_MAX_ATTEMPTS
347
+ ) {
348
+ input.log(
349
+ `ctx.fetch(${method} ${url}) transport failed on attempt ${transportAttempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}; retrying: ${message}`,
350
+ );
351
+ transportRetry = true;
352
+ } else {
353
+ throw new Error(
354
+ `ctx.fetch(${method} ${url}) failed on attempt ${transportAttempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}: ${message}`,
355
+ );
356
+ }
357
+ } finally {
358
+ await releasePhysicalPermit();
359
+ }
360
+ if (transportRetry) {
361
+ await sleepWithinCtxFetchDeadline({
362
+ callerSignal: init.signal,
363
+ deadline: fetchDeadline,
364
+ delayMs: FETCH_TRANSPORT_RETRY_DELAY_MS * transportAttempt,
365
+ });
366
+ transportAttempt += 1;
367
+ continue;
368
+ }
369
+ if (
370
+ !response ||
371
+ bodyText === null ||
372
+ !replayableBody ||
373
+ !isCtxFetchBackpressure(response, bodyText)
374
+ )
375
+ break;
376
+ rateLimitAttempt += 1;
377
+ const retryAfter = response.headers.get('retry-after');
378
+ const attemptCap = retryAfter?.trim()
379
+ ? TOOL_EXECUTE_RATE_LIMIT_MAX_ATTEMPTS
380
+ : TOOL_EXECUTE_BARE_RATE_LIMIT_MAX_ATTEMPTS;
381
+ if (rateLimitAttempt >= attemptCap) break;
382
+ const delayMs = Math.max(
383
+ parseToolExecuteRetryAfterMs(retryAfter),
384
+ Math.min(
385
+ TOOL_EXECUTE_RATE_LIMIT_RETRY_DELAY_MAX_MS,
386
+ TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS * rateLimitAttempt,
387
+ ),
388
+ );
389
+ input.log(
390
+ `[runtime.ctx_fetch_backpressure] ${JSON.stringify({
391
+ key: input.key,
392
+ method,
393
+ origin: new URL(response.url || url).origin,
394
+ http_status: response.status,
395
+ attempt: rateLimitAttempt,
396
+ max_attempts: attemptCap,
397
+ retry_delay_ms: delayMs,
398
+ })}`,
399
+ );
400
+ // Destination feedback must not cool the shared generic_http
401
+ // bucket used by unrelated websites. Pace this logical request.
402
+ await sleepWithinCtxFetchDeadline({
403
+ callerSignal: init.signal,
404
+ deadline: fetchDeadline,
405
+ delayMs,
406
+ });
407
+ }
408
+ if (!response) {
409
+ throw new Error(
410
+ `ctx.fetch(${method} ${url}) failed before receiving a response.`,
411
+ );
412
+ }
413
+ if (bodyText === null) {
414
+ throw new Error(
415
+ `ctx.fetch(${method} ${url}) failed while reading the response body.`,
416
+ );
417
+ }
418
+ return { response, bodyText };
419
+ }
@@ -389,3 +389,13 @@ export function resolveRuntimeReliabilityPolicy(
389
389
  }
390
390
 
391
391
  export const RUNTIME_RELIABILITY_POLICY = resolveRuntimeReliabilityPolicy();
392
+
393
+ /** Resolve optional runtime intervals and I/O deadlines with the same fallback policy. */
394
+ export function positiveTimeoutMs(
395
+ value: number | undefined,
396
+ fallback: number,
397
+ ): number {
398
+ return value === undefined || !Number.isFinite(value) || value <= 0
399
+ ? fallback
400
+ : Math.max(1, Math.floor(value));
401
+ }
@@ -7091,6 +7091,7 @@ export type AbsurdWorkerWakeHook = ((
7091
7091
  wake: () => void,
7092
7092
  ) => (() => void) | void) & {
7093
7093
  isListening: () => boolean;
7094
+ close: () => void;
7094
7095
  };
7095
7096
 
7096
7097
  export function resolveAbsurdIdlePollFallbackMs(
@@ -7117,7 +7118,10 @@ export function createAbsurdWorkerWakeHook(input: {
7117
7118
  }): AbsurdWorkerWakeHook | undefined {
7118
7119
  if (input.draining || input.useDirectNotify === false) return undefined;
7119
7120
  let listening = false;
7121
+ let closed = false;
7122
+ const subscriptions = new Set<() => void>();
7120
7123
  const hook: AbsurdWorkerWakeHook = (wake) => {
7124
+ if (closed) return;
7121
7125
  const acquireNotifyHub =
7122
7126
  input.acquireNotifyHub ??
7123
7127
  ((notifyInput: {
@@ -7143,13 +7147,24 @@ export function createAbsurdWorkerWakeHook(input: {
7143
7147
  listening = state === 'listening';
7144
7148
  if (listening || wasListening) wake();
7145
7149
  });
7146
- return () => {
7150
+ let released = false;
7151
+ const release = () => {
7152
+ if (released) return;
7153
+ released = true;
7147
7154
  unsubscribeNotify();
7148
7155
  unsubscribeState();
7149
7156
  lease.release();
7157
+ subscriptions.delete(release);
7150
7158
  };
7159
+ subscriptions.add(release);
7160
+ return release;
7151
7161
  };
7152
7162
  hook.isListening = () => listening;
7163
+ hook.close = () => {
7164
+ closed = true;
7165
+ listening = false;
7166
+ for (const release of subscriptions) release();
7167
+ };
7153
7168
  return hook;
7154
7169
  }
7155
7170
 
@@ -7530,6 +7545,10 @@ export async function startRuntimeWorkerHost(
7530
7545
 
7531
7546
  return {
7532
7547
  close: async () => {
7548
+ // Accepted handlers and maintenance can take minutes to settle. Stop
7549
+ // owning a direct LISTEN session before waiting for that drain, so a
7550
+ // replacement can elect its listener without exhausting primary slots.
7551
+ onWake?.close();
7533
7552
  drainStopped = true;
7534
7553
  billingStopped = true;
7535
7554
  cleanupStopped = true;