pi-commandcode-provider 0.3.1 → 0.4.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/CHANGELOG.md +6 -0
- package/RELEASE.md +25 -11
- package/package.json +3 -2
- package/src/core.ts +217 -67
- package/src/types.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.4.0 - 2026-06-02
|
|
6
|
+
|
|
7
|
+
- Add retry mechanism for transient HTTP errors (429, 5xx) and stream-level errors, configurable via pi `settings.json` `retry.provider` fields (`timeoutMs`, `maxRetries`, `maxRetryDelayMs`). Supports exponential backoff with jitter and `Retry-After` header.
|
|
8
|
+
|
|
3
9
|
## 0.3.1 - 2026-05-29
|
|
4
10
|
|
|
5
11
|
- Bump CLI version header to `0.29.0` for Command Code API parity.
|
package/RELEASE.md
CHANGED
|
@@ -7,7 +7,8 @@ Recommended flow:
|
|
|
7
7
|
- publish prereleases with the `next` dist-tag
|
|
8
8
|
- smoke-test the npm package directly in pi
|
|
9
9
|
- publish stable releases with the `latest` dist-tag
|
|
10
|
-
- commit and
|
|
10
|
+
- commit the release on a branch, open a PR, and merge after CI passes
|
|
11
|
+
- tag the stable release on `main` after merge
|
|
11
12
|
- comment on the related PR or issue after shipping
|
|
12
13
|
|
|
13
14
|
## Prerelease flow
|
|
@@ -144,21 +145,41 @@ npm pack --dry-run
|
|
|
144
145
|
git diff --check
|
|
145
146
|
```
|
|
146
147
|
|
|
147
|
-
Commit and
|
|
148
|
+
Commit on a release branch and open a PR:
|
|
148
149
|
|
|
149
150
|
```sh
|
|
151
|
+
git checkout -b release/0.1.1
|
|
150
152
|
git add .
|
|
151
153
|
git commit -m "Release 0.1.1"
|
|
154
|
+
git push origin release/0.1.1
|
|
155
|
+
gh pr create --title "chore(release): publish 0.1.1" --base main
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`main` is branch-protected. The release must go through a PR with passing CI.
|
|
159
|
+
|
|
160
|
+
Once CI passes, approve and merge:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
gh pr review <number> --approve
|
|
164
|
+
gh pr merge <number> --squash --delete-branch
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
After merge, pull `main` and tag locally:
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
git checkout main
|
|
171
|
+
git pull origin main
|
|
152
172
|
git tag -a v0.1.1 -m "Release 0.1.1"
|
|
173
|
+
git push origin v0.1.1
|
|
153
174
|
```
|
|
154
175
|
|
|
155
|
-
Publish stable:
|
|
176
|
+
Publish stable locally:
|
|
156
177
|
|
|
157
178
|
```sh
|
|
158
179
|
npm publish --tag latest --access public
|
|
159
180
|
```
|
|
160
181
|
|
|
161
|
-
If npm asks for browser or OTP auth,
|
|
182
|
+
Publishing is intentionally manual/local; there is no GitHub Actions publish workflow. If npm asks for browser or OTP auth, complete the npm prompt locally.
|
|
162
183
|
|
|
163
184
|
Verify npm:
|
|
164
185
|
|
|
@@ -172,13 +193,6 @@ Expected:
|
|
|
172
193
|
- `latest` points to the stable version
|
|
173
194
|
- the stable version exists on npm
|
|
174
195
|
|
|
175
|
-
Push commit and tag:
|
|
176
|
-
|
|
177
|
-
```sh
|
|
178
|
-
git push origin main
|
|
179
|
-
git push origin v0.1.1
|
|
180
|
-
```
|
|
181
|
-
|
|
182
196
|
## GitHub follow-up
|
|
183
197
|
|
|
184
198
|
Comment on the related PR and issue after publishing and pushing:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-commandcode-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "pi custom provider for Command Code API (commandcode.ai)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"LICENSE"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
|
-
"test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-pricing.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
|
|
31
|
+
"test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-pricing.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
|
|
34
34
|
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"test:oauth": "tsx tests/test-oauth.ts",
|
|
39
39
|
"test:abort": "tsx tests/test-abort.ts",
|
|
40
40
|
"test:stream": "tsx tests/test-stream.ts",
|
|
41
|
+
"test:retry": "tsx tests/test-retry.ts",
|
|
41
42
|
"test:pi-local": "node tests/test-pi-local.mjs",
|
|
42
43
|
"test:smoke": "node tests/test-smoke.mjs"
|
|
43
44
|
},
|
package/src/core.ts
CHANGED
|
@@ -42,6 +42,43 @@ export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
|
|
42
42
|
export const COMMAND_CODE_CLI_VERSION = "0.29.0"
|
|
43
43
|
|
|
44
44
|
const DEFAULT_GENERATE_MAX_TOKENS = 64_000
|
|
45
|
+
const DEFAULT_MAX_RETRIES = 0
|
|
46
|
+
const DEFAULT_MAX_RETRY_DELAY_MS = 60_000
|
|
47
|
+
const BASE_RETRY_DELAY_MS = 500
|
|
48
|
+
|
|
49
|
+
function isRetryableStatus(status: number): boolean {
|
|
50
|
+
return status === 429 || (status >= 500 && status < 600)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseRetryAfterSeconds(value: string | null): number | undefined {
|
|
54
|
+
if (!value) return undefined
|
|
55
|
+
const seconds = Number(value)
|
|
56
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds
|
|
57
|
+
const date = Date.parse(value)
|
|
58
|
+
if (!Number.isNaN(date)) return Math.max(0, (date - Date.now()) / 1000)
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function effectiveMaxRetryDelayMs(value: number | undefined): number {
|
|
63
|
+
if (value === undefined) return DEFAULT_MAX_RETRY_DELAY_MS
|
|
64
|
+
if (value === 0) return Number.POSITIVE_INFINITY
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function retryDelayMs(
|
|
69
|
+
attempt: number,
|
|
70
|
+
retryAfterHeader: string | null,
|
|
71
|
+
maxDelayMs: number,
|
|
72
|
+
): number {
|
|
73
|
+
const retryAfterMs = parseRetryAfterSeconds(retryAfterHeader)
|
|
74
|
+
if (retryAfterMs !== undefined) {
|
|
75
|
+
if (retryAfterMs * 1000 > maxDelayMs) return -1
|
|
76
|
+
return retryAfterMs * 1000
|
|
77
|
+
}
|
|
78
|
+
const exponential = BASE_RETRY_DELAY_MS * 2 ** attempt
|
|
79
|
+
const jitter = exponential * 0.2 * Math.random()
|
|
80
|
+
return Math.min(exponential + jitter, maxDelayMs)
|
|
81
|
+
}
|
|
45
82
|
|
|
46
83
|
function defaultUsage(): Usage {
|
|
47
84
|
return {
|
|
@@ -76,6 +113,14 @@ function abortError(message = "The operation was aborted"): DOMException {
|
|
|
76
113
|
return new DOMException(message, "AbortError")
|
|
77
114
|
}
|
|
78
115
|
|
|
116
|
+
function timeoutError(timeoutMs: number | undefined): Error {
|
|
117
|
+
return new Error(
|
|
118
|
+
timeoutMs === undefined
|
|
119
|
+
? "Command Code API request timed out"
|
|
120
|
+
: `Command Code API request timed out after ${timeoutMs}ms`,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
79
124
|
function successStopReason(reason: TerminalReason): StopReason {
|
|
80
125
|
if (reason === "length" || reason === "toolUse") return reason
|
|
81
126
|
return "stop"
|
|
@@ -104,6 +149,22 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
104
149
|
const cwd = deps.cwd ?? (() => process.cwd())
|
|
105
150
|
const now = deps.now ?? (() => Date.now())
|
|
106
151
|
const uuid = deps.uuid ?? (() => randomUUID())
|
|
152
|
+
const delay =
|
|
153
|
+
deps.delay ??
|
|
154
|
+
((ms: number, signal: AbortSignal) => {
|
|
155
|
+
if (signal.aborted) return Promise.reject(abortError())
|
|
156
|
+
return new Promise<void>((resolve, reject) => {
|
|
157
|
+
const id = setTimeout(() => {
|
|
158
|
+
signal.removeEventListener("abort", onAbort)
|
|
159
|
+
resolve()
|
|
160
|
+
}, ms)
|
|
161
|
+
const onAbort = () => {
|
|
162
|
+
clearTimeout(id)
|
|
163
|
+
reject(abortError())
|
|
164
|
+
}
|
|
165
|
+
signal.addEventListener("abort", onAbort, { once: true })
|
|
166
|
+
})
|
|
167
|
+
})
|
|
107
168
|
|
|
108
169
|
function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
109
170
|
if (signal.aborted) return Promise.reject(abortError())
|
|
@@ -386,81 +447,170 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
386
447
|
)
|
|
387
448
|
if (nextBody !== undefined) body = nextBody
|
|
388
449
|
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
},
|
|
402
|
-
body: JSON.stringify(body),
|
|
403
|
-
signal: controller.signal,
|
|
404
|
-
}),
|
|
405
|
-
controller.signal,
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
await raceAbort(
|
|
409
|
-
Promise.resolve(
|
|
410
|
-
options?.onResponse?.(
|
|
411
|
-
{
|
|
412
|
-
status: response.status,
|
|
413
|
-
headers: headersToRecord(response.headers),
|
|
414
|
-
},
|
|
415
|
-
model,
|
|
416
|
-
),
|
|
417
|
-
),
|
|
418
|
-
controller.signal,
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
if (!response.ok) {
|
|
422
|
-
const errBody = await raceAbort(
|
|
423
|
-
response.text().catch(() => ""),
|
|
424
|
-
controller.signal,
|
|
425
|
-
)
|
|
426
|
-
throw new Error(`Command Code API error ${response.status}: ${errBody.slice(0, 500)}`)
|
|
450
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES
|
|
451
|
+
const maxRetryDelayMs = effectiveMaxRetryDelayMs(options?.maxRetryDelayMs)
|
|
452
|
+
const timeoutMs = options?.timeoutMs
|
|
453
|
+
const requestHeaders = {
|
|
454
|
+
"Content-Type": "application/json",
|
|
455
|
+
Authorization: `Bearer ${apiKey}`,
|
|
456
|
+
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
457
|
+
"x-cli-environment": "production",
|
|
458
|
+
"x-project-slug": projectSlugFromPath(workingDir),
|
|
459
|
+
"x-taste-learning": "true",
|
|
460
|
+
"x-co-flag": "false",
|
|
461
|
+
...options?.headers,
|
|
427
462
|
}
|
|
463
|
+
const bodyStr = JSON.stringify(body)
|
|
464
|
+
|
|
465
|
+
let response!: Response
|
|
466
|
+
retryLoop: for (let attempt = 0; ; attempt++) {
|
|
467
|
+
const attemptController = new AbortController()
|
|
468
|
+
let attemptTimedOut = false
|
|
469
|
+
let attemptTimeoutId: ReturnType<typeof setTimeout> | undefined
|
|
470
|
+
|
|
471
|
+
const clearAttemptTimeout = () => {
|
|
472
|
+
if (attemptTimeoutId !== undefined) {
|
|
473
|
+
clearTimeout(attemptTimeoutId)
|
|
474
|
+
attemptTimeoutId = undefined
|
|
475
|
+
}
|
|
476
|
+
}
|
|
428
477
|
|
|
429
|
-
|
|
430
|
-
|
|
478
|
+
if (timeoutMs !== undefined) {
|
|
479
|
+
attemptTimeoutId = setTimeout(() => {
|
|
480
|
+
attemptTimedOut = true
|
|
481
|
+
attemptController.abort()
|
|
482
|
+
}, timeoutMs)
|
|
483
|
+
}
|
|
484
|
+
const onOuterAbort = () => attemptController.abort()
|
|
485
|
+
controller.signal.addEventListener("abort", onOuterAbort, { once: true })
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
try {
|
|
489
|
+
response = await fetchImpl(`${apiBase}/alpha/generate`, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: requestHeaders,
|
|
492
|
+
body: bodyStr,
|
|
493
|
+
signal: attemptController.signal,
|
|
494
|
+
})
|
|
495
|
+
} catch (fetchError: unknown) {
|
|
496
|
+
if (controller.signal.aborted) throw abortError("Aborted")
|
|
497
|
+
if (attemptTimedOut) {
|
|
498
|
+
if (attempt < maxRetries) continue retryLoop
|
|
499
|
+
throw timeoutError(timeoutMs)
|
|
500
|
+
}
|
|
501
|
+
throw fetchError
|
|
502
|
+
}
|
|
431
503
|
|
|
432
|
-
|
|
433
|
-
|
|
504
|
+
// --- HTTP-level retry ---
|
|
505
|
+
if (!response.ok && isRetryableStatus(response.status)) {
|
|
506
|
+
const retryAfter = response.headers.get("retry-after")
|
|
507
|
+
const waitMs = retryDelayMs(attempt, retryAfter, maxRetryDelayMs)
|
|
508
|
+
if (waitMs < 0) {
|
|
509
|
+
const requestedSeconds = parseRetryAfterSeconds(retryAfter) ?? 0
|
|
510
|
+
const capLabel =
|
|
511
|
+
maxRetryDelayMs === Number.POSITIVE_INFINITY ? "disabled" : `${maxRetryDelayMs}ms`
|
|
512
|
+
throw new Error(`Retry-After delay ${requestedSeconds}s exceeds max ${capLabel}`)
|
|
513
|
+
}
|
|
514
|
+
if (attempt < maxRetries) {
|
|
515
|
+
await response.text().catch(() => "")
|
|
516
|
+
if (waitMs > 0) await delay(waitMs, controller.signal)
|
|
517
|
+
continue retryLoop
|
|
518
|
+
}
|
|
519
|
+
}
|
|
434
520
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
521
|
+
await raceAbort(
|
|
522
|
+
Promise.resolve(
|
|
523
|
+
options?.onResponse?.(
|
|
524
|
+
{
|
|
525
|
+
status: response.status,
|
|
526
|
+
headers: headersToRecord(response.headers),
|
|
527
|
+
},
|
|
528
|
+
model,
|
|
529
|
+
),
|
|
530
|
+
),
|
|
531
|
+
controller.signal,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
if (!response.ok) {
|
|
535
|
+
const errBody = await raceAbort(
|
|
536
|
+
response.text().catch(() => ""),
|
|
537
|
+
controller.signal,
|
|
538
|
+
)
|
|
539
|
+
throw new Error(`Command Code API error ${response.status}: ${errBody.slice(0, 500)}`)
|
|
540
|
+
}
|
|
443
541
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
542
|
+
// --- Read response stream ---
|
|
543
|
+
reader = response.body?.getReader()
|
|
544
|
+
if (!reader) throw new Error("No response body")
|
|
545
|
+
|
|
546
|
+
const decoder = new TextDecoder()
|
|
547
|
+
let buffer = ""
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
readLoop: for (;;) {
|
|
551
|
+
if (controller.signal.aborted) throw abortError("Aborted")
|
|
552
|
+
const { done, value } = await raceAbort(reader.read(), attemptController.signal)
|
|
553
|
+
if (done) {
|
|
554
|
+
if (buffer.trim()) handleEvent(parseStreamEventLine(buffer))
|
|
555
|
+
break
|
|
556
|
+
}
|
|
557
|
+
if (controller.signal.aborted) throw abortError("Aborted")
|
|
558
|
+
|
|
559
|
+
buffer += decoder.decode(value, { stream: true })
|
|
560
|
+
const lines = buffer.split("\n")
|
|
561
|
+
buffer = lines.pop() ?? ""
|
|
562
|
+
|
|
563
|
+
for (const line of lines) {
|
|
564
|
+
if (controller.signal.aborted) throw abortError("Aborted")
|
|
565
|
+
handleEvent(parseStreamEventLine(line))
|
|
566
|
+
if (finished) break readLoop
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
} catch (streamError: unknown) {
|
|
570
|
+
// Stream-level error (e.g. API returned 200 OK but sent an error event)
|
|
571
|
+
// or per-attempt timeout during stream reading.
|
|
572
|
+
await reader.cancel().catch(() => {})
|
|
573
|
+
try {
|
|
574
|
+
reader.releaseLock()
|
|
575
|
+
} catch {}
|
|
576
|
+
reader = undefined
|
|
577
|
+
|
|
578
|
+
if (controller.signal.aborted) throw streamError
|
|
579
|
+
|
|
580
|
+
// Never retry after visible content was emitted (including timeout mid-stream).
|
|
581
|
+
const canRetry = output.content.length === 0 && attempt < maxRetries
|
|
582
|
+
if (canRetry) {
|
|
583
|
+
output.content.length = 0
|
|
584
|
+
textBlock = undefined
|
|
585
|
+
currentTextIdx = -1
|
|
586
|
+
thinkingIdx = -1
|
|
587
|
+
output.stopReason = "stop"
|
|
588
|
+
output.errorMessage = undefined
|
|
589
|
+
finished = false
|
|
590
|
+
const waitMs = attemptTimedOut ? 0 : retryDelayMs(attempt, null, maxRetryDelayMs)
|
|
591
|
+
if (waitMs > 0) await delay(waitMs, controller.signal)
|
|
592
|
+
continue retryLoop
|
|
593
|
+
}
|
|
594
|
+
if (attemptTimedOut) throw timeoutError(timeoutMs)
|
|
595
|
+
throw streamError
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Stream completed successfully.
|
|
599
|
+
endTextBlock()
|
|
600
|
+
endThinking()
|
|
447
601
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
602
|
+
stream.push({
|
|
603
|
+
type: "done",
|
|
604
|
+
reason: successStopReason(output.stopReason),
|
|
605
|
+
message: output,
|
|
606
|
+
})
|
|
607
|
+
stream.end()
|
|
608
|
+
break retryLoop
|
|
609
|
+
} finally {
|
|
610
|
+
controller.signal.removeEventListener("abort", onOuterAbort)
|
|
611
|
+
clearAttemptTimeout()
|
|
452
612
|
}
|
|
453
613
|
}
|
|
454
|
-
|
|
455
|
-
endTextBlock()
|
|
456
|
-
endThinking()
|
|
457
|
-
|
|
458
|
-
stream.push({
|
|
459
|
-
type: "done",
|
|
460
|
-
reason: successStopReason(output.stopReason),
|
|
461
|
-
message: output,
|
|
462
|
-
})
|
|
463
|
-
stream.end()
|
|
464
614
|
} catch (error: unknown) {
|
|
465
615
|
const reason: ErrorReason = controller.signal.aborted ? "aborted" : "error"
|
|
466
616
|
output.stopReason = reason
|
package/src/types.ts
CHANGED
|
@@ -89,6 +89,23 @@ export interface StreamOptions {
|
|
|
89
89
|
maxTokens?: number
|
|
90
90
|
onPayload?: (payload: unknown, model: ModelLike) => unknown | Promise<unknown>
|
|
91
91
|
onResponse?: (response: ProviderResponseInfo, model: ModelLike) => void | Promise<void>
|
|
92
|
+
/**
|
|
93
|
+
* HTTP request timeout in milliseconds.
|
|
94
|
+
* Applied per-attempt; on timeout the request is retried if retries remain.
|
|
95
|
+
*/
|
|
96
|
+
timeoutMs?: number
|
|
97
|
+
/**
|
|
98
|
+
* Maximum retry attempts for transient HTTP errors (429, 5xx).
|
|
99
|
+
* Default: 0 (pi agent-level retry handles visible retries when unset).
|
|
100
|
+
*/
|
|
101
|
+
maxRetries?: number
|
|
102
|
+
/**
|
|
103
|
+
* Maximum delay in milliseconds to wait for a retry when the server requests
|
|
104
|
+
* a long wait via Retry-After. If the server's requested delay exceeds this
|
|
105
|
+
* value, the request fails immediately. Default: 60000 (60 seconds).
|
|
106
|
+
* Set to 0 to disable the cap.
|
|
107
|
+
*/
|
|
108
|
+
maxRetryDelayMs?: number
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
export type AssistantMessageEvent =
|
|
@@ -153,4 +170,6 @@ export interface CoreDependencies {
|
|
|
153
170
|
now?: () => number
|
|
154
171
|
uuid?: () => string
|
|
155
172
|
homeDir?: () => string
|
|
173
|
+
/** Injectable delay for retry backoff. Defaults to setTimeout. */
|
|
174
|
+
delay?: (ms: number, signal: AbortSignal) => Promise<void>
|
|
156
175
|
}
|