@kapeta/local-cluster-service 0.67.4 → 0.67.5

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 CHANGED
@@ -1,3 +1,11 @@
1
+ ## [0.67.5](https://github.com/kapetacom/local-cluster-service/compare/v0.67.4...v0.67.5) (2024-09-02)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * better tracking of async events [CORE-3409] ([5e8f8cd](https://github.com/kapetacom/local-cluster-service/commit/5e8f8cdbe7e714cf0dfb5b0e7cbb8b60f837c2f9))
7
+ * dont generate individual pages when page matches path w/ params ([c3ca35c](https://github.com/kapetacom/local-cluster-service/commit/c3ca35ce56781ca90f3ca1070fee76aa3f5512ab))
8
+
1
9
  ## [0.67.4](https://github.com/kapetacom/local-cluster-service/compare/v0.67.3...v0.67.4) (2024-08-30)
2
10
 
3
11
 
@@ -6,7 +6,6 @@
6
6
  import { UIPagePrompt } from './stormClient';
7
7
  import { ReferenceClassification, StormEvent, StormEventPage, StormImage, UIShell } from './events';
8
8
  import { EventEmitter } from 'node:events';
9
- import { FuturePromise } from './PromiseQueue';
10
9
  export interface ImagePrompt {
11
10
  name: string;
12
11
  description: string;
@@ -21,6 +20,7 @@ type InitialPrompt = Omit<UIPagePrompt, 'shell_page'> & {
21
20
  };
22
21
  export declare class PageQueue extends EventEmitter {
23
22
  private readonly queue;
23
+ private readonly eventQueue;
24
24
  private readonly systemId;
25
25
  private readonly systemPrompt;
26
26
  private readonly references;
@@ -29,12 +29,12 @@ export declare class PageQueue extends EventEmitter {
29
29
  private uiShells;
30
30
  private theme;
31
31
  constructor(systemId: string, systemPrompt: string, concurrency?: number);
32
- on(event: 'event', listener: (data: StormEvent) => void): this;
33
- on(event: 'page', listener: (data: StormEventPage) => void): this;
34
- on(event: 'image', listener: (data: StormImage, source: ImagePrompt, future: FuturePromise<void>) => void): this;
32
+ on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
33
+ on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
34
+ on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
35
35
  emit(type: 'event', event: StormEvent): boolean;
36
36
  emit(type: 'page', event: StormEventPage): boolean;
37
- emit(type: 'image', event: StormImage, source: ImagePrompt, future: FuturePromise<void>): boolean;
37
+ emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
38
38
  addUiShell(uiShell: UIShell): void;
39
39
  setUiTheme(theme: string): void;
40
40
  private hasPrompt;
@@ -47,6 +47,7 @@ export declare class PageQueue extends EventEmitter {
47
47
  private addImagePrompt;
48
48
  }
49
49
  export declare class PageGenerator extends EventEmitter {
50
+ private readonly eventQueue;
50
51
  private readonly conversationId;
51
52
  readonly prompt: UIPagePrompt;
52
53
  constructor(prompt: UIPagePrompt, conversationId?: string);
@@ -54,13 +55,11 @@ export declare class PageGenerator extends EventEmitter {
54
55
  on(event: 'page_refs', listener: (data: {
55
56
  event: StormEventPage;
56
57
  references: ReferenceClassification[];
57
- future: FuturePromise<void>;
58
- }) => void): this;
58
+ }) => Promise<void>): this;
59
59
  emit(type: 'event', event: StormEvent): boolean;
60
60
  emit(type: 'page_refs', event: {
61
61
  event: StormEventPage;
62
62
  references: ReferenceClassification[];
63
- future: FuturePromise<void>;
64
63
  }): boolean;
65
64
  generate(): Promise<void>;
66
65
  private resolveReferences;
@@ -15,6 +15,7 @@ const PromiseQueue_1 = require("./PromiseQueue");
15
15
  const page_utils_1 = require("./page-utils");
16
16
  class PageQueue extends node_events_1.EventEmitter {
17
17
  queue;
18
+ eventQueue;
18
19
  systemId;
19
20
  systemPrompt;
20
21
  references = new Map();
@@ -27,9 +28,12 @@ class PageQueue extends node_events_1.EventEmitter {
27
28
  this.systemId = systemId;
28
29
  this.systemPrompt = systemPrompt;
29
30
  this.queue = new PromiseQueue_1.PromiseQueue(concurrency);
31
+ this.eventQueue = new PromiseQueue_1.PromiseQueue(Number.MAX_VALUE);
30
32
  }
31
33
  on(event, listener) {
32
- return super.on(event, listener);
34
+ return super.on(event, (...args) => {
35
+ void this.eventQueue.add(async () => listener(...args));
36
+ });
33
37
  }
34
38
  emit(eventName, ...args) {
35
39
  return super.emit(eventName, ...args);
@@ -56,6 +60,7 @@ class PageQueue extends node_events_1.EventEmitter {
56
60
  //console.log('Ignoring duplicate prompt', initialPrompt.path);
57
61
  return Promise.resolve();
58
62
  }
63
+ console.log('Generating page for', initialPrompt.method, initialPrompt.path);
59
64
  const prompt = {
60
65
  ...initialPrompt,
61
66
  shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
@@ -75,7 +80,7 @@ class PageQueue extends node_events_1.EventEmitter {
75
80
  return promptPrefix;
76
81
  }
77
82
  wrapPagePrompt(pagePath, prompt) {
78
- let promptPrefix = this.getPrefix();
83
+ const promptPrefix = this.getPrefix();
79
84
  let promptPostfix = '';
80
85
  if (this.pages.size > 0) {
81
86
  promptPostfix = `\nThe following pages are already implemented:\n`;
@@ -96,10 +101,10 @@ class PageQueue extends node_events_1.EventEmitter {
96
101
  }
97
102
  async addPageGenerator(generator) {
98
103
  generator.on('event', (event) => this.emit('event', event));
99
- generator.on('page_refs', async ({ event, references, future }) => {
104
+ generator.on('page_refs', async ({ event, references }) => {
100
105
  try {
101
106
  const initialPrompts = [];
102
- let promises = references.map(async (reference) => {
107
+ const resourcePromises = references.map(async (reference) => {
103
108
  if (reference.url.startsWith('#') ||
104
109
  reference.url.startsWith('javascript:') ||
105
110
  reference.url.startsWith('http://') ||
@@ -135,8 +140,11 @@ class PageQueue extends node_events_1.EventEmitter {
135
140
  break;
136
141
  }
137
142
  });
138
- await Promise.allSettled(promises);
139
- initialPrompts.forEach((prompt) => {
143
+ // Wait for resources to be generated
144
+ await Promise.allSettled(resourcePromises);
145
+ this.emit('page', event);
146
+ // Emit any new pages after the current page to increase responsiveness
147
+ const newPages = initialPrompts.map((prompt) => {
140
148
  if (!this.hasPrompt(prompt.path)) {
141
149
  this.emit('page', {
142
150
  type: 'PAGE',
@@ -155,21 +163,24 @@ class PageQueue extends node_events_1.EventEmitter {
155
163
  },
156
164
  });
157
165
  }
158
- this.addPrompt(prompt);
166
+ return this.addPrompt(prompt);
159
167
  });
160
- this.emit('page', event);
168
+ await Promise.allSettled(newPages);
161
169
  }
162
- finally {
163
- future.resolve();
170
+ catch (e) {
171
+ console.error('Failed to process event', e);
172
+ throw e;
164
173
  }
165
174
  });
166
175
  return this.queue.add(() => generator.generate());
167
176
  }
168
177
  cancel() {
169
178
  this.queue.cancel();
179
+ this.eventQueue.cancel();
170
180
  }
171
- wait() {
172
- return this.queue.wait();
181
+ async wait() {
182
+ await this.eventQueue.wait();
183
+ await this.queue.wait();
173
184
  }
174
185
  async addImagePrompt(prompt) {
175
186
  if (this.images.has(prompt.url)) {
@@ -180,65 +191,54 @@ class PageQueue extends node_events_1.EventEmitter {
180
191
  const prefix = this.getPrefix();
181
192
  const result = await stormClient_1.stormClient.createImage(prefix + `Create an image for the url "${prompt.url}" with this description: ${prompt.description}`.trim());
182
193
  //console.log('Adding image prompt', prompt);
183
- const futures = [];
184
- result.on('data', async (event) => {
194
+ result.on('data', (event) => {
185
195
  if (event.type === 'IMAGE') {
186
- const future = (0, PromiseQueue_1.createFuture)();
187
- futures.push(future);
188
- this.emit('image', event, prompt, future);
189
- setTimeout(() => {
190
- if (!future.isResolved()) {
191
- console.log('Image prompt timed out', prompt);
192
- future.reject(new Error('Image prompt timed out'));
193
- }
194
- }, 30000);
196
+ this.emit('image', event, prompt);
195
197
  }
196
198
  });
197
199
  await result.waitForDone();
198
- await Promise.allSettled(futures.map((f) => f.promise));
199
200
  }
200
201
  }
201
202
  exports.PageQueue = PageQueue;
202
203
  class PageGenerator extends node_events_1.EventEmitter {
204
+ eventQueue;
203
205
  conversationId;
204
206
  prompt;
205
207
  constructor(prompt, conversationId = node_uuid_1.default.v4()) {
206
208
  super();
207
209
  this.conversationId = conversationId;
208
210
  this.prompt = prompt;
211
+ this.eventQueue = new PromiseQueue_1.PromiseQueue(Number.MAX_VALUE);
209
212
  }
210
213
  on(event, listener) {
211
- return super.on(event, listener);
214
+ return super.on(event, (...args) => {
215
+ void this.eventQueue.add(async () => listener(...args));
216
+ });
212
217
  }
213
218
  emit(eventName, ...args) {
214
219
  return super.emit(eventName, ...args);
215
220
  }
216
221
  async generate() {
217
- return new Promise(async (resolve) => {
218
- const promises = [];
219
- const screenStream = await stormClient_1.stormClient.createUIPage(this.prompt, this.conversationId);
220
- screenStream.on('data', (event) => {
221
- if (event.type === 'PAGE') {
222
- event.payload.conversationId = this.conversationId;
223
- promises.push((async () => {
224
- const references = await this.resolveReferences(event.payload.content);
225
- //console.log('Resolved references for page', references, event.payload);
226
- const future = (0, PromiseQueue_1.createFuture)();
227
- this.emit('page_refs', {
228
- event,
229
- references,
230
- future,
231
- });
232
- await future.promise;
233
- })());
234
- return;
235
- }
236
- this.emit('event', event);
237
- });
238
- await screenStream.waitForDone();
239
- await Promise.all(promises);
240
- resolve();
222
+ const promises = [];
223
+ const screenStream = await stormClient_1.stormClient.createUIPage(this.prompt, this.conversationId);
224
+ screenStream.on('data', (event) => {
225
+ if (event.type === 'PAGE') {
226
+ event.payload.conversationId = this.conversationId;
227
+ promises.push((async () => {
228
+ const references = await this.resolveReferences(event.payload.content);
229
+ //console.log('Resolved references for page', references, event.payload);
230
+ this.emit('page_refs', {
231
+ event,
232
+ references,
233
+ });
234
+ })());
235
+ return;
236
+ }
237
+ this.emit('event', event);
241
238
  });
239
+ await screenStream.waitForDone();
240
+ await Promise.all(promises);
241
+ await this.eventQueue.wait();
242
242
  }
243
243
  async resolveReferences(content) {
244
244
  const referenceStream = await stormClient_1.stormClient.classifyUIReferences(content);
@@ -12,7 +12,7 @@ function normalizePath(path) {
12
12
  return path
13
13
  .replace(/\?.*$/gi, '')
14
14
  .replace(/:[a-z][a-z_]*\b/gi, '*')
15
- .replace(/\{[a-z]+}/gi, '*');
15
+ .replace(/\{[a-z-.]+}/gi, '*');
16
16
  }
17
17
  async function writePageToDisk(systemId, event) {
18
18
  const baseDir = getSystemBaseDir(systemId);
@@ -53,13 +53,8 @@ async function writeImageToDisk(systemId, event, prompt) {
53
53
  }
54
54
  exports.writeImageToDisk = writeImageToDisk;
55
55
  function hasPageOnDisk(systemId, method, path) {
56
- if (!systemId || !method || !path) {
57
- return false;
58
- }
59
- const baseDir = getSystemBaseDir(systemId);
60
- const filePath = getFilePath(method);
61
- const fullPath = path_1.default.join(baseDir, normalizePath(path), filePath);
62
- return fs_extra_1.default.existsSync(fullPath);
56
+ const fullPath = resolveReadPath(systemId, method, path);
57
+ return !!fullPath && fs_extra_1.default.existsSync(fullPath);
63
58
  }
64
59
  exports.hasPageOnDisk = hasPageOnDisk;
65
60
  function getSystemBaseDir(systemId) {
@@ -90,7 +85,7 @@ function resolveReadPath(systemId, path, method) {
90
85
  }
91
86
  const parts = path.split(/\*/g);
92
87
  let currentPath = '';
93
- for (let part in parts) {
88
+ for (let part of parts) {
94
89
  const thisPath = path_1.default.join(currentPath, part);
95
90
  const starPath = path_1.default.join(currentPath, '*');
96
91
  const thisPathDir = path_1.default.join(baseDir, thisPath);
@@ -103,7 +98,6 @@ function resolveReadPath(systemId, path, method) {
103
98
  currentPath = starPath;
104
99
  continue;
105
100
  }
106
- console.log('Path not found', thisPathDir, starPathDir);
107
101
  // Path not found
108
102
  return null;
109
103
  }
@@ -74,24 +74,17 @@ router.post('/ui/screen', async (req, res) => {
74
74
  queue.cancel();
75
75
  });
76
76
  const promises = [];
77
- queue.on('page', (data) => {
78
- if (systemId) {
79
- promises.push(sendPageEvent(systemId, data, res));
80
- }
81
- });
82
- queue.on('image', async (screenData, prompt, future) => {
77
+ queue.on('page', (data) => (systemId ? sendPageEvent(systemId, data, res) : undefined));
78
+ queue.on('image', async (screenData, prompt) => {
83
79
  if (!systemId) {
84
80
  return;
85
81
  }
86
82
  try {
87
- const promise = handleImageEvent(systemId, screenData, prompt);
88
- promises.push(promise);
89
- await promise;
90
- future.resolve();
83
+ await handleImageEvent(systemId, screenData, prompt);
91
84
  }
92
85
  catch (e) {
93
86
  console.error('Failed to handle image event', e);
94
- future.reject(e);
87
+ throw e;
95
88
  }
96
89
  });
97
90
  await queue.addPrompt(aiRequest, conversationId, true);
@@ -186,19 +179,14 @@ router.post('/:handle/ui/iterative', async (req, res) => {
186
179
  onRequestAborted(req, res, () => {
187
180
  pageQueue.cancel();
188
181
  });
189
- pageQueue.on('page', (screenData) => {
190
- pageEventPromises.push(sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
191
- });
192
- pageQueue.on('image', async (screenData, prompt, future) => {
182
+ pageQueue.on('page', (screenData) => sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
183
+ pageQueue.on('image', async (screenData, prompt) => {
193
184
  try {
194
- const promise = handleImageEvent(landingPagesStream.getConversationId(), screenData, prompt);
195
- pageEventPromises.push(promise);
196
- await promise;
197
- future.resolve();
185
+ await handleImageEvent(landingPagesStream.getConversationId(), screenData, prompt);
198
186
  }
199
187
  catch (e) {
200
188
  console.error('Failed to handle image event', e);
201
- future.reject(e);
189
+ throw e;
202
190
  }
203
191
  });
204
192
  pageQueue.on('event', (screenData) => {
@@ -346,31 +334,25 @@ router.post('/:handle/ui', async (req, res) => {
346
334
  },
347
335
  created: Date.now(),
348
336
  });
349
- const pagePromises = [];
350
337
  onRequestAborted(req, res, () => {
351
338
  queue.cancel();
352
339
  });
353
- const pageEventPromises = [];
354
- queue.on('page', (screenData) => {
355
- pageEventPromises.push(sendPageEvent(outerConversationId, screenData, res));
356
- });
357
- queue.on('image', async (screenData, prompt, future) => {
340
+ queue.on('page', (screenData) => sendPageEvent(outerConversationId, screenData, res));
341
+ queue.on('image', async (screenData, prompt) => {
358
342
  try {
359
- const promise = handleImageEvent(outerConversationId, screenData, prompt);
360
- pageEventPromises.push(promise);
361
- await promise;
362
- future.resolve();
343
+ await handleImageEvent(outerConversationId, screenData, prompt);
363
344
  }
364
345
  catch (e) {
365
346
  console.error('Failed to handle image event', e);
366
- future.reject(e);
347
+ throw e;
367
348
  }
368
349
  });
369
350
  queue.on('event', (screenData) => {
370
351
  sendEvent(res, screenData);
371
352
  });
372
353
  for (const screen of Object.values(uniqueUserJourneyScreens)) {
373
- pagePromises.push(queue.addPrompt({
354
+ queue
355
+ .addPrompt({
374
356
  prompt: screen.requirements,
375
357
  method: screen.method,
376
358
  path: screen.path,
@@ -380,14 +362,15 @@ router.post('/:handle/ui', async (req, res) => {
380
362
  filename: screen.filename,
381
363
  storage_prefix: outerConversationId + '_',
382
364
  theme,
383
- }));
365
+ })
366
+ .catch((e) => {
367
+ console.error('Failed to generate page for screen %s', screen.name, e);
368
+ });
384
369
  }
385
370
  if (userJourneysStream.isAborted()) {
386
371
  return;
387
372
  }
388
373
  await queue.wait();
389
- await Promise.allSettled(pagePromises);
390
- await Promise.allSettled(pageEventPromises);
391
374
  sendDone(res);
392
375
  }
393
376
  catch (err) {
@@ -6,7 +6,6 @@
6
6
  import { UIPagePrompt } from './stormClient';
7
7
  import { ReferenceClassification, StormEvent, StormEventPage, StormImage, UIShell } from './events';
8
8
  import { EventEmitter } from 'node:events';
9
- import { FuturePromise } from './PromiseQueue';
10
9
  export interface ImagePrompt {
11
10
  name: string;
12
11
  description: string;
@@ -21,6 +20,7 @@ type InitialPrompt = Omit<UIPagePrompt, 'shell_page'> & {
21
20
  };
22
21
  export declare class PageQueue extends EventEmitter {
23
22
  private readonly queue;
23
+ private readonly eventQueue;
24
24
  private readonly systemId;
25
25
  private readonly systemPrompt;
26
26
  private readonly references;
@@ -29,12 +29,12 @@ export declare class PageQueue extends EventEmitter {
29
29
  private uiShells;
30
30
  private theme;
31
31
  constructor(systemId: string, systemPrompt: string, concurrency?: number);
32
- on(event: 'event', listener: (data: StormEvent) => void): this;
33
- on(event: 'page', listener: (data: StormEventPage) => void): this;
34
- on(event: 'image', listener: (data: StormImage, source: ImagePrompt, future: FuturePromise<void>) => void): this;
32
+ on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
33
+ on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
34
+ on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
35
35
  emit(type: 'event', event: StormEvent): boolean;
36
36
  emit(type: 'page', event: StormEventPage): boolean;
37
- emit(type: 'image', event: StormImage, source: ImagePrompt, future: FuturePromise<void>): boolean;
37
+ emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
38
38
  addUiShell(uiShell: UIShell): void;
39
39
  setUiTheme(theme: string): void;
40
40
  private hasPrompt;
@@ -47,6 +47,7 @@ export declare class PageQueue extends EventEmitter {
47
47
  private addImagePrompt;
48
48
  }
49
49
  export declare class PageGenerator extends EventEmitter {
50
+ private readonly eventQueue;
50
51
  private readonly conversationId;
51
52
  readonly prompt: UIPagePrompt;
52
53
  constructor(prompt: UIPagePrompt, conversationId?: string);
@@ -54,13 +55,11 @@ export declare class PageGenerator extends EventEmitter {
54
55
  on(event: 'page_refs', listener: (data: {
55
56
  event: StormEventPage;
56
57
  references: ReferenceClassification[];
57
- future: FuturePromise<void>;
58
- }) => void): this;
58
+ }) => Promise<void>): this;
59
59
  emit(type: 'event', event: StormEvent): boolean;
60
60
  emit(type: 'page_refs', event: {
61
61
  event: StormEventPage;
62
62
  references: ReferenceClassification[];
63
- future: FuturePromise<void>;
64
63
  }): boolean;
65
64
  generate(): Promise<void>;
66
65
  private resolveReferences;
@@ -15,6 +15,7 @@ const PromiseQueue_1 = require("./PromiseQueue");
15
15
  const page_utils_1 = require("./page-utils");
16
16
  class PageQueue extends node_events_1.EventEmitter {
17
17
  queue;
18
+ eventQueue;
18
19
  systemId;
19
20
  systemPrompt;
20
21
  references = new Map();
@@ -27,9 +28,12 @@ class PageQueue extends node_events_1.EventEmitter {
27
28
  this.systemId = systemId;
28
29
  this.systemPrompt = systemPrompt;
29
30
  this.queue = new PromiseQueue_1.PromiseQueue(concurrency);
31
+ this.eventQueue = new PromiseQueue_1.PromiseQueue(Number.MAX_VALUE);
30
32
  }
31
33
  on(event, listener) {
32
- return super.on(event, listener);
34
+ return super.on(event, (...args) => {
35
+ void this.eventQueue.add(async () => listener(...args));
36
+ });
33
37
  }
34
38
  emit(eventName, ...args) {
35
39
  return super.emit(eventName, ...args);
@@ -56,6 +60,7 @@ class PageQueue extends node_events_1.EventEmitter {
56
60
  //console.log('Ignoring duplicate prompt', initialPrompt.path);
57
61
  return Promise.resolve();
58
62
  }
63
+ console.log('Generating page for', initialPrompt.method, initialPrompt.path);
59
64
  const prompt = {
60
65
  ...initialPrompt,
61
66
  shell_page: this.uiShells.find((shell) => shell.screens.includes(initialPrompt.name))?.content,
@@ -75,7 +80,7 @@ class PageQueue extends node_events_1.EventEmitter {
75
80
  return promptPrefix;
76
81
  }
77
82
  wrapPagePrompt(pagePath, prompt) {
78
- let promptPrefix = this.getPrefix();
83
+ const promptPrefix = this.getPrefix();
79
84
  let promptPostfix = '';
80
85
  if (this.pages.size > 0) {
81
86
  promptPostfix = `\nThe following pages are already implemented:\n`;
@@ -96,10 +101,10 @@ class PageQueue extends node_events_1.EventEmitter {
96
101
  }
97
102
  async addPageGenerator(generator) {
98
103
  generator.on('event', (event) => this.emit('event', event));
99
- generator.on('page_refs', async ({ event, references, future }) => {
104
+ generator.on('page_refs', async ({ event, references }) => {
100
105
  try {
101
106
  const initialPrompts = [];
102
- let promises = references.map(async (reference) => {
107
+ const resourcePromises = references.map(async (reference) => {
103
108
  if (reference.url.startsWith('#') ||
104
109
  reference.url.startsWith('javascript:') ||
105
110
  reference.url.startsWith('http://') ||
@@ -135,8 +140,11 @@ class PageQueue extends node_events_1.EventEmitter {
135
140
  break;
136
141
  }
137
142
  });
138
- await Promise.allSettled(promises);
139
- initialPrompts.forEach((prompt) => {
143
+ // Wait for resources to be generated
144
+ await Promise.allSettled(resourcePromises);
145
+ this.emit('page', event);
146
+ // Emit any new pages after the current page to increase responsiveness
147
+ const newPages = initialPrompts.map((prompt) => {
140
148
  if (!this.hasPrompt(prompt.path)) {
141
149
  this.emit('page', {
142
150
  type: 'PAGE',
@@ -155,21 +163,24 @@ class PageQueue extends node_events_1.EventEmitter {
155
163
  },
156
164
  });
157
165
  }
158
- this.addPrompt(prompt);
166
+ return this.addPrompt(prompt);
159
167
  });
160
- this.emit('page', event);
168
+ await Promise.allSettled(newPages);
161
169
  }
162
- finally {
163
- future.resolve();
170
+ catch (e) {
171
+ console.error('Failed to process event', e);
172
+ throw e;
164
173
  }
165
174
  });
166
175
  return this.queue.add(() => generator.generate());
167
176
  }
168
177
  cancel() {
169
178
  this.queue.cancel();
179
+ this.eventQueue.cancel();
170
180
  }
171
- wait() {
172
- return this.queue.wait();
181
+ async wait() {
182
+ await this.eventQueue.wait();
183
+ await this.queue.wait();
173
184
  }
174
185
  async addImagePrompt(prompt) {
175
186
  if (this.images.has(prompt.url)) {
@@ -180,65 +191,54 @@ class PageQueue extends node_events_1.EventEmitter {
180
191
  const prefix = this.getPrefix();
181
192
  const result = await stormClient_1.stormClient.createImage(prefix + `Create an image for the url "${prompt.url}" with this description: ${prompt.description}`.trim());
182
193
  //console.log('Adding image prompt', prompt);
183
- const futures = [];
184
- result.on('data', async (event) => {
194
+ result.on('data', (event) => {
185
195
  if (event.type === 'IMAGE') {
186
- const future = (0, PromiseQueue_1.createFuture)();
187
- futures.push(future);
188
- this.emit('image', event, prompt, future);
189
- setTimeout(() => {
190
- if (!future.isResolved()) {
191
- console.log('Image prompt timed out', prompt);
192
- future.reject(new Error('Image prompt timed out'));
193
- }
194
- }, 30000);
196
+ this.emit('image', event, prompt);
195
197
  }
196
198
  });
197
199
  await result.waitForDone();
198
- await Promise.allSettled(futures.map((f) => f.promise));
199
200
  }
200
201
  }
201
202
  exports.PageQueue = PageQueue;
202
203
  class PageGenerator extends node_events_1.EventEmitter {
204
+ eventQueue;
203
205
  conversationId;
204
206
  prompt;
205
207
  constructor(prompt, conversationId = node_uuid_1.default.v4()) {
206
208
  super();
207
209
  this.conversationId = conversationId;
208
210
  this.prompt = prompt;
211
+ this.eventQueue = new PromiseQueue_1.PromiseQueue(Number.MAX_VALUE);
209
212
  }
210
213
  on(event, listener) {
211
- return super.on(event, listener);
214
+ return super.on(event, (...args) => {
215
+ void this.eventQueue.add(async () => listener(...args));
216
+ });
212
217
  }
213
218
  emit(eventName, ...args) {
214
219
  return super.emit(eventName, ...args);
215
220
  }
216
221
  async generate() {
217
- return new Promise(async (resolve) => {
218
- const promises = [];
219
- const screenStream = await stormClient_1.stormClient.createUIPage(this.prompt, this.conversationId);
220
- screenStream.on('data', (event) => {
221
- if (event.type === 'PAGE') {
222
- event.payload.conversationId = this.conversationId;
223
- promises.push((async () => {
224
- const references = await this.resolveReferences(event.payload.content);
225
- //console.log('Resolved references for page', references, event.payload);
226
- const future = (0, PromiseQueue_1.createFuture)();
227
- this.emit('page_refs', {
228
- event,
229
- references,
230
- future,
231
- });
232
- await future.promise;
233
- })());
234
- return;
235
- }
236
- this.emit('event', event);
237
- });
238
- await screenStream.waitForDone();
239
- await Promise.all(promises);
240
- resolve();
222
+ const promises = [];
223
+ const screenStream = await stormClient_1.stormClient.createUIPage(this.prompt, this.conversationId);
224
+ screenStream.on('data', (event) => {
225
+ if (event.type === 'PAGE') {
226
+ event.payload.conversationId = this.conversationId;
227
+ promises.push((async () => {
228
+ const references = await this.resolveReferences(event.payload.content);
229
+ //console.log('Resolved references for page', references, event.payload);
230
+ this.emit('page_refs', {
231
+ event,
232
+ references,
233
+ });
234
+ })());
235
+ return;
236
+ }
237
+ this.emit('event', event);
241
238
  });
239
+ await screenStream.waitForDone();
240
+ await Promise.all(promises);
241
+ await this.eventQueue.wait();
242
242
  }
243
243
  async resolveReferences(content) {
244
244
  const referenceStream = await stormClient_1.stormClient.classifyUIReferences(content);
@@ -12,7 +12,7 @@ function normalizePath(path) {
12
12
  return path
13
13
  .replace(/\?.*$/gi, '')
14
14
  .replace(/:[a-z][a-z_]*\b/gi, '*')
15
- .replace(/\{[a-z]+}/gi, '*');
15
+ .replace(/\{[a-z-.]+}/gi, '*');
16
16
  }
17
17
  async function writePageToDisk(systemId, event) {
18
18
  const baseDir = getSystemBaseDir(systemId);
@@ -53,13 +53,8 @@ async function writeImageToDisk(systemId, event, prompt) {
53
53
  }
54
54
  exports.writeImageToDisk = writeImageToDisk;
55
55
  function hasPageOnDisk(systemId, method, path) {
56
- if (!systemId || !method || !path) {
57
- return false;
58
- }
59
- const baseDir = getSystemBaseDir(systemId);
60
- const filePath = getFilePath(method);
61
- const fullPath = path_1.default.join(baseDir, normalizePath(path), filePath);
62
- return fs_extra_1.default.existsSync(fullPath);
56
+ const fullPath = resolveReadPath(systemId, method, path);
57
+ return !!fullPath && fs_extra_1.default.existsSync(fullPath);
63
58
  }
64
59
  exports.hasPageOnDisk = hasPageOnDisk;
65
60
  function getSystemBaseDir(systemId) {
@@ -90,7 +85,7 @@ function resolveReadPath(systemId, path, method) {
90
85
  }
91
86
  const parts = path.split(/\*/g);
92
87
  let currentPath = '';
93
- for (let part in parts) {
88
+ for (let part of parts) {
94
89
  const thisPath = path_1.default.join(currentPath, part);
95
90
  const starPath = path_1.default.join(currentPath, '*');
96
91
  const thisPathDir = path_1.default.join(baseDir, thisPath);
@@ -103,7 +98,6 @@ function resolveReadPath(systemId, path, method) {
103
98
  currentPath = starPath;
104
99
  continue;
105
100
  }
106
- console.log('Path not found', thisPathDir, starPathDir);
107
101
  // Path not found
108
102
  return null;
109
103
  }
@@ -74,24 +74,17 @@ router.post('/ui/screen', async (req, res) => {
74
74
  queue.cancel();
75
75
  });
76
76
  const promises = [];
77
- queue.on('page', (data) => {
78
- if (systemId) {
79
- promises.push(sendPageEvent(systemId, data, res));
80
- }
81
- });
82
- queue.on('image', async (screenData, prompt, future) => {
77
+ queue.on('page', (data) => (systemId ? sendPageEvent(systemId, data, res) : undefined));
78
+ queue.on('image', async (screenData, prompt) => {
83
79
  if (!systemId) {
84
80
  return;
85
81
  }
86
82
  try {
87
- const promise = handleImageEvent(systemId, screenData, prompt);
88
- promises.push(promise);
89
- await promise;
90
- future.resolve();
83
+ await handleImageEvent(systemId, screenData, prompt);
91
84
  }
92
85
  catch (e) {
93
86
  console.error('Failed to handle image event', e);
94
- future.reject(e);
87
+ throw e;
95
88
  }
96
89
  });
97
90
  await queue.addPrompt(aiRequest, conversationId, true);
@@ -186,19 +179,14 @@ router.post('/:handle/ui/iterative', async (req, res) => {
186
179
  onRequestAborted(req, res, () => {
187
180
  pageQueue.cancel();
188
181
  });
189
- pageQueue.on('page', (screenData) => {
190
- pageEventPromises.push(sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
191
- });
192
- pageQueue.on('image', async (screenData, prompt, future) => {
182
+ pageQueue.on('page', (screenData) => sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
183
+ pageQueue.on('image', async (screenData, prompt) => {
193
184
  try {
194
- const promise = handleImageEvent(landingPagesStream.getConversationId(), screenData, prompt);
195
- pageEventPromises.push(promise);
196
- await promise;
197
- future.resolve();
185
+ await handleImageEvent(landingPagesStream.getConversationId(), screenData, prompt);
198
186
  }
199
187
  catch (e) {
200
188
  console.error('Failed to handle image event', e);
201
- future.reject(e);
189
+ throw e;
202
190
  }
203
191
  });
204
192
  pageQueue.on('event', (screenData) => {
@@ -346,31 +334,25 @@ router.post('/:handle/ui', async (req, res) => {
346
334
  },
347
335
  created: Date.now(),
348
336
  });
349
- const pagePromises = [];
350
337
  onRequestAborted(req, res, () => {
351
338
  queue.cancel();
352
339
  });
353
- const pageEventPromises = [];
354
- queue.on('page', (screenData) => {
355
- pageEventPromises.push(sendPageEvent(outerConversationId, screenData, res));
356
- });
357
- queue.on('image', async (screenData, prompt, future) => {
340
+ queue.on('page', (screenData) => sendPageEvent(outerConversationId, screenData, res));
341
+ queue.on('image', async (screenData, prompt) => {
358
342
  try {
359
- const promise = handleImageEvent(outerConversationId, screenData, prompt);
360
- pageEventPromises.push(promise);
361
- await promise;
362
- future.resolve();
343
+ await handleImageEvent(outerConversationId, screenData, prompt);
363
344
  }
364
345
  catch (e) {
365
346
  console.error('Failed to handle image event', e);
366
- future.reject(e);
347
+ throw e;
367
348
  }
368
349
  });
369
350
  queue.on('event', (screenData) => {
370
351
  sendEvent(res, screenData);
371
352
  });
372
353
  for (const screen of Object.values(uniqueUserJourneyScreens)) {
373
- pagePromises.push(queue.addPrompt({
354
+ queue
355
+ .addPrompt({
374
356
  prompt: screen.requirements,
375
357
  method: screen.method,
376
358
  path: screen.path,
@@ -380,14 +362,15 @@ router.post('/:handle/ui', async (req, res) => {
380
362
  filename: screen.filename,
381
363
  storage_prefix: outerConversationId + '_',
382
364
  theme,
383
- }));
365
+ })
366
+ .catch((e) => {
367
+ console.error('Failed to generate page for screen %s', screen.name, e);
368
+ });
384
369
  }
385
370
  if (userJourneysStream.isAborted()) {
386
371
  return;
387
372
  }
388
373
  await queue.wait();
389
- await Promise.allSettled(pagePromises);
390
- await Promise.allSettled(pageEventPromises);
391
374
  sendDone(res);
392
375
  }
393
376
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.67.4",
3
+ "version": "0.67.5",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -7,7 +7,7 @@ import uuid from 'node-uuid';
7
7
  import { stormClient, UIPagePrompt } from './stormClient';
8
8
  import { ReferenceClassification, StormEvent, StormEventPage, StormImage, UIShell } from './events';
9
9
  import { EventEmitter } from 'node:events';
10
- import { createFuture, Future, FuturePromise, PromiseQueue } from './PromiseQueue';
10
+ import { PromiseQueue } from './PromiseQueue';
11
11
  import { hasPageOnDisk } from './page-utils';
12
12
 
13
13
  export interface ImagePrompt {
@@ -24,6 +24,7 @@ type InitialPrompt = Omit<UIPagePrompt, 'shell_page'> & { shellType?: 'public' |
24
24
 
25
25
  export class PageQueue extends EventEmitter {
26
26
  private readonly queue: PromiseQueue;
27
+ private readonly eventQueue: PromiseQueue;
27
28
  private readonly systemId: string;
28
29
  private readonly systemPrompt: string;
29
30
  private readonly references: Map<string, PageGenerator> = new Map();
@@ -37,19 +38,22 @@ export class PageQueue extends EventEmitter {
37
38
  this.systemId = systemId;
38
39
  this.systemPrompt = systemPrompt;
39
40
  this.queue = new PromiseQueue(concurrency);
41
+ this.eventQueue = new PromiseQueue(Number.MAX_VALUE);
40
42
  }
41
43
 
42
- on(event: 'event', listener: (data: StormEvent) => void): this;
43
- on(event: 'page', listener: (data: StormEventPage) => void): this;
44
- on(event: 'image', listener: (data: StormImage, source: ImagePrompt, future: FuturePromise<void>) => void): this;
44
+ on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
45
+ on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
46
+ on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
45
47
 
46
- on(event: string, listener: (...args: any[]) => void): this {
47
- return super.on(event, listener);
48
+ on(event: string, listener: (...args: any[]) => void | Promise<void>): this {
49
+ return super.on(event, (...args) => {
50
+ void this.eventQueue.add(async () => listener(...args));
51
+ });
48
52
  }
49
53
 
50
54
  emit(type: 'event', event: StormEvent): boolean;
51
55
  emit(type: 'page', event: StormEventPage): boolean;
52
- emit(type: 'image', event: StormImage, source: ImagePrompt, future: FuturePromise<void>): boolean;
56
+ emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
53
57
  emit(eventName: string | symbol, ...args: any[]): boolean {
54
58
  return super.emit(eventName, ...args);
55
59
  }
@@ -81,6 +85,7 @@ export class PageQueue extends EventEmitter {
81
85
  //console.log('Ignoring duplicate prompt', initialPrompt.path);
82
86
  return Promise.resolve();
83
87
  }
88
+ console.log('Generating page for', initialPrompt.method, initialPrompt.path);
84
89
 
85
90
  const prompt: UIPagePrompt = {
86
91
  ...initialPrompt,
@@ -104,7 +109,7 @@ export class PageQueue extends EventEmitter {
104
109
  }
105
110
 
106
111
  private wrapPagePrompt(pagePath: string, prompt: string): string {
107
- let promptPrefix = this.getPrefix();
112
+ const promptPrefix = this.getPrefix();
108
113
  let promptPostfix = '';
109
114
 
110
115
  if (this.pages.size > 0) {
@@ -129,10 +134,10 @@ export class PageQueue extends EventEmitter {
129
134
 
130
135
  private async addPageGenerator(generator: PageGenerator) {
131
136
  generator.on('event', (event: StormEvent) => this.emit('event', event));
132
- generator.on('page_refs', async ({ event, references, future }) => {
137
+ generator.on('page_refs', async ({ event, references }) => {
133
138
  try {
134
139
  const initialPrompts: InitialPrompt[] = [];
135
- let promises = references.map(async (reference) => {
140
+ const resourcePromises = references.map(async (reference) => {
136
141
  if (
137
142
  reference.url.startsWith('#') ||
138
143
  reference.url.startsWith('javascript:') ||
@@ -174,8 +179,12 @@ export class PageQueue extends EventEmitter {
174
179
  }
175
180
  });
176
181
 
177
- await Promise.allSettled(promises);
178
- initialPrompts.forEach((prompt) => {
182
+ // Wait for resources to be generated
183
+ await Promise.allSettled(resourcePromises);
184
+ this.emit('page', event);
185
+
186
+ // Emit any new pages after the current page to increase responsiveness
187
+ const newPages = initialPrompts.map((prompt) => {
179
188
  if (!this.hasPrompt(prompt.path)) {
180
189
  this.emit('page', {
181
190
  type: 'PAGE',
@@ -194,12 +203,12 @@ export class PageQueue extends EventEmitter {
194
203
  },
195
204
  });
196
205
  }
197
- this.addPrompt(prompt);
206
+ return this.addPrompt(prompt);
198
207
  });
199
-
200
- this.emit('page', event);
201
- } finally {
202
- future.resolve();
208
+ await Promise.allSettled(newPages);
209
+ } catch (e) {
210
+ console.error('Failed to process event', e);
211
+ throw e;
203
212
  }
204
213
  });
205
214
  return this.queue.add(() => generator.generate());
@@ -207,10 +216,12 @@ export class PageQueue extends EventEmitter {
207
216
 
208
217
  public cancel() {
209
218
  this.queue.cancel();
219
+ this.eventQueue.cancel();
210
220
  }
211
221
 
212
- public wait() {
213
- return this.queue.wait();
222
+ public async wait() {
223
+ await this.eventQueue.wait();
224
+ await this.queue.wait();
214
225
  }
215
226
 
216
227
  private async addImagePrompt(prompt: ImagePrompt) {
@@ -225,29 +236,18 @@ export class PageQueue extends EventEmitter {
225
236
  );
226
237
 
227
238
  //console.log('Adding image prompt', prompt);
228
-
229
- const futures: FuturePromise<void>[] = [];
230
-
231
- result.on('data', async (event: StormEvent) => {
239
+ result.on('data', (event: StormEvent) => {
232
240
  if (event.type === 'IMAGE') {
233
- const future = createFuture();
234
- futures.push(future);
235
- this.emit('image', event, prompt, future);
236
- setTimeout(() => {
237
- if (!future.isResolved()) {
238
- console.log('Image prompt timed out', prompt);
239
- future.reject(new Error('Image prompt timed out'));
240
- }
241
- }, 30000);
241
+ this.emit('image', event, prompt);
242
242
  }
243
243
  });
244
244
 
245
245
  await result.waitForDone();
246
- await Promise.allSettled(futures.map((f) => f.promise));
247
246
  }
248
247
  }
249
248
 
250
249
  export class PageGenerator extends EventEmitter {
250
+ private readonly eventQueue: PromiseQueue;
251
251
  private readonly conversationId: string;
252
252
  public readonly prompt: UIPagePrompt;
253
253
 
@@ -255,64 +255,54 @@ export class PageGenerator extends EventEmitter {
255
255
  super();
256
256
  this.conversationId = conversationId;
257
257
  this.prompt = prompt;
258
+ this.eventQueue = new PromiseQueue(Number.MAX_VALUE);
258
259
  }
259
260
 
260
261
  on(event: 'event', listener: (data: StormEvent) => void): this;
261
262
  on(
262
263
  event: 'page_refs',
263
- listener: (data: {
264
- event: StormEventPage;
265
- references: ReferenceClassification[];
266
- future: FuturePromise<void>;
267
- }) => void
264
+ listener: (data: { event: StormEventPage; references: ReferenceClassification[] }) => Promise<void>
268
265
  ): this;
269
266
 
270
267
  on(event: string, listener: (...args: any[]) => void): this {
271
- return super.on(event, listener);
268
+ return super.on(event, (...args) => {
269
+ void this.eventQueue.add(async () => listener(...args));
270
+ });
272
271
  }
273
272
 
274
273
  emit(type: 'event', event: StormEvent): boolean;
275
- emit(
276
- type: 'page_refs',
277
- event: { event: StormEventPage; references: ReferenceClassification[]; future: FuturePromise<void> }
278
- ): boolean;
274
+ emit(type: 'page_refs', event: { event: StormEventPage; references: ReferenceClassification[] }): boolean;
279
275
  emit(eventName: string | symbol, ...args: any[]): boolean {
280
276
  return super.emit(eventName, ...args);
281
277
  }
282
278
 
283
279
  public async generate() {
284
- return new Promise<void>(async (resolve) => {
285
- const promises: Promise<void>[] = [];
286
- const screenStream = await stormClient.createUIPage(this.prompt, this.conversationId);
287
-
288
- screenStream.on('data', (event: StormEvent) => {
289
- if (event.type === 'PAGE') {
290
- event.payload.conversationId = this.conversationId;
291
-
292
- promises.push(
293
- (async () => {
294
- const references = await this.resolveReferences(event.payload.content);
295
- //console.log('Resolved references for page', references, event.payload);
296
- const future = createFuture();
297
- this.emit('page_refs', {
298
- event,
299
- references,
300
- future,
301
- });
302
-
303
- await future.promise;
304
- })()
305
- );
306
- return;
307
- }
308
-
309
- this.emit('event', event);
310
- });
280
+ const promises: Promise<void>[] = [];
281
+ const screenStream = await stormClient.createUIPage(this.prompt, this.conversationId);
282
+
283
+ screenStream.on('data', (event: StormEvent) => {
284
+ if (event.type === 'PAGE') {
285
+ event.payload.conversationId = this.conversationId;
286
+
287
+ promises.push(
288
+ (async () => {
289
+ const references = await this.resolveReferences(event.payload.content);
290
+ //console.log('Resolved references for page', references, event.payload);
291
+ this.emit('page_refs', {
292
+ event,
293
+ references,
294
+ });
295
+ })()
296
+ );
297
+ return;
298
+ }
311
299
 
312
- await screenStream.waitForDone();
313
- await Promise.all(promises);
314
- resolve();
300
+ this.emit('event', event);
315
301
  });
302
+
303
+ await screenStream.waitForDone();
304
+ await Promise.all(promises);
305
+ await this.eventQueue.wait();
316
306
  }
317
307
 
318
308
  private async resolveReferences(content: string) {
@@ -19,7 +19,7 @@ function normalizePath(path: string) {
19
19
  return path
20
20
  .replace(/\?.*$/gi, '')
21
21
  .replace(/:[a-z][a-z_]*\b/gi, '*')
22
- .replace(/\{[a-z]+}/gi, '*');
22
+ .replace(/\{[a-z-.]+}/gi, '*');
23
23
  }
24
24
 
25
25
  export async function writePageToDisk(systemId: string, event: StormEventPage) {
@@ -69,13 +69,8 @@ export async function writeImageToDisk(systemId: string, event: StormImage, prom
69
69
  }
70
70
 
71
71
  export function hasPageOnDisk(systemId: string, method: string, path: string) {
72
- if (!systemId || !method || !path) {
73
- return false;
74
- }
75
- const baseDir = getSystemBaseDir(systemId);
76
- const filePath = getFilePath(method);
77
- const fullPath = Path.join(baseDir, normalizePath(path), filePath);
78
- return FS.existsSync(fullPath);
72
+ const fullPath = resolveReadPath(systemId, method, path);
73
+ return !!fullPath && FS.existsSync(fullPath);
79
74
  }
80
75
 
81
76
  export function getSystemBaseDir(systemId: string) {
@@ -114,7 +109,7 @@ export function resolveReadPath(systemId: string, path: string, method: string)
114
109
 
115
110
  let currentPath = '';
116
111
 
117
- for (let part in parts) {
112
+ for (let part of parts) {
118
113
  const thisPath = Path.join(currentPath, part);
119
114
  const starPath = Path.join(currentPath, '*');
120
115
  const thisPathDir = Path.join(baseDir, thisPath);
@@ -130,7 +125,6 @@ export function resolveReadPath(systemId: string, path: string, method: string)
130
125
  continue;
131
126
  }
132
127
 
133
- console.log('Path not found', thisPathDir, starPathDir);
134
128
  // Path not found
135
129
  return null;
136
130
  }
@@ -102,24 +102,17 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
102
102
 
103
103
  const promises: Promise<void>[] = [];
104
104
 
105
- queue.on('page', (data) => {
106
- if (systemId) {
107
- promises.push(sendPageEvent(systemId, data, res));
108
- }
109
- });
105
+ queue.on('page', (data) => (systemId ? sendPageEvent(systemId, data, res) : undefined));
110
106
 
111
- queue.on('image', async (screenData, prompt, future) => {
107
+ queue.on('image', async (screenData, prompt) => {
112
108
  if (!systemId) {
113
109
  return;
114
110
  }
115
111
  try {
116
- const promise = handleImageEvent(systemId, screenData, prompt);
117
- promises.push(promise);
118
- await promise;
119
- future.resolve();
112
+ await handleImageEvent(systemId, screenData, prompt);
120
113
  } catch (e) {
121
114
  console.error('Failed to handle image event', e);
122
- future.reject(e);
115
+ throw e;
123
116
  }
124
117
  });
125
118
 
@@ -229,19 +222,16 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
229
222
  pageQueue.cancel();
230
223
  });
231
224
 
232
- pageQueue.on('page', (screenData: StormEventPage) => {
233
- pageEventPromises.push(sendPageEvent(landingPagesStream.getConversationId(), screenData, res));
234
- });
225
+ pageQueue.on('page', (screenData: StormEventPage) =>
226
+ sendPageEvent(landingPagesStream.getConversationId(), screenData, res)
227
+ );
235
228
 
236
- pageQueue.on('image', async (screenData, prompt, future) => {
229
+ pageQueue.on('image', async (screenData, prompt) => {
237
230
  try {
238
- const promise = handleImageEvent(landingPagesStream.getConversationId(), screenData, prompt);
239
- pageEventPromises.push(promise);
240
- await promise;
241
- future.resolve();
231
+ await handleImageEvent(landingPagesStream.getConversationId(), screenData, prompt);
242
232
  } catch (e) {
243
233
  console.error('Failed to handle image event', e);
244
- future.reject(e);
234
+ throw e;
245
235
  }
246
236
  });
247
237
 
@@ -420,25 +410,18 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
420
410
  created: Date.now(),
421
411
  });
422
412
 
423
- const pagePromises: Promise<void>[] = [];
424
413
  onRequestAborted(req, res, () => {
425
414
  queue.cancel();
426
415
  });
427
416
 
428
- const pageEventPromises: Promise<void>[] = [];
429
- queue.on('page', (screenData: StormEventPage) => {
430
- pageEventPromises.push(sendPageEvent(outerConversationId, screenData, res));
431
- });
417
+ queue.on('page', (screenData: StormEventPage) => sendPageEvent(outerConversationId, screenData, res));
432
418
 
433
- queue.on('image', async (screenData, prompt, future) => {
419
+ queue.on('image', async (screenData, prompt) => {
434
420
  try {
435
- const promise = handleImageEvent(outerConversationId, screenData, prompt);
436
- pageEventPromises.push(promise);
437
- await promise;
438
- future.resolve();
421
+ await handleImageEvent(outerConversationId, screenData, prompt);
439
422
  } catch (e) {
440
423
  console.error('Failed to handle image event', e);
441
- future.reject(e);
424
+ throw e;
442
425
  }
443
426
  });
444
427
 
@@ -447,8 +430,8 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
447
430
  });
448
431
 
449
432
  for (const screen of Object.values(uniqueUserJourneyScreens)) {
450
- pagePromises.push(
451
- queue.addPrompt({
433
+ queue
434
+ .addPrompt({
452
435
  prompt: screen.requirements,
453
436
  method: screen.method,
454
437
  path: screen.path,
@@ -459,7 +442,9 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
459
442
  storage_prefix: outerConversationId + '_',
460
443
  theme,
461
444
  })
462
- );
445
+ .catch((e) => {
446
+ console.error('Failed to generate page for screen %s', screen.name, e);
447
+ });
463
448
  }
464
449
 
465
450
  if (userJourneysStream.isAborted()) {
@@ -467,8 +452,6 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
467
452
  }
468
453
 
469
454
  await queue.wait();
470
- await Promise.allSettled(pagePromises);
471
- await Promise.allSettled(pageEventPromises);
472
455
 
473
456
  sendDone(res);
474
457
  } catch (err) {