webspresso 0.0.108 → 0.0.110
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 +21 -0
- package/README.md +3 -3
- package/bin/commands/add-esbuild.js +7 -3
- package/bin/commands/add-tailwind.js +9 -5
- package/bin/commands/build.js +20 -4
- package/bin/commands/dev.js +57 -7
- package/bin/commands/doctor.js +42 -1
- package/bin/commands/favicon-generate.js +10 -3
- package/bin/commands/new.js +8 -3
- package/bin/commands/start.js +3 -1
- package/bin/utils/add-command.js +16 -0
- package/core/app.js +45 -3
- package/core/auth/authenticate.js +3 -3
- package/core/auth/hash.js +167 -43
- package/core/auth/index.js +13 -1
- package/core/auth/jwt.js +21 -2
- package/core/auth/manager.js +63 -14
- package/core/auth/middleware.js +4 -2
- package/core/auth/oauth.js +5 -1
- package/core/auth/tokens.js +7 -2
- package/core/auth/totp.js +42 -6
- package/core/auth/user-agent.js +2 -0
- package/core/client/adapter/datastar.js +1401 -0
- package/core/client/api.js +1337 -0
- package/core/client/controller.js +1706 -0
- package/core/client/debug.js +367 -0
- package/core/client/devtool.js +1134 -0
- package/core/client/forms.js +1164 -0
- package/core/client/fragments.js +894 -0
- package/core/client/index.d.ts +59 -0
- package/core/client/index.js +391 -0
- package/core/client/lite.js +175 -0
- package/core/client/server-fragments.js +868 -0
- package/core/client/storage.js +339 -0
- package/core/client/utils.js +281 -0
- package/core/compression/middleware.js +277 -26
- package/core/context/app-context.js +25 -3
- package/core/context/request.js +32 -4
- package/core/errors/middleware.js +14 -0
- package/core/events/dispatcher.js +33 -0
- package/core/forms/flash.js +136 -102
- package/core/health/middleware.js +36 -6
- package/core/http/pipeline.js +45 -6
- package/core/index.js +25 -7
- package/core/kernel/view.js +41 -10
- package/core/metrics/middleware.js +30 -6
- package/core/metrics/registry.js +13 -5
- package/core/orm/cache/fingerprint.js +32 -11
- package/core/orm/cache/layer.js +19 -2
- package/core/orm/hidden.js +145 -0
- package/core/orm/model.js +48 -2
- package/core/orm/query-builder.js +343 -6
- package/core/orm/query-filters.js +14 -2
- package/core/orm/repository.js +64 -13
- package/core/orm/schema-helpers.js +9 -6
- package/core/orm/transaction.js +50 -6
- package/core/orm/types.js +4 -0
- package/core/queue/adapters/database.js +184 -2
- package/core/queue/adapters/memory.js +153 -5
- package/core/queue/adapters/redis.js +59 -4
- package/core/queue/job.js +14 -1
- package/core/queue/manager.js +115 -0
- package/core/resilience/circuit-breaker.js +27 -8
- package/core/services/builtins/exchange.js +174 -7
- package/core/services/builtins/file-manager.js +36 -0
- package/core/services/builtins/mail.js +27 -0
- package/core/services/builtins/queue.js +144 -0
- package/core/services/builtins/security.js +118 -0
- package/core/services/builtins/webhook.js +110 -0
- package/core/services/executor.js +38 -16
- package/core/services/index.js +4 -1
- package/core/sse/stream.js +28 -5
- package/core/utils/timeout.js +8 -1
- package/core/validation/compiler.js +10 -7
- package/index.d.ts +1320 -74
- package/index.js +128 -8
- package/package.json +75 -27
- package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
- package/plugins/admin-panel/index.js +19 -8
- package/plugins/csrf/index.js +3 -3
- package/plugins/data-exchange/export.js +214 -0
- package/plugins/data-exchange/import.js +48 -5
- package/plugins/data-exchange/index.js +70 -28
- package/plugins/data-exchange/preview.js +266 -0
- package/plugins/data-exchange/template.js +242 -0
- package/plugins/email/admin-component.js +783 -141
- package/plugins/email/api-handlers.js +51 -1
- package/plugins/email/index.js +5 -0
- package/plugins/email/service.js +48 -0
- package/plugins/file-manager/admin-component.js +1546 -70
- package/plugins/file-manager/api-handlers.js +166 -1
- package/plugins/file-manager/core.js +598 -8
- package/plugins/file-manager/index.js +8 -0
- package/plugins/index.js +7 -0
- package/plugins/mcp/auth.js +3 -3
- package/plugins/oauth/index.js +36 -3
- package/plugins/queue/admin-component.js +1092 -0
- package/plugins/queue/api-handlers.js +248 -0
- package/plugins/queue/index.js +69 -0
- package/plugins/rest-resources/index.js +7 -2
- package/plugins/security/admin-component.js +689 -0
- package/plugins/security/api-handlers.js +169 -0
- package/plugins/security/audit.js +153 -0
- package/plugins/security/bruteforce.js +244 -0
- package/plugins/security/firewall.js +459 -0
- package/plugins/security/headers.js +214 -0
- package/plugins/security/honeypot.js +234 -0
- package/plugins/security/incident-recorder.js +137 -0
- package/plugins/security/index.js +211 -0
- package/plugins/seo-checker/analyzer.js +20 -7
- package/plugins/upload/index.js +36 -0
- package/plugins/webhooks/admin-component.js +702 -175
- package/plugins/webhooks/api-handlers.js +75 -1
- package/plugins/webhooks/dispatcher.js +290 -11
- package/plugins/webhooks/index.js +37 -2
- package/plugins/webhooks/sender.js +56 -0
- package/plugins/xlsx/engine.js +19 -4
- package/src/api/api-loader.js +19 -1
- package/src/bundle/index.js +443 -17
- package/src/client-runtime/bootstrap-swup.js +9 -0
- package/src/client-runtime/mount.js +15 -20
- package/src/client-runtime/resolve.js +3 -2
- package/src/dev/api-typegen.js +1300 -0
- package/src/dev/banner.js +10 -1
- package/src/dev/colors.js +1 -1
- package/src/dev/index.js +7 -1
- package/src/dev/typegen.js +173 -17
- package/src/dev-reload/index.js +1 -1
- package/src/discovery/file-route-parser.js +17 -3
- package/src/discovery/scan-routes.js +4 -1
- package/src/file-router.js +253 -69
- package/src/helpers.js +46 -26
- package/src/njk-frontmatter.js +30 -4
- package/src/routing/cascading.js +26 -8
- package/src/routing/mount-discovered-routes.js +76 -0
- package/src/server.js +231 -70
- package/views/partials/webspresso-client-runtime.njk +1 -9
- package/.agents/ADMIN.md +0 -426
- package/.agents/AGENTS.md +0 -96
- package/.agents/ARCHITECTURE.md +0 -163
- package/.agents/AUTH.md +0 -581
- package/.agents/CACHE.md +0 -148
- package/.agents/CLI.md +0 -649
- package/.agents/COMPRESSION.md +0 -59
- package/.agents/CONTEXT.md +0 -152
- package/.agents/CONVENTIONS.md +0 -50
- package/.agents/EARLY_HINTS.md +0 -120
- package/.agents/EMAIL.md +0 -223
- package/.agents/ERRORS.md +0 -260
- package/.agents/EVENTS.md +0 -179
- package/.agents/FLAGS.md +0 -118
- package/.agents/HEALTH.md +0 -118
- package/.agents/LIMITS.md +0 -109
- package/.agents/LOGGING.md +0 -93
- package/.agents/MCP.md +0 -103
- package/.agents/METRICS.md +0 -127
- package/.agents/MIGRATION_JOBS.md +0 -95
- package/.agents/ORM.md +0 -700
- package/.agents/PLUGINS.md +0 -729
- package/.agents/QUEUE.md +0 -290
- package/.agents/RATE_LIMIT.md +0 -147
- package/.agents/REALTIME.md +0 -213
- package/.agents/RESILIENCE.md +0 -235
- package/.agents/ROUTING.md +0 -605
- package/.agents/SCALE_OUT.md +0 -109
- package/.agents/SECRETS.md +0 -117
- package/.agents/SERVICES.md +0 -341
- package/.agents/SHUTDOWN.md +0 -74
- package/.agents/STREAMING.md +0 -108
- package/.agents/WORKFLOW.md +0 -27
- package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
- package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
- package/.agents/skills/webspresso-framework/SKILL.md +0 -89
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
- package/.agents/skills/webspresso-services/SKILL.md +0 -134
- package/.cursorrules +0 -14
- package/AGENTS.md +0 -203
- package/CLAUDE.md +0 -54
- package/core/kernel/run-demo.js +0 -58
- package/plugins/admin-panel/styles.js +0 -9
- package/plugins/data-exchange/export-xlsx.js +0 -119
- package/src/client-runtime/bootstrap-alpine-swup.js +0 -34
package/.agents/ERRORS.md
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
# Webspresso Framework Exceptions & Error Handling Guide
|
|
2
|
-
|
|
3
|
-
[← Back to AGENTS.md](.agents/AGENTS.md)
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## ⚠️ STRICT RULE: Throw Semantic Errors (NO Generic `new Error()` or Manual `res.status`)
|
|
8
|
-
|
|
9
|
-
- **NEVER** return manual error payloads like `return res.status(404).json(...)` inside services or loaders.
|
|
10
|
-
- **NEVER** throw a generic `throw new Error('Not found')` for business or client-facing issues. In production, unmapped `Error` instances are automatically masked as `500 Internal Server Error` with stack traces hidden!
|
|
11
|
-
- **ALWAYS** throw semantic framework exceptions:
|
|
12
|
-
- `throw new NotFoundError('Item not found')` → returns HTTP 404
|
|
13
|
-
- `throw new ValidationError('Invalid data', { fields: { ... } })` → returns HTTP 422
|
|
14
|
-
- `throw new UnauthorizedError('Please log in')` → returns HTTP 401
|
|
15
|
-
- `throw new ForbiddenError('Access denied')` → returns HTTP 403
|
|
16
|
-
- `throw new BadRequestError('Invalid query')` → returns HTTP 400
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## 1. Overview & Exception Hierarchy
|
|
21
|
-
|
|
22
|
-
Webspresso features a centralized, lightweight, zero-dependency exception hierarchy inspired by Django's failure domain separation. Thrown synchronous and asynchronous exceptions are automatically caught by the global error boundary, normalized, and formatted as standardized JSON responses for API routes or styled HTML pages for web clients.
|
|
23
|
-
|
|
24
|
-
```text
|
|
25
|
-
WebspressoError (base class with cause, code, details)
|
|
26
|
-
├── ConfigurationError (bootstrap & config errors)
|
|
27
|
-
├── PluginError (plugin lifecycle & dependency errors)
|
|
28
|
-
├── ValidationError (validation failures with fields, 422 default)
|
|
29
|
-
├── SecurityError (suspicious requests, CSRF/cookie tampering, 400 default)
|
|
30
|
-
├── RequestError
|
|
31
|
-
│ └── RequestAbortedError (client socket disconnection, 499 default)
|
|
32
|
-
├── RouterError
|
|
33
|
-
│ ├── RouteNotFoundError (404 URL resolution error)
|
|
34
|
-
│ └── RouteGenerationError (named route generation error)
|
|
35
|
-
└── HttpError (base for HTTP-mapped exceptions)
|
|
36
|
-
├── BadRequestError (400)
|
|
37
|
-
├── UnauthorizedError (401)
|
|
38
|
-
├── ForbiddenError (403)
|
|
39
|
-
├── NotFoundError (404)
|
|
40
|
-
├── MethodNotAllowedError (405)
|
|
41
|
-
├── ConflictError (409)
|
|
42
|
-
├── PayloadTooLargeError (413)
|
|
43
|
-
├── UnsupportedMediaTypeError (415)
|
|
44
|
-
├── UnprocessableEntityError (422)
|
|
45
|
-
└── TooManyRequestsError (429)
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## 2. Public Imports & Subpath
|
|
51
|
-
|
|
52
|
-
```js
|
|
53
|
-
import {
|
|
54
|
-
WebspressoError,
|
|
55
|
-
HttpError,
|
|
56
|
-
BadRequestError,
|
|
57
|
-
UnauthorizedError,
|
|
58
|
-
ForbiddenError,
|
|
59
|
-
NotFoundError,
|
|
60
|
-
MethodNotAllowedError,
|
|
61
|
-
ConflictError,
|
|
62
|
-
PayloadTooLargeError,
|
|
63
|
-
UnsupportedMediaTypeError,
|
|
64
|
-
UnprocessableEntityError,
|
|
65
|
-
TooManyRequestsError,
|
|
66
|
-
ValidationError,
|
|
67
|
-
ConfigurationError,
|
|
68
|
-
PluginError,
|
|
69
|
-
SecurityError,
|
|
70
|
-
RequestAbortedError,
|
|
71
|
-
RouteNotFoundError,
|
|
72
|
-
RouteGenerationError,
|
|
73
|
-
} from 'webspresso';
|
|
74
|
-
|
|
75
|
-
// Or via subpath:
|
|
76
|
-
import { NotFoundError, ValidationError } from 'webspresso/errors';
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## 3. Usage Examples
|
|
82
|
-
|
|
83
|
-
### 3.1 HTTP Exceptions in API Handlers
|
|
84
|
-
```js
|
|
85
|
-
// pages/api/users/[id].get.js
|
|
86
|
-
import { NotFoundError } from 'webspresso';
|
|
87
|
-
|
|
88
|
-
module.exports = async function handler(req, res) {
|
|
89
|
-
const user = await req.db.getRepository('User').findById(req.params.id);
|
|
90
|
-
if (!user) {
|
|
91
|
-
throw new NotFoundError('User not found', {
|
|
92
|
-
code: 'USER_NOT_FOUND',
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
res.json({ user });
|
|
96
|
-
};
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### 3.2 Structured Validation Errors (422)
|
|
100
|
-
```js
|
|
101
|
-
import { ValidationError } from 'webspresso';
|
|
102
|
-
|
|
103
|
-
throw new ValidationError('Validation failed', {
|
|
104
|
-
code: 'INVALID_INPUT',
|
|
105
|
-
fields: {
|
|
106
|
-
email: ['Invalid email address'],
|
|
107
|
-
age: ['Must be at least 18'],
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### 3.3 Custom Error Classes
|
|
113
|
-
```js
|
|
114
|
-
import { HttpError } from 'webspresso';
|
|
115
|
-
|
|
116
|
-
export class PaymentRequiredError extends HttpError {
|
|
117
|
-
constructor(message = 'Payment Required', options = {}) {
|
|
118
|
-
super(402, message, {
|
|
119
|
-
code: options.code || 'PAYMENT_REQUIRED',
|
|
120
|
-
expose: true,
|
|
121
|
-
...options,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### 3.4 Custom Error Handler (`app.setErrorHandler`)
|
|
128
|
-
```js
|
|
129
|
-
const { app } = createApp({ pagesDir: './pages' });
|
|
130
|
-
|
|
131
|
-
app.setErrorHandler(async (err, req, res, next) => {
|
|
132
|
-
if (err instanceof ValidationError) {
|
|
133
|
-
return res.status(422).json({
|
|
134
|
-
success: false,
|
|
135
|
-
errors: err.fields,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
// Re-throwing or returning unhandled delegates to Webspresso default handler
|
|
139
|
-
});
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## 4. Response Contract & Production Masking
|
|
145
|
-
|
|
146
|
-
### Default JSON Contract:
|
|
147
|
-
```json
|
|
148
|
-
{
|
|
149
|
-
"status": 404,
|
|
150
|
-
"error": "Not Found",
|
|
151
|
-
"message": "User not found",
|
|
152
|
-
"code": "USER_NOT_FOUND"
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### Development vs Production:
|
|
157
|
-
- **Production (`NODE_ENV === 'production'`)**: 500 errors are masked to `"Internal Server Error"`. `stack`, `cause`, and internal file paths are never exposed to clients.
|
|
158
|
-
- **Development**: Returns `stack`, `cause`, and a structured `trace` object identifying the exact route, module, source file, and execution phase.
|
|
159
|
-
|
|
160
|
-
---
|
|
161
|
-
|
|
162
|
-
## 5. Route Tracing & Request ID Observability
|
|
163
|
-
|
|
164
|
-
Webspresso automatically correlates every request and enriches thrown errors with route-level execution metadata:
|
|
165
|
-
|
|
166
|
-
### 5.1 Request Correlation ID (`req.id` / `X-Request-Id`)
|
|
167
|
-
Every incoming request is assigned a unique UUID (`req.id`) available on `req.id` and returned in the `X-Request-Id` response header:
|
|
168
|
-
```javascript
|
|
169
|
-
// Access in services, loaders, or logs:
|
|
170
|
-
const reqId = req.id; // e.g. "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### 5.2 Context-Enriched Trace Object (Development Mode)
|
|
174
|
-
When an exception occurs during request execution in `definePage`, `defineApi`, or a feature module, the development JSON payload contains an explicit `trace` object:
|
|
175
|
-
|
|
176
|
-
```json
|
|
177
|
-
{
|
|
178
|
-
"status": 500,
|
|
179
|
-
"error": "Internal Server Error",
|
|
180
|
-
"message": "Database query failed",
|
|
181
|
-
"trace": {
|
|
182
|
-
"route": "/api/auth/me",
|
|
183
|
-
"method": "GET",
|
|
184
|
-
"source": "src/modules/auth/api/me.get.js",
|
|
185
|
-
"module": "auth",
|
|
186
|
-
"phase": "handler",
|
|
187
|
-
"requestId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
|
|
188
|
-
},
|
|
189
|
-
"stack": "Error: Database query failed\n at ...",
|
|
190
|
-
"cause": null
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### 5.3 Visual Console Error Banners
|
|
195
|
-
In server logs, 500 errors are automatically highlighted with visual banners pinpointing the root cause:
|
|
196
|
-
```txt
|
|
197
|
-
[webspresso] 🚨 Error at GET /api/auth/me [module: auth] (reqId: 9b1deb4d-3b7d-...)
|
|
198
|
-
Source: src/modules/auth/api/me.get.js
|
|
199
|
-
Phase: handler
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## 6. Directory-Level Error Boundaries (`_error.njk` & `_error.js`)
|
|
205
|
-
|
|
206
|
-
In addition to API JSON errors and the top-level 500 error page, Webspresso provides **Directory-Level Error Boundaries** for SSR pages (similar to Next.js `error.js` or Nuxt `error.vue`):
|
|
207
|
-
|
|
208
|
-
```
|
|
209
|
-
pages/
|
|
210
|
-
├── _layout.njk # Root site layout
|
|
211
|
-
├── _error.njk # Global site error fallback
|
|
212
|
-
└── dashboard/
|
|
213
|
-
├── _layout.njk # Dashboard layout (sidebar, header)
|
|
214
|
-
├── _error.njk # Dashboard-scoped error boundary
|
|
215
|
-
├── _error.js # Optional companion loader
|
|
216
|
-
├── billing/
|
|
217
|
-
│ └── invoices.njk # If load() fails here...
|
|
218
|
-
└── settings.njk
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### 6.1 Isolated Error Containment
|
|
222
|
-
When an unhandled error is thrown in a page route's `load()`, template rendering, or lifecycle hooks:
|
|
223
|
-
1. Webspresso searches up the directory hierarchy (innermost leaf to root `pages/`) for the nearest `_error.njk`.
|
|
224
|
-
2. The error boundary is rendered **inside** the ancestor cascading layouts (`_layout.njk`), ensuring that headers, sidebars, and navigation remain visible while displaying the isolated error state.
|
|
225
|
-
3. The HTTP response status is preserved from `error.status || error.statusCode || 500` (e.g. throwing `NotFoundError` sets HTTP 404).
|
|
226
|
-
|
|
227
|
-
### 6.2 Companion Loader (`_error.js`)
|
|
228
|
-
An optional companion `_error.js` can prefetch support data, report errors to monitoring services, or enrich the context:
|
|
229
|
-
|
|
230
|
-
```javascript
|
|
231
|
-
// pages/dashboard/_error.js
|
|
232
|
-
module.exports = {
|
|
233
|
-
load: async ({ error, req, res, ctx, db }) => {
|
|
234
|
-
return {
|
|
235
|
-
supportEmail: 'support@example.com',
|
|
236
|
-
incidentId: req.id,
|
|
237
|
-
};
|
|
238
|
-
},
|
|
239
|
-
};
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### 6.3 Template Context Variables
|
|
243
|
-
Inside `_error.njk`, the following variables are injected:
|
|
244
|
-
- `error`: `{ message, status, statusCode, code, name, details, fields, stack }` (stack is only exposed when `isDev === true`).
|
|
245
|
-
- `route`: `{ routePath, file }`.
|
|
246
|
-
- `req`: `{ path, query, params, method, url }`.
|
|
247
|
-
- `fsy`: Template helper suite.
|
|
248
|
-
- Plus any data returned by companion `_error.js` loader.
|
|
249
|
-
|
|
250
|
-
### 6.4 Layout Opt-Out
|
|
251
|
-
To render a full-page custom error view without the parent layout, set `layout: false` in `_error.js` or frontmatter:
|
|
252
|
-
```yaml
|
|
253
|
-
---
|
|
254
|
-
layout: false
|
|
255
|
-
---
|
|
256
|
-
<div class="standalone-error-screen">
|
|
257
|
-
<h1>{{ error.status }} - {{ error.message }}</h1>
|
|
258
|
-
</div>
|
|
259
|
-
```
|
|
260
|
-
|
package/.agents/EVENTS.md
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
# Webspresso Events & Signals Architecture
|
|
2
|
-
|
|
3
|
-
[← Back to AGENTS.md](.agents/AGENTS.md)
|
|
4
|
-
|
|
5
|
-
Webspresso provides an enterprise, zero-dependency **Decoupled Event & Signals System** (`core/events`), combining **AdonisJS Events** (wildcard matching, async parallel/serial/waterfall dispatch, queue bridging, and listener auto-discovery) with **Django Signals** (declarative `Signal` channels, sender filtering, and ORM model signals).
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1. Overview & Key Capabilities
|
|
10
|
-
|
|
11
|
-
- **Zero External Dependencies**: Built entirely with native Node.js standard modules (`events`, `path`, `fs`).
|
|
12
|
-
- **Synchronous & Asynchronous Emission**:
|
|
13
|
-
- `events.emit(name, data)`: Fast sync/fire-and-forget dispatch.
|
|
14
|
-
- `await events.emitAsync(name, data)`: Parallel concurrent execution (`Promise.allSettled`).
|
|
15
|
-
- `await events.emitSerial(name, data)`: Sequential execution in priority order.
|
|
16
|
-
- `await events.emitWaterfall(name, initialData)`: Sequential pipeline where output of listener $N$ feeds listener $N+1$.
|
|
17
|
-
- **Wildcard & Pattern Matching**:
|
|
18
|
-
- `events.on('order.*', (data, meta) => ...)`
|
|
19
|
-
- `events.on('system.**', (data, meta) => ...)`
|
|
20
|
-
- `events.onAny((eventName, data, meta) => ...)`
|
|
21
|
-
- **Django-Style Signals & Sender Filtering**:
|
|
22
|
-
- `const orderPlaced = defineSignal('order_placed')`
|
|
23
|
-
- `orderPlaced.connect(receiver, { sender: 'Checkout' })`
|
|
24
|
-
- `orderPlaced.send({ orderId: 101 }, { sender: 'Checkout' })`
|
|
25
|
-
- **Background Queue Bridging**:
|
|
26
|
-
- `events.listenAndQueue('user.created', 'send-welcome-email', { delay: 1000 })`
|
|
27
|
-
- Automatically bridges emitted events directly to Webspresso's job queue (`core/queue`).
|
|
28
|
-
- **ORM Model Signals Auto-Bridge**:
|
|
29
|
-
- Automatically translates `ModelEvents` (`User.afterCreate`, `*.afterDelete`) to application events (`model:created`, `user:created`, `model:deleted`) with `sender: ctx.model`.
|
|
30
|
-
- **Automatic Request & Context Decoration**:
|
|
31
|
-
- Injected onto `req.events` on every Express request.
|
|
32
|
-
- Available inside `req.context.events` for services: `ctx.events.emit('order.created', order)`.
|
|
33
|
-
- Accessible on `app.events` and plugin contexts.
|
|
34
|
-
- **Graceful Shutdown Integration**:
|
|
35
|
-
- Cleaned up automatically on graceful shutdown via `ShutdownManager.onShutdown()`.
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## 2. Using Events in API Routes & Services
|
|
40
|
-
|
|
41
|
-
### 2.1 Emitting Events in File-Based Routes (`pages/api/...`)
|
|
42
|
-
|
|
43
|
-
```javascript
|
|
44
|
-
// pages/api/orders.post.js
|
|
45
|
-
const { defineApi } = require('webspresso');
|
|
46
|
-
|
|
47
|
-
module.exports = defineApi({
|
|
48
|
-
schema: ({ z }) => ({
|
|
49
|
-
body: z.object({
|
|
50
|
-
total: z.number().positive(),
|
|
51
|
-
items: z.array(z.string()),
|
|
52
|
-
}),
|
|
53
|
-
}),
|
|
54
|
-
async handler(req, res) {
|
|
55
|
-
const order = await req.db.getRepository('Order').create(req.input.body);
|
|
56
|
-
|
|
57
|
-
// Synchronous fire-and-forget or awaited async parallel
|
|
58
|
-
req.events.emit('order.created', order, { sender: 'Order' });
|
|
59
|
-
// or: await req.events.emitAsync('order.created', order);
|
|
60
|
-
|
|
61
|
-
return res.status(201).json(order);
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### 2.2 Emitting Events inside Services (`services/...`)
|
|
67
|
-
|
|
68
|
-
```javascript
|
|
69
|
-
// services/order/checkout.js
|
|
70
|
-
const { defineService } = require('webspresso');
|
|
71
|
-
|
|
72
|
-
module.exports = defineService({
|
|
73
|
-
transaction: true,
|
|
74
|
-
async handle(input, ctx) {
|
|
75
|
-
const order = await ctx.db.getRepository('Order').create(input);
|
|
76
|
-
|
|
77
|
-
// ctx.events is automatically available
|
|
78
|
-
ctx.events.emit('order.checkout.completed', { orderId: order.id, user: ctx.req?.user });
|
|
79
|
-
|
|
80
|
-
return order;
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## 3. Registering Listeners
|
|
88
|
-
|
|
89
|
-
### 3.1 Programmatic Subscriptions
|
|
90
|
-
|
|
91
|
-
```javascript
|
|
92
|
-
const { events } = require('webspresso');
|
|
93
|
-
|
|
94
|
-
// 1. Basic Listener
|
|
95
|
-
const unsub = events.on('order.created', (order, meta) => {
|
|
96
|
-
console.log(`Order ${order.id} received at ${meta.timestamp}`);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// 2. Wildcard Listener
|
|
100
|
-
events.on('order.*', (data, meta) => {
|
|
101
|
-
console.log(`Order event ${meta.event} occurred`);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// 3. One-time Listener
|
|
105
|
-
events.once('app.ready', () => {
|
|
106
|
-
console.log('Application initialized');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// 4. Priority Ordering (higher runs first)
|
|
110
|
-
events.on('user.login', auditLogin, { priority: 100 });
|
|
111
|
-
events.on('user.login', notifyFriends, { priority: 0 });
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### 3.2 File-Based Discovery (`listeners/` directory)
|
|
115
|
-
|
|
116
|
-
Webspresso auto-discovers listeners in the `listeners/` root directory:
|
|
117
|
-
|
|
118
|
-
```javascript
|
|
119
|
-
// listeners/send-welcome-email.js
|
|
120
|
-
const { defineListener } = require('webspresso');
|
|
121
|
-
|
|
122
|
-
module.exports = defineListener('user.created', async (user, meta) => {
|
|
123
|
-
console.log(`Sending welcome email to ${user.email}`);
|
|
124
|
-
});
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Or multiple listeners in a single file:
|
|
128
|
-
|
|
129
|
-
```javascript
|
|
130
|
-
// listeners/audit-events.js
|
|
131
|
-
module.exports = [
|
|
132
|
-
{
|
|
133
|
-
event: 'order.*',
|
|
134
|
-
priority: 50,
|
|
135
|
-
handle: async (data, meta) => {
|
|
136
|
-
console.log(`Audit: ${meta.event}`, data);
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
];
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## 4. Django-Style Signals
|
|
145
|
-
|
|
146
|
-
For decoupled domain events with strict sender boundaries:
|
|
147
|
-
|
|
148
|
-
```javascript
|
|
149
|
-
const { defineSignal } = require('webspresso');
|
|
150
|
-
|
|
151
|
-
// Define signal
|
|
152
|
-
const invoicePaid = defineSignal('invoice_paid');
|
|
153
|
-
|
|
154
|
-
// Connect receiver with sender filter
|
|
155
|
-
invoicePaid.connect(
|
|
156
|
-
(invoice) => {
|
|
157
|
-
console.log(`Invoice ${invoice.id} processed for subscription`);
|
|
158
|
-
},
|
|
159
|
-
{ sender: 'Subscription' }
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// Trigger signal
|
|
163
|
-
invoicePaid.send({ id: 'INV-100' }, { sender: 'Subscription' });
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
## 5. Background Queue Bridging (`listenAndQueue`)
|
|
169
|
-
|
|
170
|
-
Connect application events directly to background jobs without manual boilerplate:
|
|
171
|
-
|
|
172
|
-
```javascript
|
|
173
|
-
// In plugin or server setup:
|
|
174
|
-
app.events.listenAndQueue('order.created', 'send-order-confirmation', {
|
|
175
|
-
delay: 2000,
|
|
176
|
-
attempts: 3,
|
|
177
|
-
});
|
|
178
|
-
```
|
|
179
|
-
When `order.created` is emitted, Webspresso automatically calls `req.queue.dispatch('send-order-confirmation', order, options)`.
|
package/.agents/FLAGS.md
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# Feature Flags & Canary Traffic Splitter (`core/flags`)
|
|
2
|
-
|
|
3
|
-
Webspresso features an enterprise-grade, zero-dependency **Feature Flag & Canary Traffic Splitting Engine** in `core/flags/`. It enables gradual rollouts, multivariate A/B testing, user/role/domain targeting, and runtime overrides across Nunjucks SSR templates, API endpoints (`defineApi`), service layers, and Edge/Hono adapters.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 🎯 Key Capabilities
|
|
8
|
-
|
|
9
|
-
1. **Sub-Microsecond Deterministic Bucketing**:
|
|
10
|
-
- FNV-1a 32-bit hashing algorithm (`getBucket`) maps user identifiers to deterministic integer buckets `0 .. 99` across distributed nodes without network calls.
|
|
11
|
-
2. **Flexible Targeting Rules**:
|
|
12
|
-
- **Boolean Toggles**: `enabled: true | false`.
|
|
13
|
-
- **Canary Percentage Rollout**: `rollout: 10` (10% of traffic, consistent per user).
|
|
14
|
-
- **Targeted Whitelists**: `users: ['user_123', 'admin@corp.com']`.
|
|
15
|
-
- **Role-Based Targeting**: `roles: ['admin', 'beta_tester']`.
|
|
16
|
-
- **Email Domain Targeting**: `emailDomains: ['acme.com']`.
|
|
17
|
-
- **Multivariate A/B Variants**: `variants: [{ name: 'blue_button', weight: 50 }, { name: 'green_button', weight: 50 }]`.
|
|
18
|
-
- **Custom Predicates**: `condition: (ctx) => ctx.plan === 'enterprise'`.
|
|
19
|
-
3. **Multi-Layer Integration**:
|
|
20
|
-
- **Express / Hono Middleware**: Automatically injects `req.flags`, `req.hasFeature()`, and `req.getVariant()`.
|
|
21
|
-
- **Nunjucks SSR Templates**: Access via `{{ fsy.hasFeature('new_ui') }}` and `{{ fsy.flag('theme') }}`.
|
|
22
|
-
- **Declarative API Route Guard**: `defineApi({ flag: 'beta_feature' })` returns HTTP 404 (or configured status) if disabled.
|
|
23
|
-
- **Environment Variable Overrides**: Instant runtime overrides via `FEATURE_<FLAG_NAME>=true|false`.
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## 🚀 Quick Start
|
|
28
|
-
|
|
29
|
-
### 1. Application Configuration
|
|
30
|
-
|
|
31
|
-
```javascript
|
|
32
|
-
const { createApp } = require('webspresso');
|
|
33
|
-
|
|
34
|
-
const app = createApp({
|
|
35
|
-
flags: {
|
|
36
|
-
// 1. Simple toggle
|
|
37
|
-
dark_mode: true,
|
|
38
|
-
|
|
39
|
-
// 2. 20% Canary rollout
|
|
40
|
-
new_checkout: {
|
|
41
|
-
rollout: 20,
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
// 3. User & Role targeting
|
|
45
|
-
ai_assistant: {
|
|
46
|
-
roles: ['pro', 'admin'],
|
|
47
|
-
users: ['vip_client_1'],
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
// 4. Multivariate A/B Test
|
|
51
|
-
pricing_page_test: {
|
|
52
|
-
variants: [
|
|
53
|
-
{ name: 'control', weight: 50 },
|
|
54
|
-
{ name: 'discount_banner', weight: 50 },
|
|
55
|
-
],
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### 2. Nunjucks SSR Templates (`pages/*.njk`)
|
|
62
|
-
|
|
63
|
-
```nunjucks
|
|
64
|
-
{% if fsy.hasFeature('new_checkout') %}
|
|
65
|
-
{% include "components/new-checkout.njk" %}
|
|
66
|
-
{% else %}
|
|
67
|
-
{% include "components/legacy-checkout.njk" %}
|
|
68
|
-
{% endif %}
|
|
69
|
-
|
|
70
|
-
{% if fsy.flag('pricing_page_test') == 'discount_banner' %}
|
|
71
|
-
<div class="banner">Special 20% Discount Applied!</div>
|
|
72
|
-
{% endif %}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### 3. File-Based API Endpoints (`pages/api/...`)
|
|
76
|
-
|
|
77
|
-
```javascript
|
|
78
|
-
const { defineApi } = require('webspresso');
|
|
79
|
-
|
|
80
|
-
module.exports = defineApi({
|
|
81
|
-
// Automatically blocks request with HTTP 404 if flag is disabled
|
|
82
|
-
flag: 'ai_assistant',
|
|
83
|
-
handler: async (req, res, ctx) => {
|
|
84
|
-
// Also accessible inside ctx or req
|
|
85
|
-
const isVip = ctx.hasFeature('vip_access');
|
|
86
|
-
return res.json({ success: true, isVip });
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### 4. Route Guard Middleware
|
|
92
|
-
|
|
93
|
-
```javascript
|
|
94
|
-
const { createFeatureFlagManager } = require('webspresso');
|
|
95
|
-
|
|
96
|
-
const flags = createFeatureFlagManager({
|
|
97
|
-
flags: { secret_admin: { roles: ['admin'] } },
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Use as standalone route middleware
|
|
101
|
-
app.get('/admin/secret', flags.guard('secret_admin', { statusCode: 403 }), (req, res) => {
|
|
102
|
-
res.send('Welcome, Admin!');
|
|
103
|
-
});
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
## ⚙️ Environment Overrides
|
|
109
|
-
|
|
110
|
-
Flags can be overridden at runtime without code deployment or restart:
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
# Enable flag
|
|
114
|
-
FEATURE_NEW_CHECKOUT=true npm start
|
|
115
|
-
|
|
116
|
-
# Disable flag
|
|
117
|
-
FEATURE_NEW_CHECKOUT=false npm start
|
|
118
|
-
```
|
package/.agents/HEALTH.md
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# Cloud & Kubernetes Health & Readiness Probes (`core/health`)
|
|
2
|
-
|
|
3
|
-
Webspresso includes a zero-dependency, Docker & Kubernetes-native health inspection system. It provides standard liveness (`/healthz`), readiness (`/readyz`), and combined diagnostics endpoints with automatic database pings, background worker queue checks, event loop delay metrics, and instant graceful shutdown draining integration.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 🎯 Key Capabilities
|
|
8
|
-
|
|
9
|
-
1. **Kubernetes Liveness Probe (`/healthz`)**:
|
|
10
|
-
- Confirms process vitality and responsiveness.
|
|
11
|
-
- Gathers memory footprint (`rss`, `heapUsed`, `heapTotal` formatted and raw bytes), process uptime, PID, platform, and event loop lag statistics (`meanMs`, `maxMs`, `p99Ms`).
|
|
12
|
-
- If event loop delay exceeds threshold (`eventLoopThresholdMs`), flags status as `degraded`.
|
|
13
|
-
|
|
14
|
-
2. **Kubernetes Readiness Probe (`/readyz`)**:
|
|
15
|
-
- Confirms the container is fully prepared to handle live client traffic.
|
|
16
|
-
- **Database Connectivity**: Pings active Knex instance (`SELECT 1`) with timeout protection.
|
|
17
|
-
- **Queue Readiness**: Probes worker queue instance status.
|
|
18
|
-
- **Custom Probes**: Executes user-registered async checks concurrently (`checks: { redis, stripe }`).
|
|
19
|
-
- Returns `HTTP 200` (`{ ready: true, status: 'ready' }`) when all components are healthy.
|
|
20
|
-
- Returns `HTTP 503` (`{ ready: false, status: 'unhealthy', checks: ... }`) if any component fails.
|
|
21
|
-
|
|
22
|
-
3. **Graceful Draining Integration (`ShutdownManager`)**:
|
|
23
|
-
- As soon as the server enters shutdown draining (`app.close()` or `SIGTERM`/`SIGINT`), `/readyz` immediately returns `HTTP 503` (`{ ready: false, status: 'draining' }`).
|
|
24
|
-
- Cloud load balancers (AWS ALB, K8s Ingress, Google Cloud, Cloudflare) immediately stop routing new traffic to this instance, while existing active requests drain cleanly without dropping a single connection.
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## 🚀 Configuration Examples
|
|
29
|
-
|
|
30
|
-
### 1. Simple Enablement
|
|
31
|
-
|
|
32
|
-
```javascript
|
|
33
|
-
const { createApp } = require('webspresso');
|
|
34
|
-
|
|
35
|
-
const app = createApp({
|
|
36
|
-
pagesDir: 'pages',
|
|
37
|
-
// Enables /healthz (liveness), /readyz (readiness), and /health (combined)
|
|
38
|
-
healthCheck: true,
|
|
39
|
-
});
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Shorthand custom path:
|
|
43
|
-
```javascript
|
|
44
|
-
const app = createApp({
|
|
45
|
-
pagesDir: 'pages',
|
|
46
|
-
healthCheck: '/api/status', // Mounts combined status on /api/status
|
|
47
|
-
});
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### 2. Full Production Configuration
|
|
51
|
-
|
|
52
|
-
```javascript
|
|
53
|
-
const app = createApp({
|
|
54
|
-
pagesDir: 'pages',
|
|
55
|
-
db: myDatabaseInstance,
|
|
56
|
-
queue: myQueueInstance,
|
|
57
|
-
healthCheck: {
|
|
58
|
-
enabled: true,
|
|
59
|
-
path: '/healthz', // Liveness probe URL
|
|
60
|
-
readyPath: '/readyz', // Readiness probe URL
|
|
61
|
-
combinedPath: '/health', // Diagnostic summary URL
|
|
62
|
-
db: true, // Automatically probe database (SELECT 1)
|
|
63
|
-
queue: true, // Automatically probe worker queue
|
|
64
|
-
eventLoopLag: true, // Monitor event loop delay
|
|
65
|
-
eventLoopThresholdMs: 300, // Lag threshold in ms
|
|
66
|
-
timeoutMs: 2500, // Probe timeout before marking down
|
|
67
|
-
checks: { // Custom dependency probes
|
|
68
|
-
redis: async () => redisClient.ping(),
|
|
69
|
-
stripe: async () => checkStripeHealth(),
|
|
70
|
-
},
|
|
71
|
-
headers: {
|
|
72
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
|
73
|
-
'X-Content-Type-Options': 'nosniff',
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## ☸️ Kubernetes Pod Spec Example
|
|
82
|
-
|
|
83
|
-
```yaml
|
|
84
|
-
livenessProbe:
|
|
85
|
-
httpGet:
|
|
86
|
-
path: /healthz
|
|
87
|
-
port: 3000
|
|
88
|
-
initialDelaySeconds: 5
|
|
89
|
-
periodSeconds: 10
|
|
90
|
-
timeoutSeconds: 3
|
|
91
|
-
failureThreshold: 3
|
|
92
|
-
|
|
93
|
-
readinessProbe:
|
|
94
|
-
httpGet:
|
|
95
|
-
path: /readyz
|
|
96
|
-
port: 3000
|
|
97
|
-
initialDelaySeconds: 2
|
|
98
|
-
periodSeconds: 5
|
|
99
|
-
timeoutSeconds: 2
|
|
100
|
-
failureThreshold: 2
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## 🔍 Programmatic Inspection
|
|
106
|
-
|
|
107
|
-
Access health state directly in code or CLI scripts:
|
|
108
|
-
|
|
109
|
-
```javascript
|
|
110
|
-
// Get complete diagnostic payload
|
|
111
|
-
const status = await app.getHealthStatus();
|
|
112
|
-
console.log('Liveness:', status.liveness);
|
|
113
|
-
console.log('Readiness:', status.readiness);
|
|
114
|
-
|
|
115
|
-
// Or via healthManager
|
|
116
|
-
const liveness = await app.healthManager.getLiveness();
|
|
117
|
-
const readiness = await app.healthManager.getReadiness();
|
|
118
|
-
```
|