@pablozaiden/terminatui 0.2.0 → 0.3.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 +64 -43
- package/package.json +11 -8
- package/src/__tests__/application.test.ts +87 -68
- package/src/__tests__/buildCliCommand.test.ts +99 -119
- package/src/__tests__/builtins.test.ts +27 -75
- package/src/__tests__/command.test.ts +100 -131
- package/src/__tests__/configOnChange.test.ts +63 -0
- package/src/__tests__/context.test.ts +1 -26
- package/src/__tests__/helpCore.test.ts +227 -0
- package/src/__tests__/parser.test.ts +98 -244
- package/src/__tests__/registry.test.ts +33 -160
- package/src/__tests__/schemaToFields.test.ts +75 -158
- package/src/builtins/help.ts +12 -4
- package/src/builtins/settings.ts +18 -32
- package/src/builtins/version.ts +3 -3
- package/src/cli/output/colors.ts +1 -1
- package/src/cli/parser.ts +26 -95
- package/src/core/application.ts +192 -110
- package/src/core/command.ts +26 -9
- package/src/core/context.ts +31 -20
- package/src/core/help.ts +24 -18
- package/src/core/knownCommands.ts +13 -0
- package/src/core/logger.ts +39 -42
- package/src/core/registry.ts +5 -12
- package/src/index.ts +22 -137
- package/src/tui/TuiApplication.tsx +63 -120
- package/src/tui/TuiRoot.tsx +135 -0
- package/src/tui/adapters/factory.ts +19 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +139 -0
- package/src/tui/adapters/ink/components/Button.tsx +12 -0
- package/src/tui/adapters/ink/components/Code.tsx +6 -0
- package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
- package/src/tui/adapters/ink/components/Container.tsx +5 -0
- package/src/tui/adapters/ink/components/Field.tsx +12 -0
- package/src/tui/adapters/ink/components/Label.tsx +24 -0
- package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
- package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
- package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
- package/src/tui/adapters/ink/components/Panel.tsx +15 -0
- package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
- package/src/tui/adapters/ink/components/Select.tsx +44 -0
- package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
- package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
- package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
- package/src/tui/adapters/ink/components/Value.tsx +7 -0
- package/src/tui/adapters/ink/keyboard.ts +97 -0
- package/src/tui/adapters/ink/utils.ts +16 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +119 -0
- package/src/tui/adapters/opentui/components/Button.tsx +13 -0
- package/src/tui/adapters/opentui/components/Code.tsx +12 -0
- package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
- package/src/tui/adapters/opentui/components/Container.tsx +56 -0
- package/src/tui/adapters/opentui/components/Field.tsx +18 -0
- package/src/tui/adapters/opentui/components/Label.tsx +15 -0
- package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
- package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
- package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
- package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
- package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
- package/src/tui/adapters/opentui/components/Select.tsx +59 -0
- package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
- package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
- package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
- package/src/tui/adapters/opentui/components/Value.tsx +13 -0
- package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
- package/src/tui/adapters/opentui/keyboard.ts +61 -0
- package/src/tui/adapters/types.ts +71 -0
- package/src/tui/components/ActionButton.tsx +0 -36
- package/src/tui/components/CommandSelector.tsx +45 -92
- package/src/tui/components/ConfigForm.tsx +68 -42
- package/src/tui/components/FieldRow.tsx +0 -30
- package/src/tui/components/Header.tsx +14 -13
- package/src/tui/components/JsonHighlight.tsx +10 -17
- package/src/tui/components/ModalBase.tsx +38 -0
- package/src/tui/components/ResultsPanel.tsx +27 -36
- package/src/tui/components/StatusBar.tsx +24 -39
- package/src/tui/components/logColors.ts +12 -0
- package/src/tui/context/ClipboardContext.tsx +87 -0
- package/src/tui/context/ExecutorContext.tsx +139 -0
- package/src/tui/context/KeyboardContext.tsx +85 -71
- package/src/tui/context/LogsContext.tsx +35 -0
- package/src/tui/context/NavigationContext.tsx +194 -0
- package/src/tui/context/RendererContext.tsx +20 -0
- package/src/tui/context/TuiAppContext.tsx +58 -0
- package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
- package/src/tui/hooks/useBackHandler.ts +34 -0
- package/src/tui/hooks/useClipboard.ts +40 -25
- package/src/tui/hooks/useClipboardProvider.ts +42 -0
- package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
- package/src/tui/modals/CliModal.tsx +82 -0
- package/src/tui/modals/EditorModal.tsx +207 -0
- package/src/tui/modals/LogsModal.tsx +98 -0
- package/src/tui/registry.ts +102 -0
- package/src/tui/screens/CommandSelectScreen.tsx +162 -0
- package/src/tui/screens/ConfigScreen.tsx +165 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +68 -0
- package/src/tui/screens/RunningScreen.tsx +72 -0
- package/src/tui/screens/ScreenBase.ts +6 -0
- package/src/tui/semantic/Button.tsx +7 -0
- package/src/tui/semantic/Code.tsx +7 -0
- package/src/tui/semantic/CodeHighlight.tsx +7 -0
- package/src/tui/semantic/Container.tsx +7 -0
- package/src/tui/semantic/Field.tsx +7 -0
- package/src/tui/semantic/Label.tsx +7 -0
- package/src/tui/semantic/MenuButton.tsx +7 -0
- package/src/tui/semantic/MenuItem.tsx +7 -0
- package/src/tui/semantic/Overlay.tsx +7 -0
- package/src/tui/semantic/Panel.tsx +7 -0
- package/src/tui/semantic/ScrollView.tsx +9 -0
- package/src/tui/semantic/Select.tsx +7 -0
- package/src/tui/semantic/Spacer.tsx +7 -0
- package/src/tui/semantic/Spinner.tsx +7 -0
- package/src/tui/semantic/TextInput.tsx +7 -0
- package/src/tui/semantic/Value.tsx +7 -0
- package/src/tui/semantic/types.ts +195 -0
- package/src/tui/theme.ts +25 -14
- package/src/tui/utils/buildCliCommand.ts +1 -0
- package/src/tui/utils/getEnumKeys.ts +3 -0
- package/src/tui/utils/parameterPersistence.ts +1 -0
- package/src/types/command.ts +0 -60
- package/.devcontainer/devcontainer.json +0 -19
- package/.devcontainer/install-prerequisites.sh +0 -49
- package/.github/workflows/copilot-setup-steps.yml +0 -32
- package/.github/workflows/pull-request.yml +0 -27
- package/.github/workflows/release-npm-package.yml +0 -81
- package/AGENTS.md +0 -31
- package/bun.lock +0 -236
- package/examples/tui-app/commands/config/app/get.ts +0 -66
- package/examples/tui-app/commands/config/app/index.ts +0 -27
- package/examples/tui-app/commands/config/app/set.ts +0 -86
- package/examples/tui-app/commands/config/index.ts +0 -32
- package/examples/tui-app/commands/config/user/get.ts +0 -65
- package/examples/tui-app/commands/config/user/index.ts +0 -27
- package/examples/tui-app/commands/config/user/set.ts +0 -61
- package/examples/tui-app/commands/greet.ts +0 -76
- package/examples/tui-app/commands/index.ts +0 -4
- package/examples/tui-app/commands/math.ts +0 -115
- package/examples/tui-app/commands/status.ts +0 -77
- package/examples/tui-app/index.ts +0 -35
- package/guides/01-hello-world.md +0 -96
- package/guides/02-adding-options.md +0 -103
- package/guides/03-multiple-commands.md +0 -163
- package/guides/04-subcommands.md +0 -206
- package/guides/05-interactive-tui.md +0 -194
- package/guides/06-config-validation.md +0 -264
- package/guides/07-async-cancellation.md +0 -336
- package/guides/08-complete-application.md +0 -537
- package/guides/README.md +0 -74
- package/src/__tests__/colors.test.ts +0 -127
- package/src/__tests__/commandClass.test.ts +0 -130
- package/src/__tests__/help.test.ts +0 -412
- package/src/__tests__/registryNew.test.ts +0 -160
- package/src/__tests__/table.test.ts +0 -146
- package/src/__tests__/tui.test.ts +0 -26
- package/src/builtins/index.ts +0 -4
- package/src/cli/help.ts +0 -174
- package/src/cli/index.ts +0 -3
- package/src/cli/output/index.ts +0 -2
- package/src/cli/output/table.ts +0 -141
- package/src/commands/help.ts +0 -50
- package/src/commands/index.ts +0 -1
- package/src/components/index.ts +0 -147
- package/src/core/index.ts +0 -15
- package/src/hooks/index.ts +0 -131
- package/src/registry/commandRegistry.ts +0 -77
- package/src/registry/index.ts +0 -1
- package/src/tui/TuiApp.tsx +0 -619
- package/src/tui/app.ts +0 -29
- package/src/tui/components/CliModal.tsx +0 -81
- package/src/tui/components/EditorModal.tsx +0 -177
- package/src/tui/components/LogsPanel.tsx +0 -86
- package/src/tui/components/index.ts +0 -13
- package/src/tui/context/index.ts +0 -7
- package/src/tui/hooks/index.ts +0 -35
- package/src/tui/hooks/useKeyboardHandler.ts +0 -91
- package/src/tui/hooks/useLogStream.ts +0 -96
- package/src/tui/index.ts +0 -65
- package/src/tui/utils/index.ts +0 -13
- package/src/types/index.ts +0 -1
- package/tsconfig.json +0 -25
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
# Guide 7: Async Commands with Cancellation (Complex)
|
|
2
|
-
|
|
3
|
-
Build commands that support cancellation with proper cleanup for long-running operations.
|
|
4
|
-
|
|
5
|
-
## What You'll Build
|
|
6
|
-
|
|
7
|
-
A download manager that:
|
|
8
|
-
- Downloads files asynchronously
|
|
9
|
-
- Shows progress updates
|
|
10
|
-
- Supports cancellation (Ctrl+C in CLI, Esc in TUI)
|
|
11
|
-
- Cleans up partial downloads when cancelled
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
download https://example.com/large-file.zip --output ./downloads/
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Step 1: Define the Command
|
|
18
|
-
|
|
19
|
-
Create `src/commands/download.ts`:
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
import path from "node:path";
|
|
23
|
-
import {
|
|
24
|
-
Command,
|
|
25
|
-
ConfigValidationError,
|
|
26
|
-
type AppContext,
|
|
27
|
-
type OptionSchema,
|
|
28
|
-
type OptionValues,
|
|
29
|
-
type CommandResult,
|
|
30
|
-
type CommandExecutionContext
|
|
31
|
-
} from "@pablozaiden/terminatui";
|
|
32
|
-
|
|
33
|
-
const options = {
|
|
34
|
-
url: {
|
|
35
|
-
type: "string",
|
|
36
|
-
description: "URL to download",
|
|
37
|
-
required: true,
|
|
38
|
-
label: "Download URL",
|
|
39
|
-
},
|
|
40
|
-
output: {
|
|
41
|
-
type: "string",
|
|
42
|
-
description: "Output directory",
|
|
43
|
-
default: "./downloads",
|
|
44
|
-
label: "Output Directory",
|
|
45
|
-
},
|
|
46
|
-
"chunk-size": {
|
|
47
|
-
type: "string",
|
|
48
|
-
description: "Download chunk size in KB",
|
|
49
|
-
default: "1024",
|
|
50
|
-
label: "Chunk Size (KB)",
|
|
51
|
-
},
|
|
52
|
-
} satisfies OptionSchema;
|
|
53
|
-
|
|
54
|
-
interface DownloadConfig {
|
|
55
|
-
url: URL;
|
|
56
|
-
outputDir: string;
|
|
57
|
-
fileName: string;
|
|
58
|
-
chunkSize: number;
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Step 2: Implement buildConfig
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
export class DownloadCommand extends Command<typeof options, DownloadConfig> {
|
|
66
|
-
readonly name = "download";
|
|
67
|
-
readonly description = "Download a file from URL";
|
|
68
|
-
readonly options = options;
|
|
69
|
-
readonly displayName = "File Downloader";
|
|
70
|
-
readonly actionLabel = "Download";
|
|
71
|
-
|
|
72
|
-
override buildConfig(
|
|
73
|
-
_ctx: AppContext,
|
|
74
|
-
opts: OptionValues<typeof options>
|
|
75
|
-
): DownloadConfig {
|
|
76
|
-
// Validate URL
|
|
77
|
-
const urlStr = opts["url"] as string;
|
|
78
|
-
if (!urlStr) {
|
|
79
|
-
throw new ConfigValidationError("URL is required", "url");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let url: URL;
|
|
83
|
-
try {
|
|
84
|
-
url = new URL(urlStr);
|
|
85
|
-
} catch {
|
|
86
|
-
throw new ConfigValidationError("Invalid URL format", "url");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Validate output directory
|
|
90
|
-
const outputDir = path.resolve(opts["output"] as string ?? "./downloads");
|
|
91
|
-
|
|
92
|
-
// Extract filename from URL
|
|
93
|
-
const fileName = path.basename(url.pathname) || "download";
|
|
94
|
-
|
|
95
|
-
// Parse chunk size
|
|
96
|
-
const chunkSizeStr = opts["chunk-size"] as string ?? "1024";
|
|
97
|
-
const chunkSize = parseInt(chunkSizeStr, 10) * 1024; // Convert KB to bytes
|
|
98
|
-
if (isNaN(chunkSize) || chunkSize <= 0) {
|
|
99
|
-
throw new ConfigValidationError("Chunk size must be a positive number", "chunk-size");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { url, outputDir, fileName, chunkSize };
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Step 3: Implement Cancellable Download
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
async execute(
|
|
110
|
-
ctx: AppContext,
|
|
111
|
-
config: DownloadConfig,
|
|
112
|
-
execCtx?: CommandExecutionContext
|
|
113
|
-
): Promise<CommandResult> {
|
|
114
|
-
const { url, outputDir, fileName, chunkSize } = config;
|
|
115
|
-
const outputPath = path.join(outputDir, fileName);
|
|
116
|
-
const signal = execCtx?.signal;
|
|
117
|
-
|
|
118
|
-
ctx.logger.info(`Starting download: ${url}`);
|
|
119
|
-
ctx.logger.info(`Output: ${outputPath}`);
|
|
120
|
-
|
|
121
|
-
// Create output directory
|
|
122
|
-
await Bun.write(path.join(outputDir, ".keep"), "");
|
|
123
|
-
|
|
124
|
-
// Track download state for cleanup
|
|
125
|
-
let downloadedBytes = 0;
|
|
126
|
-
let totalBytes = 0;
|
|
127
|
-
let partialFile: Bun.FileSink | null = null;
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
// Check for cancellation before starting
|
|
131
|
-
if (signal?.aborted) {
|
|
132
|
-
return { success: false, message: "Download cancelled before start" };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Fetch with AbortSignal
|
|
136
|
-
const response = await fetch(url.toString(), { signal });
|
|
137
|
-
|
|
138
|
-
if (!response.ok) {
|
|
139
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
totalBytes = parseInt(response.headers.get("content-length") ?? "0", 10);
|
|
143
|
-
const reader = response.body?.getReader();
|
|
144
|
-
|
|
145
|
-
if (!reader) {
|
|
146
|
-
throw new Error("No response body");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Open file for writing
|
|
150
|
-
partialFile = Bun.file(outputPath).writer();
|
|
151
|
-
|
|
152
|
-
console.log(`Downloading ${fileName}...`);
|
|
153
|
-
if (totalBytes > 0) {
|
|
154
|
-
console.log(`Total size: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Read chunks with cancellation checks
|
|
158
|
-
while (true) {
|
|
159
|
-
// Check for cancellation between chunks
|
|
160
|
-
if (signal?.aborted) {
|
|
161
|
-
ctx.logger.warn("Download cancelled by user");
|
|
162
|
-
throw new Error("AbortError");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const { done, value } = await reader.read();
|
|
166
|
-
|
|
167
|
-
if (done) {
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Write chunk
|
|
172
|
-
partialFile.write(value);
|
|
173
|
-
downloadedBytes += value.byteLength;
|
|
174
|
-
|
|
175
|
-
// Log progress
|
|
176
|
-
if (totalBytes > 0) {
|
|
177
|
-
const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
|
|
178
|
-
const mbDownloaded = (downloadedBytes / 1024 / 1024).toFixed(2);
|
|
179
|
-
const mbTotal = (totalBytes / 1024 / 1024).toFixed(2);
|
|
180
|
-
process.stdout.write(`\rProgress: ${percent}% (${mbDownloaded}/${mbTotal} MB)`);
|
|
181
|
-
} else {
|
|
182
|
-
const mbDownloaded = (downloadedBytes / 1024 / 1024).toFixed(2);
|
|
183
|
-
process.stdout.write(`\rDownloaded: ${mbDownloaded} MB`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Finalize file
|
|
188
|
-
await partialFile.end();
|
|
189
|
-
console.log("\nDownload complete!");
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
success: true,
|
|
193
|
-
data: {
|
|
194
|
-
file: outputPath,
|
|
195
|
-
size: downloadedBytes,
|
|
196
|
-
url: url.toString(),
|
|
197
|
-
},
|
|
198
|
-
message: `Downloaded ${fileName} (${(downloadedBytes / 1024 / 1024).toFixed(2)} MB)`,
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
} catch (error) {
|
|
202
|
-
// Handle cancellation
|
|
203
|
-
if (signal?.aborted || (error as Error).name === "AbortError") {
|
|
204
|
-
console.log("\nDownload cancelled.");
|
|
205
|
-
|
|
206
|
-
// Cleanup partial file
|
|
207
|
-
await this.cleanup(outputPath, partialFile);
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
success: false,
|
|
211
|
-
message: "Download cancelled by user",
|
|
212
|
-
data: {
|
|
213
|
-
downloadedBytes,
|
|
214
|
-
cancelled: true,
|
|
215
|
-
},
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Handle other errors
|
|
220
|
-
await this.cleanup(outputPath, partialFile);
|
|
221
|
-
throw error;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private async cleanup(
|
|
226
|
-
outputPath: string,
|
|
227
|
-
sink: Bun.FileSink | null
|
|
228
|
-
): Promise<void> {
|
|
229
|
-
try {
|
|
230
|
-
// Close file handle
|
|
231
|
-
if (sink) {
|
|
232
|
-
await sink.end();
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Remove partial file
|
|
236
|
-
const file = Bun.file(outputPath);
|
|
237
|
-
if (await file.exists()) {
|
|
238
|
-
await Bun.write(outputPath, ""); // Clear file
|
|
239
|
-
// In production: fs.unlinkSync(outputPath);
|
|
240
|
-
console.log("Cleaned up partial download.");
|
|
241
|
-
}
|
|
242
|
-
} catch (e) {
|
|
243
|
-
// Ignore cleanup errors
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
## Step 4: Create the Application
|
|
250
|
-
|
|
251
|
-
Create `src/index.ts`:
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
import { TuiApplication } from "@pablozaiden/terminatui";
|
|
255
|
-
import { DownloadCommand } from "./commands/download";
|
|
256
|
-
|
|
257
|
-
class DownloadManager extends TuiApplication {
|
|
258
|
-
constructor() {
|
|
259
|
-
super({
|
|
260
|
-
name: "download-manager",
|
|
261
|
-
version: "1.0.0",
|
|
262
|
-
commands: [new DownloadCommand()],
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
await new DownloadManager().run();
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
## Step 6: Test Cancellation
|
|
271
|
-
|
|
272
|
-
```bash
|
|
273
|
-
# Start a large download
|
|
274
|
-
bun src/index.ts download https://speed.hetzner.de/100MB.bin --output ./test-downloads
|
|
275
|
-
|
|
276
|
-
# While downloading, press Ctrl+C
|
|
277
|
-
# Should see: "Download cancelled. Cleaned up partial download."
|
|
278
|
-
|
|
279
|
-
# Run TUI mode
|
|
280
|
-
bun src/index.ts --tui
|
|
281
|
-
|
|
282
|
-
# Start download, then press Esc to cancel
|
|
283
|
-
# Same cancellation behavior with cleanup
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
## Cancellation Patterns
|
|
287
|
-
|
|
288
|
-
### 1. Check Signal Before Long Operations
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
if (signal?.aborted) {
|
|
292
|
-
return { success: false, message: "Cancelled" };
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### 2. Pass Signal to fetch/APIs
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
await fetch(url, { signal });
|
|
300
|
-
await someAsyncApi({ signal });
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### 3. Check Between Iterations
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
for (const item of items) {
|
|
307
|
-
if (signal?.aborted) break;
|
|
308
|
-
await processItem(item);
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
### 4. Always Cleanup
|
|
313
|
-
|
|
314
|
-
```typescript
|
|
315
|
-
try {
|
|
316
|
-
// ... cancellable work ...
|
|
317
|
-
} catch (error) {
|
|
318
|
-
if (signal?.aborted) {
|
|
319
|
-
await cleanup();
|
|
320
|
-
return { success: false, message: "Cancelled" };
|
|
321
|
-
}
|
|
322
|
-
throw error;
|
|
323
|
-
}
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
## What You Learned
|
|
327
|
-
|
|
328
|
-
- Accept `CommandExecutionContext` with `AbortSignal`
|
|
329
|
-
- Check `signal.aborted` between operations
|
|
330
|
-
- Pass signal to `fetch` and other async APIs
|
|
331
|
-
- Clean up resources on cancellation
|
|
332
|
-
- Return meaningful results for cancelled operations
|
|
333
|
-
|
|
334
|
-
## Next Steps
|
|
335
|
-
|
|
336
|
-
→ [Guide 8: Building a Complete Application](08-complete-application.md)
|