webspresso 0.0.90 → 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.
Files changed (94) hide show
  1. package/README.md +358 -8
  2. package/bin/commands/service.js +193 -0
  3. package/bin/webspresso.js +2 -0
  4. package/core/auth/middleware.js +9 -0
  5. package/core/compression/compressible.js +51 -0
  6. package/core/compression/compression-stream.js +59 -0
  7. package/core/compression/index.js +25 -0
  8. package/core/compression/middleware.js +301 -0
  9. package/core/compression/negotiate.js +153 -0
  10. package/core/errors/base.js +31 -0
  11. package/core/errors/domain.js +193 -0
  12. package/core/errors/http.js +133 -0
  13. package/core/errors/index.js +77 -0
  14. package/core/errors/middleware.js +162 -0
  15. package/core/errors/normalize.js +171 -0
  16. package/core/orm/complexity.js +120 -0
  17. package/core/orm/events.js +64 -14
  18. package/core/orm/index.js +45 -10
  19. package/core/orm/model.js +2 -0
  20. package/core/orm/query-builder.js +30 -4
  21. package/core/orm/repository.js +57 -25
  22. package/core/orm/transaction.js +39 -5
  23. package/core/orm/utils.js +6 -3
  24. package/core/queue/adapters/database.js +284 -0
  25. package/core/queue/adapters/memory.js +155 -0
  26. package/core/queue/adapters/redis.js +336 -0
  27. package/core/queue/index.js +21 -0
  28. package/core/queue/job.js +103 -0
  29. package/core/queue/manager.js +387 -0
  30. package/core/realtime/adapters/redis.js +431 -0
  31. package/core/realtime/adapters/socket-io.js +203 -0
  32. package/core/realtime/adapters/sse.js +210 -0
  33. package/core/realtime/adapters/websocket.js +337 -0
  34. package/core/realtime/auth-manager.js +140 -0
  35. package/core/realtime/core.js +359 -0
  36. package/core/realtime/emitter.js +98 -0
  37. package/core/realtime/errors.js +71 -0
  38. package/core/realtime/index.js +79 -0
  39. package/core/realtime/reconnect-manager.js +118 -0
  40. package/core/realtime/registry.js +243 -0
  41. package/core/shutdown/http-adapter.js +134 -0
  42. package/core/shutdown/index.js +12 -0
  43. package/core/shutdown/shutdown-manager.js +312 -0
  44. package/core/ssr/index.js +13 -0
  45. package/core/ssr/stream.js +174 -0
  46. package/index.d.ts +829 -52
  47. package/index.js +103 -0
  48. package/package.json +22 -2
  49. package/plugins/admin-panel/api.js +37 -4
  50. package/plugins/admin-panel/app.js +57 -17
  51. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
  52. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  53. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
  54. package/plugins/admin-panel/client/parts/08-record-list.js +17 -24
  55. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  56. package/plugins/admin-panel/core/admin-module.js +2 -2
  57. package/plugins/admin-panel/core/api-extensions.js +149 -0
  58. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  59. package/plugins/admin-panel/index.js +90 -17
  60. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  61. package/plugins/admin-panel/modules/menu.js +85 -0
  62. package/plugins/admin-panel/modules/user-management.js +5 -2
  63. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  64. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  65. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  66. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  67. package/plugins/basic-auth/index.js +293 -0
  68. package/plugins/data-exchange/export-xlsx.js +5 -1
  69. package/plugins/data-exchange/import.js +16 -2
  70. package/plugins/data-exchange/index.js +14 -0
  71. package/plugins/email/index.js +14 -1
  72. package/plugins/index.js +29 -0
  73. package/plugins/queue/index.js +108 -0
  74. package/plugins/realtime/index.js +150 -0
  75. package/plugins/recaptcha/index.js +11 -0
  76. package/plugins/redirect/index.js +9 -1
  77. package/plugins/rest-resources/index.js +280 -11
  78. package/plugins/upload/index.js +16 -0
  79. package/src/app-context.js +46 -6
  80. package/src/file-router.js +196 -21
  81. package/src/helpers.js +20 -2
  82. package/src/plugin-manager.js +124 -4
  83. package/src/server.js +304 -44
  84. package/src/services/builtins/auth.js +273 -0
  85. package/src/services/builtins/exchange.js +229 -0
  86. package/src/services/builtins/mail.js +134 -0
  87. package/src/services/builtins/media.js +196 -0
  88. package/src/services/builtins/system.js +221 -0
  89. package/src/services/discovery.js +140 -0
  90. package/src/services/executor.js +292 -0
  91. package/src/services/index.js +70 -0
  92. package/src/services/memoize.js +256 -0
  93. package/src/services/registry.js +288 -0
  94. package/src/services/validator.js +281 -0
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Queue Manager & Worker Dispatcher
3
+ * @module core/queue/manager
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const EventEmitter = require('events');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+ const { Job } = require('./job');
12
+ const { MemoryQueueAdapter } = require('./adapters/memory');
13
+
14
+ class QueueManager extends EventEmitter {
15
+ /**
16
+ * @param {Object} [options]
17
+ * @param {any} [options.adapter] - Queue adapter instance (defaults to MemoryQueueAdapter)
18
+ * @param {number} [options.concurrency=2] - Maximum concurrent running jobs
19
+ * @param {number} [options.pollInterval=500] - Polling interval in ms when idle
20
+ * @param {string} [options.jobsDir] - Directory to auto-discover job handlers
21
+ * @param {boolean} [options.autoStart=true] - Start worker loop immediately
22
+ */
23
+ constructor(options = {}) {
24
+ super();
25
+
26
+ this.adapter = options.adapter || new MemoryQueueAdapter();
27
+ this.concurrency = Math.max(1, options.concurrency || 2);
28
+ this.pollInterval = Math.max(50, options.pollInterval || 500);
29
+ this.jobsDir = options.jobsDir || null;
30
+
31
+ /** @type {Map<string, { handler: Function, options: Object }>} */
32
+ this.handlers = new Map();
33
+
34
+ /** @type {Map<string|number, Job>} */
35
+ this.activeJobs = new Map();
36
+
37
+ this.runningCount = 0;
38
+ this.isPaused = false;
39
+ this.isDraining = false;
40
+ this.isRunning = false;
41
+ this._timer = null;
42
+
43
+ if (this.jobsDir && fs.existsSync(this.jobsDir)) {
44
+ this.loadJobsFromDirectory(this.jobsDir);
45
+ }
46
+
47
+ if (options.autoStart !== false) {
48
+ this.start();
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Register a job processor handler
54
+ * @param {string} name - Job name (e.g. 'email.send')
55
+ * @param {Function} handler - Async worker function (job, ctx) => Promise<any>
56
+ * @param {Object} [options]
57
+ * @param {number} [options.attempts=3]
58
+ * @param {number|Object} [options.backoff]
59
+ * @param {number} [options.timeout=30000]
60
+ * @param {number} [options.priority=10]
61
+ */
62
+ define(name, handler, options = {}) {
63
+ if (typeof name !== 'string' || !name) {
64
+ throw new Error('Job name must be a non-empty string');
65
+ }
66
+ if (typeof handler !== 'function') {
67
+ throw new Error(`Job handler for '${name}' must be a function`);
68
+ }
69
+
70
+ this.handlers.set(name, {
71
+ handler,
72
+ options: {
73
+ attempts: 3,
74
+ timeout: 30000,
75
+ priority: 10,
76
+ ...options,
77
+ },
78
+ });
79
+
80
+ return this;
81
+ }
82
+
83
+ /**
84
+ * Check if a job handler is defined
85
+ * @param {string} name
86
+ * @returns {boolean}
87
+ */
88
+ has(name) {
89
+ return this.handlers.has(name);
90
+ }
91
+
92
+ /**
93
+ * Dispatch a job into the queue
94
+ * @param {string} name - Registered job name
95
+ * @param {any} [data={}] - Job payload
96
+ * @param {Object} [options] - Dispatch options (delay, priority, attempts, timeout, backoff)
97
+ * @returns {Promise<Job>}
98
+ */
99
+ async dispatch(name, data = {}, options = {}) {
100
+ const registered = this.handlers.get(name);
101
+ const defaults = registered ? registered.options : {};
102
+
103
+ let runAt = new Date();
104
+ if (options.delay) {
105
+ if (typeof options.delay === 'number') {
106
+ runAt = new Date(Date.now() + options.delay);
107
+ } else if (options.delay instanceof Date) {
108
+ runAt = options.delay;
109
+ }
110
+ }
111
+
112
+ const job = new Job({
113
+ name,
114
+ data,
115
+ runAt,
116
+ priority: options.priority !== undefined ? options.priority : defaults.priority,
117
+ maxAttempts: options.attempts !== undefined ? options.attempts : defaults.attempts,
118
+ timeout: options.timeout !== undefined ? options.timeout : defaults.timeout,
119
+ backoff: options.backoff !== undefined ? options.backoff : defaults.backoff,
120
+ onProgress: (j, percent, msg) => {
121
+ this.emit('job:progress', j, percent, msg);
122
+ },
123
+ });
124
+
125
+ const enqueued = await this.adapter.enqueue(job);
126
+ this.emit('job:queued', enqueued);
127
+
128
+ // Trigger immediate poll if capacity is available
129
+ if (this.isRunning && !this.isPaused && this.runningCount < this.concurrency) {
130
+ setImmediate(() => this._tick());
131
+ }
132
+
133
+ return enqueued;
134
+ }
135
+
136
+ /**
137
+ * Start processing jobs
138
+ */
139
+ start() {
140
+ if (this.isRunning) return this;
141
+ this.isRunning = true;
142
+ this.isPaused = false;
143
+ this._scheduleNextTick(0);
144
+ this.emit('start');
145
+ return this;
146
+ }
147
+
148
+ /**
149
+ * Pause processing (running jobs continue until finished)
150
+ */
151
+ pause() {
152
+ this.isPaused = true;
153
+ if (this._timer) {
154
+ clearTimeout(this._timer);
155
+ this._timer = null;
156
+ }
157
+ this.emit('pause');
158
+ return this;
159
+ }
160
+
161
+ /**
162
+ * Resume processing
163
+ */
164
+ resume() {
165
+ if (!this.isRunning) return this.start();
166
+ this.isPaused = false;
167
+ this._scheduleNextTick(0);
168
+ this.emit('resume');
169
+ return this;
170
+ }
171
+
172
+ /**
173
+ * Gracefully drain all active jobs before stopping
174
+ * @param {number} [timeoutMs=10000]
175
+ * @returns {Promise<void>}
176
+ */
177
+ async drain(timeoutMs = 10000) {
178
+ this.isDraining = true;
179
+ this.pause();
180
+
181
+ if (this.runningCount === 0) {
182
+ this.isDraining = false;
183
+ this.isRunning = false;
184
+ return;
185
+ }
186
+
187
+ const start = Date.now();
188
+ while (this.runningCount > 0) {
189
+ if (Date.now() - start > timeoutMs) {
190
+ break;
191
+ }
192
+ await new Promise((res) => setTimeout(res, 50));
193
+ }
194
+
195
+ this.isDraining = false;
196
+ this.isRunning = false;
197
+ this.emit('drain');
198
+ }
199
+
200
+ /**
201
+ * Stop worker loop
202
+ */
203
+ async stop() {
204
+ this.pause();
205
+ this.isRunning = false;
206
+ this.emit('stop');
207
+ }
208
+
209
+ /**
210
+ * Get queue statistics
211
+ */
212
+ async getStats() {
213
+ const stats = await this.adapter.getStats();
214
+ return {
215
+ ...stats,
216
+ activeWorkers: this.runningCount,
217
+ concurrency: this.concurrency,
218
+ registeredJobs: Array.from(this.handlers.keys()),
219
+ isPaused: this.isPaused,
220
+ isRunning: this.isRunning,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Get job by ID
226
+ * @param {string|number} jobId
227
+ */
228
+ async getJob(jobId) {
229
+ return this.adapter.getJob(jobId);
230
+ }
231
+
232
+ /**
233
+ * Auto-discover and register job files from directory recursively
234
+ * @param {string} dirPath
235
+ */
236
+ loadJobsFromDirectory(dirPath) {
237
+ if (!fs.existsSync(dirPath)) return;
238
+
239
+ const readRecursive = (curDir, basePrefix = '') => {
240
+ const entries = fs.readdirSync(curDir, { withFileTypes: true });
241
+ for (const entry of entries) {
242
+ const fullPath = path.join(curDir, entry.name);
243
+ if (entry.isDirectory()) {
244
+ readRecursive(fullPath, basePrefix ? `${basePrefix}.${entry.name}` : entry.name);
245
+ } else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.cjs'))) {
246
+ const nameWithoutExt = entry.name.replace(/\.(c)?js$/, '');
247
+ const jobName = basePrefix ? `${basePrefix}.${nameWithoutExt}` : nameWithoutExt;
248
+
249
+ try {
250
+ const mod = require(fullPath);
251
+ const handler = typeof mod === 'function' ? mod : (mod.handler || mod.default);
252
+ const options = mod.options || {};
253
+
254
+ if (typeof handler === 'function') {
255
+ this.define(jobName, handler, options);
256
+ }
257
+ } catch (err) {
258
+ console.error(`[QueueManager] Failed to load job from ${fullPath}:`, err.message);
259
+ }
260
+ }
261
+ }
262
+ };
263
+
264
+ readRecursive(dirPath);
265
+ }
266
+
267
+ /**
268
+ * Schedule next tick
269
+ * @private
270
+ */
271
+ _scheduleNextTick(delay = this.pollInterval) {
272
+ if (!this.isRunning || this.isPaused || this.isDraining) return;
273
+ if (this._timer) clearTimeout(this._timer);
274
+ this._timer = setTimeout(() => this._tick(), delay);
275
+ }
276
+
277
+ /**
278
+ * Process loop tick
279
+ * @private
280
+ */
281
+ async _tick() {
282
+ if (!this.isRunning || this.isPaused || this.isDraining) return;
283
+
284
+ while (this.runningCount < this.concurrency) {
285
+ if (this.isPaused || this.isDraining) break;
286
+
287
+ const registeredNames = Array.from(this.handlers.keys());
288
+ if (registeredNames.length === 0) {
289
+ break;
290
+ }
291
+
292
+ let job = null;
293
+ try {
294
+ job = await this.adapter.dequeue(registeredNames);
295
+ } catch (err) {
296
+ this.emit('error', err);
297
+ break;
298
+ }
299
+
300
+ if (!job) {
301
+ // No jobs ready
302
+ break;
303
+ }
304
+
305
+ // Found a job -> process it
306
+ this.runningCount++;
307
+ this.activeJobs.set(job.id, job);
308
+
309
+ // Attach progress handler
310
+ job._onProgress = (j, percent, msg) => {
311
+ this.emit('job:progress', j, percent, msg);
312
+ };
313
+
314
+ this._executeJob(job).finally(() => {
315
+ this.runningCount--;
316
+ this.activeJobs.delete(job.id);
317
+ if (this.isRunning && !this.isPaused) {
318
+ this._scheduleNextTick(0);
319
+ }
320
+ });
321
+ }
322
+
323
+ this._scheduleNextTick(this.pollInterval);
324
+ }
325
+
326
+ /**
327
+ * Execute single job with timeout & retry handling
328
+ * @private
329
+ */
330
+ async _executeJob(job) {
331
+ const registered = this.handlers.get(job.name);
332
+ if (!registered) {
333
+ const err = new Error(`No handler registered for job '${job.name}'`);
334
+ await this.adapter.fail(job.id, err);
335
+ this.emit('job:failed', job, err);
336
+ return;
337
+ }
338
+
339
+ this.emit('job:started', job);
340
+
341
+ const { handler } = registered;
342
+ const timeoutMs = job.timeout || 30000;
343
+
344
+ let timeoutTimer = null;
345
+ const timeoutPromise = new Promise((_, reject) => {
346
+ timeoutTimer = setTimeout(() => {
347
+ reject(new Error(`Job '${job.name}' (${job.id}) exceeded timeout of ${timeoutMs}ms`));
348
+ }, timeoutMs);
349
+ });
350
+
351
+ try {
352
+ const result = await Promise.race([
353
+ handler(job, { queue: this }),
354
+ timeoutPromise,
355
+ ]);
356
+ clearTimeout(timeoutTimer);
357
+
358
+ const completedJob = await this.adapter.complete(job.id, result);
359
+ this.emit('job:completed', completedJob || job, result);
360
+ } catch (err) {
361
+ clearTimeout(timeoutTimer);
362
+
363
+ if (job.attempts < job.maxAttempts) {
364
+ const delayMs = job.getNextRetryDelay();
365
+ const retryingJob = await this.adapter.retry(job.id, delayMs, err);
366
+ this.emit('job:retrying', retryingJob || job, err, delayMs);
367
+ } else {
368
+ const failedJob = await this.adapter.fail(job.id, err);
369
+ this.emit('job:failed', failedJob || job, err);
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Factory to create QueueManager
377
+ * @param {Object} [options]
378
+ * @returns {QueueManager}
379
+ */
380
+ function createQueueManager(options = {}) {
381
+ return new QueueManager(options);
382
+ }
383
+
384
+ module.exports = {
385
+ QueueManager,
386
+ createQueueManager,
387
+ };