@yolk-sdk/emulators 0.1.0-canary.96

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 (333) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2135 -0
  3. package/dist/anthropic.d.mts +269 -0
  4. package/dist/anthropic.d.mts.map +1 -0
  5. package/dist/anthropic.mjs +177 -0
  6. package/dist/anthropic.mjs.map +1 -0
  7. package/dist/chat-completions.d.mts +288 -0
  8. package/dist/chat-completions.d.mts.map +1 -0
  9. package/dist/chat-completions.mjs +451 -0
  10. package/dist/chat-completions.mjs.map +1 -0
  11. package/dist/codex.d.mts +265 -0
  12. package/dist/codex.d.mts.map +1 -0
  13. package/dist/codex.mjs +199 -0
  14. package/dist/codex.mjs.map +1 -0
  15. package/dist/dropbox/api.d.mts +63 -0
  16. package/dist/dropbox/api.d.mts.map +1 -0
  17. package/dist/dropbox/api.mjs +565 -0
  18. package/dist/dropbox/api.mjs.map +1 -0
  19. package/dist/dropbox/state.d.mts +136 -0
  20. package/dist/dropbox/state.d.mts.map +1 -0
  21. package/dist/dropbox/state.mjs +209 -0
  22. package/dist/dropbox/state.mjs.map +1 -0
  23. package/dist/dropbox.d.mts +79 -0
  24. package/dist/dropbox.d.mts.map +1 -0
  25. package/dist/dropbox.mjs +124 -0
  26. package/dist/dropbox.mjs.map +1 -0
  27. package/dist/email-fixtures.d.mts +36 -0
  28. package/dist/email-fixtures.d.mts.map +1 -0
  29. package/dist/email-fixtures.mjs +1080 -0
  30. package/dist/email-fixtures.mjs.map +1 -0
  31. package/dist/email.d.mts +160 -0
  32. package/dist/email.d.mts.map +1 -0
  33. package/dist/email.mjs +608 -0
  34. package/dist/email.mjs.map +1 -0
  35. package/dist/emulator-compose.d.mts +40 -0
  36. package/dist/emulator-compose.d.mts.map +1 -0
  37. package/dist/emulator-compose.mjs +91 -0
  38. package/dist/emulator-compose.mjs.map +1 -0
  39. package/dist/emulator-http.d.mts +53 -0
  40. package/dist/emulator-http.d.mts.map +1 -0
  41. package/dist/emulator-http.mjs +117 -0
  42. package/dist/emulator-http.mjs.map +1 -0
  43. package/dist/emulator-kernel.d.mts +176 -0
  44. package/dist/emulator-kernel.d.mts.map +1 -0
  45. package/dist/emulator-kernel.mjs +413 -0
  46. package/dist/emulator-kernel.mjs.map +1 -0
  47. package/dist/fixture-route.d.mts +129 -0
  48. package/dist/fixture-route.d.mts.map +1 -0
  49. package/dist/fixture-route.mjs +257 -0
  50. package/dist/fixture-route.mjs.map +1 -0
  51. package/dist/fortnox/api.d.mts +92 -0
  52. package/dist/fortnox/api.d.mts.map +1 -0
  53. package/dist/fortnox/api.mjs +750 -0
  54. package/dist/fortnox/api.mjs.map +1 -0
  55. package/dist/fortnox/state.d.mts +533 -0
  56. package/dist/fortnox/state.d.mts.map +1 -0
  57. package/dist/fortnox/state.mjs +612 -0
  58. package/dist/fortnox/state.mjs.map +1 -0
  59. package/dist/fortnox.d.mts +128 -0
  60. package/dist/fortnox.d.mts.map +1 -0
  61. package/dist/fortnox.mjs +403 -0
  62. package/dist/fortnox.mjs.map +1 -0
  63. package/dist/gateway-evaluate-recordings.d.mts +12 -0
  64. package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
  65. package/dist/gateway-evaluate-recordings.mjs +128 -0
  66. package/dist/gateway-evaluate-recordings.mjs.map +1 -0
  67. package/dist/gateway.d.mts +272 -0
  68. package/dist/gateway.d.mts.map +1 -0
  69. package/dist/gateway.mjs +400 -0
  70. package/dist/gateway.mjs.map +1 -0
  71. package/dist/github/api.d.mts +63 -0
  72. package/dist/github/api.d.mts.map +1 -0
  73. package/dist/github/api.mjs +527 -0
  74. package/dist/github/api.mjs.map +1 -0
  75. package/dist/github/state.d.mts +272 -0
  76. package/dist/github/state.d.mts.map +1 -0
  77. package/dist/github/state.mjs +303 -0
  78. package/dist/github/state.mjs.map +1 -0
  79. package/dist/github.d.mts +77 -0
  80. package/dist/github.d.mts.map +1 -0
  81. package/dist/github.mjs +180 -0
  82. package/dist/github.mjs.map +1 -0
  83. package/dist/google/calendar.d.mts +10 -0
  84. package/dist/google/calendar.d.mts.map +1 -0
  85. package/dist/google/calendar.mjs +252 -0
  86. package/dist/google/calendar.mjs.map +1 -0
  87. package/dist/google/drive.d.mts +8 -0
  88. package/dist/google/drive.d.mts.map +1 -0
  89. package/dist/google/drive.mjs +252 -0
  90. package/dist/google/drive.mjs.map +1 -0
  91. package/dist/google/gmail.d.mts +10 -0
  92. package/dist/google/gmail.d.mts.map +1 -0
  93. package/dist/google/gmail.mjs +591 -0
  94. package/dist/google/gmail.mjs.map +1 -0
  95. package/dist/google/shared.d.mts +107 -0
  96. package/dist/google/shared.d.mts.map +1 -0
  97. package/dist/google/shared.mjs +146 -0
  98. package/dist/google/shared.mjs.map +1 -0
  99. package/dist/google/state.d.mts +385 -0
  100. package/dist/google/state.d.mts.map +1 -0
  101. package/dist/google/state.mjs +496 -0
  102. package/dist/google/state.mjs.map +1 -0
  103. package/dist/google.d.mts +75 -0
  104. package/dist/google.d.mts.map +1 -0
  105. package/dist/google.mjs +210 -0
  106. package/dist/google.mjs.map +1 -0
  107. package/dist/linkedin-search/api.d.mts +45 -0
  108. package/dist/linkedin-search/api.d.mts.map +1 -0
  109. package/dist/linkedin-search/api.mjs +181 -0
  110. package/dist/linkedin-search/api.mjs.map +1 -0
  111. package/dist/linkedin-search/state.d.mts +123 -0
  112. package/dist/linkedin-search/state.d.mts.map +1 -0
  113. package/dist/linkedin-search/state.mjs +261 -0
  114. package/dist/linkedin-search/state.mjs.map +1 -0
  115. package/dist/linkedin-search.d.mts +69 -0
  116. package/dist/linkedin-search.d.mts.map +1 -0
  117. package/dist/linkedin-search.mjs +158 -0
  118. package/dist/linkedin-search.mjs.map +1 -0
  119. package/dist/mcp/api.d.mts +44 -0
  120. package/dist/mcp/api.d.mts.map +1 -0
  121. package/dist/mcp/api.mjs +557 -0
  122. package/dist/mcp/api.mjs.map +1 -0
  123. package/dist/mcp/recordings.d.mts +40 -0
  124. package/dist/mcp/recordings.d.mts.map +1 -0
  125. package/dist/mcp/recordings.mjs +2390 -0
  126. package/dist/mcp/recordings.mjs.map +1 -0
  127. package/dist/mcp/state.d.mts +71 -0
  128. package/dist/mcp/state.d.mts.map +1 -0
  129. package/dist/mcp/state.mjs +83 -0
  130. package/dist/mcp/state.mjs.map +1 -0
  131. package/dist/mcp.d.mts +84 -0
  132. package/dist/mcp.d.mts.map +1 -0
  133. package/dist/mcp.mjs +241 -0
  134. package/dist/mcp.mjs.map +1 -0
  135. package/dist/messages.d.mts +183 -0
  136. package/dist/messages.d.mts.map +1 -0
  137. package/dist/messages.mjs +532 -0
  138. package/dist/messages.mjs.map +1 -0
  139. package/dist/microsoft/api.d.mts +47 -0
  140. package/dist/microsoft/api.d.mts.map +1 -0
  141. package/dist/microsoft/api.mjs +178 -0
  142. package/dist/microsoft/api.mjs.map +1 -0
  143. package/dist/microsoft/calendar.d.mts +30 -0
  144. package/dist/microsoft/calendar.d.mts.map +1 -0
  145. package/dist/microsoft/calendar.mjs +271 -0
  146. package/dist/microsoft/calendar.mjs.map +1 -0
  147. package/dist/microsoft/drive.d.mts +36 -0
  148. package/dist/microsoft/drive.d.mts.map +1 -0
  149. package/dist/microsoft/drive.mjs +298 -0
  150. package/dist/microsoft/drive.mjs.map +1 -0
  151. package/dist/microsoft/graph.d.mts +198 -0
  152. package/dist/microsoft/graph.d.mts.map +1 -0
  153. package/dist/microsoft/graph.mjs +264 -0
  154. package/dist/microsoft/graph.mjs.map +1 -0
  155. package/dist/microsoft/mail.d.mts +48 -0
  156. package/dist/microsoft/mail.d.mts.map +1 -0
  157. package/dist/microsoft/mail.mjs +488 -0
  158. package/dist/microsoft/mail.mjs.map +1 -0
  159. package/dist/microsoft/state.d.mts +480 -0
  160. package/dist/microsoft/state.d.mts.map +1 -0
  161. package/dist/microsoft/state.mjs +625 -0
  162. package/dist/microsoft/state.mjs.map +1 -0
  163. package/dist/microsoft.d.mts +168 -0
  164. package/dist/microsoft.d.mts.map +1 -0
  165. package/dist/microsoft.mjs +483 -0
  166. package/dist/microsoft.mjs.map +1 -0
  167. package/dist/node.d.mts +47 -0
  168. package/dist/node.d.mts.map +1 -0
  169. package/dist/node.mjs +178 -0
  170. package/dist/node.mjs.map +1 -0
  171. package/dist/notion/api.d.mts +51 -0
  172. package/dist/notion/api.d.mts.map +1 -0
  173. package/dist/notion/api.mjs +507 -0
  174. package/dist/notion/api.mjs.map +1 -0
  175. package/dist/notion/state.d.mts +329 -0
  176. package/dist/notion/state.d.mts.map +1 -0
  177. package/dist/notion/state.mjs +432 -0
  178. package/dist/notion/state.mjs.map +1 -0
  179. package/dist/notion.d.mts +72 -0
  180. package/dist/notion.d.mts.map +1 -0
  181. package/dist/notion.mjs +127 -0
  182. package/dist/notion.mjs.map +1 -0
  183. package/dist/openai.d.mts +165 -0
  184. package/dist/openai.d.mts.map +1 -0
  185. package/dist/openai.mjs +141 -0
  186. package/dist/openai.mjs.map +1 -0
  187. package/dist/opencode-recordings.d.mts +7 -0
  188. package/dist/opencode-recordings.d.mts.map +1 -0
  189. package/dist/opencode-recordings.mjs +222 -0
  190. package/dist/opencode-recordings.mjs.map +1 -0
  191. package/dist/opencode.d.mts +70 -0
  192. package/dist/opencode.d.mts.map +1 -0
  193. package/dist/opencode.mjs +217 -0
  194. package/dist/opencode.mjs.map +1 -0
  195. package/dist/r2-fixtures.d.mts +36 -0
  196. package/dist/r2-fixtures.d.mts.map +1 -0
  197. package/dist/r2-fixtures.mjs +245 -0
  198. package/dist/r2-fixtures.mjs.map +1 -0
  199. package/dist/r2-guard.d.mts +43 -0
  200. package/dist/r2-guard.d.mts.map +1 -0
  201. package/dist/r2-guard.mjs +215 -0
  202. package/dist/r2-guard.mjs.map +1 -0
  203. package/dist/r2.d.mts +166 -0
  204. package/dist/r2.d.mts.map +1 -0
  205. package/dist/r2.mjs +517 -0
  206. package/dist/r2.mjs.map +1 -0
  207. package/dist/responses.d.mts +231 -0
  208. package/dist/responses.d.mts.map +1 -0
  209. package/dist/responses.mjs +556 -0
  210. package/dist/responses.mjs.map +1 -0
  211. package/dist/route-evidence.d.mts +45 -0
  212. package/dist/route-evidence.d.mts.map +1 -0
  213. package/dist/route-evidence.mjs +56 -0
  214. package/dist/route-evidence.mjs.map +1 -0
  215. package/dist/router.d.mts +93 -0
  216. package/dist/router.d.mts.map +1 -0
  217. package/dist/router.mjs +259 -0
  218. package/dist/router.mjs.map +1 -0
  219. package/dist/stateful-core.d.mts +17 -0
  220. package/dist/stateful-core.d.mts.map +1 -0
  221. package/dist/stateful-core.mjs +41 -0
  222. package/dist/stateful-core.mjs.map +1 -0
  223. package/dist/stateful-emulator.d.mts +565 -0
  224. package/dist/stateful-emulator.d.mts.map +1 -0
  225. package/dist/stateful-emulator.mjs +1228 -0
  226. package/dist/stateful-emulator.mjs.map +1 -0
  227. package/dist/stateful-secrets.d.mts +84 -0
  228. package/dist/stateful-secrets.d.mts.map +1 -0
  229. package/dist/stateful-secrets.mjs +216 -0
  230. package/dist/stateful-secrets.mjs.map +1 -0
  231. package/dist/subscription-usage-recordings.d.mts +9 -0
  232. package/dist/subscription-usage-recordings.d.mts.map +1 -0
  233. package/dist/subscription-usage-recordings.mjs +53 -0
  234. package/dist/subscription-usage-recordings.mjs.map +1 -0
  235. package/dist/subscription-usage.d.mts +53 -0
  236. package/dist/subscription-usage.d.mts.map +1 -0
  237. package/dist/subscription-usage.mjs +21 -0
  238. package/dist/subscription-usage.mjs.map +1 -0
  239. package/dist/telegram/api.d.mts +34 -0
  240. package/dist/telegram/api.d.mts.map +1 -0
  241. package/dist/telegram/api.mjs +257 -0
  242. package/dist/telegram/api.mjs.map +1 -0
  243. package/dist/telegram/state.d.mts +128 -0
  244. package/dist/telegram/state.d.mts.map +1 -0
  245. package/dist/telegram/state.mjs +154 -0
  246. package/dist/telegram/state.mjs.map +1 -0
  247. package/dist/telegram.d.mts +62 -0
  248. package/dist/telegram.d.mts.map +1 -0
  249. package/dist/telegram.mjs +127 -0
  250. package/dist/telegram.mjs.map +1 -0
  251. package/dist/todoist/api.d.mts +55 -0
  252. package/dist/todoist/api.d.mts.map +1 -0
  253. package/dist/todoist/api.mjs +415 -0
  254. package/dist/todoist/api.mjs.map +1 -0
  255. package/dist/todoist/state.d.mts +275 -0
  256. package/dist/todoist/state.d.mts.map +1 -0
  257. package/dist/todoist/state.mjs +345 -0
  258. package/dist/todoist/state.mjs.map +1 -0
  259. package/dist/todoist.d.mts +68 -0
  260. package/dist/todoist.d.mts.map +1 -0
  261. package/dist/todoist.mjs +181 -0
  262. package/dist/todoist.mjs.map +1 -0
  263. package/dist/xai.d.mts +267 -0
  264. package/dist/xai.d.mts.map +1 -0
  265. package/dist/xai.mjs +252 -0
  266. package/dist/xai.mjs.map +1 -0
  267. package/package.json +157 -0
  268. package/src/anthropic.ts +289 -0
  269. package/src/chat-completions.ts +924 -0
  270. package/src/codex.ts +296 -0
  271. package/src/dropbox/api.ts +1084 -0
  272. package/src/dropbox/state.ts +364 -0
  273. package/src/dropbox.ts +203 -0
  274. package/src/email-fixtures.ts +1297 -0
  275. package/src/email.ts +1108 -0
  276. package/src/emulator-compose.ts +147 -0
  277. package/src/emulator-http.ts +184 -0
  278. package/src/emulator-kernel.ts +844 -0
  279. package/src/fixture-route.ts +561 -0
  280. package/src/fortnox/api.ts +1352 -0
  281. package/src/fortnox/state.ts +798 -0
  282. package/src/fortnox.ts +801 -0
  283. package/src/gateway-evaluate-recordings.ts +153 -0
  284. package/src/gateway.ts +530 -0
  285. package/src/github/api.ts +986 -0
  286. package/src/github/state.ts +439 -0
  287. package/src/github.ts +271 -0
  288. package/src/google/calendar.ts +486 -0
  289. package/src/google/drive.ts +471 -0
  290. package/src/google/gmail.ts +1011 -0
  291. package/src/google/shared.ts +321 -0
  292. package/src/google/state.ts +686 -0
  293. package/src/google.ts +298 -0
  294. package/src/linkedin-search/api.ts +363 -0
  295. package/src/linkedin-search/state.ts +373 -0
  296. package/src/linkedin-search.ts +241 -0
  297. package/src/mcp/api.ts +995 -0
  298. package/src/mcp/recordings.ts +2665 -0
  299. package/src/mcp/state.ts +125 -0
  300. package/src/mcp.ts +319 -0
  301. package/src/messages.ts +844 -0
  302. package/src/microsoft/api.ts +426 -0
  303. package/src/microsoft/calendar.ts +491 -0
  304. package/src/microsoft/drive.ts +541 -0
  305. package/src/microsoft/graph.ts +550 -0
  306. package/src/microsoft/mail.ts +833 -0
  307. package/src/microsoft/state.ts +822 -0
  308. package/src/microsoft.ts +982 -0
  309. package/src/node.ts +293 -0
  310. package/src/notion/api.ts +976 -0
  311. package/src/notion/state.ts +512 -0
  312. package/src/notion.ts +210 -0
  313. package/src/openai.ts +198 -0
  314. package/src/opencode-recordings.ts +257 -0
  315. package/src/opencode.ts +271 -0
  316. package/src/r2-fixtures.ts +295 -0
  317. package/src/r2-guard.ts +323 -0
  318. package/src/r2.ts +890 -0
  319. package/src/responses.ts +901 -0
  320. package/src/route-evidence.ts +90 -0
  321. package/src/router.ts +490 -0
  322. package/src/stateful-core.ts +70 -0
  323. package/src/stateful-emulator.ts +2571 -0
  324. package/src/stateful-secrets.ts +299 -0
  325. package/src/subscription-usage-recordings.ts +77 -0
  326. package/src/subscription-usage.ts +68 -0
  327. package/src/telegram/api.ts +437 -0
  328. package/src/telegram/state.ts +229 -0
  329. package/src/telegram.ts +227 -0
  330. package/src/todoist/api.ts +736 -0
  331. package/src/todoist/state.ts +456 -0
  332. package/src/todoist.ts +316 -0
  333. package/src/xai.ts +341 -0
package/src/node.ts ADDED
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Node boundary: serve an emulator fetch handler on loopback.
3
+ *
4
+ * This is the only module in the package that imports `node:` builtins.
5
+ * Servers bind to `127.0.0.1` only. Streamed response bodies are written
6
+ * chunk by chunk (honoring backpressure) so progressive delivery survives the
7
+ * socket, and an erroring body stream destroys the connection mid-response,
8
+ * the way a dropped upstream does.
9
+ *
10
+ * @experimental
11
+ */
12
+ import { Buffer } from 'node:buffer'
13
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
14
+ import type { Socket } from 'node:net'
15
+ import { Data, Effect, Predicate, type Scope } from 'effect'
16
+
17
+ /** A fetch handler, such as the one returned by an emulator factory. */
18
+ export type FetchHandler = (request: Request) => Promise<Response>
19
+
20
+ export type ServeFetchHandlerOptions = {
21
+ /** TCP port; `0` (default) picks a free port. */
22
+ readonly port?: number
23
+ /** Only `127.0.0.1` is accepted (the default); any other host is refused. */
24
+ readonly host?: string
25
+ }
26
+
27
+ /** The only host emulator servers bind to. */
28
+ export const emulatorServerHost = '127.0.0.1'
29
+
30
+ /** Serving failed: a refused host, an invalid port, or a listen error. */
31
+ export class EmulatorServeError extends Data.TaggedError('EmulatorServeError')<{
32
+ readonly reason: 'host-not-allowed' | 'invalid-port' | 'listen-failed'
33
+ readonly cause?: unknown
34
+ }> {
35
+ override get message(): string {
36
+ switch (this.reason) {
37
+ case 'host-not-allowed':
38
+ return `Emulator servers bind to ${emulatorServerHost} only`
39
+ case 'invalid-port':
40
+ return 'Emulator server port must be an integer from 0 to 65535'
41
+ case 'listen-failed':
42
+ return 'Emulator server could not listen'
43
+ }
44
+ }
45
+ }
46
+
47
+ /** A running loopback server (Promise variant). `close` is idempotent. */
48
+ export type RunningFetchHandlerServer = {
49
+ /** Base URL, for example `http://127.0.0.1:43123`. */
50
+ readonly url: string
51
+ readonly close: () => Promise<void>
52
+ }
53
+
54
+ /** A running loopback server (Effect variant). `close` is idempotent. */
55
+ export type ServedFetchHandler = {
56
+ readonly url: string
57
+ readonly close: Effect.Effect<void>
58
+ }
59
+
60
+ const validateOptions = (options: ServeFetchHandlerOptions): EmulatorServeError | undefined => {
61
+ if (options.host !== undefined && options.host !== emulatorServerHost) {
62
+ return new EmulatorServeError({ reason: 'host-not-allowed' })
63
+ }
64
+
65
+ const port = options.port ?? 0
66
+
67
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
68
+ return new EmulatorServeError({ reason: 'invalid-port' })
69
+ }
70
+
71
+ return undefined
72
+ }
73
+
74
+ const readBody = (incoming: IncomingMessage): Promise<Uint8Array<ArrayBuffer>> =>
75
+ new Promise((resolve, reject) => {
76
+ const chunks: Array<Buffer> = []
77
+
78
+ incoming.on('data', (chunk: Buffer | string) => {
79
+ chunks.push(Predicate.isString(chunk) ? Buffer.from(chunk) : chunk)
80
+ })
81
+ incoming.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))))
82
+ incoming.on('error', reject)
83
+ })
84
+
85
+ const toWebRequest = async (incoming: IncomingMessage, origin: string): Promise<Request> => {
86
+ const headers = new Headers()
87
+
88
+ for (const [name, value] of Object.entries(incoming.headers)) {
89
+ if (Array.isArray(value)) {
90
+ for (const item of value) headers.append(name, item)
91
+ } else if (value !== undefined) {
92
+ headers.set(name, value)
93
+ }
94
+ }
95
+
96
+ const method = incoming.method ?? 'GET'
97
+ const body = await readBody(incoming)
98
+ const init: RequestInit = { method, headers }
99
+
100
+ if (method !== 'GET' && method !== 'HEAD' && body.byteLength > 0) {
101
+ init.body = body
102
+ }
103
+
104
+ return new Request(new URL(incoming.url ?? '/', origin), init)
105
+ }
106
+
107
+ const waitForDrain = (outgoing: ServerResponse): Promise<void> =>
108
+ new Promise(resolve => {
109
+ const done = () => {
110
+ outgoing.off('drain', done)
111
+ outgoing.off('close', done)
112
+ resolve()
113
+ }
114
+
115
+ outgoing.on('drain', done)
116
+ outgoing.on('close', done)
117
+ })
118
+
119
+ /**
120
+ * Write a web `Response` to a Node response: status and headers first, then
121
+ * each body chunk as soon as the handler produces it. A body stream error
122
+ * destroys the socket mid-response.
123
+ */
124
+ const writeResponse = async (outgoing: ServerResponse, response: Response): Promise<void> => {
125
+ outgoing.statusCode = response.status
126
+
127
+ if (response.statusText.length > 0) {
128
+ outgoing.statusMessage = response.statusText
129
+ }
130
+
131
+ response.headers.forEach((value, name) => {
132
+ if (name !== 'set-cookie') outgoing.setHeader(name, value)
133
+ })
134
+
135
+ const cookies = response.headers.getSetCookie()
136
+
137
+ if (cookies.length > 0) {
138
+ outgoing.setHeader('set-cookie', cookies)
139
+ }
140
+
141
+ if (response.body === null) {
142
+ outgoing.end()
143
+
144
+ return
145
+ }
146
+
147
+ outgoing.flushHeaders()
148
+
149
+ const reader = response.body.getReader()
150
+
151
+ outgoing.on('close', () => {
152
+ if (!outgoing.writableFinished) {
153
+ reader.cancel().catch(() => undefined)
154
+ }
155
+ })
156
+
157
+ const pump = async (): Promise<void> => {
158
+ const { done, value } = await reader.read()
159
+
160
+ if (done || outgoing.destroyed) {
161
+ outgoing.end()
162
+
163
+ return
164
+ }
165
+
166
+ if (!outgoing.write(value)) {
167
+ await waitForDrain(outgoing)
168
+ }
169
+
170
+ return pump()
171
+ }
172
+
173
+ // Destroy without an error argument: the client sees a dropped connection, and no unhandled
174
+ // 'error' event is emitted on the response.
175
+ await pump().catch(() => {
176
+ outgoing.destroy()
177
+ })
178
+ }
179
+
180
+ const handleRequest = (
181
+ handler: FetchHandler,
182
+ origin: string,
183
+ incoming: IncomingMessage,
184
+ outgoing: ServerResponse
185
+ ): Promise<void> =>
186
+ toWebRequest(incoming, origin)
187
+ .then(handler)
188
+ .then(
189
+ response => writeResponse(outgoing, response),
190
+ () => {
191
+ if (outgoing.headersSent) {
192
+ outgoing.destroy()
193
+
194
+ return
195
+ }
196
+
197
+ outgoing.statusCode = 500
198
+ outgoing.setHeader('content-type', 'text/plain')
199
+ outgoing.end('emulator handler failed')
200
+ }
201
+ )
202
+ // A response that cannot be written (for example an invalid header value) drops the connection.
203
+ .catch(() => {
204
+ outgoing.destroy()
205
+ })
206
+
207
+ /**
208
+ * Serve a fetch handler on `127.0.0.1` (Promise variant, for hosts without an
209
+ * Effect runtime). Rejects with `EmulatorServeError` for a non-loopback host,
210
+ * an invalid port, or a listen failure.
211
+ */
212
+ export const startFetchHandlerServer = (
213
+ handler: FetchHandler,
214
+ options: ServeFetchHandlerOptions = {}
215
+ ): Promise<RunningFetchHandlerServer> => {
216
+ const invalid = validateOptions(options)
217
+
218
+ if (invalid !== undefined) {
219
+ return Promise.reject(invalid)
220
+ }
221
+
222
+ const sockets = new Set<Socket>()
223
+ const server = createServer()
224
+ let origin = ''
225
+
226
+ server.on('connection', socket => {
227
+ socket.setNoDelay(true)
228
+ sockets.add(socket)
229
+ socket.on('close', () => sockets.delete(socket))
230
+ })
231
+
232
+ server.on('request', (incoming: IncomingMessage, outgoing: ServerResponse) => {
233
+ void handleRequest(handler, origin, incoming, outgoing)
234
+ })
235
+
236
+ let closing: Promise<void> | undefined
237
+
238
+ const close = (): Promise<void> => {
239
+ closing ??= new Promise(resolve => {
240
+ server.close(() => resolve())
241
+
242
+ for (const socket of sockets) socket.destroy()
243
+ })
244
+
245
+ return closing
246
+ }
247
+
248
+ return new Promise((resolve, reject) => {
249
+ server.once('error', cause =>
250
+ reject(new EmulatorServeError({ reason: 'listen-failed', cause }))
251
+ )
252
+ server.listen(options.port ?? 0, emulatorServerHost, () => {
253
+ const address = server.address()
254
+
255
+ if (address === null || Predicate.isString(address)) {
256
+ void close()
257
+ reject(new EmulatorServeError({ reason: 'listen-failed' }))
258
+
259
+ return
260
+ }
261
+
262
+ origin = `http://${emulatorServerHost}:${address.port}`
263
+ resolve({ url: origin, close })
264
+ })
265
+ })
266
+ }
267
+
268
+ /**
269
+ * Serve a fetch handler on `127.0.0.1` as a scoped resource: the server
270
+ * closes when the scope closes (or earlier via `close`). Fails with
271
+ * `EmulatorServeError` for a non-loopback host, an invalid port, or a listen
272
+ * failure.
273
+ */
274
+ export const serveFetchHandler = (
275
+ handler: FetchHandler,
276
+ options: ServeFetchHandlerOptions = {}
277
+ ): Effect.Effect<ServedFetchHandler, EmulatorServeError, Scope.Scope> =>
278
+ Effect.acquireRelease(
279
+ Effect.suspend(() => {
280
+ const invalid = validateOptions(options)
281
+
282
+ return invalid === undefined
283
+ ? Effect.tryPromise({
284
+ try: () => startFetchHandlerServer(handler, options),
285
+ catch: cause =>
286
+ cause instanceof EmulatorServeError
287
+ ? cause
288
+ : new EmulatorServeError({ reason: 'listen-failed', cause })
289
+ })
290
+ : Effect.fail(invalid)
291
+ }),
292
+ server => Effect.promise(() => server.close())
293
+ ).pipe(Effect.map(server => ({ url: server.url, close: Effect.promise(() => server.close()) })))