@tagma/sdk 0.6.0 → 0.6.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/LICENSE +21 -21
- package/README.md +573 -573
- package/dist/bootstrap.d.ts +11 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +18 -9
- package/dist/bootstrap.js.map +1 -1
- package/dist/drivers/opencode.d.ts.map +1 -1
- package/dist/drivers/opencode.js +47 -17
- package/dist/drivers/opencode.js.map +1 -1
- package/dist/engine.d.ts +8 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +17 -16
- package/dist/engine.js.map +1 -1
- package/dist/plugin-registry.test.d.ts +2 -0
- package/dist/plugin-registry.test.d.ts.map +1 -0
- package/dist/plugin-registry.test.js +188 -0
- package/dist/plugin-registry.test.js.map +1 -0
- package/dist/registry.d.ts +52 -28
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +126 -91
- package/dist/registry.js.map +1 -1
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +1 -1
- package/dist/sdk.js.map +1 -1
- package/package.json +2 -2
- package/src/bootstrap.ts +46 -37
- package/src/completions/output-check.ts +92 -92
- package/src/dag.ts +245 -245
- package/src/drivers/opencode.ts +410 -371
- package/src/engine.ts +1228 -1220
- package/src/hooks.ts +193 -193
- package/src/middlewares/static-context.ts +49 -49
- package/src/pipeline-runner.ts +173 -173
- package/src/plugin-registry.test.ts +230 -0
- package/src/prompt-doc.ts +49 -49
- package/src/registry.ts +316 -267
- package/src/runner.ts +460 -460
- package/src/schema.test.ts +101 -101
- package/src/schema.ts +338 -338
- package/src/sdk.ts +120 -118
- package/src/task-ref.test.ts +401 -401
- package/src/task-ref.ts +120 -120
- package/src/validate-raw.ts +412 -412
- package/dist/drivers/claude-code.d.ts +0 -3
- package/dist/drivers/claude-code.d.ts.map +0 -1
- package/dist/drivers/claude-code.js +0 -225
- package/dist/drivers/claude-code.js.map +0 -1
package/dist/sdk.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.js","sourceRoot":"","sources":["../src/sdk.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,EAAE;AACF,4EAA4E;AAC5E,oEAAoE;AAEpE,oBAAoB;AACpB,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAGjF,8DAA8D;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnD,oDAAoD;AACpD,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,SAAS,EACT,WAAW,EACX,UAAU,EACV,UAAU,EACV,QAAQ,EACR,YAAY,GACb,MAAM,cAAc,CAAC;AAEtB,mDAAmD;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,8DAA8D;AAC9D,OAAO,EACL,SAAS,EACT,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,GACf,MAAM,UAAU,CAAC;AAElB,YAAY;AACZ,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAG9C,wBAAwB;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,kBAAkB,GACnB,MAAM,YAAY,CAAC;AAGpB,yBAAyB;AACzB,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAUrD,0BAA0B;AAC1B,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAEvE,OAAO,EAAE,8BAA8B,EAAE,MAAM,+BAA+B,CAAC;AAM/E,eAAe;AACf,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAMnD,8BAA8B;AAC9B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,aAAa,EACb,MAAM,EACN,eAAe,EACf,gBAAgB,GACjB,MAAM,SAAS,CAAC;AAEjB,4DAA4D;AAC5D,OAAO,EACL,UAAU,EACV,aAAa,EACb,aAAa,EACb,cAAc,EACd,cAAc,EACd,cAAc,EACd,SAAS,GACV,MAAM,YAAY,CAAC;AAGpB,+DAA+D;AAC/D,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,aAAa,GACd,MAAM,cAAc,CAAC;AAEtB,wDAAwD;AACxD,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"sdk.js","sourceRoot":"","sources":["../src/sdk.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,EAAE;AACF,4EAA4E;AAC5E,oEAAoE;AAEpE,oBAAoB;AACpB,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAGjF,8DAA8D;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnD,oDAAoD;AACpD,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,SAAS,EACT,WAAW,EACX,UAAU,EACV,UAAU,EACV,QAAQ,EACR,YAAY,GACb,MAAM,cAAc,CAAC;AAEtB,mDAAmD;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,8DAA8D;AAC9D,OAAO,EACL,SAAS,EACT,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,GACf,MAAM,UAAU,CAAC;AAElB,YAAY;AACZ,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAG9C,wBAAwB;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,cAAc,EACd,eAAe,EACf,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,kBAAkB,GACnB,MAAM,YAAY,CAAC;AAGpB,yBAAyB;AACzB,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAUrD,0BAA0B;AAC1B,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAEvE,OAAO,EAAE,8BAA8B,EAAE,MAAM,+BAA+B,CAAC;AAM/E,eAAe;AACf,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAMnD,8BAA8B;AAC9B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,aAAa,EACb,MAAM,EACN,eAAe,EACf,gBAAgB,GACjB,MAAM,SAAS,CAAC;AAEjB,4DAA4D;AAC5D,OAAO,EACL,UAAU,EACV,aAAa,EACb,aAAa,EACb,cAAc,EACd,cAAc,EACd,cAAc,EACd,SAAS,GACV,MAAM,YAAY,CAAC;AAGpB,+DAA+D;AAC/D,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,aAAa,GACd,MAAM,cAAc,CAAC;AAEtB,wDAAwD;AACxD,cAAc,SAAS,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tagma/sdk",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Local AI task orchestration engine — core SDK for tagma pipelines",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"release:publish": "bun scripts/release.ts --publish"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@tagma/types": "0.4.
|
|
45
|
+
"@tagma/types": "0.4.1",
|
|
46
46
|
"js-yaml": "^4.1.0",
|
|
47
47
|
"chokidar": "^4.0.0"
|
|
48
48
|
},
|
package/src/bootstrap.ts
CHANGED
|
@@ -1,37 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
// Built-in Drivers
|
|
4
|
-
// Only opencode is built in. Other drivers (codex, claude-code) ship as
|
|
5
|
-
// workspace plugins under packages/ and must be declared in pipeline.yaml
|
|
6
|
-
// via the `plugins` field, e.g.:
|
|
7
|
-
// plugins: ["@tagma/driver-codex", "@tagma/driver-claude-code"]
|
|
8
|
-
import { OpenCodeDriver } from './drivers/opencode';
|
|
9
|
-
|
|
10
|
-
// Built-in Triggers
|
|
11
|
-
import { FileTrigger } from './triggers/file';
|
|
12
|
-
import { ManualTrigger } from './triggers/manual';
|
|
13
|
-
|
|
14
|
-
// Built-in Completions
|
|
15
|
-
import { ExitCodeCompletion } from './completions/exit-code';
|
|
16
|
-
import { FileExistsCompletion } from './completions/file-exists';
|
|
17
|
-
import { OutputCheckCompletion } from './completions/output-check';
|
|
18
|
-
|
|
19
|
-
// Built-in Middleware
|
|
20
|
-
import { StaticContextMiddleware } from './middlewares/static-context';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
registerPlugin('
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
registerPlugin('
|
|
37
|
-
|
|
1
|
+
import { defaultRegistry, type PluginRegistry } from './registry';
|
|
2
|
+
|
|
3
|
+
// Built-in Drivers
|
|
4
|
+
// Only opencode is built in. Other drivers (codex, claude-code) ship as
|
|
5
|
+
// workspace plugins under packages/ and must be declared in pipeline.yaml
|
|
6
|
+
// via the `plugins` field, e.g.:
|
|
7
|
+
// plugins: ["@tagma/driver-codex", "@tagma/driver-claude-code"]
|
|
8
|
+
import { OpenCodeDriver } from './drivers/opencode';
|
|
9
|
+
|
|
10
|
+
// Built-in Triggers
|
|
11
|
+
import { FileTrigger } from './triggers/file';
|
|
12
|
+
import { ManualTrigger } from './triggers/manual';
|
|
13
|
+
|
|
14
|
+
// Built-in Completions
|
|
15
|
+
import { ExitCodeCompletion } from './completions/exit-code';
|
|
16
|
+
import { FileExistsCompletion } from './completions/file-exists';
|
|
17
|
+
import { OutputCheckCompletion } from './completions/output-check';
|
|
18
|
+
|
|
19
|
+
// Built-in Middleware
|
|
20
|
+
import { StaticContextMiddleware } from './middlewares/static-context';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register every built-in plugin into `target` (defaults to the process-wide
|
|
24
|
+
* default registry). Multi-tenant hosts instantiate one PluginRegistry per
|
|
25
|
+
* workspace and call this once per instance so each workspace sees the same
|
|
26
|
+
* built-ins without sharing registration state.
|
|
27
|
+
*
|
|
28
|
+
* Built-in handlers are stateless module singletons — registering the same
|
|
29
|
+
* handler object into N registries is cheap and safe; no cloning is needed.
|
|
30
|
+
*/
|
|
31
|
+
export function bootstrapBuiltins(target: PluginRegistry = defaultRegistry): void {
|
|
32
|
+
// Drivers
|
|
33
|
+
target.registerPlugin('drivers', 'opencode', OpenCodeDriver);
|
|
34
|
+
|
|
35
|
+
// Triggers
|
|
36
|
+
target.registerPlugin('triggers', 'file', FileTrigger);
|
|
37
|
+
target.registerPlugin('triggers', 'manual', ManualTrigger);
|
|
38
|
+
|
|
39
|
+
// Completions
|
|
40
|
+
target.registerPlugin('completions', 'exit_code', ExitCodeCompletion);
|
|
41
|
+
target.registerPlugin('completions', 'file_exists', FileExistsCompletion);
|
|
42
|
+
target.registerPlugin('completions', 'output_check', OutputCheckCompletion);
|
|
43
|
+
|
|
44
|
+
// Middlewares
|
|
45
|
+
target.registerPlugin('middlewares', 'static_context', StaticContextMiddleware);
|
|
46
|
+
}
|
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
import type { CompletionPlugin, CompletionContext, TaskResult } from '../types';
|
|
2
|
-
import { shellArgs, parseDuration } from '../utils';
|
|
3
|
-
|
|
4
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
|
-
|
|
6
|
-
export const OutputCheckCompletion: CompletionPlugin = {
|
|
7
|
-
name: 'output_check',
|
|
8
|
-
schema: {
|
|
9
|
-
description:
|
|
10
|
-
'Pipe the task output into a shell command; mark success when that command exits 0. For AI driver tasks the driver-normalized text is piped (not the raw NDJSON); command tasks see their raw stdout.',
|
|
11
|
-
fields: {
|
|
12
|
-
check: {
|
|
13
|
-
type: 'string',
|
|
14
|
-
required: true,
|
|
15
|
-
description:
|
|
16
|
-
'Shell command to run. The task output is piped to its stdin — normalizedOutput when the driver provides one, otherwise raw stdout.',
|
|
17
|
-
placeholder: "grep -q 'PASS'",
|
|
18
|
-
},
|
|
19
|
-
timeout: {
|
|
20
|
-
type: 'duration',
|
|
21
|
-
default: '30s',
|
|
22
|
-
description: 'Maximum time to wait for the check command.',
|
|
23
|
-
placeholder: '30s',
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
async check(
|
|
29
|
-
config: Record<string, unknown>,
|
|
30
|
-
result: TaskResult,
|
|
31
|
-
ctx: CompletionContext,
|
|
32
|
-
): Promise<boolean> {
|
|
33
|
-
const checkCmd = config.check as string;
|
|
34
|
-
if (!checkCmd) throw new Error('output_check completion: "check" is required');
|
|
35
|
-
|
|
36
|
-
const timeoutMs =
|
|
37
|
-
config.timeout != null ? parseDuration(String(config.timeout)) : DEFAULT_TIMEOUT_MS;
|
|
38
|
-
|
|
39
|
-
const controller = new AbortController();
|
|
40
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
41
|
-
|
|
42
|
-
// Wire pipeline abort signal into the check process so external abort
|
|
43
|
-
// terminates the child instead of leaving it running undetected.
|
|
44
|
-
const onAbort = () => controller.abort();
|
|
45
|
-
if (ctx.signal) {
|
|
46
|
-
if (ctx.signal.aborted) {
|
|
47
|
-
controller.abort();
|
|
48
|
-
} else {
|
|
49
|
-
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const proc = Bun.spawn(shellArgs(checkCmd) as string[], {
|
|
54
|
-
cwd: ctx.workDir,
|
|
55
|
-
stdin: 'pipe',
|
|
56
|
-
stdout: 'pipe',
|
|
57
|
-
stderr: 'pipe',
|
|
58
|
-
signal: controller.signal,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
if (proc.stdin) {
|
|
63
|
-
try {
|
|
64
|
-
// Prefer driver-normalized text (e.g. concatenated message text for
|
|
65
|
-
// AI drivers that emit NDJSON). Falling back to raw stdout keeps
|
|
66
|
-
// command tasks and drivers without parseResult working.
|
|
67
|
-
const payload = result.normalizedOutput ?? result.stdout;
|
|
68
|
-
proc.stdin.write(payload);
|
|
69
|
-
proc.stdin.end(); // no await — consistent with runner.ts; proc.exited handles sync
|
|
70
|
-
} catch (err: unknown) {
|
|
71
|
-
// EPIPE is expected when the check process exits before reading all of stdin
|
|
72
|
-
// (e.g. `grep -q` exits on first match). Anything else is a real failure.
|
|
73
|
-
const code = (err as NodeJS.ErrnoException)?.code;
|
|
74
|
-
if (code !== 'EPIPE') throw err;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Consume stderr concurrently with waiting for exit to prevent pipe-buffer
|
|
79
|
-
// deadlock when check script emits more than ~64 KB of stderr output.
|
|
80
|
-
const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
|
|
81
|
-
|
|
82
|
-
if (exitCode !== 0 && stderr.trim()) {
|
|
83
|
-
console.warn(`[output_check] "${checkCmd}" exit=${exitCode}: ${stderr.trim()}`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return exitCode === 0;
|
|
87
|
-
} finally {
|
|
88
|
-
clearTimeout(timer);
|
|
89
|
-
if (ctx.signal) ctx.signal.removeEventListener('abort', onAbort);
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
};
|
|
1
|
+
import type { CompletionPlugin, CompletionContext, TaskResult } from '../types';
|
|
2
|
+
import { shellArgs, parseDuration } from '../utils';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
|
+
|
|
6
|
+
export const OutputCheckCompletion: CompletionPlugin = {
|
|
7
|
+
name: 'output_check',
|
|
8
|
+
schema: {
|
|
9
|
+
description:
|
|
10
|
+
'Pipe the task output into a shell command; mark success when that command exits 0. For AI driver tasks the driver-normalized text is piped (not the raw NDJSON); command tasks see their raw stdout.',
|
|
11
|
+
fields: {
|
|
12
|
+
check: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
required: true,
|
|
15
|
+
description:
|
|
16
|
+
'Shell command to run. The task output is piped to its stdin — normalizedOutput when the driver provides one, otherwise raw stdout.',
|
|
17
|
+
placeholder: "grep -q 'PASS'",
|
|
18
|
+
},
|
|
19
|
+
timeout: {
|
|
20
|
+
type: 'duration',
|
|
21
|
+
default: '30s',
|
|
22
|
+
description: 'Maximum time to wait for the check command.',
|
|
23
|
+
placeholder: '30s',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
async check(
|
|
29
|
+
config: Record<string, unknown>,
|
|
30
|
+
result: TaskResult,
|
|
31
|
+
ctx: CompletionContext,
|
|
32
|
+
): Promise<boolean> {
|
|
33
|
+
const checkCmd = config.check as string;
|
|
34
|
+
if (!checkCmd) throw new Error('output_check completion: "check" is required');
|
|
35
|
+
|
|
36
|
+
const timeoutMs =
|
|
37
|
+
config.timeout != null ? parseDuration(String(config.timeout)) : DEFAULT_TIMEOUT_MS;
|
|
38
|
+
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
41
|
+
|
|
42
|
+
// Wire pipeline abort signal into the check process so external abort
|
|
43
|
+
// terminates the child instead of leaving it running undetected.
|
|
44
|
+
const onAbort = () => controller.abort();
|
|
45
|
+
if (ctx.signal) {
|
|
46
|
+
if (ctx.signal.aborted) {
|
|
47
|
+
controller.abort();
|
|
48
|
+
} else {
|
|
49
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const proc = Bun.spawn(shellArgs(checkCmd) as string[], {
|
|
54
|
+
cwd: ctx.workDir,
|
|
55
|
+
stdin: 'pipe',
|
|
56
|
+
stdout: 'pipe',
|
|
57
|
+
stderr: 'pipe',
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
if (proc.stdin) {
|
|
63
|
+
try {
|
|
64
|
+
// Prefer driver-normalized text (e.g. concatenated message text for
|
|
65
|
+
// AI drivers that emit NDJSON). Falling back to raw stdout keeps
|
|
66
|
+
// command tasks and drivers without parseResult working.
|
|
67
|
+
const payload = result.normalizedOutput ?? result.stdout;
|
|
68
|
+
proc.stdin.write(payload);
|
|
69
|
+
proc.stdin.end(); // no await — consistent with runner.ts; proc.exited handles sync
|
|
70
|
+
} catch (err: unknown) {
|
|
71
|
+
// EPIPE is expected when the check process exits before reading all of stdin
|
|
72
|
+
// (e.g. `grep -q` exits on first match). Anything else is a real failure.
|
|
73
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
74
|
+
if (code !== 'EPIPE') throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Consume stderr concurrently with waiting for exit to prevent pipe-buffer
|
|
79
|
+
// deadlock when check script emits more than ~64 KB of stderr output.
|
|
80
|
+
const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
|
|
81
|
+
|
|
82
|
+
if (exitCode !== 0 && stderr.trim()) {
|
|
83
|
+
console.warn(`[output_check] "${checkCmd}" exit=${exitCode}: ${stderr.trim()}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return exitCode === 0;
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
if (ctx.signal) ctx.signal.removeEventListener('abort', onAbort);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|