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.
- package/dist/bundling-sources/sdk/src/client.ts +16 -523
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/tool-calls.ts +530 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +33 -415
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-http-request.ts +419 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-reliability-policy.ts +10 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +20 -1
- package/dist/cli/index.js +831 -800
- package/dist/cli/index.mjs +831 -795
- package/dist/index.d.mts +191 -190
- package/dist/index.d.ts +191 -190
- package/dist/index.js +489 -472
- package/dist/index.mjs +489 -472
- package/dist/install-integrity.json +2 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|