@lovrozagar/honey 0.1.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.
Files changed (131) hide show
  1. package/README.md +51 -0
  2. package/docs/api-ref/go.md +399 -0
  3. package/docs/api-ref/python.md +399 -0
  4. package/docs/api-ref/rust.md +399 -0
  5. package/docs/api-ref/typescript.md +399 -0
  6. package/docs/invalidate-spec.md +303 -0
  7. package/docs/sdk.md +722 -0
  8. package/package.json +120 -0
  9. package/src/accepts.ts +78 -0
  10. package/src/body-limit.ts +100 -0
  11. package/src/build/index.ts +174 -0
  12. package/src/cli-go/auth.go +15 -0
  13. package/src/cli-go/config.go +165 -0
  14. package/src/cli-go/errors.go +57 -0
  15. package/src/cli-go/multipart.go +55 -0
  16. package/src/cli-go/output.go +109 -0
  17. package/src/cli-go/root.go +30 -0
  18. package/src/cli-go/stream.go +88 -0
  19. package/src/cli-go/version.go +5 -0
  20. package/src/cli.ts +204 -0
  21. package/src/client/error.ts +27 -0
  22. package/src/client/http.ts +502 -0
  23. package/src/client/index.ts +127 -0
  24. package/src/client/sdk.ts +265 -0
  25. package/src/client/sse.ts +102 -0
  26. package/src/client/types.ts +211 -0
  27. package/src/client/ws.ts +115 -0
  28. package/src/client-go/errors.go +207 -0
  29. package/src/client-go/invalidation.go +373 -0
  30. package/src/client-go/realtime.go +317 -0
  31. package/src/client-go/result.go +20 -0
  32. package/src/client-go/runtime.go +413 -0
  33. package/src/client-go/sse.go +149 -0
  34. package/src/client-go/transport.go +452 -0
  35. package/src/client-go/ws.go +104 -0
  36. package/src/client-mcp/README.md +10 -0
  37. package/src/client-mcp/server.ts +94 -0
  38. package/src/client-mcp/types.ts +19 -0
  39. package/src/client-python/_errors.py +115 -0
  40. package/src/client-python/_invalidation.py +578 -0
  41. package/src/client-python/_realtime.py +156 -0
  42. package/src/client-python/_runtime.py +469 -0
  43. package/src/client-python/_sse.py +99 -0
  44. package/src/client-python/_transport.py +335 -0
  45. package/src/client-python/_ws.py +131 -0
  46. package/src/client-python/py.typed +0 -0
  47. package/src/client-rust/errors.rs +200 -0
  48. package/src/client-rust/invalidation.rs +441 -0
  49. package/src/client-rust/invalidation_sync.rs +306 -0
  50. package/src/client-rust/realtime.rs +695 -0
  51. package/src/client-rust/result.rs +22 -0
  52. package/src/client-rust/runtime.rs +547 -0
  53. package/src/client-rust/runtime_sync.rs +452 -0
  54. package/src/client-rust/sse.rs +167 -0
  55. package/src/client-rust/ws.rs +143 -0
  56. package/src/cloudflare/realtime.ts +236 -0
  57. package/src/codegen-go-cli.ts +1180 -0
  58. package/src/codegen-go.ts +1010 -0
  59. package/src/codegen-hash.ts +8 -0
  60. package/src/codegen-ir.ts +517 -0
  61. package/src/codegen-loaders.ts +53 -0
  62. package/src/codegen-mcp.ts +364 -0
  63. package/src/codegen-python.ts +1028 -0
  64. package/src/codegen-rust.ts +1652 -0
  65. package/src/codegen-sanitize.ts +67 -0
  66. package/src/codegen-schema-naming.ts +146 -0
  67. package/src/codegen.ts +5015 -0
  68. package/src/context.ts +210 -0
  69. package/src/cookie-sign.ts +58 -0
  70. package/src/cookie.ts +89 -0
  71. package/src/cors.ts +96 -0
  72. package/src/crypto.ts +19 -0
  73. package/src/csrf.ts +54 -0
  74. package/src/curl-logger.ts +235 -0
  75. package/src/detect-runtime.ts +19 -0
  76. package/src/error.ts +52 -0
  77. package/src/errors.ts +87 -0
  78. package/src/etag.ts +74 -0
  79. package/src/feature-load.ts +23 -0
  80. package/src/go-type-emitter.ts +404 -0
  81. package/src/honey-response.ts +354 -0
  82. package/src/i18n-slot.ts +20 -0
  83. package/src/i18n.ts +234 -0
  84. package/src/index.ts +3843 -0
  85. package/src/init.ts +149 -0
  86. package/src/input.ts +8 -0
  87. package/src/ip-restrict.ts +192 -0
  88. package/src/logger.ts +149 -0
  89. package/src/middleware.ts +199 -0
  90. package/src/node-request.ts +240 -0
  91. package/src/node.ts +212 -0
  92. package/src/openapi/register.ts +19 -0
  93. package/src/openapi/scalar.ts +33 -0
  94. package/src/openapi/spec-factory.ts +46 -0
  95. package/src/openapi/spec.ts +31 -0
  96. package/src/openapi/swagger.ts +34 -0
  97. package/src/plugin.ts +771 -0
  98. package/src/powered-by.ts +17 -0
  99. package/src/pretty-json.ts +39 -0
  100. package/src/proxy.ts +155 -0
  101. package/src/python-type-emitter.ts +144 -0
  102. package/src/realtime/buffer.ts +101 -0
  103. package/src/realtime/bus.ts +93 -0
  104. package/src/realtime/longpoll.ts +148 -0
  105. package/src/realtime/protocol.ts +128 -0
  106. package/src/realtime/route.ts +104 -0
  107. package/src/realtime/runtime.ts +430 -0
  108. package/src/request-id.ts +23 -0
  109. package/src/request-to-curl.ts +34 -0
  110. package/src/response.ts +414 -0
  111. package/src/rust-type-emitter.ts +412 -0
  112. package/src/secure-headers.ts +69 -0
  113. package/src/serve-register.ts +8 -0
  114. package/src/serve-slot.ts +20 -0
  115. package/src/serve.ts +141 -0
  116. package/src/server-timing.ts +66 -0
  117. package/src/static.ts +60 -0
  118. package/src/telemetry/otel.ts +139 -0
  119. package/src/testing.ts +114 -0
  120. package/src/timeout.ts +36 -0
  121. package/src/tree.ts +464 -0
  122. package/src/ts-type-emitter.ts +106 -0
  123. package/src/type-emitter.ts +524 -0
  124. package/src/type-extractor.ts +749 -0
  125. package/src/types.ts +638 -0
  126. package/src/validation.ts +300 -0
  127. package/src/ws/bun.ts +89 -0
  128. package/src/ws/cloudflare.ts +133 -0
  129. package/src/ws/deno.ts +109 -0
  130. package/src/ws/node.ts +161 -0
  131. package/src/yaml.ts +80 -0
@@ -0,0 +1,300 @@
1
+ import { HoneyError } from "./error.ts"
2
+ import type {
3
+ FieldError,
4
+ InputSchemaEntry,
5
+ InputSchemasDef,
6
+ NormalizedIssue,
7
+ StandardSchemaIssue,
8
+ StandardSchemaLike,
9
+ } from "./types.ts"
10
+ import { EK, SK } from "./types.ts"
11
+
12
+ type ValidatedInput = {
13
+ cookies?: unknown
14
+ form?: unknown
15
+ headers?: unknown
16
+ json?: unknown
17
+ params?: unknown
18
+ search?: unknown
19
+ }
20
+
21
+ const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"])
22
+
23
+ const CODE_MAP: Record<string, string> = {
24
+ invalid_format: "field_invalid_format",
25
+ invalid_value: "field_invalid_enum",
26
+ not_multiple: "field_not_multiple_of",
27
+ required: "field_required",
28
+ too_big: "field_too_big",
29
+ too_long: "field_too_long",
30
+ too_short: "field_too_short",
31
+ too_small: "field_too_small",
32
+ unrecognized: "field_unrecognized_keys",
33
+ }
34
+
35
+ export function mapNormalizedCode(code: string): string {
36
+ return CODE_MAP[code] ?? "field_invalid"
37
+ }
38
+
39
+ export function selectParser(contentType: string | null): "form" | "json" | null {
40
+ if (contentType === null) return null
41
+ if (contentType.startsWith("application/json")) return "json"
42
+ if (contentType.startsWith("multipart/form-data")) return "form"
43
+ if (contentType.startsWith("application/x-www-form-urlencoded")) return "form"
44
+ return null
45
+ }
46
+
47
+ function tryDecodeCookieValue(value: string): string {
48
+ try {
49
+ return decodeURIComponent(value)
50
+ } catch {
51
+ return value
52
+ }
53
+ }
54
+
55
+ export function parseCookies(header: string): Record<string, string> {
56
+ const result: Record<string, string> = {}
57
+ if (header.length === 0) return result
58
+ for (const pair of header.split(";")) {
59
+ const trimmed = pair.trim()
60
+ const eqIdx = trimmed.indexOf("=")
61
+ if (eqIdx === -1) continue
62
+ const name = trimmed.slice(0, eqIdx)
63
+ if (DANGEROUS_KEYS.has(name)) continue
64
+ let value = trimmed.slice(eqIdx + 1)
65
+ if (value.indexOf("%") !== -1) value = tryDecodeCookieValue(value)
66
+ /* RFC 6265: strip surrounding double quotes */
67
+ if (
68
+ value.length >= 2 &&
69
+ value.charCodeAt(0) === 34 &&
70
+ value.charCodeAt(value.length - 1) === 34
71
+ ) {
72
+ value = value.slice(1, -1)
73
+ }
74
+ result[name] = value
75
+ }
76
+ return result
77
+ }
78
+
79
+ function toPropertyKey(segment: PropertyKey | { readonly key: PropertyKey }): PropertyKey {
80
+ if (typeof segment === "object" && segment !== null && "key" in segment) {
81
+ return segment.key
82
+ }
83
+ return segment
84
+ }
85
+
86
+ type VendorIssue = StandardSchemaIssue & { code?: string; type?: string }
87
+
88
+ function extractVendorCode(issue: StandardSchemaIssue, vendor: string): string {
89
+ const vi = issue as VendorIssue
90
+ if (vendor === "zod" || vendor === "arktype") {
91
+ return typeof vi.code === "string" ? vi.code : "unknown"
92
+ }
93
+ if (vendor === "valibot") {
94
+ return typeof vi.type === "string" ? vi.type : "unknown"
95
+ }
96
+ return "unknown"
97
+ }
98
+
99
+ export function normalizeIssues(
100
+ issues: ReadonlyArray<StandardSchemaIssue>,
101
+ vendor: string,
102
+ ): NormalizedIssue[] {
103
+ return issues.map((issue) => {
104
+ const path = issue.path ? issue.path.map(toPropertyKey) : []
105
+ return {
106
+ code: extractVendorCode(issue, vendor),
107
+ message: issue.message,
108
+ meta: {
109
+ field: path.at(-1)?.toString(),
110
+ },
111
+ path,
112
+ }
113
+ })
114
+ }
115
+
116
+ export function issuesToFieldErrors(
117
+ issues: NormalizedIssue[],
118
+ prefix: string,
119
+ ): Record<string, FieldError[]> {
120
+ const fields: Record<string, FieldError[]> = {}
121
+ for (const issue of issues) {
122
+ const fieldName = issue.path.at(-1)?.toString() ?? "unknown"
123
+ const fullPath = `${prefix}.${issue.path.map(String).join(".")}`
124
+ const error: FieldError = {
125
+ error_key: mapNormalizedCode(issue.code),
126
+ message: issue.message,
127
+ path: fullPath,
128
+ }
129
+ if (fields[fieldName] === undefined) {
130
+ fields[fieldName] = []
131
+ }
132
+ fields[fieldName].push(error)
133
+ }
134
+ return fields
135
+ }
136
+
137
+ async function runSchema(
138
+ schema: StandardSchemaLike,
139
+ data: unknown,
140
+ prefix: string,
141
+ ): Promise<unknown> {
142
+ const result = await schema["~standard"].validate(data)
143
+ if (result.issues) {
144
+ const normalized = normalizeIssues(result.issues, schema["~standard"].vendor)
145
+ const fields = issuesToFieldErrors(normalized, prefix)
146
+ throw new HoneyError({
147
+ errorKey: EK.validation_failed,
148
+ fields,
149
+ status: SK.bad_request,
150
+ })
151
+ }
152
+ return result.value
153
+ }
154
+
155
+ function parseSearchParams(url: string): Record<string, string | string[]> {
156
+ const searchParams = new URL(url).searchParams
157
+ const result: Record<string, string | string[]> = {}
158
+ for (const [key, value] of searchParams) {
159
+ const existing = result[key]
160
+ if (existing === undefined) {
161
+ result[key] = value
162
+ } else if (Array.isArray(existing)) {
163
+ existing.push(value)
164
+ } else {
165
+ result[key] = [existing, value]
166
+ }
167
+ }
168
+ return result
169
+ }
170
+
171
+ function headersToRecord(headers: Headers): Record<string, string> {
172
+ const result: Record<string, string> = {}
173
+ headers.forEach((value, key) => {
174
+ result[key] = value
175
+ })
176
+ return result
177
+ }
178
+
179
+ type ResolvedSchema = {
180
+ mode: "readableStream" | "standard"
181
+ schema: StandardSchemaLike
182
+ }
183
+
184
+ function resolveSchema(entry: InputSchemaEntry): ResolvedSchema {
185
+ if ("_tag" in entry) {
186
+ return {
187
+ mode: "readableStream",
188
+ schema: entry.schema as StandardSchemaLike,
189
+ }
190
+ }
191
+ return { mode: "standard", schema: entry }
192
+ }
193
+
194
+ function formDataToRecord(formData: FormData): Record<string, unknown> {
195
+ const record: Record<string, unknown> = {}
196
+ formData.forEach((value, key) => {
197
+ if (DANGEROUS_KEYS.has(key)) return
198
+ record[key] = value
199
+ })
200
+ return record
201
+ }
202
+
203
+ function urlEncodedToRecord(body: string): Record<string, string> {
204
+ const params = new URLSearchParams(body)
205
+ const record: Record<string, string> = {}
206
+ params.forEach((value, key) => {
207
+ if (DANGEROUS_KEYS.has(key)) return
208
+ record[key] = value
209
+ })
210
+ return record
211
+ }
212
+
213
+ export async function validateInput(
214
+ schemas: InputSchemasDef,
215
+ req: Request,
216
+ params: Record<string, string>,
217
+ ): Promise<ValidatedInput> {
218
+ const result: ValidatedInput = {}
219
+
220
+ /* params */
221
+ if (schemas.params) {
222
+ const { mode, schema } = resolveSchema(schemas.params)
223
+ result.params = mode === "readableStream" ? params : await runSchema(schema, params, "params")
224
+ }
225
+
226
+ /* search */
227
+ if (schemas.search) {
228
+ const { mode, schema } = resolveSchema(schemas.search)
229
+ const parsed = parseSearchParams(req.url)
230
+ result.search = mode === "readableStream" ? parsed : await runSchema(schema, parsed, "search")
231
+ }
232
+
233
+ /* headers */
234
+ if (schemas.headers) {
235
+ const { mode, schema } = resolveSchema(schemas.headers)
236
+ const headerRecord = headersToRecord(req.headers)
237
+ result.headers =
238
+ mode === "readableStream" ? headerRecord : await runSchema(schema, headerRecord, "headers")
239
+ }
240
+
241
+ /* cookies */
242
+ if (schemas.cookies) {
243
+ const { mode, schema } = resolveSchema(schemas.cookies)
244
+ const cookieHeader = req.headers.get("cookie") ?? ""
245
+ const parsed = parseCookies(cookieHeader)
246
+ result.cookies = mode === "readableStream" ? parsed : await runSchema(schema, parsed, "cookies")
247
+ }
248
+
249
+ /* body-based: json or form — skip for methods without a request body */
250
+ const hasBody =
251
+ req.method !== "DELETE" &&
252
+ req.method !== "GET" &&
253
+ req.method !== "HEAD" &&
254
+ req.method !== "OPTIONS"
255
+ if (hasBody && (schemas.json || schemas.form)) {
256
+ const contentType = req.headers.get("content-type")
257
+ const parser = selectParser(contentType)
258
+
259
+ if (schemas.json && parser === "json") {
260
+ const { mode, schema } = resolveSchema(schemas.json)
261
+ if (mode === "readableStream") {
262
+ /* body untouched — handler reads ctx.req.body directly */
263
+ } else {
264
+ const body: unknown = await req.json()
265
+ result.json = await runSchema(schema, body, "json")
266
+ }
267
+ } else if (schemas.form && parser === "form") {
268
+ const { mode, schema } = resolveSchema(schemas.form)
269
+ if (mode === "readableStream") {
270
+ /* body untouched — handler reads ctx.req.body directly */
271
+ } else if (contentType?.startsWith("multipart/form-data")) {
272
+ result.form = await runSchema(schema, formDataToRecord(await req.formData()), "form")
273
+ } else {
274
+ result.form = await runSchema(schema, urlEncodedToRecord(await req.text()), "form")
275
+ }
276
+ } else {
277
+ throw new HoneyError({
278
+ errorKey: EK.unsupported_media_type,
279
+ status: SK.unsupported_media_type,
280
+ })
281
+ }
282
+ }
283
+
284
+ return result
285
+ }
286
+
287
+ export async function validateOutput(
288
+ schema: StandardSchemaLike,
289
+ statusKey: string,
290
+ data: unknown,
291
+ ): Promise<void> {
292
+ const result = await schema["~standard"].validate(data)
293
+ if (result.issues) {
294
+ throw new HoneyError({
295
+ errorKey: EK.output_validation_failed,
296
+ status: SK.internal_server_error,
297
+ vars: { statusKey },
298
+ })
299
+ }
300
+ }
package/src/ws/bun.ts ADDED
@@ -0,0 +1,89 @@
1
+ export {
2
+ type RawSocket,
3
+ type WSAdapter,
4
+ type WSContext,
5
+ WSContextImpl,
6
+ type WSHandler,
7
+ } from "./cloudflare.ts"
8
+
9
+ import type { WSAdapter, WSContext, WSHandler } from "./cloudflare.ts"
10
+ import { WSContextImpl } from "./cloudflare.ts"
11
+
12
+ type BunRawSocket = {
13
+ close(code?: number, reason?: string): void
14
+ data: BunWSData
15
+ readyState: number
16
+ send(data: ArrayBufferLike | ArrayBufferView | string, compress?: boolean): number
17
+ }
18
+
19
+ type BunWSData = {
20
+ handler: WSHandler<unknown>
21
+ socket: WSContext
22
+ }
23
+
24
+ type BunServer = {
25
+ upgrade<T>(req: Request, opts?: { data?: T }): boolean
26
+ }
27
+
28
+ export type BunWSAdapter = WSAdapter & {
29
+ websocket: {
30
+ close(ws: BunRawSocket, code: number, reason: string): void
31
+ message(ws: BunRawSocket, data: ArrayBufferView | string): void
32
+ open(ws: BunRawSocket): void
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Bun WebSocket adapter.
38
+ * Returns adapter + websocket handler object for Bun.serve().
39
+ * Bun has no onError callback (noted limitation).
40
+ * Keepalive is not supported — Bun's WS API lacks ping/pong control.
41
+ * Use nodeWebSocket() for keepalive support.
42
+ */
43
+ export function bunWebSocket(): BunWSAdapter {
44
+ return {
45
+ upgrade(req: Request, env: unknown, handler: WSHandler<unknown>) {
46
+ const server = (env as Record<string, unknown>).server as BunServer
47
+ /*
48
+ * Socket is undefined here — Bun creates it lazily in the open callback.
49
+ * WSContext cast is safe: actual socket is assigned in open/message/close
50
+ * before any user code accesses it.
51
+ */
52
+ const success = server.upgrade<BunWSData>(req, {
53
+ data: { handler, socket: undefined as unknown as WSContext },
54
+ })
55
+ if (!success) {
56
+ return {
57
+ response: new Response("WebSocket upgrade failed", { status: 500 }),
58
+ socket: undefined as unknown as WSContext,
59
+ }
60
+ }
61
+ /* Node's Response rejects status 101 (Fetch spec: 200-599 only).
62
+ Create a valid Response and override status for the sentinel check. */
63
+ const response = new Response(null)
64
+ Object.defineProperty(response, "status", { value: 101 })
65
+ return {
66
+ response,
67
+ socket: undefined as unknown as WSContext,
68
+ }
69
+ },
70
+
71
+ websocket: {
72
+ close(ws: BunRawSocket, code: number, reason: string) {
73
+ if (!ws.data.socket) ws.data.socket = new WSContextImpl(ws)
74
+ ws.data.handler.onClose?.(undefined, ws.data.socket, code, reason)
75
+ },
76
+ message(ws: BunRawSocket, data: ArrayBufferView | string) {
77
+ if (!ws.data.socket) ws.data.socket = new WSContextImpl(ws)
78
+ /* Bun passes Buffer (Uint8Array) for binary — normalize to ArrayBuffer for WSHandler */
79
+ const normalized =
80
+ typeof data === "string" ? data : ((data as Uint8Array).buffer as ArrayBuffer)
81
+ ws.data.handler.onMessage?.(undefined, ws.data.socket, normalized)
82
+ },
83
+ open(ws: BunRawSocket) {
84
+ if (!ws.data.socket) ws.data.socket = new WSContextImpl(ws)
85
+ ws.data.handler.onOpen?.(undefined, ws.data.socket)
86
+ },
87
+ },
88
+ }
89
+ }
@@ -0,0 +1,133 @@
1
+ import type { WSReadyState } from "../types.ts"
2
+
3
+ export const WS_SEND_BUFFER_MAX = 32
4
+
5
+ export type WSContext<T = unknown> = {
6
+ close(code?: number, reason?: string): void
7
+ raw: T
8
+ readyState: WSReadyState
9
+ send(data: ArrayBuffer | Uint8Array | object | string): void
10
+ }
11
+
12
+ export type WSHandler<TCtx = unknown> = {
13
+ onClose?(ctx: TCtx, ws: WSContext, code: number, reason: string): Promise<void> | void
14
+ onError?(ctx: TCtx, ws: WSContext, error: unknown): Promise<void> | void
15
+ onMessage?(ctx: TCtx, ws: WSContext, data: ArrayBuffer | string): Promise<void> | void
16
+ onOpen?(ctx: TCtx, ws: WSContext): Promise<void> | void
17
+ onReconnect?(ctx: TCtx, ws: WSContext, token: string): Promise<void> | void
18
+ }
19
+
20
+ export type WSPreUpgrade = {
21
+ response: Response
22
+ socket: WSContext
23
+ /** Run `fn` now if the socket is already open, otherwise on the next open. */
24
+ whenOpen(fn: () => void): void
25
+ }
26
+
27
+ export type WSAdapter = {
28
+ upgrade(
29
+ req: Request,
30
+ env: unknown,
31
+ handler: WSHandler<unknown>,
32
+ ): Promise<{ response: Response; socket: WSContext }> | { response: Response; socket: WSContext }
33
+ /**
34
+ * Deno requires `Deno.upgradeWebSocket` in the same turn as the serve
35
+ * callback. When present, Honey calls this synchronously from `fetch`
36
+ * and returns `response` before any middleware Promise.
37
+ */
38
+ preUpgrade?(req: Request): WSPreUpgrade | undefined
39
+ }
40
+
41
+ export type RawSocket = {
42
+ close(code?: number, reason?: string): void
43
+ readyState: number
44
+ send(data: ArrayBuffer | Uint8Array | string): void
45
+ }
46
+
47
+ export class WSContextImpl<T extends RawSocket = RawSocket> implements WSContext<T> {
48
+ raw: T
49
+ private sendBuffer: Array<ArrayBuffer | Uint8Array | string> = []
50
+
51
+ constructor(raw: T) {
52
+ this.raw = raw
53
+ const target = raw as T & { addEventListener?(type: string, listener: () => void): void }
54
+ if (raw.readyState !== 1 && typeof target.addEventListener === "function") {
55
+ target.addEventListener("open", () => {
56
+ for (const msg of this.sendBuffer) this.raw.send(msg)
57
+ this.sendBuffer = []
58
+ })
59
+ }
60
+ }
61
+
62
+ get readyState(): WSReadyState {
63
+ return this.raw.readyState as WSReadyState
64
+ }
65
+
66
+ close(code?: number, reason?: string): void {
67
+ this.sendBuffer = []
68
+ this.raw.close(code, reason)
69
+ }
70
+
71
+ send(data: ArrayBuffer | Uint8Array | object | string): void {
72
+ const payload =
73
+ typeof data === "string" || data instanceof ArrayBuffer || data instanceof Uint8Array
74
+ ? data
75
+ : JSON.stringify(data)
76
+ if (this.raw.readyState === 1) {
77
+ this.raw.send(payload)
78
+ return
79
+ }
80
+ if (this.sendBuffer.length >= WS_SEND_BUFFER_MAX) this.sendBuffer.shift()
81
+ this.sendBuffer.push(payload)
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Cloudflare Workers WebSocket adapter.
87
+ * Uses WebSocketPair + server.accept() + addEventListener.
88
+ */
89
+ export function cfWebSocket(): WSAdapter {
90
+ return {
91
+ upgrade(_req: Request, _env: unknown, handler: WSHandler<unknown>) {
92
+ type CFWebSocket = RawSocket & { accept(): void }
93
+ const Ctor = (globalThis as Record<string, unknown>)["WebSocketPair"] as new () => [
94
+ CFWebSocket,
95
+ CFWebSocket,
96
+ ]
97
+ const [client, server] = new Ctor()
98
+ server.accept()
99
+ const socket = new WSContextImpl(server)
100
+
101
+ const raw = server as unknown as EventTarget & RawSocket
102
+ if ("addEventListener" in raw) {
103
+ raw.addEventListener("message", (evt: Event) => {
104
+ const data = (evt as MessageEvent).data
105
+ handler.onMessage?.(undefined, socket, data)
106
+ })
107
+ raw.addEventListener("close", (evt: Event) => {
108
+ const closeEvt = evt as CloseEvent
109
+ handler.onClose?.(undefined, socket, closeEvt.code, closeEvt.reason)
110
+ /* echo close frame so the client handshake completes */
111
+ try {
112
+ server.close(closeEvt.code, closeEvt.reason)
113
+ } catch {
114
+ /* already closed */
115
+ }
116
+ })
117
+ raw.addEventListener("error", (evt: Event) => {
118
+ handler.onError?.(undefined, socket, evt)
119
+ })
120
+ }
121
+ handler.onOpen?.(undefined, socket)
122
+
123
+ return {
124
+ response: new Response(null, {
125
+ headers: { upgrade: "websocket" },
126
+ status: 101,
127
+ webSocket: client,
128
+ } as unknown as ResponseInit),
129
+ socket,
130
+ }
131
+ },
132
+ }
133
+ }
package/src/ws/deno.ts ADDED
@@ -0,0 +1,109 @@
1
+ export {
2
+ type RawSocket,
3
+ type WSAdapter,
4
+ type WSContext,
5
+ type WSPreUpgrade,
6
+ WSContextImpl,
7
+ type WSHandler,
8
+ } from "./cloudflare.ts";
9
+
10
+ import type { WSAdapter, WSHandler, WSPreUpgrade } from "./cloudflare.ts";
11
+ import { WSContextImpl } from "./cloudflare.ts";
12
+
13
+ type DenoNs = {
14
+ Deno: {
15
+ upgradeWebSocket(req: Request): {
16
+ response: Response;
17
+ socket: {
18
+ addEventListener(
19
+ type: string,
20
+ listener: (...args: never[]) => void,
21
+ ): void;
22
+ } & DenoRawSocket;
23
+ };
24
+ };
25
+ };
26
+
27
+ type DenoRawSocket = {
28
+ close(code?: number, reason?: string): void;
29
+ readyState: number;
30
+ send(data: ArrayBuffer | Uint8Array | string): void;
31
+ };
32
+
33
+ type DenoSocket = {
34
+ addEventListener(type: string, listener: (...args: never[]) => void): void
35
+ } & DenoRawSocket
36
+
37
+ function bindHandlers(rawSocket: DenoSocket, socket: WSContextImpl<DenoRawSocket>, handler: WSHandler<unknown>): void {
38
+ rawSocket.addEventListener("open", () => {
39
+ handler.onOpen?.(undefined, socket);
40
+ });
41
+ rawSocket.addEventListener(
42
+ "message",
43
+ (evt: { data: ArrayBuffer | string }) => {
44
+ handler.onMessage?.(undefined, socket, evt.data);
45
+ },
46
+ );
47
+ rawSocket.addEventListener(
48
+ "close",
49
+ (evt: { code: number; reason: string }) => {
50
+ handler.onClose?.(undefined, socket, evt.code, evt.reason);
51
+ },
52
+ );
53
+ rawSocket.addEventListener("error", (evt: unknown) => {
54
+ handler.onError?.(undefined, socket, evt);
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Deno WebSocket adapter.
60
+ * Uses Deno.upgradeWebSocket() and addEventListener for event binding.
61
+ * Keepalive is not supported — Deno's WebSocket lacks a ping() method.
62
+ * Use nodeWebSocket() for keepalive support.
63
+ *
64
+ * `preUpgrade` calls `Deno.upgradeWebSocket` in the same turn as the serve
65
+ * callback (Deno rejects upgrades performed after the first await).
66
+ */
67
+ export function denoWebSocket(): WSAdapter {
68
+ const pending = new WeakMap<Request, { pre: WSPreUpgrade; raw: DenoSocket }>()
69
+
70
+ return {
71
+ preUpgrade(req: Request): WSPreUpgrade {
72
+ const denoNs = globalThis as unknown as DenoNs;
73
+ const { response, socket: rawSocket } = denoNs.Deno.upgradeWebSocket(req);
74
+ const socket = new WSContextImpl(rawSocket as DenoRawSocket);
75
+ const pre: WSPreUpgrade = {
76
+ response,
77
+ socket,
78
+ whenOpen(fn: () => void) {
79
+ if (rawSocket.readyState === 1) {
80
+ fn()
81
+ return
82
+ }
83
+ rawSocket.addEventListener("open", () => fn())
84
+ },
85
+ }
86
+ pending.set(req, { pre, raw: rawSocket })
87
+ return pre
88
+ },
89
+ upgrade(req: Request, _env: unknown, handler: WSHandler<unknown>) {
90
+ const held = pending.get(req)
91
+ if (held) {
92
+ pending.delete(req)
93
+ bindHandlers(held.raw, held.pre.socket as WSContextImpl<DenoRawSocket>, handler)
94
+ /* handshake may have completed before listeners were attached */
95
+ if (held.raw.readyState === 1) {
96
+ handler.onOpen?.(undefined, held.pre.socket)
97
+ }
98
+ return held.pre
99
+ }
100
+
101
+ /* globalThis cast: Deno namespace only exists at runtime on Deno — no way to type it statically */
102
+ const denoNs = globalThis as unknown as DenoNs;
103
+ const { response, socket: rawSocket } = denoNs.Deno.upgradeWebSocket(req);
104
+ const socket = new WSContextImpl(rawSocket as DenoRawSocket);
105
+ bindHandlers(rawSocket, socket, handler)
106
+ return { response, socket };
107
+ },
108
+ };
109
+ }