ti2 1.0.95 → 1.0.96
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/controllers/README.md +70 -1
- package/controllers/__tests__/bookings-searchProducts.js +533 -0
- package/controllers/__tests__/bookings.js +28 -276
- package/controllers/app.js +11 -8
- package/controllers/bookings.js +188 -108
- package/package.json +1 -1
- package/test/utils.js +36 -35
- package/worker/__tests__/job-plugin.js +207 -0
- package/worker/index.js +193 -121
- package/worker/queue.js +1 -0
package/worker/index.js
CHANGED
|
@@ -2,137 +2,209 @@ const throng = require('throng');
|
|
|
2
2
|
require('util').inspect.defaultOptions.depth = null;
|
|
3
3
|
const R = require('ramda');
|
|
4
4
|
const { queue, saveResult } = require('./queue');
|
|
5
|
+
const bookingsControllerFactory = require('../controllers/bookings');
|
|
6
|
+
const mainAxios = require('axios'); // Generic axios instance for direct calls
|
|
5
7
|
|
|
6
8
|
const workers = process.env.WEB_CONCURRENCY || 2;
|
|
7
9
|
const maxJobsPerWorker = process.env.MAX_JOBS_PER_WORKER || 1;
|
|
8
10
|
|
|
9
|
-
const worker = ({ plugins: pluginsParam }) =>
|
|
10
|
-
|
|
11
|
+
const worker = ({ plugins: pluginsParam }) => { // pluginsParam are instantiated plugins
|
|
12
|
+
// Initialize bookingsCtrl once per worker instance using the passed instantiated plugins.
|
|
13
|
+
const bookingsCtrl = bookingsControllerFactory(pluginsParam);
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
console.log(`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
return (id, disconnect) => {
|
|
16
|
+
console.log(`Started worker ${id}`);
|
|
17
|
+
|
|
18
|
+
const bye = () => {
|
|
19
|
+
console.log(`Worker ${id} exiting`);
|
|
20
|
+
disconnect();
|
|
21
|
+
// Graceful shutdown of the queue connection
|
|
22
|
+
queue.close().then(() => process.exit(0)).catch(() => process.exit(1));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
process.on('SIGTERM', bye);
|
|
26
|
+
process.on('SIGUSR2', bye);
|
|
27
|
+
process.on('SIGINT', bye); // Good for local development
|
|
28
|
+
|
|
29
|
+
queue.process(maxJobsPerWorker, async job => {
|
|
30
|
+
const {
|
|
31
|
+
data, // Full job.data object
|
|
32
|
+
id: jobId,
|
|
33
|
+
} = job;
|
|
17
34
|
|
|
18
|
-
|
|
19
|
-
process.on('SIGUSR2', bye);
|
|
20
|
-
process.on('SIGINT', bye);
|
|
21
|
-
|
|
22
|
-
queue.process(maxJobsPerWorker, async job => {
|
|
23
|
-
const {
|
|
24
|
-
data: {
|
|
25
|
-
type = 'plugin',
|
|
26
|
-
pluginName,
|
|
27
|
-
inTesting,
|
|
28
|
-
payload = {}
|
|
29
|
-
},
|
|
30
|
-
id: jobId,
|
|
31
|
-
data: params,
|
|
32
|
-
} = job;
|
|
33
|
-
let resultValue;
|
|
34
|
-
const jobLog = msg => console.log(`job ${jobId} > `, msg)
|
|
35
|
-
const jobErr = msg => console.error(`job ${jobId} > `, msg)
|
|
36
|
-
jobLog(`of type ${type} started`);
|
|
37
|
-
if (type === 'api') {
|
|
35
|
+
// Destructure from data for clarity and to match existing patterns
|
|
38
36
|
const {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
37
|
+
type = 'plugin', // Default job type if not specified
|
|
38
|
+
pluginName, // Name of the plugin context for the job
|
|
39
|
+
inTesting, // Flag if running in a testing environment
|
|
40
|
+
payload: jobDataPayload, // This is job.data.payload (e.g., { methodName: '...', args: {...} } or other plugin args)
|
|
41
|
+
method: jobDataMethod, // This is job.data.method (e.g., 'searchProducts' for generic plugin calls)
|
|
42
|
+
// Note: 'token' can also be a top-level property in data (data.token)
|
|
43
|
+
} = data;
|
|
44
|
+
|
|
45
|
+
let resultValue; // This will be set by all job type handlers
|
|
46
|
+
const jobLog = msg => console.log(`job ${jobId} (type: ${type}, plugin: ${pluginName || 'N/A'}) > `, msg);
|
|
47
|
+
const jobErr = msg => console.error(`job ${jobId} (type: ${type}, plugin: ${pluginName || 'N/A'}) > `, msg);
|
|
48
|
+
jobLog(`started`);
|
|
49
|
+
|
|
49
50
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
51
|
+
if (type === 'api') {
|
|
52
|
+
// params is job.data, so data.method, data.url etc. are correct here
|
|
53
|
+
jobLog(`running method ${data.method} ${data.url}`);
|
|
54
|
+
let code, result, server;
|
|
55
|
+
const http = require('http');
|
|
56
|
+
const axios = require('axios'); // Local axios for API call type
|
|
57
|
+
try {
|
|
58
|
+
const app = await require('../index')({
|
|
59
|
+
pluginsInstantiated: pluginsParam, // Pass the already instantiated plugins
|
|
60
|
+
startServer: false,
|
|
61
|
+
worker: false,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
server = http.createServer(app);
|
|
65
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
66
|
+
const { address, port } = server.address();
|
|
67
|
+
const baseURL = `http://${address}:${port}`;
|
|
68
|
+
jobLog(`Worker internal server for job ${jobId} listening on: ${baseURL}`);
|
|
69
|
+
|
|
70
|
+
// data.payload is the payload for the API call job type
|
|
71
|
+
const { token: jwtTokenForHeader, ...requestBodyForWorker } = data.payload || {};
|
|
72
|
+
|
|
73
|
+
const requestHeaders = {
|
|
74
|
+
...R.omit(['content-length', 'host', 'connection', 'accept-encoding'], data.headers),
|
|
75
|
+
...(jwtTokenForHeader ? { Authorization: `Bearer ${jwtTokenForHeader}` } : {})
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const response = await axios({
|
|
79
|
+
method: data.method.toLowerCase(),
|
|
80
|
+
url: `${baseURL}${data.url}`,
|
|
81
|
+
data: R.omit(['backgroundJob'], requestBodyForWorker),
|
|
82
|
+
headers: requestHeaders,
|
|
83
|
+
validateStatus: () => true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
code = response.status;
|
|
87
|
+
result = response.data;
|
|
88
|
+
jobLog(`Worker internal request for job ${jobId} completed with code ${code}`);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
jobErr(`Worker internal request error for job ${jobId}: ${error.message}`);
|
|
91
|
+
code = R.pathOr(500, ['response', 'status'], error);
|
|
92
|
+
result = R.pathOr({ message: error.message }, ['response', 'data'], error);
|
|
93
|
+
} finally {
|
|
94
|
+
if (server) {
|
|
95
|
+
await new Promise(resolve => server.close(resolve));
|
|
96
|
+
jobLog(`Worker internal server for job ${jobId} closed.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
resultValue = { code, result, success: code >= 200 && code < 300 };
|
|
100
|
+
|
|
101
|
+
} else if (type === 'callback') {
|
|
102
|
+
const { callbackUrl, request, result } = jobDataPayload; // jobDataPayload is job.data.payload
|
|
103
|
+
jobLog(`sending callback to ${callbackUrl}`);
|
|
104
|
+
await require('../lib/callback').sendCallback({
|
|
105
|
+
callbackUrl,
|
|
106
|
+
request,
|
|
107
|
+
result,
|
|
108
|
+
});
|
|
109
|
+
resultValue = { success: true };
|
|
110
|
+
|
|
111
|
+
} else if (type === 'plugin') {
|
|
112
|
+
// jobDataPayload is job.data.payload
|
|
113
|
+
// jobDataMethod is job.data.method (for generic plugin calls)
|
|
114
|
+
|
|
115
|
+
// Check for our specific controller-level background task
|
|
116
|
+
if (jobDataPayload && jobDataPayload.methodName === '$bookingsProductSearchInternal') {
|
|
117
|
+
jobLog(`Processing via bookingsCtrl.$bookingsProductSearchInternal`);
|
|
118
|
+
|
|
119
|
+
const controllerArgs = jobDataPayload.args;
|
|
120
|
+
if (!controllerArgs) {
|
|
121
|
+
throw new Error('$bookingsProductSearchInternal job is missing "args" in its payload');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Ensure appKey from args matches pluginName from job data if pluginName is used for context
|
|
125
|
+
if (pluginName && controllerArgs.appKey !== pluginName) {
|
|
126
|
+
jobErr(`Job data pluginName (${pluginName}) mismatch with args.appKey (${controllerArgs.appKey}). Using args.appKey for plugin context.`);
|
|
127
|
+
}
|
|
128
|
+
// If this job type implies a forced refresh, `forceRefresh: true` should be part of `controllerArgs`.
|
|
129
|
+
// The `backgroundJob` flag is removed from $bookingsProductSearch.
|
|
130
|
+
const executionPayload = {
|
|
131
|
+
...controllerArgs, // Contains appKey, userId, hint, payload (for $bps), headers
|
|
132
|
+
// Potentially `forceRefresh: true` if needed for this job type.
|
|
133
|
+
axios: mainAxios, // Provide the worker's generic axios instance
|
|
134
|
+
requestId: jobId, // Use job ID for tracing
|
|
135
|
+
// plugins are already available to bookingsCtrl via its factory initialization
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
resultValue = await bookingsCtrl.$bookingsProductSearch(executionPayload);
|
|
139
|
+
jobLog(`bookingsCtrl.$bookingsProductSearchInternal completed.`);
|
|
140
|
+
|
|
141
|
+
} else {
|
|
142
|
+
// Handle generic plugin method call
|
|
143
|
+
jobLog(`Processing plugin method: ${jobDataMethod} for plugin ${pluginName}`);
|
|
144
|
+
|
|
145
|
+
const currentJobPlugins = await (async () => {
|
|
146
|
+
if (inTesting) {
|
|
147
|
+
const fakePlugin = require('../test/plugin');
|
|
148
|
+
const FakePluginClass = fakePlugin.default || fakePlugin;
|
|
149
|
+
return [...pluginsParam, new FakePluginClass({ name: pluginName })];
|
|
150
|
+
}
|
|
151
|
+
return pluginsParam;
|
|
152
|
+
})();
|
|
153
|
+
|
|
154
|
+
const thePlugin = currentJobPlugins.find(({ name }) => name === pluginName);
|
|
155
|
+
|
|
156
|
+
if (!thePlugin) {
|
|
157
|
+
throw new Error(`Plugin ${pluginName} not found for job ${jobId}.`);
|
|
158
|
+
}
|
|
159
|
+
if (!jobDataMethod || typeof thePlugin[jobDataMethod] !== 'function') {
|
|
160
|
+
throw new Error(`Method ${jobDataMethod} not found or not a function on plugin ${pluginName} for job ${jobId}.`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Execute the plugin method
|
|
164
|
+
const pluginMethodResult = await thePlugin[jobDataMethod]({
|
|
165
|
+
...(jobDataPayload || {}), // Spread job.data.payload (e.g., contains inner 'payload' and 'userId')
|
|
166
|
+
token: data.token, // Pass job.data.token
|
|
167
|
+
plugins: currentJobPlugins,
|
|
168
|
+
typeDefsAndQueries: bookingsControllerFactory.typeDefsAndQueries, // As per existing pattern
|
|
169
|
+
axios: mainAxios, // Inject worker's axios instance
|
|
170
|
+
requestId: jobId, // Inject job ID as requestId
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
jobLog(`Plugin method ${jobDataMethod} for ${pluginName} completed.`);
|
|
174
|
+
resultValue = pluginMethodResult; // The primary result of the job is the plugin's output
|
|
175
|
+
|
|
176
|
+
// Post-processing step
|
|
177
|
+
if (data.postProcess) {
|
|
178
|
+
jobLog(`Initiating post-processing step: ${data.postProcess.controller}.${data.postProcess.action}`);
|
|
179
|
+
if (data.postProcess.controller === 'bookings' && bookingsCtrl[data.postProcess.action]) {
|
|
180
|
+
await bookingsCtrl[data.postProcess.action]({
|
|
181
|
+
...(data.postProcess.args || {}), // Spread static args from job data
|
|
182
|
+
pluginResult: pluginMethodResult, // Pass the dynamic result from the plugin
|
|
183
|
+
requestId: jobId, // Pass job ID for tracing in post-process
|
|
184
|
+
// 'plugins' are already part of bookingsCtrl via its factory
|
|
185
|
+
});
|
|
186
|
+
jobLog(`Post-processing step ${data.postProcess.action} completed for job ${jobId}.`);
|
|
187
|
+
} else {
|
|
188
|
+
jobErr(`Post-processing action ${data.postProcess.action} on controller ${data.postProcess.controller} not found or controller not supported for job ${jobId}.`);
|
|
189
|
+
// Depending on requirements, this could throw an error to fail the job
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
jobErr(`Unknown job type: ${type} for job ${jobId}`);
|
|
195
|
+
throw new Error(`Unknown job type: ${type} for job ${jobId}`);
|
|
93
196
|
}
|
|
197
|
+
|
|
198
|
+
jobLog(`completed`);
|
|
199
|
+
await saveResult({ id: jobId, resultValue });
|
|
200
|
+
|
|
201
|
+
} catch (error) {
|
|
202
|
+
jobErr(`Processing failed: ${error.message}`);
|
|
203
|
+
if (error.stack) jobErr(error.stack); // Log full stack for debugging
|
|
204
|
+
throw new Error(error.message) // Re-throw to mark job as failed in Bull (use a summarized mesage)
|
|
94
205
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
result,
|
|
98
|
-
success: code >= 200 && code < 300,
|
|
99
|
-
};
|
|
100
|
-
} else if (type === 'callback') {
|
|
101
|
-
const {
|
|
102
|
-
callbackUrl,
|
|
103
|
-
request,
|
|
104
|
-
result,
|
|
105
|
-
} = payload;
|
|
106
|
-
jobLog(`job ${jobId} > sending callback to ${callbackUrl}`);
|
|
107
|
-
await require('../lib/callback').sendCallback({
|
|
108
|
-
callbackUrl,
|
|
109
|
-
request,
|
|
110
|
-
result,
|
|
111
|
-
});
|
|
112
|
-
resultValue = { success: true };
|
|
113
|
-
} else if (type === 'plugin') {
|
|
114
|
-
const {
|
|
115
|
-
method,
|
|
116
|
-
payload,
|
|
117
|
-
} = params;
|
|
118
|
-
const plugins = await (async () => {
|
|
119
|
-
if (inTesting) {
|
|
120
|
-
const fakePlugin = require('../test/plugin');
|
|
121
|
-
return [...pluginsParam, await new fakePlugin({ name: pluginName })];
|
|
122
|
-
}
|
|
123
|
-
return pluginsParam;
|
|
124
|
-
})();
|
|
125
|
-
const thePlugin = plugins.find(({ name }) => name === pluginName);
|
|
126
|
-
resultValue = await thePlugin[method]({
|
|
127
|
-
...payload,
|
|
128
|
-
token: params.token,
|
|
129
|
-
plugins,
|
|
130
|
-
typeDefsAndQueries: require('../controllers/bookings').typeDefsAndQueries,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
jobLog(`job ${jobId} > completed`);
|
|
134
|
-
await saveResult({ id: jobId, resultValue });
|
|
135
|
-
});
|
|
206
|
+
});
|
|
207
|
+
};
|
|
136
208
|
};
|
|
137
209
|
|
|
138
210
|
module.exports = async args => throng({
|