webspresso 0.0.99 → 0.0.101

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 (108) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +4 -2
  3. package/.agents/AUTH.md +309 -0
  4. package/.agents/CLI.md +128 -0
  5. package/.agents/EMAIL.md +84 -2
  6. package/.agents/ERRORS.md +59 -0
  7. package/.agents/EVENTS.md +179 -0
  8. package/.agents/ORM.md +149 -0
  9. package/.agents/PLUGINS.md +44 -7
  10. package/.agents/QUEUE.md +18 -10
  11. package/.agents/ROUTING.md +97 -1
  12. package/.agents/STREAMING.md +35 -0
  13. package/AGENTS.md +2 -0
  14. package/README.md +145 -93
  15. package/bin/commands/add-docker.js +467 -0
  16. package/bin/commands/db-makemigrations.js +83 -0
  17. package/bin/commands/deploy-check.js +339 -0
  18. package/bin/commands/list.js +96 -0
  19. package/bin/commands/make-command.js +93 -0
  20. package/bin/commands/make-event.js +80 -0
  21. package/bin/commands/make-factory.js +72 -0
  22. package/bin/commands/make-mail.js +93 -0
  23. package/bin/commands/make-policy.js +106 -0
  24. package/bin/commands/run.js +195 -0
  25. package/bin/webspresso.js +21 -0
  26. package/core/auth/bouncer.js +196 -0
  27. package/core/auth/index.js +73 -1
  28. package/core/auth/magic-link.js +170 -0
  29. package/core/auth/manager.js +343 -3
  30. package/core/auth/middleware.js +45 -6
  31. package/core/auth/session-manager.js +255 -0
  32. package/core/auth/sessions.js +351 -0
  33. package/core/auth/tokens.js +1 -0
  34. package/core/auth/totp.js +4 -1
  35. package/core/auth/user-agent.js +161 -0
  36. package/core/commands/define-command.js +48 -0
  37. package/core/commands/index.js +18 -0
  38. package/core/commands/registry.js +156 -0
  39. package/core/commands/runner.js +76 -0
  40. package/core/compression/compressible.js +1 -1
  41. package/core/compression/middleware.js +35 -251
  42. package/core/content/service.js +8 -2
  43. package/core/events/discovery.js +142 -0
  44. package/core/events/dispatcher.js +552 -0
  45. package/core/events/index.js +23 -0
  46. package/core/events/signal.js +92 -0
  47. package/core/forms/flash.js +234 -0
  48. package/core/forms/form-builder.js +214 -0
  49. package/core/forms/index.js +16 -0
  50. package/core/mail/index.js +23 -0
  51. package/core/mail/mailable.js +250 -0
  52. package/core/mail/mailer.js +184 -0
  53. package/core/mail/transports.js +238 -0
  54. package/core/orm/factory.js +250 -0
  55. package/core/orm/index.js +8 -1
  56. package/core/orm/migrations/diff.js +185 -0
  57. package/core/orm/migrations/index.js +22 -4
  58. package/core/orm/model.js +55 -1
  59. package/core/orm/repository.js +283 -13
  60. package/core/orm/scopes.js +3 -2
  61. package/core/orm/types.js +6 -1
  62. package/core/queue/cron.js +252 -39
  63. package/core/queue/manager.js +20 -2
  64. package/core/queue/scheduler.js +115 -59
  65. package/core/ssr/stream.js +62 -2
  66. package/index.d.ts +540 -14
  67. package/index.js +44 -0
  68. package/package.json +11 -5
  69. package/plugins/admin-panel/api.js +509 -3
  70. package/plugins/admin-panel/index.js +17 -0
  71. package/plugins/admin-panel/modules/admin-users.js +1 -1
  72. package/plugins/audit-log/admin-component.js +54 -54
  73. package/plugins/audit-log/index.js +2 -2
  74. package/plugins/content/client/inline-edit.js +2 -46
  75. package/plugins/content/models/content-entry.js +4 -3
  76. package/plugins/content/models/content-type.js +4 -3
  77. package/plugins/cors.js +15 -4
  78. package/plugins/email/auth-bridge.js +66 -0
  79. package/plugins/email/bundled-templates.js +15 -0
  80. package/plugins/email/index.js +48 -0
  81. package/plugins/email/preview-handler.js +555 -0
  82. package/plugins/email/service.js +23 -12
  83. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  84. package/plugins/email/transport.js +30 -11
  85. package/plugins/file-manager/admin-component.js +49 -49
  86. package/plugins/file-manager/index.js +4 -4
  87. package/plugins/index.js +2 -0
  88. package/plugins/redirect/index.js +1 -1
  89. package/plugins/webhooks/admin-component.js +608 -0
  90. package/plugins/webhooks/api-handlers.js +237 -0
  91. package/plugins/webhooks/crypto.js +144 -0
  92. package/plugins/webhooks/dispatcher.js +708 -0
  93. package/plugins/webhooks/index.js +263 -0
  94. package/plugins/webhooks/matcher.js +113 -0
  95. package/plugins/webhooks/migration-template.js +75 -0
  96. package/plugins/webhooks/models.js +98 -0
  97. package/plugins/webhooks/sender.js +176 -0
  98. package/src/api/api-loader.js +1 -1
  99. package/src/app-context.js +48 -6
  100. package/src/bundle/index.js +2 -1
  101. package/src/dev-reload/index.js +9 -1
  102. package/src/discovery/scan-routes.js +10 -1
  103. package/src/file-router.js +173 -4
  104. package/src/helpers.js +150 -1
  105. package/src/njk-frontmatter.js +3 -0
  106. package/src/pages/page-loader.js +1 -1
  107. package/src/routing/cascading.js +120 -4
  108. package/src/server.js +180 -26
@@ -8,8 +8,6 @@
8
8
 
9
9
  const { CronExpression } = require('./cron');
10
10
 
11
- let scheduleCounter = 0;
12
-
13
11
  class TaskScheduler {
14
12
  /**
15
13
  * @param {import('./manager').QueueManager} queue - QueueManager instance
@@ -23,7 +21,7 @@ class TaskScheduler {
23
21
  }
24
22
 
25
23
  this.queue = queue;
26
- this.maxIntervalMs = options.maxIntervalMs || 60000;
24
+ this.maxIntervalMs = Math.max(100, options.maxIntervalMs || 60000);
27
25
  this.isRunning = false;
28
26
  this._timer = null;
29
27
 
@@ -55,8 +53,11 @@ class TaskScheduler {
55
53
  * @param {Object} [options]
56
54
  * @param {string} [options.id] - Optional custom schedule ID
57
55
  * @param {boolean} [options.overlap=false] - Allow concurrent executions of the same schedule
56
+ * @param {boolean} [options.preventOverlap] - Inverted alias for overlap: false
58
57
  * @param {number} [options.maxRuns] - Maximum number of runs before auto-unscheduling
59
58
  * @param {boolean} [options.utc=false] - Evaluate schedule in UTC
59
+ * @param {string} [options.timezone] - IANA Timezone (e.g. 'Europe/Istanbul')
60
+ * @param {string} [options.tz] - Alias for timezone
60
61
  * @param {boolean} [options.runImmediately=false] - Run immediately upon registration
61
62
  * @returns {Object} Registered schedule object
62
63
  */
@@ -67,13 +68,18 @@ class TaskScheduler {
67
68
 
68
69
  const cron = cronExpression instanceof CronExpression
69
70
  ? cronExpression
70
- : new CronExpression(cronExpression, { utc: !!options.utc });
71
+ : new CronExpression(cronExpression, {
72
+ utc: !!options.utc,
73
+ timezone: options.timezone || options.tz,
74
+ });
71
75
 
72
76
  const id = options.id || `schedule:${jobName}:${cron.resolvedExpression}`;
73
77
 
74
78
  const now = new Date();
75
79
  const nextRunAt = cron.getNextDate(now);
76
80
 
81
+ const allowOverlap = options.overlap === true || (options.preventOverlap === false);
82
+
77
83
  const scheduleObj = {
78
84
  id,
79
85
  name: jobName,
@@ -81,7 +87,8 @@ class TaskScheduler {
81
87
  cron,
82
88
  data,
83
89
  options: {
84
- overlap: options.overlap === true,
90
+ overlap: allowOverlap,
91
+ preventOverlap: !allowOverlap,
85
92
  maxRuns: typeof options.maxRuns === 'number' && options.maxRuns > 0 ? options.maxRuns : null,
86
93
  runImmediately: !!options.runImmediately,
87
94
  ...options,
@@ -91,6 +98,7 @@ class TaskScheduler {
91
98
  nextRunAt,
92
99
  isRunning: false,
93
100
  activeJobId: null,
101
+ activeJobIds: new Set(),
94
102
  registeredAt: now,
95
103
  };
96
104
 
@@ -98,7 +106,11 @@ class TaskScheduler {
98
106
  this.queue.emit('schedule:registered', scheduleObj);
99
107
 
100
108
  if (options.runImmediately) {
101
- setImmediate(() => this.trigger(id));
109
+ setImmediate(() => {
110
+ this.trigger(id).catch((err) => {
111
+ this.queue.emit('schedule:error', scheduleObj, err);
112
+ });
113
+ });
102
114
  }
103
115
 
104
116
  this._rescheduleTimer();
@@ -118,24 +130,28 @@ class TaskScheduler {
118
130
  * @returns {boolean} True if removed, false if not found
119
131
  */
120
132
  unschedule(idOrJobName) {
121
- let targetId = null;
122
-
123
133
  if (this.schedules.has(idOrJobName)) {
124
- targetId = idOrJobName;
125
- } else {
126
- for (const [id, s] of this.schedules.entries()) {
127
- if (s.name === idOrJobName) {
128
- targetId = id;
129
- break;
130
- }
134
+ const scheduleObj = this.schedules.get(idOrJobName);
135
+ this.schedules.delete(idOrJobName);
136
+ this.queue.emit('schedule:unregistered', scheduleObj);
137
+ this._rescheduleTimer();
138
+ return true;
139
+ }
140
+
141
+ const toRemove = [];
142
+ for (const [id, s] of this.schedules.entries()) {
143
+ if (s.name === idOrJobName) {
144
+ toRemove.push({ id, s });
131
145
  }
132
146
  }
133
147
 
134
- if (!targetId) return false;
148
+ if (toRemove.length === 0) return false;
149
+
150
+ for (const { id, s } of toRemove) {
151
+ this.schedules.delete(id);
152
+ this.queue.emit('schedule:unregistered', s);
153
+ }
135
154
 
136
- const scheduleObj = this.schedules.get(targetId);
137
- this.schedules.delete(targetId);
138
- this.queue.emit('schedule:unregistered', scheduleObj);
139
155
  this._rescheduleTimer();
140
156
  return true;
141
157
  }
@@ -156,6 +172,7 @@ class TaskScheduler {
156
172
  nextRunAt: s.nextRunAt,
157
173
  isRunning: s.isRunning,
158
174
  activeJobId: s.activeJobId,
175
+ activeJobIds: Array.from(s.activeJobIds || []),
159
176
  registeredAt: s.registeredAt,
160
177
  }));
161
178
  }
@@ -186,7 +203,8 @@ class TaskScheduler {
186
203
  throw new Error(`Schedule not found: '${idOrJobName}'`);
187
204
  }
188
205
 
189
- if (!s.options.overlap && s.isRunning) {
206
+ const allowOverlap = s.options.overlap === true || s.options.preventOverlap === false;
207
+ if (!allowOverlap && s.isRunning) {
190
208
  this.queue.emit('schedule:skipped', s, 'overlap');
191
209
  return null;
192
210
  }
@@ -198,6 +216,9 @@ class TaskScheduler {
198
216
  try {
199
217
  const job = await this.queue.dispatch(s.name, s.data, s.options);
200
218
  s.activeJobId = job.id;
219
+ if (!s.activeJobIds) s.activeJobIds = new Set();
220
+ s.activeJobIds.add(job.id);
221
+
201
222
  this.queue.emit('schedule:triggered', s, job);
202
223
 
203
224
  if (s.options.maxRuns && s.runsCount >= s.options.maxRuns) {
@@ -206,8 +227,10 @@ class TaskScheduler {
206
227
 
207
228
  return job;
208
229
  } catch (err) {
209
- s.isRunning = false;
210
- s.activeJobId = null;
230
+ if (!s.activeJobIds || s.activeJobIds.size === 0) {
231
+ s.isRunning = false;
232
+ s.activeJobId = null;
233
+ }
211
234
  throw err;
212
235
  }
213
236
  }
@@ -236,6 +259,20 @@ class TaskScheduler {
236
259
  return this;
237
260
  }
238
261
 
262
+ /**
263
+ * Pause alias for stop
264
+ */
265
+ pause() {
266
+ return this.stop();
267
+ }
268
+
269
+ /**
270
+ * Resume alias for start
271
+ */
272
+ resume() {
273
+ return this.start();
274
+ }
275
+
239
276
  /**
240
277
  * Clear all schedules and stop timer
241
278
  */
@@ -260,9 +297,16 @@ class TaskScheduler {
260
297
  */
261
298
  _handleJobSettled(job, err, result) {
262
299
  for (const s of this.schedules.values()) {
263
- if (s.activeJobId === job.id) {
264
- s.isRunning = false;
265
- s.activeJobId = null;
300
+ if (s.activeJobIds?.has(job.id) || s.activeJobId === job.id) {
301
+ if (s.activeJobIds) {
302
+ s.activeJobIds.delete(job.id);
303
+ }
304
+ if (!s.activeJobIds || s.activeJobIds.size === 0) {
305
+ s.isRunning = false;
306
+ s.activeJobId = null;
307
+ } else {
308
+ s.activeJobId = s.activeJobIds.values().next().value || null;
309
+ }
266
310
  this.queue.emit('schedule:finished', s, job, err ? { error: err } : { result });
267
311
  break;
268
312
  }
@@ -297,7 +341,7 @@ class TaskScheduler {
297
341
 
298
342
  if (earliest === Infinity) return;
299
343
 
300
- const delay = Math.max(50, Math.min(earliest - now, this.maxIntervalMs));
344
+ const delay = Math.max(0, Math.min(earliest - now, this.maxIntervalMs));
301
345
 
302
346
  this._timer = setTimeout(() => this._tick(), delay);
303
347
  }
@@ -309,51 +353,63 @@ class TaskScheduler {
309
353
  async _tick() {
310
354
  if (!this.isRunning) return;
311
355
 
312
- const now = new Date();
313
- const nowTime = now.getTime();
356
+ try {
357
+ const now = new Date();
358
+ const nowTime = now.getTime();
314
359
 
315
- const dueList = [];
316
- for (const s of this.schedules.values()) {
317
- if (s.nextRunAt && s.nextRunAt.getTime() <= nowTime) {
318
- dueList.push(s);
360
+ const dueList = [];
361
+ for (const s of this.schedules.values()) {
362
+ if (s.nextRunAt && s.nextRunAt.getTime() <= nowTime) {
363
+ dueList.push(s);
364
+ }
319
365
  }
320
- }
321
366
 
322
- for (const s of dueList) {
323
- // Calculate next run date immediately
324
- s.nextRunAt = s.cron.getNextDate(now);
367
+ for (const s of dueList) {
368
+ if (!this.isRunning) break;
325
369
 
326
- // Check overlap
327
- if (!s.options.overlap && s.isRunning) {
328
- this.queue.emit('schedule:skipped', s, 'overlap');
329
- s.lastRunAt = now;
330
- continue;
331
- }
370
+ // Advance next run date immediately to avoid double tick
371
+ s.nextRunAt = s.cron.getNextDate(now);
332
372
 
333
- s.isRunning = true;
334
- s.lastRunAt = now;
335
- s.runsCount++;
373
+ const allowOverlap = s.options.overlap === true || s.options.preventOverlap === false;
374
+ if (!allowOverlap && s.isRunning) {
375
+ this.queue.emit('schedule:skipped', s, 'overlap');
376
+ s.lastRunAt = now;
377
+ continue;
378
+ }
336
379
 
337
- try {
338
- const job = await this.queue.dispatch(s.name, s.data, s.options);
339
- s.activeJobId = job.id;
340
- this.queue.emit('schedule:triggered', s, job);
341
- } catch (err) {
342
- s.isRunning = false;
343
- s.activeJobId = null;
344
- this.queue.emit('schedule:error', s, err);
345
- }
380
+ s.isRunning = true;
381
+ s.lastRunAt = now;
382
+ s.runsCount++;
383
+
384
+ try {
385
+ const job = await this.queue.dispatch(s.name, s.data, s.options);
386
+ s.activeJobId = job.id;
387
+ if (!s.activeJobIds) s.activeJobIds = new Set();
388
+ s.activeJobIds.add(job.id);
389
+
390
+ this.queue.emit('schedule:triggered', s, job);
391
+ } catch (err) {
392
+ if (!s.activeJobIds || s.activeJobIds.size === 0) {
393
+ s.isRunning = false;
394
+ s.activeJobId = null;
395
+ }
396
+ this.queue.emit('schedule:error', s, err);
397
+ }
346
398
 
347
- // Check maxRuns
348
- if (s.options.maxRuns && s.runsCount >= s.options.maxRuns) {
349
- this.unschedule(s.id);
399
+ // Check maxRuns
400
+ if (s.options.maxRuns && s.runsCount >= s.options.maxRuns) {
401
+ this.unschedule(s.id);
402
+ }
350
403
  }
404
+ } catch (err) {
405
+ this.queue.emit('schedule:error', null, err);
406
+ } finally {
407
+ this._rescheduleTimer();
351
408
  }
352
-
353
- this._rescheduleTimer();
354
409
  }
355
410
  }
356
411
 
357
412
  module.exports = {
358
413
  TaskScheduler,
359
414
  };
415
+
@@ -17,7 +17,15 @@ const { Readable } = require('stream');
17
17
  * @returns {Readable}
18
18
  */
19
19
  function createHtmlStream(options = {}) {
20
- const { env, templatePath, context = {}, defer = {}, nonce: optionNonce } = options;
20
+ const {
21
+ env,
22
+ templatePath,
23
+ context = {},
24
+ defer = {},
25
+ nonce: optionNonce,
26
+ loadingTemplate,
27
+ layouts = [],
28
+ } = options;
21
29
  const nonce = optionNonce || context.nonce || context.cspNonce || '';
22
30
  const scriptTag = nonce ? `<script nonce="${nonce}">` : '<script>';
23
31
 
@@ -35,16 +43,33 @@ function createHtmlStream(options = {}) {
35
43
 
36
44
  if (deferredKeys.length === 0) {
37
45
  // Standard template streaming (render and stream)
38
- const html = env.render(templatePath, context);
46
+ let html = env.render(templatePath, context);
47
+ if (layouts && layouts.length > 0) {
48
+ const { applyCascadingLayouts } = require('../../src/routing/cascading');
49
+ html = applyCascadingLayouts(env, layouts, html, context);
50
+ }
39
51
  stream.push(html);
40
52
  stream.push(null);
41
53
  return;
42
54
  }
43
55
 
56
+ // Render loading skeleton if provided
57
+ let loadingHtml = '';
58
+ if (loadingTemplate) {
59
+ try {
60
+ loadingHtml = env.render(loadingTemplate, { ...context, __streaming: true });
61
+ } catch (_) {
62
+ loadingHtml = '';
63
+ }
64
+ }
65
+
44
66
  // Initial shell rendering with deferred slot placeholders
45
67
  const shellContext = {
46
68
  ...context,
47
69
  __streaming: true,
70
+ loadingHtml,
71
+ __loadingHtml: loadingHtml,
72
+ loadingSkeleton: loadingHtml,
48
73
  };
49
74
 
50
75
  for (const key of deferredKeys) {
@@ -53,6 +78,35 @@ function createHtmlStream(options = {}) {
53
78
 
54
79
  let shellHtml = env.render(templatePath, shellContext);
55
80
 
81
+ // Check slot-specific loading templates first
82
+ for (const key of deferredKeys) {
83
+ const deferredItem = defer[key];
84
+ if (deferredItem && typeof deferredItem === 'object' && deferredItem.loadingTemplate) {
85
+ try {
86
+ const slotLoadingHtml = env.render(deferredItem.loadingTemplate, { ...context, __streaming: true });
87
+ const slotRegex = new RegExp(
88
+ `(<([a-zA-Z0-9_-]+)\\b[^>]*data-stream-target=["']${key}["'][^>]*>)\\s*(<\\/\\2>)`,
89
+ 'g'
90
+ );
91
+ shellHtml = shellHtml.replace(slotRegex, `$1${slotLoadingHtml}$3`);
92
+ } catch (_) {}
93
+ }
94
+ }
95
+
96
+ // If empty stream targets exist and default loadingHtml is available, populate them
97
+ if (loadingHtml) {
98
+ shellHtml = shellHtml.replace(
99
+ /(<([a-zA-Z0-9_-]+)\b[^>]*data-stream-target=["'][^"']+["'][^>]*>)\s*(<\/\2>)/g,
100
+ `$1${loadingHtml}$3`
101
+ );
102
+ }
103
+
104
+ // Wrap shell with cascading nested layouts if provided
105
+ if (layouts && layouts.length > 0) {
106
+ const { applyCascadingLayouts } = require('../../src/routing/cascading');
107
+ shellHtml = applyCascadingLayouts(env, layouts, shellHtml, shellContext);
108
+ }
109
+
56
110
  // Check if template contains streaming slot markers, or split at first slot
57
111
  // We push the initial shell chunk immediately to start browser TTFB & CSS download
58
112
  stream.push(shellHtml);
@@ -127,6 +181,8 @@ ${scriptTag}
127
181
  * @param {Object} [options.defer] - Deferred slot promises
128
182
  * @param {number} [options.status=200] - HTTP status code
129
183
  * @param {boolean} [options.flushImmediately=true] - Flush headers immediately
184
+ * @param {string} [options.loadingTemplate] - Loading skeleton template
185
+ * @param {Array<Object>} [options.layouts] - Cascading layouts
130
186
  * @returns {Promise<void>}
131
187
  */
132
188
  function renderStream(res, templatePath, context = {}, options = {}) {
@@ -137,6 +193,8 @@ function renderStream(res, templatePath, context = {}, options = {}) {
137
193
  status = 200,
138
194
  flushImmediately = true,
139
195
  nonce: optionNonce,
196
+ loadingTemplate,
197
+ layouts,
140
198
  } = options;
141
199
 
142
200
  const nonce =
@@ -169,6 +227,8 @@ function renderStream(res, templatePath, context = {}, options = {}) {
169
227
  context,
170
228
  defer,
171
229
  nonce,
230
+ loadingTemplate,
231
+ layouts,
172
232
  });
173
233
 
174
234
  let finished = false;