@kapeta/local-cluster-service 0.58.1 → 0.58.3

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,17 @@
1
+ ## [0.58.3](https://github.com/kapetacom/local-cluster-service/compare/v0.58.2...v0.58.3) (2024-07-26)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Ensure that we can control concurrency in page generation ([#203](https://github.com/kapetacom/local-cluster-service/issues/203)) ([4a919e0](https://github.com/kapetacom/local-cluster-service/commit/4a919e013b20f10d4199927970fbe38091a2b858))
7
+
8
+ ## [0.58.2](https://github.com/kapetacom/local-cluster-service/compare/v0.58.1...v0.58.2) (2024-07-25)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Include title in journeys and pages ([16def8d](https://github.com/kapetacom/local-cluster-service/commit/16def8d726fb3b922ffc6934ce59c981996c3523))
14
+
1
15
  ## [0.58.1](https://github.com/kapetacom/local-cluster-service/compare/v0.58.0...v0.58.1) (2024-07-24)
2
16
 
3
17
 
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ export type Future<T> = () => Promise<T>;
6
+ export declare class PromiseQueue {
7
+ private readonly queue;
8
+ private readonly active;
9
+ private readonly maxConcurrency;
10
+ constructor(maxConcurrency?: number);
11
+ private toInternal;
12
+ add<T>(future: Future<T>): Promise<T>;
13
+ private next;
14
+ cancel(): void;
15
+ wait(): Promise<void>;
16
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PromiseQueue = void 0;
4
+ class PromiseQueue {
5
+ queue = [];
6
+ active = [];
7
+ // Maximum number of concurrent promises
8
+ maxConcurrency;
9
+ constructor(maxConcurrency = 5) {
10
+ this.maxConcurrency = maxConcurrency;
11
+ }
12
+ toInternal(future) {
13
+ let resolve = () => { };
14
+ let reject = () => { };
15
+ const promise = new Promise((res, rej) => {
16
+ resolve = res;
17
+ reject = rej;
18
+ });
19
+ return {
20
+ execute: future,
21
+ promise,
22
+ resolve,
23
+ reject,
24
+ };
25
+ }
26
+ add(future) {
27
+ const internal = this.toInternal(future);
28
+ this.queue.push(internal);
29
+ this.next();
30
+ return internal.promise;
31
+ }
32
+ next() {
33
+ if (this.active.length >= this.maxConcurrency) {
34
+ return false;
35
+ }
36
+ if (this.queue.length === 0) {
37
+ return false;
38
+ }
39
+ const future = this.queue.shift();
40
+ if (!future) {
41
+ return false;
42
+ }
43
+ const promise = future
44
+ .execute()
45
+ .then(future.resolve)
46
+ .catch(future.reject)
47
+ .finally(() => {
48
+ this.active.splice(this.active.indexOf(promise), 1);
49
+ this.next();
50
+ });
51
+ this.active.push(promise);
52
+ return this.next();
53
+ }
54
+ cancel() {
55
+ this.queue.splice(0, this.queue.length);
56
+ this.active.splice(0, this.active.length);
57
+ }
58
+ async wait() {
59
+ while (this.queue.length > 0 || this.active.length > 0) {
60
+ await Promise.all(this.active);
61
+ }
62
+ }
63
+ }
64
+ exports.PromiseQueue = PromiseQueue;
@@ -268,6 +268,7 @@ export interface StormEventPhases {
268
268
  }
269
269
  export interface Page {
270
270
  name: string;
271
+ title: string;
271
272
  description: string;
272
273
  content: string;
273
274
  path: string;
@@ -282,6 +283,7 @@ export interface StormEventPage {
282
283
  }
283
284
  export interface UserJourneyScreen {
284
285
  name: string;
286
+ title: string;
285
287
  filename: string;
286
288
  requirements: string;
287
289
  path: string;
@@ -19,6 +19,7 @@ const event_parser_1 = require("./event-parser");
19
19
  const codegen_1 = require("./codegen");
20
20
  const assetManager_1 = require("../assetManager");
21
21
  const node_uuid_1 = __importDefault(require("node-uuid"));
22
+ const PromiseQueue_1 = require("./PromiseQueue");
22
23
  const router = (0, express_promise_router_1.default)();
23
24
  router.use('/', cors_1.corsHandler);
24
25
  router.use('/', stringBody_1.stringBody);
@@ -61,6 +62,10 @@ router.post('/:handle/ui', async (req, res) => {
61
62
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
62
63
  res.set(stormClient_1.ConversationIdHeader, userJourneysStream.getConversationId());
63
64
  const promises = {};
65
+ const queue = new PromiseQueue_1.PromiseQueue(10);
66
+ onRequestAborted(req, res, () => {
67
+ queue.cancel();
68
+ });
64
69
  userJourneysStream.on('data', async (data) => {
65
70
  try {
66
71
  console.log('Processing user journey event', data);
@@ -68,11 +73,14 @@ router.post('/:handle/ui', async (req, res) => {
68
73
  if (data.type !== 'USER_JOURNEY') {
69
74
  return;
70
75
  }
76
+ if (userJourneysStream.isAborted()) {
77
+ return;
78
+ }
71
79
  data.payload.screens.forEach((screen) => {
72
80
  if (screen.name in promises) {
73
81
  return;
74
82
  }
75
- promises[screen.name] = new Promise(async (resolve, reject) => {
83
+ promises[screen.name] = queue.add(() => new Promise(async (resolve, reject) => {
76
84
  try {
77
85
  const innerConversationId = node_uuid_1.default.v4();
78
86
  const screenStream = await stormClient_1.stormClient.createUIPage({
@@ -81,13 +89,13 @@ router.post('/:handle/ui', async (req, res) => {
81
89
  path: screen.path,
82
90
  description: screen.requirements,
83
91
  name: screen.name,
92
+ title: screen.title,
84
93
  filename: screen.filename,
85
94
  }, innerConversationId);
86
95
  screenStream.on('data', (screenData) => {
87
96
  if (screenData.type === 'PAGE') {
88
97
  screenData.payload.conversationId = innerConversationId;
89
98
  }
90
- console.log('Processing screen event', screenData);
91
99
  sendEvent(res, screenData);
92
100
  });
93
101
  screenStream.on('end', () => {
@@ -95,9 +103,10 @@ router.post('/:handle/ui', async (req, res) => {
95
103
  });
96
104
  }
97
105
  catch (e) {
106
+ console.error('Failed to process screen', e);
98
107
  reject(e);
99
108
  }
100
- });
109
+ }));
101
110
  });
102
111
  }
103
112
  catch (e) {
@@ -105,10 +114,10 @@ router.post('/:handle/ui', async (req, res) => {
105
114
  }
106
115
  });
107
116
  await waitForStormStream(userJourneysStream);
117
+ await queue.wait();
108
118
  if (userJourneysStream.isAborted()) {
109
119
  return;
110
120
  }
111
- await Promise.all(Object.values(promises));
112
121
  sendDone(res);
113
122
  }
114
123
  catch (err) {
@@ -3,6 +3,7 @@ export declare const STORM_ID = "storm";
3
3
  export declare const ConversationIdHeader = "Conversation-Id";
4
4
  export interface UIPagePrompt {
5
5
  name: string;
6
+ title: string;
6
7
  filename: string;
7
8
  prompt: string;
8
9
  path: string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ export type Future<T> = () => Promise<T>;
6
+ export declare class PromiseQueue {
7
+ private readonly queue;
8
+ private readonly active;
9
+ private readonly maxConcurrency;
10
+ constructor(maxConcurrency?: number);
11
+ private toInternal;
12
+ add<T>(future: Future<T>): Promise<T>;
13
+ private next;
14
+ cancel(): void;
15
+ wait(): Promise<void>;
16
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PromiseQueue = void 0;
4
+ class PromiseQueue {
5
+ queue = [];
6
+ active = [];
7
+ // Maximum number of concurrent promises
8
+ maxConcurrency;
9
+ constructor(maxConcurrency = 5) {
10
+ this.maxConcurrency = maxConcurrency;
11
+ }
12
+ toInternal(future) {
13
+ let resolve = () => { };
14
+ let reject = () => { };
15
+ const promise = new Promise((res, rej) => {
16
+ resolve = res;
17
+ reject = rej;
18
+ });
19
+ return {
20
+ execute: future,
21
+ promise,
22
+ resolve,
23
+ reject,
24
+ };
25
+ }
26
+ add(future) {
27
+ const internal = this.toInternal(future);
28
+ this.queue.push(internal);
29
+ this.next();
30
+ return internal.promise;
31
+ }
32
+ next() {
33
+ if (this.active.length >= this.maxConcurrency) {
34
+ return false;
35
+ }
36
+ if (this.queue.length === 0) {
37
+ return false;
38
+ }
39
+ const future = this.queue.shift();
40
+ if (!future) {
41
+ return false;
42
+ }
43
+ const promise = future
44
+ .execute()
45
+ .then(future.resolve)
46
+ .catch(future.reject)
47
+ .finally(() => {
48
+ this.active.splice(this.active.indexOf(promise), 1);
49
+ this.next();
50
+ });
51
+ this.active.push(promise);
52
+ return this.next();
53
+ }
54
+ cancel() {
55
+ this.queue.splice(0, this.queue.length);
56
+ this.active.splice(0, this.active.length);
57
+ }
58
+ async wait() {
59
+ while (this.queue.length > 0 || this.active.length > 0) {
60
+ await Promise.all(this.active);
61
+ }
62
+ }
63
+ }
64
+ exports.PromiseQueue = PromiseQueue;
@@ -268,6 +268,7 @@ export interface StormEventPhases {
268
268
  }
269
269
  export interface Page {
270
270
  name: string;
271
+ title: string;
271
272
  description: string;
272
273
  content: string;
273
274
  path: string;
@@ -282,6 +283,7 @@ export interface StormEventPage {
282
283
  }
283
284
  export interface UserJourneyScreen {
284
285
  name: string;
286
+ title: string;
285
287
  filename: string;
286
288
  requirements: string;
287
289
  path: string;
@@ -19,6 +19,7 @@ const event_parser_1 = require("./event-parser");
19
19
  const codegen_1 = require("./codegen");
20
20
  const assetManager_1 = require("../assetManager");
21
21
  const node_uuid_1 = __importDefault(require("node-uuid"));
22
+ const PromiseQueue_1 = require("./PromiseQueue");
22
23
  const router = (0, express_promise_router_1.default)();
23
24
  router.use('/', cors_1.corsHandler);
24
25
  router.use('/', stringBody_1.stringBody);
@@ -61,6 +62,10 @@ router.post('/:handle/ui', async (req, res) => {
61
62
  res.set('Access-Control-Expose-Headers', stormClient_1.ConversationIdHeader);
62
63
  res.set(stormClient_1.ConversationIdHeader, userJourneysStream.getConversationId());
63
64
  const promises = {};
65
+ const queue = new PromiseQueue_1.PromiseQueue(10);
66
+ onRequestAborted(req, res, () => {
67
+ queue.cancel();
68
+ });
64
69
  userJourneysStream.on('data', async (data) => {
65
70
  try {
66
71
  console.log('Processing user journey event', data);
@@ -68,11 +73,14 @@ router.post('/:handle/ui', async (req, res) => {
68
73
  if (data.type !== 'USER_JOURNEY') {
69
74
  return;
70
75
  }
76
+ if (userJourneysStream.isAborted()) {
77
+ return;
78
+ }
71
79
  data.payload.screens.forEach((screen) => {
72
80
  if (screen.name in promises) {
73
81
  return;
74
82
  }
75
- promises[screen.name] = new Promise(async (resolve, reject) => {
83
+ promises[screen.name] = queue.add(() => new Promise(async (resolve, reject) => {
76
84
  try {
77
85
  const innerConversationId = node_uuid_1.default.v4();
78
86
  const screenStream = await stormClient_1.stormClient.createUIPage({
@@ -81,13 +89,13 @@ router.post('/:handle/ui', async (req, res) => {
81
89
  path: screen.path,
82
90
  description: screen.requirements,
83
91
  name: screen.name,
92
+ title: screen.title,
84
93
  filename: screen.filename,
85
94
  }, innerConversationId);
86
95
  screenStream.on('data', (screenData) => {
87
96
  if (screenData.type === 'PAGE') {
88
97
  screenData.payload.conversationId = innerConversationId;
89
98
  }
90
- console.log('Processing screen event', screenData);
91
99
  sendEvent(res, screenData);
92
100
  });
93
101
  screenStream.on('end', () => {
@@ -95,9 +103,10 @@ router.post('/:handle/ui', async (req, res) => {
95
103
  });
96
104
  }
97
105
  catch (e) {
106
+ console.error('Failed to process screen', e);
98
107
  reject(e);
99
108
  }
100
- });
109
+ }));
101
110
  });
102
111
  }
103
112
  catch (e) {
@@ -105,10 +114,10 @@ router.post('/:handle/ui', async (req, res) => {
105
114
  }
106
115
  });
107
116
  await waitForStormStream(userJourneysStream);
117
+ await queue.wait();
108
118
  if (userJourneysStream.isAborted()) {
109
119
  return;
110
120
  }
111
- await Promise.all(Object.values(promises));
112
121
  sendDone(res);
113
122
  }
114
123
  catch (err) {
@@ -3,6 +3,7 @@ export declare const STORM_ID = "storm";
3
3
  export declare const ConversationIdHeader = "Conversation-Id";
4
4
  export interface UIPagePrompt {
5
5
  name: string;
6
+ title: string;
6
7
  filename: string;
7
8
  prompt: string;
8
9
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.58.1",
3
+ "version": "0.58.3",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ export type Future<T> = () => Promise<T>;
6
+
7
+ type InternalFuture<T> = {
8
+ execute: Future<T>;
9
+ promise: Promise<T>;
10
+ resolve: (value: T) => void;
11
+ reject: (reason: any) => void;
12
+ };
13
+
14
+ export class PromiseQueue {
15
+ private readonly queue: InternalFuture<any>[] = [];
16
+ private readonly active: Promise<any>[] = [];
17
+
18
+ // Maximum number of concurrent promises
19
+ private readonly maxConcurrency: number;
20
+
21
+ constructor(maxConcurrency: number = 5) {
22
+ this.maxConcurrency = maxConcurrency;
23
+ }
24
+
25
+ private toInternal<T>(future: Future<T>): InternalFuture<T> {
26
+ let resolve: (value: T) => void = () => {};
27
+ let reject: (reason: any) => void = () => {};
28
+
29
+ const promise = new Promise<T>((res, rej) => {
30
+ resolve = res;
31
+ reject = rej;
32
+ });
33
+ return {
34
+ execute: future,
35
+ promise,
36
+ resolve,
37
+ reject,
38
+ };
39
+ }
40
+
41
+ public add<T>(future: Future<T>) {
42
+ const internal = this.toInternal<T>(future);
43
+ this.queue.push(internal);
44
+ this.next();
45
+
46
+ return internal.promise;
47
+ }
48
+
49
+ private next(): boolean {
50
+ if (this.active.length >= this.maxConcurrency) {
51
+ return false;
52
+ }
53
+ if (this.queue.length === 0) {
54
+ return false;
55
+ }
56
+
57
+ const future = this.queue.shift();
58
+ if (!future) {
59
+ return false;
60
+ }
61
+
62
+ const promise = future
63
+ .execute()
64
+ .then(future.resolve)
65
+ .catch(future.reject)
66
+ .finally(() => {
67
+ this.active.splice(this.active.indexOf(promise), 1);
68
+ this.next();
69
+ });
70
+
71
+ this.active.push(promise);
72
+
73
+ return this.next();
74
+ }
75
+
76
+ public cancel() {
77
+ this.queue.splice(0, this.queue.length);
78
+ this.active.splice(0, this.active.length);
79
+ }
80
+
81
+ public async wait() {
82
+ while (this.queue.length > 0 || this.active.length > 0) {
83
+ await Promise.all(this.active);
84
+ }
85
+ }
86
+ }
@@ -318,6 +318,7 @@ export interface StormEventPhases {
318
318
 
319
319
  export interface Page {
320
320
  name: string;
321
+ title: string;
321
322
  description: string;
322
323
  content: string;
323
324
  path: string;
@@ -335,6 +336,7 @@ export interface StormEventPage {
335
336
 
336
337
  export interface UserJourneyScreen {
337
338
  name: string;
339
+ title: string;
338
340
  filename: string;
339
341
  requirements: string;
340
342
  path: string;
@@ -24,6 +24,7 @@ import {
24
24
  import { StormCodegen } from './codegen';
25
25
  import { assetManager } from '../assetManager';
26
26
  import uuid from 'node-uuid';
27
+ import { PromiseQueue } from './PromiseQueue';
27
28
 
28
29
  const router = Router();
29
30
 
@@ -82,6 +83,11 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
82
83
 
83
84
  const promises: { [key: string]: Promise<void> } = {};
84
85
 
86
+ const queue = new PromiseQueue(10);
87
+ onRequestAborted(req, res, () => {
88
+ queue.cancel();
89
+ });
90
+
85
91
  userJourneysStream.on('data', async (data: StormEvent) => {
86
92
  try {
87
93
  console.log('Processing user journey event', data);
@@ -90,38 +96,47 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
90
96
  return;
91
97
  }
92
98
 
99
+ if (userJourneysStream.isAborted()) {
100
+ return;
101
+ }
102
+
93
103
  data.payload.screens.forEach((screen) => {
94
104
  if (screen.name in promises) {
95
105
  return;
96
106
  }
97
- promises[screen.name] = new Promise(async (resolve, reject) => {
98
- try {
99
- const innerConversationId = uuid.v4();
100
- const screenStream = await stormClient.createUIPage(
101
- {
102
- prompt: screen.requirements,
103
- method: screen.method,
104
- path: screen.path,
105
- description: screen.requirements,
106
- name: screen.name,
107
- filename: screen.filename,
108
- },
109
- innerConversationId
110
- );
111
- screenStream.on('data', (screenData: StormEvent) => {
112
- if (screenData.type === 'PAGE') {
113
- screenData.payload.conversationId = innerConversationId;
107
+ promises[screen.name] = queue.add(
108
+ () =>
109
+ new Promise(async (resolve, reject) => {
110
+ try {
111
+ const innerConversationId = uuid.v4();
112
+ const screenStream = await stormClient.createUIPage(
113
+ {
114
+ prompt: screen.requirements,
115
+ method: screen.method,
116
+ path: screen.path,
117
+ description: screen.requirements,
118
+ name: screen.name,
119
+ title: screen.title,
120
+ filename: screen.filename,
121
+ },
122
+ innerConversationId
123
+ );
124
+ screenStream.on('data', (screenData: StormEvent) => {
125
+ if (screenData.type === 'PAGE') {
126
+ screenData.payload.conversationId = innerConversationId;
127
+ }
128
+
129
+ sendEvent(res, screenData);
130
+ });
131
+ screenStream.on('end', () => {
132
+ resolve();
133
+ });
134
+ } catch (e: any) {
135
+ console.error('Failed to process screen', e);
136
+ reject(e);
114
137
  }
115
- console.log('Processing screen event', screenData);
116
- sendEvent(res, screenData);
117
- });
118
- screenStream.on('end', () => {
119
- resolve();
120
- });
121
- } catch (e: any) {
122
- reject(e);
123
- }
124
- });
138
+ })
139
+ );
125
140
  });
126
141
  } catch (e) {
127
142
  console.error('Failed to process event', e);
@@ -129,13 +144,12 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
129
144
  });
130
145
 
131
146
  await waitForStormStream(userJourneysStream);
147
+ await queue.wait();
132
148
 
133
149
  if (userJourneysStream.isAborted()) {
134
150
  return;
135
151
  }
136
152
 
137
- await Promise.all(Object.values(promises));
138
-
139
153
  sendDone(res);
140
154
  } catch (err: any) {
141
155
  sendError(err, res);
@@ -22,6 +22,7 @@ export const ConversationIdHeader = 'Conversation-Id';
22
22
 
23
23
  export interface UIPagePrompt {
24
24
  name: string;
25
+ title: string;
25
26
  filename: string;
26
27
  prompt: string;
27
28
  path: string;