@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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/errors.d.ts +21 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/errors.js +26 -0
  6. package/dist/errors.js.map +1 -0
  7. package/dist/index.d.ts +26 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +21 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/internal/brand.d.ts +11 -0
  12. package/dist/internal/brand.d.ts.map +1 -0
  13. package/dist/internal/brand.js +10 -0
  14. package/dist/internal/brand.js.map +1 -0
  15. package/dist/internal/secret.d.ts +9 -0
  16. package/dist/internal/secret.d.ts.map +1 -0
  17. package/dist/internal/secret.js +19 -0
  18. package/dist/internal/secret.js.map +1 -0
  19. package/dist/router/router.d.ts +61 -0
  20. package/dist/router/router.d.ts.map +1 -0
  21. package/dist/router/router.js +324 -0
  22. package/dist/router/router.js.map +1 -0
  23. package/dist/schema/standard.d.ts +46 -0
  24. package/dist/schema/standard.d.ts.map +1 -0
  25. package/dist/schema/standard.js +20 -0
  26. package/dist/schema/standard.js.map +1 -0
  27. package/dist/seo.d.ts +48 -0
  28. package/dist/seo.d.ts.map +1 -0
  29. package/dist/seo.js +109 -0
  30. package/dist/seo.js.map +1 -0
  31. package/dist/server/body.d.ts +39 -0
  32. package/dist/server/body.d.ts.map +1 -0
  33. package/dist/server/body.js +85 -0
  34. package/dist/server/body.js.map +1 -0
  35. package/dist/server/context.d.ts +111 -0
  36. package/dist/server/context.d.ts.map +1 -0
  37. package/dist/server/context.js +2 -0
  38. package/dist/server/context.js.map +1 -0
  39. package/dist/server/contract.d.ts +142 -0
  40. package/dist/server/contract.d.ts.map +1 -0
  41. package/dist/server/contract.js +59 -0
  42. package/dist/server/contract.js.map +1 -0
  43. package/dist/server/cookies.d.ts +41 -0
  44. package/dist/server/cookies.d.ts.map +1 -0
  45. package/dist/server/cookies.js +181 -0
  46. package/dist/server/cookies.js.map +1 -0
  47. package/dist/server/logger.d.ts +49 -0
  48. package/dist/server/logger.d.ts.map +1 -0
  49. package/dist/server/logger.js +125 -0
  50. package/dist/server/logger.js.map +1 -0
  51. package/dist/server/registry.d.ts +46 -0
  52. package/dist/server/registry.d.ts.map +1 -0
  53. package/dist/server/registry.js +2 -0
  54. package/dist/server/registry.js.map +1 -0
  55. package/dist/server/server.d.ts +528 -0
  56. package/dist/server/server.d.ts.map +1 -0
  57. package/dist/server/server.js +1660 -0
  58. package/dist/server/server.js.map +1 -0
  59. package/dist/server/sse.d.ts +54 -0
  60. package/dist/server/sse.d.ts.map +1 -0
  61. package/dist/server/sse.js +126 -0
  62. package/dist/server/sse.js.map +1 -0
  63. package/dist/server/webhook.d.ts +42 -0
  64. package/dist/server/webhook.d.ts.map +1 -0
  65. package/dist/server/webhook.js +149 -0
  66. package/dist/server/webhook.js.map +1 -0
  67. package/dist/server/websocket.d.ts +152 -0
  68. package/dist/server/websocket.d.ts.map +1 -0
  69. package/dist/server/websocket.js +178 -0
  70. package/dist/server/websocket.js.map +1 -0
  71. package/dist/server/ws-bun.d.ts +27 -0
  72. package/dist/server/ws-bun.d.ts.map +1 -0
  73. package/dist/server/ws-bun.js +82 -0
  74. package/dist/server/ws-bun.js.map +1 -0
  75. package/dist/server/ws-hook.d.ts +36 -0
  76. package/dist/server/ws-hook.d.ts.map +1 -0
  77. package/dist/server/ws-hook.js +18 -0
  78. package/dist/server/ws-hook.js.map +1 -0
  79. package/dist/ws.d.ts +19 -0
  80. package/dist/ws.d.ts.map +1 -0
  81. package/dist/ws.js +26 -0
  82. package/dist/ws.js.map +1 -0
  83. package/package.json +63 -0
  84. package/src/errors.ts +41 -0
  85. package/src/index.ts +115 -0
  86. package/src/internal/brand.ts +11 -0
  87. package/src/internal/secret.ts +24 -0
  88. package/src/router/router.ts +429 -0
  89. package/src/schema/standard.ts +76 -0
  90. package/src/seo.ts +155 -0
  91. package/src/server/body.ts +89 -0
  92. package/src/server/context.ts +124 -0
  93. package/src/server/contract.ts +209 -0
  94. package/src/server/cookies.ts +198 -0
  95. package/src/server/logger.ts +167 -0
  96. package/src/server/registry.ts +63 -0
  97. package/src/server/server.ts +2479 -0
  98. package/src/server/sse.ts +161 -0
  99. package/src/server/webhook.ts +207 -0
  100. package/src/server/websocket.ts +308 -0
  101. package/src/server/ws-bun.ts +106 -0
  102. package/src/server/ws-hook.ts +53 -0
  103. 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 }