@kapeta/local-cluster-service 0.70.2 → 0.70.4

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,21 @@
1
+ ## [0.70.4](https://github.com/kapetacom/local-cluster-service/compare/v0.70.3...v0.70.4) (2024-09-10)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * better dedupe of refs ([be5c8d2](https://github.com/kapetacom/local-cluster-service/commit/be5c8d2ee555b6e53a47043ce42ecec0f9a0f36c))
7
+ * ignore more protocols in refs ([e44475b](https://github.com/kapetacom/local-cluster-service/commit/e44475b20409ed714b19d387f137600f9d461cd6))
8
+ * migrate to p-queue package for tasks ([441c480](https://github.com/kapetacom/local-cluster-service/commit/441c480cc35d178a807fe57506b6cb54258e2518))
9
+ * tweak page routing logic + WIP screen ([bd7cb0c](https://github.com/kapetacom/local-cluster-service/commit/bd7cb0c580c089a8e2d0a52253b1908142c39537))
10
+
11
+ ## [0.70.3](https://github.com/kapetacom/local-cluster-service/compare/v0.70.2...v0.70.3) (2024-09-10)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * better dedupe of refs ([48f676e](https://github.com/kapetacom/local-cluster-service/commit/48f676e0f6780580c2b77cdb8e6f3336f6633e77))
17
+ * match full pattern in lazy check ([5b5e9d7](https://github.com/kapetacom/local-cluster-service/commit/5b5e9d7cfaf3e6e9b3eacf378e90362e99433d86))
18
+
1
19
  ## [0.70.2](https://github.com/kapetacom/local-cluster-service/compare/v0.70.1...v0.70.2) (2024-09-09)
2
20
 
3
21
 
@@ -4,7 +4,7 @@
4
4
  */
5
5
  /// <reference types="node" />
6
6
  import { UIPagePrompt } from './stormClient';
7
- import { ReferenceClassification, StormEvent, StormEventPage, StormImage, UIShell } from './events';
7
+ import { StormEvent, StormEventPage, StormImage, UIShell } from './events';
8
8
  import { EventEmitter } from 'node:events';
9
9
  export interface ImagePrompt {
10
10
  name: string;
@@ -41,28 +41,11 @@ export declare class PageQueue extends EventEmitter {
41
41
  addPrompt(initialPrompt: InitialPrompt, conversationId?: string, overwrite?: boolean): Promise<void>;
42
42
  private getPrefix;
43
43
  private wrapPagePrompt;
44
- private addPageGenerator;
44
+ private processPageEventWithReferences;
45
45
  cancel(): void;
46
46
  wait(): Promise<void>;
47
- get length(): number;
48
47
  private addImagePrompt;
49
- }
50
- export declare class PageGenerator extends EventEmitter {
51
- private readonly eventQueue;
52
- private readonly conversationId;
53
- readonly prompt: UIPagePrompt;
54
- constructor(prompt: UIPagePrompt, conversationId?: string);
55
- on(event: 'event', listener: (data: StormEvent) => void): this;
56
- on(event: 'page_refs', listener: (data: {
57
- event: StormEventPage;
58
- references: ReferenceClassification[];
59
- }) => Promise<void>): this;
60
- emit(type: 'event', event: StormEvent): boolean;
61
- emit(type: 'page_refs', event: {
62
- event: StormEventPage;
63
- references: ReferenceClassification[];
64
- }): boolean;
65
- generate(): Promise<void>;
48
+ generate(prompt: UIPagePrompt, conversationId: string): Promise<void>;
66
49
  private resolveReferences;
67
50
  }
68
51
  export {};
@@ -7,11 +7,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.PageGenerator = exports.PageQueue = void 0;
10
+ exports.PageQueue = void 0;
11
11
  const node_uuid_1 = __importDefault(require("node-uuid"));
12
12
  const stormClient_1 = require("./stormClient");
13
13
  const node_events_1 = require("node:events");
14
- const PromiseQueue_1 = require("./PromiseQueue");
14
+ const p_queue_1 = __importDefault(require("p-queue"));
15
15
  const page_utils_1 = require("./page-utils");
16
16
  class PageQueue extends node_events_1.EventEmitter {
17
17
  queue;
@@ -27,8 +27,8 @@ class PageQueue extends node_events_1.EventEmitter {
27
27
  super();
28
28
  this.systemId = systemId;
29
29
  this.systemPrompt = systemPrompt;
30
- this.queue = new PromiseQueue_1.PromiseQueue(concurrency);
31
- this.eventQueue = new PromiseQueue_1.PromiseQueue(Number.MAX_VALUE);
30
+ this.queue = new p_queue_1.default({ concurrency });
31
+ this.eventQueue = new p_queue_1.default({ concurrency: Number.MAX_VALUE });
32
32
  }
33
33
  on(event, listener) {
34
34
  return super.on(event, (...args) => {
@@ -67,10 +67,9 @@ class PageQueue extends node_events_1.EventEmitter {
67
67
  prompt: this.wrapPagePrompt(initialPrompt.path, initialPrompt.prompt),
68
68
  theme: this.theme,
69
69
  };
70
- const generator = new PageGenerator(prompt, conversationId);
71
- this.references.set(prompt.path, generator);
70
+ this.references.set(prompt.path, true);
72
71
  this.pages.set(prompt.path, prompt.description);
73
- return this.addPageGenerator(generator);
72
+ return this.queue.add(() => this.generate(prompt, conversationId));
74
73
  }
75
74
  getPrefix() {
76
75
  let promptPrefix = '';
@@ -99,101 +98,99 @@ class PageQueue extends node_events_1.EventEmitter {
99
98
  }
100
99
  return promptPrefix + prompt + promptPostfix;
101
100
  }
102
- async addPageGenerator(generator) {
103
- generator.on('event', (event) => this.emit('event', event));
104
- generator.on('page_refs', async ({ event, references }) => {
105
- try {
106
- const matchesExistingPages = (url) => {
107
- return [...this.pages.keys()].some((path) => new RegExp(path.replaceAll('/*', '/[^/]+')).test(url));
108
- };
109
- const initialPrompts = [];
110
- const resourcePromises = references.map(async (reference) => {
111
- if (reference.url.startsWith('#') ||
112
- reference.url.startsWith('javascript:') ||
113
- reference.url.startsWith('http://') ||
114
- reference.url.startsWith('https://')) {
115
- return;
116
- }
117
- switch (reference.type) {
118
- case 'image':
119
- await this.addImagePrompt({
120
- ...reference,
121
- content: event.payload.content,
122
- });
123
- break;
124
- case 'css':
125
- case 'javascript':
126
- //console.log('Ignoring reference', reference);
127
- break;
128
- case 'html':
129
- //console.log('Adding page generator for', reference);
130
- if (matchesExistingPages(reference.url)) {
131
- break;
132
- }
133
- this.pages.set(reference.url, reference.description);
134
- initialPrompts.push({
135
- name: reference.name,
136
- title: reference.title,
137
- path: reference.url,
138
- method: 'GET',
139
- storage_prefix: this.systemId + '_',
140
- prompt: `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
141
- `The page was referenced from this page: \n### PATH: ${event.payload.path}\n\`\`\`html\n${event.payload.content}\n\`\`\`\n`,
142
- description: reference.description,
143
- // Only used for matching
144
- filename: reference.name + '.ref.html',
145
- theme: this.theme,
146
- });
101
+ async processPageEventWithReferences(event) {
102
+ try {
103
+ console.log('Processing page event', event.payload.path);
104
+ const references = await this.resolveReferences(event.payload.content);
105
+ const matchesExistingPages = (url) => {
106
+ return [...this.pages.keys()].some((path) => new RegExp(`^${path.replaceAll('/*', '/[^/]+')}$`).test(url));
107
+ };
108
+ const initialPrompts = [];
109
+ const resourcePromises = references.map(async (reference) => {
110
+ if (reference.url.startsWith('#') ||
111
+ reference.url.startsWith('javascript:') ||
112
+ reference.url.startsWith('http://') ||
113
+ reference.url.startsWith('https://') ||
114
+ reference.url.startsWith('data:') ||
115
+ reference.url.startsWith('blob:') ||
116
+ reference.url.startsWith('mailto:')) {
117
+ return;
118
+ }
119
+ switch (reference.type) {
120
+ case 'image':
121
+ await this.addImagePrompt({
122
+ ...reference,
123
+ content: event.payload.content,
124
+ });
125
+ break;
126
+ case 'css':
127
+ case 'javascript':
128
+ //console.log('Ignoring reference', reference);
129
+ break;
130
+ case 'html': {
131
+ const normalizedPath = (0, page_utils_1.normalizePath)(reference.url);
132
+ if (matchesExistingPages(normalizedPath)) {
147
133
  break;
148
- }
149
- });
150
- // Wait for resources to be generated
151
- await Promise.allSettled(resourcePromises);
152
- this.emit('page', event);
153
- // Emit any new pages after the current page to increase responsiveness
154
- const newPages = initialPrompts.map((prompt) => {
155
- if (!this.hasPrompt(prompt.path)) {
156
- this.emit('page', {
157
- type: 'PAGE',
158
- reason: 'reference',
159
- created: Date.now(),
160
- payload: {
161
- name: prompt.name,
162
- title: prompt.title,
163
- filename: prompt.filename,
164
- method: 'GET',
165
- path: prompt.path,
166
- prompt: prompt.description,
167
- conversationId: '',
168
- content: '',
169
- description: prompt.description,
170
- },
134
+ }
135
+ this.pages.set(normalizedPath, reference.description);
136
+ initialPrompts.push({
137
+ name: reference.name,
138
+ title: reference.title,
139
+ path: normalizedPath,
140
+ method: 'GET',
141
+ storage_prefix: this.systemId + '_',
142
+ prompt: `Implement a page for ${reference.name} at ${reference.url} with the following description: ${reference.description}.\n` +
143
+ `The page was referenced from this page: \n### PATH: ${event.payload.path}\n\`\`\`html\n${event.payload.content}\n\`\`\`\n`,
144
+ description: reference.description,
145
+ // Only used for matching
146
+ filename: reference.name + '.ref.html',
147
+ theme: this.theme,
171
148
  });
149
+ break;
172
150
  }
173
- return this.addPrompt(prompt);
174
- });
175
- await Promise.allSettled(newPages);
176
- }
177
- catch (e) {
178
- console.error('Failed to process event', e);
179
- throw e;
180
- }
181
- });
182
- return this.queue.add(() => generator.generate());
151
+ }
152
+ });
153
+ // Wait for resources to be generated
154
+ await Promise.allSettled(resourcePromises);
155
+ this.emit('page', event);
156
+ // Emit any new pages after the current page to increase responsiveness
157
+ void initialPrompts.map((prompt) => {
158
+ if (!this.hasPrompt(prompt.path)) {
159
+ this.emit('page', {
160
+ type: 'PAGE',
161
+ reason: 'reference',
162
+ created: Date.now(),
163
+ payload: {
164
+ name: prompt.name,
165
+ title: prompt.title,
166
+ filename: prompt.filename,
167
+ method: 'GET',
168
+ path: prompt.path,
169
+ prompt: prompt.description,
170
+ conversationId: '',
171
+ content: '',
172
+ description: prompt.description,
173
+ },
174
+ });
175
+ }
176
+ return this.addPrompt(prompt);
177
+ });
178
+ }
179
+ catch (e) {
180
+ console.error('Failed to process event', e);
181
+ throw e;
182
+ }
183
183
  }
184
184
  cancel() {
185
- this.queue.cancel();
186
- this.eventQueue.cancel();
185
+ this.queue.clear();
186
+ this.eventQueue.clear();
187
187
  }
188
188
  async wait() {
189
- while (!this.eventQueue.empty || !this.queue.empty) {
190
- await this.eventQueue.wait();
191
- await this.queue.wait();
189
+ while (this.eventQueue.size || this.eventQueue.pending || this.queue.size || this.queue.pending) {
190
+ await this.eventQueue.onIdle();
191
+ await this.queue.onIdle();
192
192
  }
193
193
  }
194
- get length() {
195
- return this.queue.length + this.eventQueue.length;
196
- }
197
194
  async addImagePrompt(prompt) {
198
195
  if (this.images.has(prompt.url)) {
199
196
  //console.log('Ignoring duplicate image prompt', prompt);
@@ -210,47 +207,19 @@ class PageQueue extends node_events_1.EventEmitter {
210
207
  });
211
208
  await result.waitForDone();
212
209
  }
213
- }
214
- exports.PageQueue = PageQueue;
215
- class PageGenerator extends node_events_1.EventEmitter {
216
- eventQueue;
217
- conversationId;
218
- prompt;
219
- constructor(prompt, conversationId = node_uuid_1.default.v4()) {
220
- super();
221
- this.conversationId = conversationId;
222
- this.prompt = prompt;
223
- this.eventQueue = new PromiseQueue_1.PromiseQueue(Number.MAX_VALUE);
224
- }
225
- on(event, listener) {
226
- return super.on(event, (...args) => {
227
- void this.eventQueue.add(async () => listener(...args));
228
- });
229
- }
230
- emit(eventName, ...args) {
231
- return super.emit(eventName, ...args);
232
- }
233
- async generate() {
210
+ async generate(prompt, conversationId) {
234
211
  const promises = [];
235
- const screenStream = await stormClient_1.stormClient.createUIPage(this.prompt, this.conversationId);
212
+ const screenStream = await stormClient_1.stormClient.createUIPage(prompt, conversationId);
236
213
  screenStream.on('data', (event) => {
237
214
  if (event.type === 'PAGE') {
238
- event.payload.conversationId = this.conversationId;
239
- promises.push((async () => {
240
- const references = await this.resolveReferences(event.payload.content);
241
- //console.log('Resolved references for page', references, event.payload);
242
- this.emit('page_refs', {
243
- event,
244
- references,
245
- });
246
- })());
215
+ event.payload.conversationId = conversationId;
216
+ promises.push(this.processPageEventWithReferences(event));
247
217
  return;
248
218
  }
249
219
  this.emit('event', event);
250
220
  });
251
221
  await screenStream.waitForDone();
252
222
  await Promise.all(promises);
253
- await this.eventQueue.wait();
254
223
  }
255
224
  async resolveReferences(content) {
256
225
  const referenceStream = await stormClient_1.stormClient.classifyUIReferences(content);
@@ -266,4 +235,4 @@ class PageGenerator extends node_events_1.EventEmitter {
266
235
  return references;
267
236
  }
268
237
  }
269
- exports.PageGenerator = PageGenerator;
238
+ exports.PageQueue = PageQueue;
@@ -7,6 +7,7 @@ import { Response } from 'express';
7
7
  import { ConversationItem } from './stream';
8
8
  import { ImagePrompt } from './PageGenerator';
9
9
  export declare const SystemIdHeader = "System-Id";
10
+ export declare function normalizePath(path: string): string;
10
11
  export declare function writePageToDisk(systemId: string, event: StormEventPage): Promise<{
11
12
  path: string;
12
13
  }>;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.getSystemBaseDir = exports.hasPageOnDisk = exports.writeImageToDisk = exports.writeAssetToDisk = exports.writePageToDisk = exports.SystemIdHeader = void 0;
6
+ exports.writeConversationToFile = exports.readConversationFromFile = exports.readPageFromDisk = exports.readPageFromDiskAsString = exports.resolveReadPath = exports.getSystemBaseDir = exports.hasPageOnDisk = exports.writeImageToDisk = exports.writeAssetToDisk = exports.writePageToDisk = exports.normalizePath = exports.SystemIdHeader = void 0;
7
7
  const node_os_1 = __importDefault(require("node:os"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -14,6 +14,7 @@ function normalizePath(path) {
14
14
  .replace(/:[a-z][a-z_]*\b/gi, '*')
15
15
  .replace(/\{[a-z-.]+}/gi, '*');
16
16
  }
17
+ exports.normalizePath = normalizePath;
17
18
  async function writePageToDisk(systemId, event) {
18
19
  const baseDir = getSystemBaseDir(systemId);
19
20
  const filePath = getFilePath(event.payload.method);
@@ -62,7 +63,8 @@ function getSystemBaseDir(systemId) {
62
63
  }
63
64
  exports.getSystemBaseDir = getSystemBaseDir;
64
65
  function getFilePath(method) {
65
- return path_1.default.join(method.toLowerCase(), 'index.html');
66
+ // For HEAD requests, we assume we're serving looking for a GET resource
67
+ return path_1.default.join(method === 'HEAD' ? 'get' : method.toLowerCase(), 'index.html');
66
68
  }
67
69
  function resolveReadPath(systemId, path, method) {
68
70
  const baseDir = getSystemBaseDir(systemId);
@@ -83,7 +85,7 @@ function resolveReadPath(systemId, path, method) {
83
85
  if (fs_extra_1.default.existsSync(fullPath)) {
84
86
  return fullPath;
85
87
  }
86
- const parts = path.split(/\*/g);
88
+ const parts = path.split('/');
87
89
  let currentPath = '';
88
90
  for (let part of parts) {
89
91
  const thisPath = path_1.default.join(currentPath, part);
@@ -184,7 +186,7 @@ function getFallbackHtml(path, method) {
184
186
  <script>
185
187
  const checkInterval = 3000;
186
188
  function checkPageReady() {
187
- fetch('${path}', { method: 'HEAD' })
189
+ fetch('/${path}', { method: 'HEAD' })
188
190
  .then(response => {
189
191
  if (response.status === 200) {
190
192
  // The page is ready, reload to fetch it
@@ -24,7 +24,6 @@ const page_utils_1 = require("./page-utils");
24
24
  const UIServer_1 = require("./UIServer");
25
25
  const crypto_1 = require("crypto");
26
26
  const PageGenerator_1 = require("./PageGenerator");
27
- const PromiseQueue_1 = require("./PromiseQueue");
28
27
  const utils_1 = require("./utils");
29
28
  const UI_SERVERS = {};
30
29
  const router = (0, express_promise_router_1.default)();
@@ -154,7 +153,7 @@ router.post('/:handle/ui/iterative', async (req, res) => {
154
153
  const promises = {};
155
154
  const pageEventPromises = [];
156
155
  const systemId = landingPagesStream.getConversationId();
157
- const systemPrompt = (0, PromiseQueue_1.createFuture)();
156
+ const systemPrompt = (0, utils_1.createFuture)();
158
157
  if (aiRequest.skipImprovement) {
159
158
  systemPrompt.resolve(aiRequest.prompt);
160
159
  }
@@ -196,9 +195,7 @@ router.post('/:handle/ui/iterative', async (req, res) => {
196
195
  UI_SERVERS[systemId] = new UIServer_1.UIServer(systemId);
197
196
  await UI_SERVERS[systemId].start();
198
197
  waitForStormStream(landingPagesStream).then(() => {
199
- if (!systemPrompt.isResolved()) {
200
- systemPrompt.resolve(aiRequest.prompt);
201
- }
198
+ systemPrompt.resolve(aiRequest.prompt);
202
199
  });
203
200
  const pageQueue = new PageGenerator_1.PageQueue(systemId, await systemPrompt.promise, 5);
204
201
  onRequestAborted(req, res, () => {
@@ -1 +1,6 @@
1
1
  export declare function copyDirectory(src: string, dest: string, modifyHtml: (fileName: string, content: string) => Promise<string>): Promise<void>;
2
+ export declare function createFuture<T = void>(): {
3
+ promise: Promise<T>;
4
+ resolve: (value: T | PromiseLike<T>) => void;
5
+ reject: (reason?: any) => void;
6
+ };
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.copyDirectory = void 0;
6
+ exports.createFuture = exports.copyDirectory = void 0;
7
7
  /**
8
8
  * Copyright 2023 Kapeta Inc.
9
9
  * SPDX-License-Identifier: BUSL-1.1
@@ -29,3 +29,17 @@ async function copyDirectory(src, dest, modifyHtml) {
29
29
  }
30
30
  }
31
31
  exports.copyDirectory = copyDirectory;
32
+ function createFuture() {
33
+ let resolve = () => { };
34
+ let reject = () => { };
35
+ const promise = new Promise((res, rej) => {
36
+ resolve = res;
37
+ reject = rej;
38
+ });
39
+ return {
40
+ promise,
41
+ resolve,
42
+ reject,
43
+ };
44
+ }
45
+ exports.createFuture = createFuture;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  /// <reference types="node" />
6
6
  import { UIPagePrompt } from './stormClient';
7
- import { ReferenceClassification, StormEvent, StormEventPage, StormImage, UIShell } from './events';
7
+ import { StormEvent, StormEventPage, StormImage, UIShell } from './events';
8
8
  import { EventEmitter } from 'node:events';
9
9
  export interface ImagePrompt {
10
10
  name: string;
@@ -41,28 +41,11 @@ export declare class PageQueue extends EventEmitter {
41
41
  addPrompt(initialPrompt: InitialPrompt, conversationId?: string, overwrite?: boolean): Promise<void>;
42
42
  private getPrefix;
43
43
  private wrapPagePrompt;
44
- private addPageGenerator;
44
+ private processPageEventWithReferences;
45
45
  cancel(): void;
46
46
  wait(): Promise<void>;
47
- get length(): number;
48
47
  private addImagePrompt;
49
- }
50
- export declare class PageGenerator extends EventEmitter {
51
- private readonly eventQueue;
52
- private readonly conversationId;
53
- readonly prompt: UIPagePrompt;
54
- constructor(prompt: UIPagePrompt, conversationId?: string);
55
- on(event: 'event', listener: (data: StormEvent) => void): this;
56
- on(event: 'page_refs', listener: (data: {
57
- event: StormEventPage;
58
- references: ReferenceClassification[];
59
- }) => Promise<void>): this;
60
- emit(type: 'event', event: StormEvent): boolean;
61
- emit(type: 'page_refs', event: {
62
- event: StormEventPage;
63
- references: ReferenceClassification[];
64
- }): boolean;
65
- generate(): Promise<void>;
48
+ generate(prompt: UIPagePrompt, conversationId: string): Promise<void>;
66
49
  private resolveReferences;
67
50
  }
68
51
  export {};