@quatrain/worker 1.1.42 → 1.1.43
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.md +15 -0
- package/lib/Worker.test.d.ts +1 -0
- package/lib/Worker.test.js +219 -0
- package/package.json +44 -45
- package/src/FileSystem.ts +186 -0
- package/src/Handler.ts +50 -0
- package/src/Helpers.ts +40 -0
- package/src/Worker.test.ts +289 -0
- package/src/Worker.ts +173 -0
- package/src/index.ts +16 -0
- package/src/types/HandlerParameters.ts +10 -0
- package/src/types/MessagehandlerParameters.ts +4 -0
- package/src/types/ModeEnum.ts +5 -0
- package/lib/types/HandlerParameters copy.d.ts +0 -9
- package/lib/types/HandlerParameters copy.js +0 -2
- package/lib/types/ModeTypes.d.ts +0 -5
- package/lib/types/ModeTypes.js +0 -9
package/LICENSE.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# LICENSE UPDATE NOTICE
|
|
2
|
+
|
|
3
|
+
As of 01/01/2026, Quatrain Core is licensed under the **GNU Affero General Public License v3.0 (AGPL v3)**.
|
|
4
|
+
Previous versions remain under the MIT License.
|
|
5
|
+
|
|
6
|
+
## Why AGPL?
|
|
7
|
+
|
|
8
|
+
We believe in open collaboration for the development ecosystem. The AGPL ensures that any modification or deployment of this BaaS stack, including over a network, benefits the entire community.
|
|
9
|
+
|
|
10
|
+
## Commercial Services & Enterprise Usage
|
|
11
|
+
|
|
12
|
+
We provide official deployment services, technical training, and certification for Quatrain Core.
|
|
13
|
+
For organizations requiring a non-copyleft license (commercial license) or custom proprietary integrations, please contact the copyright holder: **Quatrain Technologies**.
|
|
14
|
+
|
|
15
|
+
Copyright © 2024-2026 Quatrain Technologies. All Rights Reserved.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const Worker_1 = require("./Worker");
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
jest.mock('axios');
|
|
11
|
+
jest.mock('child_process');
|
|
12
|
+
const mockedAxios = axios_1.default;
|
|
13
|
+
const mockedSpawn = child_process_1.spawn;
|
|
14
|
+
describe('Worker', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Clear all mocks before each test
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
// Reset Worker.endpoint
|
|
19
|
+
Worker_1.Worker.endpoint = '';
|
|
20
|
+
});
|
|
21
|
+
describe('endpoint property', () => {
|
|
22
|
+
it('should have an empty endpoint by default', () => {
|
|
23
|
+
expect(Worker_1.Worker.endpoint).toBe('');
|
|
24
|
+
});
|
|
25
|
+
it('should allow setting the endpoint', () => {
|
|
26
|
+
Worker_1.Worker.endpoint = 'https://api.example.com/events';
|
|
27
|
+
expect(Worker_1.Worker.endpoint).toBe('https://api.example.com/events');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('pushEvent', () => {
|
|
31
|
+
it('should return false when endpoint is not set', () => {
|
|
32
|
+
const result = Worker_1.Worker.pushEvent('test-event');
|
|
33
|
+
expect(result).toBe(false);
|
|
34
|
+
expect(mockedAxios.patch).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
it('should send event when endpoint is set', () => {
|
|
37
|
+
Worker_1.Worker.endpoint = 'https://api.example.com/events';
|
|
38
|
+
mockedAxios.patch.mockResolvedValue({
|
|
39
|
+
statusText: 'OK',
|
|
40
|
+
data: { success: true },
|
|
41
|
+
});
|
|
42
|
+
Worker_1.Worker.pushEvent('test-event', { foo: 'bar' });
|
|
43
|
+
expect(mockedAxios.patch).toHaveBeenCalledWith('https://api.example.com/events', expect.objectContaining({
|
|
44
|
+
event: 'test-event',
|
|
45
|
+
foo: 'bar',
|
|
46
|
+
}));
|
|
47
|
+
});
|
|
48
|
+
it('should include custom timestamp in payload', () => {
|
|
49
|
+
Worker_1.Worker.endpoint = 'https://api.example.com/events';
|
|
50
|
+
const customTs = 1234567890;
|
|
51
|
+
mockedAxios.patch.mockResolvedValue({
|
|
52
|
+
statusText: 'OK',
|
|
53
|
+
data: { success: true },
|
|
54
|
+
});
|
|
55
|
+
Worker_1.Worker.pushEvent('test-event', {}, customTs);
|
|
56
|
+
expect(mockedAxios.patch).toHaveBeenCalledWith('https://api.example.com/events', expect.objectContaining({
|
|
57
|
+
event: 'test-event',
|
|
58
|
+
ts: expect.any(Number),
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
61
|
+
it('should include additional data in payload', () => {
|
|
62
|
+
Worker_1.Worker.endpoint = 'https://api.example.com/events';
|
|
63
|
+
mockedAxios.patch.mockResolvedValue({
|
|
64
|
+
statusText: 'OK',
|
|
65
|
+
data: { success: true },
|
|
66
|
+
});
|
|
67
|
+
const additionalData = {
|
|
68
|
+
userId: '123',
|
|
69
|
+
action: 'upload',
|
|
70
|
+
metadata: { size: 1024 },
|
|
71
|
+
};
|
|
72
|
+
Worker_1.Worker.pushEvent('custom-event', additionalData);
|
|
73
|
+
expect(mockedAxios.patch).toHaveBeenCalledWith('https://api.example.com/events', expect.objectContaining({
|
|
74
|
+
event: 'custom-event',
|
|
75
|
+
userId: '123',
|
|
76
|
+
action: 'upload',
|
|
77
|
+
metadata: { size: 1024 },
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('pushEventAsync', () => {
|
|
82
|
+
it('should return false when endpoint is not set', async () => {
|
|
83
|
+
const result = await Worker_1.Worker.pushEventAsync('test-event');
|
|
84
|
+
expect(result).toBe(false);
|
|
85
|
+
expect(mockedAxios.patch).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
it('should send event and return true on success', async () => {
|
|
88
|
+
Worker_1.Worker.endpoint = 'https://api.example.com/events';
|
|
89
|
+
mockedAxios.patch.mockResolvedValue({
|
|
90
|
+
statusText: 'OK',
|
|
91
|
+
data: { success: true },
|
|
92
|
+
});
|
|
93
|
+
const result = await Worker_1.Worker.pushEventAsync('test-event', {
|
|
94
|
+
foo: 'bar',
|
|
95
|
+
});
|
|
96
|
+
expect(result).toBe(true);
|
|
97
|
+
expect(mockedAxios.patch).toHaveBeenCalledWith('https://api.example.com/events', expect.objectContaining({
|
|
98
|
+
event: 'test-event',
|
|
99
|
+
foo: 'bar',
|
|
100
|
+
}));
|
|
101
|
+
});
|
|
102
|
+
it('should return false on axios error', async () => {
|
|
103
|
+
Worker_1.Worker.endpoint = 'https://api.example.com/events';
|
|
104
|
+
mockedAxios.patch.mockRejectedValue(new Error('Network error'));
|
|
105
|
+
const result = await Worker_1.Worker.pushEventAsync('test-event');
|
|
106
|
+
expect(result).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
it('should handle non-OK status response', async () => {
|
|
109
|
+
Worker_1.Worker.endpoint = 'https://api.example.com/events';
|
|
110
|
+
mockedAxios.patch.mockResolvedValue({
|
|
111
|
+
statusText: 'Bad Request',
|
|
112
|
+
data: { error: 'Invalid payload' },
|
|
113
|
+
});
|
|
114
|
+
const result = await Worker_1.Worker.pushEventAsync('test-event');
|
|
115
|
+
expect(result).toBe(undefined);
|
|
116
|
+
});
|
|
117
|
+
it('should include custom timestamp', async () => {
|
|
118
|
+
Worker_1.Worker.endpoint = 'https://api.example.com/events';
|
|
119
|
+
const customTs = 9876543210;
|
|
120
|
+
mockedAxios.patch.mockResolvedValue({
|
|
121
|
+
statusText: 'OK',
|
|
122
|
+
data: { success: true },
|
|
123
|
+
});
|
|
124
|
+
await Worker_1.Worker.pushEventAsync('test-event', {}, customTs);
|
|
125
|
+
expect(mockedAxios.patch).toHaveBeenCalledWith('https://api.example.com/events', expect.objectContaining({
|
|
126
|
+
event: 'test-event',
|
|
127
|
+
ts: expect.any(Number),
|
|
128
|
+
}));
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('execPromise', () => {
|
|
132
|
+
it('should execute command successfully', async () => {
|
|
133
|
+
const mockChild = {
|
|
134
|
+
stdout: {
|
|
135
|
+
on: jest.fn((event, callback) => {
|
|
136
|
+
if (event === 'data') {
|
|
137
|
+
// Simulate stdout data
|
|
138
|
+
setTimeout(() => callback(Buffer.from('Command output')), 10);
|
|
139
|
+
}
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
stderr: {
|
|
143
|
+
on: jest.fn(),
|
|
144
|
+
},
|
|
145
|
+
on: jest.fn((event, callback) => {
|
|
146
|
+
if (event === 'close') {
|
|
147
|
+
// Simulate successful completion
|
|
148
|
+
setTimeout(() => callback(0), 20);
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
151
|
+
};
|
|
152
|
+
mockedSpawn.mockReturnValue(mockChild);
|
|
153
|
+
await expect(Worker_1.Worker.execPromise('ls', ['-la'], '/tmp')).resolves.toBeUndefined();
|
|
154
|
+
expect(mockedSpawn).toHaveBeenCalledWith('ls', ['-la'], {
|
|
155
|
+
cwd: '/tmp',
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
it('should reject on command failure', async () => {
|
|
159
|
+
const mockChild = {
|
|
160
|
+
stdout: {
|
|
161
|
+
on: jest.fn(),
|
|
162
|
+
},
|
|
163
|
+
stderr: {
|
|
164
|
+
on: jest.fn((event, callback) => {
|
|
165
|
+
if (event === 'data') {
|
|
166
|
+
setTimeout(() => callback(Buffer.from('Error output')), 10);
|
|
167
|
+
}
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
on: jest.fn((event, callback) => {
|
|
171
|
+
if (event === 'close') {
|
|
172
|
+
// Simulate failure with exit code 1
|
|
173
|
+
setTimeout(() => callback(1), 20);
|
|
174
|
+
}
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
mockedSpawn.mockReturnValue(mockChild);
|
|
178
|
+
await expect(Worker_1.Worker.execPromise('invalid-command', [])).rejects.toThrow('Process failed and returned code: 1');
|
|
179
|
+
});
|
|
180
|
+
it('should use current working directory by default', async () => {
|
|
181
|
+
const mockChild = {
|
|
182
|
+
stdout: { on: jest.fn() },
|
|
183
|
+
stderr: { on: jest.fn() },
|
|
184
|
+
on: jest.fn((event, callback) => {
|
|
185
|
+
if (event === 'close') {
|
|
186
|
+
setTimeout(() => callback(0), 10);
|
|
187
|
+
}
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
mockedSpawn.mockReturnValue(mockChild);
|
|
191
|
+
await Worker_1.Worker.execPromise('echo', ['test']);
|
|
192
|
+
expect(mockedSpawn).toHaveBeenCalledWith('echo', ['test'], {
|
|
193
|
+
cwd: process.cwd(),
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
it('should pass arguments to command', async () => {
|
|
197
|
+
const mockChild = {
|
|
198
|
+
stdout: { on: jest.fn() },
|
|
199
|
+
stderr: { on: jest.fn() },
|
|
200
|
+
on: jest.fn((event, callback) => {
|
|
201
|
+
if (event === 'close') {
|
|
202
|
+
setTimeout(() => callback(0), 10);
|
|
203
|
+
}
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
mockedSpawn.mockReturnValue(mockChild);
|
|
207
|
+
const args = ['arg1', 'arg2', '--flag'];
|
|
208
|
+
await Worker_1.Worker.execPromise('command', args, '/custom/path');
|
|
209
|
+
expect(mockedSpawn).toHaveBeenCalledWith('command', args, {
|
|
210
|
+
cwd: '/custom/path',
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe('logger', () => {
|
|
215
|
+
it('should have a logger instance', () => {
|
|
216
|
+
expect(Worker_1.Worker.logger).toBeDefined();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
package/package.json
CHANGED
|
@@ -1,46 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
2
|
+
"name": "@quatrain/worker",
|
|
3
|
+
"version": "1.1.43",
|
|
4
|
+
"description": "Container Worker helpers",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"bun": "src/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"LICENSE.md",
|
|
10
|
+
"src/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"author": "Quatrain Développement SAS <developers@quatrain.com>",
|
|
15
|
+
"license": "AGPL-3.0-only",
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@tsconfig/recommended": "^1.0.1",
|
|
18
|
+
"@types/fluent-ffmpeg": "^2",
|
|
19
|
+
"@types/fs-extra": "^11.0.4",
|
|
20
|
+
"@types/jest": "^27.0.3",
|
|
21
|
+
"@types/node": "^22.9.0",
|
|
22
|
+
"@types/object-hash": "^3.0.6",
|
|
23
|
+
"jest": "^30.2.0",
|
|
24
|
+
"jest-node-exports-resolver": "^1.1.6",
|
|
25
|
+
"trace-unhandled": "^2.0.1",
|
|
26
|
+
"ts-jest": "^27.1.2",
|
|
27
|
+
"ts-node": "^10.4.0",
|
|
28
|
+
"typescript": "^5.1.5"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@quatrain/core": "^1.1.43",
|
|
32
|
+
"@quatrain/queue": "^1.1.20",
|
|
33
|
+
"@quatrain/storage": "^1.1.24",
|
|
34
|
+
"axios": "^1.7.7",
|
|
35
|
+
"fluent-ffmpeg": "^2.1.2",
|
|
36
|
+
"fs-extra": "^11.2.0",
|
|
37
|
+
"node-fetch-native": "^1.6.4"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test-ci": "jest --runInBand",
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"wbuild": "tsc --watch",
|
|
43
|
+
"bump-to": "yarn version"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs, { readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import axios from 'axios'
|
|
4
|
+
import { Worker } from './Worker'
|
|
5
|
+
import fetch from 'node-fetch-native'
|
|
6
|
+
import * as ffmpeg from 'fluent-ffmpeg'
|
|
7
|
+
import { FileType } from '@quatrain/storage'
|
|
8
|
+
|
|
9
|
+
export class FileSystem {
|
|
10
|
+
static prepare(folder: string) {
|
|
11
|
+
Worker.debug(`Setting up process folder ${folder}`)
|
|
12
|
+
this.removeFolder(folder)
|
|
13
|
+
|
|
14
|
+
fs.mkdirSync(folder)
|
|
15
|
+
fs.mkdirSync(folder + path.sep + 'images')
|
|
16
|
+
fs.mkdirSync(folder + path.sep + 'vecto')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static makeFolder(folder: string) {
|
|
20
|
+
fs.mkdirSync(folder)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static removeFolder(folder: string, recursively = true) {
|
|
24
|
+
if (fs.existsSync(folder)) {
|
|
25
|
+
fs.readdirSync(folder).forEach((element) => {
|
|
26
|
+
const item = path.join(folder, element)
|
|
27
|
+
if (fs.lstatSync(item).isDirectory()) {
|
|
28
|
+
if (recursively !== true) {
|
|
29
|
+
throw new Error(`Folder contains folder`)
|
|
30
|
+
}
|
|
31
|
+
this.removeFolder(item)
|
|
32
|
+
} else {
|
|
33
|
+
fs.unlinkSync(item)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
fs.rmdirSync(folder)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static async downloadFile(url: string, filepath: string) {
|
|
42
|
+
try {
|
|
43
|
+
Worker.debug(`Downloading file at ${url} to ${filepath}`)
|
|
44
|
+
const writer = fs.createWriteStream(filepath)
|
|
45
|
+
const response = await axios.get(url, {
|
|
46
|
+
responseType: 'stream',
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
response.data.pipe(writer)
|
|
50
|
+
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
writer.on('finish', resolve)
|
|
53
|
+
writer.on('error', reject)
|
|
54
|
+
})
|
|
55
|
+
} catch (err) {
|
|
56
|
+
Worker.error(`Download error: ${(err as Error).message}`)
|
|
57
|
+
throw err
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static safeString(name: string) {
|
|
62
|
+
return name.replace(/\s+/g, '_')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Upload file to public URL and return meta data
|
|
67
|
+
* @param filename string
|
|
68
|
+
* @param meta
|
|
69
|
+
* @param mime
|
|
70
|
+
* @returns Promise
|
|
71
|
+
*/
|
|
72
|
+
static uploadFile(
|
|
73
|
+
filename: string,
|
|
74
|
+
meta: FileType,
|
|
75
|
+
mime = 'video/mp4'
|
|
76
|
+
): Promise<FileType> {
|
|
77
|
+
// try to get more file metadata
|
|
78
|
+
meta = { ...meta, ...FileSystem.getInfo(filename) }
|
|
79
|
+
Worker.info(`Uploading file ${filename} to ${meta.uploadUrl}`)
|
|
80
|
+
const { size } = fs.statSync(filename)
|
|
81
|
+
|
|
82
|
+
if (size < 32 * 1024) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
fetch(meta.uploadUrl, {
|
|
85
|
+
method: 'PUT',
|
|
86
|
+
mode: 'cors',
|
|
87
|
+
duplex: 'half',
|
|
88
|
+
body: readFileSync(filename),
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': meta.contentType || mime,
|
|
91
|
+
'Content-length': String(size),
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
.then(() => resolve({ ...meta, size, uploadUrl: undefined }))
|
|
95
|
+
.catch((err) => {
|
|
96
|
+
Worker.error(
|
|
97
|
+
`An error occured while uploading ${filename}: ${err.message}`
|
|
98
|
+
)
|
|
99
|
+
reject(err)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
try {
|
|
106
|
+
let done = 0 // total bytes uploaded
|
|
107
|
+
let prev = 0 // latest value of done stored when % done was displayed
|
|
108
|
+
const { size } = fs.statSync(filename)
|
|
109
|
+
const readStream = fs.createReadStream(filename)
|
|
110
|
+
|
|
111
|
+
const sizeMB = (size / (1024 * 1024)).toFixed(2)
|
|
112
|
+
|
|
113
|
+
readStream.on('data', (data) => {
|
|
114
|
+
done += data.length
|
|
115
|
+
if (done - prev >= size / 20) {
|
|
116
|
+
// only display message when 5% more has been uploaded
|
|
117
|
+
Worker.info(
|
|
118
|
+
`${((done / size) * 100).toFixed(2)}% - ${(
|
|
119
|
+
done /
|
|
120
|
+
(1024 * 1024)
|
|
121
|
+
).toFixed(2)}MB / ${sizeMB}MB`
|
|
122
|
+
)
|
|
123
|
+
prev = done
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
Worker.info(`Uploading file ${filename} with size ${size} bytes`)
|
|
128
|
+
fetch(meta.uploadUrl, {
|
|
129
|
+
method: 'PUT',
|
|
130
|
+
mode: 'cors',
|
|
131
|
+
duplex: 'half',
|
|
132
|
+
body: readStream,
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': meta.contentType || mime,
|
|
135
|
+
'Content-length': String(size),
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
.then(() => resolve({ ...meta, size, uploadUrl: undefined }))
|
|
139
|
+
.catch((err) => {
|
|
140
|
+
Worker.error(
|
|
141
|
+
`An error occured while uploading ${filename}: ${err.message}`
|
|
142
|
+
)
|
|
143
|
+
reject(err)
|
|
144
|
+
})
|
|
145
|
+
} catch (err) {
|
|
146
|
+
Worker.error(err)
|
|
147
|
+
reject(err)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Return meta data on given file
|
|
154
|
+
*/
|
|
155
|
+
static getInfo = (file: string): Promise<any> => {
|
|
156
|
+
if (file.endsWith('.mp4') || file.endsWith('.insv')) {
|
|
157
|
+
return new Promise((resolve, reject) =>
|
|
158
|
+
ffmpeg.ffprobe(file, (err: any, metadata: any) => {
|
|
159
|
+
if (err) {
|
|
160
|
+
Worker.error(err)
|
|
161
|
+
reject(err)
|
|
162
|
+
}
|
|
163
|
+
// Worker.debug(`ffprobe getInfo: ${JSON.stringify(metadata)}`)
|
|
164
|
+
const {
|
|
165
|
+
width,
|
|
166
|
+
height,
|
|
167
|
+
duration,
|
|
168
|
+
bit_rate: bitrate,
|
|
169
|
+
nb_frames: nbFramees,
|
|
170
|
+
} = metadata.streams[0]
|
|
171
|
+
const nb_frames: number = parseFloat(nbFramees as string)
|
|
172
|
+
const framerate = nb_frames / parseFloat(duration as string)
|
|
173
|
+
resolve({
|
|
174
|
+
width,
|
|
175
|
+
height,
|
|
176
|
+
framerate: parseInt(framerate.toFixed(0)),
|
|
177
|
+
duration: parseInt(duration as string),
|
|
178
|
+
bitrate,
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
// Handle other cases silently
|
|
184
|
+
return new Promise((resolve) => resolve({}))
|
|
185
|
+
}
|
|
186
|
+
}
|
package/src/Handler.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Worker } from './Worker'
|
|
2
|
+
|
|
3
|
+
export const handler = async (
|
|
4
|
+
messageHandler: Function,
|
|
5
|
+
config: any,
|
|
6
|
+
adapters: any
|
|
7
|
+
) => {
|
|
8
|
+
try {
|
|
9
|
+
Worker.info(
|
|
10
|
+
`Worker version ${require('../package.json').version} started in ${
|
|
11
|
+
config.MQ_MODE
|
|
12
|
+
} mode`
|
|
13
|
+
)
|
|
14
|
+
switch (config.MQ_MODE) {
|
|
15
|
+
case 'queue':
|
|
16
|
+
const { Queue } = require('@quatrain/queue')
|
|
17
|
+
Queue.addQueue(adapters.queue, 'default', true)
|
|
18
|
+
Queue.info(
|
|
19
|
+
`Connecting to amqp://${config.MQ_HOST}:${config.MQ_PORT}`
|
|
20
|
+
)
|
|
21
|
+
Queue.getQueue().listen(config.MQ_TOPIC, messageHandler, {
|
|
22
|
+
concurrency: config.MQ_CONCURRENCY,
|
|
23
|
+
gpu: config.MQ_GPU,
|
|
24
|
+
})
|
|
25
|
+
break
|
|
26
|
+
|
|
27
|
+
case 'cli':
|
|
28
|
+
case 'test':
|
|
29
|
+
Worker.warn(`Message received from CLI.`)
|
|
30
|
+
const json =
|
|
31
|
+
config.MQ_MODE === 'test'
|
|
32
|
+
? require('../test.json')
|
|
33
|
+
: process.env.JSON
|
|
34
|
+
|
|
35
|
+
if (!json) {
|
|
36
|
+
throw new Error(`CLI call with missing environment variables`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await messageHandler(json)
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
default:
|
|
43
|
+
Worker.error(`Unknown mode option: '${config.MQ_MODE}'`)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
Worker.error((error as Error).message)
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/Helpers.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Worker } from './Worker'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
export class Helpers {
|
|
5
|
+
static FFMPEG = '/usr/bin/ffmpeg'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate a thubnail from a video file at given frame position
|
|
9
|
+
* @param videoPath path to video file
|
|
10
|
+
* @param frame frame to extract thumbnail from
|
|
11
|
+
* @param width width of thumbnail (4/3 ratio)
|
|
12
|
+
* @returns
|
|
13
|
+
*/
|
|
14
|
+
static generateVideoThumbnail = async (
|
|
15
|
+
videoPath: string,
|
|
16
|
+
outputPath: string,
|
|
17
|
+
frame: number = 0,
|
|
18
|
+
width: number = 320
|
|
19
|
+
) => {
|
|
20
|
+
const resolution = `${width}x${Math.ceil(width * 0.75)}`
|
|
21
|
+
const ffmpegParams = [
|
|
22
|
+
'-i',
|
|
23
|
+
videoPath,
|
|
24
|
+
'-vframes',
|
|
25
|
+
'1',
|
|
26
|
+
'-vf',
|
|
27
|
+
String.raw`select=gte(n\,${frame})`,
|
|
28
|
+
'-s',
|
|
29
|
+
resolution,
|
|
30
|
+
'-ss',
|
|
31
|
+
'1',
|
|
32
|
+
outputPath,
|
|
33
|
+
'-y',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
Worker.info(`Generating Thumbnail : ${outputPath}`)
|
|
37
|
+
|
|
38
|
+
return await Worker.execPromise(Helpers.FFMPEG, ffmpegParams)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { Worker } from './Worker'
|
|
2
|
+
import axios from 'axios'
|
|
3
|
+
import { spawn } from 'child_process'
|
|
4
|
+
|
|
5
|
+
// Mock dependencies
|
|
6
|
+
jest.mock('axios')
|
|
7
|
+
jest.mock('child_process')
|
|
8
|
+
|
|
9
|
+
const mockedAxios = axios as jest.Mocked<typeof axios>
|
|
10
|
+
const mockedSpawn = spawn as jest.MockedFunction<typeof spawn>
|
|
11
|
+
|
|
12
|
+
describe('Worker', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// Clear all mocks before each test
|
|
15
|
+
jest.clearAllMocks()
|
|
16
|
+
|
|
17
|
+
// Reset Worker.endpoint
|
|
18
|
+
Worker.endpoint = ''
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('endpoint property', () => {
|
|
22
|
+
it('should have an empty endpoint by default', () => {
|
|
23
|
+
expect(Worker.endpoint).toBe('')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should allow setting the endpoint', () => {
|
|
27
|
+
Worker.endpoint = 'https://api.example.com/events'
|
|
28
|
+
expect(Worker.endpoint).toBe('https://api.example.com/events')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('pushEvent', () => {
|
|
33
|
+
it('should return false when endpoint is not set', () => {
|
|
34
|
+
const result = Worker.pushEvent('test-event')
|
|
35
|
+
expect(result).toBe(false)
|
|
36
|
+
expect(mockedAxios.patch).not.toHaveBeenCalled()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should send event when endpoint is set', () => {
|
|
40
|
+
Worker.endpoint = 'https://api.example.com/events'
|
|
41
|
+
|
|
42
|
+
mockedAxios.patch.mockResolvedValue({
|
|
43
|
+
statusText: 'OK',
|
|
44
|
+
data: { success: true },
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
Worker.pushEvent('test-event', { foo: 'bar' })
|
|
48
|
+
|
|
49
|
+
expect(mockedAxios.patch).toHaveBeenCalledWith(
|
|
50
|
+
'https://api.example.com/events',
|
|
51
|
+
expect.objectContaining({
|
|
52
|
+
event: 'test-event',
|
|
53
|
+
foo: 'bar',
|
|
54
|
+
})
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should include custom timestamp in payload', () => {
|
|
59
|
+
Worker.endpoint = 'https://api.example.com/events'
|
|
60
|
+
const customTs = 1234567890
|
|
61
|
+
|
|
62
|
+
mockedAxios.patch.mockResolvedValue({
|
|
63
|
+
statusText: 'OK',
|
|
64
|
+
data: { success: true },
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
Worker.pushEvent('test-event', {}, customTs)
|
|
68
|
+
|
|
69
|
+
expect(mockedAxios.patch).toHaveBeenCalledWith(
|
|
70
|
+
'https://api.example.com/events',
|
|
71
|
+
expect.objectContaining({
|
|
72
|
+
event: 'test-event',
|
|
73
|
+
ts: expect.any(Number),
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should include additional data in payload', () => {
|
|
79
|
+
Worker.endpoint = 'https://api.example.com/events'
|
|
80
|
+
|
|
81
|
+
mockedAxios.patch.mockResolvedValue({
|
|
82
|
+
statusText: 'OK',
|
|
83
|
+
data: { success: true },
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const additionalData = {
|
|
87
|
+
userId: '123',
|
|
88
|
+
action: 'upload',
|
|
89
|
+
metadata: { size: 1024 },
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Worker.pushEvent('custom-event', additionalData)
|
|
93
|
+
|
|
94
|
+
expect(mockedAxios.patch).toHaveBeenCalledWith(
|
|
95
|
+
'https://api.example.com/events',
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
event: 'custom-event',
|
|
98
|
+
userId: '123',
|
|
99
|
+
action: 'upload',
|
|
100
|
+
metadata: { size: 1024 },
|
|
101
|
+
})
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('pushEventAsync', () => {
|
|
107
|
+
it('should return false when endpoint is not set', async () => {
|
|
108
|
+
const result = await Worker.pushEventAsync('test-event')
|
|
109
|
+
expect(result).toBe(false)
|
|
110
|
+
expect(mockedAxios.patch).not.toHaveBeenCalled()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should send event and return true on success', async () => {
|
|
114
|
+
Worker.endpoint = 'https://api.example.com/events'
|
|
115
|
+
|
|
116
|
+
mockedAxios.patch.mockResolvedValue({
|
|
117
|
+
statusText: 'OK',
|
|
118
|
+
data: { success: true },
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const result = await Worker.pushEventAsync('test-event', {
|
|
122
|
+
foo: 'bar',
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect(result).toBe(true)
|
|
126
|
+
expect(mockedAxios.patch).toHaveBeenCalledWith(
|
|
127
|
+
'https://api.example.com/events',
|
|
128
|
+
expect.objectContaining({
|
|
129
|
+
event: 'test-event',
|
|
130
|
+
foo: 'bar',
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should return false on axios error', async () => {
|
|
136
|
+
Worker.endpoint = 'https://api.example.com/events'
|
|
137
|
+
|
|
138
|
+
mockedAxios.patch.mockRejectedValue(new Error('Network error'))
|
|
139
|
+
|
|
140
|
+
const result = await Worker.pushEventAsync('test-event')
|
|
141
|
+
|
|
142
|
+
expect(result).toBe(false)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should handle non-OK status response', async () => {
|
|
146
|
+
Worker.endpoint = 'https://api.example.com/events'
|
|
147
|
+
|
|
148
|
+
mockedAxios.patch.mockResolvedValue({
|
|
149
|
+
statusText: 'Bad Request',
|
|
150
|
+
data: { error: 'Invalid payload' },
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const result = await Worker.pushEventAsync('test-event')
|
|
154
|
+
|
|
155
|
+
expect(result).toBe(undefined)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should include custom timestamp', async () => {
|
|
159
|
+
Worker.endpoint = 'https://api.example.com/events'
|
|
160
|
+
const customTs = 9876543210
|
|
161
|
+
|
|
162
|
+
mockedAxios.patch.mockResolvedValue({
|
|
163
|
+
statusText: 'OK',
|
|
164
|
+
data: { success: true },
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
await Worker.pushEventAsync('test-event', {}, customTs)
|
|
168
|
+
|
|
169
|
+
expect(mockedAxios.patch).toHaveBeenCalledWith(
|
|
170
|
+
'https://api.example.com/events',
|
|
171
|
+
expect.objectContaining({
|
|
172
|
+
event: 'test-event',
|
|
173
|
+
ts: expect.any(Number),
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('execPromise', () => {
|
|
180
|
+
it('should execute command successfully', async () => {
|
|
181
|
+
const mockChild = {
|
|
182
|
+
stdout: {
|
|
183
|
+
on: jest.fn((event, callback) => {
|
|
184
|
+
if (event === 'data') {
|
|
185
|
+
// Simulate stdout data
|
|
186
|
+
setTimeout(
|
|
187
|
+
() => callback(Buffer.from('Command output')),
|
|
188
|
+
10
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
}),
|
|
192
|
+
},
|
|
193
|
+
stderr: {
|
|
194
|
+
on: jest.fn(),
|
|
195
|
+
},
|
|
196
|
+
on: jest.fn((event, callback) => {
|
|
197
|
+
if (event === 'close') {
|
|
198
|
+
// Simulate successful completion
|
|
199
|
+
setTimeout(() => callback(0), 20)
|
|
200
|
+
}
|
|
201
|
+
}),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
mockedSpawn.mockReturnValue(mockChild as any)
|
|
205
|
+
|
|
206
|
+
await expect(
|
|
207
|
+
Worker.execPromise('ls', ['-la'], '/tmp')
|
|
208
|
+
).resolves.toBeUndefined()
|
|
209
|
+
|
|
210
|
+
expect(mockedSpawn).toHaveBeenCalledWith('ls', ['-la'], {
|
|
211
|
+
cwd: '/tmp',
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should reject on command failure', async () => {
|
|
216
|
+
const mockChild = {
|
|
217
|
+
stdout: {
|
|
218
|
+
on: jest.fn(),
|
|
219
|
+
},
|
|
220
|
+
stderr: {
|
|
221
|
+
on: jest.fn((event, callback) => {
|
|
222
|
+
if (event === 'data') {
|
|
223
|
+
setTimeout(() => callback(Buffer.from('Error output')), 10)
|
|
224
|
+
}
|
|
225
|
+
}),
|
|
226
|
+
},
|
|
227
|
+
on: jest.fn((event, callback) => {
|
|
228
|
+
if (event === 'close') {
|
|
229
|
+
// Simulate failure with exit code 1
|
|
230
|
+
setTimeout(() => callback(1), 20)
|
|
231
|
+
}
|
|
232
|
+
}),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
mockedSpawn.mockReturnValue(mockChild as any)
|
|
236
|
+
|
|
237
|
+
await expect(
|
|
238
|
+
Worker.execPromise('invalid-command', [])
|
|
239
|
+
).rejects.toThrow('Process failed and returned code: 1')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('should use current working directory by default', async () => {
|
|
243
|
+
const mockChild = {
|
|
244
|
+
stdout: { on: jest.fn() },
|
|
245
|
+
stderr: { on: jest.fn() },
|
|
246
|
+
on: jest.fn((event, callback) => {
|
|
247
|
+
if (event === 'close') {
|
|
248
|
+
setTimeout(() => callback(0), 10)
|
|
249
|
+
}
|
|
250
|
+
}),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
mockedSpawn.mockReturnValue(mockChild as any)
|
|
254
|
+
|
|
255
|
+
await Worker.execPromise('echo', ['test'])
|
|
256
|
+
|
|
257
|
+
expect(mockedSpawn).toHaveBeenCalledWith('echo', ['test'], {
|
|
258
|
+
cwd: process.cwd(),
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should pass arguments to command', async () => {
|
|
263
|
+
const mockChild = {
|
|
264
|
+
stdout: { on: jest.fn() },
|
|
265
|
+
stderr: { on: jest.fn() },
|
|
266
|
+
on: jest.fn((event, callback) => {
|
|
267
|
+
if (event === 'close') {
|
|
268
|
+
setTimeout(() => callback(0), 10)
|
|
269
|
+
}
|
|
270
|
+
}),
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
mockedSpawn.mockReturnValue(mockChild as any)
|
|
274
|
+
|
|
275
|
+
const args = ['arg1', 'arg2', '--flag']
|
|
276
|
+
await Worker.execPromise('command', args, '/custom/path')
|
|
277
|
+
|
|
278
|
+
expect(mockedSpawn).toHaveBeenCalledWith('command', args, {
|
|
279
|
+
cwd: '/custom/path',
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
describe('logger', () => {
|
|
285
|
+
it('should have a logger instance', () => {
|
|
286
|
+
expect(Worker.logger).toBeDefined()
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
})
|
package/src/Worker.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Core } from '@quatrain/core'
|
|
2
|
+
import os from 'os'
|
|
3
|
+
import axios from 'axios'
|
|
4
|
+
import { spawn } from 'child_process'
|
|
5
|
+
import { HandlerParameters } from './types/HandlerParameters'
|
|
6
|
+
|
|
7
|
+
export class Worker extends Core {
|
|
8
|
+
static endpoint: string = ''
|
|
9
|
+
static readonly logger = this.addLogger('Worker')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute an external command in a promise
|
|
13
|
+
* @see https://stackoverflow.com/questions/46289682/how-to-wait-for-child-process-spawn-execution-with-async-await
|
|
14
|
+
* @see https://dzone.com/articles/understanding-execfile-spawn-exec-and-fork-in-node
|
|
15
|
+
* @param string command
|
|
16
|
+
* @param array args
|
|
17
|
+
* @return Promise
|
|
18
|
+
*/
|
|
19
|
+
static readonly execPromise = (
|
|
20
|
+
command: string,
|
|
21
|
+
args: any[] = [],
|
|
22
|
+
cwd = process.cwd()
|
|
23
|
+
): Promise<any> => {
|
|
24
|
+
try {
|
|
25
|
+
Worker.info(`Executing command ${command} in ${cwd} with arguments:`)
|
|
26
|
+
args.forEach((arg) => console.log(`\t${arg}`))
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const child = spawn(command, args, { cwd })
|
|
29
|
+
|
|
30
|
+
child.stdout.on('data', (data: Buffer) =>
|
|
31
|
+
Worker.debug(data.toString())
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
child.stderr.on('data', (data: Buffer) =>
|
|
35
|
+
Worker.debug(data.toString())
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
child.on('close', (code) => {
|
|
39
|
+
if (code !== 0) {
|
|
40
|
+
Worker.error(`Command execution failed with code: ${code}`)
|
|
41
|
+
reject(new Error(`Process failed and returned code: ${code}`))
|
|
42
|
+
} else {
|
|
43
|
+
Worker.info(`Command execution completed with code: ${code}`)
|
|
44
|
+
resolve(undefined)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
} catch (err) {
|
|
49
|
+
Worker.error((err as Error).message)
|
|
50
|
+
throw err
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Push an event to the backend endpoint, if available
|
|
56
|
+
* @param event string
|
|
57
|
+
* @param data
|
|
58
|
+
* @param ts timestamp
|
|
59
|
+
* @returns boolean
|
|
60
|
+
*/
|
|
61
|
+
static pushEvent(event: string, data = {}, ts = 0) {
|
|
62
|
+
if (!this.endpoint) {
|
|
63
|
+
Worker.warn(`Events endpoint is not set, can't send update!`)
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ts = ts === Date.now() ? Date.now() + 1 : Date.now()
|
|
68
|
+
const payload = {
|
|
69
|
+
event,
|
|
70
|
+
worker: `Container ${os.hostname}`,
|
|
71
|
+
os: `${os.type} ${os.release} (${os.platform} ${os.arch})`,
|
|
72
|
+
...data,
|
|
73
|
+
ts,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Worker.debug('event payload', payload)
|
|
77
|
+
|
|
78
|
+
axios
|
|
79
|
+
.patch(Worker.endpoint, payload)
|
|
80
|
+
.then((res) => {
|
|
81
|
+
Worker.info(`Event pushed to backend: ${res.statusText}`)
|
|
82
|
+
Worker.info(res.data)
|
|
83
|
+
return true
|
|
84
|
+
})
|
|
85
|
+
.catch((err) => {
|
|
86
|
+
Worker.error(`Failed to push event to backend: ${err}`)
|
|
87
|
+
return false
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Async Push an event to the backend endpoint, if available
|
|
93
|
+
* @param event string
|
|
94
|
+
* @param data
|
|
95
|
+
* @param ts timestamp
|
|
96
|
+
* @returns boolean
|
|
97
|
+
*/
|
|
98
|
+
static async pushEventAsync(event: string, data = {}, ts = 0) {
|
|
99
|
+
if (!this.endpoint) {
|
|
100
|
+
Worker.warn(`Events endpoint is not set, can't send update!`)
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
ts = ts === Date.now() ? Date.now() + 1 : Date.now()
|
|
106
|
+
const payload = {
|
|
107
|
+
event,
|
|
108
|
+
worker: `Container ${os.hostname}`,
|
|
109
|
+
os: `${os.type} ${os.release} (${os.platform} ${os.arch})`,
|
|
110
|
+
...data,
|
|
111
|
+
ts,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const res = await axios.patch(Worker.endpoint, payload)
|
|
115
|
+
|
|
116
|
+
if (res.statusText === 'OK') {
|
|
117
|
+
Worker.info(`Event pushed to backend: ${res.statusText}`)
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
Worker.error(`Failed to push event to backend: ${err}`)
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Global handling function to process received messages
|
|
128
|
+
* @param messageHandler function
|
|
129
|
+
* @param config object
|
|
130
|
+
*/
|
|
131
|
+
static readonly handler = async (
|
|
132
|
+
messageHandler: Function,
|
|
133
|
+
config: HandlerParameters
|
|
134
|
+
) => {
|
|
135
|
+
try {
|
|
136
|
+
switch (config.mode) {
|
|
137
|
+
case 'queue':
|
|
138
|
+
const { Queue } = require('@quatrain/queue')
|
|
139
|
+
Queue.addQueue(config.queueAdapter, 'default', true)
|
|
140
|
+
Queue.getQueue().listen(config.topic, messageHandler, {
|
|
141
|
+
concurrency: config.concurrency,
|
|
142
|
+
gpu: config.gpu,
|
|
143
|
+
})
|
|
144
|
+
Queue.info(
|
|
145
|
+
`Connected and listening to ${config.topic}, ready to receive messages.`
|
|
146
|
+
)
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
case 'cli':
|
|
150
|
+
case 'test':
|
|
151
|
+
Worker.warn(`Message received from CLI.`)
|
|
152
|
+
const json =
|
|
153
|
+
config.mode === 'test'
|
|
154
|
+
? require('../test.json')
|
|
155
|
+
: process.env.JSON
|
|
156
|
+
|
|
157
|
+
if (!json) {
|
|
158
|
+
throw new Error(`CLI call with missing environment variables`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await messageHandler(json)
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
default:
|
|
165
|
+
Worker.error(`Unknown mode option: '${config.mode}'`)
|
|
166
|
+
process.exit(1)
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
Worker.error((error as Error).message)
|
|
170
|
+
process.exit(1)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Worker } from './Worker'
|
|
2
|
+
import { FileSystem } from './FileSystem'
|
|
3
|
+
import { Helpers } from './Helpers'
|
|
4
|
+
|
|
5
|
+
import { HandlerParameters } from './types/HandlerParameters'
|
|
6
|
+
import { MessagehandlerParameters } from './types/MessagehandlerParameters'
|
|
7
|
+
import { ModeEnum } from './types/ModeEnum'
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
Worker,
|
|
11
|
+
FileSystem,
|
|
12
|
+
Helpers,
|
|
13
|
+
ModeEnum,
|
|
14
|
+
HandlerParameters,
|
|
15
|
+
MessagehandlerParameters,
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ModeEnum } from './ModeEnum'
|
|
2
|
+
import { AbstractQueueAdapter } from '@quatrain/queue'
|
|
3
|
+
|
|
4
|
+
export interface HandlerParameters {
|
|
5
|
+
mode: string | typeof ModeEnum
|
|
6
|
+
topic?: string
|
|
7
|
+
concurrency?: number
|
|
8
|
+
queueAdapter?: AbstractQueueAdapter
|
|
9
|
+
gpu?: boolean
|
|
10
|
+
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { ModeEnum } from './ModeEnum';
|
|
2
|
-
import { AbstractQueueAdapter } from '@quatrain/queue';
|
|
3
|
-
export interface HandlerParameters {
|
|
4
|
-
mode: string | typeof ModeEnum;
|
|
5
|
-
topic?: string;
|
|
6
|
-
concurrency?: number;
|
|
7
|
-
queueAdapter?: AbstractQueueAdapter;
|
|
8
|
-
gpu?: boolean;
|
|
9
|
-
}
|
package/lib/types/ModeTypes.d.ts
DELETED
package/lib/types/ModeTypes.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ModeEnum = void 0;
|
|
4
|
-
var ModeEnum;
|
|
5
|
-
(function (ModeEnum) {
|
|
6
|
-
ModeEnum["MODE_CLI"] = "cli";
|
|
7
|
-
ModeEnum["MODE_QUEUE"] = "queue";
|
|
8
|
-
ModeEnum["MODE_TEST"] = "test";
|
|
9
|
-
})(ModeEnum || (exports.ModeEnum = ModeEnum = {}));
|