@nifrajs/core 0.1.0-alpha.1
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 +21 -0
- package/README.md +35 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/brand.d.ts +11 -0
- package/dist/internal/brand.d.ts.map +1 -0
- package/dist/internal/brand.js +10 -0
- package/dist/internal/brand.js.map +1 -0
- package/dist/internal/secret.d.ts +9 -0
- package/dist/internal/secret.d.ts.map +1 -0
- package/dist/internal/secret.js +19 -0
- package/dist/internal/secret.js.map +1 -0
- package/dist/router/router.d.ts +61 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/router.js +324 -0
- package/dist/router/router.js.map +1 -0
- package/dist/schema/standard.d.ts +46 -0
- package/dist/schema/standard.d.ts.map +1 -0
- package/dist/schema/standard.js +20 -0
- package/dist/schema/standard.js.map +1 -0
- package/dist/seo.d.ts +48 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +109 -0
- package/dist/seo.js.map +1 -0
- package/dist/server/body.d.ts +39 -0
- package/dist/server/body.d.ts.map +1 -0
- package/dist/server/body.js +85 -0
- package/dist/server/body.js.map +1 -0
- package/dist/server/context.d.ts +111 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +2 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/contract.d.ts +142 -0
- package/dist/server/contract.d.ts.map +1 -0
- package/dist/server/contract.js +59 -0
- package/dist/server/contract.js.map +1 -0
- package/dist/server/cookies.d.ts +41 -0
- package/dist/server/cookies.d.ts.map +1 -0
- package/dist/server/cookies.js +181 -0
- package/dist/server/cookies.js.map +1 -0
- package/dist/server/logger.d.ts +49 -0
- package/dist/server/logger.d.ts.map +1 -0
- package/dist/server/logger.js +125 -0
- package/dist/server/logger.js.map +1 -0
- package/dist/server/registry.d.ts +46 -0
- package/dist/server/registry.d.ts.map +1 -0
- package/dist/server/registry.js +2 -0
- package/dist/server/registry.js.map +1 -0
- package/dist/server/server.d.ts +528 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +1660 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/sse.d.ts +54 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +126 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/server/webhook.d.ts +42 -0
- package/dist/server/webhook.d.ts.map +1 -0
- package/dist/server/webhook.js +149 -0
- package/dist/server/webhook.js.map +1 -0
- package/dist/server/websocket.d.ts +152 -0
- package/dist/server/websocket.d.ts.map +1 -0
- package/dist/server/websocket.js +178 -0
- package/dist/server/websocket.js.map +1 -0
- package/dist/server/ws-bun.d.ts +27 -0
- package/dist/server/ws-bun.d.ts.map +1 -0
- package/dist/server/ws-bun.js +82 -0
- package/dist/server/ws-bun.js.map +1 -0
- package/dist/server/ws-hook.d.ts +36 -0
- package/dist/server/ws-hook.d.ts.map +1 -0
- package/dist/server/ws-hook.js +18 -0
- package/dist/server/ws-hook.js.map +1 -0
- package/dist/ws.d.ts +19 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +26 -0
- package/dist/ws.js.map +1 -0
- package/package.json +63 -0
- package/src/errors.ts +41 -0
- package/src/index.ts +115 -0
- package/src/internal/brand.ts +11 -0
- package/src/internal/secret.ts +24 -0
- package/src/router/router.ts +429 -0
- package/src/schema/standard.ts +76 -0
- package/src/seo.ts +155 -0
- package/src/server/body.ts +89 -0
- package/src/server/context.ts +124 -0
- package/src/server/contract.ts +209 -0
- package/src/server/cookies.ts +198 -0
- package/src/server/logger.ts +167 -0
- package/src/server/registry.ts +63 -0
- package/src/server/server.ts +2479 -0
- package/src/server/sse.ts +161 -0
- package/src/server/webhook.ts +207 -0
- package/src/server/websocket.ts +308 -0
- package/src/server/ws-bun.ts +106 -0
- package/src/server/ws-hook.ts +53 -0
- package/src/ws.ts +40 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun WebSocket plumbing — the `websocket` dispatcher config `listen()` hands to Bun's serve when
|
|
3
|
+
* the app has WS routes. Lives behind the `@nifrajs/core/ws` subpath (via `ws-hook.ts`) so the base
|
|
4
|
+
* bundle of a no-WebSocket app never ships it; `server.ts` only imports the **types** from here
|
|
5
|
+
* (erased). No `Bun.*` calls here — the runtime seam stays in `server.ts` (see runtime-boundary.test).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { NifraWebSocket, TopicRegistry, WebSocketData, WebSocketHandler } from "./websocket.ts"
|
|
9
|
+
import type { BunWsHandlers } from "./ws-hook.ts"
|
|
10
|
+
|
|
11
|
+
/** Per-connection state Bun's `websocket` callbacks read via `ws.data`: the matched handler, the
|
|
12
|
+
* `upgrade()`-seeded user data, and the memoized portable {@link NifraWebSocket} wrapper. */
|
|
13
|
+
export interface BunWsData {
|
|
14
|
+
readonly handler: WebSocketHandler
|
|
15
|
+
data: unknown
|
|
16
|
+
nifra?: NifraWebSocket
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Structural view of Bun's `ServerWebSocket` — keeps `Bun.*` types out of the public `.d.ts`
|
|
20
|
+
* (`listen()` casts for the same reason). The real socket satisfies this. */
|
|
21
|
+
export interface BunSocket {
|
|
22
|
+
send(data: string | ArrayBufferView | ArrayBuffer, compress?: boolean): number
|
|
23
|
+
close(code?: number, reason?: string): void
|
|
24
|
+
readonly readyState: number
|
|
25
|
+
// readonly: we never reassign `ws.data` wholesale, only mutate its fields (`.nifra`, `.data`).
|
|
26
|
+
readonly data: BunWsData
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Normalize a received binary frame to `Uint8Array` (Bun hands a `Buffer`, already one; an
|
|
30
|
+
* `ArrayBuffer` is wrapped). Text frames stay `string`. */
|
|
31
|
+
function toBinaryData(message: ArrayBuffer | Uint8Array): Uint8Array {
|
|
32
|
+
return message instanceof Uint8Array ? message : new Uint8Array(message)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Wrap a Bun `ServerWebSocket` as the portable {@link NifraWebSocket} handed to WS callbacks. */
|
|
36
|
+
function wrapBunSocket(raw: BunSocket, topics: TopicRegistry): NifraWebSocket {
|
|
37
|
+
const ws: NifraWebSocket = {
|
|
38
|
+
send: (data) => {
|
|
39
|
+
raw.send(data)
|
|
40
|
+
},
|
|
41
|
+
close: (code, reason) => raw.close(code, reason),
|
|
42
|
+
get readyState() {
|
|
43
|
+
return raw.readyState
|
|
44
|
+
},
|
|
45
|
+
subscribe: (topic) => topics.subscribe(topic, ws),
|
|
46
|
+
unsubscribe: (topic) => topics.unsubscribe(topic, ws),
|
|
47
|
+
get data() {
|
|
48
|
+
return raw.data.data
|
|
49
|
+
},
|
|
50
|
+
set data(value) {
|
|
51
|
+
raw.data.data = value
|
|
52
|
+
},
|
|
53
|
+
raw,
|
|
54
|
+
}
|
|
55
|
+
return ws
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function reportWsError(error: unknown, ws: NifraWebSocket, handler: WebSocketHandler): void {
|
|
59
|
+
if (handler.error === undefined) return
|
|
60
|
+
try {
|
|
61
|
+
const result = handler.error(ws, error)
|
|
62
|
+
if (result instanceof Promise) result.catch(() => {}) // a throwing error handler is the last resort
|
|
63
|
+
} catch {
|
|
64
|
+
/* swallow: the error handler itself failed; nothing left to do but not crash */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Run a WS lifecycle callback, routing a sync throw or async rejection to `handler.error` so a
|
|
69
|
+
* failing callback never crashes the socket loop. */
|
|
70
|
+
function dispatchWsCallback(
|
|
71
|
+
call: () => void | Promise<void>,
|
|
72
|
+
ws: NifraWebSocket,
|
|
73
|
+
handler: WebSocketHandler,
|
|
74
|
+
): void {
|
|
75
|
+
try {
|
|
76
|
+
const result = call()
|
|
77
|
+
if (result instanceof Promise) result.catch((e: unknown) => reportWsError(e, ws, handler))
|
|
78
|
+
} catch (error) {
|
|
79
|
+
reportWsError(error, ws, handler)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The shared Bun `websocket` dispatcher config for one app — each connection's
|
|
84
|
+
* `ws.data.handler` is the matched route's handler, set by `server.upgrade`. */
|
|
85
|
+
export function createBunWsHandlers(topics: TopicRegistry): BunWsHandlers {
|
|
86
|
+
return {
|
|
87
|
+
open: (raw) => {
|
|
88
|
+
const ws = raw as BunSocket
|
|
89
|
+
const nifra = wrapBunSocket(ws, topics)
|
|
90
|
+
ws.data.nifra = nifra
|
|
91
|
+
dispatchWsCallback(() => ws.data.handler.open?.(nifra), nifra, ws.data.handler)
|
|
92
|
+
},
|
|
93
|
+
message: (raw, message) => {
|
|
94
|
+
const ws = raw as BunSocket
|
|
95
|
+
const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics)
|
|
96
|
+
const data: WebSocketData = typeof message === "string" ? message : toBinaryData(message)
|
|
97
|
+
dispatchWsCallback(() => ws.data.handler.message?.(nifra, data), nifra, ws.data.handler)
|
|
98
|
+
},
|
|
99
|
+
close: (raw, code, reason) => {
|
|
100
|
+
const ws = raw as BunSocket
|
|
101
|
+
const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics)
|
|
102
|
+
topics.unsubscribeAll(nifra) // drop topic subscriptions so the registry never holds a dead socket
|
|
103
|
+
dispatchWsCallback(() => ws.data.handler.close?.(nifra, code, reason), nifra, ws.data.handler)
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy WebSocket runtime seam. `server.ts` never imports the WS implementation statically — the
|
|
3
|
+
* `@nifrajs/core/ws` subpath entry registers it here on import. That keeps the WS dispatcher, pub/sub
|
|
4
|
+
* registry, Bun socket plumbing, and message-schema validation out of the base server bundle for
|
|
5
|
+
* apps that never call `app.ws()` (~1.5 KB gzipped on the minimal-app benchmark).
|
|
6
|
+
*
|
|
7
|
+
* Only type imports below — this module must stay weightless, it is always in the base bundle.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
NifraWebSocket,
|
|
12
|
+
StandardWebSocket,
|
|
13
|
+
TopicRegistry,
|
|
14
|
+
WebSocketHandler,
|
|
15
|
+
} from "./websocket.ts"
|
|
16
|
+
|
|
17
|
+
/** The Bun serve `websocket` callback trio, closed over one app's `TopicRegistry`. The `ws`
|
|
18
|
+
* params are `unknown` so `Bun.*` types never leak into the public `.d.ts`; the implementation
|
|
19
|
+
* narrows to its structural `BunSocket` view. */
|
|
20
|
+
export interface BunWsHandlers {
|
|
21
|
+
open(ws: unknown): void
|
|
22
|
+
message(ws: unknown, message: string | ArrayBuffer | Uint8Array): void
|
|
23
|
+
close(ws: unknown, code: number, reason: string): void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** What `@nifrajs/core/ws` registers: everything `server.ts` and `toFetchHandler` need at runtime. */
|
|
27
|
+
export interface WsRuntime {
|
|
28
|
+
/** `wrapWebSocketMessageValidation` — applied once at `app.ws()` registration. */
|
|
29
|
+
wrapHandler(handler: WebSocketHandler): WebSocketHandler
|
|
30
|
+
/** One in-process pub/sub registry per app (backs `ws.subscribe` + `app.publish`). */
|
|
31
|
+
createTopics(): TopicRegistry
|
|
32
|
+
/** The Bun `websocket` config for `listen()` when the app has WS routes. */
|
|
33
|
+
bunHandlers(topics: TopicRegistry): BunWsHandlers
|
|
34
|
+
/** `attachWebSocket` — wires a standard server socket (Workers `WebSocketPair`) to a handler. */
|
|
35
|
+
attach(
|
|
36
|
+
socket: StandardWebSocket,
|
|
37
|
+
handler: WebSocketHandler,
|
|
38
|
+
data: unknown,
|
|
39
|
+
options: { openNow: boolean; pubsub: TopicRegistry },
|
|
40
|
+
): NifraWebSocket
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let runtime: WsRuntime | undefined
|
|
44
|
+
|
|
45
|
+
/** Called by `@nifrajs/core/ws` on import. Last registration wins (idempotent in practice — there is
|
|
46
|
+
* one implementation; double-import is a no-op re-set of the same functions). */
|
|
47
|
+
export function setWsRuntime(impl: WsRuntime): void {
|
|
48
|
+
runtime = impl
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getWsRuntime(): WsRuntime | undefined {
|
|
52
|
+
return runtime
|
|
53
|
+
}
|
package/src/ws.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket runtime for `@nifrajs/core` — import once at your server entry to enable `app.ws()`:
|
|
3
|
+
*
|
|
4
|
+
* import "@nifrajs/core/ws"
|
|
5
|
+
*
|
|
6
|
+
* const app = server().ws("/chat", { message: (ws, data) => ws.send(data) })
|
|
7
|
+
*
|
|
8
|
+
* Why a subpath: an app that never uses WebSockets shouldn't ship the WS dispatcher, pub/sub
|
|
9
|
+
* registry, and Bun socket plumbing in its server bundle (~1.5 KB gzipped on the minimal-app size
|
|
10
|
+
* benchmark). Importing this module registers the runtime with the core server (idempotent);
|
|
11
|
+
* calling `app.ws()` without it fails loud at boot with a `WS_RUNTIME_MISSING` error pointing here.
|
|
12
|
+
*
|
|
13
|
+
* Serving adapters that wire standard sockets themselves (`@nifrajs/workers`) import
|
|
14
|
+
* `attachWebSocket` / `TopicRegistry` from here too.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
attachWebSocket,
|
|
19
|
+
TopicRegistry,
|
|
20
|
+
wrapWebSocketMessageValidation,
|
|
21
|
+
} from "./server/websocket.ts"
|
|
22
|
+
import { createBunWsHandlers } from "./server/ws-bun.ts"
|
|
23
|
+
import { setWsRuntime } from "./server/ws-hook.ts"
|
|
24
|
+
|
|
25
|
+
setWsRuntime({
|
|
26
|
+
wrapHandler: wrapWebSocketMessageValidation,
|
|
27
|
+
createTopics: () => new TopicRegistry(),
|
|
28
|
+
bunHandlers: createBunWsHandlers,
|
|
29
|
+
attach: attachWebSocket,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export type {
|
|
33
|
+
NifraWebSocket,
|
|
34
|
+
StandardWebSocket,
|
|
35
|
+
WebSocketContext,
|
|
36
|
+
WebSocketData,
|
|
37
|
+
WebSocketHandler,
|
|
38
|
+
WebSocketUpgradeOutcome,
|
|
39
|
+
} from "./server/websocket.ts"
|
|
40
|
+
export { attachWebSocket, TopicRegistry, wrapWebSocketMessageValidation }
|