lupine.api 1.0.41

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.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/admin/admin-about.tsx +16 -0
  4. package/admin/admin-config.tsx +44 -0
  5. package/admin/admin-css.tsx +3 -0
  6. package/admin/admin-db.tsx +74 -0
  7. package/admin/admin-frame-props.tsx +9 -0
  8. package/admin/admin-frame.tsx +466 -0
  9. package/admin/admin-index.tsx +66 -0
  10. package/admin/admin-login.tsx +99 -0
  11. package/admin/admin-menu-edit.tsx +637 -0
  12. package/admin/admin-menu-list.tsx +87 -0
  13. package/admin/admin-page-edit.tsx +564 -0
  14. package/admin/admin-page-list.tsx +83 -0
  15. package/admin/admin-performance.tsx +28 -0
  16. package/admin/admin-release.tsx +320 -0
  17. package/admin/admin-resources.tsx +385 -0
  18. package/admin/admin-shell.tsx +89 -0
  19. package/admin/admin-table-data.tsx +146 -0
  20. package/admin/admin-table-list.tsx +231 -0
  21. package/admin/admin-test-animations.tsx +379 -0
  22. package/admin/admin-test-component.tsx +808 -0
  23. package/admin/admin-test-edit.tsx +319 -0
  24. package/admin/admin-test-themes.tsx +56 -0
  25. package/admin/admin-tokens.tsx +338 -0
  26. package/admin/design/admin-design.tsx +174 -0
  27. package/admin/design/block-grid.tsx +36 -0
  28. package/admin/design/block-grid1.tsx +21 -0
  29. package/admin/design/block-paragraph.tsx +19 -0
  30. package/admin/design/block-title.tsx +19 -0
  31. package/admin/design/design-block-box.tsx +140 -0
  32. package/admin/design/drag-data.tsx +24 -0
  33. package/admin/index.ts +6 -0
  34. package/admin/package.json +15 -0
  35. package/admin/tsconfig.json +127 -0
  36. package/dev/copy-folder.js +32 -0
  37. package/dev/cp-index-html.js +69 -0
  38. package/dev/file-utils.js +12 -0
  39. package/dev/index.js +19 -0
  40. package/dev/package.json +12 -0
  41. package/dev/plugin-gen-versions.js +20 -0
  42. package/dev/plugin-ifelse.js +155 -0
  43. package/dev/plugin-ifelse.test.js +37 -0
  44. package/dev/run-cmd.js +14 -0
  45. package/dev/send-request.js +12 -0
  46. package/package.json +55 -0
  47. package/src/admin-api/admin-api.ts +59 -0
  48. package/src/admin-api/admin-auth.ts +87 -0
  49. package/src/admin-api/admin-config.ts +93 -0
  50. package/src/admin-api/admin-csv.ts +81 -0
  51. package/src/admin-api/admin-db.ts +269 -0
  52. package/src/admin-api/admin-helper.ts +111 -0
  53. package/src/admin-api/admin-menu.ts +135 -0
  54. package/src/admin-api/admin-page.ts +135 -0
  55. package/src/admin-api/admin-performance.ts +128 -0
  56. package/src/admin-api/admin-release.ts +498 -0
  57. package/src/admin-api/admin-resources.ts +318 -0
  58. package/src/admin-api/admin-token-helper.ts +79 -0
  59. package/src/admin-api/admin-tokens.ts +90 -0
  60. package/src/admin-api/index.ts +2 -0
  61. package/src/api/api-cache.ts +103 -0
  62. package/src/api/api-helper.ts +44 -0
  63. package/src/api/api-module.ts +60 -0
  64. package/src/api/api-router.ts +177 -0
  65. package/src/api/api-shared-storage.ts +64 -0
  66. package/src/api/async-storage.ts +5 -0
  67. package/src/api/debug-service.ts +56 -0
  68. package/src/api/encode-html.ts +27 -0
  69. package/src/api/handle-status.ts +71 -0
  70. package/src/api/index.ts +16 -0
  71. package/src/api/mini-web-socket.ts +270 -0
  72. package/src/api/server-content-type.ts +82 -0
  73. package/src/api/server-render.ts +216 -0
  74. package/src/api/shell-service.ts +66 -0
  75. package/src/api/simple-storage.ts +80 -0
  76. package/src/api/static-server.ts +125 -0
  77. package/src/api/to-client-delivery.ts +26 -0
  78. package/src/app/app-cache.ts +55 -0
  79. package/src/app/app-loader.ts +62 -0
  80. package/src/app/app-message.ts +60 -0
  81. package/src/app/app-shared-storage.ts +317 -0
  82. package/src/app/app-start.ts +117 -0
  83. package/src/app/cleanup-exit.ts +12 -0
  84. package/src/app/host-to-path.ts +38 -0
  85. package/src/app/index.ts +11 -0
  86. package/src/app/process-dev-requests.ts +90 -0
  87. package/src/app/web-listener.ts +230 -0
  88. package/src/app/web-processor.ts +42 -0
  89. package/src/app/web-server.ts +86 -0
  90. package/src/common-js/web-env.js +104 -0
  91. package/src/index.ts +7 -0
  92. package/src/lang/api-lang-en.ts +27 -0
  93. package/src/lang/api-lang-zh-cn.ts +28 -0
  94. package/src/lang/index.ts +2 -0
  95. package/src/lang/lang-helper.ts +76 -0
  96. package/src/lang/lang-props.ts +6 -0
  97. package/src/lib/db/db-helper.ts +23 -0
  98. package/src/lib/db/db-mysql.ts +250 -0
  99. package/src/lib/db/db-sqlite.ts +101 -0
  100. package/src/lib/db/db.spec.ts +28 -0
  101. package/src/lib/db/db.ts +304 -0
  102. package/src/lib/db/index.ts +5 -0
  103. package/src/lib/index.ts +3 -0
  104. package/src/lib/logger.spec.ts +214 -0
  105. package/src/lib/logger.ts +274 -0
  106. package/src/lib/runtime-require.ts +37 -0
  107. package/src/lib/utils/cookie-util.ts +34 -0
  108. package/src/lib/utils/crypto.ts +58 -0
  109. package/src/lib/utils/date-utils.ts +317 -0
  110. package/src/lib/utils/deep-merge.ts +37 -0
  111. package/src/lib/utils/delay.ts +12 -0
  112. package/src/lib/utils/file-setting.ts +55 -0
  113. package/src/lib/utils/format-bytes.ts +11 -0
  114. package/src/lib/utils/fs-utils.ts +144 -0
  115. package/src/lib/utils/get-env.ts +27 -0
  116. package/src/lib/utils/index.ts +12 -0
  117. package/src/lib/utils/is-type.ts +48 -0
  118. package/src/lib/utils/load-env.ts +14 -0
  119. package/src/lib/utils/pad.ts +6 -0
  120. package/src/models/api-base.ts +5 -0
  121. package/src/models/api-module-props.ts +11 -0
  122. package/src/models/api-router-props.ts +26 -0
  123. package/src/models/app-cache-props.ts +33 -0
  124. package/src/models/app-data-props.ts +10 -0
  125. package/src/models/app-loader-props.ts +6 -0
  126. package/src/models/app-shared-storage-props.ts +37 -0
  127. package/src/models/app-start-props.ts +18 -0
  128. package/src/models/async-storage-props.ts +13 -0
  129. package/src/models/db-config.ts +30 -0
  130. package/src/models/host-to-path-props.ts +12 -0
  131. package/src/models/index.ts +16 -0
  132. package/src/models/json-object.ts +8 -0
  133. package/src/models/locals-props.ts +36 -0
  134. package/src/models/logger-props.ts +84 -0
  135. package/src/models/simple-storage-props.ts +14 -0
  136. package/src/models/to-client-delivery-props.ts +6 -0
  137. package/tsconfig.json +115 -0
@@ -0,0 +1,62 @@
1
+ import { Logger } from '../lib';
2
+ import path from 'path';
3
+ import { appCache } from './app-cache';
4
+ import { AppCacheKeys, AppLoaderProps, HostToPathProps, IApiModule, setAppCache } from '../models';
5
+ import { appStorage } from './app-shared-storage';
6
+
7
+ class AppLoader {
8
+ logger: Logger = new Logger('app-loader');
9
+
10
+ constructor() {}
11
+
12
+ async loadApi(config: AppLoaderProps) {
13
+ // const apps: Set<string> = new Set();
14
+ // for (let key in config.webHostMap) {
15
+ // // one app may be defined for multiple hosts, but Set only stores unique values
16
+ // apps.add(config.webHostMap[key].appName);
17
+ // config.webHostMap[key].webPath = path.join(config.serverRoot, config.webHostMap[key].appName + '_web');
18
+ // config.webHostMap[key].dataPath = path.join(config.serverRoot, config.webHostMap[key].appName + '_data');
19
+ // config.webHostMap[key].apiPath = path.join(config.serverRoot, config.webHostMap[key].appName + '_api');
20
+ // }
21
+
22
+ for (let appConfig of config.webHostMap) {
23
+ await this.callInitApi(appConfig);
24
+ appCache.set(appConfig.appName, AppCacheKeys.API_CONFIG, appConfig);
25
+ }
26
+ }
27
+
28
+ async callInitApi(appConfig: HostToPathProps) {
29
+ const apiPath = path.join(process.cwd(), 'dist/server_root', appConfig.appName + '_api/index.js');
30
+ try {
31
+ // const module = await import(apiPath);
32
+ const module = require(apiPath);
33
+ this.logger.debug(`========= ${appConfig.appName} apiModule: `, module);
34
+
35
+ if (module && module.apiModule && typeof module.apiModule.initApi === 'function') {
36
+ const apiModule = module.apiModule as IApiModule;
37
+ appCache.set(appConfig.appName, AppCacheKeys.API_MODULE, apiModule);
38
+
39
+ // getAppCache should be only called inside api scope, but set it in app scope in case it's used
40
+ setAppCache(appCache);
41
+ // setAppStorage(appStorage);
42
+ await apiModule.initApi(appConfig, appCache, appStorage);
43
+ }
44
+ } catch (err: any) {
45
+ this.logger.error(`appName: ${appConfig.appName}, load api error: `, err);
46
+ }
47
+ }
48
+
49
+ async refreshApi(appConfig: HostToPathProps) {
50
+ // TODO: call unloadApi?
51
+ const apiPath = path.join(process.cwd(), 'dist/server_root', appConfig.appName + '_api/index.js');
52
+ for (const path in require.cache) {
53
+ if (path.endsWith('.js') && path.indexOf(apiPath) <= 0) {
54
+ console.log(`clear cache: ${path}`);
55
+ delete require.cache[path];
56
+ }
57
+ }
58
+ await this.callInitApi(appConfig);
59
+ }
60
+ }
61
+
62
+ export const appLoader = /* @__PURE__ */ new AppLoader();
@@ -0,0 +1,60 @@
1
+ import cluster from 'cluster';
2
+ import { Logger, LogWriter, LogWriterMessageId } from '../lib';
3
+ import { processDebugMessage } from './process-dev-requests';
4
+ import { appStorage } from './app-shared-storage';
5
+ import { AppSharedStorageMessageId } from '../models';
6
+ import { cleanupAndExit } from './cleanup-exit';
7
+
8
+ const logger = new Logger('app-message');
9
+ // send msg to all clients
10
+ const broadcast = (msgObject: any) => {
11
+ for (let i in cluster.workers) {
12
+ if (cluster.workers[i]) cluster.workers[i].send(msgObject);
13
+ }
14
+ };
15
+
16
+ // this is a worker and msg is from Primary
17
+ // when debug is on, it's in primary, but it shouldn't receive those msgs
18
+ export const processMessageFromPrimary = (msgObject: any) => {
19
+ if (!msgObject || !msgObject.id) {
20
+ logger.warn(`Unknown message from master in work: ${cluster.worker?.id}`);
21
+ return;
22
+ }
23
+
24
+ if (msgObject.id == 'debug') {
25
+ processDebugMessage(msgObject);
26
+ } else if (msgObject.id == AppSharedStorageMessageId) {
27
+ appStorage.messageFromPrimaryProcess(msgObject);
28
+ } else {
29
+ logger.warn(`Unknown message: ${msgObject.id}`);
30
+ }
31
+ };
32
+
33
+ // this is primary, msg is from a client
34
+ export const processMessageFromWorker = (msgObject: any) => {
35
+ if (!msgObject || !msgObject.id) {
36
+ if (msgObject['watch:require']) return;
37
+ logger.warn(`Unknown message from work: ${cluster.worker?.id}`);
38
+ return;
39
+ }
40
+
41
+ if (msgObject.id == LogWriterMessageId) {
42
+ LogWriter.messageFromSubProcess(msgObject);
43
+ } else if (msgObject.id == AppSharedStorageMessageId) {
44
+ appStorage.messageFromSubProcess(msgObject);
45
+ } else if (msgObject.id == 'debug') {
46
+ logger.debug(
47
+ `Message from worker ${cluster.worker?.id}, message: ${msgObject.message}, appName: ${msgObject.appName}`
48
+ );
49
+ broadcast(msgObject);
50
+ // if it's suspend, the primary process will exit
51
+ if (msgObject.message === 'suspend') {
52
+ setTimeout(() => {
53
+ console.log(`[server primary] Received suspend command.`, cluster.workers);
54
+ cleanupAndExit();
55
+ }, 100);
56
+ }
57
+ } else {
58
+ logger.warn(`Unknown message: ${msgObject.id}`);
59
+ }
60
+ };
@@ -0,0 +1,317 @@
1
+ /**
2
+ * A persistent storage for the Api
3
+ */
4
+ import * as fs from 'fs/promises';
5
+ import * as path from 'path';
6
+ import cluster from 'cluster';
7
+ import { SimpleStorage } from '../api';
8
+ import { FsUtils, Logger } from '../lib';
9
+ import {
10
+ SimpleStorageDataProps,
11
+ AppSharedStorageApiPrefix,
12
+ AppSharedStorageMessageId,
13
+ AppSharedStorageWebPrefix,
14
+ IAppSharedStorage,
15
+ StorageMessageFromSubProcess,
16
+ } from '../models';
17
+
18
+ // in Api scope, use ApiSharedStorage instead of this
19
+ // storage cross clusters, loaded when start and saved before exist
20
+ export class AppSharedStorage implements IAppSharedStorage {
21
+ private static instance: IAppSharedStorage;
22
+ configMap: { [key: string]: { fPath: string; storage: SimpleStorage } } = {};
23
+ logger = new Logger('server-config');
24
+
25
+ private constructor() {}
26
+
27
+ public static getInstance(): IAppSharedStorage {
28
+ if (!AppSharedStorage.instance) {
29
+ AppSharedStorage.instance = new AppSharedStorage();
30
+ }
31
+ return AppSharedStorage.instance;
32
+ }
33
+
34
+ private getWorker(workerId: number) {
35
+ for (let i in cluster.workers) {
36
+ if (cluster.workers[i]?.id === workerId) return cluster.workers[i];
37
+ }
38
+ }
39
+
40
+ private getStorageMap(appName: string) {
41
+ let storage = this.configMap[appName];
42
+ if (!storage) {
43
+ this.configMap[appName] = { fPath: '', storage: new SimpleStorage({}) };
44
+ storage = this.configMap[appName];
45
+ }
46
+ return storage;
47
+ }
48
+ private getStorage(appName: string): SimpleStorage {
49
+ return this.getStorageMap(appName).storage;
50
+ }
51
+
52
+ // this is primary, msg is from a client
53
+ messageFromSubProcess(msgObject: StorageMessageFromSubProcess) {
54
+ if (!cluster.isPrimary || !msgObject.action || !msgObject.key || !msgObject.appName || !msgObject.workerId) {
55
+ console.error('AppStorage got wrong message: ', msgObject);
56
+ return;
57
+ }
58
+
59
+ if (msgObject.action === 'get') {
60
+ const storage = this.getStorage(msgObject.appName);
61
+ const value = storage.get(msgObject.key, '');
62
+ // console.log(`AppStorage get value from ${msgObject.pid} in ${process.pid}, for key: ${msgObject.key}`);
63
+ // send message back to the worker from the worker id
64
+ const worker = this.getWorker(msgObject.workerId);
65
+ if (!worker) {
66
+ console.error("AppStorage can' find the worker: ", msgObject);
67
+ return;
68
+ }
69
+ worker.send({
70
+ value,
71
+ ...msgObject,
72
+ });
73
+ } else if (msgObject.action === 'getWithPrefix') {
74
+ const storage = this.getStorage(msgObject.appName);
75
+ const value = storage.getWithPrefix(msgObject.key);
76
+ // console.log(`AppStorage getWithPrefix from ${msgObject.pid} in ${process.pid}, for key: ${msgObject.key}`);
77
+ // send message back to the worker from the worker id
78
+ const worker = this.getWorker(msgObject.workerId);
79
+ if (!worker) {
80
+ console.error("AppStorage can' find the worker: ", msgObject);
81
+ return;
82
+ }
83
+ worker.send({
84
+ value: JSON.stringify(value),
85
+ ...msgObject,
86
+ });
87
+ } else if (msgObject.action === 'set') {
88
+ const storage = this.getStorage(msgObject.appName);
89
+ storage.set(msgObject.key, msgObject.value);
90
+ } else if (msgObject.action === 'save') {
91
+ this.save(msgObject.appName);
92
+ } else {
93
+ this.logger.warn(`Unknown message: ${msgObject.action}`);
94
+ }
95
+ }
96
+
97
+ // this is a worker and msg is from Primary
98
+ // when debug is on, it's in primary, but it shouldn't receive those msgs
99
+ // mainly for get (a worker requests a get, primaary sends the value back to here)
100
+ messageFromPrimaryProcess(msgObject: StorageMessageFromSubProcess) {
101
+ AppSharedStorageWorker.messageFromPrimaryProcess(msgObject);
102
+ }
103
+
104
+ // should be only called from primary when the app is starting
105
+ async load(appName: string, rootPath: string) {
106
+ if (!cluster.isPrimary) {
107
+ throw new Error('AppStorage.load should be only called from primary');
108
+ }
109
+
110
+ const map = this.getStorageMap(appName);
111
+ map.fPath = path.join(rootPath, 'config.json');
112
+
113
+ let tempPath = map.fPath;
114
+ try {
115
+ if (!(await FsUtils.pathExist(tempPath))) {
116
+ tempPath = path.join(rootPath, 'resources', 'config_default.json');
117
+ }
118
+
119
+ let json;
120
+ if ((json = await fs.readFile(tempPath, 'utf-8'))) {
121
+ // it will still save to map.fPath
122
+ map.storage.setContent(JSON.parse(json));
123
+ map.storage.Dirty = false;
124
+ this.logger.info(`Loading shared storage for [ ${appName} ] from ${tempPath}`);
125
+ }
126
+ } catch (e: any) {
127
+ this.logger.error('Loading json file failed: ' + tempPath, e.message);
128
+ }
129
+ }
130
+
131
+ // called from primary before exit, or from api to save changes
132
+ async save(appName?: string) {
133
+ if (!cluster.isPrimary) {
134
+ AppSharedStorageWorker.save(appName);
135
+ return;
136
+ }
137
+
138
+ if (appName) {
139
+ const map = this.configMap[appName];
140
+ if (map && map.fPath && map.storage.size() > 0 && map.storage.Dirty) {
141
+ await map.storage.saveContent(map.fPath);
142
+ }
143
+ } else {
144
+ // save all data
145
+ for (let appName in this.configMap) {
146
+ const map = this.configMap[appName];
147
+ if (map && map.fPath && map.storage.size() > 0 && map.storage.Dirty) {
148
+ await map.storage.saveContent(map.fPath);
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ // this can be called in primary or worker
155
+ get(appName: string, key: string): Promise<string> {
156
+ return new Promise((resolve, reject) => {
157
+ console.log(`AppStorage get value start in ${process.pid}, for key: ${key}`);
158
+
159
+ if (!cluster.isPrimary) {
160
+ AppSharedStorageWorker.get(appName, key, resolve, reject);
161
+ return;
162
+ }
163
+
164
+ // in primary
165
+ resolve(this.getStorage(appName).get(key, ''));
166
+ });
167
+ }
168
+ getWeb(appName: string, key: string): Promise<string> {
169
+ return this.get(appName, AppSharedStorageWebPrefix + key);
170
+ }
171
+ getApi(appName: string, key: string): Promise<string> {
172
+ return this.get(appName, AppSharedStorageApiPrefix + key);
173
+ }
174
+ getWebAll(appName: string): Promise<SimpleStorageDataProps> {
175
+ return this.getWithPrefix(appName, AppSharedStorageWebPrefix);
176
+ }
177
+ getWithPrefix(appName: string, prefixKey: string): Promise<SimpleStorageDataProps> {
178
+ return new Promise((resolve, reject) => {
179
+ console.log(`AppStorage getWithPrefix start in ${process.pid}, for prefixKey: ${prefixKey}`);
180
+
181
+ if (!cluster.isPrimary) {
182
+ AppSharedStorageWorker.getWithPrefix(appName, prefixKey, resolve, reject);
183
+ return;
184
+ }
185
+
186
+ // in primary
187
+ resolve(this.getStorage(appName).getWithPrefix(prefixKey));
188
+ });
189
+ }
190
+
191
+ // this can be called in primary or worker
192
+ set(appName: string, key: string, value: any) {
193
+ if (!cluster.isPrimary) {
194
+ AppSharedStorageWorker.set(appName, key, value);
195
+ return;
196
+ }
197
+
198
+ // in primary
199
+ this.getStorage(appName).set(key, value);
200
+ }
201
+ setWeb(appName: string, key: string, value: any) {
202
+ return this.set(appName, AppSharedStorageWebPrefix + key, value);
203
+ }
204
+ setApi(appName: string, key: string, value: any) {
205
+ return this.set(appName, AppSharedStorageApiPrefix + key, value);
206
+ }
207
+ }
208
+
209
+ type AppSharedStorageWorkerMap = { [key: string]: any };
210
+ class AppSharedStorageWorker {
211
+ static handleMap: AppSharedStorageWorkerMap = {};
212
+ static logger = new Logger('server-config');
213
+
214
+ static messageFromPrimaryProcess(msgObject: StorageMessageFromSubProcess) {
215
+ if (cluster.isPrimary || !msgObject.action || !msgObject.key || !msgObject.uniqueKey) {
216
+ console.error('AppSharedStorageWorker got wrong message: ', msgObject);
217
+ return;
218
+ }
219
+
220
+ if (msgObject.action === 'get') {
221
+ console.log(`AppStorage get value end in process: ${process.pid}, for key: ${msgObject.key}`);
222
+
223
+ const value = msgObject.value;
224
+ // how to pass the value to the caller
225
+ const map = this.handleMap[msgObject.uniqueKey];
226
+ delete this.handleMap[msgObject.uniqueKey];
227
+ if (map) {
228
+ map.resolve(value);
229
+ } else {
230
+ throw new Error(`Unknown uniqueKey: ${msgObject.uniqueKey}`);
231
+ }
232
+ } else if (msgObject.action === 'getWithPrefix') {
233
+ console.log(`AppStorage get value end in process: ${process.pid}, for key: ${msgObject.key}`);
234
+ const value = JSON.parse(msgObject.value);
235
+ // how to pass the value to the caller
236
+ const map = this.handleMap[msgObject.uniqueKey];
237
+ delete this.handleMap[msgObject.uniqueKey];
238
+ if (map) {
239
+ map.resolve(value);
240
+ } else {
241
+ throw new Error(`Unknown uniqueKey: ${msgObject.uniqueKey}`);
242
+ }
243
+ } else {
244
+ this.logger.warn(`Unknown message: ${msgObject.action}`);
245
+ }
246
+ }
247
+
248
+ static async save(appName?: string) {
249
+ if (cluster.isPrimary) {
250
+ throw new Error('AppSharedStorageWorker should be only called from workers');
251
+ }
252
+ const obj: StorageMessageFromSubProcess = {
253
+ id: AppSharedStorageMessageId,
254
+ pid: process.pid,
255
+ workerId: cluster.worker?.id || 0,
256
+ action: 'save',
257
+ appName: appName || '',
258
+ key: '',
259
+ };
260
+ process.send!(obj);
261
+ }
262
+
263
+ static get(appName: string, key: string, resolve: (value: any) => void, reject: (reason: any) => void) {
264
+ if (cluster.isPrimary) {
265
+ throw new Error('AppSharedStorageWorker should be only called from workers');
266
+ }
267
+ const uniqueKey = key + ':' + crypto.randomUUID();
268
+ AppSharedStorageWorker.handleMap[uniqueKey] = { resolve, reject };
269
+ const obj: StorageMessageFromSubProcess = {
270
+ id: AppSharedStorageMessageId,
271
+ pid: process.pid,
272
+ workerId: cluster.worker?.id || 0,
273
+ action: 'get',
274
+ appName,
275
+ uniqueKey,
276
+ key,
277
+ };
278
+ process.send!(obj);
279
+ }
280
+
281
+ static getWithPrefix(appName: string, prefixKey: string, resolve: (value: any) => void, reject: (reason: any) => void) {
282
+ if (cluster.isPrimary) {
283
+ throw new Error('AppSharedStorageWorker should be only called from workers');
284
+ }
285
+ const uniqueKey = prefixKey + ':' + crypto.randomUUID();
286
+ AppSharedStorageWorker.handleMap[uniqueKey] = { resolve, reject };
287
+ const obj: StorageMessageFromSubProcess = {
288
+ id: AppSharedStorageMessageId,
289
+ pid: process.pid,
290
+ workerId: cluster.worker?.id || 0,
291
+ action: 'getWithPrefix',
292
+ appName,
293
+ uniqueKey,
294
+ key: prefixKey,
295
+ };
296
+ process.send!(obj);
297
+ }
298
+
299
+ static set(appName: string, key: string, value: any) {
300
+ if (cluster.isPrimary) {
301
+ throw new Error('AppSharedStorageWorker should be only called from workers');
302
+ }
303
+ const obj: StorageMessageFromSubProcess = {
304
+ id: AppSharedStorageMessageId,
305
+ pid: process.pid,
306
+ workerId: cluster.worker?.id || 0,
307
+ action: 'set',
308
+ appName,
309
+ key,
310
+ value,
311
+ };
312
+ process.send!(obj);
313
+ }
314
+ }
315
+
316
+ // this can be used in app, but in api, it should use getAppStorage()
317
+ export const appStorage = /* @__PURE__ */ AppSharedStorage.getInstance();
@@ -0,0 +1,117 @@
1
+ import cluster from 'cluster';
2
+ import { WebProcessor } from './web-processor';
3
+ import { appLoader } from './app-loader';
4
+ import { processMessageFromPrimary, processMessageFromWorker } from './app-message';
5
+ import { WebServer } from './web-server';
6
+ import { processDevRequests } from './process-dev-requests';
7
+ import { appCache } from './app-cache';
8
+ import { AppStartProps, InitStartProps, AppCacheGlobal, AppCacheKeys } from '../models';
9
+ import { appStorage } from './app-shared-storage';
10
+ import { HostToPath } from './host-to-path';
11
+ import { cleanupAndExit } from './cleanup-exit';
12
+
13
+ // Don't use logger before set process message
14
+ class AppStart {
15
+ debug: boolean = false;
16
+ webServer: WebServer | undefined;
17
+
18
+ getWorkerId() {
19
+ return cluster.worker ? cluster.worker.id : 0;
20
+ }
21
+
22
+ async start(props: AppStartProps, webServer?: WebServer) {
23
+ this.debug = props.debug;
24
+ this.bindProcess();
25
+
26
+ appCache.set(AppCacheGlobal, AppCacheKeys.APP_DEBUG, props.debug);
27
+ // appCache.set(AppCacheGlobal, AppCacheKeys.APP_ENV_FILE, props.appEnvFile);
28
+ appCache.set(AppCacheGlobal, AppCacheKeys.START_TIME, new Date());
29
+ appCache.set(AppCacheGlobal, AppCacheKeys.RENDER_PAGE_FUNCTIONS, props.renderPageFunctions);
30
+ const appsList = props.apiConfig.webHostMap.map((item) => item.appName);
31
+ appCache.set(AppCacheGlobal, AppCacheKeys.APP_LIST, appsList);
32
+
33
+ this.webServer = webServer || new WebServer();
34
+
35
+ // call the Logger after initLog
36
+ console.log(
37
+ `${cluster.isPrimary ? 'Primary Process' : 'Worker Process'}, Starting Server - process id ${
38
+ process.pid
39
+ }, path: ${process.cwd()}`
40
+ );
41
+
42
+ // when it's cluster.isPrimary or props.debug, initialize the shared storage first
43
+ if (cluster.isPrimary) {
44
+ for (let appConfig of props.apiConfig.webHostMap) {
45
+ await appStorage.load(appConfig.appName, appConfig.dataPath);
46
+ }
47
+ }
48
+
49
+ if (props.debug || !cluster.isPrimary) {
50
+ console.log(`Worker id ${this.getWorkerId()}`);
51
+
52
+ process.on('message', processMessageFromPrimary);
53
+
54
+ HostToPath.setHostToPathList(props.apiConfig.webHostMap);
55
+ appLoader.loadApi(props.apiConfig);
56
+ this.initServer(props.serverConfig);
57
+ } else if (cluster.isPrimary) {
58
+ const numCPUs = require('os').cpus().length;
59
+ console.log(`Master Process is trying to fork ${numCPUs} processes`);
60
+
61
+ for (let i = 0; i < numCPUs; i++) {
62
+ let worker = cluster.fork();
63
+ worker.on('message', processMessageFromWorker);
64
+ }
65
+
66
+ cluster.on('death', (worker: any) => {
67
+ console.log(`Worker ${worker.pid} died; starting a new one...`);
68
+ cluster.fork();
69
+ });
70
+ }
71
+ }
72
+
73
+ bindProcess() {
74
+ if (cluster.isPrimary) {
75
+ // it looks like the child processes are hung up here
76
+ process.stdin.resume(); // so the program will not close instantly
77
+ }
78
+ // Emitted whenever a no-error-handler Promise is rejected
79
+ process.on('unhandledRejection', (reason: string, promise) => {
80
+ console.error(`${process.pid} - Process on unhandledRejection, promise: `, promise, ', reason: ', reason);
81
+ });
82
+
83
+ // do something when app is closing
84
+ process.on('beforeExit', async () => {
85
+ cleanupAndExit();
86
+ });
87
+ process.on('exit', (ret) => {
88
+ console.log(`${process.pid} - Process on exit, code: ${ret}`);
89
+ });
90
+ // catches uncaught exceptions
91
+ process.on('uncaughtException', (err: Error) => {
92
+ console.error(`${process.pid} - Process on uncaughtException: `, err);
93
+ console.error(err.stack);
94
+ });
95
+ // catches ctrl+c event and others
96
+ ['SIGTERM', 'SIGHUP', 'SIGINT', 'SIGINT', 'SIGBREAK'].forEach((evt) => process.on(evt, cleanupAndExit));
97
+ }
98
+
99
+ async initServer(config: InitStartProps) {
100
+ const bindIp = config.bindIp || '::';
101
+ const httpPort = config.httpPort;
102
+ const httpsPort = config.httpsPort;
103
+ const sslKeyPath = config.sslKeyPath || '';
104
+ const sslCrtPath = config.sslCrtPath || '';
105
+
106
+ console.log(`Starting Web Server, httpPort: ${httpPort}, httpsPort: ${httpsPort}`);
107
+ // for dev to refresh the FE or stop the server
108
+ if (this.debug) {
109
+ WebProcessor.enableDebug('/debug', processDevRequests);
110
+ }
111
+
112
+ httpPort && this.webServer!.startHttp(httpPort, bindIp);
113
+ httpsPort && this.webServer!.startHttps(httpsPort, bindIp, sslKeyPath, sslCrtPath);
114
+ }
115
+ }
116
+
117
+ export const appStart = /* @__PURE__ */ new AppStart();
@@ -0,0 +1,12 @@
1
+ import cluster from 'cluster';
2
+ import { appStorage } from './app-shared-storage';
3
+
4
+ export const cleanupAndExit = async () => {
5
+ console.log(`${process.pid} - Process on SIGINT, exit.`);
6
+ // save shared storage first
7
+ if (cluster.isPrimary) {
8
+ // save only happens once
9
+ await appStorage.save();
10
+ }
11
+ process.exit(0);
12
+ };
@@ -0,0 +1,38 @@
1
+ // const request = require('request');
2
+ import { Logger } from '../lib';
3
+ import { HostToPathProps } from '../models';
4
+
5
+ const logger = new Logger('HostToPath');
6
+ export class HostToPath {
7
+ static props: HostToPathProps[] = [];
8
+
9
+ // this should be initialized before any request
10
+ static setHostToPathList(props: HostToPathProps[]) {
11
+ this.props = props;
12
+ }
13
+
14
+ static findHostPath(host: string): HostToPathProps | undefined {
15
+ for (let key in this.props) {
16
+ if (this.props[key].webPath && this.props[key].hosts.includes(host)) {
17
+ logger.debug(`Found ${host} in `, this.props[key].hosts);
18
+ return this.props[key];
19
+ }
20
+ }
21
+ for (let key in this.props) {
22
+ for (let domain in this.props[key].hosts) {
23
+ // if host is 'sub3.sub2.sub1.domain', it matches 'sub2.domain'
24
+ if (this.props[key].webPath && host.endsWith(this.props[key].hosts[domain])) {
25
+ logger.debug(`Found ${host} in `, this.props[key].hosts);
26
+ return this.props[key];
27
+ }
28
+ }
29
+ }
30
+ for (let key in this.props) {
31
+ if (this.props[key].webPath && this.props[key].hosts.length === 0) {
32
+ logger.debug(`Not found ${host} from any domains and use default app: ${this.props[key].appName}`);
33
+ return this.props[key];
34
+ }
35
+ }
36
+ return;
37
+ }
38
+ }
@@ -0,0 +1,11 @@
1
+ export * from './app-cache';
2
+ export * from './app-loader';
3
+ export * from './app-message';
4
+ export * from './app-shared-storage';
5
+ export * from './app-start';
6
+ export * from './cleanup-exit'
7
+ export * from './host-to-path';
8
+ export * from './process-dev-requests';
9
+ export * from './web-listener';
10
+ export * from './web-processor';
11
+ export * from './web-server';