acrylblends 0.6.3 → 0.6.4

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 (269) hide show
  1. package/README.md +3 -2
  2. package/docs/framework/README.md +2 -2
  3. package/docs/framework/UPSTREAM.md +1 -1
  4. package/docs/framework/guides/jsx.md +24 -0
  5. package/docs/framework/helpers/ssg.md +3 -6
  6. package/docs/framework/middleware/builtin/jsx-renderer.md +0 -3
  7. package/docs/framework/upstream.json +2 -2
  8. package/lib/accepts.d.ts +1 -1
  9. package/lib/acryl-base.d.ts +1 -1
  10. package/lib/adapter.d.ts +1 -1
  11. package/lib/{app-wFwpg-bt.d.ts → app-D8riatvv.d.ts} +1 -1
  12. package/lib/basic-auth.d.ts +1 -1
  13. package/lib/bearer-auth.d.ts +1 -1
  14. package/lib/body-limit.d.ts +1 -1
  15. package/lib/cache.d.ts +1 -1
  16. package/lib/cache.js +4 -4
  17. package/lib/{client-BHTr318g.js → client-DDLXwhBo.js} +2 -125
  18. package/lib/client.d.ts +2 -2
  19. package/lib/client.js +2 -1
  20. package/lib/combine.d.ts +1 -1
  21. package/lib/{components-Ce5FRyHU.js → components-0o0CBYNv.js} +2 -2
  22. package/lib/{components-DYZdxJIJ.js → components-DdI7-ns7.js} +1 -1
  23. package/lib/compress.d.ts +1 -1
  24. package/lib/conninfo.d.ts +2 -2
  25. package/lib/{context-DVQBjuns.d.ts → context-DS3fYyG-.d.ts} +1 -1
  26. package/lib/context-storage.d.ts +1 -1
  27. package/lib/cookie.d.ts +1 -1
  28. package/lib/cors.d.ts +1 -1
  29. package/lib/csrf.d.ts +1 -1
  30. package/lib/dev.d.ts +2 -2
  31. package/lib/etag.d.ts +1 -1
  32. package/lib/factory.d.ts +2 -2
  33. package/lib/{hooks-Bq5q9IMl.js → hooks-BMw6BAyP.js} +5 -5
  34. package/lib/{index-CUTfU69R.d.ts → index-Bb7YJKCq.d.ts} +2 -2
  35. package/lib/{index-CsvB-F9j.d.ts → index-BvFh1Gfx.d.ts} +1 -1
  36. package/lib/{index-Cqcdluyn.d.ts → index-CTy4N7ng.d.ts} +2 -2
  37. package/lib/index.d.ts +4 -4
  38. package/lib/ip-restriction.d.ts +2 -2
  39. package/lib/jsx/dom/client.js +1 -1
  40. package/lib/jsx/dom/jsx-dev-runtime.js +1 -1
  41. package/lib/jsx/dom/jsx-runtime.js +1 -1
  42. package/lib/jsx/dom/server.js +1 -1
  43. package/lib/jsx/dom.js +3 -3
  44. package/lib/jsx/streaming.js +1 -1
  45. package/lib/{jsx-dev-runtime-kAhR_4C7.js → jsx-dev-runtime-D18Y35TI.js} +1 -1
  46. package/lib/jsx-renderer.d.ts +75 -0
  47. package/lib/jsx-renderer.js +99 -0
  48. package/lib/jsx.js +3 -3
  49. package/lib/jwk.d.ts +1 -1
  50. package/lib/jwt.d.ts +1 -1
  51. package/lib/language.d.ts +1 -1
  52. package/lib/logger.d.ts +1 -1
  53. package/lib/loopback-only.d.ts +1 -1
  54. package/lib/method-not-allowed.d.ts +2 -2
  55. package/lib/method-override.d.ts +2 -2
  56. package/lib/mount.d.ts +1 -1
  57. package/lib/node-server/conninfo.d.ts +1 -1
  58. package/lib/node-server/serve-static.d.ts +1 -1
  59. package/lib/node-server/serve-static.js +1 -1
  60. package/lib/node-server.d.ts +1 -1
  61. package/lib/node-server.js +1 -1
  62. package/lib/powered-by.d.ts +1 -1
  63. package/lib/pretty-json.d.ts +1 -1
  64. package/lib/quick.d.ts +1 -1
  65. package/lib/request-id.d.ts +1 -1
  66. package/lib/request.d.ts +1 -1
  67. package/lib/route.d.ts +1 -1
  68. package/lib/schema-validator.d.ts +1 -1
  69. package/lib/secure-headers.d.ts +1 -1
  70. package/lib/{serve-static-Cw23shXv.js → serve-static-B14MF1Va.js} +1 -0
  71. package/lib/serve-static.d.ts +1 -1
  72. package/lib/ssg-BwjcA-Ge.js +389 -0
  73. package/lib/ssg.d.ts +156 -0
  74. package/lib/ssg.js +2 -0
  75. package/lib/streaming.d.ts +1 -1
  76. package/lib/testing.d.ts +3 -3
  77. package/lib/testing.js +1 -1
  78. package/lib/timeout.d.ts +1 -1
  79. package/lib/timing.d.ts +1 -1
  80. package/lib/tiny.d.ts +1 -1
  81. package/lib/trailing-slash.d.ts +1 -1
  82. package/lib/{types-nEL8VPDC.d.ts → types-Dv_nV2cv.d.ts} +1 -1
  83. package/lib/{types-CQiGmsWW.d.ts → types-zNxyEpG2.d.ts} +2 -2
  84. package/lib/types.d.ts +1 -1
  85. package/lib/utils/body.d.ts +1 -1
  86. package/lib/utils/ipaddr.d.ts +1 -1
  87. package/lib/utils-Cx61GFMC.js +125 -0
  88. package/lib/validator.d.ts +1 -1
  89. package/lib/ws.d.ts +1 -1
  90. package/package.json +9 -1
  91. package/plugins/acryl-inspect/upstream.json +1 -1
  92. package/plugins/acryl-node-server/upstream.json +1 -1
  93. package/plugins/acryl-router/src/core/helper/ssg/index.ts +14 -0
  94. package/plugins/acryl-router/src/core/helper/ssg/middleware.ts +83 -0
  95. package/plugins/acryl-router/src/core/helper/ssg/plugins.ts +72 -0
  96. package/plugins/acryl-router/src/core/helper/ssg/ssg.ts +471 -0
  97. package/plugins/acryl-router/src/core/helper/ssg/utils.ts +118 -0
  98. package/plugins/acryl-router/src/core/jsx/dom/render.ts +6 -6
  99. package/plugins/acryl-router/src/core/middleware/cache/index.ts +4 -4
  100. package/plugins/acryl-router/src/core/middleware/jsx-renderer/index.ts +162 -0
  101. package/plugins/acryl-router/src/helpers/index.ts +1 -0
  102. package/plugins/acryl-router/src/middleware/index.ts +1 -0
  103. package/plugins/acryl-router/upstream.json +2 -2
  104. package/plugins/acryl-router-http/upstream.json +1 -1
  105. package/plugins/acryl-state/upstream.json +1 -1
  106. package/plugins/desktop/upstream.json +1 -1
  107. package/plugins/desktop-web/upstream.json +1 -1
  108. package/plugins/extensions/resolve.ts +4 -0
  109. package/shared/AGENTS.core.md +4 -0
  110. package/templates/agent/tsconfig.json +7 -1
  111. package/templates/todo/upstream.json +1 -1
  112. package/plugins/acryl-inspect/tests/report.spec.ts +0 -69
  113. package/plugins/acryl-inspect/tests/service.spec.ts +0 -121
  114. package/plugins/acryl-node-server/tests/adapter/app.ts +0 -76
  115. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +0 -1
  116. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  117. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +0 -1
  118. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +0 -1
  119. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +0 -1
  120. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +0 -5
  121. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +0 -1
  122. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  123. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +0 -1
  124. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +0 -1
  125. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +0 -1
  126. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +0 -1
  127. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +0 -1
  128. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +0 -1
  129. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +0 -1
  130. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +0 -1
  131. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +0 -34
  132. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +0 -248
  133. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +0 -457
  134. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +0 -23
  135. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +0 -27
  136. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +0 -252
  137. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +0 -190
  138. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +0 -727
  139. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +0 -189
  140. package/plugins/acryl-node-server/tests/adapter/request.test.ts +0 -1230
  141. package/plugins/acryl-node-server/tests/adapter/response.test.ts +0 -285
  142. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +0 -849
  143. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +0 -790
  144. package/plugins/acryl-node-server/tests/adapter/server.test.ts +0 -1312
  145. package/plugins/acryl-node-server/tests/adapter/setup.ts +0 -16
  146. package/plugins/acryl-node-server/tests/adapter/url.test.ts +0 -76
  147. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +0 -23
  148. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +0 -75
  149. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +0 -235
  150. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +0 -260
  151. package/plugins/acryl-node-server/tests/server.spec.ts +0 -141
  152. package/plugins/acryl-router/src/core/app.test.ts +0 -3833
  153. package/plugins/acryl-router/src/core/client/client.test.ts +0 -2313
  154. package/plugins/acryl-router/src/core/client/types.test.ts +0 -149
  155. package/plugins/acryl-router/src/core/client/utils.test.ts +0 -405
  156. package/plugins/acryl-router/src/core/compose.test.ts +0 -751
  157. package/plugins/acryl-router/src/core/context.test.ts +0 -629
  158. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +0 -344
  159. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +0 -55
  160. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +0 -510
  161. package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +0 -506
  162. package/plugins/acryl-router/src/core/helper/css/index.test.tsx +0 -678
  163. package/plugins/acryl-router/src/core/helper/dev/index.test.ts +0 -181
  164. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +0 -434
  165. package/plugins/acryl-router/src/core/helper/html/index.test.ts +0 -78
  166. package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +0 -356
  167. package/plugins/acryl-router/src/core/helper/route/index.test.ts +0 -259
  168. package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +0 -557
  169. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +0 -128
  170. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +0 -34
  171. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +0 -42
  172. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +0 -124
  173. package/plugins/acryl-router/src/core/http-exception.test.ts +0 -45
  174. package/plugins/acryl-router/src/core/jsx/base.test.tsx +0 -97
  175. package/plugins/acryl-router/src/core/jsx/children.test.ts +0 -52
  176. package/plugins/acryl-router/src/core/jsx/components.test.tsx +0 -936
  177. package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +0 -655
  178. package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +0 -128
  179. package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +0 -330
  180. package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +0 -184
  181. package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +0 -149
  182. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +0 -172
  183. package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +0 -3681
  184. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +0 -1344
  185. package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +0 -199
  186. package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +0 -859
  187. package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +0 -40
  188. package/plugins/acryl-router/src/core/jsx/index.test.tsx +0 -1744
  189. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +0 -666
  190. package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +0 -85
  191. package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +0 -1281
  192. package/plugins/acryl-router/src/core/jsx/utils.test.ts +0 -366
  193. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +0 -434
  194. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +0 -1018
  195. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +0 -273
  196. package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +0 -1211
  197. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +0 -355
  198. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +0 -535
  199. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +0 -43
  200. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +0 -532
  201. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +0 -534
  202. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +0 -21
  203. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +0 -503
  204. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +0 -217
  205. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +0 -1135
  206. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +0 -48
  207. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +0 -802
  208. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +0 -552
  209. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +0 -204
  210. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +0 -292
  211. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +0 -221
  212. package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +0 -334
  213. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +0 -37
  214. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +0 -218
  215. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +0 -168
  216. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +0 -671
  217. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +0 -450
  218. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +0 -50
  219. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +0 -70
  220. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +0 -310
  221. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +0 -381
  222. package/plugins/acryl-router/src/core/preset/quick.test.ts +0 -42
  223. package/plugins/acryl-router/src/core/preset/tiny.test.ts +0 -42
  224. package/plugins/acryl-router/src/core/request.test.ts +0 -751
  225. package/plugins/acryl-router/src/core/router/common.case.test.ts +0 -1132
  226. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +0 -80
  227. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +0 -46
  228. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +0 -154
  229. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +0 -265
  230. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +0 -13
  231. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +0 -896
  232. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +0 -8
  233. package/plugins/acryl-router/src/core/types.test.ts +0 -4134
  234. package/plugins/acryl-router/src/core/utils/accept.test.ts +0 -299
  235. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +0 -56
  236. package/plugins/acryl-router/src/core/utils/body.test.ts +0 -496
  237. package/plugins/acryl-router/src/core/utils/buffer.test.ts +0 -147
  238. package/plugins/acryl-router/src/core/utils/color.test.ts +0 -23
  239. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +0 -69
  240. package/plugins/acryl-router/src/core/utils/cookie.test.ts +0 -460
  241. package/plugins/acryl-router/src/core/utils/crypto.test.ts +0 -43
  242. package/plugins/acryl-router/src/core/utils/encode.test.ts +0 -61
  243. package/plugins/acryl-router/src/core/utils/filepath.test.ts +0 -78
  244. package/plugins/acryl-router/src/core/utils/html.test.ts +0 -16
  245. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +0 -149
  246. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +0 -86
  247. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +0 -1788
  248. package/plugins/acryl-router/src/core/utils/mime.test.ts +0 -64
  249. package/plugins/acryl-router/src/core/utils/stream.test.ts +0 -212
  250. package/plugins/acryl-router/src/core/utils/types.test.ts +0 -295
  251. package/plugins/acryl-router/src/core/utils/url.test.ts +0 -384
  252. package/plugins/acryl-router/src/core/validator/utils.test.ts +0 -82
  253. package/plugins/acryl-router/src/core/validator/validator.test.ts +0 -1488
  254. package/plugins/acryl-router/tests/docs-examples.spec.ts +0 -152
  255. package/plugins/acryl-router/tests/jsx.spec.tsx +0 -63
  256. package/plugins/acryl-router/tests/live-install.spec.ts +0 -64
  257. package/plugins/acryl-router/tests/loader.spec.ts +0 -72
  258. package/plugins/acryl-router/tests/loopback-only.spec.ts +0 -80
  259. package/plugins/acryl-router/tests/no-dom/consumer.ts +0 -11
  260. package/plugins/acryl-router/tests/no-dom/tsconfig.json +0 -13
  261. package/plugins/acryl-router/tests/service.spec.ts +0 -426
  262. package/plugins/acryl-router/tests/shadow-state.spec.ts +0 -79
  263. package/plugins/acryl-router-http/tests/adapter.spec.ts +0 -174
  264. package/plugins/acryl-router-http/tests/stream.spec.ts +0 -133
  265. package/plugins/acryl-router-http/tests/surfaces.spec.ts +0 -101
  266. package/plugins/acryl-state/src/react.ts +0 -11
  267. package/plugins/acryl-state/tests/cell.spec.ts +0 -71
  268. package/plugins/acryl-state/tests/react.spec.ts +0 -39
  269. package/plugins/acryl-state/tests/service.spec.ts +0 -132
@@ -1,426 +0,0 @@
1
- /**
2
- * The router as a Cordis service: mounts owned by the plugin that made them, and the lifecycle mechanics the repo protocol requires -
3
- * PENDING while the provider is missing, reactivation when it appears, provider replacement without duplicate registrations, disposal,
4
- * repeated mount and reload, in-flight abort, and streams that end with the router or with the plugin that serves them. Routing itself is covered by the core's own suite.
5
- */
6
-
7
- import { Context, type FiberState } from '@deepseek-ai/cordis'
8
- import { afterEach, describe, expect, it } from 'vitest'
9
- import { streamSSE } from '../src/helpers/index.ts'
10
- import { apply, Acryl, Config, connectionOf, HTTPException, name, type Router } from '../src/index.ts'
11
-
12
- const PENDING = 0 as FiberState.PENDING
13
- const ACTIVE = 2 as FiberState.ACTIVE
14
- const DISPOSED = 4 as FiberState.DISPOSED
15
-
16
- const contexts: Context[] = []
17
- const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
18
-
19
- function root(): Context {
20
- const ctx = new Context()
21
- contexts.push(ctx)
22
- return ctx
23
- }
24
-
25
- const provider = { name, apply }
26
- const config: Config = { trailingSlash: 'strict', disposeGraceMs: 2000 }
27
-
28
- async function mountRouter(): Promise<{ ctx: Context, router: Router }> {
29
- const ctx = root()
30
- ctx.plugin(provider, config)
31
- await settle()
32
- return { ctx, router: ctx.router }
33
- }
34
-
35
- afterEach(async () => {
36
- while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
37
- })
38
-
39
- /** A consumer plugin that needs `router` and mounts one route, the way the Market does. */
40
- function consumer(path: string, label: string) {
41
- return {
42
- name: `consumer${label}`,
43
- inject: ['router'],
44
- apply(ctx: Context): void {
45
- ctx.router.route('/', new Acryl().get(path, c => c.text(label)))
46
- },
47
- }
48
- }
49
-
50
- const get = (router: Router, path: string): Promise<Response> => router.fetch(new Request(`http://acryl.test${path}`))
51
-
52
- describe('acryl-router mounts', () => {
53
- it('serves a mounted app under its path, with the app\'s own routes, middleware and error handler', async () => {
54
- const { router } = await mountRouter()
55
- const api = new Acryl()
56
- .use(async (c, next) => { await next(); c.header('x-api', 'yes') })
57
- .get('/items/:id', c => c.json({ id: c.req.param('id') }))
58
- .get('/boom', () => { throw new Error('boom') })
59
- .onError((error, c) => c.json({ handled: error instanceof Error ? error.message : 'unknown' }, 418))
60
- router.route('/api', api)
61
-
62
- const item = await get(router, '/api/items/a%20b')
63
- expect(await item.json()).toEqual({ id: 'a b' })
64
- expect(item.headers.get('x-api')).toBe('yes')
65
- const boom = await get(router, '/api/boom')
66
- expect(boom.status).toBe(418)
67
- expect(await boom.json()).toEqual({ handled: 'boom' })
68
- expect((await get(router, '/items/a')).status).toBe(404)
69
- })
70
-
71
- it('removes a mount with its remover, idempotently, and keeps the others', async () => {
72
- const { router } = await mountRouter()
73
- const removeA = router.route('/', new Acryl().get('/a', c => c.text('a')))
74
- router.route('/', new Acryl().get('/b', c => c.text('b')))
75
- expect((await get(router, '/a')).status).toBe(200)
76
- removeA()
77
- removeA()
78
- expect((await get(router, '/a')).status).toBe(404)
79
- expect(await (await get(router, '/b')).text()).toBe('b')
80
- router.route('/', new Acryl().get('/a', c => c.text('a2')))
81
- expect(await (await get(router, '/a')).text()).toBe('a2')
82
- })
83
-
84
- it('runs mounts in the order they were made, so an earlier mount\'s middleware wraps a later mount\'s routes', async () => {
85
- const { router } = await mountRouter()
86
- router.route('/', new Acryl().use('/guarded/*', async c => c.text('denied', 401)))
87
- router.route('/', new Acryl().get('/guarded/x', c => c.text('secret')).get('/open', c => c.text('open')))
88
- expect((await get(router, '/guarded/x')).status).toBe(401)
89
- expect(await (await get(router, '/open')).text()).toBe('open')
90
- })
91
-
92
- it('takes one router-wide error handler and one not-found handler, owned by the caller', async () => {
93
- const { router } = await mountRouter()
94
- router.route('/', new Acryl().get('/boom', () => { throw new Error('boom') }).get('/teapot', () => { throw new HTTPException(418, { message: 'short' }) }))
95
- expect((await get(router, '/boom')).status).toBe(500)
96
- expect((await get(router, '/teapot')).status).toBe(418)
97
-
98
- const removeError = router.onError((_error, c) => c.text('custom', 503))
99
- const removeNotFound = router.notFound(c => c.text('nothing here', 404))
100
- expect(() => router.onError((_error, c) => c.text('second'))).toThrow(/already registered/)
101
- expect((await get(router, '/boom')).status).toBe(503)
102
- expect(await (await get(router, '/missing')).text()).toBe('nothing here')
103
- removeError()
104
- removeNotFound()
105
- expect((await get(router, '/boom')).status).toBe(500)
106
- })
107
-
108
- it('treats /a and /a/ as one path when trailingSlash is ignore', async () => {
109
- const ctx = root()
110
- ctx.plugin(provider, { trailingSlash: 'ignore', disposeGraceMs: 2000 })
111
- await settle()
112
- ctx.router.route('/', new Acryl().get('/a', c => c.text('a')))
113
- expect((await get(ctx.router, '/a/')).status).toBe(200)
114
- })
115
-
116
- it('hands the transport\'s connection info to the handler and gives an in-process call an empty one', async () => {
117
- const { router } = await mountRouter()
118
- router.route('/', new Acryl().get('/who', c => c.json({ remote: connectionOf(c).remoteAddress ?? null, port: connectionOf(c).localPort ?? null })))
119
- const over = await router.fetch(new Request('http://acryl.test/who'), { remoteAddress: '127.0.0.1', localPort: 4000 })
120
- expect(await over.json()).toEqual({ remote: '127.0.0.1', port: 4000 })
121
- const inProcess = await router.request('/who')
122
- expect(await inProcess.json()).toEqual({ remote: null, port: null })
123
- })
124
- })
125
-
126
- describe('acryl-router lifecycle through Cordis', () => {
127
- it('leaves a consumer PENDING while router is missing and activates it when router appears', async () => {
128
- const ctx = root()
129
- const fiber = ctx.plugin(consumer('/c', 'A'))
130
- await settle()
131
- expect(fiber.state).toBe(PENDING)
132
-
133
- ctx.plugin(provider, config)
134
- await settle()
135
- expect(fiber.state).toBe(ACTIVE)
136
- expect(await (await get(ctx.router, '/c')).text()).toBe('A')
137
- expect(ctx.router.size).toBe(1)
138
- })
139
-
140
- it('removes a consumer\'s mounts when the consumer unloads and leaves other consumers alone', async () => {
141
- const { ctx, router } = await mountRouter()
142
- const a = ctx.plugin(consumer('/a', 'A'))
143
- ctx.plugin(consumer('/b', 'B'))
144
- await settle()
145
- expect(router.size).toBe(2)
146
-
147
- await a.dispose()
148
- await settle()
149
- expect(a.state).toBe(DISPOSED)
150
- expect((await get(router, '/a')).status).toBe(404)
151
- expect(await (await get(router, '/b')).text()).toBe('B')
152
- expect(router.size).toBe(1)
153
- })
154
-
155
- it('labels a mount by its path with one child per route, so no label grows with the app (an inspector caps label length)', async () => {
156
- const { ctx, router } = await mountRouter()
157
- const big = new Acryl().use('/api/*', async (_c, next) => { await next() })
158
- for (let i = 0; i < 60; i += 1) big.get(`/api/a-fairly-long-resource-name-${String(i)}`, c => c.text('ok'))
159
- const fiber = ctx.plugin({ name: 'big', inject: ['router'], apply: (inner: Context) => { inner.router.route('/', big) } })
160
- await settle()
161
- const [mount, ...rest] = fiber.getEffects()
162
- expect(rest).toEqual([])
163
- expect(mount?.label).toBe('acryl-router: route /')
164
- expect(mount?.children).toHaveLength(61)
165
- expect(mount?.children[0]?.label).toBe('acryl-router: serves ALL /api/*')
166
- expect(mount?.children.every(child => child.label.length < 100 && child.children.length === 0)).toBe(true)
167
- expect(router.size).toBe(1)
168
- await fiber.dispose()
169
- expect(router.size).toBe(0)
170
- })
171
-
172
- it('replaces the provider without leaving doubled registrations: consumers re-register exactly once', async () => {
173
- const ctx = root()
174
- const first = ctx.plugin(provider, config)
175
- const fiber = ctx.plugin(consumer('/c', 'A'))
176
- await settle()
177
- const oldRouter = ctx.router
178
- expect(fiber.state).toBe(ACTIVE)
179
-
180
- await first.dispose()
181
- await settle()
182
- expect(fiber.state).toBe(PENDING)
183
- expect(oldRouter.size).toBe(0)
184
-
185
- ctx.plugin(provider, config)
186
- await settle()
187
- expect(fiber.state).toBe(ACTIVE)
188
- expect(ctx.router).not.toBe(oldRouter)
189
- expect(ctx.router.size).toBe(1)
190
- expect(await (await get(ctx.router, '/c')).text()).toBe('A')
191
- })
192
-
193
- it('serves the routes an app had when it was mounted, whatever else changes later', async () => {
194
- const { ctx, router } = await mountRouter()
195
- const app = new Acryl().get('/early', c => c.text('early'))
196
- router.route('/', app)
197
- app.get('/late', c => c.text('late'))
198
- expect((await get(router, '/early')).status).toBe(200)
199
- expect((await get(router, '/late')).status).toBe(404)
200
- // Another plugin's mount recomposes the router; the late route still is not served.
201
- ctx.plugin(consumer('/other', 'other'))
202
- await settle()
203
- expect((await get(router, '/other')).status).toBe(200)
204
- expect((await get(router, '/late')).status).toBe(404)
205
- })
206
-
207
- it('serves two plugins on the same path in mount order, and hands the path to the other when the first unloads', async () => {
208
- const { ctx, router } = await mountRouter()
209
- const first = ctx.plugin(consumer('/same', 'first'))
210
- await settle()
211
- ctx.plugin(consumer('/same', 'second'))
212
- await settle()
213
- expect(await (await get(router, '/same')).text()).toBe('first')
214
- await first.dispose()
215
- await settle()
216
- expect(await (await get(router, '/same')).text()).toBe('second')
217
- })
218
-
219
- it('runs a reloaded plugin\'s handler once per request: reloading leaves no duplicate route behind, ten times', async () => {
220
- const { ctx, router } = await mountRouter()
221
- let calls = 0
222
- const counting = {
223
- name: 'counting',
224
- inject: ['router'],
225
- apply(inner: Context): void {
226
- inner.router.route('/', new Acryl().use(async (_c, next) => { calls += 1; await next() }).get('/count', c => c.text(String(calls))))
227
- },
228
- }
229
- for (let round = 0; round < 10; round += 1) {
230
- const fiber = ctx.plugin(counting)
231
- await settle()
232
- expect(router.size).toBe(1)
233
- calls = 0
234
- expect(await (await get(router, '/count')).text()).toBe('1')
235
- await fiber.dispose()
236
- await settle()
237
- expect(router.size).toBe(0)
238
- }
239
- })
240
-
241
- it('returns to its baseline after fifty consumer mount and unload cycles', async () => {
242
- const { ctx, router } = await mountRouter()
243
- ctx.plugin(consumer('/stay', 'S'))
244
- await settle()
245
- const baseline = router.size
246
- for (let i = 0; i < 50; i += 1) {
247
- const fiber = ctx.plugin(consumer(`/cycle/${String(i)}`, `C${String(i)}`))
248
- await settle()
249
- expect(router.size).toBe(baseline + 1)
250
- expect(await (await get(router, `/cycle/${String(i)}`)).text()).toBe(`C${String(i)}`)
251
- await fiber.dispose()
252
- await settle()
253
- }
254
- expect(router.size).toBe(baseline)
255
- expect((await get(router, '/cycle/0')).status).toBe(404)
256
- })
257
-
258
- it('aborts an in-flight request and waits for it when the router is disposed, then answers 503', async () => {
259
- const ctx = root()
260
- const fiber = ctx.plugin(provider, config)
261
- await settle()
262
- const router = ctx.router
263
- let aborted = false
264
- router.route('/', new Acryl().get('/slow', async c => {
265
- await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
266
- return c.text('late')
267
- }))
268
- const pending = get(router, '/slow')
269
- await settle()
270
-
271
- await fiber.dispose()
272
- await settle()
273
- expect(aborted).toBe(true)
274
- expect((await pending).status).toBe(499)
275
- expect((await get(router, '/slow')).status).toBe(503)
276
- })
277
-
278
- it('forwards the caller\'s own abort to the handler signal', async () => {
279
- const { router } = await mountRouter()
280
- let seen = false
281
- router.route('/', new Acryl().get('/slow', async c => {
282
- await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { seen = true; reject(new Error('aborted')) }, { once: true }) })
283
- return c.text('x')
284
- }))
285
- const controller = new AbortController()
286
- const pending = router.fetch(new Request('http://acryl.test/slow', { signal: controller.signal }))
287
- await settle()
288
- controller.abort()
289
- expect((await pending).status).toBe(499)
290
- expect(seen).toBe(true)
291
- })
292
-
293
- it('ends a server-sent-event stream when the router is disposed: the stream\'s abort cleanup runs and the reader sees the end', async () => {
294
- const ctx = root()
295
- const fiber = ctx.plugin(provider, config)
296
- await settle()
297
- let cleaned = false
298
- ctx.router.route('/', new Acryl().get('/events', c => streamSSE(c, async stream => {
299
- stream.onAbort(() => { cleaned = true })
300
- await stream.writeSSE({ data: 'first' })
301
- while (!stream.aborted) await stream.sleep(10)
302
- })))
303
- const response = await get(ctx.router, '/events')
304
- expect(response.headers.get('content-type')).toBe('text/event-stream')
305
- const reader = response.body!.getReader()
306
- expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
307
-
308
- await fiber.dispose()
309
- expect(cleaned).toBe(true)
310
- await expect(reader.read()).resolves.toMatchObject({ done: true })
311
- })
312
-
313
- it('ends the stream a plugin opened when that plugin unloads, and leaves another plugin\'s request running', async () => {
314
- const { ctx, router } = await mountRouter()
315
- let cleaned = false
316
- let otherAborted = false
317
- let release!: () => void
318
- const streaming = ctx.plugin({
319
- name: 'streaming',
320
- inject: ['router'],
321
- apply(inner: Context): void {
322
- inner.router.route('/', new Acryl().get('/events', c => streamSSE(c, async stream => {
323
- stream.onAbort(() => { cleaned = true })
324
- await stream.writeSSE({ data: 'first' })
325
- while (!stream.aborted) await stream.sleep(10)
326
- })))
327
- },
328
- })
329
- ctx.plugin({
330
- name: 'other',
331
- inject: ['router'],
332
- apply(inner: Context): void {
333
- inner.router.route('/', new Acryl().get('/slow', async c => {
334
- c.req.raw.signal.addEventListener('abort', () => { otherAborted = true }, { once: true })
335
- await new Promise<void>(resolve => { release = resolve })
336
- return c.text('done')
337
- }))
338
- },
339
- })
340
- await settle()
341
- const response = await get(router, '/events')
342
- const reader = response.body!.getReader()
343
- expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
344
- const slow = get(router, '/slow')
345
- await settle()
346
-
347
- await streaming.dispose()
348
- expect(cleaned).toBe(true)
349
- await expect(reader.read()).resolves.toMatchObject({ done: true })
350
- expect(otherAborted).toBe(false)
351
- release()
352
- expect(await (await slow).text()).toBe('done')
353
- })
354
-
355
- it('aborts a request still inside an unloading plugin\'s middleware, but not one that middleware already passed on', async () => {
356
- const { ctx, router } = await mountRouter()
357
- let guardAborted = false
358
- let handlerAborted = false
359
- let release!: () => void
360
- const guard = ctx.plugin({
361
- name: 'guard',
362
- inject: ['router'],
363
- apply(inner: Context): void {
364
- inner.router.route('/', new Acryl()
365
- .use('/held/*', async (c, next) => {
366
- c.req.raw.signal.addEventListener('abort', () => { guardAborted = true }, { once: true })
367
- await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true }) })
368
- await next()
369
- })
370
- .use('/passed/*', async (_c, next) => { await next() }))
371
- },
372
- })
373
- ctx.plugin({
374
- name: 'handler',
375
- inject: ['router'],
376
- apply(inner: Context): void {
377
- inner.router.route('/', new Acryl()
378
- .get('/held/x', c => c.text('held'))
379
- .get('/passed/x', c => streamSSE(c, async stream => {
380
- stream.onAbort(() => { handlerAborted = true })
381
- await stream.writeSSE({ data: 'open' })
382
- await new Promise<void>(resolve => { release = resolve })
383
- })))
384
- },
385
- })
386
- await settle()
387
- const held = get(router, '/held/x')
388
- const passed = await get(router, '/passed/x')
389
- const reader = passed.body!.getReader()
390
- await reader.read()
391
-
392
- await guard.dispose()
393
- expect(guardAborted).toBe(true)
394
- expect((await held).status).toBe(499)
395
- expect(handlerAborted).toBe(false)
396
- release()
397
- await expect(reader.read()).resolves.toMatchObject({ done: true })
398
- })
399
-
400
- it('keeps a request in flight through waitUntil until its task settles', async () => {
401
- const ctx = root()
402
- const fiber = ctx.plugin(provider, config)
403
- await settle()
404
- let finished = false
405
- ctx.router.route('/', new Acryl().get('/later', c => {
406
- c.executionCtx.waitUntil(new Promise<void>(resolve => { setTimeout(() => { finished = true; resolve() }, 30) }))
407
- return c.text('now')
408
- }))
409
- expect(await (await get(ctx.router, '/later')).text()).toBe('now')
410
- expect(finished).toBe(false)
411
- await fiber.dispose()
412
- expect(finished).toBe(true)
413
- })
414
-
415
- it('gives up on a request that ignores the abort after the grace period instead of hanging disposal', async () => {
416
- const ctx = root()
417
- const fiber = ctx.plugin(provider, { trailingSlash: 'strict', disposeGraceMs: 30 })
418
- await settle()
419
- ctx.router.route('/', new Acryl().get('/stuck', c => { c.executionCtx.waitUntil(new Promise(() => undefined)); return c.text('x') }))
420
- await get(ctx.router, '/stuck')
421
- const started = Date.now()
422
- await fiber.dispose()
423
- expect(Date.now() - started).toBeLessThan(1000)
424
- expect(fiber.state).toBe(DISPOSED)
425
- })
426
- })
@@ -1,79 +0,0 @@
1
- /**
2
- * What a Service's fields look like to the plugins that call it. A plugin reaches a service through its own shadow of the instance
3
- * (`ctx.counter` in plugin A and in plugin B are two different objects over one service), but reads and writes of fields pass through to
4
- * the one instance: a field assigned in a method is seen by every caller, as is state kept in an object that is only mutated. What the
5
- * shadow does change is `this.ctx` inside a method, which `live-install.spec.ts` covers.
6
- */
7
- import { Context, Service } from '@deepseek-ai/cordis'
8
- import { afterEach, describe, expect, it } from 'vitest'
9
-
10
- declare module '@deepseek-ai/cordis' {
11
- interface Context {
12
- counter: Counter
13
- }
14
- }
15
-
16
- const contexts: Context[] = []
17
- const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
18
- afterEach(async () => {
19
- while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
20
- })
21
-
22
- class Counter extends Service {
23
- /** Reassigned by `assign`. */
24
- assigned = 0
25
- /** Created once, only mutated. */
26
- private readonly state = { count: 0 }
27
-
28
- constructor(ctx: Context) {
29
- super(ctx, 'counter')
30
- }
31
-
32
- assign(): void {
33
- this.assigned += 1
34
- }
35
-
36
- mutate(): void {
37
- this.state.count += 1
38
- }
39
-
40
- get count(): number {
41
- return this.state.count
42
- }
43
- }
44
-
45
- async function twoCallers(): Promise<{ a: Context, b: Context }> {
46
- const root = new Context()
47
- contexts.push(root)
48
- root.plugin({ name: 'counter', apply: (ctx: Context) => { new Counter(ctx) } })
49
- const callers: Context[] = []
50
- for (const name of ['a', 'b']) root.plugin({ name, inject: ['counter'], apply: (ctx: Context) => { callers.push(ctx) } })
51
- await settle()
52
- const [a, b] = callers
53
- if (a === undefined || b === undefined) throw new Error('both callers should be active')
54
- return { a, b }
55
- }
56
-
57
- describe('service state and the shadow context', () => {
58
- it('shows a field assigned in a method through one caller to every caller', async () => {
59
- const { a, b } = await twoCallers()
60
- a.counter.assign()
61
- b.counter.assign()
62
- expect(a.counter.assigned).toBe(2)
63
- expect(b.counter.assigned).toBe(2)
64
- })
65
-
66
- it('shows a field assigned from outside through one caller to every caller', async () => {
67
- const { a, b } = await twoCallers()
68
- a.counter.assigned = 5
69
- expect(b.counter.assigned).toBe(5)
70
- })
71
-
72
- it('shares state kept in one object that is only mutated', async () => {
73
- const { a, b } = await twoCallers()
74
- a.counter.mutate()
75
- b.counter.mutate()
76
- expect(a.counter.count).toBe(2)
77
- expect(b.counter.count).toBe(2)
78
- })
79
- })
@@ -1,174 +0,0 @@
1
- /**
2
- * The Web adapter against the real DSH `webServer` listening on a loopback port: a plugin mounts a prefix, requests
3
- * travel over real HTTP into `router.fetch`, and the lifecycle (PENDING, unload, reload) is observed the way a browser
4
- * would see it. The router is the built `acryl-router` entry, as production loads it.
5
- */
6
-
7
- import { Context, type FiberState } from '@deepseek-ai/cordis'
8
- import WebServer from '@deepseek-ai/dsh-host-webserver'
9
- import { apply as routerApply, name as routerName } from '../../acryl-router/src/index.ts'
10
- import { afterEach, describe, expect, it } from 'vitest'
11
- import { Acryl, connectionOf, type RouterHttpService } from '../../acryl-router/src/index.ts'
12
- import { methodNotAllowed } from '../../acryl-router/src/middleware/index.ts'
13
- import { apply, inject, name } from '../src/index.ts'
14
-
15
- const PENDING = 0 as FiberState.PENDING
16
- const ACTIVE = 2 as FiberState.ACTIVE
17
-
18
- const contexts: Context[] = []
19
- const settle = (ms = 0): Promise<void> => new Promise(resolve => setTimeout(resolve, ms))
20
- afterEach(async () => {
21
- while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
22
- })
23
-
24
- async function until(check: () => boolean, label: string): Promise<void> {
25
- for (let i = 0; i < 300; i += 1) { if (check()) return; await settle(10) }
26
- throw new Error(`timed out waiting for: ${label}`)
27
- }
28
-
29
- const adapter = { name, inject, apply }
30
- const router = { name: routerName, apply: routerApply }
31
-
32
- /** A whole stack on one root context: webServer on an OS-assigned port, the router, and the adapter. */
33
- async function stack(): Promise<{ ctx: Context, base: () => string }> {
34
- const ctx = new Context()
35
- contexts.push(ctx)
36
- ctx.plugin(WebServer, { host: '127.0.0.1', port: 0 } as never)
37
- ctx.plugin(router, { trailingSlash: 'strict' })
38
- ctx.plugin(adapter)
39
- await until(() => (ctx as unknown as { webServer?: { port: number } }).webServer?.port !== undefined && ctx.routerHttp !== undefined, 'the stack to start')
40
- return { ctx, base: () => `http://127.0.0.1:${String((ctx as unknown as { webServer: { port: number } }).webServer.port)}` }
41
- }
42
-
43
- function demo(prefix: string, extra?: (app: Acryl) => void) {
44
- return {
45
- name: `demo${prefix.replaceAll('/', '-')}`,
46
- inject: ['router', 'routerHttp'],
47
- apply(ctx: Context): void {
48
- ctx.routerHttp.mount(prefix)
49
- const app = new Acryl()
50
- app.use(methodNotAllowed({ app }))
51
- app.get('/hello/:who', c => c.json({ hello: c.req.param('who'), q: c.req.query('q') ?? null, host: c.req.header('host') !== undefined }))
52
- app.post('/echo', async c => c.json({ got: await c.req.text(), type: c.req.header('content-type') ?? null }, 201))
53
- app.get('/cookies', () => {
54
- const headers = new Headers()
55
- headers.append('set-cookie', 'a=1; Path=/')
56
- headers.append('set-cookie', 'b=2; Path=/')
57
- return new Response('ok', { headers })
58
- })
59
- app.get('/stream', () => new Response(new ReadableStream({
60
- start(controller) { controller.enqueue(new TextEncoder().encode('one,')); controller.enqueue(new TextEncoder().encode('two')); controller.close() },
61
- })))
62
- app.get('/peer', c => c.json({ remote: connectionOf(c).remoteAddress ?? null, localPort: connectionOf(c).localPort ?? null }))
63
- extra?.(app)
64
- ctx.router.route(prefix, app)
65
- },
66
- }
67
- }
68
-
69
- describe('acryl-router-http against a real webServer', () => {
70
- it('serves a mounted prefix over HTTP: params, query, JSON, status', async () => {
71
- const { ctx, base } = await stack()
72
- ctx.plugin(demo('/api/demo'))
73
- await settle(20)
74
- const response = await fetch(`${base()}/api/demo/hello/zed%20x?q=1`)
75
- expect(response.status).toBe(200)
76
- expect(response.headers.get('content-type')).toContain('application/json')
77
- expect(await response.json()).toEqual({ hello: 'zed x', q: '1', host: true })
78
- })
79
-
80
- it('passes the socket peer address as connection info, not as part of the request', async () => {
81
- const { ctx, base } = await stack()
82
- ctx.plugin(demo('/api/demo'))
83
- await settle(20)
84
- const body = await (await fetch(`${base()}/api/demo/peer`)).json() as { remote: string | null, localPort: number | null }
85
- expect(['127.0.0.1', '::ffff:127.0.0.1']).toContain(body.remote)
86
- expect(body.localPort).toBe(Number(new URL(base()).port))
87
- })
88
-
89
- it('streams a request body into the handler and keeps status and headers on the way out', async () => {
90
- const { ctx, base } = await stack()
91
- ctx.plugin(demo('/api/demo'))
92
- await settle(20)
93
- const response = await fetch(`${base()}/api/demo/echo`, { method: 'POST', body: 'payload', headers: { 'content-type': 'text/plain' } })
94
- expect(response.status).toBe(201)
95
- expect(await response.json()).toEqual({ got: 'payload', type: 'text/plain' })
96
- })
97
-
98
- it('keeps repeated set-cookie headers separate and streams a response body', async () => {
99
- const { ctx, base } = await stack()
100
- ctx.plugin(demo('/api/demo'))
101
- await settle(20)
102
- const cookies = await fetch(`${base()}/api/demo/cookies`)
103
- expect(cookies.headers.getSetCookie()).toEqual(['a=1; Path=/', 'b=2; Path=/'])
104
- expect(await (await fetch(`${base()}/api/demo/stream`)).text()).toBe('one,two')
105
- })
106
-
107
- it('answers the router\'s 404 and 405 below the mount, and answers HEAD without a body', async () => {
108
- const { ctx, base } = await stack()
109
- ctx.plugin(demo('/api/demo'))
110
- await settle(20)
111
- const missing = await fetch(`${base()}/api/demo/none`)
112
- expect(missing.status).toBe(404)
113
- expect(await missing.text()).toBe('404 Not Found')
114
- expect((await fetch(`${base()}/api/demo/echo`)).status).toBe(405)
115
- const head = await fetch(`${base()}/api/demo/hello/a`, { method: 'HEAD' })
116
- expect(head.status).toBe(200)
117
- expect(head.headers.get('content-type')).toContain('application/json')
118
- expect(await head.text()).toBe('')
119
- })
120
-
121
- it('aborts the handler signal when the HTTP client disconnects', async () => {
122
- const { ctx, base } = await stack()
123
- let aborted = false
124
- ctx.plugin(demo('/api/demo', app => {
125
- app.get('/slow', async r => {
126
- await new Promise<void>((_resolve, reject) => { r.req.raw.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
127
- return r.text('never')
128
- })
129
- }))
130
- await settle(20)
131
- const controller = new AbortController()
132
- const pending = fetch(`${base()}/api/demo/slow`, { signal: controller.signal }).catch(() => undefined)
133
- await settle(50)
134
- controller.abort()
135
- await pending
136
- await until(() => aborted, 'the handler to see the disconnect')
137
- })
138
-
139
- it('stops serving the prefix when the owning plugin unloads, and serves it again on reload, fifty times', async () => {
140
- const { ctx, base } = await stack()
141
- for (let i = 0; i < 50; i += 1) {
142
- const fiber = ctx.plugin(demo('/api/demo'))
143
- await settle(5)
144
- expect((await fetch(`${base()}/api/demo/hello/a`)).status).toBe(200)
145
- await fiber.dispose()
146
- await settle(5)
147
- // Unmounted: the webServer has no fallback in this stack, so it answers its own empty 404, not the router's body.
148
- const gone = await fetch(`${base()}/api/demo/hello/a`)
149
- expect(gone.status).toBe(404)
150
- expect(await gone.text()).toBe('')
151
- }
152
- expect(ctx.router.size).toBe(0)
153
- })
154
-
155
- it('stays PENDING while webServer is missing and activates when it appears', async () => {
156
- const ctx = new Context()
157
- contexts.push(ctx)
158
- ctx.plugin(router, { trailingSlash: 'strict' })
159
- const fiber = ctx.plugin(adapter)
160
- await settle(20)
161
- expect(fiber.state).toBe(PENDING)
162
- ctx.plugin(WebServer, { host: '127.0.0.1', port: 0 } as never)
163
- await until(() => fiber.state === ACTIVE, 'the adapter to activate')
164
- })
165
-
166
- it('refuses a prefix that is not a clean path and a prefix another plugin already mounted', async () => {
167
- const { ctx } = await stack()
168
- const http: RouterHttpService = ctx.routerHttp
169
- expect(() => http.mount('no-slash')).toThrow(/mountable/)
170
- expect(() => http.mount('/trailing/')).toThrow(/mountable/)
171
- http.mount('/api/once')
172
- expect(() => http.mount('/api/once')).toThrow(/duplicate/)
173
- })
174
- })