@kapeta/local-cluster-service 0.70.3 → 0.70.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.
@@ -7,7 +7,8 @@ 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 { PromiseQueue } from './PromiseQueue';
10
+ import PQueue from 'p-queue';
11
+
11
12
  import { hasPageOnDisk, normalizePath } from './page-utils';
12
13
 
13
14
  export interface ImagePrompt {
@@ -23,11 +24,11 @@ export interface ImagePrompt {
23
24
  type InitialPrompt = Omit<UIPagePrompt, 'shell_page'> & { shellType?: 'public' | 'admin' | 'user' };
24
25
 
25
26
  export class PageQueue extends EventEmitter {
26
- private readonly queue: PromiseQueue;
27
- private readonly eventQueue: PromiseQueue;
27
+ private readonly queue: PQueue;
28
+ private readonly eventQueue: PQueue;
28
29
  private readonly systemId: string;
29
30
  private readonly systemPrompt: string;
30
- private readonly references: Map<string, PageGenerator> = new Map();
31
+ private readonly references: Map<string, boolean> = new Map();
31
32
  private readonly pages: Map<string, string> = new Map();
32
33
  private readonly images: Map<string, string> = new Map();
33
34
  private uiShells: UIShell[] = [];
@@ -37,8 +38,8 @@ export class PageQueue extends EventEmitter {
37
38
  super();
38
39
  this.systemId = systemId;
39
40
  this.systemPrompt = systemPrompt;
40
- this.queue = new PromiseQueue(concurrency);
41
- this.eventQueue = new PromiseQueue(Number.MAX_VALUE);
41
+ this.queue = new PQueue({ concurrency });
42
+ this.eventQueue = new PQueue({ concurrency: Number.MAX_VALUE });
42
43
  }
43
44
 
44
45
  on(event: 'event', listener: (data: StormEvent) => void | Promise<void>): this;
@@ -94,11 +95,10 @@ export class PageQueue extends EventEmitter {
94
95
  theme: this.theme,
95
96
  };
96
97
 
97
- const generator = new PageGenerator(prompt, conversationId);
98
- this.references.set(prompt.path, generator);
98
+ this.references.set(prompt.path, true);
99
99
  this.pages.set(prompt.path, prompt.description);
100
100
 
101
- return this.addPageGenerator(generator);
101
+ return this.queue.add<void>(() => this.generate(prompt, conversationId));
102
102
  }
103
103
  private getPrefix(): string {
104
104
  let promptPrefix = '';
@@ -132,114 +132,110 @@ export class PageQueue extends EventEmitter {
132
132
  return promptPrefix + prompt + promptPostfix;
133
133
  }
134
134
 
135
- private async addPageGenerator(generator: PageGenerator) {
136
- generator.on('event', (event: StormEvent) => this.emit('event', event));
137
- generator.on('page_refs', async ({ event, references }) => {
138
- try {
139
- const matchesExistingPages = (url: string) => {
140
- return [...this.pages.keys()].some((path) =>
141
- new RegExp(`^${path.replaceAll('/*', '/[^/]+')}$`).test(url.replace(/\?.*$/gi, ''))
142
- );
143
- };
144
- const initialPrompts: InitialPrompt[] = [];
145
- const resourcePromises = references.map(async (reference) => {
146
- if (
147
- reference.url.startsWith('#') ||
148
- reference.url.startsWith('javascript:') ||
149
- reference.url.startsWith('http://') ||
150
- reference.url.startsWith('https://')
151
- ) {
152
- return;
153
- }
135
+ private async processPageEventWithReferences(event: StormEventPage) {
136
+ try {
137
+ console.log('Processing page event', event.payload.path);
138
+ const references = await this.resolveReferences(event.payload.content);
139
+ const matchesExistingPages = (url: string) => {
140
+ return [...this.pages.keys()].some((path) =>
141
+ new RegExp(`^${path.replaceAll('/*', '/[^/]+')}$`).test(url)
142
+ );
143
+ };
144
+ const initialPrompts: InitialPrompt[] = [];
145
+ const resourcePromises = references.map(async (reference) => {
146
+ if (
147
+ reference.url.startsWith('#') ||
148
+ reference.url.startsWith('javascript:') ||
149
+ reference.url.startsWith('http://') ||
150
+ reference.url.startsWith('https://') ||
151
+ reference.url.startsWith('data:') ||
152
+ reference.url.startsWith('blob:') ||
153
+ reference.url.startsWith('mailto:')
154
+ ) {
155
+ return;
156
+ }
154
157
 
155
- switch (reference.type) {
156
- case 'image':
157
- await this.addImagePrompt({
158
- ...reference,
159
- content: event.payload.content,
160
- });
161
- break;
162
- case 'css':
163
- case 'javascript':
164
- //console.log('Ignoring reference', reference);
165
- break;
166
- case 'html': {
167
- const normalizedPath = normalizePath(reference.url);
168
- if (matchesExistingPages(normalizedPath)) {
169
- break;
170
- }
171
- this.pages.set(normalizedPath, reference.description);
172
-
173
- initialPrompts.push({
174
- name: reference.name,
175
- title: reference.title,
176
- path: normalizedPath,
177
- method: 'GET',
178
- storage_prefix: this.systemId + '_',
179
- prompt:
180
- `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
181
- `The page was referenced from this page: \n### PATH: ${event.payload.path}\n\`\`\`html\n${event.payload.content}\n\`\`\`\n`,
182
- description: reference.description,
183
- // Only used for matching
184
- filename: reference.name + '.ref.html',
185
- theme: this.theme,
186
- });
158
+ switch (reference.type) {
159
+ case 'image':
160
+ await this.addImagePrompt({
161
+ ...reference,
162
+ content: event.payload.content,
163
+ });
164
+ break;
165
+ case 'css':
166
+ case 'javascript':
167
+ //console.log('Ignoring reference', reference);
168
+ break;
169
+ case 'html': {
170
+ const normalizedPath = normalizePath(reference.url);
171
+ if (matchesExistingPages(normalizedPath)) {
187
172
  break;
188
173
  }
189
- }
190
- });
191
-
192
- // Wait for resources to be generated
193
- await Promise.allSettled(resourcePromises);
194
- this.emit('page', event);
195
-
196
- // Emit any new pages after the current page to increase responsiveness
197
- const newPages = initialPrompts.map((prompt) => {
198
- if (!this.hasPrompt(prompt.path)) {
199
- this.emit('page', {
200
- type: 'PAGE',
201
- reason: 'reference',
202
- created: Date.now(),
203
- payload: {
204
- name: prompt.name,
205
- title: prompt.title,
206
- filename: prompt.filename,
207
- method: 'GET',
208
- path: prompt.path,
209
- prompt: prompt.description,
210
- conversationId: '',
211
- content: '',
212
- description: prompt.description,
213
- },
174
+ this.pages.set(normalizedPath, reference.description);
175
+
176
+ initialPrompts.push({
177
+ name: reference.name,
178
+ title: reference.title,
179
+ path: normalizedPath,
180
+ method: 'GET',
181
+ storage_prefix: this.systemId + '_',
182
+ prompt:
183
+ `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
184
+ `The page was referenced from this page: \n### PATH: ${event.payload.path}\n\`\`\`html\n${event.payload.content}\n\`\`\`\n`,
185
+ description: reference.description,
186
+ // Only used for matching
187
+ filename: reference.name + '.ref.html',
188
+ theme: this.theme,
214
189
  });
190
+ break;
215
191
  }
216
- return this.addPrompt(prompt);
217
- });
218
- await Promise.allSettled(newPages);
219
- } catch (e) {
220
- console.error('Failed to process event', e);
221
- throw e;
222
- }
223
- });
224
- return this.queue.add(() => generator.generate());
192
+ }
193
+ });
194
+
195
+ // Wait for resources to be generated
196
+ await Promise.allSettled(resourcePromises);
197
+ this.emit('page', event);
198
+
199
+ // Emit any new pages after the current page to increase responsiveness
200
+ void initialPrompts.map((prompt) => {
201
+ if (!this.hasPrompt(prompt.path)) {
202
+ this.emit('page', {
203
+ type: 'PAGE',
204
+ reason: 'reference',
205
+ created: Date.now(),
206
+ payload: {
207
+ name: prompt.name,
208
+ title: prompt.title,
209
+ filename: prompt.filename,
210
+ method: 'GET',
211
+ path: prompt.path,
212
+ prompt: prompt.description,
213
+ conversationId: '',
214
+ content: '',
215
+ description: prompt.description,
216
+ },
217
+ });
218
+ }
219
+ return this.addPrompt(prompt);
220
+ });
221
+ } catch (e) {
222
+ console.error('Failed to process event', e);
223
+ throw e;
224
+ }
225
225
  }
226
226
 
227
227
  public cancel() {
228
- this.queue.cancel();
229
- this.eventQueue.cancel();
228
+ this.queue.clear();
229
+ this.eventQueue.clear();
230
230
  }
231
231
 
232
232
  public async wait() {
233
- while (!this.eventQueue.empty || !this.queue.empty) {
234
- await this.eventQueue.wait();
235
- await this.queue.wait();
233
+ while (this.eventQueue.size || this.eventQueue.pending || this.queue.size || this.queue.pending) {
234
+ await this.eventQueue.onIdle();
235
+ await this.queue.onIdle();
236
236
  }
237
237
  }
238
238
 
239
- public get length() {
240
- return this.queue.length + this.eventQueue.length;
241
- }
242
-
243
239
  private async addImagePrompt(prompt: ImagePrompt) {
244
240
  if (this.images.has(prompt.url)) {
245
241
  //console.log('Ignoring duplicate image prompt', prompt);
@@ -260,56 +256,16 @@ export class PageQueue extends EventEmitter {
260
256
 
261
257
  await result.waitForDone();
262
258
  }
263
- }
264
-
265
- export class PageGenerator extends EventEmitter {
266
- private readonly eventQueue: PromiseQueue;
267
- private readonly conversationId: string;
268
- public readonly prompt: UIPagePrompt;
269
-
270
- constructor(prompt: UIPagePrompt, conversationId: string = uuid.v4()) {
271
- super();
272
- this.conversationId = conversationId;
273
- this.prompt = prompt;
274
- this.eventQueue = new PromiseQueue(Number.MAX_VALUE);
275
- }
276
259
 
277
- on(event: 'event', listener: (data: StormEvent) => void): this;
278
- on(
279
- event: 'page_refs',
280
- listener: (data: { event: StormEventPage; references: ReferenceClassification[] }) => Promise<void>
281
- ): this;
282
-
283
- on(event: string, listener: (...args: any[]) => void): this {
284
- return super.on(event, (...args) => {
285
- void this.eventQueue.add(async () => listener(...args));
286
- });
287
- }
288
-
289
- emit(type: 'event', event: StormEvent): boolean;
290
- emit(type: 'page_refs', event: { event: StormEventPage; references: ReferenceClassification[] }): boolean;
291
- emit(eventName: string | symbol, ...args: any[]): boolean {
292
- return super.emit(eventName, ...args);
293
- }
294
-
295
- public async generate() {
260
+ public async generate(prompt: UIPagePrompt, conversationId: string) {
296
261
  const promises: Promise<void>[] = [];
297
- const screenStream = await stormClient.createUIPage(this.prompt, this.conversationId);
262
+ const screenStream = await stormClient.createUIPage(prompt, conversationId);
298
263
 
299
264
  screenStream.on('data', (event: StormEvent) => {
300
265
  if (event.type === 'PAGE') {
301
- event.payload.conversationId = this.conversationId;
302
-
303
- promises.push(
304
- (async () => {
305
- const references = await this.resolveReferences(event.payload.content);
306
- //console.log('Resolved references for page', references, event.payload);
307
- this.emit('page_refs', {
308
- event,
309
- references,
310
- });
311
- })()
312
- );
266
+ event.payload.conversationId = conversationId;
267
+
268
+ promises.push(this.processPageEventWithReferences(event));
313
269
  return;
314
270
  }
315
271
 
@@ -318,7 +274,6 @@ export class PageGenerator extends EventEmitter {
318
274
 
319
275
  await screenStream.waitForDone();
320
276
  await Promise.all(promises);
321
- await this.eventQueue.wait();
322
277
  }
323
278
 
324
279
  private async resolveReferences(content: string) {
@@ -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) {
@@ -78,7 +79,8 @@ export function getSystemBaseDir(systemId: string) {
78
79
  }
79
80
 
80
81
  function getFilePath(method: string) {
81
- return Path.join(method.toLowerCase(), 'index.html');
82
+ // For HEAD requests, we assume we're serving looking for a GET resource
83
+ return Path.join(method === 'HEAD' ? 'get' : method.toLowerCase(), 'index.html');
82
84
  }
83
85
 
84
86
  export function resolveReadPath(systemId: string, path: string, method: string) {
@@ -105,7 +107,7 @@ export function resolveReadPath(systemId: string, path: string, method: string)
105
107
  return fullPath;
106
108
  }
107
109
 
108
- const parts = path.split(/\*/g);
110
+ const parts = path.split('/');
109
111
 
110
112
  let currentPath = '';
111
113
 
@@ -214,7 +216,7 @@ function getFallbackHtml(path: string, method: string): string {
214
216
  <script>
215
217
  const checkInterval = 3000;
216
218
  function checkPageReady() {
217
- fetch('${path}', { method: 'HEAD' })
219
+ fetch('/${path}', { method: 'HEAD' })
218
220
  .then(response => {
219
221
  if (response.status === 200) {
220
222
  // The page is ready, reload to fetch it
@@ -47,8 +47,7 @@ import {
47
47
  import { UIServer } from './UIServer';
48
48
  import { randomUUID } from 'crypto';
49
49
  import { ImagePrompt, PageQueue } from './PageGenerator';
50
- import { createFuture } from './PromiseQueue';
51
- import { copyDirectory } from './utils';
50
+ import { copyDirectory, createFuture } from './utils';
52
51
 
53
52
  const UI_SERVERS: { [key: string]: UIServer } = {};
54
53
  const router = Router();
@@ -250,9 +249,7 @@ router.post('/:handle/ui/iterative', async (req: KapetaBodyRequest, res: Respons
250
249
  UI_SERVERS[systemId] = new UIServer(systemId);
251
250
  await UI_SERVERS[systemId].start();
252
251
  waitForStormStream(landingPagesStream).then(() => {
253
- if (!systemPrompt.isResolved()) {
254
- systemPrompt.resolve(aiRequest.prompt);
255
- }
252
+ systemPrompt.resolve(aiRequest.prompt);
256
253
  });
257
254
 
258
255
  const pageQueue = new PageQueue(systemId, await systemPrompt.promise, 5);
@@ -5,7 +5,11 @@
5
5
  import FS from 'fs-extra';
6
6
  import Path from 'path';
7
7
 
8
- export async function copyDirectory(src: string, dest: string, modifyHtml: (fileName: string, content: string) => Promise<string>): Promise<void> {
8
+ export async function copyDirectory(
9
+ src: string,
10
+ dest: string,
11
+ modifyHtml: (fileName: string, content: string) => Promise<string>
12
+ ): Promise<void> {
9
13
  await FS.promises.mkdir(dest, { recursive: true });
10
14
  const entries = await FS.promises.readdir(src, { withFileTypes: true });
11
15
 
@@ -26,3 +30,17 @@ export async function copyDirectory(src: string, dest: string, modifyHtml: (file
26
30
  }
27
31
  }
28
32
  }
33
+
34
+ export function createFuture<T = void>() {
35
+ let resolve: (value: T | PromiseLike<T>) => void = () => {};
36
+ let reject: (reason?: any) => void = () => {};
37
+ const promise = new Promise<T>((res, rej) => {
38
+ resolve = res;
39
+ reject = rej;
40
+ });
41
+ return {
42
+ promise,
43
+ resolve,
44
+ reject,
45
+ };
46
+ }
@@ -1,25 +0,0 @@
1
- /**
2
- * Copyright 2023 Kapeta Inc.
3
- * SPDX-License-Identifier: BUSL-1.1
4
- */
5
- export type Future<T> = () => Promise<T>;
6
- export type FuturePromise<T> = {
7
- promise: Promise<T>;
8
- resolve: (value: T) => void;
9
- reject: (reason: any) => void;
10
- isResolved: () => boolean;
11
- };
12
- export declare function createFuture<T = void>(): FuturePromise<T>;
13
- export declare class PromiseQueue {
14
- private readonly queue;
15
- private readonly active;
16
- private readonly maxConcurrency;
17
- constructor(maxConcurrency?: number);
18
- private toInternal;
19
- add<T>(future: Future<T>): Promise<T>;
20
- private next;
21
- cancel(): void;
22
- wait(): Promise<void>;
23
- get empty(): boolean;
24
- get length(): number;
25
- }
@@ -1,97 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PromiseQueue = exports.createFuture = void 0;
4
- function createFuture() {
5
- let resolved = false;
6
- let resolve = () => {
7
- resolved = true;
8
- };
9
- let reject = () => {
10
- resolved = true;
11
- };
12
- const promise = new Promise((res, rej) => {
13
- resolve = (value) => {
14
- resolved = true;
15
- res(value);
16
- };
17
- reject = (reason) => {
18
- resolved = true;
19
- rej(reason);
20
- };
21
- });
22
- return {
23
- promise,
24
- resolve,
25
- reject,
26
- isResolved: () => resolved,
27
- };
28
- }
29
- exports.createFuture = createFuture;
30
- class PromiseQueue {
31
- queue = [];
32
- active = [];
33
- // Maximum number of concurrent promises
34
- maxConcurrency;
35
- constructor(maxConcurrency = 5) {
36
- this.maxConcurrency = maxConcurrency;
37
- }
38
- toInternal(future) {
39
- let resolve = () => { };
40
- let reject = () => { };
41
- const promise = new Promise((res, rej) => {
42
- resolve = res;
43
- reject = rej;
44
- });
45
- return {
46
- execute: future,
47
- promise,
48
- resolve,
49
- reject,
50
- };
51
- }
52
- add(future) {
53
- const internal = this.toInternal(future);
54
- this.queue.push(internal);
55
- this.next();
56
- return internal.promise;
57
- }
58
- next() {
59
- if (this.active.length >= this.maxConcurrency) {
60
- return false;
61
- }
62
- if (this.queue.length === 0) {
63
- return false;
64
- }
65
- const future = this.queue.shift();
66
- if (!future) {
67
- return false;
68
- }
69
- const promise = future
70
- .execute()
71
- .then(future.resolve)
72
- .catch(future.reject)
73
- .finally(() => {
74
- this.active.splice(this.active.indexOf(promise), 1);
75
- this.next();
76
- });
77
- this.active.push(promise);
78
- return this.next();
79
- }
80
- cancel() {
81
- this.queue.splice(0, this.queue.length);
82
- this.active.splice(0, this.active.length);
83
- }
84
- async wait() {
85
- while (this.queue.length > 0 || this.active.length > 0) {
86
- await Promise.allSettled(this.active);
87
- this.next();
88
- }
89
- }
90
- get empty() {
91
- return this.queue.length === 0 && this.active.length === 0;
92
- }
93
- get length() {
94
- return this.queue.length + this.active.length;
95
- }
96
- }
97
- exports.PromiseQueue = PromiseQueue;
@@ -1,25 +0,0 @@
1
- /**
2
- * Copyright 2023 Kapeta Inc.
3
- * SPDX-License-Identifier: BUSL-1.1
4
- */
5
- export type Future<T> = () => Promise<T>;
6
- export type FuturePromise<T> = {
7
- promise: Promise<T>;
8
- resolve: (value: T) => void;
9
- reject: (reason: any) => void;
10
- isResolved: () => boolean;
11
- };
12
- export declare function createFuture<T = void>(): FuturePromise<T>;
13
- export declare class PromiseQueue {
14
- private readonly queue;
15
- private readonly active;
16
- private readonly maxConcurrency;
17
- constructor(maxConcurrency?: number);
18
- private toInternal;
19
- add<T>(future: Future<T>): Promise<T>;
20
- private next;
21
- cancel(): void;
22
- wait(): Promise<void>;
23
- get empty(): boolean;
24
- get length(): number;
25
- }
@@ -1,97 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PromiseQueue = exports.createFuture = void 0;
4
- function createFuture() {
5
- let resolved = false;
6
- let resolve = () => {
7
- resolved = true;
8
- };
9
- let reject = () => {
10
- resolved = true;
11
- };
12
- const promise = new Promise((res, rej) => {
13
- resolve = (value) => {
14
- resolved = true;
15
- res(value);
16
- };
17
- reject = (reason) => {
18
- resolved = true;
19
- rej(reason);
20
- };
21
- });
22
- return {
23
- promise,
24
- resolve,
25
- reject,
26
- isResolved: () => resolved,
27
- };
28
- }
29
- exports.createFuture = createFuture;
30
- class PromiseQueue {
31
- queue = [];
32
- active = [];
33
- // Maximum number of concurrent promises
34
- maxConcurrency;
35
- constructor(maxConcurrency = 5) {
36
- this.maxConcurrency = maxConcurrency;
37
- }
38
- toInternal(future) {
39
- let resolve = () => { };
40
- let reject = () => { };
41
- const promise = new Promise((res, rej) => {
42
- resolve = res;
43
- reject = rej;
44
- });
45
- return {
46
- execute: future,
47
- promise,
48
- resolve,
49
- reject,
50
- };
51
- }
52
- add(future) {
53
- const internal = this.toInternal(future);
54
- this.queue.push(internal);
55
- this.next();
56
- return internal.promise;
57
- }
58
- next() {
59
- if (this.active.length >= this.maxConcurrency) {
60
- return false;
61
- }
62
- if (this.queue.length === 0) {
63
- return false;
64
- }
65
- const future = this.queue.shift();
66
- if (!future) {
67
- return false;
68
- }
69
- const promise = future
70
- .execute()
71
- .then(future.resolve)
72
- .catch(future.reject)
73
- .finally(() => {
74
- this.active.splice(this.active.indexOf(promise), 1);
75
- this.next();
76
- });
77
- this.active.push(promise);
78
- return this.next();
79
- }
80
- cancel() {
81
- this.queue.splice(0, this.queue.length);
82
- this.active.splice(0, this.active.length);
83
- }
84
- async wait() {
85
- while (this.queue.length > 0 || this.active.length > 0) {
86
- await Promise.allSettled(this.active);
87
- this.next();
88
- }
89
- }
90
- get empty() {
91
- return this.queue.length === 0 && this.active.length === 0;
92
- }
93
- get length() {
94
- return this.queue.length + this.active.length;
95
- }
96
- }
97
- exports.PromiseQueue = PromiseQueue;