exisjs 0.5.9 → 0.5.11
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/dist/auth/index.d.ts +3 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +4 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/oauth/core.d.ts +96 -0
- package/dist/auth/oauth/core.d.ts.map +1 -0
- package/dist/auth/oauth/core.js +203 -0
- package/dist/auth/oauth/core.js.map +1 -0
- package/dist/auth/oauth/index.d.ts +45 -0
- package/dist/auth/oauth/index.d.ts.map +1 -0
- package/dist/auth/oauth/index.js +64 -0
- package/dist/auth/oauth/index.js.map +1 -0
- package/dist/auth/oauth/middleware.d.ts +63 -0
- package/dist/auth/oauth/middleware.d.ts.map +1 -0
- package/dist/auth/oauth/middleware.js +164 -0
- package/dist/auth/oauth/middleware.js.map +1 -0
- package/dist/auth/oauth/providers/custom.d.ts +41 -0
- package/dist/auth/oauth/providers/custom.d.ts.map +1 -0
- package/dist/auth/oauth/providers/custom.js +204 -0
- package/dist/auth/oauth/providers/custom.js.map +1 -0
- package/dist/auth/oauth/providers/discord.d.ts +23 -0
- package/dist/auth/oauth/providers/discord.d.ts.map +1 -0
- package/dist/auth/oauth/providers/discord.js +130 -0
- package/dist/auth/oauth/providers/discord.js.map +1 -0
- package/dist/auth/oauth/providers/facebook.d.ts +28 -0
- package/dist/auth/oauth/providers/facebook.d.ts.map +1 -0
- package/dist/auth/oauth/providers/facebook.js +124 -0
- package/dist/auth/oauth/providers/facebook.js.map +1 -0
- package/dist/auth/oauth/providers/github.d.ts +25 -0
- package/dist/auth/oauth/providers/github.d.ts.map +1 -0
- package/dist/auth/oauth/providers/github.js +132 -0
- package/dist/auth/oauth/providers/github.js.map +1 -0
- package/dist/auth/oauth/providers/google.d.ts +23 -0
- package/dist/auth/oauth/providers/google.d.ts.map +1 -0
- package/dist/auth/oauth/providers/google.js +134 -0
- package/dist/auth/oauth/providers/google.js.map +1 -0
- package/dist/auth/oauth/providers/microsoft.d.ts +28 -0
- package/dist/auth/oauth/providers/microsoft.d.ts.map +1 -0
- package/dist/auth/oauth/providers/microsoft.js +136 -0
- package/dist/auth/oauth/providers/microsoft.js.map +1 -0
- package/dist/auth/oauth/types.d.ts +158 -0
- package/dist/auth/oauth/types.d.ts.map +1 -0
- package/dist/auth/oauth/types.js +4 -0
- package/dist/auth/oauth/types.js.map +1 -0
- package/dist/auth/session.d.ts +4 -1
- package/dist/auth/session.d.ts.map +1 -1
- package/dist/auth/session.js +33 -5
- package/dist/auth/session.js.map +1 -1
- package/dist/cache/store.d.ts +11 -3
- package/dist/cache/store.d.ts.map +1 -1
- package/dist/cache/store.js +87 -19
- package/dist/cache/store.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +4 -6
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/database/adapters/mongodb.d.ts +58 -0
- package/dist/database/adapters/mongodb.d.ts.map +1 -0
- package/dist/database/adapters/mongodb.js +264 -0
- package/dist/database/adapters/mongodb.js.map +1 -0
- package/dist/database/adapters/mysql.d.ts +27 -0
- package/dist/database/adapters/mysql.d.ts.map +1 -0
- package/dist/database/adapters/mysql.js +188 -0
- package/dist/database/adapters/mysql.js.map +1 -0
- package/dist/database/adapters/postgres.d.ts +28 -0
- package/dist/database/adapters/postgres.d.ts.map +1 -0
- package/dist/database/adapters/postgres.js +174 -0
- package/dist/database/adapters/postgres.js.map +1 -0
- package/dist/database/adapters/sqlite.d.ts +27 -0
- package/dist/database/adapters/sqlite.d.ts.map +1 -0
- package/dist/database/adapters/sqlite.js +149 -0
- package/dist/database/adapters/sqlite.js.map +1 -0
- package/dist/database/index.d.ts +42 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +58 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/manager.d.ts +114 -0
- package/dist/database/manager.d.ts.map +1 -0
- package/dist/database/manager.js +269 -0
- package/dist/database/manager.js.map +1 -0
- package/dist/database/migration.d.ts +57 -0
- package/dist/database/migration.d.ts.map +1 -0
- package/dist/database/migration.js +329 -0
- package/dist/database/migration.js.map +1 -0
- package/dist/database/query-builder.d.ts +89 -0
- package/dist/database/query-builder.d.ts.map +1 -0
- package/dist/database/query-builder.js +353 -0
- package/dist/database/query-builder.js.map +1 -0
- package/dist/database/types.d.ts +184 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +4 -0
- package/dist/database/types.js.map +1 -0
- package/dist/docs/ARCHITECTURE.md +349 -0
- package/dist/docs/BENCHMARKS.md +36 -0
- package/dist/docs/README.md +34 -0
- package/dist/docs/content/README.md +44 -0
- package/dist/docs/content/adapters.mdx +199 -0
- package/dist/docs/content/cache.mdx +239 -0
- package/dist/docs/content/circuit-breaker.mdx +130 -0
- package/dist/docs/content/cli/commands.mdx +214 -0
- package/dist/docs/content/cli/overview.mdx +92 -0
- package/dist/docs/content/cli/usage.mdx +61 -0
- package/dist/docs/content/config.mdx +214 -0
- package/dist/docs/content/controllers.mdx +241 -0
- package/dist/docs/content/cron.mdx +109 -0
- package/dist/docs/content/database.mdx +209 -0
- package/dist/docs/content/dataloader.mdx +170 -0
- package/dist/docs/content/decorators.mdx +204 -0
- package/dist/docs/content/errors.mdx +344 -0
- package/dist/docs/content/gateways.mdx +376 -0
- package/dist/docs/content/installation.mdx +210 -0
- package/dist/docs/content/introduction.mdx +96 -0
- package/dist/docs/content/middleware.mdx +441 -0
- package/dist/docs/content/modules.mdx +144 -0
- package/dist/docs/content/oauth.mdx +179 -0
- package/dist/docs/content/providers.mdx +147 -0
- package/dist/docs/content/queues.mdx +177 -0
- package/dist/docs/content/requests.mdx +293 -0
- package/dist/docs/content/responses.mdx +271 -0
- package/dist/docs/content/routing.mdx +541 -0
- package/dist/docs/content/rpc.mdx +146 -0
- package/dist/docs/content/security.mdx +294 -0
- package/dist/docs/content/sse.mdx +159 -0
- package/dist/docs/content/structure.mdx +116 -0
- package/dist/docs/content/testing.mdx +139 -0
- package/dist/docs/content/validation.mdx +249 -0
- package/dist/docs/content/websockets.mdx +289 -0
- package/dist/docs/integrations/README.md +23 -0
- package/dist/docs/integrations/jwt.mdx +151 -0
- package/dist/docs/integrations/mongo.mdx +183 -0
- package/dist/docs/integrations/observability.mdx +125 -0
- package/dist/docs/integrations/openai.mdx +108 -0
- package/dist/docs/integrations/postgres.mdx +190 -0
- package/dist/docs/integrations/posthog.mdx +122 -0
- package/dist/docs/integrations/redis.mdx +141 -0
- package/dist/docs/integrations/resend.mdx +117 -0
- package/dist/docs/integrations/s3.mdx +122 -0
- package/dist/docs/integrations/swagger.mdx +113 -0
- package/dist/docs/reference/README.md +3 -0
- package/dist/docs/reference/adapters.mdx +130 -0
- package/dist/docs/reference/auth.mdx +183 -0
- package/dist/docs/reference/cache.mdx +132 -0
- package/dist/docs/reference/circuit-breaker.mdx +71 -0
- package/dist/docs/reference/config.mdx +118 -0
- package/dist/docs/reference/context.mdx +168 -0
- package/dist/docs/reference/dataloader.mdx +91 -0
- package/dist/docs/reference/decorators.mdx +293 -0
- package/dist/docs/reference/di.mdx +123 -0
- package/dist/docs/reference/error.mdx +170 -0
- package/dist/docs/reference/exis.mdx +124 -0
- package/dist/docs/reference/middleware.mdx +304 -0
- package/dist/docs/reference/plugin.mdx +113 -0
- package/dist/docs/reference/queue.mdx +155 -0
- package/dist/docs/reference/router.mdx +248 -0
- package/dist/docs/reference/sanitize.mdx +130 -0
- package/dist/docs/reference/storage.mdx +116 -0
- package/dist/docs/reference/swagger.mdx +132 -0
- package/dist/docs/reference/testing.mdx +145 -0
- package/dist/docs/reference/validator.mdx +186 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/backpressure.d.ts.map +1 -1
- package/dist/middleware/backpressure.js +21 -12
- package/dist/middleware/backpressure.js.map +1 -1
- package/dist/middleware/cache.d.ts +6 -0
- package/dist/middleware/cache.d.ts.map +1 -1
- package/dist/middleware/cache.js +84 -28
- package/dist/middleware/cache.js.map +1 -1
- package/dist/middleware/idempotency.d.ts +4 -1
- package/dist/middleware/idempotency.d.ts.map +1 -1
- package/dist/middleware/idempotency.js +29 -4
- package/dist/middleware/idempotency.js.map +1 -1
- package/dist/middleware/ip-filter.d.ts.map +1 -1
- package/dist/middleware/ip-filter.js +16 -0
- package/dist/middleware/ip-filter.js.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +29 -12
- package/dist/middleware/rate-limit.js.map +1 -1
- package/dist/queue/drivers/MemoryDriver.d.ts +6 -3
- package/dist/queue/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/queue/drivers/MemoryDriver.js +76 -23
- package/dist/queue/drivers/MemoryDriver.js.map +1 -1
- package/dist/server/bootstrapper.d.ts +3 -0
- package/dist/server/bootstrapper.d.ts.map +1 -1
- package/dist/server/bootstrapper.js +56 -8
- package/dist/server/bootstrapper.js.map +1 -1
- package/dist/server/bun-adapter.d.ts +2 -0
- package/dist/server/bun-adapter.d.ts.map +1 -1
- package/dist/server/bun-adapter.js +23 -12
- package/dist/server/bun-adapter.js.map +1 -1
- package/dist/server/request-handler.d.ts +0 -2
- package/dist/server/request-handler.d.ts.map +1 -1
- package/dist/server/request-handler.js +2 -24
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +24 -11
- package/dist/server/request.js.map +1 -1
- package/dist/server/uws-adapter.d.ts +80 -0
- package/dist/server/uws-adapter.d.ts.map +1 -0
- package/dist/server/uws-adapter.js +357 -0
- package/dist/server/uws-adapter.js.map +1 -0
- package/dist/server/ws-orchestrator.d.ts +1 -0
- package/dist/server/ws-orchestrator.d.ts.map +1 -1
- package/dist/server/ws-orchestrator.js +83 -0
- package/dist/server/ws-orchestrator.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/validator/tex.d.ts +21 -16
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js.map +1 -1
- package/package.json +11 -3
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
nextUrl: "/"
|
|
3
|
+
nextTitle: "Back to Home"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Adapters & Serverless
|
|
7
|
+
|
|
8
|
+
ExisJS was engineered using modern web standards (Fetch API, Request/Response patterns). Because of this, ExisJS can run across various JavaScript runtimes.
|
|
9
|
+
|
|
10
|
+
To deploy your ExisJS application to serverless or edge environments, you simply wrap your `app` with the corresponding adapter from `exisjs/adapters`.
|
|
11
|
+
|
|
12
|
+
Every adapter handles **lazy initialization** automatically—your routes, plugins, and lifecycle hooks (`onStart`) are bootstrapped on the very first incoming request (cold start), so you never have to worry about manual setup in serverless environments.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Available Adapters
|
|
17
|
+
|
|
18
|
+
| Adapter | Import | Environment |
|
|
19
|
+
|---------|--------|-------------|
|
|
20
|
+
| `vercel` | `exisjs/adapters` | Vercel Serverless Functions |
|
|
21
|
+
| `aws` | `exisjs/adapters` | AWS Lambda + API Gateway |
|
|
22
|
+
| `cloudflare` | `exisjs/adapters` | Cloudflare Workers |
|
|
23
|
+
| `netlify` | `exisjs/adapters` | Netlify Edge Functions |
|
|
24
|
+
| `deno` | `exisjs/adapters` | Deno Deploy / Deno.serve |
|
|
25
|
+
| `bun` | `exisjs/adapters` | Bun.serve |
|
|
26
|
+
| `fastly` | `exisjs/adapters` | Fastly Compute@Edge |
|
|
27
|
+
|
|
28
|
+
> [!WARNING]
|
|
29
|
+
> **Edge Limitations**: When deploying to Edge runtimes (Cloudflare Workers, Vercel Edge, Netlify Edge, Fastly), some Node.js specific features will not function. Specifically:
|
|
30
|
+
> - **Queues & Cron**: Background jobs and scheduled tasks require a long-running process and are unsupported on Edge.
|
|
31
|
+
> - **WebSockets**: State-heavy WebSockets are not supported via the standard Edge Fetch adapters.
|
|
32
|
+
> - **FileSystem Cache**: `FileSystemCacheStore` cannot write to a filesystem on Edge. You must configure a `RedisCacheStore` if you need caching on the Edge.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Vercel
|
|
37
|
+
|
|
38
|
+
To deploy an ExisJS app to Vercel, create an `api/index.js` entrypoint that wraps your app with the `vercel` adapter.
|
|
39
|
+
|
|
40
|
+
```javascript filename="api/index.js"
|
|
41
|
+
import { dirname, join } from "node:path";
|
|
42
|
+
import { fileURLToPath } from "node:url";
|
|
43
|
+
import { vercel } from "exisjs/adapters";
|
|
44
|
+
|
|
45
|
+
const projectRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
46
|
+
|
|
47
|
+
// We use a dynamic import with a file URL to prevent Vercel's bundler (@vercel/node)
|
|
48
|
+
// from statically cloning dependencies (like Mongoose) into the entrypoint.
|
|
49
|
+
// This ensures your dynamic routes and server use the exact same module instances.
|
|
50
|
+
const serverPath = join(projectRoot, ".exis", "server", "src", "http", "server.js");
|
|
51
|
+
const { default: app } = await import("file://" + serverPath);
|
|
52
|
+
|
|
53
|
+
export default vercel(app, { cwd: projectRoot });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then configure `vercel.json` to route all traffic to this function and bundle the compiled `.exis` directory:
|
|
57
|
+
|
|
58
|
+
```json filename="vercel.json"
|
|
59
|
+
{
|
|
60
|
+
"version": 2,
|
|
61
|
+
"buildCommand": "npm run build",
|
|
62
|
+
"functions": {
|
|
63
|
+
"api/index.js": {
|
|
64
|
+
"includeFiles": ".exis/**/*"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"rewrites": [
|
|
68
|
+
{ "source": "/(.*)", "destination": "/api/index" }
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> **Note:** ExisJS compiles your application into the `.exis/` directory during `exisjs build`. The `includeFiles` configuration ensures Vercel bundles these compiled files alongside your serverless function. You also need a `public/` directory (even an empty one with a `.keep` file) to prevent Vercel's missing output directory error.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## AWS Lambda
|
|
78
|
+
|
|
79
|
+
Deploying an ExisJS API as an AWS Lambda function (behind API Gateway) takes one line of code. The `aws` adapter converts API Gateway events into native Node.js HTTP streams that ExisJS understands.
|
|
80
|
+
|
|
81
|
+
```typescript filename="src/lambda.ts"
|
|
82
|
+
import app from './http/server'
|
|
83
|
+
import { aws } from 'exisjs/adapters'
|
|
84
|
+
|
|
85
|
+
// Export the AWS Lambda handler
|
|
86
|
+
export const handler = aws(app)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The adapter automatically handles:
|
|
90
|
+
- Mapping `httpMethod`, `path`, and `queryStringParameters` from API Gateway events
|
|
91
|
+
- Converting headers and request bodies
|
|
92
|
+
- Collecting the response stream and returning a properly formatted `APIGatewayResult`
|
|
93
|
+
- Base64 encoding for binary responses
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Cloudflare Workers
|
|
98
|
+
|
|
99
|
+
ExisJS works on Cloudflare Workers using the Web Standard Fetch API.
|
|
100
|
+
|
|
101
|
+
```typescript filename="src/worker.ts"
|
|
102
|
+
import app from './http/server'
|
|
103
|
+
import { cloudflare } from 'exisjs/adapters'
|
|
104
|
+
|
|
105
|
+
export default cloudflare(app)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The adapter returns an object with a `fetch` method that Cloudflare Workers expects. Your `env` and `ctx` (execution context) are automatically passed through to your route handlers.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Netlify
|
|
113
|
+
|
|
114
|
+
Netlify Edge Functions use the Web Standard Fetch API, which ExisJS supports.
|
|
115
|
+
|
|
116
|
+
```typescript filename="netlify/edge-functions/api.ts"
|
|
117
|
+
import app from '../../src/http/server'
|
|
118
|
+
import { netlify } from 'exisjs/adapters'
|
|
119
|
+
|
|
120
|
+
export default netlify(app)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The adapter wraps your app and passes both the incoming `Request` and Netlify's `Context` object through to ExisJS.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Deno
|
|
128
|
+
|
|
129
|
+
Deno natively supports the Web Standard Fetch API, so ExisJS runs on it without any compatibility layers.
|
|
130
|
+
|
|
131
|
+
```typescript filename="src/main.ts"
|
|
132
|
+
import app from './http/server'
|
|
133
|
+
import { deno } from 'exisjs/adapters'
|
|
134
|
+
|
|
135
|
+
Deno.serve({ port: 8000 }, deno(app))
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Bun
|
|
141
|
+
|
|
142
|
+
Bun provides a native HTTP server. The `bun` adapter ties directly into Bun's `serve` function.
|
|
143
|
+
|
|
144
|
+
```typescript filename="src/main.ts"
|
|
145
|
+
import app from './http/server'
|
|
146
|
+
import { bun } from 'exisjs/adapters'
|
|
147
|
+
|
|
148
|
+
Bun.serve({
|
|
149
|
+
port: 3000,
|
|
150
|
+
...bun(app)
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Fastly Compute@Edge
|
|
157
|
+
|
|
158
|
+
Fastly's Compute@Edge platform runs your code at the network edge. The `fastly` adapter integrates with Fastly's event-driven model.
|
|
159
|
+
|
|
160
|
+
```typescript filename="src/index.ts"
|
|
161
|
+
import app from './http/server'
|
|
162
|
+
import { fastly } from 'exisjs/adapters'
|
|
163
|
+
|
|
164
|
+
addEventListener('fetch', fastly(app))
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## The Fetch Adapter (Building Custom Adapters)
|
|
170
|
+
|
|
171
|
+
Under the hood, ExisJS provides a universal **Fetch adapter** (`handleFetch`) that bridges the gap between the Web Standard `Request`/`Response` API and ExisJS's internal Node.js-style request pipeline.
|
|
172
|
+
|
|
173
|
+
Every edge/serverless adapter (Cloudflare, Deno, Bun, Netlify, Fastly) ultimately calls `app.fetch()`, which internally uses `handleFetch` to:
|
|
174
|
+
|
|
175
|
+
1. **Convert** a Web Standard `Request` into a Node.js-compatible `IncomingMessage` (via `FetchIncomingMessage`)
|
|
176
|
+
2. **Route** the request through ExisJS's full middleware and routing pipeline
|
|
177
|
+
3. **Collect** the response and convert it back into a Web Standard `Response` (via `FetchServerResponse`)
|
|
178
|
+
|
|
179
|
+
This means you can create your own custom adapter for **any** runtime that supports the Fetch API:
|
|
180
|
+
|
|
181
|
+
```typescript filename="src/my-custom-adapter.ts"
|
|
182
|
+
import app from './http/server'
|
|
183
|
+
|
|
184
|
+
// app.fetch() works anywhere that has the Web Standard Request/Response API
|
|
185
|
+
const server = createMyCustomServer({
|
|
186
|
+
async handler(request: Request): Promise<Response> {
|
|
187
|
+
return app.fetch(request)
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The `app.fetch()` method accepts an optional `env` and `ctx` parameter, which are passed through to your route handlers—allowing platform-specific features (like Cloudflare's KV bindings or Deno's connection info) to be accessed from within your ExisJS routes.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// Signature
|
|
196
|
+
app.fetch(request: Request, env?: any, ctx?: any): Promise<Response>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
This architecture ensures that ExisJS remains portable. Your application code stays the same—only the deployment wrapper changes.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
---
|
|
2
|
+
nextUrl: "/docs/dataloader"
|
|
3
|
+
nextTitle: "Dataloader"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Route Caching
|
|
7
|
+
|
|
8
|
+
ExisJS comes with a built-in Caching Engine. It provides Next.js-style **Tag-based Revalidation** directly in your backend API.
|
|
9
|
+
|
|
10
|
+
By default, ExisJS uses a local File System cache (`.exis/cache`), meaning you get persistent, server-side caching with **zero configuration** required.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Basic Route Caching
|
|
15
|
+
|
|
16
|
+
To cache an endpoint, simply attach the `cache()` middleware to it. By default, it will cache `GET` requests based on the URL path.
|
|
17
|
+
|
|
18
|
+
#### Functional Paradigm
|
|
19
|
+
|
|
20
|
+
```typescript filename="src/http/products/route.ts"
|
|
21
|
+
import { controller, route } from 'exisjs/router'
|
|
22
|
+
import { cache } from 'exisjs/middleware'
|
|
23
|
+
|
|
24
|
+
export default controller({
|
|
25
|
+
// Cache this route for 60 seconds (60,000 milliseconds)
|
|
26
|
+
list: route.get('/', {
|
|
27
|
+
middlewares: [cache({ ttlMs: 60000 })],
|
|
28
|
+
handle(req, res) {
|
|
29
|
+
// This database query only runs once a minute
|
|
30
|
+
const products = await db.products.findMany()
|
|
31
|
+
return products
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
#### Class-Based (OOP) Paradigm
|
|
38
|
+
|
|
39
|
+
```typescript filename="src/http/products/route.ts"
|
|
40
|
+
import { Controller, Get, Use } from 'exisjs/decorators'
|
|
41
|
+
import { cache } from 'exisjs/middleware'
|
|
42
|
+
|
|
43
|
+
@Controller()
|
|
44
|
+
export default class ProductsController {
|
|
45
|
+
// Cache this route for 60 seconds (60,000 milliseconds)
|
|
46
|
+
@Use(cache({ ttlMs: 60000 }))
|
|
47
|
+
@Get('/')
|
|
48
|
+
async list() {
|
|
49
|
+
// This database query only runs once a minute
|
|
50
|
+
const products = await db.products.findMany()
|
|
51
|
+
return products
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When a user hits this route:
|
|
57
|
+
1. The first request takes full execution time. ExisJS automatically intercepts the response and writes it to the local cache. The response header `X-Exis-Cache: MISS` is set.
|
|
58
|
+
2. The next 10,000 requests over the next minute hit the cache instantly without running your handler. The response header `X-Exis-Cache: HIT` is set.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 2. Dynamic Cache Keys
|
|
63
|
+
|
|
64
|
+
By default, the cache key is the exact request path (`req.path`). But what if your response depends on a user's language, or a query parameter? You can customize the key generation logic.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
cache({
|
|
68
|
+
ttlMs: 60000,
|
|
69
|
+
keyGenerator: (req) => {
|
|
70
|
+
// Cache separately for English vs French users
|
|
71
|
+
const lang = req.query.lang || 'en'
|
|
72
|
+
return `${req.path}?lang=${lang}`
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
> [!WARNING]
|
|
78
|
+
> **Authentication Leak Hazard**: When using `cache()` on authenticated routes, you **must** include the user's ID or session token in your `keyGenerator`. Failing to do so will result in User B receiving a cached response generated for User A.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
cache({
|
|
82
|
+
ttlMs: 60000,
|
|
83
|
+
keyGenerator: (req) => {
|
|
84
|
+
// SAFE: The cache key includes the user ID
|
|
85
|
+
return `${req.path}?user=${req.user?.id}`
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 3. Tag-Based Revalidation (On-Demand)
|
|
93
|
+
|
|
94
|
+
Time-based caching (`ttlMs`) is great, but what if a product's price changes and you want to instantly invalidate the cache?
|
|
95
|
+
|
|
96
|
+
ExisJS allows you to assign **tags** to your cached responses.
|
|
97
|
+
|
|
98
|
+
### Step 1: Assign a Tag
|
|
99
|
+
|
|
100
|
+
#### Functional Paradigm
|
|
101
|
+
|
|
102
|
+
```typescript filename="src/http/products/route.ts"
|
|
103
|
+
import { controller, route } from 'exisjs/router'
|
|
104
|
+
import { cache } from 'exisjs/middleware'
|
|
105
|
+
|
|
106
|
+
export default controller({
|
|
107
|
+
list: route.get('/', {
|
|
108
|
+
middlewares: [
|
|
109
|
+
cache({
|
|
110
|
+
// We assign the 'products-list' tag to this cached route
|
|
111
|
+
tags: ['products-list']
|
|
112
|
+
})
|
|
113
|
+
],
|
|
114
|
+
async handle(req, res) {
|
|
115
|
+
return await db.products.findMany()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Class-Based (OOP) Paradigm
|
|
122
|
+
|
|
123
|
+
```typescript filename="src/http/products/route.ts"
|
|
124
|
+
import { Controller, Get, Use } from 'exisjs/decorators'
|
|
125
|
+
import { cache } from 'exisjs/middleware'
|
|
126
|
+
|
|
127
|
+
@Controller()
|
|
128
|
+
export default class ProductsController {
|
|
129
|
+
@Use(cache({ tags: ['products-list'] }))
|
|
130
|
+
@Get('/')
|
|
131
|
+
async list() {
|
|
132
|
+
return await db.products.findMany()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Step 2: Invalidate the Tag
|
|
138
|
+
|
|
139
|
+
Now, inside an admin route or webhook where a product is updated, you can instantly purge the cache using `revalidateTag()`.
|
|
140
|
+
|
|
141
|
+
#### Functional Paradigm
|
|
142
|
+
|
|
143
|
+
```typescript filename="src/http/admin/products/route.ts"
|
|
144
|
+
import { controller, route } from 'exisjs/router'
|
|
145
|
+
import { revalidateTag } from 'exisjs/cache'
|
|
146
|
+
|
|
147
|
+
export default controller({
|
|
148
|
+
update: route.post('/:id', {
|
|
149
|
+
async handle(req, res) {
|
|
150
|
+
// 1. Update the product in the database
|
|
151
|
+
await db.products.update(req.params.id, req.body)
|
|
152
|
+
|
|
153
|
+
// 2. Instantly purge all routes cached with 'products-list'
|
|
154
|
+
await revalidateTag('products-list')
|
|
155
|
+
|
|
156
|
+
return { success: true }
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### Class-Based (OOP) Paradigm
|
|
163
|
+
|
|
164
|
+
```typescript filename="src/http/admin/products/route.ts"
|
|
165
|
+
import { Controller, Post, Param, Body } from 'exisjs/decorators'
|
|
166
|
+
import { revalidateTag } from 'exisjs/cache'
|
|
167
|
+
|
|
168
|
+
@Controller()
|
|
169
|
+
export default class AdminProductsController {
|
|
170
|
+
@Post('/:id')
|
|
171
|
+
async update(@Param('id') id: string, @Body() body: any) {
|
|
172
|
+
// 1. Update the product in the database
|
|
173
|
+
await db.products.update(id, body)
|
|
174
|
+
|
|
175
|
+
// 2. Instantly purge all routes cached with 'products-list'
|
|
176
|
+
await revalidateTag('products-list')
|
|
177
|
+
|
|
178
|
+
return { success: true }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Dynamic tags are also supported by passing a function to `tags`:
|
|
184
|
+
```typescript
|
|
185
|
+
cache({
|
|
186
|
+
tags: (req) => [`product-${req.params.id}`]
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 4. Custom Cache Stores (Redis)
|
|
193
|
+
|
|
194
|
+
By default, ExisJS uses the local File System (`.exis/cache`) to store responses. This works well for single-server setups.
|
|
195
|
+
|
|
196
|
+
If you are deploying horizontally across a cluster (e.g. Kubernetes, AWS ECS) or Serverless (AWS Lambda), you will want a distributed cache. ExisJS exposes the `CacheStore` interface, making it possible to plug in Redis.
|
|
197
|
+
|
|
198
|
+
*(Note: The official `exisjs/redis` integration provides a Redis Cache Store out of the box, which we will cover in the Integrations documentation.)*
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 5. API Reference (`exisjs/cache`)
|
|
203
|
+
|
|
204
|
+
For advanced use-cases, you can import several utilities directly from the `exisjs/cache` package. Here is the full list of what is exported and how to use it:
|
|
205
|
+
|
|
206
|
+
### `revalidateTag(tag: string)`
|
|
207
|
+
Purges all cached responses associated with a specific tag.
|
|
208
|
+
```typescript
|
|
209
|
+
import { revalidateTag } from 'exisjs/cache'
|
|
210
|
+
await revalidateTag('products-list')
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### `setCacheStore(store: CacheStore)`
|
|
214
|
+
Allows you to globally override the default file-system cache with a custom distributed cache (like Redis or Memcached). This is typically done once in your `server.ts` or `exis.config.ts`.
|
|
215
|
+
```typescript
|
|
216
|
+
import { setCacheStore } from 'exisjs/cache'
|
|
217
|
+
import { RedisCacheStore } from 'exisjs/redis'
|
|
218
|
+
|
|
219
|
+
setCacheStore(new RedisCacheStore(process.env.REDIS_URL))
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `getCacheStore(): CacheStore`
|
|
223
|
+
Retrieves the currently active cache store. Useful if you want to manually read/write to the cache outside of the middleware layer.
|
|
224
|
+
```typescript
|
|
225
|
+
import { getCacheStore } from 'exisjs/cache'
|
|
226
|
+
|
|
227
|
+
const store = getCacheStore()
|
|
228
|
+
await store.set('/custom-key', { data: 'hello' }, [], 60000)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Interfaces & Classes
|
|
232
|
+
If you are building your own Custom Cache Store, ExisJS exports the required interfaces:
|
|
233
|
+
- `CacheStore`: The interface you must implement (`get`, `set`, `revalidateTag`, `getTags`).
|
|
234
|
+
- `CacheItem`: The expected shape of a cached response (`data`, `tags`, `createdAt`).
|
|
235
|
+
- `FileSystemCacheStore`: The default implementation used by the framework (safe for multi-process cluster mode).
|
|
236
|
+
- `MemoryCacheStore`: An in-memory implementation.
|
|
237
|
+
|
|
238
|
+
> [!IMPORTANT]
|
|
239
|
+
> **MemoryCacheStore Cluster Limitations**: The `MemoryCacheStore` is strictly process-local. If you are running in cluster mode (`workers > 1`) or multi-instance deployments, calling `revalidateTag()` on one worker will **not** invalidate the cache held in memory by sibling workers. For any multi-process setup, use `FileSystemCacheStore` (for a single server) or `RedisCacheStore` (for multiple servers).
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
nextUrl: "/docs/responses"
|
|
3
|
+
nextTitle: "Responses"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Circuit Breaker
|
|
7
|
+
|
|
8
|
+
Microservices and external APIs fail. It is a reality of distributed systems. When an external service goes down, continuing to send it traffic can cause cascading failures across your entire infrastructure as requests queue up and connection pools are exhausted.
|
|
9
|
+
|
|
10
|
+
ExisJS ships with a native **Circuit Breaker** pattern built into the core framework. It automatically detects failures and "trips" the circuit, rejecting subsequent requests so your system can recover.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. How It Works
|
|
15
|
+
|
|
16
|
+
The ExisJS Circuit Breaker operates as a state machine with three states:
|
|
17
|
+
|
|
18
|
+
1. **`CLOSED`** (Normal): The circuit is closed, and traffic flows normally.
|
|
19
|
+
2. **`OPEN`** (Failing): The failure threshold has been reached. The circuit is tripped, and all requests are rejected with a `CircuitBreakerError` to protect the system.
|
|
20
|
+
3. **`HALF_OPEN`** (Testing Recovery): After a timeout period, the circuit allows exactly *one* request through. If it succeeds, the circuit resets to `CLOSED`. If it fails, it trips back to `OPEN`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. Basic Usage
|
|
25
|
+
|
|
26
|
+
To protect an external API or a database query, wrap the call inside the `CircuitBreaker.fire()` method.
|
|
27
|
+
|
|
28
|
+
#### Functional Paradigm
|
|
29
|
+
|
|
30
|
+
```typescript filename="src/http/payment/route.ts"
|
|
31
|
+
import { controller, route } from 'exisjs/router'
|
|
32
|
+
import { CircuitBreaker } from 'exisjs'
|
|
33
|
+
|
|
34
|
+
// 1. Initialize the Circuit Breaker outside the route handler
|
|
35
|
+
const stripeCircuit = new CircuitBreaker({
|
|
36
|
+
failureThreshold: 5, // Trip after 5 consecutive failures
|
|
37
|
+
resetTimeoutMs: 10000, // Wait 10 seconds before testing recovery
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export default controller({
|
|
41
|
+
charge: route.post('/charge', {
|
|
42
|
+
async handle(req, res) {
|
|
43
|
+
try {
|
|
44
|
+
// 2. Wrap your logic inside .fire()
|
|
45
|
+
const result = await stripeCircuit.fire(async () => {
|
|
46
|
+
// If this throws an error 5 times, the circuit trips
|
|
47
|
+
return await fetch('https://api.stripe.com/v1/charges', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
body: JSON.stringify(req.body)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return { success: true, data: result }
|
|
54
|
+
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err.name === 'CircuitBreakerError') {
|
|
57
|
+
// Fast-fail response without hanging the server
|
|
58
|
+
res.status(503).json({ error: "Payment gateway is currently overloaded. Please try again later." })
|
|
59
|
+
} else {
|
|
60
|
+
res.status(500).json({ error: "Payment failed" })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Class-Based (OOP) Paradigm
|
|
69
|
+
|
|
70
|
+
```typescript filename="src/http/payment/route.ts"
|
|
71
|
+
import { Controller, Post, Body, Res } from 'exisjs/decorators'
|
|
72
|
+
import { CircuitBreaker } from 'exisjs'
|
|
73
|
+
|
|
74
|
+
// 1. Initialize the Circuit Breaker outside the controller
|
|
75
|
+
const stripeCircuit = new CircuitBreaker({
|
|
76
|
+
failureThreshold: 5, // Trip after 5 consecutive failures
|
|
77
|
+
resetTimeoutMs: 10000, // Wait 10 seconds before testing recovery
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
@Controller()
|
|
81
|
+
export default class PaymentController {
|
|
82
|
+
@Post('/charge')
|
|
83
|
+
async charge(@Body() body: any, @Res() res: any) {
|
|
84
|
+
try {
|
|
85
|
+
// 2. Wrap your logic inside .fire()
|
|
86
|
+
const result = await stripeCircuit.fire(async () => {
|
|
87
|
+
// If this throws an error 5 times, the circuit trips
|
|
88
|
+
return await fetch('https://api.stripe.com/v1/charges', {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
body: JSON.stringify(body)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return { success: true, data: result }
|
|
95
|
+
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
if (err.name === 'CircuitBreakerError') {
|
|
98
|
+
// Fast-fail response without hanging the server
|
|
99
|
+
res.status(503).json({ error: "Payment gateway is currently overloaded. Please try again later." })
|
|
100
|
+
} else {
|
|
101
|
+
res.status(500).json({ error: "Payment failed" })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 3. Configuration Options
|
|
111
|
+
|
|
112
|
+
When instantiating `new CircuitBreaker(options)`, you can configure:
|
|
113
|
+
|
|
114
|
+
- **`failureThreshold`** *(number)*: The number of consecutive failures before the circuit state transitions from `CLOSED` to `OPEN`. Default: `5`.
|
|
115
|
+
- **`resetTimeoutMs`** *(number)*: The time in milliseconds to wait before transitioning from `OPEN` to `HALF_OPEN` to test recovery. Default: `10000` (10 seconds).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## API Reference (`exisjs`)
|
|
120
|
+
|
|
121
|
+
You can import the following from the root `exisjs` package:
|
|
122
|
+
|
|
123
|
+
### `CircuitBreaker`
|
|
124
|
+
The main class. Use `new CircuitBreaker(options)` to instantiate a state machine.
|
|
125
|
+
|
|
126
|
+
### `CircuitBreakerError`
|
|
127
|
+
The native error class thrown when the circuit is `OPEN` or `HALF_OPEN` (while a probe is already in-flight). You can check `err.name === 'CircuitBreakerError'` or `err instanceof CircuitBreakerError`.
|
|
128
|
+
|
|
129
|
+
### `CircuitState`
|
|
130
|
+
An enum containing the exact states (`CLOSED`, `OPEN`, `HALF_OPEN`) if you need to manually inspect the breaker's current condition via `circuit.state`.
|