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.
- package/.agents/AGENTS.md +4 -1
- package/.agents/CACHE.md +1 -0
- package/.agents/CONTEXT.md +152 -0
- package/.agents/ORM.md +74 -0
- package/.agents/SHUTDOWN.md +8 -1
- package/.agents/skills/webspresso-framework/SKILL.md +2 -1
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +9 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +18 -2
- package/.agents/skills/webspresso-services/SKILL.md +36 -3
- package/AGENTS.md +6 -3
- package/core/app.js +7 -5
- package/core/applySchema.js +3 -72
- package/core/cache/lru.js +10 -0
- package/core/cache/tagged-lru.js +4 -0
- package/core/compileSchema.js +3 -68
- package/core/compression/compressible.js +53 -6
- package/core/compression/negotiate.js +31 -12
- package/core/context/app-context.js +201 -0
- package/core/context/index.js +55 -0
- package/core/context/request.js +241 -0
- package/core/events/dispatcher.js +144 -15
- package/core/events/signal.js +9 -0
- package/core/health/probes.js +12 -2
- package/core/http/index.js +10 -1
- package/core/http/pipeline.js +519 -368
- package/core/index.js +24 -1
- package/core/orm/eager-loader.js +90 -55
- package/core/orm/index.js +3 -39
- package/core/orm/model.js +6 -1
- package/core/orm/schema-helpers.js +59 -0
- package/core/orm/transaction.js +109 -14
- package/core/plugins/index.js +8 -0
- package/core/plugins/plugin-manager.js +1098 -0
- package/core/queue/manager.js +55 -27
- package/core/rate-limit/stores/memory.js +23 -10
- package/core/resilience/circuit-breaker.js +34 -14
- package/core/schema-cache.js +2 -64
- package/core/services/builtins/auth.js +273 -0
- package/core/services/builtins/exchange.js +229 -0
- package/core/services/builtins/file-manager.js +178 -0
- package/core/services/builtins/mail.js +134 -0
- package/core/services/builtins/media.js +196 -0
- package/core/services/builtins/system.js +219 -0
- package/core/services/discovery.js +133 -0
- package/core/services/executor.js +629 -0
- package/core/services/index.js +110 -5
- package/core/services/loader.js +161 -0
- package/core/services/memoize.js +408 -0
- package/core/services/registry.js +731 -0
- package/core/services/validator.js +424 -0
- package/core/shutdown/shutdown-manager.js +146 -85
- package/core/ssr/stream.js +109 -16
- package/core/stream/text-stream.js +45 -4
- package/core/utils/index.js +110 -0
- package/core/utils/timeout.js +4 -0
- package/core/validation/apply.js +74 -0
- package/core/validation/cache.js +66 -0
- package/core/validation/compiler.js +77 -0
- package/core/validation/index.js +22 -1
- package/index.d.ts +77 -4
- package/index.js +64 -20
- package/package.json +1 -1
- package/src/app-context.js +4 -196
- package/src/dev/typegen.js +1 -1
- package/src/plugin-manager.js +5 -1086
- package/src/server.js +4 -1
- package/src/services/builtins/auth.js +2 -273
- package/src/services/builtins/exchange.js +2 -229
- package/src/services/builtins/file-manager.js +2 -178
- package/src/services/builtins/mail.js +2 -134
- package/src/services/builtins/media.js +2 -196
- package/src/services/builtins/system.js +2 -219
- package/src/services/discovery.js +2 -133
- package/src/services/executor.js +2 -575
- package/src/services/index.js +4 -109
- package/src/services/loader.js +1 -119
- package/src/services/memoize.js +2 -408
- package/src/services/registry.js +1 -711
- package/src/services/validator.js +2 -424
- package/core/utils/array.js +0 -12
- package/core/utils/base64url.js +0 -8
- package/core/utils/ensure-leading-slash.js +0 -6
- package/core/utils/escape-html.js +0 -7
- package/core/utils/escape-regexp.js +0 -7
- package/core/utils/get-client-ip.js +0 -6
- package/core/utils/hmac.js +0 -6
- package/core/utils/random.js +0 -9
- package/core/utils/safe-compare.js +0 -3
- package/core/utils/sha256.js +0 -6
- package/core/utils/strip-trailing-slash.js +0 -6
- 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
|
package/.agents/SHUTDOWN.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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/`, `
|
|
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
|
-
-
|
|
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('
|
|
26
|
-
const { createServiceRegistry } = require('
|
|
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('
|
|
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('
|
|
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
|
|
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;
|
package/core/applySchema.js
CHANGED
|
@@ -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
|
|