@opengeni/api-router 2.10.0-canary.0 → 2.10.0-canary.2
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/app.js +1 -1
- package/dist/{chunk-XXH7DW34.js → chunk-BVX5ASFY.js} +1559 -798
- package/dist/chunk-BVX5ASFY.js.map +1 -0
- package/dist/codemode.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/command-read.d.ts +24 -0
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/session-event-view.d.ts +86 -0
- package/dist/sandbox/channel-a.d.ts +2 -0
- package/dist/sandbox/rematerialize.d.ts +2 -0
- package/dist/sandbox/viewer.d.ts +2 -0
- package/dist/session-event-view-MU3HMNV3.js +264 -0
- package/dist/session-event-view-MU3HMNV3.js.map +1 -0
- package/dist/site-uploads.d.ts +41 -0
- package/dist/workspace-artifact-content.d.ts +4 -4
- package/package.json +19 -19
- package/src/app.ts +20 -0
- package/src/codemode.ts +52 -0
- package/src/integrations/oauth-client.ts +51 -7
- package/src/mcp/command-read.ts +109 -0
- package/src/mcp/server.ts +287 -153
- package/src/mcp/session-event-view.ts +336 -0
- package/src/mcp/session-wait.ts +1 -1
- package/src/routes/github.ts +124 -1
- package/src/routes/scheduled-tasks.ts +19 -1
- package/src/routes/sessions.ts +150 -7
- package/src/routes/workspace-artifacts.ts +92 -21
- package/src/sandbox/channel-a.ts +5 -0
- package/src/sandbox/rematerialize.ts +46 -3
- package/src/sandbox/viewer.ts +3 -0
- package/src/site-uploads.ts +202 -0
- package/src/workspace-artifact-content.ts +20 -39
- package/dist/chunk-XXH7DW34.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/api-router",
|
|
3
|
-
"version": "2.10.0-canary.
|
|
3
|
+
"version": "2.10.0-canary.2",
|
|
4
4
|
"description": "OpenGeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -43,24 +43,24 @@
|
|
|
43
43
|
"@hono/zod-validator": "^0.7.6",
|
|
44
44
|
"@llamaindex/liteparse": "2.14.2",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
-
"@opengeni/agent-proto": "^0.5.1-canary.
|
|
47
|
-
"@opengeni/artifact-tool": "^0.3.19-canary.
|
|
48
|
-
"@opengeni/capabilities": "^0.3.2-canary.
|
|
49
|
-
"@opengeni/codemode": "^0.4.27-canary.
|
|
50
|
-
"@opengeni/codex": "^0.2.21-canary.
|
|
51
|
-
"@opengeni/config": "^1.0.0-canary.
|
|
52
|
-
"@opengeni/contracts": "^2.13.0-canary.
|
|
53
|
-
"@opengeni/core": "^2.7.5-canary.
|
|
54
|
-
"@opengeni/db": "^4.0.0-canary.
|
|
55
|
-
"@opengeni/documents": "^0.8.19-canary.
|
|
56
|
-
"@opengeni/events": "^0.4.17-canary.
|
|
57
|
-
"@opengeni/github": "^0.7.3-canary.
|
|
58
|
-
"@opengeni/network": "^0.3.0-canary.
|
|
59
|
-
"@opengeni/observability": "^0.8.19-canary.
|
|
60
|
-
"@opengeni/runtime": "^2.3.0-canary.
|
|
61
|
-
"@opengeni/storage": "^0.2.120-canary.
|
|
62
|
-
"@opengeni/tool-gateway": "^0.1.0-canary.
|
|
63
|
-
"@opengeni/xai-subscription": "^0.1.3-canary.
|
|
46
|
+
"@opengeni/agent-proto": "^0.5.1-canary.11",
|
|
47
|
+
"@opengeni/artifact-tool": "^0.3.19-canary.2",
|
|
48
|
+
"@opengeni/capabilities": "^0.3.2-canary.2",
|
|
49
|
+
"@opengeni/codemode": "^0.4.27-canary.2",
|
|
50
|
+
"@opengeni/codex": "^0.2.21-canary.2",
|
|
51
|
+
"@opengeni/config": "^1.0.0-canary.2",
|
|
52
|
+
"@opengeni/contracts": "^2.13.0-canary.2",
|
|
53
|
+
"@opengeni/core": "^2.7.5-canary.2",
|
|
54
|
+
"@opengeni/db": "^4.0.0-canary.2",
|
|
55
|
+
"@opengeni/documents": "^0.8.19-canary.2",
|
|
56
|
+
"@opengeni/events": "^0.4.17-canary.2",
|
|
57
|
+
"@opengeni/github": "^0.7.3-canary.2",
|
|
58
|
+
"@opengeni/network": "^0.3.0-canary.2",
|
|
59
|
+
"@opengeni/observability": "^0.8.19-canary.2",
|
|
60
|
+
"@opengeni/runtime": "^2.3.0-canary.2",
|
|
61
|
+
"@opengeni/storage": "^0.2.120-canary.2",
|
|
62
|
+
"@opengeni/tool-gateway": "^0.1.0-canary.2",
|
|
63
|
+
"@opengeni/xai-subscription": "^0.1.3-canary.2",
|
|
64
64
|
"@temporalio/client": "^1.17.0",
|
|
65
65
|
"better-auth": "^1.6.14",
|
|
66
66
|
"hono": "^4.12.18",
|
package/src/app.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codemodeSessionRequest } from "./codemode";
|
|
1
2
|
import {
|
|
2
3
|
canonicalizeConfiguredModelId,
|
|
3
4
|
configuredAllowedModels,
|
|
@@ -895,6 +896,7 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
895
896
|
: {}),
|
|
896
897
|
},
|
|
897
898
|
productAccessMode: deps.settings.productAccessMode,
|
|
899
|
+
billingMode: deps.settings.billingMode,
|
|
898
900
|
managedAuthSessionSetMode: deps.settings.managedAuthSessionSetMode,
|
|
899
901
|
auth: clientAuthConfig(deps.settings),
|
|
900
902
|
analytics: clientAnalyticsConfig(deps.settings),
|
|
@@ -1127,6 +1129,20 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
1127
1129
|
}
|
|
1128
1130
|
});
|
|
1129
1131
|
|
|
1132
|
+
app.all("/v1/workspaces/:workspaceId/codemode/sdk/*", async (c) => {
|
|
1133
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1134
|
+
const grant = await requireAccessGrant(c, routeDeps, workspaceId);
|
|
1135
|
+
const url = new URL(c.req.url);
|
|
1136
|
+
const prefix = `/v1/workspaces/${workspaceId}/codemode/sdk`;
|
|
1137
|
+
const forwarded = await codemodeSessionRequest(
|
|
1138
|
+
routeDeps,
|
|
1139
|
+
grant,
|
|
1140
|
+
c.req.raw,
|
|
1141
|
+
url.pathname.slice(prefix.length) + url.search,
|
|
1142
|
+
);
|
|
1143
|
+
return app.fetch(forwarded);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1130
1146
|
app.get("/v1/workspaces/:workspaceId/codemode/catalog", async (c) => {
|
|
1131
1147
|
const workspaceId = c.req.param("workspaceId");
|
|
1132
1148
|
const grant = await requireAccessGrant(c, routeDeps, workspaceId);
|
|
@@ -2147,6 +2163,10 @@ const routeLabelPatterns: Array<{
|
|
|
2147
2163
|
pattern: /^\/v1\/workspaces\/[^/]+\/github\/app$/,
|
|
2148
2164
|
label: "/v1/workspaces/:workspaceId/github/app",
|
|
2149
2165
|
},
|
|
2166
|
+
{
|
|
2167
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/github\/action-policies$/,
|
|
2168
|
+
label: "/v1/workspaces/:workspaceId/github/action-policies",
|
|
2169
|
+
},
|
|
2150
2170
|
{
|
|
2151
2171
|
pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories$/,
|
|
2152
2172
|
label: "/v1/workspaces/:workspaceId/github/repositories",
|
package/src/codemode.ts
CHANGED
|
@@ -20,6 +20,58 @@ import {
|
|
|
20
20
|
getCodemodeOperation,
|
|
21
21
|
submitCodemodeOperation,
|
|
22
22
|
} from "@opengeni/db";
|
|
23
|
+
import { getSession } from "@opengeni/db";
|
|
24
|
+
import { resolveFirstPartyDelegationSecret } from "@opengeni/config";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
27
|
+
signDelegatedAccessToken,
|
|
28
|
+
siteSessionPath,
|
|
29
|
+
OPENGENI_API_CONTRACT_HEADER,
|
|
30
|
+
OPENGENI_API_CONTRACT_REVISION,
|
|
31
|
+
} from "@opengeni/contracts";
|
|
32
|
+
|
|
33
|
+
/** Reuse normal REST handlers, including their resource/command authorization.
|
|
34
|
+
* The exact attempt is checked before issuing this internal-only credential. */
|
|
35
|
+
export async function codemodeSessionRequest(
|
|
36
|
+
deps: ApiRouteDeps,
|
|
37
|
+
grant: AccessGrant,
|
|
38
|
+
request: Request,
|
|
39
|
+
path: string,
|
|
40
|
+
): Promise<Request> {
|
|
41
|
+
siteSessionPath(path, grant.workspaceId, request.method);
|
|
42
|
+
const { authority } = await requireActiveCodemodeCatalog(deps, grant);
|
|
43
|
+
const session = await getSession(deps.db, authority.workspaceId, authority.sessionId);
|
|
44
|
+
const secret = resolveFirstPartyDelegationSecret(deps.settings);
|
|
45
|
+
if (!session || !secret) throw new CodemodeAuthorityError("invalid_grant");
|
|
46
|
+
const permissions = (
|
|
47
|
+
session.firstPartyMcpPermissions ?? [...DEFAULT_FIRST_PARTY_MCP_PERMISSIONS]
|
|
48
|
+
).filter((p) =>
|
|
49
|
+
["workspace:read", "sessions:read", "sessions:create", "sessions:control"].includes(p),
|
|
50
|
+
);
|
|
51
|
+
const token = await signDelegatedAccessToken(secret, {
|
|
52
|
+
...authority,
|
|
53
|
+
permissions,
|
|
54
|
+
principalKind: "agent_attempt",
|
|
55
|
+
exp: Math.floor(Date.now() / 1000) + 60,
|
|
56
|
+
});
|
|
57
|
+
const target = new URL(request.url);
|
|
58
|
+
const rewritten = siteSessionPath(path, authority.workspaceId, request.method);
|
|
59
|
+
const url = new URL(rewritten, target.origin);
|
|
60
|
+
const headers = new Headers({
|
|
61
|
+
authorization: `Bearer ${token}`,
|
|
62
|
+
[OPENGENI_API_CONTRACT_HEADER]: OPENGENI_API_CONTRACT_REVISION,
|
|
63
|
+
});
|
|
64
|
+
for (const name of ["content-type", "accept", "last-event-id"]) {
|
|
65
|
+
const value = request.headers.get(name);
|
|
66
|
+
if (value) headers.set(name, value);
|
|
67
|
+
}
|
|
68
|
+
return new Request(url, {
|
|
69
|
+
method: request.method,
|
|
70
|
+
headers,
|
|
71
|
+
signal: request.signal,
|
|
72
|
+
...(request.body ? { body: await request.text() } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
23
75
|
|
|
24
76
|
export type CodemodeGrantAuthority = {
|
|
25
77
|
accountId: string;
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
createConnection,
|
|
16
16
|
decryptEnvironmentValue,
|
|
17
17
|
encryptEnvironmentValue,
|
|
18
|
+
findActiveWorkspaceApiKeyById,
|
|
18
19
|
getConnectionMetadata,
|
|
19
20
|
getGlobalCatalogOAuthProfile,
|
|
20
21
|
getWorkspaceGrant,
|
|
@@ -191,6 +192,13 @@ class OAuthStartStageError extends Error {
|
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
class OAuthMetadataUpstreamError extends Error {
|
|
196
|
+
constructor(readonly upstreamStatus: number) {
|
|
197
|
+
super(`OAuth metadata endpoint returned HTTP ${upstreamStatus}`);
|
|
198
|
+
this.name = "OAuthMetadataUpstreamError";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
194
202
|
class OAuthStartDeadline {
|
|
195
203
|
readonly signal: AbortSignal;
|
|
196
204
|
private readonly controller = new AbortController();
|
|
@@ -811,7 +819,7 @@ async function requireOAuthCallbackGrant(db: Database, state: OAuthStatePayload)
|
|
|
811
819
|
? membershipGrant
|
|
812
820
|
: state.personalOwnerVerified
|
|
813
821
|
? await resolveNamedManagedPersonalWorkspaceGrant(db, state)
|
|
814
|
-
:
|
|
822
|
+
: await apiKeyCallbackGrant(db, state);
|
|
815
823
|
if (
|
|
816
824
|
!grant ||
|
|
817
825
|
grant.accountId !== state.accountId ||
|
|
@@ -823,6 +831,25 @@ async function requireOAuthCallbackGrant(db: Database, state: OAuthStatePayload)
|
|
|
823
831
|
}
|
|
824
832
|
}
|
|
825
833
|
|
|
834
|
+
async function apiKeyCallbackGrant(
|
|
835
|
+
db: Database,
|
|
836
|
+
state: OAuthStatePayload,
|
|
837
|
+
): Promise<{ accountId: string; permissions: Parameters<typeof hasPermission>[0] } | null> {
|
|
838
|
+
const prefix = "api_key:";
|
|
839
|
+
if (!state.subjectId.startsWith(prefix)) {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
const apiKey = await findActiveWorkspaceApiKeyById(db, {
|
|
843
|
+
accountId: state.accountId,
|
|
844
|
+
workspaceId: state.workspaceId,
|
|
845
|
+
apiKeyId: state.subjectId.slice(prefix.length),
|
|
846
|
+
});
|
|
847
|
+
if (!apiKey) {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
return { accountId: apiKey.accountId, permissions: apiKey.permissions };
|
|
851
|
+
}
|
|
852
|
+
|
|
826
853
|
/** The hosted-Slack profile's origin pins, kept exported for its tests. */
|
|
827
854
|
export function assertSlackAuthorizationServer(as: AuthorizationServerMetadata): void {
|
|
828
855
|
const pins = builtInOAuthProfileByKey("hosted-slack-mcp").authorizationServer;
|
|
@@ -928,7 +955,7 @@ async function fetchOAuthMetadata(
|
|
|
928
955
|
}
|
|
929
956
|
if (!response.ok) {
|
|
930
957
|
await cancelResponseBody(response);
|
|
931
|
-
throw new
|
|
958
|
+
throw new OAuthMetadataUpstreamError(response.status);
|
|
932
959
|
}
|
|
933
960
|
const payload = await readResponseJsonBounded<unknown>(
|
|
934
961
|
response,
|
|
@@ -1711,6 +1738,9 @@ function logOAuthStartFailure(
|
|
|
1711
1738
|
function oauthStartFailureReason(error: unknown): string {
|
|
1712
1739
|
if (error instanceof RequestDeadlineError) return "timeout";
|
|
1713
1740
|
if (error instanceof DestinationPolicyError) return error.reason;
|
|
1741
|
+
if (error instanceof OAuthMetadataUpstreamError) {
|
|
1742
|
+
return `upstream_http_${error.upstreamStatus}`;
|
|
1743
|
+
}
|
|
1714
1744
|
if (error instanceof HTTPException) return `http_${error.status}`;
|
|
1715
1745
|
if (error instanceof SyntaxError) return "invalid_response";
|
|
1716
1746
|
return "request_failed";
|
|
@@ -1754,15 +1784,29 @@ function isDatabaseStatementTimeout(error: unknown): boolean {
|
|
|
1754
1784
|
|
|
1755
1785
|
function oauthStartApiError(error: OAuthStartStageError): ApiHttpError {
|
|
1756
1786
|
const timeout = error.reason === "timeout";
|
|
1757
|
-
const
|
|
1787
|
+
const metadataUpstream = error.cause instanceof OAuthMetadataUpstreamError ? error.cause : null;
|
|
1788
|
+
const status = timeout
|
|
1789
|
+
? 408
|
|
1790
|
+
: metadataUpstream
|
|
1791
|
+
? 502
|
|
1792
|
+
: error.cause instanceof HTTPException
|
|
1793
|
+
? error.cause.status
|
|
1794
|
+
: 422;
|
|
1758
1795
|
return new ApiHttpError(status, {
|
|
1759
1796
|
code: timeout || status >= 500 ? "upstream_unavailable" : "validation_failed",
|
|
1760
|
-
retryable:
|
|
1797
|
+
retryable:
|
|
1798
|
+
timeout ||
|
|
1799
|
+
status === 429 ||
|
|
1800
|
+
(metadataUpstream
|
|
1801
|
+
? metadataUpstream.upstreamStatus === 429 || metadataUpstream.upstreamStatus >= 500
|
|
1802
|
+
: status >= 500),
|
|
1761
1803
|
message: timeout
|
|
1762
1804
|
? oauthStartTimeoutMessage(error.stage)
|
|
1763
|
-
:
|
|
1764
|
-
? error.
|
|
1765
|
-
:
|
|
1805
|
+
: metadataUpstream
|
|
1806
|
+
? `OAuth provider returned HTTP ${metadataUpstream.upstreamStatus} during ${oauthStartStageLabel(error.stage)}.`
|
|
1807
|
+
: error.cause instanceof HTTPException
|
|
1808
|
+
? error.cause.message
|
|
1809
|
+
: `Connection setup failed during ${oauthStartStageLabel(error.stage)}.`,
|
|
1766
1810
|
details: {
|
|
1767
1811
|
oauthStage: error.stage,
|
|
1768
1812
|
oauthReason: error.reason,
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { SessionEvent } from "@opengeni/contracts";
|
|
2
|
+
import { COMMAND_OUTPUT_MAX_BYTES } from "@opengeni/db/session-background-commands";
|
|
3
|
+
|
|
4
|
+
export const COMMAND_READ_MAX_WAIT_SECONDS = 50;
|
|
5
|
+
export const COMMAND_WAIT_DEFAULT_SECONDS = 45;
|
|
6
|
+
|
|
7
|
+
/** Shared provider-neutral command_read/command_wait operation. Notifications
|
|
8
|
+
* are only wake hints. Every result, including timeout, comes from durable DB
|
|
9
|
+
* state. Subscribe before reading to close the finish/subscription race, and
|
|
10
|
+
* periodically recheck durable state because live hints may be lost. */
|
|
11
|
+
export async function readCommandWithWait<
|
|
12
|
+
T extends { terminal: boolean; chunks: unknown[]; hasMore?: boolean },
|
|
13
|
+
>(input: {
|
|
14
|
+
commandId: string;
|
|
15
|
+
waitSeconds?: number;
|
|
16
|
+
maxOutputBytes?: number | undefined;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
read: () => Promise<T>;
|
|
19
|
+
subscribe: (onEvents: (events: SessionEvent[]) => void) => Promise<() => void>;
|
|
20
|
+
}) {
|
|
21
|
+
const seconds = input.waitSeconds ?? 0;
|
|
22
|
+
if (!Number.isInteger(seconds) || seconds < 0 || seconds > COMMAND_READ_MAX_WAIT_SECONDS) {
|
|
23
|
+
throw new Error("waitSeconds must be between 0 and 50");
|
|
24
|
+
}
|
|
25
|
+
if (
|
|
26
|
+
input.maxOutputBytes !== undefined &&
|
|
27
|
+
(!Number.isInteger(input.maxOutputBytes) ||
|
|
28
|
+
input.maxOutputBytes < 4 ||
|
|
29
|
+
input.maxOutputBytes > COMMAND_OUTPUT_MAX_BYTES)
|
|
30
|
+
) {
|
|
31
|
+
throw new Error("maxOutputBytes must be between 4 and 65536");
|
|
32
|
+
}
|
|
33
|
+
const started = Date.now();
|
|
34
|
+
const deadline = started + seconds * 1000;
|
|
35
|
+
let unsubscribe: (() => void) | undefined;
|
|
36
|
+
let wake: (() => void) | undefined;
|
|
37
|
+
let changed = false;
|
|
38
|
+
let liveFanout = true;
|
|
39
|
+
const notify = () => {
|
|
40
|
+
changed = true;
|
|
41
|
+
wake?.();
|
|
42
|
+
};
|
|
43
|
+
input.signal?.addEventListener("abort", notify);
|
|
44
|
+
try {
|
|
45
|
+
if (seconds > 0) {
|
|
46
|
+
try {
|
|
47
|
+
unsubscribe = await input.subscribe((events) => {
|
|
48
|
+
if (
|
|
49
|
+
events.some(
|
|
50
|
+
(event) =>
|
|
51
|
+
(event.type === "session.command.finished" ||
|
|
52
|
+
event.type === "sandbox.command.output.delta") &&
|
|
53
|
+
event.payload !== null &&
|
|
54
|
+
typeof event.payload === "object" &&
|
|
55
|
+
(event.payload as Record<string, unknown>).commandId === input.commandId,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
notify();
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
liveFanout = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
while (true) {
|
|
65
|
+
changed = false;
|
|
66
|
+
const result = await input.read();
|
|
67
|
+
const expired = Date.now() >= deadline;
|
|
68
|
+
if (
|
|
69
|
+
result.terminal ||
|
|
70
|
+
result.chunks.length > 0 ||
|
|
71
|
+
result.hasMore ||
|
|
72
|
+
expired ||
|
|
73
|
+
input.signal?.aborted
|
|
74
|
+
) {
|
|
75
|
+
return {
|
|
76
|
+
...result,
|
|
77
|
+
waitedMs: Date.now() - started,
|
|
78
|
+
timedOut:
|
|
79
|
+
seconds > 0 &&
|
|
80
|
+
expired &&
|
|
81
|
+
!result.terminal &&
|
|
82
|
+
result.chunks.length === 0 &&
|
|
83
|
+
!result.hasMore,
|
|
84
|
+
aborted: input.signal?.aborted ?? false,
|
|
85
|
+
liveFanout,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (changed) continue;
|
|
89
|
+
await new Promise<void>((resolve) => {
|
|
90
|
+
const timer = setTimeout(
|
|
91
|
+
() => {
|
|
92
|
+
wake = undefined;
|
|
93
|
+
resolve();
|
|
94
|
+
},
|
|
95
|
+
Math.max(0, Math.min(1_000, deadline - Date.now())),
|
|
96
|
+
);
|
|
97
|
+
wake = () => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
wake = undefined;
|
|
100
|
+
resolve();
|
|
101
|
+
};
|
|
102
|
+
if (changed || input.signal?.aborted) wake();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
input.signal?.removeEventListener("abort", notify);
|
|
107
|
+
unsubscribe?.();
|
|
108
|
+
}
|
|
109
|
+
}
|