@runium/core 0.0.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.prettierrc.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "semi": true,
3
- "trailingComma": "es5",
4
- "singleQuote": true,
5
- "printWidth": 80,
6
- "tabWidth": 2,
7
- "useTabs": false,
8
- "endOfLine": "lf",
9
- "arrowParens": "avoid"
10
- }
package/README.md DELETED
@@ -1 +0,0 @@
1
- # @runium/core
package/src/error.ts DELETED
@@ -1,22 +0,0 @@
1
- /**
2
- * Runium error class
3
- */
4
- export class RuniumError extends Error {
5
- code: string;
6
-
7
- payload: unknown;
8
-
9
- constructor(message: string, code: string, payload: unknown = null) {
10
- super(message);
11
- this.code = code;
12
- this.payload = payload;
13
- }
14
- }
15
-
16
- /**
17
- * Checks if the given error is a RuniumError
18
- * @param error
19
- */
20
- export function isRuniumError(error: unknown): boolean {
21
- return !!error && error instanceof RuniumError;
22
- }
package/src/file.ts DELETED
@@ -1,53 +0,0 @@
1
- import { readFile, writeFile } from 'node:fs/promises';
2
- import { RuniumError } from './error';
3
-
4
- export type JSONValue = string | number | boolean | JSONObject | JSONValue[];
5
-
6
- export interface JSONObject {
7
- [x: string]: JSONValue;
8
- }
9
-
10
- /**
11
- * File error codes
12
- */
13
- export enum FileErrorCode {
14
- READ_JSON = 'file-read-json',
15
- WRITE_JSON = 'file-write-json',
16
- }
17
-
18
- /**
19
- * Reads a JSON file
20
- * @param path
21
- */
22
- export async function readJsonFile<T = JSONValue>(path: string): Promise<T> {
23
- try {
24
- const data = await readFile(path, { encoding: 'utf-8' });
25
- return JSON.parse(data);
26
- } catch (ex) {
27
- throw new RuniumError(
28
- `Can not read JSON file ${path}`,
29
- FileErrorCode.READ_JSON,
30
- { path, original: ex }
31
- );
32
- }
33
- }
34
-
35
- /**
36
- * Writes a JSON file
37
- * @param path
38
- * @param data
39
- */
40
- export async function writeJsonFile<T = JSONValue>(
41
- path: string,
42
- data: T
43
- ): Promise<void> {
44
- try {
45
- await writeFile(path, JSON.stringify(data, null, 2), { encoding: 'utf-8' });
46
- } catch (ex) {
47
- throw new RuniumError(
48
- `Can not write JSON file ${path}`,
49
- FileErrorCode.WRITE_JSON,
50
- { path, data, original: ex }
51
- );
52
- }
53
- }
package/src/index.ts DELETED
@@ -1,8 +0,0 @@
1
- export * from './error';
2
- export * from './file';
3
- export * from './macros';
4
- export * from './project';
5
- export * from './project-config';
6
- export * from './project-schema';
7
- export * from './task';
8
- export * from './trigger';
package/src/macros.ts DELETED
@@ -1,91 +0,0 @@
1
- import { RuniumError } from './error';
2
-
3
- export type Macro = (...params: string[]) => string;
4
-
5
- export type MacrosCollection = Record<string, Macro>;
6
-
7
- const UNWRAP_MACROS_PATTERN = String.raw`"\$unwrap\((.*)\)"`;
8
-
9
- export enum MacrosErrorCode {
10
- MACRO_APPLY_ERROR = 'macro-apply-error',
11
- }
12
-
13
- /**
14
- * Apply unwrap macro
15
- * @param value
16
- */
17
- function unwrap(value: string): string {
18
- const unwrapRegex = new RegExp(UNWRAP_MACROS_PATTERN, 'g');
19
-
20
- return value.replace(unwrapRegex, (match: string, value: string) => {
21
- return value.trim();
22
- });
23
- }
24
-
25
- /**
26
- * Apply macros to a text
27
- * @param text
28
- * @param macros
29
- */
30
- export function applyMacros(text: string, macros: MacrosCollection): string {
31
- const macrosNames = Object.keys(macros);
32
-
33
- const pattern = String.raw`\$(${macrosNames.join('|')})\(([^()]*(?:\([^()]*\)[^()]*)*)\)`;
34
-
35
- const apply = (data: string): string => {
36
- const macrosRegex = new RegExp(pattern, 'g');
37
-
38
- let result = data;
39
-
40
- let hasMacros = true;
41
- while (hasMacros) {
42
- hasMacros = false;
43
- result = result.replace(
44
- macrosRegex,
45
- (match: string, type: string, args: string) => {
46
- hasMacros = true;
47
-
48
- const macroArgs: string[] = [];
49
- let depth = 0;
50
- let current = '';
51
-
52
- for (const char of args) {
53
- if (char === ',' && depth === 0) {
54
- macroArgs.push(current.trim());
55
- current = '';
56
- } else {
57
- if (char === '(') {
58
- depth++;
59
- }
60
- if (char === ')') {
61
- depth--;
62
- }
63
- current += char;
64
- }
65
- }
66
- macroArgs.push(current.trim());
67
-
68
- const values = macroArgs.map(arg => {
69
- return arg ? apply(arg) : '';
70
- });
71
- try {
72
- return macros[type](...values);
73
- } catch (ex) {
74
- throw new RuniumError(
75
- `Can not apply macro "${type}"`,
76
- MacrosErrorCode.MACRO_APPLY_ERROR,
77
- { type, args: macroArgs, original: ex }
78
- );
79
- }
80
- }
81
- );
82
- }
83
-
84
- return result;
85
- };
86
-
87
- // process text
88
- const processedText = macrosNames.length ? apply(text) : text;
89
- // apply unwrap macros
90
- return unwrap(processedText);
91
- }
@@ -1,483 +0,0 @@
1
- import Ajv from 'ajv/dist/2020.js';
2
- import ajvKeywords from 'ajv-keywords';
3
- import { RuniumTaskState, TaskOptions } from './task';
4
- import { RuniumError } from './error';
5
-
6
- /**
7
- * Project task start mode
8
- */
9
- export enum ProjectTaskStartMode {
10
- IMMEDIATE = 'immediate',
11
- DEFERRED = 'deferred',
12
- IGNORE = 'ignore',
13
- }
14
-
15
- /**
16
- * Project action type
17
- */
18
- export enum ProjectActionType {
19
- EMIT_EVENT = 'emit-event',
20
- START_TASK = 'start-task',
21
- RESTART_TASK = 'restart-task',
22
- STOP_TASK = 'stop-task',
23
- STOP_PROJECT = 'stop-project',
24
- ENABLE_TRIGGER = 'enable-trigger',
25
- DISABLE_TRIGGER = 'disable-trigger',
26
- }
27
-
28
- /**
29
- * Project task state condition
30
- */
31
- export type ProjectTaskStateCondition =
32
- | string
33
- | boolean
34
- | Partial<RuniumTaskState>
35
- | ((state: RuniumTaskState) => boolean);
36
-
37
- /**
38
- * Project task handler
39
- */
40
- export interface ProjectTaskHandler {
41
- condition: ProjectTaskStateCondition;
42
- action: ProjectAction;
43
- }
44
-
45
- /**
46
- * Project task dependency
47
- */
48
- export interface ProjectTaskDependency {
49
- taskId: string;
50
- condition: ProjectTaskStateCondition;
51
- }
52
-
53
- /**
54
- * Project task type
55
- */
56
- export enum ProjectTaskType {
57
- DEFAULT = 'default',
58
- }
59
-
60
- /**
61
- * Project task config
62
- */
63
- export interface ProjectTaskConfig<Options = unknown> {
64
- id: string;
65
- options: Options;
66
- type?: ProjectTaskType | string;
67
- name?: string;
68
- mode?: ProjectTaskStartMode;
69
- dependencies?: ProjectTaskDependency[];
70
- handlers?: ProjectTaskHandler[];
71
- restart?: ProjectTaskRestartPolicy;
72
- }
73
-
74
- /**
75
- * Project default task config
76
- */
77
- export interface ProjectDefaultTaskConfig
78
- extends ProjectTaskConfig<TaskOptions> {
79
- type?: ProjectTaskType.DEFAULT;
80
- }
81
-
82
- /**
83
- * Project action emit event
84
- */
85
- export interface ProjectActionEmitEvent {
86
- type: ProjectActionType.EMIT_EVENT;
87
- event: string;
88
- }
89
-
90
- /**
91
- * Project action process task
92
- */
93
- export interface ProjectActionProcessTask {
94
- type:
95
- | ProjectActionType.START_TASK
96
- | ProjectActionType.RESTART_TASK
97
- | ProjectActionType.STOP_TASK;
98
- taskId: string;
99
- }
100
-
101
- /**
102
- * Project action stop project
103
- */
104
- export interface ProjectActionStopProject {
105
- type: ProjectActionType.STOP_PROJECT;
106
- }
107
-
108
- /**
109
- * Project action toggle trigger
110
- */
111
- export interface ProjectActionToggleTrigger {
112
- type: ProjectActionType.ENABLE_TRIGGER | ProjectActionType.DISABLE_TRIGGER;
113
- triggerId: string;
114
- }
115
-
116
- /**
117
- * Project custom action
118
- */
119
- export interface ProjectCustomAction {
120
- type: string;
121
- payload?: unknown;
122
- }
123
-
124
- /**
125
- * Project action
126
- */
127
- export type ProjectAction =
128
- | ProjectCustomAction
129
- | ProjectActionEmitEvent
130
- | ProjectActionProcessTask
131
- | ProjectActionStopProject
132
- | ProjectActionToggleTrigger;
133
-
134
- /**
135
- * Project trigger type
136
- */
137
- export enum ProjectTriggerType {
138
- EVENT = 'event',
139
- TIMEOUT = 'timeout',
140
- INTERVAL = 'interval',
141
- }
142
-
143
- /**
144
- * Project trigger base
145
- */
146
- interface ProjectTriggerBase {
147
- id: string;
148
- type: ProjectTriggerType | string;
149
- action: ProjectAction;
150
- disabled?: boolean;
151
- }
152
-
153
- /**
154
- * Project trigger event
155
- */
156
- export interface ProjectTriggerEvent extends ProjectTriggerBase {
157
- type: ProjectTriggerType.EVENT;
158
- event: string;
159
- }
160
-
161
- /**
162
- * Project trigger interval
163
- */
164
- export interface ProjectTriggerInterval extends ProjectTriggerBase {
165
- type: ProjectTriggerType.INTERVAL;
166
- interval: number;
167
- }
168
-
169
- /**
170
- * Project trigger timeout
171
- */
172
- export interface ProjectTriggerTimeout extends ProjectTriggerBase {
173
- type: ProjectTriggerType.TIMEOUT;
174
- timeout: number;
175
- }
176
-
177
- /**
178
- * Project custom trigger
179
- */
180
- export interface ProjectCustomTrigger extends ProjectTriggerBase {
181
- type: string;
182
- payload?: unknown;
183
- }
184
-
185
- /**
186
- * Project trigger
187
- */
188
- export type ProjectTrigger =
189
- | ProjectTriggerEvent
190
- | ProjectTriggerInterval
191
- | ProjectTriggerTimeout
192
- | ProjectCustomTrigger;
193
-
194
- /**
195
- * Project task restart policy type
196
- */
197
- export enum ProjectTaskRestartPolicyType {
198
- ALWAYS = 'always',
199
- ON_FAILURE = 'on-failure',
200
- }
201
-
202
- /**
203
- * Project task restart policy always
204
- */
205
- export interface ProjectTaskRestartPolicyAlways {
206
- policy: ProjectTaskRestartPolicyType.ALWAYS;
207
- delay?: number;
208
- }
209
-
210
- /**
211
- * Project task restart policy on failure
212
- */
213
- export interface ProjectTaskRestartPolicyOnFailure {
214
- policy: ProjectTaskRestartPolicyType.ON_FAILURE;
215
- delay?: number;
216
- maxRetries?: number;
217
- }
218
-
219
- /**
220
- * Project task restart policy
221
- */
222
- export type ProjectTaskRestartPolicy =
223
- | ProjectTaskRestartPolicyAlways
224
- | ProjectTaskRestartPolicyOnFailure;
225
-
226
- /**
227
- * Project config
228
- */
229
- export interface ProjectConfig {
230
- id: string;
231
- name?: string;
232
- tasks: ProjectTaskConfig[];
233
- triggers?: ProjectTrigger[];
234
- }
235
-
236
- /**
237
- * Project config error codes
238
- */
239
- export enum ProjectConfigErrorCode {
240
- INCORRECT_DATA = 'project-config-incorrect-data',
241
- TASKS_CIRCULAR_DEPENDENCY = 'project-config-tasks-circular-dependency',
242
- TASK_NOT_EXISTS = 'project-config-task-not-exists',
243
- TRIGGER_NOT_EXISTS = 'project-config-trigger-not-exists',
244
- }
245
-
246
- /**
247
- * Process task action types
248
- */
249
- const PROCESS_TASK_ACTION_TYPES = new Set<ProjectActionType>([
250
- ProjectActionType.START_TASK,
251
- ProjectActionType.RESTART_TASK,
252
- ProjectActionType.STOP_TASK,
253
- ]);
254
-
255
- /**
256
- * Toggle trigger action types
257
- */
258
- const TOGGLE_TRIGGER_ACTION_TYPES = new Set<ProjectActionType>([
259
- ProjectActionType.ENABLE_TRIGGER,
260
- ProjectActionType.DISABLE_TRIGGER,
261
- ]);
262
-
263
- /**
264
- * Runium action types
265
- */
266
- const RUNIUM_ACTION_TYPES = new Set(Object.values(ProjectActionType));
267
-
268
- /**
269
- * Runium trigger types
270
- */
271
- const RUNIUM_TRIGGER_TYPES = new Set(Object.values(ProjectTriggerType));
272
-
273
- /**
274
- * Is process task action guard
275
- * @param action
276
- */
277
- export function isProcessTaskAction(
278
- action: ProjectAction
279
- ): action is ProjectActionProcessTask {
280
- return PROCESS_TASK_ACTION_TYPES.has(action.type as ProjectActionType);
281
- }
282
-
283
- /**
284
- * Is toggle trigger action guard
285
- * @param action
286
- */
287
- export function isToggleTriggerAction(
288
- action: ProjectAction
289
- ): action is ProjectActionToggleTrigger {
290
- return TOGGLE_TRIGGER_ACTION_TYPES.has(action.type as ProjectActionType);
291
- }
292
-
293
- /**
294
- * Is custom action guard
295
- * @param action
296
- */
297
- export function isCustomAction(
298
- action: ProjectAction
299
- ): action is ProjectCustomAction {
300
- return !RUNIUM_ACTION_TYPES.has(action.type as ProjectActionType);
301
- }
302
-
303
- /**
304
- * Is custom trigger guard
305
- * @param trigger
306
- */
307
- export function isCustomTrigger(
308
- trigger: ProjectTrigger
309
- ): trigger is ProjectCustomTrigger {
310
- return !RUNIUM_TRIGGER_TYPES.has(trigger.type as ProjectTriggerType);
311
- }
312
-
313
- const ajv = new Ajv({ allowUnionTypes: true, allErrors: true, verbose: true });
314
- ajvKeywords(ajv, ['uniqueItemProperties']);
315
-
316
- /**
317
- * Validate project schema
318
- * @param project
319
- * @param schema
320
- */
321
- function validateSchema(project: ProjectConfig, schema: object): void {
322
- const validate = ajv.compile(schema || {});
323
- const result = validate(project);
324
- if (!result) {
325
- throw new RuniumError(
326
- 'Incorrect project config data',
327
- ProjectConfigErrorCode.INCORRECT_DATA,
328
- {
329
- errors: validate.errors,
330
- }
331
- );
332
- }
333
- }
334
-
335
- /**
336
- * Detect cycles in dependencies
337
- * @param deps
338
- */
339
- function detectCycles(deps: Map<string, string[]>) {
340
- const visited: Set<string> = new Set();
341
- const stack: Set<string> = new Set();
342
-
343
- const dfs = (node: string) => {
344
- if (stack.has(node)) {
345
- const cycle = [...stack, node].join(' -> ');
346
- throw new RuniumError(
347
- `Project config tasks circular dependency detected ${cycle}`,
348
- ProjectConfigErrorCode.TASKS_CIRCULAR_DEPENDENCY,
349
- {
350
- task: node,
351
- dependencies: [...stack],
352
- }
353
- );
354
- }
355
- if (visited.has(node)) return;
356
-
357
- stack.add(node);
358
- for (const neighbor of deps.get(node) || []) {
359
- dfs(neighbor);
360
- }
361
- stack.delete(node);
362
- visited.add(node);
363
- };
364
-
365
- for (const node of deps.keys()) {
366
- if (!visited.has(node)) {
367
- dfs(node);
368
- }
369
- }
370
- }
371
-
372
- /**
373
- * Validate existing elements
374
- * @param project
375
- */
376
- function validateExistingElements(project: ProjectConfig): void {
377
- // task dependencies graph
378
- const deps: Map<string, string[]> = new Map();
379
-
380
- // task ids
381
- const taskIds: Set<string> = new Set();
382
- for (const task of project.tasks) {
383
- taskIds.add(task.id);
384
- deps.set(task.id, []);
385
- }
386
-
387
- const triggerIds: Set<string> = new Set(
388
- project.triggers?.map(t => t.id) || []
389
- );
390
-
391
- // check every task dependencies, handler actions use existing task and existing triggers
392
- for (const task of project.tasks) {
393
- for (const dependency of task.dependencies || []) {
394
- if (!taskIds.has(dependency.taskId)) {
395
- throw new RuniumError(
396
- `Task "${task.id}" depends on not existing task "${dependency.taskId}"`,
397
- ProjectConfigErrorCode.TASK_NOT_EXISTS,
398
- {
399
- taskId: task.id,
400
- dependency: { ...dependency },
401
- scope: 'dependencies',
402
- }
403
- );
404
- }
405
- deps.get(dependency.taskId)!.push(task.id);
406
- }
407
-
408
- for (const handler of task.handlers || []) {
409
- if (
410
- isProcessTaskAction(handler.action) &&
411
- !taskIds.has(handler.action.taskId)
412
- ) {
413
- throw new RuniumError(
414
- `Task "${task.id}" handler action uses not existing task "${handler.action.taskId}"`,
415
- ProjectConfigErrorCode.TASK_NOT_EXISTS,
416
- {
417
- taskId: task.id,
418
- handler: { ...handler },
419
- scope: 'handlers',
420
- }
421
- );
422
- } else if (
423
- isToggleTriggerAction(handler.action) &&
424
- !triggerIds.has(handler.action.triggerId)
425
- ) {
426
- throw new RuniumError(
427
- `Task "${task.id}" handler action uses not existing trigger "${handler.action.triggerId}"`,
428
- ProjectConfigErrorCode.TRIGGER_NOT_EXISTS,
429
- {
430
- taskId: task.id,
431
- handler: { ...handler },
432
- scope: 'handlers',
433
- }
434
- );
435
- }
436
- }
437
- }
438
-
439
- // check project triggers use existing tasks and triggers
440
- for (const trigger of project.triggers || []) {
441
- if (
442
- isProcessTaskAction(trigger.action) &&
443
- !taskIds.has(trigger.action.taskId)
444
- ) {
445
- throw new RuniumError(
446
- `Trigger "${trigger.id}" action uses not existing task "${trigger.action.taskId}"`,
447
- ProjectConfigErrorCode.TASK_NOT_EXISTS,
448
- {
449
- trigger: { ...trigger },
450
- scope: 'triggers',
451
- }
452
- );
453
- } else if (
454
- isToggleTriggerAction(trigger.action) &&
455
- !triggerIds.has(trigger.action.triggerId)
456
- ) {
457
- throw new RuniumError(
458
- `Trigger "${trigger.id}" action uses not existing trigger "${trigger.action.triggerId}"`,
459
- ProjectConfigErrorCode.TRIGGER_NOT_EXISTS,
460
- {
461
- trigger: { ...trigger },
462
- scope: 'triggers',
463
- }
464
- );
465
- }
466
- }
467
-
468
- // detect circular and non unique dependencies
469
- detectCycles(deps);
470
- }
471
-
472
- /**
473
- * Validate project
474
- * @param project
475
- * @param schema
476
- */
477
- export function validateProject(project: ProjectConfig, schema: object): void {
478
- // validate schema
479
- validateSchema(project, schema);
480
-
481
- // validate existing elements
482
- validateExistingElements(project);
483
- }