webspresso 0.0.89 → 0.0.91
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/README.md +450 -53
- package/bin/commands/service.js +193 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/manager.js +36 -26
- package/core/auth/middleware.js +21 -2
- package/core/compression/compressible.js +51 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +301 -0
- package/core/compression/negotiate.js +153 -0
- package/core/errors/base.js +31 -0
- package/core/errors/domain.js +193 -0
- package/core/errors/http.js +133 -0
- package/core/errors/index.js +77 -0
- package/core/errors/middleware.js +162 -0
- package/core/errors/normalize.js +171 -0
- package/core/orm/complexity.js +120 -0
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +45 -10
- package/core/orm/model.js +10 -2
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/scopes.js +2 -1
- package/core/orm/transaction.js +39 -5
- package/core/orm/utils.js +6 -3
- package/core/queue/adapters/database.js +284 -0
- package/core/queue/adapters/memory.js +155 -0
- package/core/queue/adapters/redis.js +336 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +387 -0
- package/core/realtime/adapters/redis.js +431 -0
- package/core/realtime/adapters/socket-io.js +203 -0
- package/core/realtime/adapters/sse.js +210 -0
- package/core/realtime/adapters/websocket.js +337 -0
- package/core/realtime/auth-manager.js +140 -0
- package/core/realtime/core.js +359 -0
- package/core/realtime/emitter.js +98 -0
- package/core/realtime/errors.js +71 -0
- package/core/realtime/index.js +79 -0
- package/core/realtime/reconnect-manager.js +118 -0
- package/core/realtime/registry.js +243 -0
- package/core/shutdown/http-adapter.js +134 -0
- package/core/shutdown/index.js +12 -0
- package/core/shutdown/shutdown-manager.js +312 -0
- package/core/ssr/index.js +13 -0
- package/core/ssr/stream.js +174 -0
- package/index.d.ts +829 -52
- package/index.js +103 -0
- package/package.json +22 -2
- package/plugins/admin-panel/api.js +37 -4
- package/plugins/admin-panel/app.js +67 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
- package/plugins/admin-panel/client/parts/03-pagination-intro.js +25 -18
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
- package/plugins/admin-panel/client/parts/08-record-list.js +27 -34
- package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
- package/plugins/admin-panel/core/admin-module.js +2 -2
- package/plugins/admin-panel/core/api-extensions.js +149 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
- package/plugins/admin-panel/index.js +131 -18
- package/plugins/admin-panel/modules/admin-users.js +770 -0
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- package/plugins/admin-panel/modules/menu.js +85 -0
- package/plugins/admin-panel/modules/user-management.js +5 -2
- package/plugins/admin-panel/vendor/mithril.min.js +1 -0
- package/plugins/admin-panel/vendor/quill.min.js +7 -0
- package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
- package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
- package/plugins/basic-auth/index.js +293 -0
- package/plugins/content/client/inline-edit.js +41 -17
- package/plugins/cors.js +12 -8
- package/plugins/csrf/index.js +8 -8
- package/plugins/data-exchange/export-xlsx.js +5 -1
- package/plugins/data-exchange/import.js +16 -2
- package/plugins/data-exchange/index.js +14 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +29 -0
- package/plugins/queue/index.js +108 -0
- package/plugins/realtime/index.js +150 -0
- package/plugins/recaptcha/index.js +11 -0
- package/plugins/redirect/index.js +9 -1
- package/plugins/rest-resources/index.js +280 -11
- package/plugins/upload/index.js +16 -0
- package/src/app-context.js +46 -6
- package/src/file-router.js +220 -28
- package/src/helpers.js +24 -5
- package/src/plugin-manager.js +134 -7
- package/src/server.js +470 -44
- package/src/services/builtins/auth.js +273 -0
- package/src/services/builtins/exchange.js +229 -0
- package/src/services/builtins/mail.js +134 -0
- package/src/services/builtins/media.js +196 -0
- package/src/services/builtins/system.js +221 -0
- package/src/services/discovery.js +140 -0
- package/src/services/executor.js +292 -0
- package/src/services/index.js +70 -0
- package/src/services/memoize.js +256 -0
- package/src/services/registry.js +288 -0
- package/src/services/validator.js +281 -0
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +2 -2
package/core/orm/utils.js
CHANGED
|
@@ -107,11 +107,14 @@ function deepClone(obj) {
|
|
|
107
107
|
if (Array.isArray(obj)) {
|
|
108
108
|
return obj.map(deepClone);
|
|
109
109
|
}
|
|
110
|
-
const cloned =
|
|
111
|
-
for (const key
|
|
110
|
+
const cloned = Object.create(null);
|
|
111
|
+
for (const key of Object.keys(obj)) {
|
|
112
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
112
115
|
cloned[key] = deepClone(obj[key]);
|
|
113
116
|
}
|
|
114
|
-
return cloned;
|
|
117
|
+
return { ...cloned };
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/**
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knex Database-Backed Queue Adapter
|
|
3
|
+
* @module core/queue/adapters/database
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const { Job } = require('../job');
|
|
9
|
+
|
|
10
|
+
class DatabaseQueueAdapter {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {import('knex').Knex} options.knex - Knex instance
|
|
14
|
+
* @param {string} [options.tableName='webspresso_jobs'] - Queue table name
|
|
15
|
+
*/
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
if (!options.knex) {
|
|
18
|
+
throw new Error('DatabaseQueueAdapter requires a Knex instance in options.knex');
|
|
19
|
+
}
|
|
20
|
+
this.knex = options.knex;
|
|
21
|
+
this.tableName = options.tableName || 'webspresso_jobs';
|
|
22
|
+
this._tableChecked = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ensure the jobs table exists in the database
|
|
27
|
+
*/
|
|
28
|
+
async ensureTable() {
|
|
29
|
+
if (this._tableChecked) return;
|
|
30
|
+
|
|
31
|
+
const exists = await this.knex.schema.hasTable(this.tableName);
|
|
32
|
+
if (!exists) {
|
|
33
|
+
await this.knex.schema.createTable(this.tableName, (table) => {
|
|
34
|
+
table.string('id').primary();
|
|
35
|
+
table.string('name').notNullable().index();
|
|
36
|
+
table.text('data').nullable();
|
|
37
|
+
table.string('status').notNullable().defaultTo('pending').index();
|
|
38
|
+
table.integer('attempts').notNullable().defaultTo(0);
|
|
39
|
+
table.integer('max_attempts').notNullable().defaultTo(3);
|
|
40
|
+
table.text('backoff').nullable();
|
|
41
|
+
table.integer('priority').notNullable().defaultTo(10);
|
|
42
|
+
table.integer('timeout').notNullable().defaultTo(30000);
|
|
43
|
+
table.integer('progress').notNullable().defaultTo(0);
|
|
44
|
+
table.text('result').nullable();
|
|
45
|
+
table.text('error').nullable();
|
|
46
|
+
table.dateTime('run_at').notNullable().index();
|
|
47
|
+
table.dateTime('created_at').notNullable();
|
|
48
|
+
table.dateTime('updated_at').notNullable();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this._tableChecked = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Enqueue a job into the database
|
|
57
|
+
* @param {Job} job
|
|
58
|
+
* @returns {Promise<Job>}
|
|
59
|
+
*/
|
|
60
|
+
async enqueue(job) {
|
|
61
|
+
await this.ensureTable();
|
|
62
|
+
const jobInstance = job instanceof Job ? job : new Job(job);
|
|
63
|
+
|
|
64
|
+
await this.knex(this.tableName).insert({
|
|
65
|
+
id: String(jobInstance.id),
|
|
66
|
+
name: jobInstance.name,
|
|
67
|
+
data: JSON.stringify(jobInstance.data),
|
|
68
|
+
status: jobInstance.status,
|
|
69
|
+
attempts: jobInstance.attempts,
|
|
70
|
+
max_attempts: jobInstance.maxAttempts,
|
|
71
|
+
backoff: JSON.stringify(jobInstance.backoff),
|
|
72
|
+
priority: jobInstance.priority,
|
|
73
|
+
timeout: jobInstance.timeout,
|
|
74
|
+
progress: jobInstance.progressValue,
|
|
75
|
+
result: jobInstance.result ? JSON.stringify(jobInstance.result) : null,
|
|
76
|
+
error: jobInstance.error ? (jobInstance.error.message || String(jobInstance.error)) : null,
|
|
77
|
+
run_at: jobInstance.runAt,
|
|
78
|
+
created_at: jobInstance.createdAt,
|
|
79
|
+
updated_at: jobInstance.updatedAt,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return jobInstance;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Dequeue next pending job
|
|
87
|
+
* @param {string[]} [jobNames]
|
|
88
|
+
* @returns {Promise<Job|null>}
|
|
89
|
+
*/
|
|
90
|
+
async dequeue(jobNames = []) {
|
|
91
|
+
await this.ensureTable();
|
|
92
|
+
const now = new Date();
|
|
93
|
+
|
|
94
|
+
return this.knex.transaction(async (trx) => {
|
|
95
|
+
let query = trx(this.tableName)
|
|
96
|
+
.where('status', 'pending')
|
|
97
|
+
.where('run_at', '<=', now)
|
|
98
|
+
.orderBy('priority', 'desc')
|
|
99
|
+
.orderBy('run_at', 'asc')
|
|
100
|
+
.limit(1);
|
|
101
|
+
|
|
102
|
+
if (jobNames && jobNames.length > 0) {
|
|
103
|
+
query = query.whereIn('name', jobNames);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If Postgres or MySQL support forUpdate / skipLocked
|
|
107
|
+
const client = this.knex.client.config.client;
|
|
108
|
+
if (client === 'pg' || client === 'postgres' || client === 'mysql' || client === 'mysql2') {
|
|
109
|
+
try {
|
|
110
|
+
query = query.forUpdate().skipLocked();
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const rows = await query;
|
|
115
|
+
if (!rows || rows.length === 0) return null;
|
|
116
|
+
|
|
117
|
+
const row = rows[0];
|
|
118
|
+
const nextAttempts = Number(row.attempts) + 1;
|
|
119
|
+
const updatedAt = new Date();
|
|
120
|
+
|
|
121
|
+
await trx(this.tableName)
|
|
122
|
+
.where('id', row.id)
|
|
123
|
+
.update({
|
|
124
|
+
status: 'running',
|
|
125
|
+
attempts: nextAttempts,
|
|
126
|
+
updated_at: updatedAt,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return this._rowToJob({
|
|
130
|
+
...row,
|
|
131
|
+
status: 'running',
|
|
132
|
+
attempts: nextAttempts,
|
|
133
|
+
updated_at: updatedAt,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Mark job as completed
|
|
140
|
+
* @param {string|number} jobId
|
|
141
|
+
* @param {any} [result]
|
|
142
|
+
* @returns {Promise<Job|null>}
|
|
143
|
+
*/
|
|
144
|
+
async complete(jobId, result = null) {
|
|
145
|
+
await this.ensureTable();
|
|
146
|
+
const updatedAt = new Date();
|
|
147
|
+
await this.knex(this.tableName)
|
|
148
|
+
.where('id', String(jobId))
|
|
149
|
+
.update({
|
|
150
|
+
status: 'completed',
|
|
151
|
+
result: result !== undefined ? JSON.stringify(result) : null,
|
|
152
|
+
progress: 100,
|
|
153
|
+
updated_at: updatedAt,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return this.getJob(jobId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Mark job as failed
|
|
161
|
+
* @param {string|number} jobId
|
|
162
|
+
* @param {any} [error]
|
|
163
|
+
* @returns {Promise<Job|null>}
|
|
164
|
+
*/
|
|
165
|
+
async fail(jobId, error = null) {
|
|
166
|
+
await this.ensureTable();
|
|
167
|
+
const updatedAt = new Date();
|
|
168
|
+
const errorStr = error ? (error.message || String(error)) : null;
|
|
169
|
+
|
|
170
|
+
await this.knex(this.tableName)
|
|
171
|
+
.where('id', String(jobId))
|
|
172
|
+
.update({
|
|
173
|
+
status: 'failed',
|
|
174
|
+
error: errorStr,
|
|
175
|
+
updated_at: updatedAt,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return this.getJob(jobId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Reschedule job for retry
|
|
183
|
+
* @param {string|number} jobId
|
|
184
|
+
* @param {number} delayMs
|
|
185
|
+
* @param {any} [error]
|
|
186
|
+
* @returns {Promise<Job|null>}
|
|
187
|
+
*/
|
|
188
|
+
async retry(jobId, delayMs, error = null) {
|
|
189
|
+
await this.ensureTable();
|
|
190
|
+
const updatedAt = new Date();
|
|
191
|
+
const runAt = new Date(Date.now() + Math.max(0, delayMs));
|
|
192
|
+
const errorStr = error ? (error.message || String(error)) : null;
|
|
193
|
+
|
|
194
|
+
await this.knex(this.tableName)
|
|
195
|
+
.where('id', String(jobId))
|
|
196
|
+
.update({
|
|
197
|
+
status: 'pending',
|
|
198
|
+
run_at: runAt,
|
|
199
|
+
error: errorStr,
|
|
200
|
+
updated_at: updatedAt,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return this.getJob(jobId);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get job by ID
|
|
208
|
+
* @param {string|number} jobId
|
|
209
|
+
* @returns {Promise<Job|null>}
|
|
210
|
+
*/
|
|
211
|
+
async getJob(jobId) {
|
|
212
|
+
await this.ensureTable();
|
|
213
|
+
const row = await this.knex(this.tableName).where('id', String(jobId)).first();
|
|
214
|
+
if (!row) return null;
|
|
215
|
+
return this._rowToJob(row);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get stats summary
|
|
220
|
+
* @returns {Promise<{ pending: number, running: number, completed: number, failed: number, total: number }>}
|
|
221
|
+
*/
|
|
222
|
+
async getStats() {
|
|
223
|
+
await this.ensureTable();
|
|
224
|
+
const rows = await this.knex(this.tableName)
|
|
225
|
+
.select('status')
|
|
226
|
+
.count('* as count')
|
|
227
|
+
.groupBy('status');
|
|
228
|
+
|
|
229
|
+
const stats = { pending: 0, running: 0, completed: 0, failed: 0, total: 0 };
|
|
230
|
+
for (const r of rows) {
|
|
231
|
+
const count = Number(r.count || r.COUNT || 0);
|
|
232
|
+
if (r.status in stats) {
|
|
233
|
+
stats[r.status] = count;
|
|
234
|
+
}
|
|
235
|
+
stats.total += count;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return stats;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Clear all jobs from table
|
|
243
|
+
* @returns {Promise<void>}
|
|
244
|
+
*/
|
|
245
|
+
async clear() {
|
|
246
|
+
await this.ensureTable();
|
|
247
|
+
await this.knex(this.tableName).del();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Convert DB row to Job instance
|
|
252
|
+
* @private
|
|
253
|
+
*/
|
|
254
|
+
_rowToJob(row) {
|
|
255
|
+
let data = {};
|
|
256
|
+
try { data = JSON.parse(row.data); } catch {}
|
|
257
|
+
|
|
258
|
+
let backoff = { type: 'exponential', delay: 1000 };
|
|
259
|
+
try { if (row.backoff) backoff = JSON.parse(row.backoff); } catch {}
|
|
260
|
+
|
|
261
|
+
let result = null;
|
|
262
|
+
try { if (row.result) result = JSON.parse(row.result); } catch {}
|
|
263
|
+
|
|
264
|
+
return new Job({
|
|
265
|
+
id: row.id,
|
|
266
|
+
name: row.name,
|
|
267
|
+
data,
|
|
268
|
+
status: row.status,
|
|
269
|
+
attempts: Number(row.attempts),
|
|
270
|
+
maxAttempts: Number(row.max_attempts),
|
|
271
|
+
backoff,
|
|
272
|
+
priority: Number(row.priority),
|
|
273
|
+
timeout: Number(row.timeout),
|
|
274
|
+
progress: Number(row.progress),
|
|
275
|
+
result,
|
|
276
|
+
error: row.error,
|
|
277
|
+
runAt: row.run_at,
|
|
278
|
+
createdAt: row.created_at,
|
|
279
|
+
updatedAt: row.updated_at,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = { DatabaseQueueAdapter };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-Memory Queue Adapter
|
|
3
|
+
* @module core/queue/adapters/memory
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const { Job } = require('../job');
|
|
9
|
+
|
|
10
|
+
class MemoryQueueAdapter {
|
|
11
|
+
constructor() {
|
|
12
|
+
/** @type {Map<string|number, Job>} */
|
|
13
|
+
this.jobs = new Map();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Enqueue a job into memory
|
|
18
|
+
* @param {Job} job
|
|
19
|
+
* @returns {Promise<Job>}
|
|
20
|
+
*/
|
|
21
|
+
async enqueue(job) {
|
|
22
|
+
const jobInstance = job instanceof Job ? job : new Job(job);
|
|
23
|
+
this.jobs.set(jobInstance.id, jobInstance);
|
|
24
|
+
return jobInstance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Dequeue the highest priority pending job matching registered job names
|
|
29
|
+
* @param {string[]} [jobNames]
|
|
30
|
+
* @returns {Promise<Job|null>}
|
|
31
|
+
*/
|
|
32
|
+
async dequeue(jobNames = []) {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
let candidate = null;
|
|
35
|
+
|
|
36
|
+
for (const job of this.jobs.values()) {
|
|
37
|
+
if (job.status !== 'pending') continue;
|
|
38
|
+
if (job.runAt && job.runAt.getTime() > now) continue;
|
|
39
|
+
if (jobNames.length > 0 && !jobNames.includes(job.name)) continue;
|
|
40
|
+
|
|
41
|
+
if (!candidate) {
|
|
42
|
+
candidate = job;
|
|
43
|
+
} else {
|
|
44
|
+
// Priority (higher first) then runAt (earlier first)
|
|
45
|
+
if (job.priority > candidate.priority) {
|
|
46
|
+
candidate = job;
|
|
47
|
+
} else if (job.priority === candidate.priority && job.runAt.getTime() < candidate.runAt.getTime()) {
|
|
48
|
+
candidate = job;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (candidate) {
|
|
54
|
+
candidate.status = 'running';
|
|
55
|
+
candidate.attempts += 1;
|
|
56
|
+
candidate.updatedAt = new Date();
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Mark job as completed
|
|
65
|
+
* @param {string|number} jobId
|
|
66
|
+
* @param {any} [result]
|
|
67
|
+
* @returns {Promise<Job|null>}
|
|
68
|
+
*/
|
|
69
|
+
async complete(jobId, result = null) {
|
|
70
|
+
const job = this.jobs.get(jobId);
|
|
71
|
+
if (!job) return null;
|
|
72
|
+
job.status = 'completed';
|
|
73
|
+
job.result = result;
|
|
74
|
+
job.progressValue = 100;
|
|
75
|
+
job.updatedAt = new Date();
|
|
76
|
+
return job;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Mark job as permanently failed
|
|
81
|
+
* @param {string|number} jobId
|
|
82
|
+
* @param {any} [error]
|
|
83
|
+
* @returns {Promise<Job|null>}
|
|
84
|
+
*/
|
|
85
|
+
async fail(jobId, error = null) {
|
|
86
|
+
const job = this.jobs.get(jobId);
|
|
87
|
+
if (!job) return null;
|
|
88
|
+
job.status = 'failed';
|
|
89
|
+
job.error = error;
|
|
90
|
+
job.updatedAt = new Date();
|
|
91
|
+
return job;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Re-schedule job for retry
|
|
96
|
+
* @param {string|number} jobId
|
|
97
|
+
* @param {number} delayMs
|
|
98
|
+
* @param {any} [error]
|
|
99
|
+
* @returns {Promise<Job|null>}
|
|
100
|
+
*/
|
|
101
|
+
async retry(jobId, delayMs, error = null) {
|
|
102
|
+
const job = this.jobs.get(jobId);
|
|
103
|
+
if (!job) return null;
|
|
104
|
+
job.status = 'pending';
|
|
105
|
+
job.runAt = new Date(Date.now() + Math.max(0, delayMs));
|
|
106
|
+
job.error = error;
|
|
107
|
+
job.updatedAt = new Date();
|
|
108
|
+
return job;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get job by ID
|
|
113
|
+
* @param {string|number} jobId
|
|
114
|
+
* @returns {Promise<Job|null>}
|
|
115
|
+
*/
|
|
116
|
+
async getJob(jobId) {
|
|
117
|
+
return this.jobs.get(jobId) || null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get queue statistics
|
|
122
|
+
* @returns {Promise<{ pending: number, running: number, completed: number, failed: number, total: number }>}
|
|
123
|
+
*/
|
|
124
|
+
async getStats() {
|
|
125
|
+
let pending = 0;
|
|
126
|
+
let running = 0;
|
|
127
|
+
let completed = 0;
|
|
128
|
+
let failed = 0;
|
|
129
|
+
|
|
130
|
+
for (const job of this.jobs.values()) {
|
|
131
|
+
if (job.status === 'pending') pending++;
|
|
132
|
+
else if (job.status === 'running') running++;
|
|
133
|
+
else if (job.status === 'completed') completed++;
|
|
134
|
+
else if (job.status === 'failed') failed++;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
pending,
|
|
139
|
+
running,
|
|
140
|
+
completed,
|
|
141
|
+
failed,
|
|
142
|
+
total: this.jobs.size,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clear all jobs
|
|
148
|
+
* @returns {Promise<void>}
|
|
149
|
+
*/
|
|
150
|
+
async clear() {
|
|
151
|
+
this.jobs.clear();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { MemoryQueueAdapter };
|