@kapeta/local-cluster-service 0.67.4 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/cjs/src/storm/PageGenerator.d.ts +7 -8
- package/dist/cjs/src/storm/PageGenerator.js +49 -49
- package/dist/cjs/src/storm/page-utils.js +93 -13
- package/dist/cjs/src/storm/routes.js +18 -35
- package/dist/esm/src/storm/PageGenerator.d.ts +7 -8
- package/dist/esm/src/storm/PageGenerator.js +49 -49
- package/dist/esm/src/storm/page-utils.js +93 -13
- package/dist/esm/src/storm/routes.js +18 -35
- package/package.json +1 -1
- package/src/storm/PageGenerator.ts +63 -73
- package/src/storm/page-utils.ts +93 -13
- package/src/storm/routes.ts +19 -36
@@ -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 {
|
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
|
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,
|
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
|
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
|
-
|
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
|
137
|
+
generator.on('page_refs', async ({ event, references }) => {
|
133
138
|
try {
|
134
139
|
const initialPrompts: InitialPrompt[] = [];
|
135
|
-
|
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
|
-
|
178
|
-
|
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
|
-
|
201
|
-
|
202
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
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) {
|
package/src/storm/page-utils.ts
CHANGED
@@ -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
|
-
|
73
|
-
|
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
|
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
|
}
|
@@ -150,15 +144,101 @@ export function readPageFromDiskAsString(systemId: string, path: string, method:
|
|
150
144
|
export function readPageFromDisk(systemId: string, path: string, method: string, res: Response) {
|
151
145
|
const filePath = resolveReadPath(systemId, path, method);
|
152
146
|
if (!filePath || !FS.existsSync(filePath)) {
|
153
|
-
|
147
|
+
if (method === 'HEAD') {
|
148
|
+
// For HEAD requests, only return the status and headers
|
149
|
+
res.status(202).set('Retry-After', '3').end();
|
150
|
+
} else {
|
151
|
+
// For GET requests, return the fallback HTML with status 202
|
152
|
+
res.status(202).set('Retry-After', '3').send(getFallbackHtml(path, method));
|
153
|
+
}
|
154
154
|
return;
|
155
155
|
}
|
156
156
|
|
157
157
|
res.type(filePath.split('.').pop() as string);
|
158
158
|
|
159
159
|
const content = FS.readFileSync(filePath);
|
160
|
-
|
161
|
-
|
160
|
+
|
161
|
+
if (method === 'HEAD') {
|
162
|
+
// For HEAD requests, just end the response after setting headers
|
163
|
+
res.status(200).end();
|
164
|
+
} else {
|
165
|
+
// For GET requests, return the full content
|
166
|
+
res.write(content);
|
167
|
+
res.end();
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
function getFallbackHtml(path: string, method: string): string {
|
172
|
+
return `
|
173
|
+
<!DOCTYPE html>
|
174
|
+
<html lang="en">
|
175
|
+
|
176
|
+
<head>
|
177
|
+
<meta charset="UTF-8">
|
178
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
179
|
+
<title>Page Not Ready</title>
|
180
|
+
<style>
|
181
|
+
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;600&display=swap');
|
182
|
+
|
183
|
+
body {
|
184
|
+
margin: 0;
|
185
|
+
padding: 0;
|
186
|
+
display: flex;
|
187
|
+
align-items: center;
|
188
|
+
justify-content: center;
|
189
|
+
height: 100vh;
|
190
|
+
font-family: 'Roboto', sans-serif;
|
191
|
+
background: #1E1F20;
|
192
|
+
color: white;
|
193
|
+
text-align: center;
|
194
|
+
}
|
195
|
+
|
196
|
+
h1 {
|
197
|
+
font-size: 2rem;
|
198
|
+
font-weight: 600;
|
199
|
+
margin-bottom: 1rem;
|
200
|
+
}
|
201
|
+
|
202
|
+
p {
|
203
|
+
font-size: 1rem;
|
204
|
+
font-weight: 400;
|
205
|
+
}
|
206
|
+
</style>
|
207
|
+
</head>
|
208
|
+
|
209
|
+
<body>
|
210
|
+
<div>
|
211
|
+
<h1>Page Not Ready</h1>
|
212
|
+
<p>Henrik is still working on this page. Please wait...</p>
|
213
|
+
</div>
|
214
|
+
<script>
|
215
|
+
const checkInterval = 3000;
|
216
|
+
function checkPageReady() {
|
217
|
+
fetch('${path}', { method: 'HEAD' })
|
218
|
+
.then(response => {
|
219
|
+
if (response.status === 200) {
|
220
|
+
// The page is ready, reload to fetch it
|
221
|
+
window.location.reload();
|
222
|
+
} else if (response.status === 202) {
|
223
|
+
const retryAfter = response.headers.get('Retry-After');
|
224
|
+
const retryInterval = retryAfter ? parseInt(retryAfter) * 1000 : 3000;
|
225
|
+
setTimeout(checkPageReady, retryInterval);
|
226
|
+
} else {
|
227
|
+
// Handle other unexpected statuses
|
228
|
+
setTimeout(checkPageReady, 3000);
|
229
|
+
}
|
230
|
+
})
|
231
|
+
.catch(error => {
|
232
|
+
console.error('Error checking page status:', error);
|
233
|
+
setTimeout(checkPageReady, checkInterval);
|
234
|
+
});
|
235
|
+
}
|
236
|
+
setTimeout(checkPageReady, checkInterval);
|
237
|
+
</script>
|
238
|
+
</body>
|
239
|
+
|
240
|
+
</html>
|
241
|
+
`;
|
162
242
|
}
|
163
243
|
|
164
244
|
export interface Conversation {
|
package/src/storm/routes.ts
CHANGED
@@ -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
|
107
|
+
queue.on('image', async (screenData, prompt) => {
|
112
108
|
if (!systemId) {
|
113
109
|
return;
|
114
110
|
}
|
115
111
|
try {
|
116
|
-
|
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
|
-
|
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
|
-
|
234
|
-
|
225
|
+
pageQueue.on('page', (screenData: StormEventPage) =>
|
226
|
+
sendPageEvent(landingPagesStream.getConversationId(), screenData, res)
|
227
|
+
);
|
235
228
|
|
236
|
-
pageQueue.on('image', async (screenData, prompt
|
229
|
+
pageQueue.on('image', async (screenData, prompt) => {
|
237
230
|
try {
|
238
|
-
|
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
|
-
|
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
|
-
|
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
|
419
|
+
queue.on('image', async (screenData, prompt) => {
|
434
420
|
try {
|
435
|
-
|
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
|
-
|
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
|
-
|
451
|
-
|
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) {
|