@runium/core 0.0.7 → 0.0.9

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/src/task.ts ADDED
@@ -0,0 +1,381 @@
1
+ import {
2
+ ChildProcessWithoutNullStreams,
3
+ SpawnOptionsWithoutStdio,
4
+ spawn,
5
+ } from 'node:child_process';
6
+ import { EventEmitter } from 'node:events';
7
+ import { createWriteStream, WriteStream } from 'node:fs';
8
+ import { mkdir } from 'node:fs/promises';
9
+ import { dirname, resolve } from 'node:path';
10
+
11
+ /**
12
+ * Task status
13
+ */
14
+ export enum TaskStatus {
15
+ IDLE = 'idle',
16
+ STARTING = 'starting',
17
+ STARTED = 'started',
18
+ COMPLETED = 'completed',
19
+ FAILED = 'failed',
20
+ STOPPING = 'stopping',
21
+ STOPPED = 'stopped',
22
+ }
23
+
24
+ /**
25
+ * Task event
26
+ */
27
+ export enum TaskEvent {
28
+ STATE_CHANGE = 'state-change',
29
+ STDOUT = 'stdout',
30
+ STDERR = 'stderr',
31
+ }
32
+
33
+ /**
34
+ * Base runium task state
35
+ */
36
+ export interface RuniumTaskState {
37
+ status: TaskStatus;
38
+ timestamp: number;
39
+ iteration: number;
40
+ exitCode?: number;
41
+ error?: Error;
42
+ reason?: string;
43
+ }
44
+
45
+ /**
46
+ * Task options
47
+ */
48
+ export interface TaskOptions {
49
+ command: string;
50
+ arguments?: string[];
51
+ shell?: boolean;
52
+ stopSignal?: string;
53
+ cwd?: string;
54
+ env?: { [key: string]: string | number | boolean };
55
+ ttl?: number;
56
+ log?: {
57
+ stdout?: string | null;
58
+ stderr?: string | null;
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Task state
64
+ */
65
+ export interface TaskState extends RuniumTaskState {
66
+ pid: number;
67
+ }
68
+
69
+ /**
70
+ * Runium task constructor
71
+ */
72
+ export type RuniumTaskConstructor<
73
+ Options = unknown,
74
+ State extends RuniumTaskState = RuniumTaskState,
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ > = new (options: any) => RuniumTask<Options, State>;
77
+
78
+ /**
79
+ * Silent exit code
80
+ * use when stopping task without error
81
+ */
82
+ export const SILENT_EXIT_CODE = -1;
83
+
84
+ const MAX_LISTENERS_COUNT = 50;
85
+
86
+ /**
87
+ * Base runium task class
88
+ */
89
+ export abstract class RuniumTask<
90
+ Options = unknown,
91
+ State = RuniumTaskState,
92
+ > extends EventEmitter {
93
+ protected constructor(protected options: Options) {
94
+ super();
95
+ }
96
+ abstract getOptions(): Options;
97
+ abstract getState(): State;
98
+ abstract start(): Promise<void>;
99
+ abstract stop(reason?: string): Promise<void>;
100
+ abstract restart(): Promise<void>;
101
+ }
102
+
103
+ /**
104
+ * Task class
105
+ */
106
+ export class Task extends RuniumTask<TaskOptions, TaskState> {
107
+ protected state: TaskState = {
108
+ status: TaskStatus.IDLE,
109
+ pid: -1,
110
+ timestamp: Date.now(),
111
+ iteration: 0,
112
+ };
113
+ protected process: ChildProcessWithoutNullStreams | null = null;
114
+ protected stdoutStream: WriteStream | null = null;
115
+ protected stderrStream: WriteStream | null = null;
116
+ protected ttlTimer: NodeJS.Timeout | null = null;
117
+
118
+ constructor(protected readonly options: TaskOptions) {
119
+ super(options);
120
+ this.setMaxListeners(MAX_LISTENERS_COUNT);
121
+
122
+ this.options.env = {
123
+ ...process.env,
124
+ ...(options.env || {}),
125
+ } as TaskOptions['env'];
126
+ this.options.cwd = resolve(process.cwd(), options.cwd || '');
127
+ }
128
+
129
+ /**
130
+ * Get task state
131
+ */
132
+ getState(): TaskState {
133
+ return { ...this.state };
134
+ }
135
+
136
+ /**
137
+ * Get task options
138
+ */
139
+ getOptions(): TaskOptions {
140
+ return { ...this.options };
141
+ }
142
+
143
+ /**
144
+ * Check if task can start
145
+ */
146
+ protected canStart(): boolean {
147
+ const { status } = this.state;
148
+ return (
149
+ status !== TaskStatus.STARTED &&
150
+ status !== TaskStatus.STARTING &&
151
+ status !== TaskStatus.STOPPING
152
+ );
153
+ }
154
+
155
+ /**
156
+ * Start task
157
+ */
158
+ async start(): Promise<void> {
159
+ if (!this.canStart()) {
160
+ return;
161
+ }
162
+
163
+ this.updateState({
164
+ status: TaskStatus.STARTING,
165
+ iteration: this.state.iteration + 1,
166
+ pid: -1,
167
+ exitCode: undefined,
168
+ error: undefined,
169
+ });
170
+
171
+ try {
172
+ await this.initLogStreams();
173
+
174
+ const {
175
+ cwd,
176
+ command,
177
+ arguments: args = [],
178
+ env,
179
+ shell = true,
180
+ } = this.options;
181
+
182
+ this.process = spawn(command, args, {
183
+ cwd,
184
+ env: env as NodeJS.ProcessEnv,
185
+ shell,
186
+ stdio: ['ignore', 'pipe', 'pipe'],
187
+ } as SpawnOptionsWithoutStdio);
188
+
189
+ this.addProcessListeners();
190
+ this.setTTLTimer();
191
+
192
+ this.updateState({
193
+ status: TaskStatus.STARTED,
194
+ pid: this.process!.pid,
195
+ });
196
+ } catch (error) {
197
+ this.onError(error as Error);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Check if task can stop
203
+ */
204
+ protected canStop(): boolean {
205
+ const { status } = this.state;
206
+ return !!this.process && status === TaskStatus.STARTED;
207
+ }
208
+
209
+ /**
210
+ * Stop task
211
+ * @param reason
212
+ */
213
+ async stop(reason: string = ''): Promise<void> {
214
+ if (!this.canStop()) {
215
+ return;
216
+ }
217
+
218
+ this.updateState({
219
+ status: TaskStatus.STOPPING,
220
+ reason,
221
+ });
222
+
223
+ return new Promise(resolve => {
224
+ const signal = this.options.stopSignal || 'SIGTERM';
225
+ this.process!.kill(signal as NodeJS.Signals);
226
+ this.process!.on('exit', () => {
227
+ resolve();
228
+ });
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Restart task
234
+ */
235
+ async restart(): Promise<void> {
236
+ await this.stop('restart');
237
+ this.start();
238
+ }
239
+
240
+ /**
241
+ * Update task state
242
+ */
243
+ protected updateState(state: Partial<TaskState>): void {
244
+ const newState = { ...this.state, ...state, timestamp: Date.now() };
245
+ this.state = Object.fromEntries(
246
+ Object.entries(newState).filter(([_, value]) => {
247
+ return value !== undefined;
248
+ })
249
+ ) as unknown as TaskState;
250
+ this.emit(TaskEvent.STATE_CHANGE, this.getState());
251
+ this.emit(this.state.status, this.getState());
252
+ }
253
+
254
+ /**
255
+ * Initialize log streams
256
+ */
257
+ protected async initLogStreams(): Promise<void> {
258
+ const { stdout = null, stderr = null } = this.options.log || {};
259
+ if (stdout) {
260
+ const stdOutPath = resolve(stdout);
261
+ await mkdir(dirname(stdOutPath), { recursive: true });
262
+ this.stdoutStream = createWriteStream(stdout, {
263
+ flags: 'w',
264
+ });
265
+ }
266
+ if (stderr) {
267
+ const stdErrPath = resolve(stderr);
268
+ await mkdir(dirname(stdErrPath), { recursive: true });
269
+ this.stderrStream = createWriteStream(stderr, {
270
+ flags: 'w',
271
+ });
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Set TTL timer
277
+ */
278
+ protected setTTLTimer(): void {
279
+ const { ttl } = this.options;
280
+ if (ttl && this.process) {
281
+ this.ttlTimer = setTimeout(() => {
282
+ this.stop('ttl');
283
+ }, ttl);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Add process listeners
289
+ */
290
+ protected addProcessListeners(): void {
291
+ if (!this.process) return;
292
+
293
+ this.process.stdout?.on('data', (data: Buffer) => this.onStdOutData(data));
294
+
295
+ this.process.stderr?.on('data', (data: Buffer) => this.onStdErrData(data));
296
+
297
+ this.process.on('exit', (code: number | null) => this.onExit(code));
298
+
299
+ this.process.on('error', (error: Error) => this.onError(error));
300
+ }
301
+
302
+ /**
303
+ * On standard output data
304
+ */
305
+ protected onStdOutData(data: Buffer): void {
306
+ const output = data.toString();
307
+ this.emit(TaskEvent.STDOUT, output);
308
+ if (this.stdoutStream) {
309
+ this.stdoutStream!.write(output);
310
+ }
311
+ }
312
+
313
+ /**
314
+ * On standard error data
315
+ */
316
+ protected onStdErrData(data: Buffer): void {
317
+ const output = data.toString();
318
+ this.emit(TaskEvent.STDERR, output);
319
+ if (this.stderrStream) {
320
+ this.stderrStream!.write(output);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * On exit
326
+ */
327
+ protected onExit(code: number | null): void {
328
+ const exitCode = code !== null ? code : SILENT_EXIT_CODE;
329
+
330
+ this.updateState({
331
+ status:
332
+ exitCode === 0
333
+ ? TaskStatus.COMPLETED
334
+ : exitCode === SILENT_EXIT_CODE
335
+ ? TaskStatus.STOPPED
336
+ : TaskStatus.FAILED,
337
+ exitCode,
338
+ });
339
+
340
+ this.cleanup();
341
+ }
342
+
343
+ /**
344
+ * On error
345
+ */
346
+ protected onError(error: Error): void {
347
+ this.updateState({
348
+ status: TaskStatus.FAILED,
349
+ error,
350
+ });
351
+
352
+ this.cleanup();
353
+ }
354
+
355
+ /**
356
+ * Cleanup
357
+ */
358
+ protected cleanup(): void {
359
+ if (this.ttlTimer) {
360
+ clearTimeout(this.ttlTimer);
361
+ this.ttlTimer = null;
362
+ }
363
+
364
+ if (this.process) {
365
+ this.process.removeAllListeners();
366
+ this.process.stdout?.removeAllListeners();
367
+ this.process.stderr?.removeAllListeners();
368
+ this.process = null;
369
+ }
370
+
371
+ if (this.stdoutStream) {
372
+ this.stdoutStream.end();
373
+ this.stdoutStream = null;
374
+ }
375
+
376
+ if (this.stderrStream) {
377
+ this.stderrStream.end();
378
+ this.stderrStream = null;
379
+ }
380
+ }
381
+ }
package/src/trigger.ts ADDED
@@ -0,0 +1,170 @@
1
+ import { ProjectAction } from './project-config';
2
+
3
+ /**
4
+ * Base runium trigger options
5
+ */
6
+ export interface RuniumTriggerOptions {
7
+ id: string;
8
+ action: ProjectAction;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ /**
13
+ * Base runium trigger project accessible interface
14
+ */
15
+ export interface RuniumTriggerProjectAccessible {
16
+ processAction(action: ProjectAction): void;
17
+ on: NodeJS.EventEmitter['on'];
18
+ off: NodeJS.EventEmitter['off'];
19
+ }
20
+
21
+ /**
22
+ * Runium trigger constructor
23
+ */
24
+ export type RuniumTriggerConstructor<Options extends RuniumTriggerOptions> =
25
+ new (
26
+ options: Options,
27
+ project: RuniumTriggerProjectAccessible
28
+ ) => RuniumTrigger<Options>;
29
+
30
+ /**
31
+ * Base runium trigger class
32
+ */
33
+ export abstract class RuniumTrigger<Options extends RuniumTriggerOptions> {
34
+ protected project: RuniumTriggerProjectAccessible;
35
+ protected id: string;
36
+ protected action: ProjectAction;
37
+ protected disabled: boolean;
38
+
39
+ constructor(options: Options, project: RuniumTriggerProjectAccessible) {
40
+ this.id = options.id;
41
+ this.action = options.action;
42
+ this.disabled = options.disabled ?? false;
43
+ this.project = project;
44
+ }
45
+
46
+ abstract enable(): void;
47
+ abstract disable(): void;
48
+
49
+ /**
50
+ * Get trigger id
51
+ */
52
+ getId(): string {
53
+ return this.id;
54
+ }
55
+
56
+ /**
57
+ * Check if trigger is disabled
58
+ */
59
+ isDisabled(): boolean {
60
+ return this.disabled;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Event trigger options
66
+ */
67
+ export interface EventTriggerOptions extends RuniumTriggerOptions {
68
+ event: string;
69
+ }
70
+
71
+ /**
72
+ * Event trigger class
73
+ */
74
+ export class EventTrigger extends RuniumTrigger<EventTriggerOptions> {
75
+ private readonly event: string;
76
+
77
+ constructor(
78
+ options: EventTriggerOptions,
79
+ project: RuniumTriggerProjectAccessible
80
+ ) {
81
+ super(options, project);
82
+ this.event = options.event;
83
+ }
84
+
85
+ enable(): void {
86
+ this.project.on(this.event, this.handler);
87
+ this.disabled = false;
88
+ }
89
+
90
+ disable(): void {
91
+ this.project.off(this.event, this.handler);
92
+ this.disabled = true;
93
+ }
94
+
95
+ private handler = () => {
96
+ this.project.processAction(this.action);
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Interval trigger options
102
+ */
103
+ export interface IntervalTriggerOptions extends RuniumTriggerOptions {
104
+ interval: number;
105
+ }
106
+
107
+ /**
108
+ * Interval trigger class
109
+ */
110
+ export class IntervalTrigger extends RuniumTrigger<IntervalTriggerOptions> {
111
+ private readonly interval: number;
112
+ private intervalId: NodeJS.Timeout | null = null;
113
+
114
+ constructor(
115
+ options: IntervalTriggerOptions,
116
+ project: RuniumTriggerProjectAccessible
117
+ ) {
118
+ super(options, project);
119
+ this.interval = options.interval;
120
+ }
121
+
122
+ enable(): void {
123
+ this.intervalId = setInterval(() => {
124
+ this.project.processAction(this.action);
125
+ }, this.interval);
126
+ this.disabled = false;
127
+ }
128
+
129
+ disable(): void {
130
+ this.intervalId && clearInterval(this.intervalId);
131
+ this.disabled = true;
132
+ this.intervalId = null;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Timeout trigger options
138
+ */
139
+ export interface TimeoutTriggerOptions extends RuniumTriggerOptions {
140
+ timeout: number;
141
+ }
142
+
143
+ /**
144
+ * Timeout trigger class
145
+ */
146
+ export class TimeoutTrigger extends RuniumTrigger<TimeoutTriggerOptions> {
147
+ private readonly timeout: number;
148
+ private timeoutId: NodeJS.Timeout | null = null;
149
+
150
+ constructor(
151
+ options: TimeoutTriggerOptions,
152
+ project: RuniumTriggerProjectAccessible
153
+ ) {
154
+ super(options, project);
155
+ this.timeout = options.timeout;
156
+ }
157
+
158
+ enable(): void {
159
+ this.timeoutId = setTimeout(() => {
160
+ this.project.processAction(this.action);
161
+ }, this.timeout);
162
+ this.disabled = false;
163
+ }
164
+
165
+ disable(): void {
166
+ this.timeoutId && clearTimeout(this.timeoutId);
167
+ this.disabled = true;
168
+ this.timeoutId = null;
169
+ }
170
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "target": "ES2022",
5
+ "module": "NodeNext",
6
+ "lib": [
7
+ "ES2022"
8
+ ],
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "moduleResolution": "node",
16
+ "resolveJsonModule": true,
17
+ "declaration": true,
18
+ "sourceMap": false,
19
+ "strictNullChecks": true,
20
+ "removeComments": true
21
+ },
22
+ "include": [
23
+ "src/**/*"
24
+ ],
25
+ "exclude": [
26
+ "node_modules",
27
+ "dist"
28
+ ]
29
+ }