@lengkapp/edge 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/README.md +514 -0
- package/edge-client.js +251 -243
- package/edge-server.d.ts +128 -63
- package/edge-server.js +122 -37
- package/package.json +3 -15
- package/client.min.d.ts +0 -3
- package/edge-client.min.js +0 -1
- package/edge-server.min.js +0 -12
- package/readme.md +0 -118
- package/server.min.d.ts +0 -83
package/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
LengkApp Edge License
|
|
2
|
+
|
|
3
|
+
Copyright (c) LengkApp — Yasir Haris
|
|
4
|
+
Contact: yh@lengk.app / yasir.haris@gmail.com
|
|
5
|
+
|
|
6
|
+
Permission is granted to any person obtaining a copy of this software
|
|
7
|
+
and associated documentation files (the "Software") to use, copy, and
|
|
8
|
+
distribute the Software free of charge, for any purpose, including
|
|
9
|
+
commercial use, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
1. The Software may not be modified, adapted, or altered in any way
|
|
12
|
+
without prior written permission from the copyright holder.
|
|
13
|
+
|
|
14
|
+
2. Redistribution of the Software, in whole or in part, must retain
|
|
15
|
+
this LICENSE file unmodified and include the copyright notice above.
|
|
16
|
+
|
|
17
|
+
3. This permission notice does not grant any right to use the
|
|
18
|
+
LengkApp name, brand, or trademarks without separate written
|
|
19
|
+
permission.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
22
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
23
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
24
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE
|
|
25
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
26
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
27
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
# @lengkapp/edge
|
|
2
|
+
|
|
3
|
+
A minimal, high-performance framework for Cloudflare Workers with built-in server-side rendering, routing, auth, rate limiting, caching, and a declarative client-side partial-update library.
|
|
4
|
+
|
|
5
|
+
Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of performance while shipping with **zero runtime dependencies**. See [Performance](#performance) for a local benchmark against Hono, `hono/tiny`, and a plain native Workers handler.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Trie-based routing** – static & dynamic routes (`/users/:id`) with fast lookups.
|
|
12
|
+
- **JSX support** – render JSX components to HTML without a build step (using `jsx` and `renderToString`).
|
|
13
|
+
- **Middleware options** – authentication, rate limiting, CORS, logging, caching, compression, and custom validation.
|
|
14
|
+
- **Cookie helpers** – set, get, and delete cookies with flexible options.
|
|
15
|
+
- **Declarative client** – enhance HTML with `_get`, `_post`, `_target`, `_trigger` attributes to fetch and replace content dynamically, with skeleton loaders and retry buttons.
|
|
16
|
+
- **Resource injection** – automatically load CSS/JS files from response headers.
|
|
17
|
+
- **Scheduled tasks** – built-in support for Cron triggers.
|
|
18
|
+
- **Zero dependencies** – lightweight and fast.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @lengkapp/edge
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
**1. Create a worker script (`worker.js` or `src/index.ts`):**
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { Edge, renderToString } from '@lengkapp/edge';
|
|
36
|
+
|
|
37
|
+
const app = new Edge();
|
|
38
|
+
|
|
39
|
+
app.get('/', (ctx) => ctx.text('Hello World!'));
|
|
40
|
+
|
|
41
|
+
app.get('/users/:id', (ctx) => {
|
|
42
|
+
return ctx.json({ id: ctx.params.id });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export default {
|
|
46
|
+
fetch: (request, env, ctx) => app.fetch(request, env, ctx),
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**2. Create `wrangler.jsonc`:**
|
|
51
|
+
|
|
52
|
+
Cloudflare recommends `wrangler.jsonc` for new projects (TOML is still supported — see [Configuration](#configuration)).
|
|
53
|
+
|
|
54
|
+
```jsonc
|
|
55
|
+
{
|
|
56
|
+
"$schema": "./node_modules/wrangler/config-schema.json",
|
|
57
|
+
"name": "my-edge-app",
|
|
58
|
+
"main": "worker.js",
|
|
59
|
+
// Set this to today's date
|
|
60
|
+
"compatibility_date": "2026-09-06",
|
|
61
|
+
"observability": {
|
|
62
|
+
"enabled": true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**3. Deploy:**
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
wrangler deploy
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> `wrangler publish` was removed in favor of `wrangler deploy`. If you're on an older Wrangler version, run `npm install -g wrangler@latest` first.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Server API
|
|
78
|
+
|
|
79
|
+
### Creating an App
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { Edge } from '@lengkapp/edge';
|
|
83
|
+
const app = new Edge();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Routing
|
|
87
|
+
|
|
88
|
+
Register routes using HTTP method helpers. Both static and dynamic paths are supported.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// Static
|
|
92
|
+
app.get('/home', handler);
|
|
93
|
+
app.post('/submit', handler);
|
|
94
|
+
|
|
95
|
+
// Dynamic (colon-prefixed segments)
|
|
96
|
+
app.get('/users/:id', (ctx) => {
|
|
97
|
+
const userId = ctx.params.id;
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Supported methods: `get`, `post`, `put`, `delete`, `patch`, `options`, `head`.
|
|
102
|
+
|
|
103
|
+
### Context Object
|
|
104
|
+
|
|
105
|
+
Each handler receives a `Context` object (`ctx`) with the following members:
|
|
106
|
+
|
|
107
|
+
| Property / Method | Description |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `ctx.req` | The original `Request` object |
|
|
110
|
+
| `ctx.env` | The environment bindings (KV, secrets, etc.) |
|
|
111
|
+
| `ctx.executionCtx` | The `ExecutionContext` (for `waitUntil`) |
|
|
112
|
+
| `ctx.params` | Route parameters (e.g., `{ id: '123' }`) |
|
|
113
|
+
| `ctx.status` | HTTP status code (default `200`) |
|
|
114
|
+
| `ctx.headers` | `Headers` object for the response |
|
|
115
|
+
| `ctx.query` | `URLSearchParams` (lazy) |
|
|
116
|
+
| `ctx.getCookie(name)` | Get a cookie value |
|
|
117
|
+
| `ctx.setCookie(name, value, options)` | Set a cookie |
|
|
118
|
+
| `ctx.deleteCookie(name, options)` | Delete a cookie (expires immediately) |
|
|
119
|
+
| `ctx.text(data, status?, headers?)` | Return plain text |
|
|
120
|
+
| `ctx.json(data, status?, headers?)` | Return JSON |
|
|
121
|
+
| `ctx.html(data, status?, headers?)` | Return HTML |
|
|
122
|
+
|
|
123
|
+
Cookie options: `path`, `domain`, `maxAge`, `expires`, `secure`, `httpOnly`, `sameSite`.
|
|
124
|
+
|
|
125
|
+
### Returning Responses
|
|
126
|
+
|
|
127
|
+
You can return a `Response` object, a string (becomes text), or an object (automatically JSON).
|
|
128
|
+
If the handler returns a JSX element, it will be rendered to HTML automatically (see [JSX Support](#jsx-support)).
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
app.get('/json', (ctx) => {
|
|
132
|
+
return { hello: 'world' }; // → ctx.json()
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
app.get('/html', (ctx) => {
|
|
136
|
+
return '<h1>Hi</h1>'; // → ctx.html()
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Route Options
|
|
141
|
+
|
|
142
|
+
Pass an options object as the second argument (before the handler) to enable middleware.
|
|
143
|
+
|
|
144
|
+
#### Authentication (`auth`)
|
|
145
|
+
|
|
146
|
+
Requires an `AUTH_KV` KV binding. Valid tokens are stored as keys with a JSON payload.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
app.get('/protected', { auth: true }, handler);
|
|
150
|
+
app.get('/admin', { auth: { role: 'admin' } }, handler);
|
|
151
|
+
app.get('/scoped', { auth: { scopes: ['read', 'write'] } }, handler);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The client must send the token via `Authorization: Bearer <token>` or a cookie named `auth_token`.
|
|
155
|
+
|
|
156
|
+
#### Rate Limiting (`rateLimit`)
|
|
157
|
+
|
|
158
|
+
Requires a `RATE_LIMIT_KV` binding.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
app.get('/limited', { rateLimit: { max: 100, window: 60 } }, handler);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### Caching (`cache`)
|
|
165
|
+
|
|
166
|
+
Caches successful GET responses in the Cloudflare cache.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
app.get('/cached', { cache: { ttl: 60, staleWhileRevalidate: 30 } }, handler);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### CORS (`cors`)
|
|
173
|
+
|
|
174
|
+
Enables CORS headers.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
app.get('/api', { cors: true }, handler);
|
|
178
|
+
|
|
179
|
+
// Custom origin / methods
|
|
180
|
+
app.get('/api', { cors: { origin: 'https://example.com', methods: 'GET,POST' } }, handler);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Logging (`log`)
|
|
184
|
+
|
|
185
|
+
Logs method, URL, and status to the console.
|
|
186
|
+
|
|
187
|
+
#### Compression (`compress`)
|
|
188
|
+
|
|
189
|
+
Compresses the response using gzip/deflate/br if the client supports it.
|
|
190
|
+
|
|
191
|
+
#### Custom Validation (`validate`)
|
|
192
|
+
|
|
193
|
+
A function returning `true`/`false` (or a `Promise`) to allow/deny the request.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
app.post('/submit', {
|
|
197
|
+
validate: (ctx) => ctx.req.headers.get('Authorization') === 'Bearer secret'
|
|
198
|
+
}, handler);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## JSX Support
|
|
204
|
+
|
|
205
|
+
The package includes a minimal JSX runtime. Write components as functions returning JSX.
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { jsx, renderToString, Fragment } from '@lengkapp/edge';
|
|
209
|
+
|
|
210
|
+
function Card({ title }) {
|
|
211
|
+
return (
|
|
212
|
+
<div class="card">
|
|
213
|
+
<h2>{title}</h2>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
app.get('/card', (ctx) => {
|
|
219
|
+
return ctx.html(renderToString(<Card title="Hello" />));
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
If your handler returns a JSX element directly, Edge will automatically render it:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
app.get('/auto', (ctx) => <h1>Auto rendered</h1>);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Scheduled Tasks
|
|
232
|
+
|
|
233
|
+
Register a scheduled handler for Cron triggers.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
app.scheduled(async (event, env, ctx) => {
|
|
237
|
+
console.log('Cron executed:', event.cron);
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Then export it in your worker:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
export default {
|
|
245
|
+
fetch: (req, env, ctx) => app.fetch(req, env, ctx),
|
|
246
|
+
scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx)
|
|
247
|
+
};
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Client (Declarative Partial Updates)
|
|
253
|
+
|
|
254
|
+
Add the client script to your page (or include it via a CDN):
|
|
255
|
+
|
|
256
|
+
```html
|
|
257
|
+
<script src="https://cdn.example.com/edge-client.min.js"></script>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Then use HTML attributes to make elements fetch content asynchronously.
|
|
261
|
+
|
|
262
|
+
### Attributes
|
|
263
|
+
|
|
264
|
+
| Attribute | Description |
|
|
265
|
+
| --- | --- |
|
|
266
|
+
| `_get` | URL to fetch via GET |
|
|
267
|
+
| `_post` | URL to fetch via POST |
|
|
268
|
+
| `_target` | CSS selector for the container to replace. Use `"this"` to replace the element itself. |
|
|
269
|
+
| `_trigger` | Comma-separated list of events that trigger the fetch (default: `click`; `load` if `_target="this"`). Supported: `click`, `load`, `visible`, `intersect`, `submit`, etc. |
|
|
270
|
+
| `_form` | (with `_post`) ID of a form to serialize as the POST body (URL-encoded). |
|
|
271
|
+
| `_json` | (with `_post`) Comma-separated names of input fields to send as JSON. |
|
|
272
|
+
|
|
273
|
+
### Examples
|
|
274
|
+
|
|
275
|
+
**Load content on click:**
|
|
276
|
+
|
|
277
|
+
```html
|
|
278
|
+
<button _get="/more-posts" _target="#posts" _trigger="click">Load More</button>
|
|
279
|
+
<div id="posts"></div>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Auto-load on page load:**
|
|
283
|
+
|
|
284
|
+
```html
|
|
285
|
+
<div _get="/user-profile" _target="this"></div>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Post a form via AJAX:**
|
|
289
|
+
|
|
290
|
+
```html
|
|
291
|
+
<form id="contact-form">
|
|
292
|
+
<input name="email" />
|
|
293
|
+
<button _post="/submit" _form="contact-form" _target="#result">Submit</button>
|
|
294
|
+
</form>
|
|
295
|
+
<div id="result"></div>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Send JSON data:**
|
|
299
|
+
|
|
300
|
+
```html
|
|
301
|
+
<input name="username" />
|
|
302
|
+
<input name="password" />
|
|
303
|
+
<button _post="/login" _json="username,password" _target="#status">Login</button>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Load when element becomes visible (IntersectionObserver):**
|
|
307
|
+
|
|
308
|
+
```html
|
|
309
|
+
<div _get="/lazy-content" _target="this" _trigger="visible"></div>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Skeleton Loading
|
|
313
|
+
|
|
314
|
+
While the request is in progress, the target container is filled with a skeleton loader (three shimmer bars). On error, a "Retry" button is shown.
|
|
315
|
+
|
|
316
|
+
### Resource Injection
|
|
317
|
+
|
|
318
|
+
If the server response includes headers `x-css-required` or `x-js-required`, the client will automatically inject those resources (once per URL) into the page.
|
|
319
|
+
|
|
320
|
+
Example server route:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
app.get('/widget', (ctx) => {
|
|
324
|
+
ctx.headers.set('x-css-required', '["/widget.css"]');
|
|
325
|
+
ctx.headers.set('x-js-required', '["/widget.js"]');
|
|
326
|
+
return ctx.html('<div class="widget">...</div>');
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Configuration
|
|
333
|
+
|
|
334
|
+
### `wrangler.jsonc`
|
|
335
|
+
|
|
336
|
+
If you use authentication or rate limiting, you need KV namespaces. Create them first:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
wrangler kv namespace create AUTH_KV
|
|
340
|
+
wrangler kv namespace create RATE_LIMIT_KV
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Then wire up the returned IDs in your config:
|
|
344
|
+
|
|
345
|
+
```jsonc
|
|
346
|
+
{
|
|
347
|
+
"$schema": "./node_modules/wrangler/config-schema.json",
|
|
348
|
+
"name": "my-edge-app",
|
|
349
|
+
"main": "worker.js",
|
|
350
|
+
// Set this to today's date
|
|
351
|
+
"compatibility_date": "2026-09-06",
|
|
352
|
+
"observability": {
|
|
353
|
+
"enabled": true
|
|
354
|
+
},
|
|
355
|
+
"kv_namespaces": [
|
|
356
|
+
{ "binding": "AUTH_KV", "id": "your-auth-kv-id" },
|
|
357
|
+
{ "binding": "RATE_LIMIT_KV", "id": "your-ratelimit-kv-id" }
|
|
358
|
+
],
|
|
359
|
+
// Optional Cron triggers
|
|
360
|
+
"triggers": {
|
|
361
|
+
"crons": ["*/5 * * * *"]
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
<details>
|
|
367
|
+
<summary>Equivalent <code>wrangler.toml</code></summary>
|
|
368
|
+
|
|
369
|
+
```toml
|
|
370
|
+
name = "my-edge-app"
|
|
371
|
+
main = "worker.js"
|
|
372
|
+
compatibility_date = "2026-09-06"
|
|
373
|
+
|
|
374
|
+
[observability]
|
|
375
|
+
enabled = true
|
|
376
|
+
|
|
377
|
+
[[kv_namespaces]]
|
|
378
|
+
binding = "AUTH_KV"
|
|
379
|
+
id = "your-auth-kv-id"
|
|
380
|
+
|
|
381
|
+
[[kv_namespaces]]
|
|
382
|
+
binding = "RATE_LIMIT_KV"
|
|
383
|
+
id = "your-ratelimit-kv-id"
|
|
384
|
+
|
|
385
|
+
# Optional Cron triggers
|
|
386
|
+
[triggers]
|
|
387
|
+
crons = ["*/5 * * * *"]
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
</details>
|
|
391
|
+
|
|
392
|
+
Wrangler supports both `wrangler.jsonc` and `wrangler.toml` — they configure the same fields, just in different syntax. Don't keep both in the same project. Keep `compatibility_date` current (Cloudflare recommends staying within the last 30 days); see the [compatibility dates docs](https://developers.cloudflare.com/workers/configuration/compatibility-dates/).
|
|
393
|
+
|
|
394
|
+
### Environment Variables
|
|
395
|
+
|
|
396
|
+
The KV binding names are configurable on the Edge instance:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
app.authKvBinding = 'CUSTOM_AUTH_KV';
|
|
400
|
+
app.rateLimitKvBinding = 'CUSTOM_RATE_KV';
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Full Example
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
import { Edge, renderToString } from '@lengkapp/edge';
|
|
409
|
+
import { HomePage } from './Page.jsx';
|
|
410
|
+
|
|
411
|
+
const app = new Edge();
|
|
412
|
+
|
|
413
|
+
// Basic text
|
|
414
|
+
app.get('/', (ctx) => ctx.text('Hello from Edge!'));
|
|
415
|
+
|
|
416
|
+
// JSX component
|
|
417
|
+
app.get('/home', (ctx) => ctx.html(renderToString(<HomePage />)));
|
|
418
|
+
|
|
419
|
+
// JSON with route params
|
|
420
|
+
app.get('/users/:id', (ctx) => ctx.json({ userId: ctx.params.id }));
|
|
421
|
+
|
|
422
|
+
// POST JSON
|
|
423
|
+
app.post('/users', async (ctx) => {
|
|
424
|
+
const body = await ctx.req.json();
|
|
425
|
+
return ctx.json({ created: true, user: body }, 201);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Auth
|
|
429
|
+
app.get('/protected', { auth: true }, (ctx) => ctx.text('Authenticated'));
|
|
430
|
+
|
|
431
|
+
// Rate limiting
|
|
432
|
+
app.get('/limited', { rateLimit: { max: 5, window: 60 } }, (ctx) => ctx.text('Limited'));
|
|
433
|
+
|
|
434
|
+
// Caching
|
|
435
|
+
app.get('/cached', { cache: { ttl: 60 } }, (ctx) => ctx.text('Cached'));
|
|
436
|
+
|
|
437
|
+
// Cookies
|
|
438
|
+
app.get('/set-cookie', (ctx) => {
|
|
439
|
+
ctx.setCookie('session', 'abc123', { httpOnly: true, path: '/' });
|
|
440
|
+
return ctx.text('Cookie set');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
app.get('/get-cookie', (ctx) => {
|
|
444
|
+
const session = ctx.getCookie('session');
|
|
445
|
+
return ctx.text(`Cookie: ${session}`);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Scheduled
|
|
449
|
+
app.scheduled(async (event, env, ctx) => {
|
|
450
|
+
console.log('Cron:', event.cron);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
export default {
|
|
454
|
+
fetch: (req, env, ctx) => app.fetch(req, env, ctx),
|
|
455
|
+
scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx)
|
|
456
|
+
};
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Performance
|
|
462
|
+
|
|
463
|
+
@lengkapp/edge is inspired by [Hono](https://hono.dev) — one of the fastest Workers frameworks around — but ships with **zero dependencies**. A quick local benchmark shows it holding its own against both Hono builds (`hono` and `hono/tiny`) and a plain native `fetch` handler with no framework at all.
|
|
464
|
+
|
|
465
|
+
### Test setup
|
|
466
|
+
|
|
467
|
+
- **Tool:** `autocannon` — 10 connections, 10s run per route
|
|
468
|
+
- **Target:** local `wrangler dev` server
|
|
469
|
+
- **Routes:** `GET /text` (plain text) and `GET /json` (JSON)
|
|
470
|
+
- **Contenders:** `@lengkapp/edge`, `hono`, `hono/tiny`, and a native Workers handler with no framework
|
|
471
|
+
|
|
472
|
+
> This is a single local run, not a formal benchmark suite — treat the numbers as directional. Latency stdev was ±3–4 ms across all four, so differences smaller than that are within noise.
|
|
473
|
+
|
|
474
|
+
### Results
|
|
475
|
+
|
|
476
|
+
| Framework | Route | Avg Req/sec | Avg Latency | Requests | Data Read |
|
|
477
|
+
| --- | --- | --- | --- | --- | --- |
|
|
478
|
+
| **@lengkapp/edge** | `/text` | 503.1 | 19.38 ms | 5,000 / 10.03s | 1.38 MB |
|
|
479
|
+
| **@lengkapp/edge** | `/json` | **500.4** | 19.47 ms | 5,000 / 10.03s | 1.42 MB |
|
|
480
|
+
| Hono | `/text` | **506.0** | 19.27 ms | 5,000 / 10.03s | 455 kB |
|
|
481
|
+
| Hono | `/json` | 495.2 | 19.69 ms | 5,000 / 10.03s | 426 kB |
|
|
482
|
+
| Hono (`hono/tiny`) | `/text` | 505.5 | 19.28 ms | 5,000 / 10.03s | 455 kB |
|
|
483
|
+
| Hono (`hono/tiny`) | `/json` | 497.9 | 19.57 ms | 5,000 / 10.03s | 428 kB |
|
|
484
|
+
| Native Workers (no framework) | `/text` | 498.4 | 19.55 ms | 5,000 / 10.02s | 379 kB |
|
|
485
|
+
| Native Workers (no framework) | `/json` | 499.7 | 19.51 ms | 5,000 / 10.03s | 430 kB |
|
|
486
|
+
|
|
487
|
+
```mermaid
|
|
488
|
+
xychart-beta
|
|
489
|
+
title "Avg requests/sec — GET /text"
|
|
490
|
+
x-axis ["@lengkapp/edge", "Hono", "Hono (tiny)", "Native Workers"]
|
|
491
|
+
y-axis "Req/sec" 490 --> 510
|
|
492
|
+
bar [503.1, 506.0, 505.5, 498.4]
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
```mermaid
|
|
496
|
+
xychart-beta
|
|
497
|
+
title "Avg requests/sec — GET /json"
|
|
498
|
+
x-axis ["@lengkapp/edge", "Hono", "Hono (tiny)", "Native Workers"]
|
|
499
|
+
y-axis "Req/sec" 490 --> 505
|
|
500
|
+
bar [500.4, 495.2, 497.9, 499.7]
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Takeaways
|
|
504
|
+
|
|
505
|
+
- On `/json`, @lengkapp/edge posted the highest average throughput of the four, ahead of both Hono builds and the native handler.
|
|
506
|
+
- On `/text`, it landed within ~1% of `hono`/`hono/tiny` and ahead of the native baseline — effectively a tie once you account for run-to-run variance.
|
|
507
|
+
- All four sit in the same performance tier; the practical difference is that @lengkapp/edge gets there with **zero runtime dependencies**, matching `hono/tiny`'s footprint without having to choose a "tiny" build.
|
|
508
|
+
- @lengkapp/edge read more bytes per request than the other three in this run — worth profiling if minimal payload size matters for your use case.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## License
|
|
513
|
+
|
|
514
|
+
See [LICENSE](./LICENSE). © LengkApp — Yasir Haris
|