@slopware/sloppy-darwin-arm64 0.1.0-alpha.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 (434) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +5 -0
  3. package/bin/sloppy +0 -0
  4. package/bin/sloppyc +0 -0
  5. package/docs/KNOWN_LIMITATIONS.md +16 -0
  6. package/docs/LICENSES.md +6 -0
  7. package/docs/NOTICE.md +8 -0
  8. package/examples/README.md +140 -0
  9. package/examples/auth-api/README.md +20 -0
  10. package/examples/auth-api/app.js +61 -0
  11. package/examples/auth-api/appsettings.json +7 -0
  12. package/examples/auth-api/sloppy.json +5 -0
  13. package/examples/cache-basic/README.md +9 -0
  14. package/examples/cache-basic/app.js +32 -0
  15. package/examples/cache-hybrid-postgres/README.md +10 -0
  16. package/examples/cache-hybrid-postgres/app.js +27 -0
  17. package/examples/cache-output-api/README.md +10 -0
  18. package/examples/cache-output-api/app.js +35 -0
  19. package/examples/codec-base64-hex/README.md +14 -0
  20. package/examples/codec-base64-hex/app.js +15 -0
  21. package/examples/codec-checksums/README.md +15 -0
  22. package/examples/codec-checksums/app.js +8 -0
  23. package/examples/codec-compression/README.md +13 -0
  24. package/examples/codec-compression/app.js +9 -0
  25. package/examples/codec-streaming-compression/README.md +19 -0
  26. package/examples/codec-streaming-compression/app.js +16 -0
  27. package/examples/codec-text-binary/README.md +16 -0
  28. package/examples/codec-text-binary/app.js +17 -0
  29. package/examples/compiler-hello/README.md +71 -0
  30. package/examples/compiler-hello/app.js +7 -0
  31. package/examples/compiler-hello/expected/app.js +8 -0
  32. package/examples/compiler-hello/expected/app.js.map +53 -0
  33. package/examples/compiler-hello/expected/app.plan.json +229 -0
  34. package/examples/compiler-hello/expected/routes.slrt +0 -0
  35. package/examples/config-basic/README.md +13 -0
  36. package/examples/config-basic/app.js +13 -0
  37. package/examples/config-basic/appsettings.json +7 -0
  38. package/examples/config-secrets-redaction/README.md +9 -0
  39. package/examples/config-secrets-redaction/app.js +9 -0
  40. package/examples/config-secrets-redaction/appsettings.json +5 -0
  41. package/examples/config-strict-mode/README.md +7 -0
  42. package/examples/config-strict-mode/app.js +10 -0
  43. package/examples/config-strict-mode/appsettings.json +7 -0
  44. package/examples/configured-api/README.md +38 -0
  45. package/examples/configured-api/app.js +12 -0
  46. package/examples/configured-api/appsettings.Development.json +5 -0
  47. package/examples/configured-api/appsettings.json +6 -0
  48. package/examples/configured-api/sloppy.json +5 -0
  49. package/examples/core-config-secrets/README.md +10 -0
  50. package/examples/core-config-secrets/app.js +15 -0
  51. package/examples/core-fs-time-codec/README.md +9 -0
  52. package/examples/core-fs-time-codec/app.js +8 -0
  53. package/examples/core-network-time-codec/README.md +11 -0
  54. package/examples/core-network-time-codec/app.js +20 -0
  55. package/examples/core-policy-audit/README.md +7 -0
  56. package/examples/core-policy-audit/app.js +22 -0
  57. package/examples/core-process-time-codec/README.md +8 -0
  58. package/examples/core-process-time-codec/app.js +28 -0
  59. package/examples/core-worker-time/README.md +8 -0
  60. package/examples/core-worker-time/app.js +17 -0
  61. package/examples/crypto-hash-hmac/README.md +17 -0
  62. package/examples/crypto-hash-hmac/app.js +29 -0
  63. package/examples/crypto-password/README.md +21 -0
  64. package/examples/crypto-password/app.js +12 -0
  65. package/examples/crypto-random-token/README.md +16 -0
  66. package/examples/crypto-random-token/app.js +12 -0
  67. package/examples/crypto-secret-constant-time/README.md +21 -0
  68. package/examples/crypto-secret-constant-time/app.js +15 -0
  69. package/examples/data-foundation/README.md +39 -0
  70. package/examples/data-foundation/app.js +63 -0
  71. package/examples/dependency-graph/README.md +19 -0
  72. package/examples/dependency-graph/fixtures/graph-helper/index.js +3 -0
  73. package/examples/dependency-graph/fixtures/graph-helper/package.json +6 -0
  74. package/examples/dependency-graph/package.json +7 -0
  75. package/examples/dependency-graph/public/message.txt +1 -0
  76. package/examples/dependency-graph/sloppy.json +9 -0
  77. package/examples/dependency-graph/src/main.ts +8 -0
  78. package/examples/dogfood/README.md +23 -0
  79. package/examples/dogfood/dogfood.json +136 -0
  80. package/examples/dynamic-module-include/README.md +20 -0
  81. package/examples/dynamic-module-include/public/readme.txt +1 -0
  82. package/examples/dynamic-module-include/sloppy.json +12 -0
  83. package/examples/dynamic-module-include/src/main.ts +6 -0
  84. package/examples/dynamic-module-include/src/plugins/alpha.js +3 -0
  85. package/examples/dynamic-module-include/src/plugins/beta.js +3 -0
  86. package/examples/ergonomics/README.md +42 -0
  87. package/examples/ergonomics/app.js +38 -0
  88. package/examples/framework-controller/README.md +12 -0
  89. package/examples/framework-controller/app.js +31 -0
  90. package/examples/framework-di-services/README.md +17 -0
  91. package/examples/framework-di-services/app.ts +40 -0
  92. package/examples/framework-explicit-binding/README.md +12 -0
  93. package/examples/framework-explicit-binding/app.ts +34 -0
  94. package/examples/framework-hello/README.md +16 -0
  95. package/examples/framework-hello/app.ts +16 -0
  96. package/examples/framework-postgres-crud/README.md +73 -0
  97. package/examples/framework-postgres-crud/app.ts +64 -0
  98. package/examples/framework-sqlite-crud/README.md +52 -0
  99. package/examples/framework-sqlite-crud/app.ts +90 -0
  100. package/examples/framework-sqlite-crud/appsettings.json +11 -0
  101. package/examples/framework-sqlserver-crud/README.md +73 -0
  102. package/examples/framework-sqlserver-crud/app.ts +64 -0
  103. package/examples/framework-validation-errors/README.md +12 -0
  104. package/examples/framework-validation-errors/app.ts +16 -0
  105. package/examples/fs-basic/README.md +24 -0
  106. package/examples/fs-basic/app.js +12 -0
  107. package/examples/fs-roots-policy/README.md +14 -0
  108. package/examples/fs-roots-policy/app.js +4 -0
  109. package/examples/fs-streams/README.md +18 -0
  110. package/examples/fs-streams/app.js +11 -0
  111. package/examples/fs-watch/README.md +19 -0
  112. package/examples/fs-watch/app.js +11 -0
  113. package/examples/hello/README.md +63 -0
  114. package/examples/hello/app.js +19 -0
  115. package/examples/hello-minimal/README.md +51 -0
  116. package/examples/hello-minimal/sloppy.json +5 -0
  117. package/examples/hello-minimal/src/main.ts +9 -0
  118. package/examples/http-client-basic/README.md +11 -0
  119. package/examples/http-client-basic/app.js +46 -0
  120. package/examples/http-client-generated/README.md +22 -0
  121. package/examples/http-client-generated/openapi.json +45 -0
  122. package/examples/http-client-resilience/README.md +4 -0
  123. package/examples/http-client-resilience/app.js +38 -0
  124. package/examples/http-client-runtime-loopback/README.md +24 -0
  125. package/examples/http-client-testhost/README.md +4 -0
  126. package/examples/http-client-testhost/app.js +27 -0
  127. package/examples/http-client-testhost-package-mock/README.md +26 -0
  128. package/examples/http-client-typed/README.md +5 -0
  129. package/examples/http-client-typed/app.js +33 -0
  130. package/examples/modules-api/README.md +30 -0
  131. package/examples/modules-api/app.js +9 -0
  132. package/examples/modules-api/modules/routes.js +16 -0
  133. package/examples/modules-api/sloppy.json +5 -0
  134. package/examples/modules-basic/README.md +32 -0
  135. package/examples/modules-basic/app.js +41 -0
  136. package/examples/net-deadline-cancel/README.md +13 -0
  137. package/examples/net-deadline-cancel/app.js +34 -0
  138. package/examples/net-local-ipc/README.md +12 -0
  139. package/examples/net-local-ipc/app.js +46 -0
  140. package/examples/net-policy-strict/README.md +12 -0
  141. package/examples/net-policy-strict/app.js +34 -0
  142. package/examples/net-tcp-client/README.md +10 -0
  143. package/examples/net-tcp-client/app.js +23 -0
  144. package/examples/net-tcp-echo/README.md +11 -0
  145. package/examples/net-tcp-echo/app.js +45 -0
  146. package/examples/net-tcp-server/README.md +10 -0
  147. package/examples/net-tcp-server/app.js +28 -0
  148. package/examples/node-compat-path-events/README.md +15 -0
  149. package/examples/node-compat-path-events/sloppy.json +6 -0
  150. package/examples/node-compat-path-events/src/main.ts +15 -0
  151. package/examples/ops-compiler/README.md +9 -0
  152. package/examples/ops-compiler/app.js +26 -0
  153. package/examples/ops-health-metrics-management/README.md +14 -0
  154. package/examples/ops-health-metrics-management/app.js +24 -0
  155. package/examples/orm-basic/README.md +17 -0
  156. package/examples/orm-basic/app.js +82 -0
  157. package/examples/orm-cursor-export/README.md +16 -0
  158. package/examples/orm-cursor-export/app.js +28 -0
  159. package/examples/orm-migrations/README.md +14 -0
  160. package/examples/orm-migrations/migrations/.gitkeep +1 -0
  161. package/examples/orm-migrations/sloppy.json +9 -0
  162. package/examples/orm-migrations/src/app.ts +34 -0
  163. package/examples/orm-relations-includes/README.md +10 -0
  164. package/examples/orm-relations-includes/app.js +47 -0
  165. package/examples/orm-testservices/README.md +37 -0
  166. package/examples/orm-testservices/test.mjs +32 -0
  167. package/examples/os-runtime-api/README.md +11 -0
  168. package/examples/os-runtime-api/app.js +44 -0
  169. package/examples/package-zod-like/README.md +28 -0
  170. package/examples/package-zod-like/fixtures/zod-like/index.js +48 -0
  171. package/examples/package-zod-like/fixtures/zod-like/package.json +12 -0
  172. package/examples/package-zod-like/package.json +7 -0
  173. package/examples/package-zod-like/sloppy.json +6 -0
  174. package/examples/package-zod-like/src/main.ts +16 -0
  175. package/examples/postgres-basic/README.md +31 -0
  176. package/examples/postgres-basic/app.js +50 -0
  177. package/examples/prealpha-control-plane/README.md +50 -0
  178. package/examples/prealpha-control-plane/appsettings.Development.json +11 -0
  179. package/examples/prealpha-control-plane/appsettings.json +15 -0
  180. package/examples/prealpha-control-plane/sloppy.json +5 -0
  181. package/examples/prealpha-control-plane/src/db/schema.js +7 -0
  182. package/examples/prealpha-control-plane/src/db/seed.js +6 -0
  183. package/examples/prealpha-control-plane/src/main.js +21 -0
  184. package/examples/prealpha-control-plane/src/routes/apps.js +34 -0
  185. package/examples/prealpha-control-plane/src/routes/builds.js +25 -0
  186. package/examples/prealpha-control-plane/src/routes/deployments.js +19 -0
  187. package/examples/prealpha-control-plane/src/routes/diagnostics.js +11 -0
  188. package/examples/prealpha-control-plane/src/routes/health.js +27 -0
  189. package/examples/prealpha-control-plane/src/routes/projects.js +38 -0
  190. package/examples/prealpha-control-plane/src/services/diagnosticsSink.js +11 -0
  191. package/examples/prealpha-control-plane/src/services/repositories.js +9 -0
  192. package/examples/prealpha-control-plane/src/validation/schemas.js +6 -0
  193. package/examples/program-fs-process/README.md +31 -0
  194. package/examples/program-fs-process/sloppy.json +9 -0
  195. package/examples/program-fs-process/src/main.ts +27 -0
  196. package/examples/program-hello/README.md +32 -0
  197. package/examples/program-hello/main.ts +8 -0
  198. package/examples/program-hello/message.ts +1 -0
  199. package/examples/program-hello/sloppy.json +5 -0
  200. package/examples/rate-limit-auth/README.md +3 -0
  201. package/examples/rate-limit-auth/app.js +14 -0
  202. package/examples/rate-limit-basic/README.md +3 -0
  203. package/examples/rate-limit-basic/app.js +13 -0
  204. package/examples/rate-limit-redis/README.md +5 -0
  205. package/examples/rate-limit-redis/app.js +20 -0
  206. package/examples/rate-limit-testhost/README.md +4 -0
  207. package/examples/rate-limit-testhost/app.js +13 -0
  208. package/examples/rate-limit-websocket/README.md +3 -0
  209. package/examples/rate-limit-websocket/app.js +16 -0
  210. package/examples/realtime-auth/README.md +8 -0
  211. package/examples/realtime-auth/app.js +25 -0
  212. package/examples/realtime-auth/test.mjs +43 -0
  213. package/examples/realtime-chat/README.md +8 -0
  214. package/examples/realtime-chat/app.js +32 -0
  215. package/examples/realtime-chat/test.mjs +52 -0
  216. package/examples/realtime-dashboard/README.md +20 -0
  217. package/examples/realtime-dashboard/app.js +37 -0
  218. package/examples/realtime-presence/README.md +8 -0
  219. package/examples/realtime-presence/app.js +32 -0
  220. package/examples/realtime-presence/test.mjs +50 -0
  221. package/examples/realtime-testhost/README.md +8 -0
  222. package/examples/realtime-testhost/test.mjs +31 -0
  223. package/examples/redis-basic/README.md +17 -0
  224. package/examples/redis-basic/app.js +39 -0
  225. package/examples/redis-cache/README.md +14 -0
  226. package/examples/redis-cache/app.js +36 -0
  227. package/examples/redis-locks/README.md +13 -0
  228. package/examples/redis-locks/app.js +49 -0
  229. package/examples/request-context/README.md +32 -0
  230. package/examples/request-context/app.js +15 -0
  231. package/examples/sqlite-basic/README.md +52 -0
  232. package/examples/sqlite-basic/app.js +56 -0
  233. package/examples/sqlserver-basic/README.md +36 -0
  234. package/examples/sqlserver-basic/app.js +59 -0
  235. package/examples/static-files-basic/README.md +11 -0
  236. package/examples/static-files-basic/app.js +12 -0
  237. package/examples/static-files-basic/public/app.js +1 -0
  238. package/examples/static-files-basic/public/site.css +3 -0
  239. package/examples/static-files-package/README.md +12 -0
  240. package/examples/static-files-package/app.js +10 -0
  241. package/examples/static-files-package/public/index.html +2 -0
  242. package/examples/static-files-precompressed/README.md +12 -0
  243. package/examples/static-files-precompressed/app.js +11 -0
  244. package/examples/static-files-precompressed/public/app.js +1 -0
  245. package/examples/static-files-precompressed/public/app.js.br +0 -0
  246. package/examples/static-files-precompressed/public/app.js.gz +0 -0
  247. package/examples/static-files-spa/README.md +12 -0
  248. package/examples/static-files-spa/app.js +16 -0
  249. package/examples/static-files-spa/dist/assets/app.js +1 -0
  250. package/examples/static-files-spa/dist/index.html +4 -0
  251. package/examples/static-files-testhost/README.md +8 -0
  252. package/examples/static-files-testhost/app.js +13 -0
  253. package/examples/static-files-testhost/public/app.js +1 -0
  254. package/examples/static-files-testhost/public/app.js.gz +0 -0
  255. package/examples/static-files-testhost/test.mjs +38 -0
  256. package/examples/testhost-basic/README.md +26 -0
  257. package/examples/testhost-db/README.md +31 -0
  258. package/examples/testservices-postgres/README.md +68 -0
  259. package/examples/testservices-redis/README.md +71 -0
  260. package/examples/testservices-sqlserver/README.md +75 -0
  261. package/examples/time-basic/README.md +18 -0
  262. package/examples/time-basic/app.js +12 -0
  263. package/examples/time-deadline-cancellation/README.md +11 -0
  264. package/examples/time-deadline-cancellation/app.js +27 -0
  265. package/examples/time-fake-clock/README.md +14 -0
  266. package/examples/time-fake-clock/app.js +25 -0
  267. package/examples/time-interval-schedule/README.md +13 -0
  268. package/examples/time-interval-schedule/app.js +60 -0
  269. package/examples/users-api-sqlite/README.md +74 -0
  270. package/examples/users-api-sqlite/app.js +11 -0
  271. package/examples/users-api-sqlite/appsettings.Development.json +11 -0
  272. package/examples/users-api-sqlite/appsettings.json +11 -0
  273. package/examples/users-api-sqlite/modules/users.js +40 -0
  274. package/examples/users-api-sqlite/sloppy.json +5 -0
  275. package/examples/validation-errors/README.md +36 -0
  276. package/examples/validation-errors/app.js +14 -0
  277. package/examples/validation-errors/invalid-user.http +6 -0
  278. package/examples/validation-errors/sloppy.json +5 -0
  279. package/examples/web-dynamic-routes/README.md +17 -0
  280. package/examples/web-dynamic-routes/app.ts +27 -0
  281. package/examples/webhooks-basic/README.md +11 -0
  282. package/examples/webhooks-basic/app.js +48 -0
  283. package/examples/websocket-auth/README.md +8 -0
  284. package/examples/websocket-auth/app.js +16 -0
  285. package/examples/websocket-echo/README.md +9 -0
  286. package/examples/websocket-echo/app.js +36 -0
  287. package/examples/websocket-json-schema/README.md +5 -0
  288. package/examples/websocket-json-schema/app.js +25 -0
  289. package/examples/websocket-testhost/README.md +11 -0
  290. package/examples/websocket-testhost/test.mjs +49 -0
  291. package/examples/workers-background-service/README.md +7 -0
  292. package/examples/workers-background-service/app.js +16 -0
  293. package/examples/workers-js-isolate/README.md +8 -0
  294. package/examples/workers-js-isolate/app.js +19 -0
  295. package/examples/workers-js-isolate/workers/parser.ts +11 -0
  296. package/examples/workers-shutdown/README.md +6 -0
  297. package/examples/workers-shutdown/app.js +26 -0
  298. package/examples/workers-workerpool/README.md +6 -0
  299. package/examples/workers-workerpool/app.js +23 -0
  300. package/examples/workers-workqueue/README.md +8 -0
  301. package/examples/workers-workqueue/app.js +24 -0
  302. package/manifest.json +59 -0
  303. package/package.json +31 -0
  304. package/stdlib/sloppy/README.md +177 -0
  305. package/stdlib/sloppy/app.js +2142 -0
  306. package/stdlib/sloppy/auth.js +1813 -0
  307. package/stdlib/sloppy/bootstrap.manifest.json +83 -0
  308. package/stdlib/sloppy/cache.js +1542 -0
  309. package/stdlib/sloppy/codec.js +1153 -0
  310. package/stdlib/sloppy/config.js +61 -0
  311. package/stdlib/sloppy/crypto.js +312 -0
  312. package/stdlib/sloppy/data.js +2945 -0
  313. package/stdlib/sloppy/ffi.js +185 -0
  314. package/stdlib/sloppy/fs.js +795 -0
  315. package/stdlib/sloppy/health.js +603 -0
  316. package/stdlib/sloppy/http.js +1595 -0
  317. package/stdlib/sloppy/index.js +59 -0
  318. package/stdlib/sloppy/internal/bytes.js +31 -0
  319. package/stdlib/sloppy/internal/capabilities.js +155 -0
  320. package/stdlib/sloppy/internal/config.js +640 -0
  321. package/stdlib/sloppy/internal/disposable.js +31 -0
  322. package/stdlib/sloppy/internal/headers.js +63 -0
  323. package/stdlib/sloppy/internal/intrinsics.js +2 -0
  324. package/stdlib/sloppy/internal/json.js +20 -0
  325. package/stdlib/sloppy/internal/logging.js +278 -0
  326. package/stdlib/sloppy/internal/modules.js +405 -0
  327. package/stdlib/sloppy/internal/redaction.js +87 -0
  328. package/stdlib/sloppy/internal/routes.js +2279 -0
  329. package/stdlib/sloppy/internal/runtime-classic.js +19837 -0
  330. package/stdlib/sloppy/internal/services.js +690 -0
  331. package/stdlib/sloppy/internal/shared.js +32 -0
  332. package/stdlib/sloppy/internal/testhost-diagnostics.js +88 -0
  333. package/stdlib/sloppy/internal/testhost-http-server.js +238 -0
  334. package/stdlib/sloppy/internal/testhost-http.js +118 -0
  335. package/stdlib/sloppy/internal/testhost-loopback.js +50 -0
  336. package/stdlib/sloppy/internal/testservices-docker.js +154 -0
  337. package/stdlib/sloppy/internal/validation.js +117 -0
  338. package/stdlib/sloppy/metrics.js +427 -0
  339. package/stdlib/sloppy/net.js +5208 -0
  340. package/stdlib/sloppy/node/assert/strict.js +39 -0
  341. package/stdlib/sloppy/node/assert.js +228 -0
  342. package/stdlib/sloppy/node/buffer.js +247 -0
  343. package/stdlib/sloppy/node/console.js +33 -0
  344. package/stdlib/sloppy/node/constants.js +9 -0
  345. package/stdlib/sloppy/node/crypto.js +89 -0
  346. package/stdlib/sloppy/node/diagnostics_channel.js +41 -0
  347. package/stdlib/sloppy/node/events.js +113 -0
  348. package/stdlib/sloppy/node/fs/promises.js +27 -0
  349. package/stdlib/sloppy/node/fs.js +280 -0
  350. package/stdlib/sloppy/node/http.js +11 -0
  351. package/stdlib/sloppy/node/https.js +11 -0
  352. package/stdlib/sloppy/node/module.js +40 -0
  353. package/stdlib/sloppy/node/os.js +22 -0
  354. package/stdlib/sloppy/node/path.js +78 -0
  355. package/stdlib/sloppy/node/perf_hooks.js +12 -0
  356. package/stdlib/sloppy/node/process.js +129 -0
  357. package/stdlib/sloppy/node/querystring.js +21 -0
  358. package/stdlib/sloppy/node/stream/promises.js +3 -0
  359. package/stdlib/sloppy/node/stream.js +132 -0
  360. package/stdlib/sloppy/node/string_decoder.js +23 -0
  361. package/stdlib/sloppy/node/timers.js +26 -0
  362. package/stdlib/sloppy/node/tty.js +18 -0
  363. package/stdlib/sloppy/node/url.js +17 -0
  364. package/stdlib/sloppy/node/util.js +95 -0
  365. package/stdlib/sloppy/node/zlib.js +72 -0
  366. package/stdlib/sloppy/orm.js +2188 -0
  367. package/stdlib/sloppy/os.js +580 -0
  368. package/stdlib/sloppy/problem-details.js +29 -0
  369. package/stdlib/sloppy/providers/sqlite.js +26 -0
  370. package/stdlib/sloppy/rate-limit.js +856 -0
  371. package/stdlib/sloppy/realtime.js +1508 -0
  372. package/stdlib/sloppy/redis.js +1272 -0
  373. package/stdlib/sloppy/request-id.js +184 -0
  374. package/stdlib/sloppy/request-logging.js +101 -0
  375. package/stdlib/sloppy/results.js +933 -0
  376. package/stdlib/sloppy/schema.js +546 -0
  377. package/stdlib/sloppy/testing.js +4081 -0
  378. package/stdlib/sloppy/testservices.js +1041 -0
  379. package/stdlib/sloppy/time.js +894 -0
  380. package/stdlib/sloppy/webhooks.js +1330 -0
  381. package/stdlib/sloppy/workers.js +986 -0
  382. package/templates/api/README.md +82 -0
  383. package/templates/api/appsettings.Development.json +14 -0
  384. package/templates/api/appsettings.json +13 -0
  385. package/templates/api/data/.gitkeep +1 -0
  386. package/templates/api/gitignore +4 -0
  387. package/templates/api/migrations/0001_create_users.sql +1 -0
  388. package/templates/api/package.json +16 -0
  389. package/templates/api/public/hello.txt +1 -0
  390. package/templates/api/sloppy.json +14 -0
  391. package/templates/api/src/config.ts +1 -0
  392. package/templates/api/src/db/migrate.ts +14 -0
  393. package/templates/api/src/db/schema.ts +4 -0
  394. package/templates/api/src/db/usersRepository.ts +23 -0
  395. package/templates/api/src/main.ts +18 -0
  396. package/templates/api/src/models/user.ts +7 -0
  397. package/templates/api/src/routes/health.ts +20 -0
  398. package/templates/api/src/routes/users.ts +40 -0
  399. package/templates/api/src/services/usersService.ts +21 -0
  400. package/templates/api/tsconfig.json +15 -0
  401. package/templates/cli/README.md +16 -0
  402. package/templates/cli/gitignore +2 -0
  403. package/templates/cli/package.json +13 -0
  404. package/templates/cli/sloppy.json +6 -0
  405. package/templates/cli/src/commands/echo.ts +9 -0
  406. package/templates/cli/src/commands/inspect.ts +20 -0
  407. package/templates/cli/src/main.ts +50 -0
  408. package/templates/cli/tsconfig.json +15 -0
  409. package/templates/minimal-api/README.md +14 -0
  410. package/templates/minimal-api/gitignore +3 -0
  411. package/templates/minimal-api/package.json +14 -0
  412. package/templates/minimal-api/sloppy.json +5 -0
  413. package/templates/minimal-api/src/main.ts +9 -0
  414. package/templates/minimal-api/tsconfig.json +15 -0
  415. package/templates/node-compat/README.md +40 -0
  416. package/templates/node-compat/gitignore +2 -0
  417. package/templates/node-compat/package.json +11 -0
  418. package/templates/node-compat/sloppy.json +6 -0
  419. package/templates/node-compat/src/main.ts +40 -0
  420. package/templates/package-api/README.md +44 -0
  421. package/templates/package-api/fixtures/validator-lite/index.js +7 -0
  422. package/templates/package-api/fixtures/validator-lite/package.json +6 -0
  423. package/templates/package-api/gitignore +3 -0
  424. package/templates/package-api/package.json +17 -0
  425. package/templates/package-api/sloppy.json +5 -0
  426. package/templates/package-api/src/main.ts +10 -0
  427. package/templates/package-api/src/routes/health.ts +5 -0
  428. package/templates/package-api/src/routes/users.ts +12 -0
  429. package/templates/package-api/tsconfig.json +15 -0
  430. package/templates/program/README.md +12 -0
  431. package/templates/program/gitignore +1 -0
  432. package/templates/program/package.json +10 -0
  433. package/templates/program/sloppy.json +6 -0
  434. package/templates/program/src/main.ts +9 -0
@@ -0,0 +1,1595 @@
1
+ import { Text } from "./codec.js";
2
+ import { Random } from "./crypto.js";
3
+ import { disposeAll } from "./internal/disposable.js";
4
+ import { createHeaderLookup } from "./internal/headers.js";
5
+ import { redactHeaders, redactUrlTemplate } from "./internal/redaction.js";
6
+ import {
7
+ isPlainObject,
8
+ optionalNonNegativeInteger,
9
+ optionalPositiveInteger,
10
+ requireHttpToken,
11
+ requirePlainObject,
12
+ } from "./internal/validation.js";
13
+ import { HttpClient } from "./net.js";
14
+
15
+ const CLIENT_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/u;
16
+ const METHOD_WITH_BODY = new Set(["POST", "PUT", "PATCH"]);
17
+ const SAFE_RETRY_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE"]);
18
+ const HTTP_CLIENT_TOKEN_PREFIX = "http.";
19
+ const TYPED_CLIENT_RESERVED_ENDPOINT_NAMES = new Set(["send", "metrics", "diagnostics", "dispose", "close"]);
20
+
21
+ class SloppyHttpClientError extends Error {
22
+ constructor(code, message, options = undefined) {
23
+ super(`${code}: ${message}`);
24
+ this.name = "SloppyHttpClientError";
25
+ this.code = code;
26
+ if (options?.cause !== undefined) {
27
+ this.cause = options.cause;
28
+ }
29
+ if (options !== undefined) {
30
+ for (const [key, value] of Object.entries(options)) {
31
+ if (key !== "cause") {
32
+ this[key] = value;
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ const HttpError = SloppyHttpClientError;
40
+
41
+ function isSchema(value) {
42
+ return value !== null && typeof value === "object" && typeof value.validate === "function";
43
+ }
44
+
45
+ function isConfigReference(value) {
46
+ return value !== null && typeof value === "object" && value.__sloppyConfigReference === true;
47
+ }
48
+
49
+ function validateClientName(name, subject = "Http.client") {
50
+ if (typeof name !== "string" || !CLIENT_NAME_PATTERN.test(name)) {
51
+ throw new TypeError(`${subject} name must start with a letter and contain only letters, digits, '.', '_', or '-'.`);
52
+ }
53
+ return name;
54
+ }
55
+
56
+ function httpServiceToken(name) {
57
+ return `${HTTP_CLIENT_TOKEN_PREFIX}${validateClientName(name, "Http client token")}`;
58
+ }
59
+
60
+ function typedHttpServiceToken(name) {
61
+ return `${httpServiceToken(name)}.typed`;
62
+ }
63
+
64
+ function validateAbsoluteBaseUrl(value, subject) {
65
+ if (isConfigReference(value)) {
66
+ return value;
67
+ }
68
+ if (typeof value !== "string" || value.length === 0) {
69
+ throw new TypeError(`${subject} baseUrl must be an absolute http:// or https:// URL.`);
70
+ }
71
+ let parsed;
72
+ try {
73
+ parsed = new URL(value);
74
+ } catch {
75
+ throw new TypeError(`${subject} baseUrl must be an absolute http:// or https:// URL.`);
76
+ }
77
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || parsed.hash.length !== 0) {
78
+ throw new TypeError(`${subject} baseUrl must be an absolute http:// or https:// URL without a fragment.`);
79
+ }
80
+ return parsed.toString().replace(/\/$/u, "");
81
+ }
82
+
83
+ function validateEndpointPath(path, subject) {
84
+ if (typeof path !== "string" || path.length === 0 || path.includes("#")) {
85
+ throw new TypeError(`${subject} path must be a relative path or absolute http:// or https:// URL without a fragment.`);
86
+ }
87
+ if (path.startsWith("http://") || path.startsWith("https://")) {
88
+ let parsed;
89
+ try {
90
+ parsed = new URL(path);
91
+ } catch {
92
+ throw new TypeError(`${subject} path must be a relative path or absolute http:// or https:// URL without a fragment.`);
93
+ }
94
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || parsed.hash.length !== 0) {
95
+ throw new TypeError(`${subject} path must be a relative path or absolute http:// or https:// URL without a fragment.`);
96
+ }
97
+ return parsed.toString();
98
+ }
99
+ if (!path.startsWith("/")) {
100
+ throw new TypeError(`${subject} path must start with '/' or be an absolute http:// or https:// URL.`);
101
+ }
102
+ if (/[\x00-\x1F\x7F]/u.test(path)) {
103
+ throw new TypeError(`${subject} path must not contain control characters.`);
104
+ }
105
+ return path;
106
+ }
107
+
108
+ function validateHeaders(headers, subject) {
109
+ if (headers === undefined) {
110
+ return undefined;
111
+ }
112
+ requirePlainObject(headers, `${subject} headers must be a plain object.`);
113
+ const normalized = {};
114
+ for (const [name, value] of Object.entries(headers)) {
115
+ requireHttpToken(name, `${subject} header names must be safe HTTP tokens.`);
116
+ if (typeof value !== "string" || /[\x00-\x08\x0A-\x1F\x7F]/u.test(value)) {
117
+ throw new TypeError(`${subject} header values must be safe strings.`);
118
+ }
119
+ normalized[name] = value;
120
+ }
121
+ return Object.freeze(normalized);
122
+ }
123
+
124
+ function positiveInteger(value, subject, defaultValue = undefined) {
125
+ return optionalPositiveInteger(value, `${subject} must be a positive integer.`, defaultValue);
126
+ }
127
+
128
+ function nonNegativeInteger(value, subject, defaultValue = undefined) {
129
+ return optionalNonNegativeInteger(value, `${subject} must be a non-negative integer.`, defaultValue);
130
+ }
131
+
132
+ function optionalDelayMs(options, subject) {
133
+ if (options === undefined) {
134
+ return undefined;
135
+ }
136
+ requirePlainObject(options, `${subject} options must be a plain object.`);
137
+ return nonNegativeInteger(options.delayMs, `${subject} delayMs`);
138
+ }
139
+
140
+ function normalizePoolOptions(value, subject) {
141
+ if (value === undefined) {
142
+ return Object.freeze({
143
+ maxConnectionsPerOrigin: 8,
144
+ idleTimeoutMs: 30000,
145
+ connectionLifetimeMs: undefined,
146
+ pendingQueueLimit: 0,
147
+ pendingQueueTimeoutMs: 1000,
148
+ });
149
+ }
150
+ if (!isPlainObject(value)) {
151
+ throw new TypeError(`${subject} pool must be a plain object.`);
152
+ }
153
+ return Object.freeze({
154
+ maxConnectionsPerOrigin: positiveInteger(value.maxConnectionsPerOrigin, `${subject} pool.maxConnectionsPerOrigin`, 8),
155
+ idleTimeoutMs: nonNegativeInteger(value.idleTimeoutMs, `${subject} pool.idleTimeoutMs`, 30000),
156
+ connectionLifetimeMs: nonNegativeInteger(value.connectionLifetimeMs, `${subject} pool.connectionLifetimeMs`),
157
+ pendingQueueLimit: nonNegativeInteger(value.pendingQueueLimit, `${subject} pool.pendingQueueLimit`, 0),
158
+ pendingQueueTimeoutMs: nonNegativeInteger(value.pendingQueueTimeoutMs, `${subject} pool.pendingQueueTimeoutMs`, 1000),
159
+ });
160
+ }
161
+
162
+ function normalizeRetryPolicy(policy) {
163
+ if (policy === undefined || policy === null) {
164
+ return retryNone();
165
+ }
166
+ if (!isPlainObject(policy) || policy.kind === undefined) {
167
+ throw new TypeError("Http retry policy must come from Http.retry.");
168
+ }
169
+ return policy;
170
+ }
171
+
172
+ function retryNone() {
173
+ return Object.freeze({ kind: "none", maxAttempts: 1 });
174
+ }
175
+
176
+ function normalizeRetryMethods(methods, defaultMethods) {
177
+ if (methods === undefined) {
178
+ return Object.freeze([...defaultMethods]);
179
+ }
180
+ if (!Array.isArray(methods)) {
181
+ throw new TypeError("Http retryOnMethods must be an array.");
182
+ }
183
+ return Object.freeze(methods.map((method) => String(method).toUpperCase()));
184
+ }
185
+
186
+ function retryFixed(options = {}) {
187
+ if (!isPlainObject(options)) {
188
+ throw new TypeError("Http.retry.fixed options must be a plain object.");
189
+ }
190
+ return Object.freeze({
191
+ kind: "fixed",
192
+ maxAttempts: positiveInteger(options.maxAttempts, "Http.retry.fixed maxAttempts", 3),
193
+ delayMs: nonNegativeInteger(options.delayMs, "Http.retry.fixed delayMs", 100),
194
+ retryOnStatus: Object.freeze([...(options.retryOnStatus ?? [408, 429, 500, 502, 503, 504])]),
195
+ retryOnMethods: normalizeRetryMethods(options.retryOnMethods, SAFE_RETRY_METHODS),
196
+ jitter: options.jitter === true,
197
+ allowPostWithIdempotencyKey: options.allowPostWithIdempotencyKey === true,
198
+ });
199
+ }
200
+
201
+ function retryExponential(options = {}) {
202
+ if (!isPlainObject(options)) {
203
+ throw new TypeError("Http.retry.exponential options must be a plain object.");
204
+ }
205
+ return Object.freeze({
206
+ kind: "exponential",
207
+ maxAttempts: positiveInteger(options.maxAttempts, "Http.retry.exponential maxAttempts", 3),
208
+ initialDelayMs: nonNegativeInteger(options.initialDelayMs, "Http.retry.exponential initialDelayMs", 100),
209
+ maxDelayMs: nonNegativeInteger(options.maxDelayMs, "Http.retry.exponential maxDelayMs", 2000),
210
+ retryOnStatus: Object.freeze([...(options.retryOnStatus ?? [408, 429, 500, 502, 503, 504])]),
211
+ retryOnMethods: normalizeRetryMethods(options.retryOnMethods, SAFE_RETRY_METHODS),
212
+ jitter: options.jitter === true,
213
+ allowPostWithIdempotencyKey: options.allowPostWithIdempotencyKey === true,
214
+ });
215
+ }
216
+
217
+ function circuitBreaker(options = {}) {
218
+ if (!isPlainObject(options)) {
219
+ throw new TypeError("Http.circuitBreaker options must be a plain object.");
220
+ }
221
+ const failureRatio = options.failureRatio ?? 0.5;
222
+ if (!Number.isFinite(failureRatio) || failureRatio <= 0 || failureRatio > 1) {
223
+ throw new TypeError("Http.circuitBreaker failureRatio must be greater than 0 and at most 1.");
224
+ }
225
+ return Object.freeze({
226
+ failureRatio,
227
+ minimumThroughput: positiveInteger(options.minimumThroughput, "Http.circuitBreaker minimumThroughput", 10),
228
+ samplingWindowMs: positiveInteger(options.samplingWindowMs, "Http.circuitBreaker samplingWindowMs", 30000),
229
+ breakDurationMs: positiveInteger(options.breakDurationMs, "Http.circuitBreaker breakDurationMs", 30000),
230
+ halfOpenMaxCalls: positiveInteger(options.halfOpenMaxCalls, "Http.circuitBreaker halfOpenMaxCalls", 1),
231
+ });
232
+ }
233
+
234
+ function bulkhead(options = {}) {
235
+ if (!isPlainObject(options)) {
236
+ throw new TypeError("Http.bulkhead options must be a plain object.");
237
+ }
238
+ return Object.freeze({
239
+ maxConcurrent: positiveInteger(options.maxConcurrent, "Http.bulkhead maxConcurrent", 32),
240
+ maxQueue: nonNegativeInteger(options.maxQueue, "Http.bulkhead maxQueue", 128),
241
+ queueTimeoutMs: nonNegativeInteger(options.queueTimeoutMs, "Http.bulkhead queueTimeoutMs", 1000),
242
+ });
243
+ }
244
+
245
+ function timeout(ms) {
246
+ return Object.freeze({ timeoutMs: positiveInteger(ms, "Http.timeout") });
247
+ }
248
+
249
+ function normalizeClientOptions(options = {}, subject = "Http.client") {
250
+ if (!isPlainObject(options)) {
251
+ throw new TypeError(`${subject} options must be a plain object.`);
252
+ }
253
+ const normalized = {
254
+ baseUrl: validateAbsoluteBaseUrl(options.baseUrl, subject),
255
+ headers: validateHeaders(options.headers, subject),
256
+ timeoutMs: positiveInteger(options.timeoutMs, `${subject} timeoutMs`),
257
+ retry: normalizeRetryPolicy(options.retry),
258
+ circuitBreaker: options.circuitBreaker === undefined ? undefined : circuitBreaker(options.circuitBreaker),
259
+ bulkhead: options.bulkhead === undefined ? undefined : bulkhead(options.bulkhead),
260
+ pool: normalizePoolOptions(options.pool, subject),
261
+ metrics: options.metrics === false ? false : true,
262
+ diagnostics: options.diagnostics === false ? false : true,
263
+ };
264
+ return Object.freeze(normalized);
265
+ }
266
+
267
+ function resolveConfigValue(value, config, subject) {
268
+ if (!isConfigReference(value)) {
269
+ return value;
270
+ }
271
+ if (config === undefined || typeof config.require !== "function") {
272
+ throw new SloppyHttpClientError(
273
+ "SLOPPY_E_HTTP_CONFIG_UNAVAILABLE",
274
+ `${subject} requires app config to resolve '${value.key}'.`,
275
+ );
276
+ }
277
+ return validateAbsoluteBaseUrl(config.require(value.key), subject);
278
+ }
279
+
280
+ function createMetrics() {
281
+ const counters = new Map();
282
+ function key(name, labels = {}) {
283
+ return `${name}:${JSON.stringify(Object.keys(labels).sort().map((entry) => [entry, labels[entry]]))}`;
284
+ }
285
+ return {
286
+ increment(name, labels = {}, amount = 1) {
287
+ const id = key(name, labels);
288
+ const current = counters.get(id);
289
+ counters.set(id, {
290
+ name,
291
+ labels: Object.freeze({ ...labels }),
292
+ value: (current?.value ?? 0) + amount,
293
+ });
294
+ },
295
+ snapshot() {
296
+ return Object.freeze(Array.from(counters.values()).map((entry) => Object.freeze({
297
+ name: entry.name,
298
+ labels: entry.labels,
299
+ value: entry.value,
300
+ })));
301
+ },
302
+ };
303
+ }
304
+
305
+ function createDiagnostics(limit = 128) {
306
+ const records = [];
307
+ return {
308
+ record(record) {
309
+ if (records.length >= limit) {
310
+ records.shift();
311
+ }
312
+ records.push(Object.freeze({ ...record, fields: Object.freeze(record.fields ?? {}) }));
313
+ },
314
+ snapshot() {
315
+ return Object.freeze([...records]);
316
+ },
317
+ };
318
+ }
319
+
320
+ function sleep(ms, signal) {
321
+ if (ms <= 0) {
322
+ return Promise.resolve();
323
+ }
324
+ return new Promise((resolve, reject) => {
325
+ let done = false;
326
+ let cleanup = () => {};
327
+ const finish = (fn) => {
328
+ if (done) {
329
+ return;
330
+ }
331
+ done = true;
332
+ clearTimeout(timer);
333
+ cleanup();
334
+ fn();
335
+ };
336
+ const timer = setTimeout(() => finish(resolve), ms);
337
+ if (signal !== undefined && typeof signal.addEventListener === "function") {
338
+ const abort = () => {
339
+ finish(() => reject(new SloppyHttpClientError("SLOPPY_E_HTTP_CANCELLED", "HTTP request was cancelled.")));
340
+ };
341
+ signal.addEventListener("abort", abort, { once: true });
342
+ cleanup = () => signal.removeEventListener?.("abort", abort);
343
+ }
344
+ });
345
+ }
346
+
347
+ function validateWithSchema(schema, value, code, message) {
348
+ if (schema === undefined || schema === null) {
349
+ return value;
350
+ }
351
+ if (!isSchema(schema)) {
352
+ throw new TypeError(`${message} schema must be a Sloppy schema.`);
353
+ }
354
+ const result = schema.validate(value);
355
+ if (!result.ok) {
356
+ throw new SloppyHttpClientError(code, message, { issues: result.issues });
357
+ }
358
+ return result.value;
359
+ }
360
+
361
+ function appendPathQuery(path, query) {
362
+ if (query === undefined || query === null) {
363
+ return path;
364
+ }
365
+ if (!isPlainObject(query)) {
366
+ throw new TypeError("Http request query must be a plain object.");
367
+ }
368
+ const params = new URLSearchParams();
369
+ for (const [key, value] of Object.entries(query)) {
370
+ if (value === undefined || value === null) {
371
+ continue;
372
+ }
373
+ if (Array.isArray(value)) {
374
+ for (const item of value) {
375
+ params.append(key, String(item));
376
+ }
377
+ } else {
378
+ params.append(key, String(value));
379
+ }
380
+ }
381
+ const text = params.toString();
382
+ return text.length === 0 ? path : `${path}?${text}`;
383
+ }
384
+
385
+ function expandPath(path, params = {}) {
386
+ if (params === undefined || params === null) {
387
+ params = {};
388
+ }
389
+ if (!isPlainObject(params)) {
390
+ throw new TypeError("Http request params must be a plain object.");
391
+ }
392
+ return path.replace(/\{([A-Za-z_][0-9A-Za-z_]*)\}/gu, (match, name) => {
393
+ if (!Object.prototype.hasOwnProperty.call(params, name)) {
394
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_MISSING_PATH_PARAM", `HTTP request path parameter '${name}' is missing.`);
395
+ }
396
+ return encodeURIComponent(String(params[name]));
397
+ });
398
+ }
399
+
400
+ function responseStatusClass(status) {
401
+ return `${Math.floor(status / 100)}xx`;
402
+ }
403
+
404
+ function retryDelay(policy, attempt, response) {
405
+ const retryAfter = response?.headers?.get?.("retry-after");
406
+ if (retryAfter !== undefined && retryAfter !== null) {
407
+ const seconds = Number(retryAfter);
408
+ if (Number.isFinite(seconds) && seconds >= 0) {
409
+ return seconds * 1000;
410
+ }
411
+ }
412
+ if (policy.kind === "fixed") {
413
+ return policy.jitter ? randomInteger(policy.delayMs + 1) : policy.delayMs;
414
+ }
415
+ const base = Math.min(policy.maxDelayMs, policy.initialDelayMs * (2 ** Math.max(0, attempt - 1)));
416
+ return policy.jitter ? randomInteger(base + 1) : base;
417
+ }
418
+
419
+ function randomInteger(exclusiveMax) {
420
+ if (exclusiveMax <= 1) {
421
+ return 0;
422
+ }
423
+ const bytes = Random.bytes(4);
424
+ const value = (
425
+ (bytes[0] * 0x1000000) +
426
+ (bytes[1] << 16) +
427
+ (bytes[2] << 8) +
428
+ bytes[3]
429
+ ) >>> 0;
430
+ return value % exclusiveMax;
431
+ }
432
+
433
+ function canRetryMethod(policy, method, requestOptions) {
434
+ if (policy.retryOnMethods.includes(method)) {
435
+ return true;
436
+ }
437
+ if (method === "POST" && policy.allowPostWithIdempotencyKey === true) {
438
+ const headers = requestOptions.headers ?? {};
439
+ return Object.keys(headers).some((name) => name.toLowerCase() === "idempotency-key");
440
+ }
441
+ return false;
442
+ }
443
+
444
+ class CircuitState {
445
+ constructor(policy) {
446
+ this.policy = policy;
447
+ this.state = "closed";
448
+ this.openedAt = 0;
449
+ this.halfOpenCalls = 0;
450
+ this.samples = [];
451
+ }
452
+
453
+ beforeRequest() {
454
+ if (this.policy === undefined) {
455
+ return;
456
+ }
457
+ const now = Date.now();
458
+ if (this.state === "open") {
459
+ if (now - this.openedAt < this.policy.breakDurationMs) {
460
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_CIRCUIT_OPEN", "HTTP client circuit is open.");
461
+ }
462
+ this.state = "half-open";
463
+ this.halfOpenCalls = 0;
464
+ }
465
+ if (this.state === "half-open") {
466
+ if (this.halfOpenCalls >= this.policy.halfOpenMaxCalls) {
467
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_CIRCUIT_OPEN", "HTTP client circuit is open.");
468
+ }
469
+ this.halfOpenCalls += 1;
470
+ }
471
+ }
472
+
473
+ afterRequest(successful) {
474
+ if (this.policy === undefined) {
475
+ return;
476
+ }
477
+ const now = Date.now();
478
+ if (this.state === "half-open") {
479
+ if (successful) {
480
+ this.state = "closed";
481
+ this.samples = [];
482
+ return;
483
+ }
484
+ this.state = "open";
485
+ this.openedAt = now;
486
+ return;
487
+ }
488
+ this.samples.push({ at: now, successful });
489
+ this.samples = this.samples.filter((sample) => now - sample.at <= this.policy.samplingWindowMs);
490
+ if (this.samples.length < this.policy.minimumThroughput) {
491
+ return;
492
+ }
493
+ const failures = this.samples.filter((sample) => !sample.successful).length;
494
+ if (failures / this.samples.length >= this.policy.failureRatio) {
495
+ this.state = "open";
496
+ this.openedAt = now;
497
+ }
498
+ }
499
+
500
+ snapshot() {
501
+ return Object.freeze({ state: this.state, sampleCount: this.samples.length });
502
+ }
503
+ }
504
+
505
+ class BulkheadState {
506
+ constructor(policy) {
507
+ this.policy = policy;
508
+ this.active = 0;
509
+ this.queue = [];
510
+ this.rejected = 0;
511
+ }
512
+
513
+ async enter(signal) {
514
+ if (this.policy === undefined) {
515
+ return () => {};
516
+ }
517
+ if (this.active < this.policy.maxConcurrent) {
518
+ this.active += 1;
519
+ return () => this.leave();
520
+ }
521
+ if (this.queue.length >= this.policy.maxQueue) {
522
+ this.rejected += 1;
523
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_BULKHEAD_REJECTED", "HTTP client bulkhead queue is full.");
524
+ }
525
+ return await new Promise((resolve, reject) => {
526
+ let timer;
527
+ let cleanupSignal = () => {};
528
+ let settled = false;
529
+ const settle = (fn) => {
530
+ if (settled) {
531
+ return;
532
+ }
533
+ settled = true;
534
+ clearTimeout(timer);
535
+ cleanupSignal();
536
+ fn();
537
+ };
538
+ const record = {
539
+ resolve: () => {
540
+ settle(() => {
541
+ this.active += 1;
542
+ resolve(() => this.leave());
543
+ });
544
+ },
545
+ reject: (error) => {
546
+ settle(() => reject(error));
547
+ },
548
+ };
549
+ this.queue.push(record);
550
+ timer = setTimeout(() => {
551
+ const index = this.queue.indexOf(record);
552
+ if (index >= 0) {
553
+ this.queue.splice(index, 1);
554
+ }
555
+ this.rejected += 1;
556
+ record.reject(new SloppyHttpClientError("SLOPPY_E_HTTP_BULKHEAD_REJECTED", "HTTP client bulkhead queue timed out."));
557
+ }, this.policy.queueTimeoutMs);
558
+ if (signal !== undefined && typeof signal.addEventListener === "function") {
559
+ const abort = () => {
560
+ const index = this.queue.indexOf(record);
561
+ if (index >= 0) {
562
+ this.queue.splice(index, 1);
563
+ }
564
+ record.reject(new SloppyHttpClientError("SLOPPY_E_HTTP_CANCELLED", "HTTP request was cancelled."));
565
+ };
566
+ signal.addEventListener("abort", abort, { once: true });
567
+ cleanupSignal = () => signal.removeEventListener?.("abort", abort);
568
+ }
569
+ });
570
+ }
571
+
572
+ leave() {
573
+ this.active = Math.max(0, this.active - 1);
574
+ const next = this.queue.shift();
575
+ if (next !== undefined) {
576
+ next.resolve();
577
+ }
578
+ }
579
+
580
+ snapshot() {
581
+ return Object.freeze({ active: this.active, queued: this.queue.length, rejected: this.rejected });
582
+ }
583
+ }
584
+
585
+ class HttpClientFactory {
586
+ constructor(options = {}) {
587
+ if (!isPlainObject(options)) {
588
+ throw new TypeError("HttpClientFactory.create options must be a plain object.");
589
+ }
590
+ this._clients = new Map();
591
+ this._closed = false;
592
+ this._disposePromise = undefined;
593
+ for (const client of options.clients ?? []) {
594
+ this.addClient(client);
595
+ }
596
+ }
597
+
598
+ static create(options = {}) {
599
+ return new HttpClientFactory(options);
600
+ }
601
+
602
+ addClient(client) {
603
+ if (this._closed) {
604
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_CLIENT_CLOSED", "HTTP client factory is closed.");
605
+ }
606
+ if (client?.__sloppyHttpClientRegistration?.kind !== "named" &&
607
+ client?.__sloppyHttpClientRegistration?.kind !== "typed")
608
+ {
609
+ throw new TypeError("HttpClientFactory.addClient expects Http.client or Http.typedClient.");
610
+ }
611
+ const name = client.__sloppyHttpClientRegistration.name;
612
+ if (this._clients.has(name)) {
613
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_CLIENT_DUPLICATE", `HTTP client '${name}' is already registered.`);
614
+ }
615
+ this._clients.set(name, client);
616
+ return this;
617
+ }
618
+
619
+ get(name) {
620
+ if (this._closed) {
621
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_CLIENT_CLOSED", "HTTP client factory is closed.");
622
+ }
623
+ validateClientName(name, "HttpClientFactory.get");
624
+ const client = this._clients.get(name);
625
+ if (client === undefined) {
626
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_CLIENT_MISSING", `HTTP client '${name}' is not registered.`);
627
+ }
628
+ return client;
629
+ }
630
+
631
+ dispose() {
632
+ if (this._disposePromise === undefined) {
633
+ this._closed = true;
634
+ this._disposePromise = disposeAll(this._clients.values());
635
+ }
636
+ return this._disposePromise;
637
+ }
638
+ }
639
+
640
+ function createManagedClient(name, options, transport = undefined) {
641
+ const metrics = createMetrics();
642
+ const diagnostics = createDiagnostics();
643
+ const circuit = new CircuitState(options.circuitBreaker);
644
+ const bulk = new BulkheadState(options.bulkhead);
645
+ const lowLevel = transport ?? HttpClient.create({
646
+ baseUrl: options.baseUrl,
647
+ headers: options.headers,
648
+ timeoutMs: options.timeoutMs,
649
+ pool: options.pool,
650
+ });
651
+ let closed = false;
652
+
653
+ function assertOpen() {
654
+ if (closed) {
655
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_CLIENT_CLOSED", `HTTP client '${name}' is closed.`);
656
+ }
657
+ }
658
+
659
+ let closePromise = undefined;
660
+ function closeClient() {
661
+ if (closePromise !== undefined) {
662
+ return closePromise;
663
+ }
664
+ closed = true;
665
+ closePromise = Promise.resolve()
666
+ .then(() => lowLevel.close?.() ?? lowLevel.dispose?.())
667
+ .then(() => undefined);
668
+ return closePromise;
669
+ }
670
+
671
+ async function execute(method, path, requestOptions = {}) {
672
+ assertOpen();
673
+ const targetPath = appendPathQuery(expandPath(path, requestOptions.params), requestOptions.query);
674
+ const safeTemplate = redactUrlTemplate(path, requestOptions.query);
675
+ const headers = { ...(requestOptions.headers ?? {}) };
676
+ if (requestOptions.correlationId !== undefined && headers["x-correlation-id"] === undefined) {
677
+ headers["x-correlation-id"] = String(requestOptions.correlationId);
678
+ }
679
+ const effectiveRetry = normalizeRetryPolicy(requestOptions.retry ?? options.retry);
680
+ let leaveBulkhead = () => {};
681
+ let attempt = 0;
682
+ let lastError;
683
+ try {
684
+ leaveBulkhead = await bulk.enter(requestOptions.signal);
685
+ while (attempt < effectiveRetry.maxAttempts) {
686
+ attempt += 1;
687
+ circuit.beforeRequest();
688
+ let response;
689
+ try {
690
+ response = await lowLevel.request({
691
+ url: targetPath,
692
+ method,
693
+ headers,
694
+ json: requestOptions.json,
695
+ text: requestOptions.text,
696
+ bytes: requestOptions.bytes,
697
+ stream: requestOptions.stream,
698
+ timeoutMs: requestOptions.timeoutMs ?? options.timeoutMs,
699
+ deadline: requestOptions.deadline,
700
+ signal: requestOptions.signal,
701
+ });
702
+ const retryableStatus = effectiveRetry.retryOnStatus?.includes(response.status) === true;
703
+ if (
704
+ retryableStatus &&
705
+ attempt < effectiveRetry.maxAttempts &&
706
+ canRetryMethod(effectiveRetry, method, { headers }) &&
707
+ requestOptions.stream === undefined
708
+ ) {
709
+ if (options.metrics) {
710
+ metrics.increment("http.client.retries.total", { client: name, method, route: path });
711
+ }
712
+ await sleep(retryDelay(effectiveRetry, attempt, response), requestOptions.signal);
713
+ continue;
714
+ }
715
+ const successful = response.status < 500;
716
+ circuit.afterRequest(successful);
717
+ if (options.metrics) {
718
+ metrics.increment("http.client.requests.total", {
719
+ client: name,
720
+ method,
721
+ route: path,
722
+ status: String(response.status),
723
+ statusClass: responseStatusClass(response.status),
724
+ outcome: successful ? "success" : "failure",
725
+ });
726
+ }
727
+ return createResponse(response, { client: name, method, path: safeTemplate, attempt });
728
+ } catch (error) {
729
+ lastError = error;
730
+ circuit.afterRequest(false);
731
+ if (
732
+ attempt < effectiveRetry.maxAttempts &&
733
+ canRetryMethod(effectiveRetry, method, { headers }) &&
734
+ requestOptions.stream === undefined &&
735
+ error?.code !== "SLOPPY_E_HTTP_CANCELLED"
736
+ ) {
737
+ if (options.metrics) {
738
+ metrics.increment("http.client.retries.total", { client: name, method, route: path });
739
+ }
740
+ await sleep(retryDelay(effectiveRetry, attempt), requestOptions.signal);
741
+ continue;
742
+ }
743
+ throw error;
744
+ }
745
+ }
746
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_RETRY_EXHAUSTED", "HTTP client retry attempts were exhausted.", { cause: lastError });
747
+ } catch (error) {
748
+ if (options.diagnostics) {
749
+ diagnostics.record({
750
+ code: error?.code ?? "SLOPPY_E_HTTP_REQUEST_FAILED",
751
+ message: "HTTP client request failed.",
752
+ fields: {
753
+ client: name,
754
+ method,
755
+ path: safeTemplate,
756
+ headers: redactHeaders(headers),
757
+ },
758
+ });
759
+ }
760
+ if (options.metrics) {
761
+ metrics.increment("http.client.errors.total", { client: name, method, route: path });
762
+ }
763
+ throw error;
764
+ } finally {
765
+ leaveBulkhead();
766
+ }
767
+ }
768
+
769
+ return {
770
+ __sloppyHttpTransport: transport,
771
+ name,
772
+ request(method, path, requestOptions = {}) {
773
+ assertOpen();
774
+ return new HttpRequestBuilder(execute, String(method).toUpperCase(), validateEndpointPath(path, "Http.client request"), requestOptions);
775
+ },
776
+ get(path, requestOptions = {}) {
777
+ return this.request("GET", path, requestOptions);
778
+ },
779
+ post(path, requestOptions = {}) {
780
+ return this.request("POST", path, requestOptions);
781
+ },
782
+ put(path, requestOptions = {}) {
783
+ return this.request("PUT", path, requestOptions);
784
+ },
785
+ patch(path, requestOptions = {}) {
786
+ return this.request("PATCH", path, requestOptions);
787
+ },
788
+ delete(path, requestOptions = {}) {
789
+ return this.request("DELETE", path, requestOptions);
790
+ },
791
+ head(path, requestOptions = {}) {
792
+ return this.request("HEAD", path, requestOptions);
793
+ },
794
+ metrics() {
795
+ return Object.freeze({
796
+ counters: options.metrics ? metrics.snapshot() : Object.freeze([]),
797
+ pool: lowLevel.poolStats?.(),
798
+ circuit: circuit.snapshot(),
799
+ bulkhead: bulk.snapshot(),
800
+ });
801
+ },
802
+ diagnostics() {
803
+ return options.diagnostics ? diagnostics.snapshot() : Object.freeze([]);
804
+ },
805
+ health() {
806
+ const circuitState = circuit.snapshot().state;
807
+ return Object.freeze({
808
+ name,
809
+ status: circuitState === "open" ? "unhealthy" : "healthy",
810
+ circuit: circuitState,
811
+ });
812
+ },
813
+ dispose() {
814
+ return closeClient();
815
+ },
816
+ close() {
817
+ return closeClient();
818
+ },
819
+ };
820
+ }
821
+
822
+ class HttpRequestBuilder {
823
+ constructor(execute, method, path, options = {}) {
824
+ this._execute = execute;
825
+ this._method = method;
826
+ this._path = path;
827
+ this._options = { ...options };
828
+ }
829
+
830
+ header(name, value) {
831
+ this._options.headers = { ...(this._options.headers ?? {}), [name]: String(value) };
832
+ return this;
833
+ }
834
+
835
+ headers(headers) {
836
+ this._options.headers = { ...(this._options.headers ?? {}), ...validateHeaders(headers, "Http request") };
837
+ return this;
838
+ }
839
+
840
+ query(query) {
841
+ this._options.query = { ...(this._options.query ?? {}), ...query };
842
+ return this;
843
+ }
844
+
845
+ timeoutMs(timeoutMs) {
846
+ this._options.timeoutMs = positiveInteger(timeoutMs, "Http request timeoutMs");
847
+ return this;
848
+ }
849
+
850
+ jsonBody(value) {
851
+ if (!METHOD_WITH_BODY.has(this._method)) {
852
+ throw new TypeError("Http request JSON body is only supported on POST, PUT, or PATCH.");
853
+ }
854
+ this._options.json = value;
855
+ return this;
856
+ }
857
+
858
+ textBody(value) {
859
+ if (!METHOD_WITH_BODY.has(this._method)) {
860
+ throw new TypeError("Http request text body is only supported on POST, PUT, or PATCH.");
861
+ }
862
+ this._options.text = String(value);
863
+ return this;
864
+ }
865
+
866
+ bytesBody(value) {
867
+ if (!METHOD_WITH_BODY.has(this._method)) {
868
+ throw new TypeError("Http request bytes body is only supported on POST, PUT, or PATCH.");
869
+ }
870
+ this._options.bytes = value;
871
+ return this;
872
+ }
873
+
874
+ send() {
875
+ return this._execute(this._method, this._path, this._options);
876
+ }
877
+
878
+ async text() {
879
+ return await (await this.send()).text();
880
+ }
881
+
882
+ async bytes() {
883
+ return await (await this.send()).bytes();
884
+ }
885
+
886
+ json(schema = undefined) {
887
+ if (METHOD_WITH_BODY.has(this._method) && !isSchema(schema) && schema !== undefined && this._options.json === undefined) {
888
+ this._options.json = schema;
889
+ return this;
890
+ }
891
+ return this.send().then((response) => response.json(schema));
892
+ }
893
+
894
+ async expectStatus(status) {
895
+ return await (await this.send()).expectStatus(status);
896
+ }
897
+
898
+ async expectJson(expected) {
899
+ return await (await this.send()).expectJson(expected);
900
+ }
901
+
902
+ then(resolve, reject) {
903
+ return this.send().then(resolve, reject);
904
+ }
905
+ }
906
+
907
+ function createResponse(response, context) {
908
+ let bodyBytesPromise;
909
+ let jsonPromise;
910
+ async function bodyBytes() {
911
+ if (bodyBytesPromise === undefined) {
912
+ bodyBytesPromise = Promise.resolve(response.bytes()).then((bytes) => new Uint8Array(bytes));
913
+ }
914
+ return new Uint8Array(await bodyBytesPromise);
915
+ }
916
+ async function bodyText() {
917
+ return Text.utf8.decode(await bodyBytes());
918
+ }
919
+ async function bodyJson() {
920
+ if (jsonPromise === undefined) {
921
+ jsonPromise = bodyText().then((text) => JSON.parse(text));
922
+ }
923
+ return await jsonPromise;
924
+ }
925
+ return Object.freeze({
926
+ status: response.status,
927
+ headers: response.headers,
928
+ context: Object.freeze({ ...context }),
929
+ async text() {
930
+ return await bodyText();
931
+ },
932
+ async bytes() {
933
+ return await bodyBytes();
934
+ },
935
+ async json(schema = undefined) {
936
+ const value = await bodyJson();
937
+ return validateWithSchema(schema, value, "SLOPPY_E_HTTP_RESPONSE_VALIDATION_FAILED", "HTTP response validation failed.");
938
+ },
939
+ async problem() {
940
+ return await this.json();
941
+ },
942
+ expectStatus(status) {
943
+ if (this.status !== status) {
944
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_UNEXPECTED_STATUS", `Expected HTTP status ${status}, got ${this.status}.`, { status: this.status });
945
+ }
946
+ return this;
947
+ },
948
+ expectHeader(name, expected) {
949
+ const actual = this.headers.get(name);
950
+ if (expected instanceof RegExp ? !expected.test(String(actual ?? "")) : actual !== expected) {
951
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_UNEXPECTED_HEADER", `Expected HTTP header '${name}' to match.`);
952
+ }
953
+ return this;
954
+ },
955
+ async expectJson(expected) {
956
+ const actual = await this.json();
957
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
958
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_UNEXPECTED_JSON", "Expected HTTP JSON response to match.", { actual, expected });
959
+ }
960
+ return this;
961
+ },
962
+ async expectProblem(expected = {}) {
963
+ const problem = await this.problem();
964
+ for (const [key, value] of Object.entries(expected)) {
965
+ if (problem[key] !== value) {
966
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_UNEXPECTED_PROBLEM", `Expected problem ${key} to be ${value}.`);
967
+ }
968
+ }
969
+ return this;
970
+ },
971
+ throwOnError() {
972
+ if (this.status >= 400) {
973
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_STATUS_ERROR", `HTTP response returned status ${this.status}.`, { status: this.status });
974
+ }
975
+ return this;
976
+ },
977
+ });
978
+ }
979
+
980
+ class EndpointBuilder {
981
+ constructor(method, path) {
982
+ this._method = method;
983
+ this._path = validateEndpointPath(path, `Http.${method.toLowerCase()}`);
984
+ this._paramsSchema = undefined;
985
+ this._querySchema = undefined;
986
+ this._bodySchema = undefined;
987
+ this._returns = new Map();
988
+ }
989
+
990
+ params(schema) {
991
+ this._paramsSchema = schema;
992
+ return this;
993
+ }
994
+
995
+ query(schema) {
996
+ this._querySchema = schema;
997
+ return this;
998
+ }
999
+
1000
+ body(schema) {
1001
+ this._bodySchema = schema;
1002
+ return this;
1003
+ }
1004
+
1005
+ returns(status, schema = undefined) {
1006
+ if (!Number.isInteger(status) || status < 100 || status > 599) {
1007
+ throw new TypeError("Http endpoint return status must be an HTTP status code.");
1008
+ }
1009
+ this._returns.set(status, schema);
1010
+ return this;
1011
+ }
1012
+
1013
+ __build(name) {
1014
+ return Object.freeze({
1015
+ name,
1016
+ method: this._method,
1017
+ path: this._path,
1018
+ paramsSchema: this._paramsSchema,
1019
+ querySchema: this._querySchema,
1020
+ bodySchema: this._bodySchema,
1021
+ returns: new Map(this._returns),
1022
+ });
1023
+ }
1024
+ }
1025
+
1026
+ function endpoint(method, path) {
1027
+ return new EndpointBuilder(method, path);
1028
+ }
1029
+
1030
+ function createTypedClient(name, options = {}, transport = undefined) {
1031
+ validateClientName(name, "Http.typedClient");
1032
+ const endpoints = options.endpoints;
1033
+ if (!isPlainObject(endpoints) || Object.keys(endpoints).length === 0) {
1034
+ throw new TypeError("Http.typedClient endpoints must be a non-empty plain object.");
1035
+ }
1036
+ const normalizedOptions = normalizeClientOptions(options, "Http.typedClient");
1037
+ const named = transport ?? createManagedClient(name, normalizedOptions);
1038
+ const methods = {};
1039
+ const endpointMetadata = [];
1040
+ for (const [endpointName, builder] of Object.entries(endpoints)) {
1041
+ if (typeof endpointName !== "string" || endpointName.length === 0) {
1042
+ throw new TypeError("Http.typedClient endpoint names must be non-empty strings.");
1043
+ }
1044
+ if (TYPED_CLIENT_RESERVED_ENDPOINT_NAMES.has(endpointName)) {
1045
+ throw new TypeError(`Http.typedClient endpoint '${endpointName}' uses a reserved client method name.`);
1046
+ }
1047
+ if (typeof builder?.__build !== "function") {
1048
+ throw new TypeError(`Http.typedClient endpoint '${endpointName}' must come from Http.get/post/put/patch/delete.`);
1049
+ }
1050
+ const contract = builder.__build(endpointName);
1051
+ endpointMetadata.push(contract);
1052
+ methods[endpointName] = async (input = {}, requestOptions = {}) => {
1053
+ if (!isPlainObject(input)) {
1054
+ throw new TypeError(`Http typed endpoint '${endpointName}' input must be a plain object.`);
1055
+ }
1056
+ const params = { ...input };
1057
+ delete params.query;
1058
+ delete params.body;
1059
+ const validatedParams = validateWithSchema(contract.paramsSchema, params, "SLOPPY_E_HTTP_REQUEST_VALIDATION_FAILED", "HTTP request params validation failed.");
1060
+ const validatedQuery = validateWithSchema(contract.querySchema, input.query ?? requestOptions.query, "SLOPPY_E_HTTP_REQUEST_VALIDATION_FAILED", "HTTP request query validation failed.");
1061
+ const validatedBody = validateWithSchema(contract.bodySchema, input.body ?? requestOptions.body, "SLOPPY_E_HTTP_REQUEST_VALIDATION_FAILED", "HTTP request body validation failed.");
1062
+ const response = await named.request(contract.method, contract.path, {
1063
+ ...requestOptions,
1064
+ params: validatedParams,
1065
+ query: validatedQuery,
1066
+ json: validatedBody,
1067
+ }).send();
1068
+ const responseSchema = contract.returns.get(response.status);
1069
+ if (!contract.returns.has(response.status)) {
1070
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_UNEXPECTED_STATUS", `HTTP typed endpoint '${endpointName}' returned unexpected status ${response.status}.`, { status: response.status });
1071
+ }
1072
+ if (response.status >= 400) {
1073
+ const problem = await response.problem().catch(() => undefined);
1074
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_STATUS_ERROR", `HTTP typed endpoint '${endpointName}' returned status ${response.status}.`, { status: response.status, problem });
1075
+ }
1076
+ if (responseSchema === undefined || response.status === 204 || response.status === 304) {
1077
+ return undefined;
1078
+ }
1079
+ return await response.json(responseSchema);
1080
+ };
1081
+ }
1082
+ methods.send = (endpointName, input = {}, requestOptions = {}) => {
1083
+ if (typeof methods[endpointName] !== "function") {
1084
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_ENDPOINT_MISSING", `HTTP typed endpoint '${endpointName}' is not defined.`);
1085
+ }
1086
+ return methods[endpointName](input, requestOptions);
1087
+ };
1088
+ methods.metrics = () => named.metrics();
1089
+ methods.diagnostics = () => named.diagnostics();
1090
+ methods.dispose = () => named.dispose();
1091
+ Object.defineProperty(methods, "__sloppyHttpClientRegistration", {
1092
+ value: Object.freeze({
1093
+ kind: "typed",
1094
+ name,
1095
+ options: normalizedOptions,
1096
+ endpoints: Object.freeze(endpointMetadata),
1097
+ token: methods,
1098
+ namedToken: httpServiceToken(name),
1099
+ createNamed: (config) => createManagedClient(name, resolveClientOptions(normalizedOptions, config)),
1100
+ createTyped: (client) => {
1101
+ const mockTransport = client?.__sloppyHttpTransport;
1102
+ if (mockTransport !== undefined) {
1103
+ return createTypedClient(
1104
+ name,
1105
+ { ...normalizedOptions, baseUrl: "http://testhost.invalid", endpoints },
1106
+ createManagedClient(name, { ...normalizedOptions, baseUrl: "http://testhost.invalid" }, mockTransport),
1107
+ );
1108
+ }
1109
+ return createTypedClient(name, { ...normalizedOptions, endpoints }, client);
1110
+ },
1111
+ }),
1112
+ });
1113
+ Object.defineProperty(methods, "__sloppyHttpClientToken", {
1114
+ value: typedHttpServiceToken(name),
1115
+ });
1116
+ return Object.freeze(methods);
1117
+ }
1118
+
1119
+ function resolveClientOptions(options, config) {
1120
+ return Object.freeze({
1121
+ ...options,
1122
+ baseUrl: resolveConfigValue(options.baseUrl, config, "Http client"),
1123
+ });
1124
+ }
1125
+
1126
+ function createNamedClient(name, options = {}, transport = undefined) {
1127
+ validateClientName(name, "Http.client");
1128
+ const normalizedOptions = normalizeClientOptions(options, "Http.client");
1129
+ const client = createManagedClient(name, normalizedOptions, transport);
1130
+ Object.defineProperty(client, "__sloppyHttpClientRegistration", {
1131
+ value: Object.freeze({
1132
+ kind: "named",
1133
+ name,
1134
+ options: normalizedOptions,
1135
+ token: httpServiceToken(name),
1136
+ createNamed: (config) => createManagedClient(name, resolveClientOptions(normalizedOptions, config)),
1137
+ }),
1138
+ });
1139
+ Object.defineProperty(client, "__sloppyHttpClientToken", {
1140
+ value: httpServiceToken(name),
1141
+ });
1142
+ return Object.freeze(client);
1143
+ }
1144
+
1145
+ function sanitizeIdentifier(value, fallback) {
1146
+ if (/^[A-Za-z_$][0-9A-Za-z_$]*$/u.test(String(value ?? ""))) {
1147
+ return String(value);
1148
+ }
1149
+ const text = String(value ?? "")
1150
+ .replace(/[^0-9A-Za-z_$]+/gu, " ")
1151
+ .trim()
1152
+ .replace(/(^| )([0-9A-Za-z_$])/gu, (_, __, ch) => ch.toUpperCase());
1153
+ const candidate = text.length === 0 ? fallback : text;
1154
+ const prefixed = /^[A-Za-z_$]/u.test(candidate) ? candidate : `_${candidate}`;
1155
+ return /^[A-Za-z_$][0-9A-Za-z_$]*$/u.test(prefixed) ? prefixed : fallback;
1156
+ }
1157
+
1158
+ function generatedEndpointName(method, path, operation) {
1159
+ if (typeof operation?.operationId === "string" && operation.operationId.length !== 0) {
1160
+ return sanitizeIdentifier(operation.operationId, `${method.toLowerCase()}Endpoint`);
1161
+ }
1162
+ const pathName = path
1163
+ .replace(/\{([^}]+)\}/gu, " $1 ")
1164
+ .replace(/[^0-9A-Za-z_$]+/gu, " ")
1165
+ .trim();
1166
+ return sanitizeIdentifier(`${method.toLowerCase()} ${pathName}`, `${method.toLowerCase()}Endpoint`);
1167
+ }
1168
+
1169
+ function generateSchemaExpression(openapi, schemaValue, warnings, subject, componentNames = new Map()) {
1170
+ if (!isPlainObject(schemaValue)) {
1171
+ warnings.push(`${subject}: schema is not an object`);
1172
+ return undefined;
1173
+ }
1174
+ if (typeof schemaValue.$ref === "string") {
1175
+ const name = componentNames.get(schemaValue.$ref);
1176
+ if (name !== undefined) {
1177
+ return name;
1178
+ }
1179
+ warnings.push(`${subject}: unsupported reference ${schemaValue.$ref}`);
1180
+ return undefined;
1181
+ }
1182
+ if (schemaValue.allOf !== undefined || schemaValue.anyOf !== undefined || schemaValue.oneOf !== undefined) {
1183
+ warnings.push(`${subject}: composed schemas are not emitted as client validators`);
1184
+ return undefined;
1185
+ }
1186
+ if (schemaValue.enum !== undefined) {
1187
+ if (!Array.isArray(schemaValue.enum)) {
1188
+ warnings.push(`${subject}: enum must be an array`);
1189
+ return undefined;
1190
+ }
1191
+ return `schema.enum(${JSON.stringify(schemaValue.enum)})`;
1192
+ }
1193
+ let expression;
1194
+ if (schemaValue.type === "string") {
1195
+ expression = "schema.string()";
1196
+ } else if (schemaValue.type === "boolean") {
1197
+ expression = "schema.boolean()";
1198
+ } else if (schemaValue.type === "integer") {
1199
+ expression = "schema.int()";
1200
+ } else if (schemaValue.type === "number") {
1201
+ expression = "schema.number()";
1202
+ } else if (schemaValue.type === "array") {
1203
+ const item = generateSchemaExpression(openapi, schemaValue.items, warnings, `${subject}[]`, componentNames);
1204
+ if (item === undefined) {
1205
+ return undefined;
1206
+ }
1207
+ expression = `schema.array(${item})`;
1208
+ } else if (schemaValue.type === "object" || schemaValue.properties !== undefined) {
1209
+ const properties = isPlainObject(schemaValue.properties) ? schemaValue.properties : {};
1210
+ const required = new Set(Array.isArray(schemaValue.required) ? schemaValue.required : []);
1211
+ const fields = [];
1212
+ for (const key of Object.keys(properties).sort()) {
1213
+ const field = generateSchemaExpression(openapi, properties[key], warnings, `${subject}.${key}`, componentNames);
1214
+ if (field === undefined) {
1215
+ continue;
1216
+ }
1217
+ fields.push(`${JSON.stringify(key)}: ${required.has(key) ? field : `${field}.optional()`}`);
1218
+ }
1219
+ if (schemaValue.additionalProperties !== undefined && schemaValue.additionalProperties !== false) {
1220
+ warnings.push(`${subject}: additionalProperties is not emitted as a client validator`);
1221
+ }
1222
+ expression = `schema.object({ ${fields.join(", ")} })`;
1223
+ } else {
1224
+ warnings.push(`${subject}: unsupported schema type ${String(schemaValue.type ?? "unknown")}`);
1225
+ return undefined;
1226
+ }
1227
+ return schemaValue.nullable === true ? `${expression}.nullable()` : expression;
1228
+ }
1229
+
1230
+ function openApiJsonSchema(operation, direction, status = undefined) {
1231
+ if (direction === "request") {
1232
+ return operation?.requestBody?.content?.["application/json"]?.schema;
1233
+ }
1234
+ return operation?.responses?.[status]?.content?.["application/json"]?.schema;
1235
+ }
1236
+
1237
+ function parametersSchemaExpression(openapi, parameters, location, warnings, subject, componentNames) {
1238
+ const fields = [];
1239
+ for (const parameter of parameters) {
1240
+ if (parameter?.in !== location) {
1241
+ continue;
1242
+ }
1243
+ if (typeof parameter.name !== "string" || parameter.name.length === 0) {
1244
+ warnings.push(`${subject}: ${location} parameter without a static name was skipped`);
1245
+ continue;
1246
+ }
1247
+ const expression = generateSchemaExpression(openapi, parameter.schema, warnings, `${subject}.${location}.${parameter.name}`, componentNames);
1248
+ if (expression === undefined) {
1249
+ continue;
1250
+ }
1251
+ fields.push(`${JSON.stringify(parameter.name)}: ${parameter.required === true ? expression : `${expression}.optional()`}`);
1252
+ }
1253
+ return fields.length === 0 ? undefined : `schema.object({ ${fields.sort().join(", ")} })`;
1254
+ }
1255
+
1256
+ function generateHttpClientFromOpenApi(openapi, options = {}) {
1257
+ if (!isPlainObject(openapi)) {
1258
+ throw new TypeError("Http.generateClientFromOpenApi expects an OpenAPI object.");
1259
+ }
1260
+ if (!isPlainObject(options)) {
1261
+ throw new TypeError("Http.generateClientFromOpenApi options must be a plain object.");
1262
+ }
1263
+ const clientName = String(options.name ?? openapi.info?.title ?? "GeneratedClient");
1264
+ validateClientName(clientName, "Http.generateClientFromOpenApi name");
1265
+ const exportName = sanitizeIdentifier(options.exportName ?? clientName, "GeneratedClient");
1266
+ const baseUrlConfigKey = options.baseUrlConfigKey ?? `${clientName}:BaseUrl`;
1267
+ const warnings = [];
1268
+ const componentNames = new Map();
1269
+ const componentSchemas = openapi.components?.schemas;
1270
+ if (isPlainObject(componentSchemas)) {
1271
+ for (const name of Object.keys(componentSchemas).sort()) {
1272
+ componentNames.set(`#/components/schemas/${name}`, sanitizeIdentifier(name, "Schema"));
1273
+ }
1274
+ }
1275
+ const lines = [
1276
+ "import { Config, Http, schema } from \"sloppy\";",
1277
+ "",
1278
+ ];
1279
+ if (isPlainObject(componentSchemas)) {
1280
+ for (const name of Object.keys(componentSchemas).sort()) {
1281
+ const identifier = componentNames.get(`#/components/schemas/${name}`);
1282
+ const expression = generateSchemaExpression(openapi, componentSchemas[name], warnings, `components.schemas.${name}`, componentNames);
1283
+ if (expression !== undefined) {
1284
+ lines.push(`const ${identifier} = ${expression};`);
1285
+ }
1286
+ }
1287
+ if (Object.keys(componentSchemas).length !== 0) {
1288
+ lines.push("");
1289
+ }
1290
+ }
1291
+ const endpointLines = [];
1292
+ const usedEndpointNames = new Set();
1293
+ const paths = isPlainObject(openapi.paths) ? openapi.paths : {};
1294
+ for (const path of Object.keys(paths).sort()) {
1295
+ const pathItem = paths[path];
1296
+ if (!isPlainObject(pathItem)) {
1297
+ warnings.push(`${path}: path item is not an object`);
1298
+ continue;
1299
+ }
1300
+ for (const method of ["get", "post", "put", "patch", "delete"]) {
1301
+ const operation = pathItem[method];
1302
+ if (!isPlainObject(operation)) {
1303
+ continue;
1304
+ }
1305
+ let endpointName = generatedEndpointName(method, path, operation);
1306
+ while (usedEndpointNames.has(endpointName) || TYPED_CLIENT_RESERVED_ENDPOINT_NAMES.has(endpointName)) {
1307
+ endpointName = `${endpointName}Endpoint`;
1308
+ }
1309
+ usedEndpointNames.add(endpointName);
1310
+ const parameters = [
1311
+ ...(Array.isArray(pathItem.parameters) ? pathItem.parameters : []),
1312
+ ...(Array.isArray(operation.parameters) ? operation.parameters : []),
1313
+ ];
1314
+ const chain = [`Http.${method}(${JSON.stringify(path)})`];
1315
+ const paramsSchema = parametersSchemaExpression(openapi, parameters, "path", warnings, `${method.toUpperCase()} ${path}`, componentNames);
1316
+ if (paramsSchema !== undefined) {
1317
+ chain.push(`.params(${paramsSchema})`);
1318
+ }
1319
+ const querySchema = parametersSchemaExpression(openapi, parameters, "query", warnings, `${method.toUpperCase()} ${path}`, componentNames);
1320
+ if (querySchema !== undefined) {
1321
+ chain.push(`.query(${querySchema})`);
1322
+ }
1323
+ const requestSchema = openApiJsonSchema(operation, "request");
1324
+ if (requestSchema !== undefined) {
1325
+ const expression = generateSchemaExpression(openapi, requestSchema, warnings, `${method.toUpperCase()} ${path} requestBody`, componentNames);
1326
+ if (expression !== undefined) {
1327
+ chain.push(`.body(${expression})`);
1328
+ }
1329
+ }
1330
+ const responses = isPlainObject(operation.responses) ? operation.responses : {};
1331
+ for (const status of Object.keys(responses).sort()) {
1332
+ const numericStatus = Number.parseInt(status, 10);
1333
+ if (!Number.isInteger(numericStatus) || numericStatus < 100 || numericStatus > 599) {
1334
+ warnings.push(`${method.toUpperCase()} ${path}: response status ${status} was skipped`);
1335
+ continue;
1336
+ }
1337
+ const responseSchema = openApiJsonSchema(operation, "response", status);
1338
+ if (responseSchema === undefined) {
1339
+ chain.push(`.returns(${numericStatus})`);
1340
+ continue;
1341
+ }
1342
+ const expression = generateSchemaExpression(openapi, responseSchema, warnings, `${method.toUpperCase()} ${path} response ${status}`, componentNames);
1343
+ chain.push(expression === undefined ? `.returns(${numericStatus})` : `.returns(${numericStatus}, ${expression})`);
1344
+ }
1345
+ endpointLines.push(` ${endpointName}: ${chain.join("\n ")},`);
1346
+ }
1347
+ }
1348
+ for (const warning of warnings) {
1349
+ lines.push(`// Unsupported OpenAPI construct: ${warning}`);
1350
+ }
1351
+ if (warnings.length !== 0) {
1352
+ lines.push("");
1353
+ }
1354
+ lines.push(`export const ${exportName} = Http.typedClient(${JSON.stringify(clientName)}, {`);
1355
+ lines.push(` baseUrl: Config.required(${JSON.stringify(baseUrlConfigKey)}),`);
1356
+ lines.push(" endpoints: {");
1357
+ lines.push(...endpointLines);
1358
+ lines.push(" },");
1359
+ lines.push("});");
1360
+ lines.push("");
1361
+ lines.push(`export default ${exportName};`);
1362
+ lines.push("");
1363
+ return Object.freeze({ source: lines.join("\n"), warnings: Object.freeze([...warnings]) });
1364
+ }
1365
+
1366
+ function mockResponse(status, headers, body) {
1367
+ const bodyBytes = typeof body === "string" ? Text.utf8.encode(body) : body;
1368
+ const headerLookup = createHeaderLookup(headers);
1369
+ return createResponse({
1370
+ status,
1371
+ headers: Object.freeze({
1372
+ get(name) {
1373
+ return headerLookup.get(name) ?? null;
1374
+ },
1375
+ }),
1376
+ async text() {
1377
+ return Text.utf8.decode(bodyBytes);
1378
+ },
1379
+ async bytes() {
1380
+ return new Uint8Array(bodyBytes);
1381
+ },
1382
+ async json() {
1383
+ return JSON.parse(Text.utf8.decode(bodyBytes));
1384
+ },
1385
+ }, { mock: true });
1386
+ }
1387
+
1388
+ function pathPatternToRegExp(path) {
1389
+ const parts = validateEndpointPath(path, "TestHttp.mock").split(/(\{[A-Za-z_][0-9A-Za-z_]*\})/u);
1390
+ return new RegExp(`^${parts.map((part) => {
1391
+ const match = /^\{([A-Za-z_][0-9A-Za-z_]*)\}$/u.exec(part);
1392
+ return match === null ? part.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&") : "([^/]+)";
1393
+ }).join("")}$`, "u");
1394
+ }
1395
+
1396
+ class TestHttpMock {
1397
+ constructor() {
1398
+ this._routes = [];
1399
+ this._routesByKey = new Map();
1400
+ this._routesByMethod = new Map();
1401
+ this._calls = [];
1402
+ this._unexpected = [];
1403
+ }
1404
+
1405
+ _route(method, path) {
1406
+ const key = `${method} ${path}`;
1407
+ let route = this._routesByKey.get(key);
1408
+ if (route === undefined) {
1409
+ route = { method, path, pattern: pathPatternToRegExp(path), responses: [] };
1410
+ this._routes.push(route);
1411
+ this._routesByKey.set(key, route);
1412
+ const methodRoutes = this._routesByMethod.get(method);
1413
+ if (methodRoutes === undefined) {
1414
+ this._routesByMethod.set(method, [route]);
1415
+ } else {
1416
+ methodRoutes.push(route);
1417
+ }
1418
+ }
1419
+ return Object.freeze({
1420
+ replyJson: (status, value, headers = {}, options = undefined) => {
1421
+ route.responses.push({ kind: "json", status, value, headers, delayMs: optionalDelayMs(options, "TestHttp.mock replyJson") });
1422
+ return this;
1423
+ },
1424
+ replyText: (status, value, headers = {}, options = undefined) => {
1425
+ route.responses.push({ kind: "text", status, value, headers, delayMs: optionalDelayMs(options, "TestHttp.mock replyText") });
1426
+ return this;
1427
+ },
1428
+ replyBytes: (status, value, headers = {}, options = undefined) => {
1429
+ route.responses.push({ kind: "bytes", status, value, headers, delayMs: optionalDelayMs(options, "TestHttp.mock replyBytes") });
1430
+ return this;
1431
+ },
1432
+ timeout: () => {
1433
+ route.responses.push({ kind: "timeout" });
1434
+ return this;
1435
+ },
1436
+ connectionError: () => {
1437
+ route.responses.push({ kind: "error" });
1438
+ return this;
1439
+ },
1440
+ });
1441
+ }
1442
+
1443
+ get(path) { return this._route("GET", path); }
1444
+ post(path) { return this._route("POST", path); }
1445
+ put(path) { return this._route("PUT", path); }
1446
+ patch(path) { return this._route("PATCH", path); }
1447
+ delete(path) { return this._route("DELETE", path); }
1448
+
1449
+ async _dispatchRaw({ method, url, json, text, bytes, headers, signal }) {
1450
+ const rawUrl = String(url);
1451
+ const path = rawUrl.startsWith("http://") || rawUrl.startsWith("https://")
1452
+ ? new URL(rawUrl).pathname
1453
+ : rawUrl.split("?")[0];
1454
+ const route = this._routesByMethod.get(method)?.find((candidate) => candidate.pattern.test(path));
1455
+ const call = Object.freeze({ method, path, url, json, text, bytes, headers: redactHeaders(headers) });
1456
+ this._calls.push(call);
1457
+ if (route === undefined) {
1458
+ this._unexpected.push(call);
1459
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_MOCK_UNEXPECTED_CALL", `Unexpected outbound HTTP call ${method} ${path}.`, { call });
1460
+ }
1461
+ const response = route.responses.length > 1 ? route.responses.shift() : route.responses[0];
1462
+ if (response === undefined) {
1463
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_MOCK_EXHAUSTED", `No mock response configured for ${method} ${path}.`);
1464
+ }
1465
+ if (response.kind === "timeout") {
1466
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_TIMEOUT", "Mock HTTP request timed out.");
1467
+ }
1468
+ if (response.kind === "error") {
1469
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_CONNECT_FAILED", "Mock HTTP connection failed.");
1470
+ }
1471
+ if (response.delayMs !== undefined) {
1472
+ await sleep(response.delayMs, signal);
1473
+ }
1474
+ const responseHeaders = { ...response.headers };
1475
+ let body;
1476
+ if (response.kind === "json") {
1477
+ body = Text.utf8.encode(JSON.stringify(response.value));
1478
+ responseHeaders["content-type"] ??= "application/json; charset=utf-8";
1479
+ } else if (response.kind === "text") {
1480
+ body = Text.utf8.encode(String(response.value));
1481
+ responseHeaders["content-type"] ??= "text/plain; charset=utf-8";
1482
+ } else {
1483
+ body = response.value instanceof Uint8Array ? response.value : new Uint8Array(response.value);
1484
+ }
1485
+ return Object.freeze({
1486
+ status: response.status,
1487
+ headers: Object.freeze(responseHeaders),
1488
+ body: new Uint8Array(body),
1489
+ });
1490
+ }
1491
+
1492
+ createClient(name = "mock", options = {}) {
1493
+ if (!isPlainObject(options)) {
1494
+ throw new TypeError("TestHttp.mock createClient options must be a plain object.");
1495
+ }
1496
+ const transport = Object.freeze({
1497
+ request: async (request) => {
1498
+ const response = await this._dispatchRaw(request);
1499
+ return mockResponse(response.status, response.headers, response.body);
1500
+ },
1501
+ poolStats: () => Object.freeze({
1502
+ connectionsCreated: 0,
1503
+ connectionsReused: Math.max(0, this._calls.length - 1),
1504
+ connectionsClosedIdle: 0,
1505
+ connectionsClosed: 0,
1506
+ poolWaitCount: 0,
1507
+ poolRejectedCount: 0,
1508
+ activeRequests: 0,
1509
+ idleConnections: 0,
1510
+ queuedRequests: 0,
1511
+ }),
1512
+ });
1513
+ return createNamedClient(name, { ...options, baseUrl: options.baseUrl ?? "http://testhost.invalid" }, transport);
1514
+ }
1515
+
1516
+ expectCalled(method, path) {
1517
+ if (!this._calls.some((call) => call.method === method && call.path === path)) {
1518
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_MOCK_EXPECTATION_FAILED", `Expected outbound HTTP call ${method} ${path}.`);
1519
+ }
1520
+ return this;
1521
+ }
1522
+
1523
+ expectNoUnexpectedCalls() {
1524
+ if (this._unexpected.length !== 0) {
1525
+ throw new SloppyHttpClientError("SLOPPY_E_HTTP_MOCK_UNEXPECTED_CALL", "Unexpected outbound HTTP calls were observed.", { calls: this._unexpected });
1526
+ }
1527
+ return this;
1528
+ }
1529
+ }
1530
+
1531
+ function createTestHttpServiceOverrides(httpClients = {}) {
1532
+ if (!isPlainObject(httpClients)) {
1533
+ throw new TypeError("Sloppy TestHost httpClients overrides must be a plain object.");
1534
+ }
1535
+ const overrides = {};
1536
+ for (const [name, mock] of Object.entries(httpClients)) {
1537
+ validateClientName(name, "TestHost httpClients");
1538
+ if (typeof mock?.createClient !== "function") {
1539
+ throw new TypeError(`Sloppy TestHost httpClients.${name} must come from TestHttp.mock().`);
1540
+ }
1541
+ overrides[httpServiceToken(name)] = mock.createClient(name);
1542
+ }
1543
+ return overrides;
1544
+ }
1545
+
1546
+ const TestHttp = Object.freeze({
1547
+ mock() {
1548
+ return new TestHttpMock();
1549
+ },
1550
+ });
1551
+
1552
+ const Http = Object.freeze({
1553
+ client: createNamedClient,
1554
+ typedClient: createTypedClient,
1555
+ generateClientFromOpenApi: generateHttpClientFromOpenApi,
1556
+ get(path) { return endpoint("GET", path); },
1557
+ post(path) { return endpoint("POST", path); },
1558
+ put(path) { return endpoint("PUT", path); },
1559
+ patch(path) { return endpoint("PATCH", path); },
1560
+ delete(path) { return endpoint("DELETE", path); },
1561
+ retry: Object.freeze({
1562
+ none: retryNone,
1563
+ fixed: retryFixed,
1564
+ exponential: retryExponential,
1565
+ }),
1566
+ circuitBreaker,
1567
+ bulkhead,
1568
+ timeout,
1569
+ clientOptionsSchema: Object.freeze({
1570
+ validate(value) {
1571
+ try {
1572
+ normalizeClientOptions(value, "Http.clientOptionsSchema");
1573
+ return Object.freeze({ ok: true, value });
1574
+ } catch (error) {
1575
+ return Object.freeze({
1576
+ ok: false,
1577
+ issues: Object.freeze([Object.freeze({
1578
+ path: Object.freeze([]),
1579
+ code: "http.client.options",
1580
+ message: error.message,
1581
+ })]),
1582
+ });
1583
+ }
1584
+ },
1585
+ }),
1586
+ });
1587
+
1588
+ export {
1589
+ createTestHttpServiceOverrides,
1590
+ Http,
1591
+ HttpClientFactory,
1592
+ HttpError,
1593
+ SloppyHttpClientError,
1594
+ TestHttp,
1595
+ };