lsh-framework 0.5.4
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/.env.example +51 -0
- package/README.md +399 -0
- package/dist/app.js +33 -0
- package/dist/cicd/analytics.js +261 -0
- package/dist/cicd/auth.js +269 -0
- package/dist/cicd/cache-manager.js +172 -0
- package/dist/cicd/data-retention.js +305 -0
- package/dist/cicd/performance-monitor.js +224 -0
- package/dist/cicd/webhook-receiver.js +634 -0
- package/dist/cli.js +500 -0
- package/dist/commands/api.js +343 -0
- package/dist/commands/self.js +318 -0
- package/dist/commands/theme.js +257 -0
- package/dist/commands/zsh-import.js +240 -0
- package/dist/components/App.js +1 -0
- package/dist/components/Divider.js +29 -0
- package/dist/components/REPL.js +43 -0
- package/dist/components/Terminal.js +232 -0
- package/dist/components/UserInput.js +30 -0
- package/dist/daemon/api-server.js +315 -0
- package/dist/daemon/job-registry.js +554 -0
- package/dist/daemon/lshd.js +822 -0
- package/dist/daemon/monitoring-api.js +220 -0
- package/dist/examples/supabase-integration.js +106 -0
- package/dist/lib/api-error-handler.js +183 -0
- package/dist/lib/associative-arrays.js +285 -0
- package/dist/lib/base-api-server.js +290 -0
- package/dist/lib/base-command-registrar.js +286 -0
- package/dist/lib/base-job-manager.js +293 -0
- package/dist/lib/brace-expansion.js +160 -0
- package/dist/lib/builtin-commands.js +439 -0
- package/dist/lib/cloud-config-manager.js +347 -0
- package/dist/lib/command-validator.js +190 -0
- package/dist/lib/completion-system.js +344 -0
- package/dist/lib/cron-job-manager.js +364 -0
- package/dist/lib/daemon-client-helper.js +141 -0
- package/dist/lib/daemon-client.js +501 -0
- package/dist/lib/database-persistence.js +638 -0
- package/dist/lib/database-schema.js +259 -0
- package/dist/lib/enhanced-history-system.js +246 -0
- package/dist/lib/env-validator.js +265 -0
- package/dist/lib/executors/builtin-executor.js +52 -0
- package/dist/lib/extended-globbing.js +411 -0
- package/dist/lib/extended-parameter-expansion.js +227 -0
- package/dist/lib/floating-point-arithmetic.js +256 -0
- package/dist/lib/history-system.js +245 -0
- package/dist/lib/interactive-shell.js +460 -0
- package/dist/lib/job-builtins.js +580 -0
- package/dist/lib/job-manager.js +386 -0
- package/dist/lib/job-storage-database.js +156 -0
- package/dist/lib/job-storage-memory.js +73 -0
- package/dist/lib/logger.js +274 -0
- package/dist/lib/lshrc-init.js +177 -0
- package/dist/lib/pathname-expansion.js +216 -0
- package/dist/lib/prompt-system.js +328 -0
- package/dist/lib/script-runner.js +226 -0
- package/dist/lib/secrets-manager.js +193 -0
- package/dist/lib/shell-executor.js +2504 -0
- package/dist/lib/shell-parser.js +958 -0
- package/dist/lib/shell-types.js +6 -0
- package/dist/lib/shell.lib.js +40 -0
- package/dist/lib/supabase-client.js +58 -0
- package/dist/lib/theme-manager.js +476 -0
- package/dist/lib/variable-expansion.js +385 -0
- package/dist/lib/zsh-compatibility.js +658 -0
- package/dist/lib/zsh-import-manager.js +699 -0
- package/dist/lib/zsh-options.js +328 -0
- package/dist/pipeline/job-tracker.js +491 -0
- package/dist/pipeline/mcli-bridge.js +302 -0
- package/dist/pipeline/pipeline-service.js +1116 -0
- package/dist/pipeline/workflow-engine.js +867 -0
- package/dist/services/api/api.js +58 -0
- package/dist/services/api/auth.js +35 -0
- package/dist/services/api/config.js +7 -0
- package/dist/services/api/file.js +22 -0
- package/dist/services/cron/cron-registrar.js +235 -0
- package/dist/services/cron/cron.js +9 -0
- package/dist/services/daemon/daemon-registrar.js +565 -0
- package/dist/services/daemon/daemon.js +9 -0
- package/dist/services/lib/lib.js +86 -0
- package/dist/services/log-file-extractor.js +170 -0
- package/dist/services/secrets/secrets.js +94 -0
- package/dist/services/shell/shell.js +28 -0
- package/dist/services/supabase/supabase-registrar.js +367 -0
- package/dist/services/supabase/supabase.js +9 -0
- package/dist/services/zapier.js +16 -0
- package/dist/simple-api-server.js +148 -0
- package/dist/store/store.js +31 -0
- package/dist/util/lib.util.js +11 -0
- package/package.json +144 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Associative Arrays Implementation
|
|
3
|
+
* Provides ZSH-compatible associative array functionality
|
|
4
|
+
*/
|
|
5
|
+
export class AssociativeArrayManager {
|
|
6
|
+
context;
|
|
7
|
+
constructor() {
|
|
8
|
+
this.context = {
|
|
9
|
+
arrays: new Map(),
|
|
10
|
+
arrayTypes: new Map(),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Declare an associative array with typeset -A
|
|
15
|
+
*/
|
|
16
|
+
declareAssociativeArray(name) {
|
|
17
|
+
this.context.arrays.set(name, {});
|
|
18
|
+
this.context.arrayTypes.set(name, 'associative');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Declare an indexed array with typeset -a
|
|
22
|
+
*/
|
|
23
|
+
declareIndexedArray(name) {
|
|
24
|
+
this.context.arrays.set(name, {});
|
|
25
|
+
this.context.arrayTypes.set(name, 'indexed');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Set a value in an associative array
|
|
29
|
+
*/
|
|
30
|
+
setAssociativeValue(arrayName, key, value) {
|
|
31
|
+
if (!this.context.arrays.has(arrayName)) {
|
|
32
|
+
this.declareAssociativeArray(arrayName);
|
|
33
|
+
}
|
|
34
|
+
const array = this.context.arrays.get(arrayName);
|
|
35
|
+
array[key] = value;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Set a value in an indexed array
|
|
39
|
+
*/
|
|
40
|
+
setIndexedValue(arrayName, index, value) {
|
|
41
|
+
if (!this.context.arrays.has(arrayName)) {
|
|
42
|
+
this.declareIndexedArray(arrayName);
|
|
43
|
+
}
|
|
44
|
+
const array = this.context.arrays.get(arrayName);
|
|
45
|
+
array[index.toString()] = value;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get a value from an array
|
|
49
|
+
*/
|
|
50
|
+
getValue(arrayName, key) {
|
|
51
|
+
const array = this.context.arrays.get(arrayName);
|
|
52
|
+
return array ? array[key] : undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get all keys from an associative array
|
|
56
|
+
*/
|
|
57
|
+
getKeys(arrayName) {
|
|
58
|
+
const array = this.context.arrays.get(arrayName);
|
|
59
|
+
if (!array)
|
|
60
|
+
return [];
|
|
61
|
+
const type = this.context.arrayTypes.get(arrayName);
|
|
62
|
+
if (type === 'indexed') {
|
|
63
|
+
// For indexed arrays, return sorted numeric keys
|
|
64
|
+
return Object.keys(array)
|
|
65
|
+
.map(k => parseInt(k, 10))
|
|
66
|
+
.filter(k => !isNaN(k))
|
|
67
|
+
.sort((a, b) => a - b)
|
|
68
|
+
.map(k => k.toString());
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// For associative arrays, return all keys
|
|
72
|
+
return Object.keys(array);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get all values from an array
|
|
77
|
+
*/
|
|
78
|
+
getValues(arrayName) {
|
|
79
|
+
const array = this.context.arrays.get(arrayName);
|
|
80
|
+
if (!array)
|
|
81
|
+
return [];
|
|
82
|
+
const type = this.context.arrayTypes.get(arrayName);
|
|
83
|
+
if (type === 'indexed') {
|
|
84
|
+
// For indexed arrays, return values in order
|
|
85
|
+
const keys = this.getKeys(arrayName);
|
|
86
|
+
return keys.map(key => array[key]);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// For associative arrays, return all values
|
|
90
|
+
return Object.values(array);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get array length
|
|
95
|
+
*/
|
|
96
|
+
getLength(arrayName) {
|
|
97
|
+
const array = this.context.arrays.get(arrayName);
|
|
98
|
+
return array ? Object.keys(array).length : 0;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if an array exists
|
|
102
|
+
*/
|
|
103
|
+
hasArray(arrayName) {
|
|
104
|
+
return this.context.arrays.has(arrayName);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get array type
|
|
108
|
+
*/
|
|
109
|
+
getArrayType(arrayName) {
|
|
110
|
+
return this.context.arrayTypes.get(arrayName);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Remove an array
|
|
114
|
+
*/
|
|
115
|
+
removeArray(arrayName) {
|
|
116
|
+
const hadArray = this.context.arrays.has(arrayName);
|
|
117
|
+
this.context.arrays.delete(arrayName);
|
|
118
|
+
this.context.arrayTypes.delete(arrayName);
|
|
119
|
+
return hadArray;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get all array names
|
|
123
|
+
*/
|
|
124
|
+
getAllArrayNames() {
|
|
125
|
+
return Array.from(this.context.arrays.keys());
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Clear all arrays
|
|
129
|
+
*/
|
|
130
|
+
clearAllArrays() {
|
|
131
|
+
this.context.arrays.clear();
|
|
132
|
+
this.context.arrayTypes.clear();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get array slice (for indexed arrays)
|
|
136
|
+
*/
|
|
137
|
+
getSlice(arrayName, start, end) {
|
|
138
|
+
const array = this.context.arrays.get(arrayName);
|
|
139
|
+
if (!array)
|
|
140
|
+
return [];
|
|
141
|
+
const type = this.context.arrayTypes.get(arrayName);
|
|
142
|
+
if (type !== 'indexed')
|
|
143
|
+
return [];
|
|
144
|
+
const keys = this.getKeys(arrayName);
|
|
145
|
+
const startIdx = Math.max(0, start - 1); // Convert to 0-based
|
|
146
|
+
const endIdx = end ? Math.min(keys.length, end) : keys.length;
|
|
147
|
+
return keys.slice(startIdx, endIdx).map(key => array[key]);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Expand array reference like ${array[key]} or ${array[@]}
|
|
151
|
+
*/
|
|
152
|
+
expandArrayReference(reference) {
|
|
153
|
+
// Match patterns like ${array[key]}, ${array[@]}, ${array[*]}
|
|
154
|
+
const match = reference.match(/^\$\{([^[\]]+)(?:\[([^\]]+)\])?(?:\[@\*\]|\[@\]|\[\*\]|\[@\])?\}$/);
|
|
155
|
+
if (!match)
|
|
156
|
+
return [];
|
|
157
|
+
const arrayName = match[1];
|
|
158
|
+
const key = match[2];
|
|
159
|
+
if (!this.hasArray(arrayName))
|
|
160
|
+
return [];
|
|
161
|
+
if (key) {
|
|
162
|
+
// Single element access
|
|
163
|
+
const value = this.getValue(arrayName, key);
|
|
164
|
+
return value !== undefined ? [value] : [];
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// All elements access
|
|
168
|
+
return this.getValues(arrayName);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Expand array keys like ${(k)array}
|
|
173
|
+
*/
|
|
174
|
+
expandArrayKeys(reference) {
|
|
175
|
+
const match = reference.match(/^\$\{\(k\)([^}]+)\}$/);
|
|
176
|
+
if (!match)
|
|
177
|
+
return [];
|
|
178
|
+
const arrayName = match[1];
|
|
179
|
+
return this.getKeys(arrayName);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Expand array values like ${(v)array}
|
|
183
|
+
*/
|
|
184
|
+
expandArrayValues(reference) {
|
|
185
|
+
const match = reference.match(/^\$\{\(v\)([^}]+)\}$/);
|
|
186
|
+
if (!match)
|
|
187
|
+
return [];
|
|
188
|
+
const arrayName = match[1];
|
|
189
|
+
return this.getValues(arrayName);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Expand array length like ${#array}
|
|
193
|
+
*/
|
|
194
|
+
expandArrayLength(reference) {
|
|
195
|
+
const match = reference.match(/^\$\{#([^}]+)\}$/);
|
|
196
|
+
if (!match)
|
|
197
|
+
return '';
|
|
198
|
+
const arrayName = match[1];
|
|
199
|
+
return this.getLength(arrayName).toString();
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Parse typeset command
|
|
203
|
+
*/
|
|
204
|
+
parseTypesetCommand(args) {
|
|
205
|
+
if (args.length === 0) {
|
|
206
|
+
return { success: false, message: 'typeset: missing arguments' };
|
|
207
|
+
}
|
|
208
|
+
for (const arg of args) {
|
|
209
|
+
if (arg === '-A') {
|
|
210
|
+
// Declare associative array - next argument should be the name
|
|
211
|
+
const nameIndex = args.indexOf('-A') + 1;
|
|
212
|
+
if (nameIndex < args.length) {
|
|
213
|
+
const name = args[nameIndex];
|
|
214
|
+
this.declareAssociativeArray(name);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (arg === '-a') {
|
|
218
|
+
// Declare indexed array - next argument should be the name
|
|
219
|
+
const nameIndex = args.indexOf('-a') + 1;
|
|
220
|
+
if (nameIndex < args.length) {
|
|
221
|
+
const name = args[nameIndex];
|
|
222
|
+
this.declareIndexedArray(name);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (arg.includes('=')) {
|
|
226
|
+
// Assignment: name=value or name[key]=value
|
|
227
|
+
const [left, right] = arg.split('=', 2);
|
|
228
|
+
const arrayMatch = left.match(/^([^[\]]+)(?:\[([^\]]+)\])?$/);
|
|
229
|
+
if (arrayMatch) {
|
|
230
|
+
const arrayName = arrayMatch[1];
|
|
231
|
+
const key = arrayMatch[2];
|
|
232
|
+
if (key) {
|
|
233
|
+
// Associative array assignment
|
|
234
|
+
this.setAssociativeValue(arrayName, key, right);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Regular variable assignment (not array)
|
|
238
|
+
// This would be handled by the regular variable system
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { success: true, message: '' };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get array information for display
|
|
247
|
+
*/
|
|
248
|
+
getArrayInfo(arrayName) {
|
|
249
|
+
if (!this.hasArray(arrayName))
|
|
250
|
+
return null;
|
|
251
|
+
const type = this.getArrayType(arrayName);
|
|
252
|
+
const length = this.getLength(arrayName);
|
|
253
|
+
const keys = this.getKeys(arrayName);
|
|
254
|
+
const values = this.getValues(arrayName);
|
|
255
|
+
return {
|
|
256
|
+
type: type || 'unknown',
|
|
257
|
+
length,
|
|
258
|
+
keys,
|
|
259
|
+
values,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Export array data for serialization
|
|
264
|
+
*/
|
|
265
|
+
exportArrays() {
|
|
266
|
+
const arrays = {};
|
|
267
|
+
const types = {};
|
|
268
|
+
for (const [name, array] of this.context.arrays) {
|
|
269
|
+
arrays[name] = { ...array };
|
|
270
|
+
types[name] = this.context.arrayTypes.get(name) || 'unknown';
|
|
271
|
+
}
|
|
272
|
+
return { arrays, types };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Import array data from serialization
|
|
276
|
+
*/
|
|
277
|
+
importArrays(data) {
|
|
278
|
+
this.clearAllArrays();
|
|
279
|
+
for (const [name, array] of Object.entries(data.arrays)) {
|
|
280
|
+
this.context.arrays.set(name, { ...array });
|
|
281
|
+
this.context.arrayTypes.set(name, data.types[name]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
export default AssociativeArrayManager;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base API Server
|
|
3
|
+
* Abstract base class for all API servers to eliminate duplication in:
|
|
4
|
+
* - Express middleware setup
|
|
5
|
+
* - Server lifecycle management
|
|
6
|
+
* - Signal handling
|
|
7
|
+
* - Error handling
|
|
8
|
+
*/
|
|
9
|
+
import express from 'express';
|
|
10
|
+
import cors from 'cors';
|
|
11
|
+
import helmet from 'helmet';
|
|
12
|
+
import { EventEmitter } from 'events';
|
|
13
|
+
import { createLogger } from './logger.js';
|
|
14
|
+
/**
|
|
15
|
+
* Abstract base class for API servers
|
|
16
|
+
*
|
|
17
|
+
* Provides:
|
|
18
|
+
* - Express app setup with common middleware
|
|
19
|
+
* - Server lifecycle (start/stop)
|
|
20
|
+
* - Signal handling (SIGTERM, SIGINT, SIGHUP)
|
|
21
|
+
* - Error handling (uncaughtException, unhandledRejection)
|
|
22
|
+
* - Request logging
|
|
23
|
+
* - Structured logging
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* class MyAPIServer extends BaseAPIServer {
|
|
28
|
+
* constructor() {
|
|
29
|
+
* super({ port: 3000 }, 'MyAPI');
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* protected setupRoutes(): void {
|
|
33
|
+
* this.app.get('/api/hello', (req, res) => {
|
|
34
|
+
* res.json({ message: 'Hello World' });
|
|
35
|
+
* });
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export class BaseAPIServer extends EventEmitter {
|
|
41
|
+
app;
|
|
42
|
+
server;
|
|
43
|
+
config;
|
|
44
|
+
logger;
|
|
45
|
+
isShuttingDown = false;
|
|
46
|
+
/**
|
|
47
|
+
* Create a new API server
|
|
48
|
+
*
|
|
49
|
+
* @param config - Server configuration
|
|
50
|
+
* @param loggerName - Name for the logger context
|
|
51
|
+
*/
|
|
52
|
+
constructor(config, loggerName) {
|
|
53
|
+
super();
|
|
54
|
+
this.config = {
|
|
55
|
+
port: 3000,
|
|
56
|
+
corsOrigins: '*',
|
|
57
|
+
enableHelmet: true,
|
|
58
|
+
jsonLimit: '10mb',
|
|
59
|
+
enableRequestLogging: true,
|
|
60
|
+
enableSignalHandlers: true,
|
|
61
|
+
enableErrorHandlers: true,
|
|
62
|
+
...config
|
|
63
|
+
};
|
|
64
|
+
this.logger = createLogger(loggerName);
|
|
65
|
+
this.app = express();
|
|
66
|
+
this.setupMiddleware();
|
|
67
|
+
if (this.config.enableErrorHandlers) {
|
|
68
|
+
this.setupErrorHandlers();
|
|
69
|
+
}
|
|
70
|
+
if (this.config.enableSignalHandlers) {
|
|
71
|
+
this.setupSignalHandlers();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Setup Express middleware
|
|
76
|
+
* Can be overridden for custom middleware setup
|
|
77
|
+
*/
|
|
78
|
+
setupMiddleware() {
|
|
79
|
+
// Security middleware
|
|
80
|
+
if (this.config.enableHelmet) {
|
|
81
|
+
this.app.use(helmet({
|
|
82
|
+
crossOriginEmbedderPolicy: false,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
// CORS
|
|
86
|
+
this.app.use(this.configureCORS());
|
|
87
|
+
// Body parsing
|
|
88
|
+
this.app.use(express.json({ limit: this.config.jsonLimit }));
|
|
89
|
+
this.app.use(express.urlencoded({ extended: true }));
|
|
90
|
+
// Request logging
|
|
91
|
+
if (this.config.enableRequestLogging) {
|
|
92
|
+
this.app.use(this.requestLogger.bind(this));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Configure CORS middleware
|
|
97
|
+
* Can be overridden for custom CORS configuration
|
|
98
|
+
*/
|
|
99
|
+
configureCORS() {
|
|
100
|
+
const origins = this.config.corsOrigins;
|
|
101
|
+
if (origins === '*') {
|
|
102
|
+
return cors();
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(origins)) {
|
|
105
|
+
return cors({
|
|
106
|
+
origin: (origin, callback) => {
|
|
107
|
+
if (!origin || origins.some(pattern => {
|
|
108
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
109
|
+
return regex.test(origin);
|
|
110
|
+
})) {
|
|
111
|
+
callback(null, true);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
callback(new Error('Not allowed by CORS'));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return cors({ origin: origins });
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Request logging middleware
|
|
123
|
+
*/
|
|
124
|
+
requestLogger(req, res, next) {
|
|
125
|
+
this.logger.info(`${req.method} ${req.path}`);
|
|
126
|
+
next();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Setup error handlers for uncaught exceptions and unhandled rejections
|
|
130
|
+
*/
|
|
131
|
+
setupErrorHandlers() {
|
|
132
|
+
process.on('uncaughtException', (error) => {
|
|
133
|
+
this.logger.error('Uncaught Exception', error);
|
|
134
|
+
this.handleFatalError(error);
|
|
135
|
+
});
|
|
136
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
137
|
+
this.logger.error('Unhandled Rejection', reason instanceof Error ? reason : new Error(String(reason)), {
|
|
138
|
+
promise: String(promise)
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Setup signal handlers for graceful shutdown
|
|
144
|
+
*/
|
|
145
|
+
setupSignalHandlers() {
|
|
146
|
+
const signals = ['SIGTERM', 'SIGINT'];
|
|
147
|
+
signals.forEach(signal => {
|
|
148
|
+
process.on(signal, () => {
|
|
149
|
+
this.logger.info(`Received ${signal}, initiating graceful shutdown`);
|
|
150
|
+
this.gracefulShutdown(signal);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
process.on('SIGHUP', () => {
|
|
154
|
+
this.logger.info('Received SIGHUP');
|
|
155
|
+
this.handleSIGHUP();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Handle SIGHUP signal - can be overridden for custom behavior (e.g., reload config)
|
|
160
|
+
*/
|
|
161
|
+
handleSIGHUP() {
|
|
162
|
+
this.logger.info('SIGHUP handler not implemented, ignoring');
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Handle fatal errors
|
|
166
|
+
* @param error - The fatal error
|
|
167
|
+
*/
|
|
168
|
+
handleFatalError(error) {
|
|
169
|
+
this.logger.error('Fatal error, shutting down', error);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Perform graceful shutdown
|
|
174
|
+
* @param signal - The signal that triggered the shutdown
|
|
175
|
+
*/
|
|
176
|
+
async gracefulShutdown(signal) {
|
|
177
|
+
if (this.isShuttingDown) {
|
|
178
|
+
this.logger.warn('Shutdown already in progress');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.isShuttingDown = true;
|
|
182
|
+
this.logger.info(`Graceful shutdown initiated by ${signal}`);
|
|
183
|
+
try {
|
|
184
|
+
// Give ongoing requests time to complete
|
|
185
|
+
await this.stop();
|
|
186
|
+
this.logger.info('Server stopped successfully');
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
this.logger.error('Error during shutdown', error);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Start the API server
|
|
196
|
+
* @returns Promise that resolves when server is listening
|
|
197
|
+
*/
|
|
198
|
+
async start() {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
try {
|
|
201
|
+
// Setup routes before starting server
|
|
202
|
+
this.setupRoutes();
|
|
203
|
+
this.server = this.app.listen(this.config.port, () => {
|
|
204
|
+
this.logger.info(`Server started on port ${this.config.port}`);
|
|
205
|
+
this.emit('started');
|
|
206
|
+
resolve();
|
|
207
|
+
});
|
|
208
|
+
this.server.on('error', (error) => {
|
|
209
|
+
this.logger.error('Server error', error);
|
|
210
|
+
this.emit('error', error);
|
|
211
|
+
reject(error);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
this.logger.error('Failed to start server', error);
|
|
216
|
+
reject(error);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Stop the API server
|
|
222
|
+
* @param timeout - Maximum time to wait for connections to close (ms)
|
|
223
|
+
* @returns Promise that resolves when server is stopped
|
|
224
|
+
*/
|
|
225
|
+
async stop(timeout = 5000) {
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
if (!this.server) {
|
|
228
|
+
resolve();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.logger.info('Stopping server...');
|
|
232
|
+
// Set timeout for force shutdown
|
|
233
|
+
const forceShutdownTimer = setTimeout(() => {
|
|
234
|
+
this.logger.warn('Force closing server after timeout');
|
|
235
|
+
this.server?.close();
|
|
236
|
+
resolve();
|
|
237
|
+
}, timeout);
|
|
238
|
+
this.server?.close((error) => {
|
|
239
|
+
clearTimeout(forceShutdownTimer);
|
|
240
|
+
if (error) {
|
|
241
|
+
this.logger.error('Error stopping server', error);
|
|
242
|
+
reject(error);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
this.logger.info('Server stopped');
|
|
246
|
+
this.emit('stopped');
|
|
247
|
+
resolve();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
// Allow subclasses to cleanup
|
|
251
|
+
this.onStop();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Hook called when server is stopping
|
|
256
|
+
* Override this to cleanup resources, close connections, etc.
|
|
257
|
+
*/
|
|
258
|
+
onStop() {
|
|
259
|
+
// Override in subclasses if needed
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Get the Express application
|
|
263
|
+
* @returns The Express app instance
|
|
264
|
+
*/
|
|
265
|
+
getApp() {
|
|
266
|
+
return this.app;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get the HTTP server
|
|
270
|
+
* @returns The HTTP server instance
|
|
271
|
+
*/
|
|
272
|
+
getServer() {
|
|
273
|
+
return this.server;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Check if server is running
|
|
277
|
+
* @returns True if server is running
|
|
278
|
+
*/
|
|
279
|
+
isRunning() {
|
|
280
|
+
return !!this.server?.listening;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get server configuration
|
|
284
|
+
* @returns The server configuration
|
|
285
|
+
*/
|
|
286
|
+
getConfig() {
|
|
287
|
+
return { ...this.config };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export default BaseAPIServer;
|