@kapeta/local-cluster-service 0.59.0 → 0.60.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 CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.60.0](https://github.com/kapetacom/local-cluster-service/compare/v0.59.0...v0.60.0) (2024-08-05)
2
+
3
+
4
+ ### Features
5
+
6
+ * Host ui pages on a temp server so we can have it on its own server ([#208](https://github.com/kapetacom/local-cluster-service/issues/208)) ([31ba4b8](https://github.com/kapetacom/local-cluster-service/commit/31ba4b8142c4618cc347037004f8fed87c69c6b7))
7
+
1
8
  # [0.59.0](https://github.com/kapetacom/local-cluster-service/compare/v0.58.6...v0.59.0) (2024-08-01)
2
9
 
3
10
 
@@ -14,9 +14,8 @@ declare class ClusterService {
14
14
  _findClusterServicePort(): Promise<void>;
15
15
  /**
16
16
  * Gets next available port
17
- * @return {Promise<number>}
18
17
  */
19
- getNextAvailablePort(): Promise<number>;
18
+ getNextAvailablePort(startPort?: number): Promise<number>;
20
19
  _checkIfPortIsUsed(port: number, host?: string): Promise<unknown>;
21
20
  /**
22
21
  * The port of this local cluster service itself
@@ -48,14 +48,23 @@ class ClusterService {
48
48
  }
49
49
  /**
50
50
  * Gets next available port
51
- * @return {Promise<number>}
52
51
  */
53
- async getNextAvailablePort() {
52
+ async getNextAvailablePort(startPort = -1) {
53
+ let receivedStartPort = startPort > 0;
54
+ if (!receivedStartPort) {
55
+ startPort = this._currentPort;
56
+ }
54
57
  while (true) {
55
- while (this._reservedPorts.indexOf(this._currentPort) > -1) {
56
- this._currentPort++;
58
+ while (this._reservedPorts.indexOf(startPort) > -1) {
59
+ startPort++;
60
+ if (!receivedStartPort) {
61
+ this._currentPort = startPort;
62
+ }
63
+ }
64
+ const nextPort = startPort++;
65
+ if (!receivedStartPort) {
66
+ this._currentPort = startPort;
57
67
  }
58
- const nextPort = this._currentPort++;
59
68
  const isUsed = await this._checkIfPortIsUsed(nextPort);
60
69
  if (!isUsed) {
61
70
  return nextPort;
@@ -0,0 +1,11 @@
1
+ import { StormEventPage } from './events';
2
+ export declare class UIServer {
3
+ private readonly express;
4
+ private readonly systemId;
5
+ private port;
6
+ private server;
7
+ constructor(systemId: string);
8
+ start(): Promise<void>;
9
+ close(): void;
10
+ resolveUrl(screenData: StormEventPage): string;
11
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.UIServer = void 0;
7
+ /**
8
+ * Copyright 2023 Kapeta Inc.
9
+ * SPDX-License-Identifier: BUSL-1.1
10
+ */
11
+ const express_1 = __importDefault(require("express"));
12
+ const page_utils_1 = require("./page-utils");
13
+ const clusterService_1 = require("../clusterService");
14
+ class UIServer {
15
+ express;
16
+ systemId;
17
+ port = 50000;
18
+ server;
19
+ constructor(systemId) {
20
+ this.systemId = systemId;
21
+ this.express = (0, express_1.default)();
22
+ }
23
+ async start() {
24
+ this.port = await clusterService_1.clusterService.getNextAvailablePort(this.port);
25
+ this.express.all('/*', async (req, res) => {
26
+ (0, page_utils_1.readPageFromDisk)(this.systemId, req.params[0], req.method, res);
27
+ });
28
+ return new Promise((resolve) => {
29
+ this.server = this.express.listen(this.port, () => {
30
+ console.log(`UI Server started on port ${this.port}`);
31
+ resolve();
32
+ });
33
+ });
34
+ }
35
+ close() {
36
+ if (this.server) {
37
+ console.log('UI Server closed on port: %s', this.port);
38
+ this.server.close();
39
+ this.server = undefined;
40
+ }
41
+ }
42
+ resolveUrl(screenData) {
43
+ const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
44
+ return `http://localhost:${this.port}${path}`;
45
+ }
46
+ }
47
+ exports.UIServer = UIServer;
@@ -21,13 +21,18 @@ const assetManager_1 = require("../assetManager");
21
21
  const node_uuid_1 = __importDefault(require("node-uuid"));
22
22
  const PromiseQueue_1 = require("./PromiseQueue");
23
23
  const page_utils_1 = require("./page-utils");
24
+ const UIServer_1 = require("./UIServer");
25
+ const UI_SERVERS = {};
24
26
  const router = (0, express_promise_router_1.default)();
25
27
  router.use('/', cors_1.corsHandler);
26
28
  router.use('/', stringBody_1.stringBody);
27
29
  function convertPageEvent(screenData, innerConversationId, mainConversationId) {
28
30
  if (screenData.type === 'PAGE') {
31
+ const server = UI_SERVERS[mainConversationId];
32
+ if (!server) {
33
+ console.warn('No server found for conversation', mainConversationId);
34
+ }
29
35
  screenData.payload.conversationId = innerConversationId;
30
- const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
31
36
  return {
32
37
  type: 'PAGE_URL',
33
38
  reason: screenData.reason,
@@ -40,7 +45,7 @@ function convertPageEvent(screenData, innerConversationId, mainConversationId) {
40
45
  description: screenData.payload.description,
41
46
  prompt: screenData.payload.prompt,
42
47
  path: screenData.payload.path,
43
- url: `/storm/ui/${mainConversationId}/serve/${screenData.payload.method}${path}`,
48
+ url: server ? server.resolveUrl(screenData) : '',
44
49
  method: screenData.payload.method,
45
50
  conversationId: innerConversationId,
46
51
  },
@@ -88,6 +93,18 @@ router.post('/ui/screen', async (req, res) => {
88
93
  }
89
94
  }
90
95
  });
96
+ router.delete('/:handle/ui', async (req, res) => {
97
+ const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
98
+ if (!conversationId) {
99
+ res.status(400).send('Missing conversation id');
100
+ return;
101
+ }
102
+ const server = UI_SERVERS[conversationId];
103
+ if (server) {
104
+ server.close();
105
+ delete UI_SERVERS[conversationId];
106
+ }
107
+ });
91
108
  router.post('/:handle/ui', async (req, res) => {
92
109
  const handle = req.params.handle;
93
110
  try {
@@ -105,6 +122,9 @@ router.post('/:handle/ui', async (req, res) => {
105
122
  onRequestAborted(req, res, () => {
106
123
  queue.cancel();
107
124
  });
125
+ const systemId = userJourneysStream.getConversationId();
126
+ UI_SERVERS[systemId] = new UIServer_1.UIServer(systemId);
127
+ await UI_SERVERS[systemId].start();
108
128
  userJourneysStream.on('data', async (data) => {
109
129
  try {
110
130
  console.log('Processing user journey event', data);
@@ -14,9 +14,8 @@ declare class ClusterService {
14
14
  _findClusterServicePort(): Promise<void>;
15
15
  /**
16
16
  * Gets next available port
17
- * @return {Promise<number>}
18
17
  */
19
- getNextAvailablePort(): Promise<number>;
18
+ getNextAvailablePort(startPort?: number): Promise<number>;
20
19
  _checkIfPortIsUsed(port: number, host?: string): Promise<unknown>;
21
20
  /**
22
21
  * The port of this local cluster service itself
@@ -48,14 +48,23 @@ class ClusterService {
48
48
  }
49
49
  /**
50
50
  * Gets next available port
51
- * @return {Promise<number>}
52
51
  */
53
- async getNextAvailablePort() {
52
+ async getNextAvailablePort(startPort = -1) {
53
+ let receivedStartPort = startPort > 0;
54
+ if (!receivedStartPort) {
55
+ startPort = this._currentPort;
56
+ }
54
57
  while (true) {
55
- while (this._reservedPorts.indexOf(this._currentPort) > -1) {
56
- this._currentPort++;
58
+ while (this._reservedPorts.indexOf(startPort) > -1) {
59
+ startPort++;
60
+ if (!receivedStartPort) {
61
+ this._currentPort = startPort;
62
+ }
63
+ }
64
+ const nextPort = startPort++;
65
+ if (!receivedStartPort) {
66
+ this._currentPort = startPort;
57
67
  }
58
- const nextPort = this._currentPort++;
59
68
  const isUsed = await this._checkIfPortIsUsed(nextPort);
60
69
  if (!isUsed) {
61
70
  return nextPort;
@@ -0,0 +1,11 @@
1
+ import { StormEventPage } from './events';
2
+ export declare class UIServer {
3
+ private readonly express;
4
+ private readonly systemId;
5
+ private port;
6
+ private server;
7
+ constructor(systemId: string);
8
+ start(): Promise<void>;
9
+ close(): void;
10
+ resolveUrl(screenData: StormEventPage): string;
11
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.UIServer = void 0;
7
+ /**
8
+ * Copyright 2023 Kapeta Inc.
9
+ * SPDX-License-Identifier: BUSL-1.1
10
+ */
11
+ const express_1 = __importDefault(require("express"));
12
+ const page_utils_1 = require("./page-utils");
13
+ const clusterService_1 = require("../clusterService");
14
+ class UIServer {
15
+ express;
16
+ systemId;
17
+ port = 50000;
18
+ server;
19
+ constructor(systemId) {
20
+ this.systemId = systemId;
21
+ this.express = (0, express_1.default)();
22
+ }
23
+ async start() {
24
+ this.port = await clusterService_1.clusterService.getNextAvailablePort(this.port);
25
+ this.express.all('/*', async (req, res) => {
26
+ (0, page_utils_1.readPageFromDisk)(this.systemId, req.params[0], req.method, res);
27
+ });
28
+ return new Promise((resolve) => {
29
+ this.server = this.express.listen(this.port, () => {
30
+ console.log(`UI Server started on port ${this.port}`);
31
+ resolve();
32
+ });
33
+ });
34
+ }
35
+ close() {
36
+ if (this.server) {
37
+ console.log('UI Server closed on port: %s', this.port);
38
+ this.server.close();
39
+ this.server = undefined;
40
+ }
41
+ }
42
+ resolveUrl(screenData) {
43
+ const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
44
+ return `http://localhost:${this.port}${path}`;
45
+ }
46
+ }
47
+ exports.UIServer = UIServer;
@@ -21,13 +21,18 @@ const assetManager_1 = require("../assetManager");
21
21
  const node_uuid_1 = __importDefault(require("node-uuid"));
22
22
  const PromiseQueue_1 = require("./PromiseQueue");
23
23
  const page_utils_1 = require("./page-utils");
24
+ const UIServer_1 = require("./UIServer");
25
+ const UI_SERVERS = {};
24
26
  const router = (0, express_promise_router_1.default)();
25
27
  router.use('/', cors_1.corsHandler);
26
28
  router.use('/', stringBody_1.stringBody);
27
29
  function convertPageEvent(screenData, innerConversationId, mainConversationId) {
28
30
  if (screenData.type === 'PAGE') {
31
+ const server = UI_SERVERS[mainConversationId];
32
+ if (!server) {
33
+ console.warn('No server found for conversation', mainConversationId);
34
+ }
29
35
  screenData.payload.conversationId = innerConversationId;
30
- const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
31
36
  return {
32
37
  type: 'PAGE_URL',
33
38
  reason: screenData.reason,
@@ -40,7 +45,7 @@ function convertPageEvent(screenData, innerConversationId, mainConversationId) {
40
45
  description: screenData.payload.description,
41
46
  prompt: screenData.payload.prompt,
42
47
  path: screenData.payload.path,
43
- url: `/storm/ui/${mainConversationId}/serve/${screenData.payload.method}${path}`,
48
+ url: server ? server.resolveUrl(screenData) : '',
44
49
  method: screenData.payload.method,
45
50
  conversationId: innerConversationId,
46
51
  },
@@ -88,6 +93,18 @@ router.post('/ui/screen', async (req, res) => {
88
93
  }
89
94
  }
90
95
  });
96
+ router.delete('/:handle/ui', async (req, res) => {
97
+ const conversationId = req.headers[stormClient_1.ConversationIdHeader.toLowerCase()];
98
+ if (!conversationId) {
99
+ res.status(400).send('Missing conversation id');
100
+ return;
101
+ }
102
+ const server = UI_SERVERS[conversationId];
103
+ if (server) {
104
+ server.close();
105
+ delete UI_SERVERS[conversationId];
106
+ }
107
+ });
91
108
  router.post('/:handle/ui', async (req, res) => {
92
109
  const handle = req.params.handle;
93
110
  try {
@@ -105,6 +122,9 @@ router.post('/:handle/ui', async (req, res) => {
105
122
  onRequestAborted(req, res, () => {
106
123
  queue.cancel();
107
124
  });
125
+ const systemId = userJourneysStream.getConversationId();
126
+ UI_SERVERS[systemId] = new UIServer_1.UIServer(systemId);
127
+ await UI_SERVERS[systemId].start();
108
128
  userJourneysStream.on('data', async (data) => {
109
129
  try {
110
130
  console.log('Processing user journey event', data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -56,15 +56,24 @@ class ClusterService {
56
56
 
57
57
  /**
58
58
  * Gets next available port
59
- * @return {Promise<number>}
60
59
  */
61
- async getNextAvailablePort() {
60
+ public async getNextAvailablePort(startPort: number = -1) {
61
+ let receivedStartPort = startPort > 0;
62
+ if (!receivedStartPort) {
63
+ startPort = this._currentPort;
64
+ }
62
65
  while (true) {
63
- while (this._reservedPorts.indexOf(this._currentPort) > -1) {
64
- this._currentPort++;
66
+ while (this._reservedPorts.indexOf(startPort) > -1) {
67
+ startPort++;
68
+ if (!receivedStartPort) {
69
+ this._currentPort = startPort;
70
+ }
65
71
  }
66
72
 
67
- const nextPort = this._currentPort++;
73
+ const nextPort = startPort++;
74
+ if (!receivedStartPort) {
75
+ this._currentPort = startPort;
76
+ }
68
77
  const isUsed = await this._checkIfPortIsUsed(nextPort);
69
78
  if (!isUsed) {
70
79
  return nextPort;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import express, { Express, Request, Response } from 'express';
6
+ import { readPageFromDisk } from './page-utils';
7
+ import { clusterService } from '../clusterService';
8
+ import { Server } from 'http';
9
+ import { StormEventPage } from './events';
10
+
11
+ export class UIServer {
12
+ private readonly express: Express;
13
+ private readonly systemId: string;
14
+
15
+ private port: number = 50000;
16
+ private server: Server | undefined;
17
+
18
+ constructor(systemId: string) {
19
+ this.systemId = systemId;
20
+ this.express = express();
21
+ }
22
+
23
+ public async start() {
24
+ this.port = await clusterService.getNextAvailablePort(this.port);
25
+
26
+ this.express.all('/*', async (req: Request, res: Response) => {
27
+ readPageFromDisk(this.systemId, req.params[0], req.method, res);
28
+ });
29
+
30
+ return new Promise<void>((resolve) => {
31
+ this.server = this.express.listen(this.port, () => {
32
+ console.log(`UI Server started on port ${this.port}`);
33
+ resolve();
34
+ });
35
+ });
36
+ }
37
+
38
+ public close() {
39
+ if (this.server) {
40
+ console.log('UI Server closed on port: %s', this.port);
41
+ this.server.close();
42
+ this.server = undefined;
43
+ }
44
+ }
45
+
46
+ resolveUrl(screenData: StormEventPage) {
47
+ const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
48
+ return `http://localhost:${this.port}${path}`;
49
+ }
50
+ }
@@ -26,7 +26,9 @@ import { assetManager } from '../assetManager';
26
26
  import uuid from 'node-uuid';
27
27
  import { PromiseQueue } from './PromiseQueue';
28
28
  import { readPageFromDisk, readPageFromDiskAsString, SystemIdHeader, writePageToDisk } from './page-utils';
29
+ import { UIServer } from './UIServer';
29
30
 
31
+ const UI_SERVERS: { [key: string]: UIServer } = {};
30
32
  const router = Router();
31
33
 
32
34
  router.use('/', corsHandler);
@@ -34,8 +36,11 @@ router.use('/', stringBody);
34
36
 
35
37
  function convertPageEvent(screenData: StormEvent, innerConversationId: string, mainConversationId: string): StormEvent {
36
38
  if (screenData.type === 'PAGE') {
39
+ const server: UIServer | undefined = UI_SERVERS[mainConversationId];
40
+ if (!server) {
41
+ console.warn('No server found for conversation', mainConversationId);
42
+ }
37
43
  screenData.payload.conversationId = innerConversationId;
38
- const path = screenData.payload.path.startsWith('/') ? screenData.payload.path : `/${screenData.payload.path}`;
39
44
  return {
40
45
  type: 'PAGE_URL',
41
46
  reason: screenData.reason,
@@ -48,7 +53,7 @@ function convertPageEvent(screenData: StormEvent, innerConversationId: string, m
48
53
  description: screenData.payload.description,
49
54
  prompt: screenData.payload.prompt,
50
55
  path: screenData.payload.path,
51
- url: `/storm/ui/${mainConversationId}/serve/${screenData.payload.method}${path}`,
56
+ url: server ? server.resolveUrl(screenData) : '',
52
57
  method: screenData.payload.method,
53
58
  conversationId: innerConversationId,
54
59
  },
@@ -106,6 +111,20 @@ router.post('/ui/screen', async (req: KapetaBodyRequest, res: Response) => {
106
111
  }
107
112
  });
108
113
 
114
+ router.delete('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
115
+ const conversationId = req.headers[ConversationIdHeader.toLowerCase()] as string | undefined;
116
+ if (!conversationId) {
117
+ res.status(400).send('Missing conversation id');
118
+ return;
119
+ }
120
+
121
+ const server = UI_SERVERS[conversationId];
122
+ if (server) {
123
+ server.close();
124
+ delete UI_SERVERS[conversationId];
125
+ }
126
+ });
127
+
109
128
  router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
110
129
  const handle = req.params.handle as string;
111
130
  try {
@@ -130,6 +149,11 @@ router.post('/:handle/ui', async (req: KapetaBodyRequest, res: Response) => {
130
149
  queue.cancel();
131
150
  });
132
151
 
152
+ const systemId = userJourneysStream.getConversationId();
153
+
154
+ UI_SERVERS[systemId] = new UIServer(systemId);
155
+ await UI_SERVERS[systemId].start();
156
+
133
157
  userJourneysStream.on('data', async (data: StormEvent) => {
134
158
  try {
135
159
  console.log('Processing user journey event', data);