webspresso 0.0.107 → 0.0.108

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 (91) hide show
  1. package/.agents/AGENTS.md +4 -1
  2. package/.agents/CACHE.md +1 -0
  3. package/.agents/CONTEXT.md +152 -0
  4. package/.agents/ORM.md +74 -0
  5. package/.agents/SHUTDOWN.md +8 -1
  6. package/.agents/skills/webspresso-framework/SKILL.md +2 -1
  7. package/.agents/skills/webspresso-orm-and-database/SKILL.md +9 -0
  8. package/.agents/skills/webspresso-routing-and-api/SKILL.md +18 -2
  9. package/.agents/skills/webspresso-services/SKILL.md +36 -3
  10. package/AGENTS.md +6 -3
  11. package/core/app.js +7 -5
  12. package/core/applySchema.js +3 -72
  13. package/core/cache/lru.js +10 -0
  14. package/core/cache/tagged-lru.js +4 -0
  15. package/core/compileSchema.js +3 -68
  16. package/core/compression/compressible.js +53 -6
  17. package/core/compression/negotiate.js +31 -12
  18. package/core/context/app-context.js +201 -0
  19. package/core/context/index.js +55 -0
  20. package/core/context/request.js +241 -0
  21. package/core/events/dispatcher.js +144 -15
  22. package/core/events/signal.js +9 -0
  23. package/core/health/probes.js +12 -2
  24. package/core/http/index.js +10 -1
  25. package/core/http/pipeline.js +519 -368
  26. package/core/index.js +24 -1
  27. package/core/orm/eager-loader.js +90 -55
  28. package/core/orm/index.js +3 -39
  29. package/core/orm/model.js +6 -1
  30. package/core/orm/schema-helpers.js +59 -0
  31. package/core/orm/transaction.js +109 -14
  32. package/core/plugins/index.js +8 -0
  33. package/core/plugins/plugin-manager.js +1098 -0
  34. package/core/queue/manager.js +55 -27
  35. package/core/rate-limit/stores/memory.js +23 -10
  36. package/core/resilience/circuit-breaker.js +34 -14
  37. package/core/schema-cache.js +2 -64
  38. package/core/services/builtins/auth.js +273 -0
  39. package/core/services/builtins/exchange.js +229 -0
  40. package/core/services/builtins/file-manager.js +178 -0
  41. package/core/services/builtins/mail.js +134 -0
  42. package/core/services/builtins/media.js +196 -0
  43. package/core/services/builtins/system.js +219 -0
  44. package/core/services/discovery.js +133 -0
  45. package/core/services/executor.js +629 -0
  46. package/core/services/index.js +110 -5
  47. package/core/services/loader.js +161 -0
  48. package/core/services/memoize.js +408 -0
  49. package/core/services/registry.js +731 -0
  50. package/core/services/validator.js +424 -0
  51. package/core/shutdown/shutdown-manager.js +146 -85
  52. package/core/ssr/stream.js +109 -16
  53. package/core/stream/text-stream.js +45 -4
  54. package/core/utils/index.js +110 -0
  55. package/core/utils/timeout.js +4 -0
  56. package/core/validation/apply.js +74 -0
  57. package/core/validation/cache.js +66 -0
  58. package/core/validation/compiler.js +77 -0
  59. package/core/validation/index.js +22 -1
  60. package/index.d.ts +77 -4
  61. package/index.js +64 -20
  62. package/package.json +1 -1
  63. package/src/app-context.js +4 -196
  64. package/src/dev/typegen.js +1 -1
  65. package/src/plugin-manager.js +5 -1086
  66. package/src/server.js +4 -1
  67. package/src/services/builtins/auth.js +2 -273
  68. package/src/services/builtins/exchange.js +2 -229
  69. package/src/services/builtins/file-manager.js +2 -178
  70. package/src/services/builtins/mail.js +2 -134
  71. package/src/services/builtins/media.js +2 -196
  72. package/src/services/builtins/system.js +2 -219
  73. package/src/services/discovery.js +2 -133
  74. package/src/services/executor.js +2 -575
  75. package/src/services/index.js +4 -109
  76. package/src/services/loader.js +1 -119
  77. package/src/services/memoize.js +2 -408
  78. package/src/services/registry.js +1 -711
  79. package/src/services/validator.js +2 -424
  80. package/core/utils/array.js +0 -12
  81. package/core/utils/base64url.js +0 -8
  82. package/core/utils/ensure-leading-slash.js +0 -6
  83. package/core/utils/escape-html.js +0 -7
  84. package/core/utils/escape-regexp.js +0 -7
  85. package/core/utils/get-client-ip.js +0 -6
  86. package/core/utils/hmac.js +0 -6
  87. package/core/utils/random.js +0 -9
  88. package/core/utils/safe-compare.js +0 -3
  89. package/core/utils/sha256.js +0 -6
  90. package/core/utils/strip-trailing-slash.js +0 -6
  91. package/core/utils/walk.js +0 -7
package/.agents/AGENTS.md CHANGED
@@ -25,6 +25,7 @@ When tasked with a feature or bugfix, **immediately consult the corresponding to
25
25
  | **Cloud Migration Jobs (Zero-Downtime DB Deploy)** | [`.agents/MIGRATION_JOBS.md`](MIGRATION_JOBS.md) | `bin/commands/add-migration-job.js`, `core/orm/migrate-cloud.js` |
26
26
  | **Rate Limiting Engine (Zero-Dependency & Redis)** | [`.agents/RATE_LIMIT.md`](RATE_LIMIT.md) | `core/rate-limit/` |
27
27
  | **Feature Flags & Canary Splitter (`core/flags`)** | [`.agents/FLAGS.md`](FLAGS.md) | `core/flags/` |
28
+ | **Ambient Context & Composable Hooks (`core/context`)** | [`.agents/CONTEXT.md`](CONTEXT.md) | `core/context/` |
28
29
  | **API Signatures & Types Verification** | [`index.d.ts`](../index.d.ts) | **Canonical Ground Truth** |
29
30
 
30
31
  ---
@@ -51,7 +52,7 @@ This `.agents/` customization root is organized into modular topic guides:
51
52
  5. **[Graceful Shutdown & Lifecycle](.agents/SHUTDOWN.md)** — `ShutdownManager`, `NodeHttpAdapter`, connection draining, force close mode, and reverse-order plugin disposer cleanup.
52
53
  6. **[HTTP Response Compression](.agents/COMPRESSION.md)** — Native streaming zlib compression (`server.compression`), Brotli/Gzip/Deflate negotiation, threshold handling, and `res.compress(false)` route opt-out.
53
54
  7. **[Dual Authentication System](.agents/AUTH.md)** — Stateful Session + Cookie & Stateless HS256 JWT + Refresh Token Rotation, `req.auth` helpers, and `middleware: ['jwt']` guard integrations.
54
- 8. **[ORM & Database Layer](.agents/ORM.md)** — Model definitions with `defineModel`, `zdb` schemas, Repositories API, Query Caching, Relations, Soft Delete, Scopes & Migrations.
55
+ 8. **[ORM & Database Layer](.agents/ORM.md)** — Model definitions with `defineModel`, `zdb` schemas, `zdb` vs `z` single source of truth, Repositories API, Query Caching, Relations, Soft Delete, Scopes & Migrations.
55
56
  9. **[Plugin Ecosystem Guide](.agents/PLUGINS.md)** — Lifecycle hooks (`register`, `onRoutesReady`), CSP headers, and detailed reference for all 20 built-in official plugins (including `webhooksPlugin`, `apiKeyPlugin`, `xlsxPlugin`, `csvPlugin`, `fileManagerPlugin`, `realtimePlugin`, and `basicAuthPlugin`).
56
57
  10. **[Realtime Layer & Adapters](.agents/REALTIME.md)** — Framework-agnostic realtime layer (`core/realtime`), generic WebSocket, SSE, and Socket.IO adapters, subscription identity, backoff reconnect, and auth lifecycle.
57
58
  11. **[Email Plugin Guide](.agents/EMAIL.md)** — MJML template compilation, Nodemailer transport, DB delivery logs, Auth Email bridge, and Admin UI.
@@ -75,6 +76,7 @@ This `.agents/` customization root is organized into modular topic guides:
75
76
  29. **[Cloud Migration Jobs & Zero-Downtime Database Deploy](.agents/MIGRATION_JOBS.md)** — Dedicated pre-deploy migration jobs for Cloud Run, AWS ECS, and Kubernetes (`webspresso add:migration-job`), connection retries, and stale lock auto-healing (`webspresso db:migrate --safe`).
76
77
  30. **[Native Zero-Dependency Rate Limiting Engine](.agents/RATE_LIMIT.md)** — Framework-agnostic rate limiter (`core/rate-limit`), sliding window in-memory & Redis stores, RFC 6585 standard headers (`RateLimit-*`, `Retry-After`), Express & Hono dual support, and `rateLimit({ window, limit })`.
77
78
  31. **[Feature Flags & Canary Traffic Splitter](.agents/FLAGS.md)** — Zero-dependency feature flag and canary rollout engine (`core/flags`), deterministic FNV-1a bucketing, targeting rules (users, roles, email domains), A/B variants, SSR template helpers (`fsy.hasFeature`), and declarative API guards (`defineApi({ flag })`).
79
+ 32. **[Ambient Context & Composable Hooks](.agents/CONTEXT.md)** — Zero-dependency execution context powered by `AsyncLocalStorage` (`core/context`), eliminating prop drilling across loaders, services, models, and workers with `useContext`, `useDb`, `useAuth`, `useUser`, `useService`, `useLogger`, `useReq`, `useRes`, `useParams`, `useQuery`, `useTransaction`, and `withContext`.
78
80
 
79
81
  ---
80
82
 
@@ -91,3 +93,4 @@ This `.agents/` customization root is organized into modular topic guides:
91
93
  - **Zero Regression Policy**: Always run Vitest tests (`npm test` or `npx vitest run`) before declaring any task complete.
92
94
  - **Strict Verification**: Inspect authoritative source files for function signatures and schema definitions before writing consuming code.
93
95
  - **Log & Traceback Inspection**: Base all bug fixes strictly on exact runtime error log tracebacks and empirical test output.
96
+ - **`zdb` vs `z` Single Source of Truth (NO `schema: z.object` in Models)**: NEVER wrap model schemas in `z.object({...})`. Pass a clean plain object `schema: { ... }` or functional schema `schema: ({ zdb, z }) => ({ ... })`. ALWAYS use `zdb.*` for persistent database columns in `models/*.js` (`zdb.id()`, `zdb.string()`, `zdb.integer()`, `zdb.email()`, etc.) to generate migrations, query filters, and Admin UI controls. Use pure `z.*` ONLY for in-memory request/service payloads (`pages/api/...`, `services/...`). Access raw Zod primitives in models via `zdb.z` without importing `zod`.
package/.agents/CACHE.md CHANGED
@@ -10,6 +10,7 @@ Webspresso includes a zero-dependency, memory-bounded, leak-free in-memory cache
10
10
  2. **Dual Capacity Boundaries**:
11
11
  - **`maxEntries`**: Restricts the maximum number of items stored.
12
12
  - **`maxBytes` / `maxMemoryMb`**: Enforces strict memory budgets using `approximateByteSize` to prevent process Out-Of-Memory (OOM) crashes under heavy or large payload loads.
13
+ - **Zero-Overhead Fast Path**: When `maxBytes` is 0 (unlimited byte budget, count-only LRU), deep recursive byte size calculations are automatically bypassed (`0` byte allocation overhead), maximizing throughput on large query and JSON payloads unless explicitly requested via `trackBytes: true`.
13
14
  3. **Symmetrical Tag Cleanup (Zero Leaks)**: When an item is evicted via LRU, expired via TTL, or deleted, all associated tag index sets are automatically pruned. Empty tag sets are deleted immediately, preventing the classic "dangling Set memory leak".
14
15
  4. **Stampede / Thundering Herd Protection**: `getOrSet(key, asyncFactory)` coalesces concurrent requests for the same missing key, invoking the expensive calculation or DB query exactly once.
15
16
  5. **Universal Integration**:
@@ -0,0 +1,152 @@
1
+ # Webspresso Ambient Request & Execution Context (`core/context`)
2
+
3
+ [← Back to AGENTS.md](AGENTS.md)
4
+
5
+ Webspresso features an enterprise-grade, zero-dependency **Ambient Request & Execution Context Engine** powered by Node.js `AsyncLocalStorage`. It enables composable hooks (`useContext`, `useDb`, `useAuth`, `useUser`, `useService`, `useLogger`, `useReq`, `useRes`, `useParams`, `useQuery`, `useBreaker`, `useTransaction`, `withContext`) across API handlers, SSR companion loaders, domain services, ORM models, and background workers without parameter threading or prop drilling.
6
+
7
+ ---
8
+
9
+ ## 🎯 Quick Reference: Composable Hooks Matrix
10
+
11
+ | Hook | Return Type | Description |
12
+ | :--- | :--- | :--- |
13
+ | `useContext()` | `RequestContextStore` | Returns the entire active context object; throws if called outside context. |
14
+ | `getContext()` | `RequestContextStore \| null` | Safe accessor returning active context or `null` without throwing. |
15
+ | `useDb()` | `Database \| null` | Accesses the active database/ORM instance or active transaction instance. |
16
+ | `useAuth()` / `useUser()` | `User \| null` | Accesses the authenticated user or principal object. |
17
+ | `useReq()` | `Request \| null` | Accesses the ambient HTTP request object (Express or Hono). |
18
+ | `useRes()` | `Response \| null` | Accesses the ambient HTTP response object. |
19
+ | `useParams()` | `Record<string, string>` | Accesses URL route parameters (e.g. `{ id: '123' }`). |
20
+ | `useQuery()` | `Record<string, any>` | Accesses URL query parameters (e.g. `{ filter: 'active' }`). |
21
+ | `useService(name, input, opts)` | `Promise<any>` | Dispatches a service call seamlessly using active ambient dependencies. |
22
+ | `useLogger()` | `Logger` | Accesses the ambient request/child logger or falls back to `console`. |
23
+ | `useQueue()` | `Queue \| null` | Accesses the ambient background job queue. |
24
+ | `useCache()` | `Cache \| null` | Accesses the ambient LRU cache engine. |
25
+ | `useBreaker(name, opts)` | `CircuitBreaker \| null` | Retrieves or creates a Circuit Breaker from registry or context. |
26
+ | `useTransaction(callback)` | `Promise<any>` | Initiates a managed transaction on `useDb()`, binding `txDb` to ambient hooks. |
27
+ | `withContext(overrides, fn)` | `any` | Executes `fn` with shallow-merged overrides on top of active context. |
28
+
29
+ ---
30
+
31
+ ## 1. Zero-Prop-Drilling in Domain Services
32
+
33
+ In complex applications, threading database connections or user objects across nested helper functions leads to tight coupling. With Webspresso hooks, helper utilities access ambient resources directly:
34
+
35
+ ```javascript
36
+ // services/billing/refund.js
37
+ const { defineService, useDb, useAuth, useLogger, useService } = require('webspresso');
38
+
39
+ async function auditRefund(orderId, amount) {
40
+ const logger = useLogger();
41
+ const user = useAuth();
42
+ logger.info('Processing refund audit', { orderId, user: user?.id });
43
+
44
+ await useService('audit.log', {
45
+ action: 'order.refund',
46
+ actorId: user?.id,
47
+ orderId,
48
+ amount,
49
+ });
50
+ }
51
+
52
+ module.exports = defineService({
53
+ transaction: true,
54
+ handler: async ({ orderId, amount }) => {
55
+ const db = useDb(); // Inside transaction: true, useDb() automatically returns transactional db!
56
+ const orderRepo = db.getRepository('Order');
57
+
58
+ const order = await orderRepo.findById(orderId);
59
+ if (!order) {
60
+ throw new NotFoundError('Order not found');
61
+ }
62
+
63
+ await orderRepo.update(order.id, { status: 'refunded' });
64
+ await auditRefund(order.id, amount);
65
+
66
+ return { success: true };
67
+ },
68
+ });
69
+ ```
70
+
71
+ ---
72
+
73
+ ## 2. Using Hooks in API Routes (`pages/api/...`)
74
+
75
+ ```javascript
76
+ // pages/api/profile.get.js
77
+ const { defineApi, useDb, useUser, useQuery } = require('webspresso');
78
+
79
+ module.exports = defineApi({
80
+ middleware: ['auth'],
81
+ handler: async (req, res) => {
82
+ const user = useUser();
83
+ const query = useQuery();
84
+ const db = useDb();
85
+
86
+ const profile = await db.getRepository('UserProfile').findOne({ userId: user.id });
87
+ return res.json({ profile, query });
88
+ },
89
+ });
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 3. Using Hooks in SSR Page Loaders (`pages/...`)
95
+
96
+ ```javascript
97
+ // pages/dashboard.js
98
+ const { definePage, useDb, useUser, useService } = require('webspresso');
99
+
100
+ module.exports = definePage({
101
+ middleware: ['auth'],
102
+ load: async () => {
103
+ const user = useUser();
104
+ const db = useDb();
105
+ const metrics = await useService('dashboard.metrics', { userId: user.id });
106
+
107
+ return {
108
+ user,
109
+ metrics,
110
+ };
111
+ },
112
+ });
113
+ ```
114
+
115
+ ---
116
+
117
+ ## 4. Managed Transactions (`useTransaction`)
118
+
119
+ When performing multi-step transactional operations outside declarative `transaction: true` services:
120
+
121
+ ```javascript
122
+ const { useTransaction, useDb } = require('webspresso');
123
+
124
+ const result = await useTransaction(async (trx) => {
125
+ // Inside this block, useDb() automatically resolves to the transactional repository runner
126
+ const db = useDb();
127
+ await db.getRepository('Account').decrement('balance', 100);
128
+ await db.getRepository('Ledger').create({ amount: 100, type: 'debit' });
129
+ return { committed: true };
130
+ });
131
+ ```
132
+
133
+ ---
134
+
135
+ ## 5. Background Jobs & CLI Commands Integration
136
+
137
+ When executing background jobs (`jobs/`) or CLI tasks, wrap executions with `runWithContext` or `withContext` to seed ambient dependencies:
138
+
139
+ ```javascript
140
+ const { runWithContext, useDb, useService } = require('webspresso');
141
+
142
+ async function processWorkerJob(job, app) {
143
+ return await runWithContext({
144
+ db: app.db,
145
+ queue: app.queue,
146
+ logger: app.logger,
147
+ }, async () => {
148
+ // Hooks are fully operational inside background job execution!
149
+ return await useService('emails.sendWelcome', job.data);
150
+ });
151
+ }
152
+ ```
package/.agents/ORM.md CHANGED
@@ -102,6 +102,80 @@ Instead of manually defining boilerplate timestamp and soft delete columns in `s
102
102
 
103
103
  *Note: All `zdb` types support chainable `.config({ sortable: boolean })` or options object `{ sortable: boolean }`.*
104
104
 
105
+ ### 1.1 `zdb` vs `z`: Understanding the Distinction (Single Source of Truth)
106
+
107
+ It is common for developers new to Webspresso to ask: *"Why does `zdb` exist alongside `z` (Zod)? What is the difference between `zdb.string()` and `z.string()`?"*
108
+
109
+ #### The Architectural Difference: Decorator over Validator
110
+
111
+ | Dimension | `z.string()` (Pure Zod Validator) | `zdb.string()` (ORM Column Descriptor + Validator) |
112
+ | :--- | :--- | :--- |
113
+ | **Output Type** | Pure validation rule (`ZodString`) | Database column metadata + Zod validation (`SchemaBuilder`) |
114
+ | **Database & SQL Knowledge** | **None.** Has no awareness of tables, SQL dialects, or migrations. | **Full.** Knows column type (`VARCHAR(255)`), max length, nullability, uniqueness, defaults. |
115
+ | **Auto Migrations (`db:makemigrations`)** | Invisible. Ignored by `diffSchema`. | Generates Knex migrations (`table.string('name', 255)`). |
116
+ | **Admin Panel SPA** | Cannot configure UI form controls. | Auto-renders `<input type="text">`, configures grid sortability, labels, placeholders. |
117
+ | **REST & Query Filters** | Not indexed in column whitelist. | Whitelisted in `model.columns` for safe parameter filtering (`?name[like]=webspresso`). |
118
+ | **JSON Auto-Hydration** | Manual JSON parsing needed. | Auto-serializes on insert/update and auto-deserializes on query. |
119
+
120
+ #### Why `zdb` Exists: Eliminating 3x Schema Duplication
121
+ In traditional Node.js setups, you must declare column definitions in three separate places:
122
+ 1. In a Knex/SQL migration (`table.string('title', 100).notNullable()`)
123
+ 2. In an ORM model/entity definition (`title: { type: DataTypes.STRING, ... }`)
124
+ 3. In a validation schema (`title: z.string().min(1).max(100)`)
125
+
126
+ Webspresso uses `zdb` as a **Single Source of Truth**: by writing `title: zdb.string({ maxLength: 100 }).min(1)`, the ORM automatically handles validation, database column generation, query filtering, and admin UI rendering from a single declaration.
127
+
128
+ #### 💡 The Golden Rule: When to Use Which
129
+ * 🗄️ **Database Models (`models/*.js`):** **ALWAYS use `zdb.*`** (`zdb.id()`, `zdb.string()`, `zdb.integer()`, `zdb.email()`, etc.).
130
+ * 🌐 **API Endpoints (`pages/api/...`) & Services (`services/...`):** **ALWAYS use `z.*`** (`z.object({ email: z.string().email() })`). Here you are validating incoming JSON payloads in RAM, not defining persistent database tables.
131
+
132
+ #### Accessing Raw Zod Primitives in Models (`zdb.z` or Functional Schema)
133
+ When you need raw Zod features in a model (such as `z.enum()`, `z.union()`, `z.literal()`), you **never need to `require('zod')`**:
134
+ 1. **Via Functional Schema (Recommended):**
135
+ ```javascript
136
+ module.exports = defineModel({
137
+ name: 'Article',
138
+ table: 'articles',
139
+ schema: ({ zdb, z }) => ({
140
+ id: zdb.id(),
141
+ title: zdb.string(),
142
+ status: z.enum(['draft', 'published', 'archived']),
143
+ }),
144
+ });
145
+ ```
146
+ 2. **Via `zdb.z`:**
147
+ ```javascript
148
+ const { defineModel, zdb } = require('webspresso');
149
+
150
+ module.exports = defineModel({
151
+ name: 'Article',
152
+ table: 'articles',
153
+ schema: {
154
+ id: zdb.id(),
155
+ title: zdb.string(),
156
+ status: zdb.z.enum(['draft', 'published', 'archived']),
157
+ },
158
+ });
159
+ ```
160
+
161
+ #### ⚠️ Anti-Pattern: Never Wrap Model Schemas in `z.object({...})`
162
+ ```javascript
163
+ // ❌ WRONG (Anti-Pattern: forces require('zod'), causes redundant schema wrapping)
164
+ const { z } = require('zod');
165
+ defineModel({
166
+ schema: z.object({
167
+ name: zdb.string(),
168
+ }),
169
+ });
170
+
171
+ // ✅ CORRECT (Webspresso standard: plain object or functional schema)
172
+ defineModel({
173
+ schema: {
174
+ name: zdb.string(),
175
+ },
176
+ });
177
+ ```
178
+
105
179
  ---
106
180
 
107
181
  ## 2. Repository API
@@ -23,6 +23,9 @@ const { app } = createApp({
23
23
  mode: 'graceful', // 'graceful' | 'force'
24
24
  timeout: 10_000, // Max time in ms to wait for active requests (Default: 10000)
25
25
  signals: ['SIGINT', 'SIGTERM'],
26
+ parallel: false, // Set true to execute async disposers & hooks concurrently (Default: false)
27
+ parallelDisposers: false, // Fine-grained control over plugin disposers
28
+ parallelHooks: false, // Fine-grained control over app.onShutdown hooks
26
29
  },
27
30
  },
28
31
  });
@@ -32,6 +35,10 @@ const { app } = createApp({
32
35
  - **`graceful` (Default)**: Rejects new connections, drains active in-flight requests, and closes idle/keep-alive connections.
33
36
  - **`force`**: Immediately destroys all active and idle sockets (`forceClose`). Recommended for rapid development/watch server reloads to avoid port lock.
34
37
 
38
+ ### Concurrency & Performance:
39
+ - **Synchronous Fast-Path**: Synchronous disposers and shutdown hooks execute immediately without creating timers or `Promise.race` wrappers, eliminating microtask delays and timer allocations during exit.
40
+ - **Parallel Mode (`parallel: true`)**: When enabled, asynchronous plugin disposers and hooks are initiated in reverse registration order and awaited concurrently via `Promise.allSettled`, drastically reducing shutdown latency for apps with multiple independent resources (DB, Redis, SQS, WebSockets).
41
+
35
42
  ---
36
43
 
37
44
  ## 3. Application Lifecycle Hooks
@@ -64,4 +71,4 @@ const customPlugin = {
64
71
  },
65
72
  };
66
73
  ```
67
- During shutdown, plugin disposers are executed sequentially in **reverse dependency order**.
74
+ During shutdown, plugin disposers are executed in **reverse dependency order** (`reverseDisposers`). When `parallel` is enabled, their initiation order is strictly preserved while awaiting resolution concurrently.
@@ -82,7 +82,8 @@ npx webspresso add tailwind --upgrade
82
82
  ## 🏗️ Core Architecture Overview
83
83
 
84
84
  - **`pages/` & `pages/api/`**: File-based SSR templates (`.njk`), companion data loaders (`.js`), and method-suffixed API endpoints (`.get.js`, `.post.js`).
85
- - **`services/`**: Auto-discovered domain services mapping to dot-separated names (`'user.create'`).
85
+ - **`services/`**: Auto-discovered domain services mapping to dot-separated names (`'user.create'`), with declarative validation, RBAC, timeouts, and transaction propagation.
86
+ - **Ambient Context & Composable Hooks (`core/context`)**: Zero-dependency `AsyncLocalStorage` hooks (`useContext`, `useDb`, `useAuth`, `useUser`, `useService`, `useLogger`, `useReq`, `useRes`, `useParams`, `useQuery`, `useTransaction`, `useBreaker`, `withContext`) eliminating prop drilling across services, loaders, and background jobs.
86
87
  - **`models/`**: Schema-driven ORM models with `zdb` definitions, repositories, relations, scopes, and query cache.
87
88
  - **`plugins/`**: Modular extension ecosystem (Admin Panel, Media Library, Polar Billing, CSV/XLSX, Realtime, CSRF, CORS, Basic Auth).
88
89
  - **`views/`**: Nunjucks layouts, partials, and `fsy` template helpers.
@@ -69,6 +69,15 @@ module.exports = defineModel({
69
69
 
70
70
  ---
71
71
 
72
+ ## 1.1 `zdb` vs `z` Distinction & Best Practices
73
+
74
+ - **`zdb.*` (Single Source of Truth for Models):** Decorates Zod with SQL column types, migration info, query filtering, and Admin UI controls. Always use `zdb` for persistent table columns in `models/*.js`.
75
+ - **`z.*` (Pure Validation):** Use only for in-memory request/service validation (`pages/api/...`, `services/...`).
76
+ - **Never wrap in `z.object({...})`:** Pass a plain object `schema: { ... }` or functional schema `schema: ({ zdb, z }) => ({ ... })`.
77
+ - **Raw Zod Types in Models:** Access Zod primitives directly via `zdb.z` or functional schema without importing `zod`.
78
+
79
+ ---
80
+
72
81
  ## 2. Repositories API
73
82
 
74
83
  ```javascript
@@ -52,6 +52,20 @@ module.exports = defineApi({
52
52
  });
53
53
  ```
54
54
 
55
+ ### Composable Hooks in API Endpoints:
56
+ ```javascript
57
+ const { defineApi, useDb, useAuth, useService } = require('webspresso');
58
+
59
+ module.exports = defineApi({
60
+ handler: async (req, res) => {
61
+ const db = useDb();
62
+ const user = useAuth();
63
+ const summary = await useService('analytics.userSummary', { userId: user.id });
64
+ return res.json({ user, summary });
65
+ },
66
+ });
67
+ ```
68
+
55
69
  ---
56
70
 
57
71
  ## 2. SSR HTML Pages & Data Loaders (`pages/...`)
@@ -62,7 +76,7 @@ module.exports = defineApi({
62
76
  ### Companion Loader (`definePage`)
63
77
 
64
78
  ```javascript
65
- const { definePage } = require('webspresso');
79
+ const { definePage, useDb, useParams } = require('webspresso');
66
80
 
67
81
  module.exports = definePage({
68
82
  middleware: ['auth'], // optional
@@ -70,7 +84,9 @@ module.exports = definePage({
70
84
  title: 'Product Details',
71
85
  },
72
86
  load: async ({ req, res, db, ctx }) => {
73
- const product = await db.getRepository('Product').findById(req.params.id);
87
+ // Both parameter injection ({ req, db }) and hooks (useDb(), useParams()) are fully supported
88
+ const params = useParams();
89
+ const product = await (db || useDb()).getRepository('Product').findById(params.id);
74
90
  if (!product) {
75
91
  throw new ctx.errors.NotFoundError('Product not found');
76
92
  }
@@ -45,7 +45,7 @@ module.exports = defineService({
45
45
  key: (input) => `refund:${input.userId}:${input.amount}`,
46
46
  },
47
47
 
48
- // Execution Handler
48
+ // Execution Handler (aliases: handler, execute, run)
49
49
  handler: async (input, ctx) => {
50
50
  // ctx.db, ctx.service, ctx.user, ctx.logger are automatically injected
51
51
  const userRepo = ctx.db.getRepository('User');
@@ -78,13 +78,46 @@ module.exports = defineService({
78
78
 
79
79
  ---
80
80
 
81
- ## 3. Invoking Services
81
+ ## 3. Composable Ambient Context Hooks (Zero-Prop-Drilling)
82
+
83
+ Webspresso provides zero-dependency ambient context hooks via `AsyncLocalStorage`. You can access the database, authenticated user, logger, and other services directly without threading `ctx` through helper functions:
84
+
85
+ ```javascript
86
+ const { useDb, useAuth, useService, useLogger, useTransaction } = require('webspresso');
87
+
88
+ async function verifyAccountStatus(userId) {
89
+ const db = useDb();
90
+ const logger = useLogger();
91
+ logger.info('Verifying account status', { userId });
92
+ return await db.getRepository('Account').findOne({ userId });
93
+ }
94
+
95
+ module.exports = defineService({
96
+ handler: async (input) => {
97
+ // Zero-prop-drilling: helper functions access ambient db/logger directly
98
+ const account = await verifyAccountStatus(input.userId);
99
+ const currentUser = useAuth();
100
+
101
+ // Invoke sibling services directly via hook
102
+ await useService('audit.log', { user: currentUser?.id });
103
+
104
+ return { account };
105
+ },
106
+ });
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 4. Invoking Services
82
112
 
83
113
  From SSR Companion Loaders:
84
114
  ```javascript
115
+ // Via ctx.service or useService() hook
116
+ const { useService } = require('webspresso');
117
+
85
118
  module.exports = definePage({
86
119
  load: async ({ ctx }) => {
87
- const data = await ctx.service('order.summary', { userId: ctx.user.id });
120
+ const data = await useService('order.summary', { userId: ctx.user.id });
88
121
  return { data };
89
122
  },
90
123
  });
package/AGENTS.md CHANGED
@@ -25,6 +25,7 @@ When tasked with a feature or bugfix, **immediately consult the corresponding to
25
25
  | **Cloud Migration Jobs (Zero-Downtime DB Deploy)** | [`.agents/MIGRATION_JOBS.md`](.agents/MIGRATION_JOBS.md) | `bin/commands/add-migration-job.js`, `core/orm/migrate-cloud.js` |
26
26
  | **Rate Limiting Engine (Zero-Dependency & Redis)** | [`.agents/RATE_LIMIT.md`](.agents/RATE_LIMIT.md) | `core/rate-limit/` |
27
27
  | **Feature Flags & Canary Splitter (`core/flags`)** | [`.agents/FLAGS.md`](.agents/FLAGS.md) | `core/flags/` |
28
+ | **Ambient Context & Composable Hooks (`core/context`)** | [`.agents/CONTEXT.md`](.agents/CONTEXT.md) | `core/context/` |
28
29
  | **API Signatures & Types Verification** | [`index.d.ts`](index.d.ts) | **Canonical Ground Truth** |
29
30
 
30
31
  ---
@@ -51,6 +52,7 @@ Every coding agent working on Webspresso **MUST** follow this 4-step execution d
51
52
  | **SSR Rendering**| `res.render('profile.njk', data)` inside custom handlers | Companion loader `pages/profile.js` exporting `async function load()` |
52
53
  | **Exceptions** | `res.status(404).json({ error: 'Not found' })` | `throw new NotFoundError('User not found')` |
53
54
  | **Background** | `setTimeout(...)`, `npm i bullmq` | `jobs/user/welcome.js` → `req.queue.dispatch('user.welcome', data)` |
55
+ | **Model Schemas** | `schema: z.object({ ... })` (raw zod wrapping in models) | `schema: { id: zdb.id(), ... }` or `schema: ({ zdb, z }) => ({ ... })` |
54
56
 
55
57
  ---
56
58
 
@@ -110,7 +112,7 @@ Every coding agent working on Webspresso **MUST** follow this 4-step execution d
110
112
  - Encapsulate domain features using `defineModule({ name, pages, api, services, middlewares })`.
111
113
  - Auto-registers routes (`/auth/login`, `/api/auth/login`), services (`'auth.login'`), and scoped middlewares without manual router glue.
112
114
 
113
- ### 1.2 Services Layer (`services/`, `src/services/`)
115
+ ### 1.2 Services Layer (`services/`, `core/services/`)
114
116
  - **File-based Auto-Discovery**: Services located in `services/` map directly to dot-separated service names (e.g. `services/user/create.js` → `'user.create'`) and camelCase aliases (`'user.reset-password'` & `'user.resetPassword'`).
115
117
  - **Declarative Validation & Authorization**: Define optional Zod `schema`, `auth` (boolean, role string/array, predicate), `timeout` (ms), `transaction: true` (ACID transaction propagation across nested calls), and `cache: { ttl, key }`.
116
118
  - **Unified Invocation**: Consumed via `ctx.service('service.name', input)` from SSR loaders, `req.service()` in API routes, or standalone `createServiceRegistry()`.
@@ -126,8 +128,8 @@ Every coding agent working on Webspresso **MUST** follow this 4-step execution d
126
128
  - `await repo.delete(id)` *(respects soft deletes)*
127
129
  - `await repo.query().where('price', '>', 50).paginate({ page, limit })`
128
130
  - `await repo.query().include('user', 'tags').find()` *(eager loading)*
129
- - **Model Definition**: Defined in `models/*.js` using `defineModel({ name, table, schema, relations, scopes, hidden, admin, cache })`.
130
- - **Schema & Types**: Built with `zdb` schema builder (`zdb.id()`, `zdb.string()`, `zdb.integer()`, `zdb.boolean()`, `zdb.file()`, `zdb.json()`).
131
+ - **Model Definition**: Defined in `models/*.js` using `defineModel({ name, table, schema, relations, scopes, hidden, admin, cache })`. Accepts clean plain object `schema: { ... }` or functional schema `schema: ({ zdb, z }) => ({ ... })`.
132
+ - **`zdb` vs `z` Single Source of Truth**: NEVER wrap model schemas in `z.object({...})`. ALWAYS use `zdb.*` (`zdb.id()`, `zdb.string()`, `zdb.integer()`, `zdb.email()`, `zdb.boolean()`, `zdb.file()`, `zdb.json()`) to decorate models with SQL column types, Knex migrations, Admin UI metadata, and query filters. Use pure `z.*` ONLY for in-memory request/service payloads (`pages/api/...`, `services/...`). Access raw Zod primitives in models via `zdb.z` or functional schema without importing `zod`.
131
133
  - **Ambient Transactions**: Repositories automatically bind to ambient transactions (`AsyncLocalStorage`) inside `db.transaction()` and services (`transaction: true`) without manual `trx` passing.
132
134
  - **Query Caching**: Memory/provider query cache configured per database instance or model (`cache: 'auto'|'smart'|true`).
133
135
  - **Migrations**: Database schema changes managed via Knex migrations in `migrations/` (`webspresso db:migrate`).
@@ -198,3 +200,4 @@ Plugins expose `name`, `version`, `dependencies`, and lifecycle hooks (`register
198
200
  8. **Page Data Loaders (NO Manual `res.render()`)**: Never call `res.render()` inside custom handlers for web pages. Use companion loaders `pages/*.js` exporting `async function load({ req, res, db, ctx })`.
199
201
  9. **Semantic Exceptions (NO Generic Errors / Manual Responses)**: Never return manual `res.status(404).json(...)` inside services or loaders. Always throw semantic errors: `NotFoundError`, `ValidationError`, `UnauthorizedError`, `ForbiddenError`.
200
202
  10. **Background Jobs (NO BullMQ / `setTimeout`)**: Never use unmanaged `setTimeout` or external queues. Place job workers in `jobs/` and dispatch via `req.queue.dispatch()`.
203
+ 11. **`zdb` vs `z` Single Source of Truth (NO `schema: z.object` in Models)**: Never wrap model schemas in `z.object({...})`. Always use `zdb.*` for persistent database columns in `models/*.js`. Use pure `z.*` ONLY for in-memory request/service payloads (`pages/api/...`, `services/...`). Access raw Zod primitives in models via `zdb.z` or functional schema `schema: ({ zdb, z }) => ({ ... })` without importing `zod`.
package/core/app.js CHANGED
@@ -22,8 +22,9 @@ const {
22
22
  } = require('./resilience');
23
23
  const { createTaggedLruCache } = require('./cache');
24
24
  const { createEventDispatcher, loadListeners } = require('./events');
25
- const { createPluginManager } = require('../src/plugin-manager');
26
- const { createServiceRegistry } = require('../src/services');
25
+ const { createPluginManager } = require('./plugins');
26
+ const { createServiceRegistry } = require('./services');
27
+ const { getContext } = require('./context/request');
27
28
 
28
29
  let isWebspressoPackageCache = null;
29
30
  function checkIsWebspressoPackage() {
@@ -153,7 +154,7 @@ class CoreApp {
153
154
 
154
155
  // Auto-register built-in auth services if database is present
155
156
  if (options.db) {
156
- const { createAuthServices } = require('../src/services/builtins/auth');
157
+ const { createAuthServices } = require('./services/builtins/auth');
157
158
  const authServices = createAuthServices({ db: options.db });
158
159
  for (const [name, def] of Object.entries(authServices)) {
159
160
  if (!this.serviceRegistry.has(name)) {
@@ -163,7 +164,7 @@ class CoreApp {
163
164
  }
164
165
 
165
166
  // Auto-register built-in system services (health, cleanup, cache flush, info)
166
- const { createSystemServices } = require('../src/services/builtins/system');
167
+ const { createSystemServices } = require('./services/builtins/system');
167
168
  const systemServices = createSystemServices({
168
169
  db: options.db ?? null,
169
170
  serviceRegistry: this.serviceRegistry,
@@ -414,7 +415,8 @@ class CoreApp {
414
415
  callOpts = maybeOpts || {};
415
416
  } else {
416
417
  callOpts = ctxOrOpts || {};
417
- const base = callOpts.context || callOpts.ctx || this.context;
418
+ const ambientStore = getContext();
419
+ const base = callOpts.context || callOpts.ctx || ambientStore || this.context;
418
420
  if (callOpts.user !== undefined || callOpts.auth !== undefined) {
419
421
  callCtx = Object.create(base);
420
422
  if (callOpts.user !== undefined) callCtx.user = callOpts.user;
@@ -1,73 +1,4 @@
1
- /**
2
- * Schema Applicator
3
- * Parses and validates request input against compiled schemas
4
- */
5
-
6
- const EMPTY_INPUT = Object.freeze({
7
- body: undefined,
8
- params: undefined,
9
- query: undefined,
10
- });
11
-
12
- /**
13
- * Apply compiled schema to request
14
- * Parses body, params, and query against their respective schemas
15
- * Stores validated results in req.input
16
- *
17
- * @param {Object} req - Express request object
18
- * @param {Object|null} compiledSchema - Compiled schema from compileSchema
19
- * @returns {void}
20
- * @throws {ZodError} If validation fails
21
- */
22
- function applySchema(req, compiledSchema) {
23
- // No schema means no validation
24
- if (!compiledSchema) {
25
- req.input = EMPTY_INPUT;
26
- return;
27
- }
28
-
29
- // Initialize req.input
30
- req.input = {
31
- body: undefined,
32
- params: undefined,
33
- query: undefined,
34
- };
35
-
36
- // Parse body if schema exists
37
- if (compiledSchema.body) {
38
- req.input.body = compiledSchema.body.parse(req.body);
39
- }
40
-
41
- // Parse params if schema exists
42
- if (compiledSchema.params) {
43
- req.input.params = compiledSchema.params.parse(req.params);
44
- }
45
-
46
- // Parse query if schema exists
47
- if (compiledSchema.query) {
48
- req.input.query = compiledSchema.query.parse(req.query);
49
- }
50
- }
51
-
52
- let _translateValidationIssues = null;
53
- function getTranslateValidationIssues() {
54
- if (!_translateValidationIssues) {
55
- try {
56
- _translateValidationIssues = require('../src/api/validation-i18n').translateValidationIssues;
57
- } catch {
58
- _translateValidationIssues = (issues) => issues;
59
- }
60
- }
61
- return _translateValidationIssues;
62
- }
63
-
64
- function translateValidationIssues(issues, options) {
65
- return getTranslateValidationIssues()(issues, options);
66
- }
67
-
68
- module.exports = {
69
- applySchema,
70
- translateValidationIssues,
71
- };
72
-
1
+ 'use strict';
73
2
 
3
+ // Backward-compatibility shim
4
+ module.exports = require('./validation/apply');
package/core/cache/lru.js CHANGED
@@ -47,6 +47,7 @@ function safeClone(value) {
47
47
  * @property {string|number} [defaultTtl=0] - Default TTL in ms or string duration ('5m', '1h', 0 = no expiry)
48
48
  * @property {function(*, string): number} [sizeCalculation] - Custom byte sizer function
49
49
  * @property {boolean} [clone=false] - Whether to deep-clone items on read/write
50
+ * @property {boolean} [trackBytes=false] - Whether to track approximate bytes when maxBytes is 0
50
51
  * @property {function(string, *, 'evict'|'expire'|'delete'|'clear'): void} [onEvict] - Eviction callback
51
52
  */
52
53
 
@@ -71,6 +72,11 @@ class LruCache {
71
72
  this.clone = options.clone === true;
72
73
  this.onEvict = typeof options.onEvict === 'function' ? options.onEvict : null;
73
74
 
75
+ // Track bytes only when explicit memory limit exists, custom sizer provided, or trackBytes is explicitly requested
76
+ this.trackBytes = options.trackBytes !== undefined
77
+ ? options.trackBytes === true
78
+ : (this.maxBytes > 0 || typeof options.sizeCalculation === 'function');
79
+
74
80
  /** @type {Map<string, { value: *, size: number, expiresAt: number }>} */
75
81
  this._store = new Map();
76
82
 
@@ -115,6 +121,10 @@ class LruCache {
115
121
  if (this.sizeCalculation) {
116
122
  return Math.max(0, this.sizeCalculation(value, key));
117
123
  }
124
+ // Fast path: if byte tracking is disabled, avoid expensive recursive object graph traversal
125
+ if (!this.trackBytes) {
126
+ return 0;
127
+ }
118
128
  return Math.max(0, approximateByteSize(value));
119
129
  }
120
130
 
@@ -60,6 +60,10 @@ class TaggedLruCache {
60
60
  return this.lru.maxBytes;
61
61
  }
62
62
 
63
+ get trackBytes() {
64
+ return this.lru.trackBytes;
65
+ }
66
+
63
67
  get maxEntries() {
64
68
  return this.lru.maxEntries;
65
69
  }