@kapeta/local-cluster-service 0.58.2 → 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 +7 -0
- package/dist/cjs/src/storm/PromiseQueue.d.ts +16 -0
- package/dist/cjs/src/storm/PromiseQueue.js +64 -0
- package/dist/cjs/src/storm/routes.js +12 -4
- package/dist/esm/src/storm/PromiseQueue.d.ts +16 -0
- package/dist/esm/src/storm/PromiseQueue.js +64 -0
- package/dist/esm/src/storm/routes.js +12 -4
- package/package.json +1 -1
- package/src/storm/PromiseQueue.ts +86 -0
- package/src/storm/routes.ts +43 -30
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
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
|
+
|
1
8
|
## [0.58.2](https://github.com/kapetacom/local-cluster-service/compare/v0.58.1...v0.58.2) (2024-07-25)
|
2
9
|
|
3
10
|
|
@@ -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;
|
@@ -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({
|
@@ -88,7 +96,6 @@ router.post('/:handle/ui', async (req, res) => {
|
|
88
96
|
if (screenData.type === 'PAGE') {
|
89
97
|
screenData.payload.conversationId = innerConversationId;
|
90
98
|
}
|
91
|
-
console.log('Processing screen event', screenData);
|
92
99
|
sendEvent(res, screenData);
|
93
100
|
});
|
94
101
|
screenStream.on('end', () => {
|
@@ -96,9 +103,10 @@ router.post('/:handle/ui', async (req, res) => {
|
|
96
103
|
});
|
97
104
|
}
|
98
105
|
catch (e) {
|
106
|
+
console.error('Failed to process screen', e);
|
99
107
|
reject(e);
|
100
108
|
}
|
101
|
-
});
|
109
|
+
}));
|
102
110
|
});
|
103
111
|
}
|
104
112
|
catch (e) {
|
@@ -106,10 +114,10 @@ router.post('/:handle/ui', async (req, res) => {
|
|
106
114
|
}
|
107
115
|
});
|
108
116
|
await waitForStormStream(userJourneysStream);
|
117
|
+
await queue.wait();
|
109
118
|
if (userJourneysStream.isAborted()) {
|
110
119
|
return;
|
111
120
|
}
|
112
|
-
await Promise.all(Object.values(promises));
|
113
121
|
sendDone(res);
|
114
122
|
}
|
115
123
|
catch (err) {
|
@@ -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;
|
@@ -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({
|
@@ -88,7 +96,6 @@ router.post('/:handle/ui', async (req, res) => {
|
|
88
96
|
if (screenData.type === 'PAGE') {
|
89
97
|
screenData.payload.conversationId = innerConversationId;
|
90
98
|
}
|
91
|
-
console.log('Processing screen event', screenData);
|
92
99
|
sendEvent(res, screenData);
|
93
100
|
});
|
94
101
|
screenStream.on('end', () => {
|
@@ -96,9 +103,10 @@ router.post('/:handle/ui', async (req, res) => {
|
|
96
103
|
});
|
97
104
|
}
|
98
105
|
catch (e) {
|
106
|
+
console.error('Failed to process screen', e);
|
99
107
|
reject(e);
|
100
108
|
}
|
101
|
-
});
|
109
|
+
}));
|
102
110
|
});
|
103
111
|
}
|
104
112
|
catch (e) {
|
@@ -106,10 +114,10 @@ router.post('/:handle/ui', async (req, res) => {
|
|
106
114
|
}
|
107
115
|
});
|
108
116
|
await waitForStormStream(userJourneysStream);
|
117
|
+
await queue.wait();
|
109
118
|
if (userJourneysStream.isAborted()) {
|
110
119
|
return;
|
111
120
|
}
|
112
|
-
await Promise.all(Object.values(promises));
|
113
121
|
sendDone(res);
|
114
122
|
}
|
115
123
|
catch (err) {
|
package/package.json
CHANGED
@@ -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
|
+
}
|
package/src/storm/routes.ts
CHANGED
@@ -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,39 +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] =
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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);
|
115
137
|
}
|
116
|
-
|
117
|
-
|
118
|
-
});
|
119
|
-
screenStream.on('end', () => {
|
120
|
-
resolve();
|
121
|
-
});
|
122
|
-
} catch (e: any) {
|
123
|
-
reject(e);
|
124
|
-
}
|
125
|
-
});
|
138
|
+
})
|
139
|
+
);
|
126
140
|
});
|
127
141
|
} catch (e) {
|
128
142
|
console.error('Failed to process event', e);
|
@@ -130,13 +144,12 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
|
|
130
144
|
});
|
131
145
|
|
132
146
|
await waitForStormStream(userJourneysStream);
|
147
|
+
await queue.wait();
|
133
148
|
|
134
149
|
if (userJourneysStream.isAborted()) {
|
135
150
|
return;
|
136
151
|
}
|
137
152
|
|
138
|
-
await Promise.all(Object.values(promises));
|
139
|
-
|
140
153
|
sendDone(res);
|
141
154
|
} catch (err: any) {
|
142
155
|
sendError(err, res);
|