@ayepi/deno 0.1.0 → 0.2.0
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/README.md +1 -1
- package/ayepi-deno.md +224 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ This package ships dense, machine-oriented reference docs written for **AI codin
|
|
|
19
19
|
|
|
20
20
|
- [`ayepi-deno.md`](./ayepi-deno.md)
|
|
21
21
|
|
|
22
|
-
They
|
|
22
|
+
They ship with this package and also live in the [repo](https://github.com/ClickerMonkey/ayepi/tree/main/packages/deno).
|
|
23
23
|
|
|
24
24
|
## License
|
|
25
25
|
|
package/ayepi-deno.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
ayepi-deno.md — reference for `@ayepi/deno`, written for coding agents.
|
|
3
|
+
|
|
4
|
+
Copy this file into any project that depends on `@ayepi/deno` (e.g. into your repo's
|
|
5
|
+
`docs/` or `.claude/` directory) and reference it from your agents and slash commands.
|
|
6
|
+
It documents the public API, the patterns the package expects, and how it works under the
|
|
7
|
+
hood, with copy-pasteable examples. Keep it in sync with the installed package version.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
# `@ayepi/deno`
|
|
11
|
+
|
|
12
|
+
`@ayepi/deno` is a thin [Deno](https://deno.com) adapter that boots an
|
|
13
|
+
[`@ayepi/core`](./ayepi-core.md) `Server` on Deno's built-in HTTP and WebSocket
|
|
14
|
+
runtime. Deno is fetch-native and upgrades WebSockets with the built-in
|
|
15
|
+
`Deno.upgradeWebSocket`, so this adapter has **zero dependencies** — plain HTTP
|
|
16
|
+
requests go straight to `app.fetch(req)`, and an upgraded socket is wired to the
|
|
17
|
+
`app.ws.open` / `message` / `close` lifecycle hooks. Use it when your ayepi server
|
|
18
|
+
runs under Deno (Deploy, `deno run`, etc.). It does **not** define your API — you
|
|
19
|
+
build the `Server` with `@ayepi/core` and hand it to `serve`.
|
|
20
|
+
|
|
21
|
+
Import it under Deno via the `npm:` specifier (see the package README):
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { serve } from 'npm:@ayepi/deno'
|
|
25
|
+
import { server } from 'npm:@ayepi/core'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The `Deno` global is read at runtime, so this package typechecks under plain
|
|
29
|
+
`tsc` in a Node toolchain but must be **executed under Deno** (>= 1.40.0).
|
|
30
|
+
|
|
31
|
+
## Public API
|
|
32
|
+
|
|
33
|
+
The package exports exactly two symbols: the `serve` function and its
|
|
34
|
+
`ServeOptions` type. Nothing else is public.
|
|
35
|
+
|
|
36
|
+
### `serve(app, opts)`
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
export function serve(app: Server<AnySpec>, opts: ServeOptions): () => Promise<void>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Boots `app` on Deno's built-in HTTP + WebSocket server (`Deno.serve`). Throws
|
|
43
|
+
immediately if there is no global `Deno` (i.e. you are not running under Deno):
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
@ayepi/deno: not running under Deno (no global `Deno`)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- `app` — any `@ayepi/core` `Server` (`Server<AnySpec>`). See
|
|
50
|
+
[`ayepi-core.md`](./ayepi-core.md) for how to build one with `server(...)`.
|
|
51
|
+
- `opts` — a `ServeOptions` object (below).
|
|
52
|
+
|
|
53
|
+
**Returns** a `close()` function: `() => Promise<void>`. Calling it invokes the
|
|
54
|
+
underlying `Deno.serve` handle's `shutdown()` and resolves once the server has
|
|
55
|
+
finished shutting down. Use it for graceful shutdown.
|
|
56
|
+
|
|
57
|
+
### `ServeOptions`
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
export interface ServeOptions {
|
|
61
|
+
/** TCP port to listen on. */
|
|
62
|
+
readonly port: number
|
|
63
|
+
/** Interface to bind. */
|
|
64
|
+
readonly hostname?: string
|
|
65
|
+
/** Restrict WebSocket upgrades to this pathname (e.g. '/ws'). */
|
|
66
|
+
readonly path?: string
|
|
67
|
+
/** Called once the server is listening. */
|
|
68
|
+
readonly onListen?: (info: { port: number; hostname: string }) => void
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| Option | Type | Required | Behavior |
|
|
73
|
+
| ---------- | ----------------------------------------------------- | -------- | -------- |
|
|
74
|
+
| `port` | `number` | yes | TCP port passed to `Deno.serve`. Use `0` to let the OS pick a free port. |
|
|
75
|
+
| `hostname` | `string` | no | Interface to bind. Passed through to `Deno.serve`; Deno's default applies when omitted. |
|
|
76
|
+
| `path` | `string` | no | If set, only requests whose `URL.pathname` equals this value are upgraded to WebSocket; all other requests fall through to `app.fetch`. If omitted, **any** request carrying an `upgrade: websocket` header is upgraded, regardless of path. |
|
|
77
|
+
| `onListen` | `(info: { port: number; hostname: string }) => void` | no | Forwarded to `Deno.serve`'s `onListen`. Fires once when the server is listening; `info.port` reflects the actual bound port (useful with `port: 0`). |
|
|
78
|
+
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
### Minimal serve
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { serve } from 'npm:@ayepi/deno'
|
|
85
|
+
import { server, spec, endpoint, implement } from 'npm:@ayepi/core'
|
|
86
|
+
import { z } from 'npm:zod'
|
|
87
|
+
|
|
88
|
+
const api = spec({
|
|
89
|
+
endpoints: {
|
|
90
|
+
getUser: endpoint({
|
|
91
|
+
params: z.object({ id: z.string() }),
|
|
92
|
+
response: z.object({ id: z.string(), name: z.string() }),
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const app = server(api, [
|
|
98
|
+
implement(api).handlers({
|
|
99
|
+
getUser: ({ data }) => ({ id: data.id, name: `u-${data.id}` }),
|
|
100
|
+
}),
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
serve(app, { port: 3000 })
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Options: bind a host, restrict WS path, log on listen
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
serve(app, {
|
|
110
|
+
port: 8080,
|
|
111
|
+
hostname: '0.0.0.0',
|
|
112
|
+
path: '/ws', // only /ws upgrades; everything else → app.fetch
|
|
113
|
+
onListen: ({ hostname, port }) => {
|
|
114
|
+
console.log(`listening on http://${hostname}:${port}`)
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Ephemeral port
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
serve(app, {
|
|
123
|
+
port: 0, // OS picks a free port
|
|
124
|
+
onListen: ({ port }) => console.log('bound to', port),
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Graceful shutdown
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const close = serve(app, { port: 3000, path: '/ws' })
|
|
132
|
+
|
|
133
|
+
// e.g. on SIGINT/SIGTERM
|
|
134
|
+
Deno.addSignalListener('SIGINT', async () => {
|
|
135
|
+
await close() // resolves once the server has fully shut down
|
|
136
|
+
Deno.exit(0)
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### WebSocket
|
|
141
|
+
|
|
142
|
+
WebSocket handling is entirely automatic — there is no WS-specific API on the
|
|
143
|
+
adapter. Define your events/streams in the spec and connect from any client; the
|
|
144
|
+
adapter routes frames through `app.ws.*` for you:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// server: just enable WS upgrades on a path
|
|
148
|
+
serve(app, { port: 3000, path: '/ws' })
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// client (browser / Deno): connect to the same path
|
|
153
|
+
const ws = new WebSocket('ws://localhost:3000/ws')
|
|
154
|
+
// ayepi call frames are sent/received as JSON text frames; use the
|
|
155
|
+
// @ayepi/core client to drive this — see ayepi-core.md.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## How it works under the hood
|
|
159
|
+
|
|
160
|
+
`serve` is glue between Deno's runtime and the `@ayepi/core` `Server` interface
|
|
161
|
+
(`app.fetch` plus the `app.ws` lifecycle hooks). The full flow:
|
|
162
|
+
|
|
163
|
+
1. **Reads the `Deno` global** at call time. If absent, it throws
|
|
164
|
+
`@ayepi/deno: not running under Deno`.
|
|
165
|
+
2. **Registers one `Deno.serve` handler** with `{ port, hostname, onListen }`.
|
|
166
|
+
The `onListen` callback is forwarded straight through.
|
|
167
|
+
3. **Per-request branch.** The handler inspects the incoming `Request`:
|
|
168
|
+
- It is treated as a WebSocket upgrade when the `upgrade` header equals
|
|
169
|
+
`websocket` (case-insensitive) **and** (`opts.path` is unset **or**
|
|
170
|
+
`new URL(req.url).pathname === opts.path`).
|
|
171
|
+
- Otherwise the request is forwarded to `app.fetch(req)`, which returns the
|
|
172
|
+
`Promise<Response>` (this is the entire HTTP surface — REST endpoints,
|
|
173
|
+
streaming responses, OpenAPI, etc., all handled by core).
|
|
174
|
+
4. **WebSocket upgrade.** For an upgrade, it calls `Deno.upgradeWebSocket(req)`,
|
|
175
|
+
which yields `{ socket, response }`. The adapter returns `response` (the 101
|
|
176
|
+
switching-protocols response) and wires the native socket's events:
|
|
177
|
+
- `socket.onopen` → `conn = app.ws.open(send, req)`, where `send` writes a
|
|
178
|
+
frame back via `socket.send(frame)` — but only while
|
|
179
|
+
`socket.readyState === 1` (`WebSocket.OPEN`), so frames emitted after the
|
|
180
|
+
socket closes are silently dropped.
|
|
181
|
+
- `socket.onmessage` → `app.ws.message(conn, String(ev.data))`. The frame data
|
|
182
|
+
is coerced to a string; the returned promise is fire-and-forgotten (`void`).
|
|
183
|
+
- `socket.onclose` and `socket.onerror` → both call `app.ws.close(conn)`,
|
|
184
|
+
which tears down the connection's subscriptions in core. An error is treated
|
|
185
|
+
as a close.
|
|
186
|
+
- All four handlers guard on `conn` being non-null, so events that race ahead
|
|
187
|
+
of `onopen` are ignored.
|
|
188
|
+
5. **Streaming.** The adapter does nothing special for streaming — it returns the
|
|
189
|
+
`Response` from `app.fetch` verbatim. Streaming response bodies (e.g.
|
|
190
|
+
`ReadableStream`) are produced by core and streamed by Deno's HTTP server
|
|
191
|
+
natively.
|
|
192
|
+
6. **Shutdown.** The returned `close()` awaits the `Deno.serve` handle's
|
|
193
|
+
`shutdown()`.
|
|
194
|
+
|
|
195
|
+
## Gotchas / constraints
|
|
196
|
+
|
|
197
|
+
- **Must run under Deno.** Importing the package is fine anywhere, but calling
|
|
198
|
+
`serve` without a global `Deno` throws. There is no Node fallback — use the
|
|
199
|
+
Node adapter for Node.
|
|
200
|
+
- **`npm:` specifier.** Under Deno, import as `npm:@ayepi/deno` (and
|
|
201
|
+
`npm:@ayepi/core`), not a bare specifier, unless you have an import map.
|
|
202
|
+
- **`path` is an exact pathname match.** `path: '/ws'` matches only `/ws`, not
|
|
203
|
+
`/ws/` or `/ws/foo`. Query strings are ignored (only `URL.pathname` is
|
|
204
|
+
compared). Omit `path` to upgrade any path that sends an upgrade header.
|
|
205
|
+
- **Upgrade detection is header-based.** A request is upgraded purely on the
|
|
206
|
+
`upgrade: websocket` header (plus the optional path filter). A request to your
|
|
207
|
+
WS path *without* that header falls through to `app.fetch`.
|
|
208
|
+
- **Text frames only.** Inbound frame data is passed through `String(...)`; the
|
|
209
|
+
adapter assumes ayepi's JSON text-frame protocol. Binary frames are stringified,
|
|
210
|
+
which is not meaningful for this protocol.
|
|
211
|
+
- **Post-close sends are dropped, not errors.** Because `send` checks
|
|
212
|
+
`readyState === OPEN`, a late `app.ws` emit after the socket closed is a no-op
|
|
213
|
+
rather than a throw.
|
|
214
|
+
- **`onerror` closes the connection.** A socket error is treated identically to a
|
|
215
|
+
clean close (`app.ws.close(conn)`); there is no separate error callback exposed.
|
|
216
|
+
- **No CORS/middleware here.** Those live in `@ayepi/core`'s `server(...)` options
|
|
217
|
+
— this adapter is transport only.
|
|
218
|
+
|
|
219
|
+
## See also
|
|
220
|
+
|
|
221
|
+
- [`ayepi-core.md`](./ayepi-core.md) — the actual API: `spec`, `endpoint`,
|
|
222
|
+
`implement`, `server`, the `Server` interface (`fetch`, `ws`, `emit`,
|
|
223
|
+
`openapi`, `asyncapi`), and the WebSocket call-frame protocol. This adapter
|
|
224
|
+
only transports what core defines.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ayepi/deno",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Deno adapter for @ayepi/core — fetch-native HTTP + native WebSocket, zero dependencies",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"type": "module",
|
|
19
19
|
"sideEffects": false,
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"ayepi-*.md"
|
|
22
23
|
],
|
|
23
24
|
"exports": {
|
|
24
25
|
".": {
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"deno": ">=1.40.0"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
|
-
"@ayepi/core": "^0.
|
|
41
|
+
"@ayepi/core": "^0.2.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@vitest/coverage-v8": "^2.1.8",
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"tsdown": "^0.12.0",
|
|
46
47
|
"vitest": "^2.1.8",
|
|
47
48
|
"zod": "^4.4.3",
|
|
48
|
-
"@ayepi/core": "0.
|
|
49
|
+
"@ayepi/core": "0.2.0"
|
|
49
50
|
},
|
|
50
51
|
"keywords": [
|
|
51
52
|
"ayepi",
|