@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,128 @@
1
+ export type ServerFrame =
2
+ | { data: unknown; id: number; t: "msg" }
3
+ | { t: "pong" }
4
+ | { reason: string; t: "bye" }
5
+ | { reconnectToken: string; t: "ready" }
6
+
7
+ export type ClientFrame =
8
+ | { t: "ping" }
9
+ | { data: unknown; t: "msg" }
10
+ | { lastId: number; reconnectToken: string; t: "resume" }
11
+
12
+ export type ProtocolBuffer = {
13
+ push(frame: { data: unknown; id: number; t: "msg" }): void
14
+ getAfter(lastId: number): Array<{ data: unknown; id: number; t: "msg" }>
15
+ }
16
+
17
+ export type Protocol = {
18
+ handleClientFrame(raw: string): ServerFrame | null
19
+ handleConnect(): ServerFrame
20
+ handleResume(lastId: number, reconnectToken: string): ServerFrame[]
21
+ readonly lastId: number
22
+ readonly reconnectToken: string
23
+ sendBye(reason: string): ServerFrame
24
+ sendMessage(data: unknown): ServerFrame
25
+ }
26
+
27
+ export function encodeServerFrame(frame: ServerFrame): string {
28
+ return JSON.stringify(frame)
29
+ }
30
+
31
+ function isRecord(value: unknown): value is Record<string, unknown> {
32
+ return typeof value === "object" && value !== null && !Array.isArray(value)
33
+ }
34
+
35
+ export function decodeClientFrame(raw: string): ClientFrame | null {
36
+ let parsed: unknown
37
+ try {
38
+ parsed = JSON.parse(raw)
39
+ } catch {
40
+ return null
41
+ }
42
+
43
+ if (!isRecord(parsed)) {
44
+ return null
45
+ }
46
+
47
+ const obj = parsed
48
+
49
+ if (obj.t === "ping") {
50
+ return { t: "ping" }
51
+ }
52
+
53
+ if (obj.t === "msg") {
54
+ if (!("data" in obj)) return null
55
+ return { data: obj.data, t: "msg" }
56
+ }
57
+
58
+ if (obj.t === "resume") {
59
+ if (
60
+ typeof obj.lastId !== "number" ||
61
+ obj.lastId < 0 ||
62
+ !Number.isInteger(obj.lastId) ||
63
+ typeof obj.reconnectToken !== "string"
64
+ ) {
65
+ return null
66
+ }
67
+ return { lastId: obj.lastId, reconnectToken: obj.reconnectToken, t: "resume" }
68
+ }
69
+
70
+ return null
71
+ }
72
+
73
+ export function createProtocol(opts?: { reconnectBuffer?: ProtocolBuffer }): Protocol {
74
+ const token = crypto.randomUUID()
75
+ const buffer = opts?.reconnectBuffer ?? null
76
+ let idCounter = 0
77
+
78
+ function handleConnect(): ServerFrame {
79
+ return { reconnectToken: token, t: "ready" }
80
+ }
81
+
82
+ function handleResume(lastId: number, reconnectToken: string): ServerFrame[] {
83
+ if (reconnectToken !== token || !buffer) {
84
+ return [{ reason: "server_restart", t: "bye" }]
85
+ }
86
+
87
+ const missed = buffer.getAfter(lastId)
88
+ return [...missed, { reconnectToken: token, t: "ready" }]
89
+ }
90
+
91
+ function handleClientFrame(raw: string): ServerFrame | null {
92
+ const frame = decodeClientFrame(raw)
93
+ if (!frame) return null
94
+
95
+ if (frame.t === "ping") {
96
+ return { t: "pong" }
97
+ }
98
+
99
+ return null
100
+ }
101
+
102
+ function sendMessage(data: unknown): ServerFrame {
103
+ idCounter++
104
+ const frame: ServerFrame = { data, id: idCounter, t: "msg" }
105
+ if (buffer) {
106
+ buffer.push({ data, id: idCounter, t: "msg" })
107
+ }
108
+ return frame
109
+ }
110
+
111
+ function sendBye(reason: string): ServerFrame {
112
+ return { reason, t: "bye" }
113
+ }
114
+
115
+ return {
116
+ handleClientFrame,
117
+ handleConnect,
118
+ handleResume,
119
+ get lastId() {
120
+ return idCounter
121
+ },
122
+ get reconnectToken() {
123
+ return token
124
+ },
125
+ sendBye,
126
+ sendMessage,
127
+ }
128
+ }
@@ -0,0 +1,104 @@
1
+ export type ConnContext = {
2
+ readonly id: string
3
+ readonly userId: string | null
4
+ readonly transport: "ws" | "sse" | "longpoll"
5
+ readonly state: Record<string, unknown>
6
+ join(topic: string): void
7
+ leave(topic: string): void
8
+ send(payload: unknown): void
9
+ publish(topic: string, payload: unknown): void
10
+ close(reason?: string): void
11
+ on(event: "message", handler: (payload: unknown) => void): void
12
+ on(event: "close", handler: (reason: string) => void): void
13
+ readonly _handlers: { message: ((payload: unknown) => void) | null; close: ((reason: string) => void) | null }
14
+ }
15
+
16
+ export type RealtimeRouteOpts = {
17
+ use?: Array<(ctx: unknown, next: () => Promise<Response>) => Promise<Response>>
18
+ reconnectBuffer?: number
19
+ handler: (c: unknown, conn: ConnContext) => void | Promise<void>
20
+ }
21
+
22
+ export type RealtimeRouteHandler = {
23
+ handler: RealtimeRouteOpts["handler"]
24
+ middlewares: RealtimeRouteOpts["use"]
25
+ reconnectBuffer: number
26
+ path: string
27
+ }
28
+
29
+ export function createConnContext(opts: {
30
+ id: string
31
+ userId: string | null
32
+ transport: "ws" | "sse" | "longpoll"
33
+ bus: {
34
+ subscribe(connId: string, topic: string): void
35
+ unsubscribe(connId: string, topic: string): void
36
+ unsubscribeAll(connId: string): void
37
+ publish(topic: string, data: unknown): void
38
+ }
39
+ sendFn: (payload: unknown) => void
40
+ closeFn: (reason?: string) => void
41
+ }): ConnContext {
42
+ const state: Record<string, unknown> = {}
43
+ const handlers: { message: ((payload: unknown) => void) | null; close: ((reason: string) => void) | null } = {
44
+ close: null,
45
+ message: null,
46
+ }
47
+
48
+ const conn: ConnContext = Object.create(null)
49
+
50
+ Object.defineProperty(conn, "id", { enumerable: true, value: opts.id, writable: false })
51
+ Object.defineProperty(conn, "userId", { enumerable: true, value: opts.userId, writable: false })
52
+ Object.defineProperty(conn, "transport", { enumerable: true, value: opts.transport, writable: false })
53
+ Object.defineProperty(conn, "state", { enumerable: true, get: () => state })
54
+ Object.defineProperty(conn, "_handlers", { enumerable: false, get: () => handlers })
55
+
56
+ Object.defineProperty(conn, "join", {
57
+ enumerable: true,
58
+ value(topic: string) {
59
+ opts.bus.subscribe(opts.id, topic)
60
+ },
61
+ })
62
+
63
+ Object.defineProperty(conn, "leave", {
64
+ enumerable: true,
65
+ value(topic: string) {
66
+ opts.bus.unsubscribe(opts.id, topic)
67
+ },
68
+ })
69
+
70
+ Object.defineProperty(conn, "send", {
71
+ enumerable: true,
72
+ value(payload: unknown) {
73
+ opts.sendFn(payload)
74
+ },
75
+ })
76
+
77
+ Object.defineProperty(conn, "publish", {
78
+ enumerable: true,
79
+ value(topic: string, payload: unknown) {
80
+ opts.bus.publish(topic, payload)
81
+ },
82
+ })
83
+
84
+ Object.defineProperty(conn, "close", {
85
+ enumerable: true,
86
+ value(reason?: string) {
87
+ opts.bus.unsubscribeAll(opts.id)
88
+ opts.closeFn(reason)
89
+ },
90
+ })
91
+
92
+ Object.defineProperty(conn, "on", {
93
+ enumerable: true,
94
+ value(event: "message" | "close", handler: ((...args: unknown[]) => void)) {
95
+ if (event === "message") {
96
+ handlers.message = handler as (payload: unknown) => void
97
+ } else {
98
+ handlers.close = handler as (reason: string) => void
99
+ }
100
+ },
101
+ })
102
+
103
+ return conn
104
+ }
@@ -0,0 +1,430 @@
1
+ import type { ServerFrame } from "./protocol.ts"
2
+
3
+ /* ------------------------------------------------------------------ */
4
+ /* Error hierarchy */
5
+ /* ------------------------------------------------------------------ */
6
+
7
+ export class RealtimeError extends Error {
8
+ readonly reason: string
9
+
10
+ constructor(message: string, reason: string) {
11
+ super(message)
12
+ this.reason = reason
13
+ this.name = "RealtimeError"
14
+ }
15
+ }
16
+
17
+ export class RealtimeConnectError extends RealtimeError {
18
+ constructor(message: string, reason: string) {
19
+ super(message, reason)
20
+ this.name = "RealtimeConnectError"
21
+ }
22
+ }
23
+
24
+ export class RealtimeAuthError extends RealtimeError {
25
+ constructor(message: string, reason: string) {
26
+ super(message, reason)
27
+ this.name = "RealtimeAuthError"
28
+ }
29
+ }
30
+
31
+ export class RealtimeKickedError extends RealtimeError {
32
+ constructor(message: string, reason: string) {
33
+ super(message, reason)
34
+ this.name = "RealtimeKickedError"
35
+ }
36
+ }
37
+
38
+ export class RealtimeAbortError extends RealtimeError {
39
+ constructor(message: string, reason: string) {
40
+ super(message, reason)
41
+ this.name = "RealtimeAbortError"
42
+ }
43
+ }
44
+
45
+ /* ------------------------------------------------------------------ */
46
+ /* Types */
47
+ /* ------------------------------------------------------------------ */
48
+
49
+ export type Transport = "ws" | "sse" | "longpoll"
50
+ export type ConnectionState = "idle" | "connecting" | "connected" | "draining" | "reconnecting" | "closed"
51
+
52
+ export type TransportAdapter = {
53
+ connect(url: string, opts: TransportOpts): TransportConnection
54
+ }
55
+
56
+ export type TransportConnection = {
57
+ send(data: string): void
58
+ close(): void
59
+ onFrame: (frame: ServerFrame) => void
60
+ onClose: (reason: string) => void
61
+ onError: (err: unknown) => void
62
+ }
63
+
64
+ export type TransportOpts = {
65
+ signal?: AbortSignal
66
+ headers?: Record<string, string>
67
+ lastId?: number
68
+ reconnectToken?: string
69
+ }
70
+
71
+ /* ------------------------------------------------------------------ */
72
+ /* KeepaliveLoop */
73
+ /* ------------------------------------------------------------------ */
74
+
75
+ export function createKeepaliveLoop(opts: {
76
+ transport: Transport
77
+ sendPing: () => void
78
+ onDead: () => void
79
+ interval?: number
80
+ timeout?: number
81
+ }): { start(): void; stop(): void; onPong(): void; onFrame(): void } {
82
+ const interval = opts.interval ?? 25_000
83
+ const timeout = opts.timeout ?? 60_000
84
+ const transport = opts.transport
85
+
86
+ let pingTimer: ReturnType<typeof setInterval> | null = null
87
+ let deadTimer: ReturnType<typeof setTimeout> | null = null
88
+
89
+ function clearAll(): void {
90
+ if (pingTimer !== null) {
91
+ clearInterval(pingTimer)
92
+ pingTimer = null
93
+ }
94
+ if (deadTimer !== null) {
95
+ clearTimeout(deadTimer)
96
+ deadTimer = null
97
+ }
98
+ }
99
+
100
+ function resetDeadTimer(): void {
101
+ if (deadTimer !== null) {
102
+ clearTimeout(deadTimer)
103
+ }
104
+ deadTimer = setTimeout(() => {
105
+ opts.onDead()
106
+ }, timeout)
107
+ }
108
+
109
+ function start(): void {
110
+ /* Idempotent — stop first to avoid stacking timers */
111
+ clearAll()
112
+
113
+ if (transport === "longpoll") return
114
+
115
+ if (transport === "ws") {
116
+ pingTimer = setInterval(() => {
117
+ opts.sendPing()
118
+ }, interval)
119
+ resetDeadTimer()
120
+ return
121
+ }
122
+
123
+ if (transport === "sse") {
124
+ /* SSE: no pings, just idle timer based on frames from server */
125
+ resetDeadTimer()
126
+ }
127
+ }
128
+
129
+ function stop(): void {
130
+ clearAll()
131
+ }
132
+
133
+ function onPong(): void {
134
+ /* Only meaningful for WS — reset the dead timer */
135
+ if (deadTimer !== null) {
136
+ resetDeadTimer()
137
+ }
138
+ }
139
+
140
+ function onFrame(): void {
141
+ /* Meaningful for SSE — reset idle timer on any frame */
142
+ if (transport === "sse" && deadTimer !== null) {
143
+ resetDeadTimer()
144
+ }
145
+ }
146
+
147
+ return { onFrame, onPong, start, stop }
148
+ }
149
+
150
+ /* ------------------------------------------------------------------ */
151
+ /* FallbackChain */
152
+ /* ------------------------------------------------------------------ */
153
+
154
+ const DEFAULT_FALLBACK_TIMEOUT = 3000
155
+
156
+ export function createFallbackChain(opts: {
157
+ transports: TransportAdapter[]
158
+ timeout?: number
159
+ }): {
160
+ connect(url: string, transportOpts: TransportOpts): Promise<{ conn: TransportConnection; transport: Transport }>
161
+ readonly provenTransport: Transport | null
162
+ } {
163
+ const timeout = opts.timeout ?? DEFAULT_FALLBACK_TIMEOUT
164
+ let proven: Transport | null = null
165
+
166
+ function tryTransport(
167
+ adapter: TransportAdapter,
168
+ url: string,
169
+ transportOpts: TransportOpts,
170
+ ): Promise<TransportConnection> {
171
+ return new Promise((resolve, reject) => {
172
+ const conn = adapter.connect(url, transportOpts)
173
+
174
+ const timer = setTimeout(() => {
175
+ conn.close()
176
+ reject(new Error("timeout"))
177
+ }, timeout)
178
+
179
+ const originalOnFrame = conn.onFrame
180
+ conn.onFrame = (frame: ServerFrame) => {
181
+ if (frame.t === "ready") {
182
+ clearTimeout(timer)
183
+ conn.onFrame = originalOnFrame
184
+ resolve(conn)
185
+ } else {
186
+ originalOnFrame(frame)
187
+ }
188
+ }
189
+
190
+ conn.onError = (err: unknown) => {
191
+ clearTimeout(timer)
192
+ conn.close()
193
+ reject(err)
194
+ }
195
+
196
+ conn.onClose = (reason: string) => {
197
+ clearTimeout(timer)
198
+ reject(new Error(reason))
199
+ }
200
+ })
201
+ }
202
+
203
+ function connect(
204
+ url: string,
205
+ transportOpts: TransportOpts,
206
+ ): Promise<{ conn: TransportConnection; transport: Transport }> {
207
+ const transports = opts.transports
208
+
209
+ if (transports.length === 0) {
210
+ const p = Promise.reject(new RealtimeConnectError("No transports configured", "no_transports"))
211
+ p.catch(() => {})
212
+ return p
213
+ }
214
+
215
+ const p = connectInternal(url, transportOpts, transports)
216
+ p.catch(() => {})
217
+ return p
218
+ }
219
+
220
+ async function connectInternal(
221
+ url: string,
222
+ transportOpts: TransportOpts,
223
+ transports: TransportAdapter[],
224
+ ): Promise<{ conn: TransportConnection; transport: Transport }> {
225
+
226
+ /* If we have a proven transport, try it first (it's at a specific index) */
227
+ if (proven !== null) {
228
+ /* proven refers to index position — try that adapter directly */
229
+ const provenIndex = provenIndexStore
230
+ if (provenIndex !== null && provenIndex < transports.length) {
231
+ try {
232
+ const conn = await tryTransport(transports[provenIndex], url, transportOpts)
233
+ return { conn, transport: proven }
234
+ } catch {
235
+ /* Proven transport failed — fall through to full chain */
236
+ }
237
+ }
238
+ }
239
+
240
+ for (let i = 0; i < transports.length; i++) {
241
+ try {
242
+ const conn = await tryTransport(transports[i], url, transportOpts)
243
+ /* Record proven transport — we use the index to map to a transport name */
244
+ let transportName: Transport = "longpoll"
245
+ if (i === 0) transportName = "ws"
246
+ else if (i === 1) transportName = "sse"
247
+ proven = transportName
248
+ provenIndexStore = i
249
+ return { conn, transport: transportName }
250
+ } catch {
251
+ /* Try next */
252
+ }
253
+ }
254
+
255
+ throw new RealtimeConnectError("All transports failed", "all_failed")
256
+ }
257
+
258
+ let provenIndexStore: number | null = null
259
+
260
+ return {
261
+ connect,
262
+ get provenTransport() {
263
+ return proven
264
+ },
265
+ }
266
+ }
267
+
268
+ /* ------------------------------------------------------------------ */
269
+ /* ResumableConnection */
270
+ /* ------------------------------------------------------------------ */
271
+
272
+ export type ResumableConnectionOpts = {
273
+ url: string
274
+ token?: () => string | Promise<string>
275
+ onAuthExpired?: () => Promise<string | null>
276
+ onReconnecting?: (attempt: number, transport: Transport) => void
277
+ onReconnected?: () => void
278
+ signal?: AbortSignal
279
+ transports?: Transport[]
280
+ keepaliveInterval?: number
281
+ }
282
+
283
+ type QueueEntry =
284
+ | { type: "value"; value: unknown }
285
+ | { type: "done" }
286
+ | { type: "error"; error: unknown }
287
+
288
+ export function createResumableConnection(opts: ResumableConnectionOpts): {
289
+ readonly state: ConnectionState
290
+ send(data: unknown): void
291
+ close(reason?: string): void
292
+ [Symbol.asyncIterator](): AsyncIterableIterator<unknown>
293
+ } {
294
+ let state: ConnectionState = "idle"
295
+ let currentConn: TransportConnection | null = null
296
+ let started = false
297
+
298
+ /* Check if already aborted */
299
+ if (opts.signal?.aborted) {
300
+ state = "closed"
301
+ }
302
+
303
+ /* Queue-based async iterator infrastructure */
304
+ const queue: QueueEntry[] = []
305
+ let pending: {
306
+ resolve: (result: IteratorResult<unknown>) => void
307
+ reject: (err: unknown) => void
308
+ } | null = null
309
+
310
+ function enqueue(entry: QueueEntry): void {
311
+ if (pending) {
312
+ const { resolve, reject } = pending
313
+ pending = null
314
+
315
+ if (entry.type === "value") {
316
+ resolve({ done: false, value: entry.value })
317
+ } else if (entry.type === "done") {
318
+ resolve({ done: true, value: undefined })
319
+ } else {
320
+ reject(entry.error)
321
+ }
322
+ } else {
323
+ queue.push(entry)
324
+ }
325
+ }
326
+
327
+ function terminate(entry: QueueEntry): void {
328
+ enqueue(entry)
329
+ }
330
+
331
+ function handleClose(): void {
332
+ if (state === "closed") return
333
+ state = "closed"
334
+ if (currentConn) {
335
+ currentConn.close()
336
+ currentConn = null
337
+ }
338
+ terminate({ type: "done" })
339
+ }
340
+
341
+ function handleAbort(): void {
342
+ if (state === "closed") return
343
+ state = "closed"
344
+ if (currentConn) {
345
+ currentConn.close()
346
+ currentConn = null
347
+ }
348
+ terminate({ error: new RealtimeAbortError("Connection aborted", "aborted"), type: "error" })
349
+ }
350
+
351
+ /* Listen for abort signal */
352
+ if (opts.signal && !opts.signal.aborted) {
353
+ opts.signal.addEventListener("abort", () => {
354
+ handleAbort()
355
+ }, { once: true })
356
+ }
357
+
358
+ function startConnection(): void {
359
+ if (started || state === "closed") return
360
+ started = true
361
+ state = "connecting"
362
+
363
+ /* Kick off connection in a microtask */
364
+ queueMicrotask(() => {
365
+ if (state === "closed") return
366
+
367
+ /* In a real implementation, this would use createFallbackChain.
368
+ For now, state transitions are driven by external transport frames. */
369
+ })
370
+ }
371
+
372
+ function send(data: unknown): void {
373
+ if (state === "closed" || !currentConn) return
374
+ currentConn.send(JSON.stringify(data))
375
+ }
376
+
377
+ function close(_reason?: string): void {
378
+ handleClose()
379
+ }
380
+
381
+ function next(): Promise<IteratorResult<unknown>> {
382
+ /* First next() call triggers connection */
383
+ if (!started && state !== "closed") {
384
+ startConnection()
385
+ }
386
+
387
+ /* Check queue first */
388
+ if (queue.length > 0) {
389
+ const entry = queue.shift()
390
+ if (!entry || entry.type === "done") {
391
+ return Promise.resolve({ done: true, value: undefined })
392
+ }
393
+ if (entry.type === "value") {
394
+ return Promise.resolve({ done: false, value: entry.value })
395
+ }
396
+ return Promise.reject(entry.error)
397
+ }
398
+
399
+ /* If already closed, return done */
400
+ if (state === "closed") {
401
+ return Promise.resolve({ done: true, value: undefined })
402
+ }
403
+
404
+ /* Wait for next entry — attach a no-op catch to suppress Node's
405
+ unhandled-rejection warning while the caller awaits the promise */
406
+ const promise = new Promise<IteratorResult<unknown>>((resolve, reject) => {
407
+ pending = { reject, resolve }
408
+ })
409
+ promise.catch(() => {})
410
+ return promise
411
+ }
412
+
413
+ function asyncIterator(): AsyncIterableIterator<unknown> {
414
+ return {
415
+ next,
416
+ [Symbol.asyncIterator]() {
417
+ return this
418
+ },
419
+ }
420
+ }
421
+
422
+ return {
423
+ [Symbol.asyncIterator]: asyncIterator,
424
+ close,
425
+ send,
426
+ get state() {
427
+ return state
428
+ },
429
+ }
430
+ }
@@ -0,0 +1,23 @@
1
+ import type { MiddlewareFn } from "./middleware.ts"
2
+
3
+ type RequestIdOptions = {
4
+ generator?: () => string
5
+ header?: string
6
+ }
7
+
8
+ export function requestId(
9
+ options?: RequestIdOptions,
10
+ ): MiddlewareFn<{ req: Request }, { requestId: string }> {
11
+ const headerName = options?.header ?? "x-request-id"
12
+ const generate = options?.generator ?? (() => crypto.randomUUID())
13
+
14
+ const mw: MiddlewareFn<{ req: Request }, { requestId: string }> = async (ctx, next) => {
15
+ const existing = ctx.req.headers.get(headerName)
16
+ const id = existing ?? generate()
17
+ const response = await next({ requestId: id })
18
+ response.headers.set(headerName, id)
19
+ return response
20
+ }
21
+
22
+ return mw
23
+ }
@@ -0,0 +1,34 @@
1
+ export type RequestToCurlOptions = {
2
+ excludeHeader?: (name: string, value: string) => boolean
3
+ }
4
+
5
+ function shellEscape(value: string): string {
6
+ return value.replace(/'/g, "'\\''")
7
+ }
8
+
9
+ /**
10
+ * Convert a native Request into a curl command string.
11
+ * The request is cloned so reading the body does not consume the original stream.
12
+ */
13
+ export async function requestToCurl(
14
+ request: Request,
15
+ options?: RequestToCurlOptions,
16
+ ): Promise<string> {
17
+ const clonedRequest = request.clone()
18
+ const parts: string[] = [`curl -X ${clonedRequest.method}`]
19
+
20
+ for (const [name, value] of clonedRequest.headers.entries()) {
21
+ if (options?.excludeHeader?.(name, value) === true) continue
22
+ parts.push(`-H '${shellEscape(name)}: ${shellEscape(value)}'`)
23
+ }
24
+
25
+ const body = await clonedRequest.text()
26
+
27
+ if (body.length > 0) {
28
+ parts.push(`--data-raw '${shellEscape(body)}'`)
29
+ }
30
+
31
+ parts.push(`'${shellEscape(clonedRequest.url)}'`)
32
+
33
+ return parts.join(" ")
34
+ }