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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/admin/admin-about.tsx +16 -0
- package/admin/admin-config.tsx +44 -0
- package/admin/admin-css.tsx +3 -0
- package/admin/admin-db.tsx +74 -0
- package/admin/admin-frame-props.tsx +9 -0
- package/admin/admin-frame.tsx +466 -0
- package/admin/admin-index.tsx +66 -0
- package/admin/admin-login.tsx +99 -0
- package/admin/admin-menu-edit.tsx +637 -0
- package/admin/admin-menu-list.tsx +87 -0
- package/admin/admin-page-edit.tsx +564 -0
- package/admin/admin-page-list.tsx +83 -0
- package/admin/admin-performance.tsx +28 -0
- package/admin/admin-release.tsx +320 -0
- package/admin/admin-resources.tsx +385 -0
- package/admin/admin-shell.tsx +89 -0
- package/admin/admin-table-data.tsx +146 -0
- package/admin/admin-table-list.tsx +231 -0
- package/admin/admin-test-animations.tsx +379 -0
- package/admin/admin-test-component.tsx +808 -0
- package/admin/admin-test-edit.tsx +319 -0
- package/admin/admin-test-themes.tsx +56 -0
- package/admin/admin-tokens.tsx +338 -0
- package/admin/design/admin-design.tsx +174 -0
- package/admin/design/block-grid.tsx +36 -0
- package/admin/design/block-grid1.tsx +21 -0
- package/admin/design/block-paragraph.tsx +19 -0
- package/admin/design/block-title.tsx +19 -0
- package/admin/design/design-block-box.tsx +140 -0
- package/admin/design/drag-data.tsx +24 -0
- package/admin/index.ts +6 -0
- package/admin/package.json +15 -0
- package/admin/tsconfig.json +127 -0
- package/dev/copy-folder.js +32 -0
- package/dev/cp-index-html.js +69 -0
- package/dev/file-utils.js +12 -0
- package/dev/index.js +19 -0
- package/dev/package.json +12 -0
- package/dev/plugin-gen-versions.js +20 -0
- package/dev/plugin-ifelse.js +155 -0
- package/dev/plugin-ifelse.test.js +37 -0
- package/dev/run-cmd.js +14 -0
- package/dev/send-request.js +12 -0
- package/package.json +55 -0
- package/src/admin-api/admin-api.ts +59 -0
- package/src/admin-api/admin-auth.ts +87 -0
- package/src/admin-api/admin-config.ts +93 -0
- package/src/admin-api/admin-csv.ts +81 -0
- package/src/admin-api/admin-db.ts +269 -0
- package/src/admin-api/admin-helper.ts +111 -0
- package/src/admin-api/admin-menu.ts +135 -0
- package/src/admin-api/admin-page.ts +135 -0
- package/src/admin-api/admin-performance.ts +128 -0
- package/src/admin-api/admin-release.ts +498 -0
- package/src/admin-api/admin-resources.ts +318 -0
- package/src/admin-api/admin-token-helper.ts +79 -0
- package/src/admin-api/admin-tokens.ts +90 -0
- package/src/admin-api/index.ts +2 -0
- package/src/api/api-cache.ts +103 -0
- package/src/api/api-helper.ts +44 -0
- package/src/api/api-module.ts +60 -0
- package/src/api/api-router.ts +177 -0
- package/src/api/api-shared-storage.ts +64 -0
- package/src/api/async-storage.ts +5 -0
- package/src/api/debug-service.ts +56 -0
- package/src/api/encode-html.ts +27 -0
- package/src/api/handle-status.ts +71 -0
- package/src/api/index.ts +16 -0
- package/src/api/mini-web-socket.ts +270 -0
- package/src/api/server-content-type.ts +82 -0
- package/src/api/server-render.ts +216 -0
- package/src/api/shell-service.ts +66 -0
- package/src/api/simple-storage.ts +80 -0
- package/src/api/static-server.ts +125 -0
- package/src/api/to-client-delivery.ts +26 -0
- package/src/app/app-cache.ts +55 -0
- package/src/app/app-loader.ts +62 -0
- package/src/app/app-message.ts +60 -0
- package/src/app/app-shared-storage.ts +317 -0
- package/src/app/app-start.ts +117 -0
- package/src/app/cleanup-exit.ts +12 -0
- package/src/app/host-to-path.ts +38 -0
- package/src/app/index.ts +11 -0
- package/src/app/process-dev-requests.ts +90 -0
- package/src/app/web-listener.ts +230 -0
- package/src/app/web-processor.ts +42 -0
- package/src/app/web-server.ts +86 -0
- package/src/common-js/web-env.js +104 -0
- package/src/index.ts +7 -0
- package/src/lang/api-lang-en.ts +27 -0
- package/src/lang/api-lang-zh-cn.ts +28 -0
- package/src/lang/index.ts +2 -0
- package/src/lang/lang-helper.ts +76 -0
- package/src/lang/lang-props.ts +6 -0
- package/src/lib/db/db-helper.ts +23 -0
- package/src/lib/db/db-mysql.ts +250 -0
- package/src/lib/db/db-sqlite.ts +101 -0
- package/src/lib/db/db.spec.ts +28 -0
- package/src/lib/db/db.ts +304 -0
- package/src/lib/db/index.ts +5 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/logger.spec.ts +214 -0
- package/src/lib/logger.ts +274 -0
- package/src/lib/runtime-require.ts +37 -0
- package/src/lib/utils/cookie-util.ts +34 -0
- package/src/lib/utils/crypto.ts +58 -0
- package/src/lib/utils/date-utils.ts +317 -0
- package/src/lib/utils/deep-merge.ts +37 -0
- package/src/lib/utils/delay.ts +12 -0
- package/src/lib/utils/file-setting.ts +55 -0
- package/src/lib/utils/format-bytes.ts +11 -0
- package/src/lib/utils/fs-utils.ts +144 -0
- package/src/lib/utils/get-env.ts +27 -0
- package/src/lib/utils/index.ts +12 -0
- package/src/lib/utils/is-type.ts +48 -0
- package/src/lib/utils/load-env.ts +14 -0
- package/src/lib/utils/pad.ts +6 -0
- package/src/models/api-base.ts +5 -0
- package/src/models/api-module-props.ts +11 -0
- package/src/models/api-router-props.ts +26 -0
- package/src/models/app-cache-props.ts +33 -0
- package/src/models/app-data-props.ts +10 -0
- package/src/models/app-loader-props.ts +6 -0
- package/src/models/app-shared-storage-props.ts +37 -0
- package/src/models/app-start-props.ts +18 -0
- package/src/models/async-storage-props.ts +13 -0
- package/src/models/db-config.ts +30 -0
- package/src/models/host-to-path-props.ts +12 -0
- package/src/models/index.ts +16 -0
- package/src/models/json-object.ts +8 -0
- package/src/models/locals-props.ts +36 -0
- package/src/models/logger-props.ts +84 -0
- package/src/models/simple-storage-props.ts +14 -0
- package/src/models/to-client-delivery-props.ts +6 -0
- 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
|
+
}
|
package/src/app/index.ts
ADDED
|
@@ -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';
|