@steipete/oracle 0.17.3 → 0.19.0
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/README.md +1 -1
- package/dist/bin/oracle-cli.js +75 -64
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/actions/archiveConversation.js +31 -7
- package/dist/src/browser/actions/assistantResponse.js +43 -1
- package/dist/src/browser/actions/attachmentContext.js +239 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
- package/dist/src/browser/actions/attachmentEvidence.js +78 -0
- package/dist/src/browser/actions/attachmentProgress.js +41 -0
- package/dist/src/browser/actions/attachmentPrompt.js +130 -0
- package/dist/src/browser/actions/attachments.js +306 -355
- package/dist/src/browser/actions/deepResearch.js +2 -1
- package/dist/src/browser/actions/modelSelection.js +13 -3
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +361 -186
- package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
- package/dist/src/browser/actions/thinkingStatus.js +2 -5
- package/dist/src/browser/actions/thinkingTime.js +267 -15
- package/dist/src/browser/attachRunning.js +46 -2
- package/dist/src/browser/attachmentValidation.js +53 -0
- package/dist/src/browser/cancellation.js +105 -0
- package/dist/src/browser/chatgptImages.js +2 -0
- package/dist/src/browser/chatgptThrottle.js +104 -0
- package/dist/src/browser/chromeLifecycle.js +267 -43
- package/dist/src/browser/config.js +15 -3
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +447 -263
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/policies.js +6 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +88 -17
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
- package/dist/src/browser/reattach.js +14 -4
- package/dist/src/browser/sessionRunner.js +71 -11
- package/dist/src/browser/tabLeaseRegistry.js +414 -38
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +40 -15
- package/dist/src/cli/browserDefaults.js +13 -0
- package/dist/src/cli/browserTabs.js +12 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -47
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/projectSources.js +5 -3
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +9 -2
- package/dist/src/cli/sessionRunner.js +91 -7
- package/dist/src/config.js +1 -0
- package/dist/src/gemini-web/client.js +67 -21
- package/dist/src/gemini-web/executor.js +4 -0
- package/dist/src/gemini-web/http.js +15 -0
- package/dist/src/gemini-web/models.js +15 -11
- package/dist/src/mcp/server.js +11 -14
- package/dist/src/mcp/tools/chatgptImage.js +4 -4
- package/dist/src/mcp/tools/consult.js +97 -26
- package/dist/src/mcp/tools/projectSources.js +4 -6
- package/dist/src/mcp/tools/sessionResources.js +1 -1
- package/dist/src/mcp/tools/sessions.js +2 -2
- package/dist/src/mcp/tools/wait.js +182 -0
- package/dist/src/mcp/types.js +7 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/remote/client.js +70 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +351 -72
- package/dist/src/sessionManager.js +36 -1
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +25 -23
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -683
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -400
- package/dist/docs-site/browser-mode.html +0 -588
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -436
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -368
- package/dist/docs-site/gemini.html +0 -379
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -595
- package/dist/docs-site/mcp.html +0 -368
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -387
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/profileSync.js +0 -141
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -6,30 +6,43 @@ import path from "node:path";
|
|
|
6
6
|
import { mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
|
7
7
|
import { appendArtifacts, computeFileSha256, resolveSessionArtifactsDir, resolveUniqueArtifactPath, sanitizeArtifactFilename, sanitizeArtifactMimeType, validateArtifactFile, } from "../browser/artifacts.js";
|
|
8
8
|
import { MAX_REMOTE_ARTIFACT_BYTES, } from "./types.js";
|
|
9
|
+
import { materializeStagedFallbackBundle } from "../browser/prompt.js";
|
|
10
|
+
import { checkRemoteHealth } from "./health.js";
|
|
9
11
|
import { parseHostPort } from "../bridge/connection.js";
|
|
12
|
+
import { BrowserRunCancelledError } from "../oracle/errors.js";
|
|
10
13
|
export function createRemoteBrowserExecutor({ host, token }) {
|
|
11
14
|
// Return a drop-in replacement for runBrowserMode so the browser session runner can stay unchanged.
|
|
12
15
|
return async function remoteBrowserExecutor(options) {
|
|
16
|
+
const callerSignal = options.signal;
|
|
17
|
+
if (callerSignal?.aborted)
|
|
18
|
+
throw new BrowserRunCancelledError("Browser run cancelled before the request was sent.");
|
|
19
|
+
if (callerSignal) {
|
|
20
|
+
const health = await checkRemoteHealth({ host, token, signal: callerSignal });
|
|
21
|
+
if (callerSignal.aborted)
|
|
22
|
+
throw new BrowserRunCancelledError();
|
|
23
|
+
if (health.capabilities?.runCancellation !== true)
|
|
24
|
+
throw new Error("Remote host does not support run cancellation; upgrade the host before using an AbortSignal.");
|
|
25
|
+
}
|
|
13
26
|
const payload = {
|
|
14
27
|
prompt: options.prompt,
|
|
15
28
|
attachments: await serializeAttachments(options.attachments ?? []),
|
|
16
|
-
fallbackSubmission: options.fallbackSubmission
|
|
17
|
-
? {
|
|
18
|
-
prompt: options.fallbackSubmission.prompt,
|
|
19
|
-
attachments: await serializeAttachments(options.fallbackSubmission.attachments ?? []),
|
|
20
|
-
}
|
|
21
|
-
: undefined,
|
|
29
|
+
fallbackSubmission: await serializeFallback(options.fallbackSubmission, { host, token }),
|
|
22
30
|
browserConfig: options.config ?? {},
|
|
23
31
|
options: {
|
|
24
32
|
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
25
33
|
verbose: options.verbose,
|
|
26
34
|
sessionId: options.sessionId,
|
|
27
35
|
followUpPrompts: options.followUpPrompts,
|
|
36
|
+
cancelOnDisconnect: callerSignal ? true : undefined,
|
|
28
37
|
},
|
|
29
38
|
};
|
|
30
39
|
const body = Buffer.from(JSON.stringify(payload));
|
|
31
40
|
const { hostname, port } = parseHost(host);
|
|
32
41
|
return new Promise((resolve, reject) => {
|
|
42
|
+
if (callerSignal?.aborted) {
|
|
43
|
+
reject(new Error("Browser run cancelled before the request was sent."));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
33
46
|
const transferredFiles = [];
|
|
34
47
|
const transferFailures = [];
|
|
35
48
|
const transferPromises = [];
|
|
@@ -40,6 +53,7 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
40
53
|
if (settled)
|
|
41
54
|
return;
|
|
42
55
|
settled = true;
|
|
56
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
43
57
|
reject(error);
|
|
44
58
|
};
|
|
45
59
|
const req = http.request({
|
|
@@ -107,17 +121,55 @@ export function createRemoteBrowserExecutor({ host, token }) {
|
|
|
107
121
|
return;
|
|
108
122
|
}
|
|
109
123
|
settled = true;
|
|
124
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
110
125
|
resolve(mergeTransferredArtifacts(resolved, transferredFiles, transferFailures));
|
|
111
126
|
})().catch(fail);
|
|
112
127
|
});
|
|
113
128
|
res.on("error", fail);
|
|
114
129
|
});
|
|
115
130
|
req.on("error", fail);
|
|
131
|
+
// Destroying the request closes the socket, which is how the service learns
|
|
132
|
+
// to abort: its own disconnect handler fires and releases the slot and the
|
|
133
|
+
// browser tab.
|
|
134
|
+
const onCallerAbort = () => {
|
|
135
|
+
req.destroy();
|
|
136
|
+
fail(new BrowserRunCancelledError("Browser run cancelled: the caller aborted."));
|
|
137
|
+
};
|
|
138
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
116
139
|
req.write(body);
|
|
117
140
|
req.end();
|
|
118
141
|
});
|
|
119
142
|
};
|
|
120
143
|
}
|
|
144
|
+
async function serializeFallback(fallback, remote) {
|
|
145
|
+
if (!fallback)
|
|
146
|
+
return undefined;
|
|
147
|
+
if (fallback.pendingBundle) {
|
|
148
|
+
const health = await checkRemoteHealth(remote);
|
|
149
|
+
if (!health.ok || health.capabilities?.deferredFallbackBundling !== true) {
|
|
150
|
+
// Older hosts ignore bundle metadata, so send a ready-to-upload fallback.
|
|
151
|
+
const prepared = await materializeStagedFallbackBundle({
|
|
152
|
+
composerText: fallback.prompt,
|
|
153
|
+
attachments: fallback.attachments,
|
|
154
|
+
...fallback.pendingBundle,
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
return {
|
|
158
|
+
prompt: prepared.composerText,
|
|
159
|
+
attachments: await serializeAttachments(prepared.attachments),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await rm(path.dirname(prepared.bundled.bundlePath), { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
prompt: fallback.prompt,
|
|
169
|
+
attachments: await serializeAttachments(fallback.attachments),
|
|
170
|
+
bundle: fallback.pendingBundle,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
121
173
|
async function serializeAttachments(attachments) {
|
|
122
174
|
const serialized = [];
|
|
123
175
|
for (const attachment of attachments) {
|
|
@@ -173,12 +225,17 @@ function handleEvent(params) {
|
|
|
173
225
|
token: params.token,
|
|
174
226
|
descriptor: event.artifact,
|
|
175
227
|
sessionId: params.options.sessionId,
|
|
228
|
+
signal: params.options.signal,
|
|
176
229
|
log: params.options.log,
|
|
177
230
|
})
|
|
178
231
|
.then((artifact) => {
|
|
179
232
|
params.onArtifact(artifact);
|
|
180
233
|
})
|
|
181
234
|
.catch((error) => {
|
|
235
|
+
if (params.options.signal?.aborted) {
|
|
236
|
+
params.onError(new BrowserRunCancelledError());
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
182
239
|
const message = error instanceof Error ? error.message : String(error);
|
|
183
240
|
const fallback = `Oracle captured the browser text response, but bridge artifact transfer failed for ${displayFilename}. Open the ChatGPT browser on the bridge host, download the ZIP/file shown in the current response, and copy it to a cloud-readable path. Reason: ${message}`;
|
|
184
241
|
params.options.log?.(`[browser] ${fallback}`);
|
|
@@ -192,6 +249,7 @@ function handleEvent(params) {
|
|
|
192
249
|
return null;
|
|
193
250
|
}
|
|
194
251
|
async function transferRemoteArtifact(params) {
|
|
252
|
+
params.signal?.throwIfAborted();
|
|
195
253
|
validateRemoteArtifactDescriptor(params.descriptor);
|
|
196
254
|
const sessionId = params.sessionId ?? params.descriptor.runId;
|
|
197
255
|
const artifactsDir = resolveSessionArtifactsDir(sessionId);
|
|
@@ -208,6 +266,7 @@ async function transferRemoteArtifact(params) {
|
|
|
208
266
|
token: params.token,
|
|
209
267
|
targetPath: partPath,
|
|
210
268
|
descriptor: params.descriptor,
|
|
269
|
+
signal: params.signal,
|
|
211
270
|
}).catch(async (error) => {
|
|
212
271
|
await rm(partPath, { force: true }).catch(() => undefined);
|
|
213
272
|
throw error;
|
|
@@ -231,6 +290,10 @@ async function transferRemoteArtifact(params) {
|
|
|
231
290
|
await rm(partPath, { force: true }).catch(() => undefined);
|
|
232
291
|
throw new Error(`${validation.type} validation failed: ${validation.error ?? "invalid"}`);
|
|
233
292
|
}
|
|
293
|
+
if (params.signal?.aborted) {
|
|
294
|
+
await rm(partPath, { force: true });
|
|
295
|
+
throw new BrowserRunCancelledError();
|
|
296
|
+
}
|
|
234
297
|
await rename(partPath, finalPath);
|
|
235
298
|
params.log?.(`[browser] Transferred artifact to ${finalPath}`);
|
|
236
299
|
const publishedFilename = path.basename(finalPath);
|
|
@@ -257,6 +320,7 @@ async function downloadArtifactToFile(params) {
|
|
|
257
320
|
port: params.port,
|
|
258
321
|
path: params.path,
|
|
259
322
|
method: "GET",
|
|
323
|
+
signal: params.signal,
|
|
260
324
|
headers: params.token ? { authorization: `Bearer ${params.token}` } : undefined,
|
|
261
325
|
}, (res) => {
|
|
262
326
|
if (res.statusCode !== 200) {
|
|
@@ -30,7 +30,7 @@ export async function checkTcpConnection(host, timeoutMs = 2000) {
|
|
|
30
30
|
socket.once("timeout", onTimeout);
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
-
export async function checkRemoteHealth({ host, token, timeoutMs = 5000, }) {
|
|
33
|
+
export async function checkRemoteHealth({ host, token, timeoutMs = 5000, signal, }) {
|
|
34
34
|
const { hostname, port } = parseHostPort(host);
|
|
35
35
|
const headers = { accept: "application/json" };
|
|
36
36
|
if (token) {
|
|
@@ -43,6 +43,7 @@ export async function checkRemoteHealth({ host, token, timeoutMs = 5000, }) {
|
|
|
43
43
|
path: "/health",
|
|
44
44
|
headers,
|
|
45
45
|
timeoutMs,
|
|
46
|
+
signal,
|
|
46
47
|
});
|
|
47
48
|
if (response.statusCode === 200 && typeof response.json === "object" && response.json) {
|
|
48
49
|
const ok = response.json.ok === true;
|
|
@@ -90,6 +91,8 @@ function parseCapabilities(value) {
|
|
|
90
91
|
return undefined;
|
|
91
92
|
}
|
|
92
93
|
return {
|
|
94
|
+
...(raw.deferredFallbackBundling === true ? { deferredFallbackBundling: true } : {}),
|
|
95
|
+
...(raw.runCancellation === true ? { runCancellation: true } : {}),
|
|
93
96
|
artifactTransfer: true,
|
|
94
97
|
artifactProtocolVersion,
|
|
95
98
|
maxArtifactBytes: Math.min(maxArtifactBytes, MAX_REMOTE_ARTIFACT_BYTES),
|
|
@@ -105,7 +108,7 @@ function extractErrorMessage(json, bodyText) {
|
|
|
105
108
|
const trimmed = bodyText.trim();
|
|
106
109
|
return trimmed.length ? trimmed : null;
|
|
107
110
|
}
|
|
108
|
-
async function requestJson({ hostname, port, path, headers, timeoutMs, }) {
|
|
111
|
+
async function requestJson({ hostname, port, path, headers, timeoutMs, signal, }) {
|
|
109
112
|
return await new Promise((resolve, reject) => {
|
|
110
113
|
const req = http.request({
|
|
111
114
|
hostname,
|
|
@@ -113,6 +116,7 @@ async function requestJson({ hostname, port, path, headers, timeoutMs, }) {
|
|
|
113
116
|
path,
|
|
114
117
|
method: "GET",
|
|
115
118
|
headers,
|
|
119
|
+
signal,
|
|
116
120
|
}, (res) => {
|
|
117
121
|
res.setEncoding("utf8");
|
|
118
122
|
let body = "";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export class RunQueueFullError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("The browser run queue is full.");
|
|
4
|
+
this.name = "RunQueueFullError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class RunSlots {
|
|
8
|
+
maxConcurrent;
|
|
9
|
+
maxQueued;
|
|
10
|
+
active = 0;
|
|
11
|
+
waiting = [];
|
|
12
|
+
constructor(maxConcurrent, maxQueued) {
|
|
13
|
+
this.maxConcurrent = maxConcurrent;
|
|
14
|
+
this.maxQueued = maxQueued;
|
|
15
|
+
if (!Number.isSafeInteger(maxConcurrent) ||
|
|
16
|
+
maxConcurrent < 1 ||
|
|
17
|
+
!Number.isSafeInteger(maxQueued) ||
|
|
18
|
+
maxQueued < 0) {
|
|
19
|
+
throw new Error("Run capacity must be a positive integer and queue capacity a nonnegative integer.");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
get activeCount() {
|
|
23
|
+
return this.active;
|
|
24
|
+
}
|
|
25
|
+
get queuedCount() {
|
|
26
|
+
return this.waiting.length;
|
|
27
|
+
}
|
|
28
|
+
get capacity() {
|
|
29
|
+
return this.maxConcurrent;
|
|
30
|
+
}
|
|
31
|
+
get queueCapacity() {
|
|
32
|
+
return this.maxQueued;
|
|
33
|
+
}
|
|
34
|
+
get isSaturated() {
|
|
35
|
+
return this.active >= this.maxConcurrent && this.waiting.length >= this.maxQueued;
|
|
36
|
+
}
|
|
37
|
+
positionFor() {
|
|
38
|
+
return this.active < this.maxConcurrent ? 0 : this.waiting.length + 1;
|
|
39
|
+
}
|
|
40
|
+
// Reservation is synchronous, including the queue bound; request-body reads cannot race it.
|
|
41
|
+
acquire(signal) {
|
|
42
|
+
if (signal?.aborted)
|
|
43
|
+
return Promise.reject(new Error("cancelled before a slot was available"));
|
|
44
|
+
if (this.active < this.maxConcurrent) {
|
|
45
|
+
this.active++;
|
|
46
|
+
return Promise.resolve(this.makeRelease());
|
|
47
|
+
}
|
|
48
|
+
if (this.waiting.length >= this.maxQueued)
|
|
49
|
+
return Promise.reject(new RunQueueFullError());
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const onAbort = () => {
|
|
52
|
+
const index = this.waiting.indexOf(entry);
|
|
53
|
+
if (index < 0)
|
|
54
|
+
return;
|
|
55
|
+
this.waiting.splice(index, 1);
|
|
56
|
+
entry.cleanup();
|
|
57
|
+
reject(new Error("cancelled while waiting for a slot"));
|
|
58
|
+
};
|
|
59
|
+
const entry = {
|
|
60
|
+
resolve,
|
|
61
|
+
reject,
|
|
62
|
+
cleanup: () => signal?.removeEventListener("abort", onAbort),
|
|
63
|
+
};
|
|
64
|
+
this.waiting.push(entry);
|
|
65
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
makeRelease() {
|
|
69
|
+
let released = false;
|
|
70
|
+
return () => {
|
|
71
|
+
if (released)
|
|
72
|
+
return;
|
|
73
|
+
released = true;
|
|
74
|
+
const next = this.waiting.shift();
|
|
75
|
+
if (next) {
|
|
76
|
+
next.cleanup();
|
|
77
|
+
next.resolve(this.makeRelease());
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
this.active--;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|