@kapeta/local-cluster-service 0.48.5 → 0.50.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/src/assetManager.d.ts +1 -1
  3. package/dist/cjs/src/assetManager.js +5 -3
  4. package/dist/cjs/src/filesystem/routes.js +10 -0
  5. package/dist/cjs/src/storm/codegen.d.ts +4 -1
  6. package/dist/cjs/src/storm/codegen.js +63 -11
  7. package/dist/cjs/src/storm/event-parser.d.ts +5 -2
  8. package/dist/cjs/src/storm/event-parser.js +24 -4
  9. package/dist/cjs/src/storm/events.d.ts +24 -1
  10. package/dist/cjs/src/storm/events.js +7 -0
  11. package/dist/cjs/src/storm/routes.js +88 -6
  12. package/dist/cjs/src/storm/stormClient.js +5 -1
  13. package/dist/cjs/src/storm/stream.d.ts +11 -0
  14. package/dist/cjs/src/storm/stream.js +11 -0
  15. package/dist/esm/src/assetManager.d.ts +1 -1
  16. package/dist/esm/src/assetManager.js +5 -3
  17. package/dist/esm/src/filesystem/routes.js +10 -0
  18. package/dist/esm/src/storm/codegen.d.ts +4 -1
  19. package/dist/esm/src/storm/codegen.js +63 -11
  20. package/dist/esm/src/storm/event-parser.d.ts +5 -2
  21. package/dist/esm/src/storm/event-parser.js +24 -4
  22. package/dist/esm/src/storm/events.d.ts +24 -1
  23. package/dist/esm/src/storm/events.js +7 -0
  24. package/dist/esm/src/storm/routes.js +88 -6
  25. package/dist/esm/src/storm/stormClient.js +5 -1
  26. package/dist/esm/src/storm/stream.d.ts +11 -0
  27. package/dist/esm/src/storm/stream.js +11 -0
  28. package/package.json +1 -1
  29. package/src/assetManager.ts +6 -4
  30. package/src/filesystem/routes.ts +10 -0
  31. package/src/storm/codegen.ts +95 -22
  32. package/src/storm/event-parser.ts +33 -5
  33. package/src/storm/events.ts +32 -4
  34. package/src/storm/routes.ts +113 -12
  35. package/src/storm/stormClient.ts +8 -1
  36. package/src/storm/stream.ts +22 -0
@@ -8,6 +8,7 @@ import { stringBody, StringBodyRequest } from '../middleware/stringBody';
8
8
  import { filesystemManager } from '../filesystemManager';
9
9
  import { corsHandler } from '../middleware/cors';
10
10
  import { NextFunction, Request, Response } from 'express';
11
+ import FS from 'fs-extra';
11
12
 
12
13
  let router = Router();
13
14
 
@@ -99,6 +100,15 @@ router.get('/readfile', async (req: Request, res: Response) => {
99
100
  }
100
101
  });
101
102
 
103
+ router.get('/exists', async (req: Request, res: Response) => {
104
+ let pathArg = req.query.path as string;
105
+ try {
106
+ res.send(await FS.pathExists(pathArg));
107
+ } catch (err) {
108
+ res.status(400).send({ error: '' + err });
109
+ }
110
+ });
111
+
102
112
  router.put('/mkdir', async (req: Request, res: Response) => {
103
113
  let pathArg = req.query.path as string;
104
114
  try {
@@ -4,18 +4,26 @@
4
4
  */
5
5
 
6
6
  import { Definition } from '@kapeta/local-cluster-config';
7
- import { AIFileTypes, BlockCodeGenerator, CodeGenerator, CodeWriter, GeneratedFile, GeneratedResult } from '@kapeta/codegen';
7
+ import {
8
+ AIFileTypes,
9
+ BlockCodeGenerator,
10
+ CodeGenerator,
11
+ CodeWriter,
12
+ GeneratedFile,
13
+ GeneratedResult,
14
+ } from '@kapeta/codegen';
8
15
  import { BlockDefinition } from '@kapeta/schemas';
9
16
  import { codeGeneratorManager } from '../codeGeneratorManager';
10
17
  import { STORM_ID, stormClient } from './stormClient';
11
18
  import { StormEvent, StormEventFile } from './events';
12
19
  import { BlockDefinitionInfo, StormEventParser } from './event-parser';
13
- import { ConversationItem, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
14
- import { KapetaURI } from '@kapeta/nodejs-utils';
20
+ import { StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
21
+ import { KapetaURI, parseKapetaUri } from '@kapeta/nodejs-utils';
15
22
  import { writeFile } from 'fs/promises';
16
23
  import path, { join } from 'path';
17
24
  import os from 'node:os';
18
25
  import { readFile, readFileSync, writeFileSync } from 'fs';
26
+ import Path from 'path';
19
27
 
20
28
  type ImplementationGenerator = (prompt: StormFileImplementationPrompt, conversationId?: string) => Promise<StormStream>;
21
29
 
@@ -25,22 +33,28 @@ export class StormCodegen {
25
33
  private readonly out = new StormStream();
26
34
  private readonly events: StormEvent[];
27
35
  private readonly tmpDir: string;
36
+ private readonly conversationId: string;
28
37
 
29
- constructor(userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
38
+ constructor(conversationId: string, userPrompt: string, blocks: BlockDefinitionInfo[], events: StormEvent[]) {
30
39
  this.userPrompt = userPrompt;
31
40
  this.blocks = blocks;
32
41
  this.events = events;
33
- this.tmpDir = os.tmpdir();
42
+ this.tmpDir = Path.join(os.tmpdir(), conversationId);
43
+ this.conversationId = conversationId;
34
44
  }
35
45
 
36
46
  public async process() {
37
- for (const block of this.blocks) {
38
- await this.processBlockCode(block);
39
- }
40
-
47
+ const promises = this.blocks.map((block) => {
48
+ return this.processBlockCode(block);
49
+ });
50
+ await Promise.all(promises);
41
51
  this.out.end();
42
52
  }
43
53
 
54
+ isAborted() {
55
+ return this.out.isAborted();
56
+ }
57
+
44
58
  public getStream() {
45
59
  return this.out;
46
60
  }
@@ -100,6 +114,9 @@ export class StormCodegen {
100
114
  * Generates the code for a block and sends it to the AI
101
115
  */
102
116
  private async processBlockCode(block: BlockDefinitionInfo) {
117
+ if (this.isAborted()) {
118
+ return;
119
+ }
103
120
  // Generate the code for the block using the standard codegen templates
104
121
  const generatedResult = await this.generateBlock(block.content);
105
122
  if (!generatedResult) {
@@ -109,7 +126,11 @@ export class StormCodegen {
109
126
  const allFiles = this.toStormFiles(generatedResult);
110
127
 
111
128
  // Send all the non-ai files to the stream
112
- this.emitFiles(block.uri, block.aiName, allFiles);
129
+ this.emitFiles(parseKapetaUri(block.uri), block.aiName, allFiles);
130
+
131
+ if (this.isAborted()) {
132
+ return;
133
+ }
113
134
 
114
135
  const relevantFiles: StormFileInfo[] = allFiles.filter(
115
136
  (file) => file.type !== AIFileTypes.IGNORE && file.type !== AIFileTypes.WEB_SCREEN
@@ -125,12 +146,20 @@ export class StormCodegen {
125
146
  });
126
147
 
127
148
  uiStream.on('data', (evt) => {
128
- this.handleUiOutput(block.uri, block.aiName, evt);
149
+ this.handleUiOutput(parseKapetaUri(block.uri), block.aiName, evt);
150
+ });
151
+
152
+ this.out.on('aborted', () => {
153
+ uiStream.abort();
129
154
  });
130
155
 
131
156
  await uiStream.waitForDone();
132
157
  }
133
158
 
159
+ if (this.isAborted()) {
160
+ return;
161
+ }
162
+
134
163
  // Gather the context files for implementation. These will be all be passed to the AI
135
164
  const contextFiles: StormFileInfo[] = relevantFiles.filter(
136
165
  (file) => ![AIFileTypes.SERVICE, AIFileTypes.WEB_SCREEN].includes(file.type)
@@ -140,7 +169,7 @@ export class StormCodegen {
140
169
  const serviceFiles: StormFileInfo[] = allFiles.filter((file) => file.type === AIFileTypes.SERVICE);
141
170
  if (serviceFiles.length > 0) {
142
171
  await this.processTemplates(
143
- block.uri,
172
+ parseKapetaUri(block.uri),
144
173
  block.aiName,
145
174
  stormClient.createServiceImplementation.bind(stormClient),
146
175
  serviceFiles,
@@ -150,6 +179,10 @@ export class StormCodegen {
150
179
 
151
180
  const basePath = this.getBasePath(block.content.metadata.name);
152
181
 
182
+ if (this.isAborted()) {
183
+ return;
184
+ }
185
+
153
186
  for (const serviceFile of serviceFiles) {
154
187
  const filePath = join(basePath, serviceFile.filename);
155
188
  await writeFile(filePath, serviceFile.content);
@@ -168,18 +201,36 @@ export class StormCodegen {
168
201
  const filesToBeFixed = serviceFiles.concat(contextFiles);
169
202
  const codeGenerator = new BlockCodeGenerator(block.content as BlockDefinition);
170
203
  await this.verifyAndFixCode(codeGenerator, basePath, filesToBeFixed, allFiles);
204
+
205
+ const blockRef = block.uri;
206
+ this.out.emit('data', {
207
+ type: 'BLOCK_READY',
208
+ reason: 'Block ready',
209
+ created: Date.now(),
210
+ payload: {
211
+ path: basePath,
212
+ blockName: block.aiName,
213
+ blockRef,
214
+ instanceId: StormEventParser.toInstanceIdFromRef(blockRef),
215
+ },
216
+ } satisfies StormEvent);
171
217
  }
172
218
 
173
- private async verifyAndFixCode(codeGenerator: CodeGenerator, basePath: string, filesToBeFixed: StormFileInfo[], knownFiles: StormFileInfo[]) {
219
+ private async verifyAndFixCode(
220
+ codeGenerator: CodeGenerator,
221
+ basePath: string,
222
+ filesToBeFixed: StormFileInfo[],
223
+ knownFiles: StormFileInfo[]
224
+ ) {
174
225
  let attempts = 0;
175
226
  let validCode = false;
176
227
  for (let i = 0; i <= 3; i++) {
177
228
  attempts++;
178
229
  try {
179
- console.log(`Validating the code in ${basePath} attempt #${attempts}`)
230
+ console.log(`Validating the code in ${basePath} attempt #${attempts}`);
180
231
  const result = await codeGenerator.validateForTarget(basePath);
181
232
  if (result && result.valid) {
182
- validCode = true;
233
+ validCode = true;
183
234
  break;
184
235
  }
185
236
 
@@ -188,20 +239,28 @@ export class StormCodegen {
188
239
  const errorStream = await stormClient.createErrorClassification(result.error, []);
189
240
  const fixes = new Map<string, Promise<string>>();
190
241
 
242
+ this.out.on('aborted', () => {
243
+ errorStream.abort();
244
+ });
245
+
191
246
  errorStream.on('data', (evt) => {
192
247
  if (evt.type === 'ERROR_CLASSIFIER') {
193
248
  // find the file that caused the error
194
249
  // strip base path from event file name, if it exists sometimes the AI sends the full path
195
- const eventFileName = this.removePrefix(basePath+'/', evt.payload.filename);
250
+ const eventFileName = this.removePrefix(basePath + '/', evt.payload.filename);
196
251
  const file = filesToBeFixed.find((f) => f.filename === eventFileName);
197
- if(!file) {
198
- console.log(`Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`);
252
+ if (!file) {
253
+ console.log(
254
+ `Could not find the file ${eventFileName} in the list of files to be fixed, Henrik might wanna create a new file for this fix`
255
+ );
199
256
  }
200
257
  // read the content of the file
201
258
  const content = readFileSync(join(basePath, eventFileName), 'utf8');
202
- const fix = `${evt.payload.potentialFix}\n---\n${knownFiles.map(e => e.filename).join("\n")}\n---\n${content}`;
203
- console.log(`trying to fix the code in ${eventFileName}`);
204
- console.debug(`with the fix:\n${fix}`)
259
+ const fix = `${evt.payload.potentialFix}\n---\n${knownFiles
260
+ .map((e) => e.filename)
261
+ .join('\n')}\n---\n${content}`;
262
+ //console.log(`trying to fix the code in ${eventFileName}`);
263
+ //console.debug(`with the fix:\n${fix}`);
205
264
  const code = this.codeFix(fix);
206
265
  fixes.set(join(basePath, eventFileName), code);
207
266
  }
@@ -226,7 +285,7 @@ export class StormCodegen {
226
285
 
227
286
  removePrefix(prefix: string, str: string): string {
228
287
  if (str.startsWith(prefix)) {
229
- return str.slice(prefix.length);
288
+ return str.slice(prefix.length);
230
289
  }
231
290
  return str;
232
291
  }
@@ -246,6 +305,9 @@ export class StormCodegen {
246
305
  resolve(evt.payload.content);
247
306
  }
248
307
  });
308
+ this.out.on('aborted', () => {
309
+ fixStream.abort();
310
+ });
249
311
  fixStream.on('error', (err) => {
250
312
  reject(err);
251
313
  });
@@ -321,6 +383,10 @@ export class StormCodegen {
321
383
 
322
384
  const files: StormEventFile[] = [];
323
385
 
386
+ this.out.on('aborted', () => {
387
+ stream.abort();
388
+ });
389
+
324
390
  stream.on('data', (evt) => {
325
391
  const file = this.handleTemplateFileOutput(blockUri, aiName, templateFile, evt);
326
392
  if (file) {
@@ -379,6 +445,9 @@ export class StormCodegen {
379
445
  * Generates the code using codegen for a given block.
380
446
  */
381
447
  private async generateBlock(yamlContent: Definition) {
448
+ if (this.isAborted()) {
449
+ return;
450
+ }
382
451
  if (!yamlContent.spec.target?.kind) {
383
452
  //Not all block types have targets
384
453
  return;
@@ -395,4 +464,8 @@ export class StormCodegen {
395
464
  new CodeWriter(basePath).write(generatedResult);
396
465
  return generatedResult;
397
466
  }
467
+
468
+ abort() {
469
+ this.out.abort();
470
+ }
398
471
  }
@@ -3,7 +3,15 @@
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
5
 
6
- import { StormBlockInfoFilled, StormBlockType, StormConnection, StormEvent, StormResourceType } from './events';
6
+ import {
7
+ StormBlockInfoFilled,
8
+ StormBlockType,
9
+ StormConnection,
10
+ StormEvent,
11
+ StormEventPhases,
12
+ StormEventPhaseType,
13
+ StormResourceType,
14
+ } from './events';
7
15
  import {
8
16
  BlockDefinition,
9
17
  BlockInstance,
@@ -29,7 +37,7 @@ import { v5 as uuid } from 'uuid';
29
37
  import { definitionsManager } from '../definitionsManager';
30
38
 
31
39
  export interface BlockDefinitionInfo {
32
- uri: KapetaURI;
40
+ uri: string;
33
41
  content: BlockDefinition;
34
42
  aiName: string;
35
43
  }
@@ -87,6 +95,24 @@ function prettifyKaplang(source: string) {
87
95
  }
88
96
  }
89
97
 
98
+ export function createPhaseStartEvent(type: StormEventPhaseType): StormEventPhases {
99
+ return createPhaseEvent(true, type);
100
+ }
101
+
102
+ export function createPhaseEndEvent(type: StormEventPhaseType): StormEventPhases {
103
+ return createPhaseEvent(false, type);
104
+ }
105
+
106
+ export function createPhaseEvent(start: boolean, type: StormEventPhaseType): StormEventPhases {
107
+ return {
108
+ type: start ? 'PHASE_START' : 'PHASE_END',
109
+ created: Date.now(),
110
+ payload: {
111
+ phaseType: type,
112
+ },
113
+ };
114
+ }
115
+
90
116
  export async function resolveOptions(): Promise<StormOptions> {
91
117
  // Predefined types for now - TODO: Allow user to select / change
92
118
 
@@ -325,7 +351,7 @@ export class StormEventParser {
325
351
  }
326
352
 
327
353
  public toResult(handle: string): StormDefinitions {
328
- const planRef = StormEventParser.toRef(handle, this.planName ?? 'undefined');
354
+ const planRef = StormEventParser.toRef(handle, this.planName || 'undefined');
329
355
  const blockDefinitions = this.toBlockDefinitions(handle);
330
356
  const refIdMap: { [key: string]: string } = {};
331
357
  const blocks = Object.entries(blockDefinitions).map(([ref, block]) => {
@@ -337,7 +363,7 @@ export class StormEventParser {
337
363
  block: {
338
364
  ref,
339
365
  },
340
- name: block.content.metadata.title ?? block.content.metadata.name,
366
+ name: block.content.metadata.title || block.content.metadata.name,
341
367
  dimensions: {
342
368
  left: 0,
343
369
  top: 0,
@@ -454,6 +480,8 @@ export class StormEventParser {
454
480
  name: planRef.fullName,
455
481
  title: this.planName,
456
482
  description: this.planDescription,
483
+ structure: 'mono',
484
+ visibility: 'private',
457
485
  },
458
486
  spec: {
459
487
  blocks,
@@ -474,7 +502,7 @@ export class StormEventParser {
474
502
  const blockRef = StormEventParser.toRef(handle, blockInfo.name);
475
503
 
476
504
  const blockDefinitionInfo: BlockDefinitionInfo = {
477
- uri: blockRef,
505
+ uri: blockRef.toNormalizedString(),
478
506
  aiName: blockInfo.name,
479
507
  content: {
480
508
  kind: this.toBlockKind(blockInfo.type),
@@ -144,9 +144,9 @@ export interface StormEventCodeFix {
144
144
  };
145
145
  }
146
146
  export interface StormEventErrorClassifierInfo {
147
- error: string,
148
- filename: string,
149
- potentialFix: string
147
+ error: string;
148
+ filename: string;
149
+ potentialFix: string;
150
150
  }
151
151
 
152
152
  export interface ScreenTemplate {
@@ -196,6 +196,18 @@ export interface StormEventFile {
196
196
  };
197
197
  }
198
198
 
199
+ export interface StormEventBlockReady {
200
+ type: 'BLOCK_READY';
201
+ reason: string;
202
+ created: number;
203
+ payload: {
204
+ path: string;
205
+ blockName: string;
206
+ blockRef: string;
207
+ instanceId: string;
208
+ };
209
+ }
210
+
199
211
  export interface StormEventDone {
200
212
  type: 'DONE';
201
213
  created: number;
@@ -208,6 +220,20 @@ export interface StormEventDefinitionChange {
208
220
  payload: StormDefinitions;
209
221
  }
210
222
 
223
+ export enum StormEventPhaseType {
224
+ META = 'META',
225
+ DEFINITIONS = 'DEFINITIONS',
226
+ IMPLEMENTATION = 'IMPLEMENTATION',
227
+ }
228
+
229
+ export interface StormEventPhases {
230
+ type: 'PHASE_START' | 'PHASE_END';
231
+ created: number;
232
+ payload: {
233
+ phaseType: StormEventPhaseType;
234
+ };
235
+ }
236
+
211
237
  export type StormEvent =
212
238
  | StormEventCreateBlock
213
239
  | StormEventCreateConnection
@@ -223,4 +249,6 @@ export type StormEvent =
223
249
  | StormEventDone
224
250
  | StormEventDefinitionChange
225
251
  | StormEventErrorClassifier
226
- | StormEventCodeFix;
252
+ | StormEventCodeFix
253
+ | StormEventBlockReady
254
+ | StormEventPhases;
@@ -4,15 +4,24 @@
4
4
  */
5
5
 
6
6
  import Router from 'express-promise-router';
7
+ import FS from 'fs-extra';
7
8
  import { Response } from 'express';
8
9
  import { corsHandler } from '../middleware/cors';
9
10
  import { stringBody } from '../middleware/stringBody';
10
11
  import { KapetaBodyRequest } from '../types';
11
- import { StormContextRequest, StormFileImplementationPrompt, StormFileInfo, StormStream } from './stream';
12
+ import { StormContextRequest, StormCreateBlockRequest, StormStream } from './stream';
12
13
  import { ConversationIdHeader, stormClient } from './stormClient';
13
- import { StormEvent } from './events';
14
- import { resolveOptions, StormDefinitions, StormEventParser } from './event-parser';
14
+ import { StormEvent, StormEventPhaseType } from './events';
15
+ import {
16
+ createPhaseEndEvent,
17
+ createPhaseStartEvent,
18
+ resolveOptions,
19
+ StormDefinitions,
20
+ StormEventParser,
21
+ } from './event-parser';
15
22
  import { StormCodegen } from './codegen';
23
+ import { assetManager } from '../assetManager';
24
+ import Path from 'path';
16
25
 
17
26
  const router = Router();
18
27
 
@@ -32,18 +41,48 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
32
41
  const aiRequest: StormContextRequest = JSON.parse(req.stringBody ?? '{}');
33
42
  const metaStream = await stormClient.createMetadata(aiRequest.prompt, conversationId);
34
43
 
44
+ onRequestAborted(req, res, () => {
45
+ metaStream.abort();
46
+ });
47
+
35
48
  res.set('Content-Type', 'application/x-ndjson');
36
49
  res.set('Access-Control-Expose-Headers', ConversationIdHeader);
37
50
  res.set(ConversationIdHeader, metaStream.getConversationId());
38
51
 
52
+ let currentPhase = StormEventPhaseType.META;
53
+
39
54
  metaStream.on('data', (data: StormEvent) => {
40
55
  const result = eventParser.processEvent(req.params.handle, data);
41
56
 
57
+ switch (data.type) {
58
+ case 'CREATE_API':
59
+ case 'CREATE_MODEL':
60
+ case 'CREATE_TYPE':
61
+ if (currentPhase !== StormEventPhaseType.DEFINITIONS) {
62
+ sendEvent(res, createPhaseEndEvent(StormEventPhaseType.META));
63
+ currentPhase = StormEventPhaseType.DEFINITIONS;
64
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.DEFINITIONS));
65
+ }
66
+ break;
67
+ }
68
+
42
69
  sendEvent(res, data);
43
70
  sendDefinitions(res, result);
44
71
  });
45
72
 
46
- await waitForStormStream(metaStream);
73
+ try {
74
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.META));
75
+
76
+ await waitForStormStream(metaStream);
77
+ } finally {
78
+ if (!metaStream.isAborted()) {
79
+ sendEvent(res, createPhaseEndEvent(currentPhase));
80
+ }
81
+ }
82
+
83
+ if (metaStream.isAborted()) {
84
+ return;
85
+ }
47
86
 
48
87
  if (!eventParser.isValid()) {
49
88
  // We can't continue if the meta stream is invalid
@@ -59,22 +98,66 @@ router.post('/:handle/all', async (req: KapetaBodyRequest, res: Response) => {
59
98
 
60
99
  const result = eventParser.toResult(handle);
61
100
 
101
+ if (metaStream.isAborted()) {
102
+ return;
103
+ }
104
+
62
105
  sendDefinitions(res, result);
63
106
 
64
107
  if (!req.query.skipCodegen) {
65
- const stormCodegen = new StormCodegen(aiRequest.prompt, result.blocks, eventParser.getEvents());
66
-
67
- const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
68
-
69
- await stormCodegen.process();
70
-
71
- await codegenPromise;
108
+ try {
109
+ sendEvent(res, createPhaseStartEvent(StormEventPhaseType.IMPLEMENTATION));
110
+ const stormCodegen = new StormCodegen(
111
+ metaStream.getConversationId(),
112
+ aiRequest.prompt,
113
+ result.blocks,
114
+ eventParser.getEvents()
115
+ );
116
+
117
+ onRequestAborted(req, res, () => {
118
+ stormCodegen.abort();
119
+ });
120
+
121
+ const codegenPromise = streamStormPartialResponse(stormCodegen.getStream(), res);
122
+
123
+ await stormCodegen.process();
124
+
125
+ await codegenPromise;
126
+ } finally {
127
+ if (!metaStream.isAborted()) {
128
+ sendEvent(res, createPhaseEndEvent(StormEventPhaseType.IMPLEMENTATION));
129
+ }
130
+ }
72
131
  }
73
132
 
74
133
  sendDone(res);
75
134
  } catch (err: any) {
76
135
  sendError(err, res);
77
- res.end();
136
+ if (!res.closed) {
137
+ res.end();
138
+ }
139
+ }
140
+ });
141
+
142
+ router.post('/block/create', async (req: KapetaBodyRequest, res: Response) => {
143
+ const createRequest: StormCreateBlockRequest = JSON.parse(req.stringBody ?? '{}');
144
+
145
+ try {
146
+ const ymlPath = Path.join(createRequest.newPath, 'kapeta.yml');
147
+
148
+ const [asset] = await assetManager.createAsset(ymlPath, createRequest.definition);
149
+
150
+ if (await FS.pathExists(createRequest.tmpPath)) {
151
+ await FS.move(createRequest.tmpPath, createRequest.newPath, {
152
+ overwrite: true,
153
+ });
154
+
155
+ res.send(await assetManager.updateAsset(asset.ref, createRequest.definition));
156
+ } else {
157
+ res.send(asset);
158
+ }
159
+ } catch (err: any) {
160
+ res.status(500).send({ error: err.message });
78
161
  }
79
162
  });
80
163
 
@@ -88,6 +171,9 @@ function sendDefinitions(res: Response, result: StormDefinitions) {
88
171
  }
89
172
 
90
173
  function sendDone(res: Response) {
174
+ if (res.closed) {
175
+ return;
176
+ }
91
177
  sendEvent(res, {
92
178
  type: 'DONE',
93
179
  created: Date.now(),
@@ -97,6 +183,9 @@ function sendDone(res: Response) {
97
183
  }
98
184
 
99
185
  function sendError(err: Error, res: Response) {
186
+ if (res.closed) {
187
+ return;
188
+ }
100
189
  console.error('Failed to send prompt', err);
101
190
  if (res.headersSent) {
102
191
  sendEvent(res, {
@@ -138,7 +227,19 @@ function streamStormPartialResponse(result: StormStream, res: Response) {
138
227
  }
139
228
 
140
229
  function sendEvent(res: Response, evt: StormEvent) {
230
+ if (res.closed) {
231
+ return;
232
+ }
141
233
  res.write(JSON.stringify(evt) + '\n');
142
234
  }
143
235
 
236
+ function onRequestAborted(req: KapetaBodyRequest, res: Response, onAborted: () => void) {
237
+ req.on('close', () => {
238
+ onAborted();
239
+ });
240
+ res.on('close', () => {
241
+ onAborted();
242
+ });
243
+ }
244
+
144
245
  export default router;
@@ -13,6 +13,7 @@ import {
13
13
  StormStream,
14
14
  StormUIImplementationPrompt,
15
15
  } from './stream';
16
+ import { getRawAsset } from 'node:sea';
16
17
 
17
18
  export const STORM_ID = 'storm';
18
19
 
@@ -60,6 +61,9 @@ class StormClient {
60
61
  conversationId: body.conversationId,
61
62
  });
62
63
 
64
+ const abort = new AbortController();
65
+ options.signal = abort.signal;
66
+
63
67
  const response = await fetch(options.url, options);
64
68
 
65
69
  if (response.status !== 200) {
@@ -69,7 +73,6 @@ class StormClient {
69
73
  }
70
74
 
71
75
  const conversationId = response.headers.get(ConversationIdHeader);
72
- console.log('Received conversationId', conversationId);
73
76
 
74
77
  const out = new StormStream(stringPrompt, conversationId);
75
78
 
@@ -87,6 +90,10 @@ class StormClient {
87
90
  out.end();
88
91
  });
89
92
 
93
+ out.on('aborted', () => {
94
+ abort.abort();
95
+ });
96
+
90
97
  return out;
91
98
  }
92
99