@kapeta/local-cluster-service 0.70.4 → 0.70.6

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,19 @@
1
+ ## [0.70.6](https://github.com/kapetacom/local-cluster-service/compare/v0.70.5...v0.70.6) (2024-09-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * disable sentry in dev mode ([9a7793f](https://github.com/kapetacom/local-cluster-service/commit/9a7793feaf81239d600d2b6650f126780f0ffdd5))
7
+ * emit errors to res instead of into void ([d1302c1](https://github.com/kapetacom/local-cluster-service/commit/d1302c1b523a72de6596b619a44a2cde073f032f))
8
+ * promise error handling and fetch retries ([d746e14](https://github.com/kapetacom/local-cluster-service/commit/d746e14c1edaaddd1a3aa5cb7bbf644dfde334b2))
9
+
10
+ ## [0.70.5](https://github.com/kapetacom/local-cluster-service/compare/v0.70.4...v0.70.5) (2024-09-11)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * strip hash of URLs for refs ([#242](https://github.com/kapetacom/local-cluster-service/issues/242)) ([681a3fd](https://github.com/kapetacom/local-cluster-service/commit/681a3fdcfd837eb4d0d3411ee719e2dc68334522))
16
+
1
17
  ## [0.70.4](https://github.com/kapetacom/local-cluster-service/compare/v0.70.3...v0.70.4) (2024-09-10)
2
18
 
3
19
 
package/dist/cjs/index.js CHANGED
@@ -64,7 +64,7 @@ const assetManager_1 = require("./src/assetManager");
64
64
  const instanceManager_1 = require("./src/instanceManager");
65
65
  Sentry.init({
66
66
  dsn: 'https://0b7cc946d82c591473d6f95fff5e210b@o4505820837249024.ingest.sentry.io/4506212692000768',
67
- enabled: process.env.NODE_ENV !== 'development',
67
+ enabled: !!process.env.NODE_ENV && process.env.NODE_ENV !== 'development',
68
68
  // Performance Monitoring on every ~20th request
69
69
  tracesSampleRate: 0.05,
70
70
  // Set sampling rate for profiling - this is relative to tracesSampleRate
@@ -29,9 +29,11 @@ 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: 'error', listener: (error: unknown) => void): this;
32
33
  on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
33
34
  on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
34
35
  on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
36
+ emit(type: 'error', error: unknown): boolean;
35
37
  emit(type: 'event', event: StormEvent): boolean;
36
38
  emit(type: 'page', event: StormEventPage): boolean;
37
39
  emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
@@ -32,7 +32,12 @@ class PageQueue extends node_events_1.EventEmitter {
32
32
  }
33
33
  on(event, listener) {
34
34
  return super.on(event, (...args) => {
35
- void this.eventQueue.add(async () => listener(...args));
35
+ this.eventQueue
36
+ .add(async () => listener(...args))
37
+ // If the event queue fails, we want to emit an error
38
+ .catch((err) => {
39
+ this.emit('error', err);
40
+ });
36
41
  });
37
42
  }
38
43
  emit(eventName, ...args) {
@@ -121,6 +126,9 @@ class PageQueue extends node_events_1.EventEmitter {
121
126
  await this.addImagePrompt({
122
127
  ...reference,
123
128
  content: event.payload.content,
129
+ }).catch((err) => {
130
+ console.error('Failed to generate image for reference', reference.name, err);
131
+ this.emit('error', err);
124
132
  });
125
133
  break;
126
134
  case 'css':
@@ -154,7 +162,7 @@ class PageQueue extends node_events_1.EventEmitter {
154
162
  await Promise.allSettled(resourcePromises);
155
163
  this.emit('page', event);
156
164
  // Emit any new pages after the current page to increase responsiveness
157
- void initialPrompts.map((prompt) => {
165
+ initialPrompts.forEach((prompt) => {
158
166
  if (!this.hasPrompt(prompt.path)) {
159
167
  this.emit('page', {
160
168
  type: 'PAGE',
@@ -173,7 +181,11 @@ class PageQueue extends node_events_1.EventEmitter {
173
181
  },
174
182
  });
175
183
  }
176
- return this.addPrompt(prompt);
184
+ // Trigger but don't wait for the "bonus" pages
185
+ this.addPrompt(prompt).catch((err) => {
186
+ console.error('Failed to generate page reference', prompt.name, err);
187
+ this.emit('error', err);
188
+ });
177
189
  });
178
190
  }
179
191
  catch (e) {
@@ -208,18 +220,21 @@ class PageQueue extends node_events_1.EventEmitter {
208
220
  await result.waitForDone();
209
221
  }
210
222
  async generate(prompt, conversationId) {
211
- const promises = [];
212
223
  const screenStream = await stormClient_1.stormClient.createUIPage(prompt, conversationId);
224
+ let pageEvent = null;
213
225
  screenStream.on('data', (event) => {
214
226
  if (event.type === 'PAGE') {
215
227
  event.payload.conversationId = conversationId;
216
- promises.push(this.processPageEventWithReferences(event));
228
+ pageEvent = event;
217
229
  return;
218
230
  }
219
231
  this.emit('event', event);
220
232
  });
221
233
  await screenStream.waitForDone();
222
- await Promise.all(promises);
234
+ if (!pageEvent) {
235
+ throw new Error('No page was generated');
236
+ }
237
+ await this.processPageEventWithReferences(pageEvent);
223
238
  }
224
239
  async resolveReferences(content) {
225
240
  const referenceStream = await stormClient_1.stormClient.classifyUIReferences(content);
@@ -10,9 +10,10 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
10
10
  exports.SystemIdHeader = 'System-Id';
11
11
  function normalizePath(path) {
12
12
  return path
13
- .replace(/\?.*$/gi, '')
14
- .replace(/:[a-z][a-z_]*\b/gi, '*')
15
- .replace(/\{[a-z-.]+}/gi, '*');
13
+ .replace(/#.*$/g, '') // Remove hash
14
+ .replace(/\?.*$/gi, '') // Remove query params
15
+ .replace(/:[a-z][a-z_]*\b/gi, '*') // Replace all params with *
16
+ .replace(/\{[a-z-.]+}/gi, '*'); // Replace all variables with *
16
17
  }
17
18
  exports.normalizePath = normalizePath;
18
19
  async function writePageToDisk(systemId, event) {
@@ -124,6 +124,10 @@ router.post('/ui/screen', async (req, res) => {
124
124
  throw e;
125
125
  }
126
126
  });
127
+ queue.on('error', (err) => {
128
+ console.error('Failed to process page', err);
129
+ sendError(err, res);
130
+ });
127
131
  await queue.addPrompt(aiRequest, conversationId, true);
128
132
  await queue.wait();
129
133
  await Promise.allSettled(promises);
@@ -214,6 +218,10 @@ router.post('/:handle/ui/iterative', async (req, res) => {
214
218
  pageQueue.on('event', (screenData) => {
215
219
  sendEvent(res, screenData);
216
220
  });
221
+ pageQueue.on('error', (err) => {
222
+ console.error('Failed to process page', err);
223
+ sendError(err, res);
224
+ });
217
225
  await waitForStormStream(landingPagesStream);
218
226
  await pageQueue.wait();
219
227
  await Promise.allSettled(pageEventPromises);
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { ConversationItem, ImplementAPIClientsRequest, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
3
2
  import { Page, StormEventPageUrl } from './events';
4
3
  export declare const STORM_ID = "storm";
@@ -66,7 +65,7 @@ declare class StormClient {
66
65
  createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
67
66
  createUILandingPages(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
68
67
  createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
69
- voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<Response>;
68
+ voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<import("undici").Response>;
70
69
  getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string): Promise<{
71
70
  vote: -1 | 0 | 1;
72
71
  }>;
@@ -13,6 +13,22 @@ const utils_1 = require("../utils/utils");
13
13
  const promises_1 = __importDefault(require("node:readline/promises"));
14
14
  const node_stream_1 = require("node:stream");
15
15
  const stream_1 = require("./stream");
16
+ const undici_1 = require("undici");
17
+ // Will only retry on error codes and GET requests by default
18
+ // See https://github.com/nodejs/undici/blob/990df2c7e37cbe5bb44fe2f576dddeaeb5916590/docs/docs/api/RetryAgent.md
19
+ const retryAgent = new undici_1.RetryAgent(new undici_1.Agent(), {
20
+ methods: [
21
+ // Added methods ↓ (not idempotent), but we want to retry on POST:
22
+ 'POST',
23
+ // defaults below
24
+ 'GET',
25
+ 'HEAD',
26
+ 'OPTIONS',
27
+ 'PUT',
28
+ 'DELETE',
29
+ 'TRACE',
30
+ ],
31
+ });
16
32
  exports.STORM_ID = 'storm';
17
33
  exports.ConversationIdHeader = 'Conversation-Id';
18
34
  class StormClient {
@@ -38,6 +54,7 @@ class StormClient {
38
54
  method: method,
39
55
  body: JSON.stringify(body),
40
56
  headers,
57
+ dispatcher: retryAgent,
41
58
  };
42
59
  }
43
60
  async send(path, body, method = 'POST') {
@@ -48,7 +65,7 @@ class StormClient {
48
65
  });
49
66
  const abort = new AbortController();
50
67
  options.signal = abort.signal;
51
- const response = await fetch(options.url, options);
68
+ const response = await (0, undici_1.fetch)(options.url, options);
52
69
  if (response.status !== 200) {
53
70
  throw new Error(`Got error response from ${options.url}: ${response.status}\nContent: ${await response.text()}`);
54
71
  }
@@ -121,25 +138,19 @@ class StormClient {
121
138
  prompt: JSON.stringify({ topic, vote, mainConversationId }),
122
139
  conversationId,
123
140
  });
124
- return fetch(options.url, {
125
- method: options.method,
126
- headers: options.headers,
127
- });
141
+ return (0, undici_1.fetch)(options.url, options);
128
142
  }
129
143
  async getVoteUIPage(topic, conversationId, mainConversationId) {
130
144
  const options = await this.createOptions('/v2/ui/get-vote', 'POST', {
131
145
  prompt: JSON.stringify({ topic, mainConversationId }),
132
146
  conversationId,
133
147
  });
134
- const response = await fetch(options.url, {
135
- method: options.method,
136
- headers: options.headers,
137
- });
148
+ const response = await (0, undici_1.fetch)(options.url, options);
138
149
  return response.json();
139
150
  }
140
151
  async implementAPIClients(prompt) {
141
152
  const u = `${this._baseUrl}/v2/ui/implement-api-clients`;
142
- const response = await fetch(u, {
153
+ const response = await (0, undici_1.fetch)(u, {
143
154
  method: 'POST',
144
155
  body: JSON.stringify({
145
156
  fileName: prompt.fileName,
@@ -214,10 +225,7 @@ class StormClient {
214
225
  prompt: '',
215
226
  conversationId: conversationId,
216
227
  });
217
- const response = await fetch(options.url, {
218
- method: options.method,
219
- headers: options.headers,
220
- });
228
+ const response = await (0, undici_1.fetch)(options.url, options);
221
229
  return response.text();
222
230
  }
223
231
  }
package/dist/esm/index.js CHANGED
@@ -64,7 +64,7 @@ const assetManager_1 = require("./src/assetManager");
64
64
  const instanceManager_1 = require("./src/instanceManager");
65
65
  Sentry.init({
66
66
  dsn: 'https://0b7cc946d82c591473d6f95fff5e210b@o4505820837249024.ingest.sentry.io/4506212692000768',
67
- enabled: process.env.NODE_ENV !== 'development',
67
+ enabled: !!process.env.NODE_ENV && process.env.NODE_ENV !== 'development',
68
68
  // Performance Monitoring on every ~20th request
69
69
  tracesSampleRate: 0.05,
70
70
  // Set sampling rate for profiling - this is relative to tracesSampleRate
@@ -29,9 +29,11 @@ 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: 'error', listener: (error: unknown) => void): this;
32
33
  on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
33
34
  on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
34
35
  on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
36
+ emit(type: 'error', error: unknown): boolean;
35
37
  emit(type: 'event', event: StormEvent): boolean;
36
38
  emit(type: 'page', event: StormEventPage): boolean;
37
39
  emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
@@ -32,7 +32,12 @@ class PageQueue extends node_events_1.EventEmitter {
32
32
  }
33
33
  on(event, listener) {
34
34
  return super.on(event, (...args) => {
35
- void this.eventQueue.add(async () => listener(...args));
35
+ this.eventQueue
36
+ .add(async () => listener(...args))
37
+ // If the event queue fails, we want to emit an error
38
+ .catch((err) => {
39
+ this.emit('error', err);
40
+ });
36
41
  });
37
42
  }
38
43
  emit(eventName, ...args) {
@@ -121,6 +126,9 @@ class PageQueue extends node_events_1.EventEmitter {
121
126
  await this.addImagePrompt({
122
127
  ...reference,
123
128
  content: event.payload.content,
129
+ }).catch((err) => {
130
+ console.error('Failed to generate image for reference', reference.name, err);
131
+ this.emit('error', err);
124
132
  });
125
133
  break;
126
134
  case 'css':
@@ -154,7 +162,7 @@ class PageQueue extends node_events_1.EventEmitter {
154
162
  await Promise.allSettled(resourcePromises);
155
163
  this.emit('page', event);
156
164
  // Emit any new pages after the current page to increase responsiveness
157
- void initialPrompts.map((prompt) => {
165
+ initialPrompts.forEach((prompt) => {
158
166
  if (!this.hasPrompt(prompt.path)) {
159
167
  this.emit('page', {
160
168
  type: 'PAGE',
@@ -173,7 +181,11 @@ class PageQueue extends node_events_1.EventEmitter {
173
181
  },
174
182
  });
175
183
  }
176
- return this.addPrompt(prompt);
184
+ // Trigger but don't wait for the "bonus" pages
185
+ this.addPrompt(prompt).catch((err) => {
186
+ console.error('Failed to generate page reference', prompt.name, err);
187
+ this.emit('error', err);
188
+ });
177
189
  });
178
190
  }
179
191
  catch (e) {
@@ -208,18 +220,21 @@ class PageQueue extends node_events_1.EventEmitter {
208
220
  await result.waitForDone();
209
221
  }
210
222
  async generate(prompt, conversationId) {
211
- const promises = [];
212
223
  const screenStream = await stormClient_1.stormClient.createUIPage(prompt, conversationId);
224
+ let pageEvent = null;
213
225
  screenStream.on('data', (event) => {
214
226
  if (event.type === 'PAGE') {
215
227
  event.payload.conversationId = conversationId;
216
- promises.push(this.processPageEventWithReferences(event));
228
+ pageEvent = event;
217
229
  return;
218
230
  }
219
231
  this.emit('event', event);
220
232
  });
221
233
  await screenStream.waitForDone();
222
- await Promise.all(promises);
234
+ if (!pageEvent) {
235
+ throw new Error('No page was generated');
236
+ }
237
+ await this.processPageEventWithReferences(pageEvent);
223
238
  }
224
239
  async resolveReferences(content) {
225
240
  const referenceStream = await stormClient_1.stormClient.classifyUIReferences(content);
@@ -10,9 +10,10 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
10
10
  exports.SystemIdHeader = 'System-Id';
11
11
  function normalizePath(path) {
12
12
  return path
13
- .replace(/\?.*$/gi, '')
14
- .replace(/:[a-z][a-z_]*\b/gi, '*')
15
- .replace(/\{[a-z-.]+}/gi, '*');
13
+ .replace(/#.*$/g, '') // Remove hash
14
+ .replace(/\?.*$/gi, '') // Remove query params
15
+ .replace(/:[a-z][a-z_]*\b/gi, '*') // Replace all params with *
16
+ .replace(/\{[a-z-.]+}/gi, '*'); // Replace all variables with *
16
17
  }
17
18
  exports.normalizePath = normalizePath;
18
19
  async function writePageToDisk(systemId, event) {
@@ -124,6 +124,10 @@ router.post('/ui/screen', async (req, res) => {
124
124
  throw e;
125
125
  }
126
126
  });
127
+ queue.on('error', (err) => {
128
+ console.error('Failed to process page', err);
129
+ sendError(err, res);
130
+ });
127
131
  await queue.addPrompt(aiRequest, conversationId, true);
128
132
  await queue.wait();
129
133
  await Promise.allSettled(promises);
@@ -214,6 +218,10 @@ router.post('/:handle/ui/iterative', async (req, res) => {
214
218
  pageQueue.on('event', (screenData) => {
215
219
  sendEvent(res, screenData);
216
220
  });
221
+ pageQueue.on('error', (err) => {
222
+ console.error('Failed to process page', err);
223
+ sendError(err, res);
224
+ });
217
225
  await waitForStormStream(landingPagesStream);
218
226
  await pageQueue.wait();
219
227
  await Promise.allSettled(pageEventPromises);
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { ConversationItem, ImplementAPIClientsRequest, StormFileImplementationPrompt, StormStream, StormUIImplementationPrompt, StormUIListPrompt } from './stream';
3
2
  import { Page, StormEventPageUrl } from './events';
4
3
  export declare const STORM_ID = "storm";
@@ -66,7 +65,7 @@ declare class StormClient {
66
65
  createUIShells(prompt: UIShellsPrompt, conversationId?: string): Promise<StormStream>;
67
66
  createUILandingPages(prompt: BasePromptRequest, conversationId?: string): Promise<StormStream>;
68
67
  createUIPage(prompt: UIPagePrompt, conversationId?: string, history?: ConversationItem[]): Promise<StormStream>;
69
- voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<Response>;
68
+ voteUIPage(topic: string, conversationId: string, vote: -1 | 0 | 1, mainConversationId?: string): Promise<import("undici").Response>;
70
69
  getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string): Promise<{
71
70
  vote: -1 | 0 | 1;
72
71
  }>;
@@ -13,6 +13,22 @@ const utils_1 = require("../utils/utils");
13
13
  const promises_1 = __importDefault(require("node:readline/promises"));
14
14
  const node_stream_1 = require("node:stream");
15
15
  const stream_1 = require("./stream");
16
+ const undici_1 = require("undici");
17
+ // Will only retry on error codes and GET requests by default
18
+ // See https://github.com/nodejs/undici/blob/990df2c7e37cbe5bb44fe2f576dddeaeb5916590/docs/docs/api/RetryAgent.md
19
+ const retryAgent = new undici_1.RetryAgent(new undici_1.Agent(), {
20
+ methods: [
21
+ // Added methods ↓ (not idempotent), but we want to retry on POST:
22
+ 'POST',
23
+ // defaults below
24
+ 'GET',
25
+ 'HEAD',
26
+ 'OPTIONS',
27
+ 'PUT',
28
+ 'DELETE',
29
+ 'TRACE',
30
+ ],
31
+ });
16
32
  exports.STORM_ID = 'storm';
17
33
  exports.ConversationIdHeader = 'Conversation-Id';
18
34
  class StormClient {
@@ -38,6 +54,7 @@ class StormClient {
38
54
  method: method,
39
55
  body: JSON.stringify(body),
40
56
  headers,
57
+ dispatcher: retryAgent,
41
58
  };
42
59
  }
43
60
  async send(path, body, method = 'POST') {
@@ -48,7 +65,7 @@ class StormClient {
48
65
  });
49
66
  const abort = new AbortController();
50
67
  options.signal = abort.signal;
51
- const response = await fetch(options.url, options);
68
+ const response = await (0, undici_1.fetch)(options.url, options);
52
69
  if (response.status !== 200) {
53
70
  throw new Error(`Got error response from ${options.url}: ${response.status}\nContent: ${await response.text()}`);
54
71
  }
@@ -121,25 +138,19 @@ class StormClient {
121
138
  prompt: JSON.stringify({ topic, vote, mainConversationId }),
122
139
  conversationId,
123
140
  });
124
- return fetch(options.url, {
125
- method: options.method,
126
- headers: options.headers,
127
- });
141
+ return (0, undici_1.fetch)(options.url, options);
128
142
  }
129
143
  async getVoteUIPage(topic, conversationId, mainConversationId) {
130
144
  const options = await this.createOptions('/v2/ui/get-vote', 'POST', {
131
145
  prompt: JSON.stringify({ topic, mainConversationId }),
132
146
  conversationId,
133
147
  });
134
- const response = await fetch(options.url, {
135
- method: options.method,
136
- headers: options.headers,
137
- });
148
+ const response = await (0, undici_1.fetch)(options.url, options);
138
149
  return response.json();
139
150
  }
140
151
  async implementAPIClients(prompt) {
141
152
  const u = `${this._baseUrl}/v2/ui/implement-api-clients`;
142
- const response = await fetch(u, {
153
+ const response = await (0, undici_1.fetch)(u, {
143
154
  method: 'POST',
144
155
  body: JSON.stringify({
145
156
  fileName: prompt.fileName,
@@ -214,10 +225,7 @@ class StormClient {
214
225
  prompt: '',
215
226
  conversationId: conversationId,
216
227
  });
217
- const response = await fetch(options.url, {
218
- method: options.method,
219
- headers: options.headers,
220
- });
228
+ const response = await (0, undici_1.fetch)(options.url, options);
221
229
  return response.text();
222
230
  }
223
231
  }
package/index.ts CHANGED
@@ -39,7 +39,7 @@ import { instanceManager } from './src/instanceManager';
39
39
 
40
40
  Sentry.init({
41
41
  dsn: 'https://0b7cc946d82c591473d6f95fff5e210b@o4505820837249024.ingest.sentry.io/4506212692000768',
42
- enabled: process.env.NODE_ENV !== 'development',
42
+ enabled: !!process.env.NODE_ENV && process.env.NODE_ENV !== 'development',
43
43
  // Performance Monitoring on every ~20th request
44
44
  tracesSampleRate: 0.05,
45
45
  // Set sampling rate for profiling - this is relative to tracesSampleRate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.70.4",
3
+ "version": "0.70.6",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -87,6 +87,7 @@
87
87
  "stream-json": "^1.8.0",
88
88
  "tar-stream": "^3.1.6",
89
89
  "typescript": "^5.1.6",
90
+ "undici": "^6.19.8",
90
91
  "uuid": "^9.0.1",
91
92
  "yaml": "^1.6.0"
92
93
  },
@@ -42,16 +42,23 @@ export class PageQueue extends EventEmitter {
42
42
  this.eventQueue = new PQueue({ concurrency: Number.MAX_VALUE });
43
43
  }
44
44
 
45
+ on(event: 'error', listener: (error: unknown) => void): this;
45
46
  on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
46
47
  on(event: 'page', listener: (data: StormEventPage) => void | Promise<void>): this;
47
48
  on(event: 'image', listener: (data: StormImage, source: ImagePrompt) => void | Promise<void>): this;
48
49
 
49
50
  on(event: string, listener: (...args: any[]) => void | Promise<void>): this {
50
51
  return super.on(event, (...args) => {
51
- void this.eventQueue.add(async () => listener(...args));
52
+ this.eventQueue
53
+ .add(async () => listener(...args))
54
+ // If the event queue fails, we want to emit an error
55
+ .catch((err) => {
56
+ this.emit('error', err);
57
+ });
52
58
  });
53
59
  }
54
60
 
61
+ emit(type: 'error', error: unknown): boolean;
55
62
  emit(type: 'event', event: StormEvent): boolean;
56
63
  emit(type: 'page', event: StormEventPage): boolean;
57
64
  emit(type: 'image', event: StormImage, source: ImagePrompt): boolean;
@@ -160,6 +167,9 @@ export class PageQueue extends EventEmitter {
160
167
  await this.addImagePrompt({
161
168
  ...reference,
162
169
  content: event.payload.content,
170
+ }).catch((err) => {
171
+ console.error('Failed to generate image for reference', reference.name, err);
172
+ this.emit('error', err);
163
173
  });
164
174
  break;
165
175
  case 'css':
@@ -197,7 +207,7 @@ export class PageQueue extends EventEmitter {
197
207
  this.emit('page', event);
198
208
 
199
209
  // Emit any new pages after the current page to increase responsiveness
200
- void initialPrompts.map((prompt) => {
210
+ initialPrompts.forEach((prompt) => {
201
211
  if (!this.hasPrompt(prompt.path)) {
202
212
  this.emit('page', {
203
213
  type: 'PAGE',
@@ -216,7 +226,11 @@ export class PageQueue extends EventEmitter {
216
226
  },
217
227
  });
218
228
  }
219
- return this.addPrompt(prompt);
229
+ // Trigger but don't wait for the "bonus" pages
230
+ this.addPrompt(prompt).catch((err) => {
231
+ console.error('Failed to generate page reference', prompt.name, err);
232
+ this.emit('error', err);
233
+ });
220
234
  });
221
235
  } catch (e) {
222
236
  console.error('Failed to process event', e);
@@ -258,25 +272,25 @@ export class PageQueue extends EventEmitter {
258
272
  }
259
273
 
260
274
  public async generate(prompt: UIPagePrompt, conversationId: string) {
261
- const promises: Promise<void>[] = [];
262
275
  const screenStream = await stormClient.createUIPage(prompt, conversationId);
263
-
276
+ let pageEvent: StormEventPage | null = null;
264
277
  screenStream.on('data', (event: StormEvent) => {
265
278
  if (event.type === 'PAGE') {
266
279
  event.payload.conversationId = conversationId;
267
-
268
- promises.push(this.processPageEventWithReferences(event));
280
+ pageEvent = event;
269
281
  return;
270
282
  }
271
-
272
283
  this.emit('event', event);
273
284
  });
274
285
 
275
286
  await screenStream.waitForDone();
276
- await Promise.all(promises);
287
+ if (!pageEvent) {
288
+ throw new Error('No page was generated');
289
+ }
290
+ await this.processPageEventWithReferences(pageEvent);
277
291
  }
278
292
 
279
- private async resolveReferences(content: string) {
293
+ private async resolveReferences(content: string): Promise<ReferenceClassification[]> {
280
294
  const referenceStream = await stormClient.classifyUIReferences(content);
281
295
 
282
296
  const references: ReferenceClassification[] = [];
@@ -17,9 +17,10 @@ export const SystemIdHeader = 'System-Id';
17
17
 
18
18
  export function normalizePath(path: string) {
19
19
  return path
20
- .replace(/\?.*$/gi, '')
21
- .replace(/:[a-z][a-z_]*\b/gi, '*')
22
- .replace(/\{[a-z-.]+}/gi, '*');
20
+ .replace(/#.*$/g, '') // Remove hash
21
+ .replace(/\?.*$/gi, '') // Remove query params
22
+ .replace(/:[a-z][a-z_]*\b/gi, '*') // Replace all params with *
23
+ .replace(/\{[a-z-.]+}/gi, '*'); // Replace all variables with *
23
24
  }
24
25
 
25
26
  export async function writePageToDisk(systemId: string, event: StormEventPage) {
@@ -168,6 +168,11 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
168
168
  }
169
169
  });
170
170
 
171
+ queue.on('error', (err) => {
172
+ console.error('Failed to process page', err);
173
+ sendError(err as any, res);
174
+ });
175
+
171
176
  await queue.addPrompt(aiRequest, conversationId, true);
172
177
 
173
178
  await queue.wait();
@@ -274,6 +279,11 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
274
279
  sendEvent(res, screenData);
275
280
  });
276
281
 
282
+ pageQueue.on('error', (err) => {
283
+ console.error('Failed to process page', err);
284
+ sendError(err as any, res);
285
+ });
286
+
277
287
  await waitForStormStream(landingPagesStream);
278
288
  await pageQueue.wait();
279
289
  await Promise.allSettled(pageEventPromises);
@@ -16,6 +16,23 @@ import {
16
16
  StormUIListPrompt,
17
17
  } from './stream';
18
18
  import { Page, StormEventPageUrl } from './events';
19
+ import { fetch, RequestInit, Agent, RetryAgent } from 'undici';
20
+
21
+ // Will only retry on error codes and GET requests by default
22
+ // See https://github.com/nodejs/undici/blob/990df2c7e37cbe5bb44fe2f576dddeaeb5916590/docs/docs/api/RetryAgent.md
23
+ const retryAgent = new RetryAgent(new Agent(), {
24
+ methods: [
25
+ // Added methods ↓ (not idempotent), but we want to retry on POST:
26
+ 'POST',
27
+ // defaults below
28
+ 'GET',
29
+ 'HEAD',
30
+ 'OPTIONS',
31
+ 'PUT',
32
+ 'DELETE',
33
+ 'TRACE',
34
+ ],
35
+ });
19
36
 
20
37
  export const STORM_ID = 'storm';
21
38
 
@@ -112,6 +129,7 @@ class StormClient {
112
129
  method: method,
113
130
  body: JSON.stringify(body),
114
131
  headers,
132
+ dispatcher: retryAgent,
115
133
  };
116
134
  }
117
135
 
@@ -222,10 +240,7 @@ class StormClient {
222
240
  conversationId,
223
241
  });
224
242
 
225
- return fetch(options.url, {
226
- method: options.method,
227
- headers: options.headers,
228
- });
243
+ return fetch(options.url, options);
229
244
  }
230
245
 
231
246
  public async getVoteUIPage(topic: string, conversationId: string, mainConversationId?: string) {
@@ -234,10 +249,7 @@ class StormClient {
234
249
  conversationId,
235
250
  });
236
251
 
237
- const response = await fetch(options.url, {
238
- method: options.method,
239
- headers: options.headers,
240
- });
252
+ const response = await fetch(options.url, options);
241
253
 
242
254
  return response.json() as Promise<{ vote: -1 | 0 | 1 }>;
243
255
  }
@@ -331,10 +343,7 @@ class StormClient {
331
343
  conversationId: conversationId,
332
344
  });
333
345
 
334
- const response = await fetch(options.url, {
335
- method: options.method,
336
- headers: options.headers,
337
- });
346
+ const response = await fetch(options.url, options);
338
347
 
339
348
  return response.text();
340
349
  }