acrylblends 0.3.0 → 0.4.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 (315) hide show
  1. package/dist/index.d.ts +6 -1
  2. package/dist/index.js +28 -3
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/acryl-inspect/plugin.json +2 -1
  6. package/plugins/acryl-inspect/src/report.ts +16 -13
  7. package/plugins/acryl-inspect/src/service.ts +9 -7
  8. package/plugins/acryl-inspect/tests/report.spec.ts +7 -8
  9. package/plugins/acryl-inspect/tests/service.spec.ts +11 -10
  10. package/plugins/acryl-inspect/upstream.json +2 -2
  11. package/plugins/acryl-node-server/LICENSE +26 -0
  12. package/plugins/acryl-node-server/plugin.json +2 -1
  13. package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
  14. package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
  15. package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
  16. package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
  17. package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
  18. package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
  19. package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
  20. package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
  21. package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
  22. package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
  23. package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
  24. package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
  25. package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
  26. package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
  27. package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
  28. package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
  29. package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
  30. package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
  31. package/plugins/acryl-node-server/src/index.ts +19 -6
  32. package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
  33. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
  34. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  35. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
  36. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
  37. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
  38. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
  39. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
  40. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  41. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
  42. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
  43. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
  44. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
  45. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
  46. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
  47. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
  48. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
  49. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
  50. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
  51. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
  52. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
  53. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
  54. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
  55. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
  56. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
  57. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
  58. package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
  59. package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
  60. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
  61. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
  62. package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
  63. package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
  64. package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
  65. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
  66. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
  67. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
  68. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
  69. package/plugins/acryl-node-server/tests/server.spec.ts +44 -8
  70. package/plugins/acryl-node-server/upstream.json +3 -3
  71. package/plugins/acryl-router/LICENSE +26 -0
  72. package/plugins/acryl-router/plugin.json +2 -1
  73. package/plugins/acryl-router/src/client.ts +2 -0
  74. package/plugins/acryl-router/src/connection.ts +42 -0
  75. package/plugins/acryl-router/src/core/app-base.ts +544 -0
  76. package/plugins/acryl-router/src/core/app.test.ts +3833 -0
  77. package/plugins/acryl-router/src/core/app.ts +34 -0
  78. package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
  79. package/plugins/acryl-router/src/core/client/client.ts +249 -0
  80. package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
  81. package/plugins/acryl-router/src/core/client/index.ts +17 -0
  82. package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
  83. package/plugins/acryl-router/src/core/client/types.ts +393 -0
  84. package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
  85. package/plugins/acryl-router/src/core/client/utils.ts +117 -0
  86. package/plugins/acryl-router/src/core/compose.test.ts +751 -0
  87. package/plugins/acryl-router/src/core/compose.ts +73 -0
  88. package/plugins/acryl-router/src/core/context.test.ts +629 -0
  89. package/plugins/acryl-router/src/core/context.ts +781 -0
  90. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
  91. package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
  92. package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
  93. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
  94. package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
  95. package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
  96. package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
  97. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
  98. package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
  99. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
  100. package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
  101. package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
  102. package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
  103. package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
  104. package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
  105. package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
  106. package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
  107. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
  108. package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
  109. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
  110. package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
  111. package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
  112. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
  113. package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
  114. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
  115. package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
  116. package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
  117. package/plugins/acryl-router/src/core/http-exception.ts +77 -0
  118. package/plugins/acryl-router/src/core/index.ts +53 -0
  119. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
  120. package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
  121. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
  122. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
  123. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
  124. package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
  125. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
  126. package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
  127. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
  128. package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
  129. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
  130. package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
  131. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
  132. package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
  133. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
  134. package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
  135. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
  136. package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
  137. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
  138. package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
  139. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
  140. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
  141. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
  142. package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
  143. package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
  144. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
  145. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
  146. package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
  147. package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
  148. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
  149. package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
  150. package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
  151. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
  152. package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
  153. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
  154. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
  155. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
  156. package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
  157. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
  158. package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
  159. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
  160. package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
  161. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
  162. package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
  163. package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
  164. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
  165. package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
  166. package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
  167. package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
  168. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
  169. package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
  170. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
  171. package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
  172. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
  173. package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
  174. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
  175. package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
  176. package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
  177. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
  178. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
  179. package/plugins/acryl-router/src/core/request/constants.ts +1 -0
  180. package/plugins/acryl-router/src/core/request.test.ts +751 -0
  181. package/plugins/acryl-router/src/core/request.ts +502 -0
  182. package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
  183. package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
  184. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
  185. package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
  186. package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
  187. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
  188. package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
  189. package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
  190. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
  191. package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
  192. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
  193. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
  194. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
  195. package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
  196. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
  197. package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
  198. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
  199. package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
  200. package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
  201. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
  202. package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
  203. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
  204. package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
  205. package/plugins/acryl-router/src/core/router/utils.ts +1 -0
  206. package/plugins/acryl-router/src/core/router.ts +103 -0
  207. package/plugins/acryl-router/src/core/types.test.ts +4134 -0
  208. package/plugins/acryl-router/src/core/types.ts +2782 -0
  209. package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
  210. package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
  211. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
  212. package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
  213. package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
  214. package/plugins/acryl-router/src/core/utils/body.ts +263 -0
  215. package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
  216. package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
  217. package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
  218. package/plugins/acryl-router/src/core/utils/color.ts +56 -0
  219. package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
  220. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
  221. package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
  222. package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
  223. package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
  224. package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
  225. package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
  226. package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
  227. package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
  228. package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
  229. package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
  230. package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
  231. package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
  232. package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
  233. package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
  234. package/plugins/acryl-router/src/core/utils/html.ts +182 -0
  235. package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
  236. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
  237. package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
  238. package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
  239. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
  240. package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
  241. package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
  242. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
  243. package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
  244. package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
  245. package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
  246. package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
  247. package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
  248. package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
  249. package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
  250. package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
  251. package/plugins/acryl-router/src/core/utils/types.ts +123 -0
  252. package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
  253. package/plugins/acryl-router/src/core/utils/url.ts +337 -0
  254. package/plugins/acryl-router/src/core/validator/index.ts +8 -0
  255. package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
  256. package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
  257. package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
  258. package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
  259. package/plugins/acryl-router/src/helpers/index.ts +13 -3
  260. package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
  261. package/plugins/acryl-router/src/index.ts +6 -6
  262. package/plugins/acryl-router/src/middleware/index.ts +25 -6
  263. package/plugins/acryl-router/src/middleware/loopback-only.ts +19 -13
  264. package/plugins/acryl-router/src/service.ts +256 -64
  265. package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
  266. package/plugins/acryl-router/tests/live-install.spec.ts +2 -2
  267. package/plugins/acryl-router/tests/loader.spec.ts +3 -2
  268. package/plugins/acryl-router/tests/service.spec.ts +264 -66
  269. package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
  270. package/plugins/acryl-router/upstream.json +2 -2
  271. package/plugins/acryl-router-http/plugin.json +2 -1
  272. package/plugins/acryl-router-http/src/bridge.ts +3 -1
  273. package/plugins/acryl-router-http/tests/adapter.spec.ts +16 -12
  274. package/plugins/acryl-router-http/tests/surfaces.spec.ts +12 -10
  275. package/plugins/acryl-router-http/upstream.json +3 -3
  276. package/plugins/acryl-state/NOTICE +30 -0
  277. package/plugins/acryl-state/plugin.json +1 -1
  278. package/plugins/acryl-state/src/cell.ts +1 -1
  279. package/plugins/acryl-state/src/service.ts +33 -15
  280. package/plugins/acryl-state/upstream.json +6 -4
  281. package/plugins/builder/script.ts +4 -7
  282. package/plugins/extensions/index.ts +2 -2
  283. package/plugins/extensions/resolve.ts +28 -3
  284. package/plugins/openai/prompt.ts +9 -7
  285. package/plugins/tsconfig.json +5 -4
  286. package/templates/agent/src/routes.ts +37 -27
  287. package/templates/agent/test/model.test.ts +2 -1
  288. package/templates/agent/tsconfig.json +8 -5
  289. package/templates/blank/src/routes.ts +6 -4
  290. package/templates/blank/tsconfig.json +2 -4
  291. package/templates/todo/src/page.ts +1 -1
  292. package/templates/todo/src/todo.ts +56 -29
  293. package/templates/todo/tsconfig.json +2 -4
  294. package/templates/todo/upstream.json +2 -2
  295. package/plugins/acryl-router/src/compose.ts +0 -21
  296. package/plugins/acryl-router/src/context.ts +0 -59
  297. package/plugins/acryl-router/src/helpers/read-body.ts +0 -59
  298. package/plugins/acryl-router/src/helpers/sse.ts +0 -83
  299. package/plugins/acryl-router/src/helpers/validate.ts +0 -44
  300. package/plugins/acryl-router/src/http-error.ts +0 -21
  301. package/plugins/acryl-router/src/middleware/body-limit.ts +0 -25
  302. package/plugins/acryl-router/src/middleware/cors.ts +0 -46
  303. package/plugins/acryl-router/src/middleware/logger.ts +0 -21
  304. package/plugins/acryl-router/src/middleware/request-id.ts +0 -24
  305. package/plugins/acryl-router/src/middleware/secure-headers.ts +0 -23
  306. package/plugins/acryl-router/src/pattern.ts +0 -89
  307. package/plugins/acryl-router/src/registrar.ts +0 -90
  308. package/plugins/acryl-router/src/table.ts +0 -156
  309. package/plugins/acryl-router/src/types.ts +0 -73
  310. package/plugins/acryl-router/src/util.ts +0 -23
  311. package/plugins/acryl-router/tests/api.spec.ts +0 -223
  312. package/plugins/acryl-router/tests/conformance.ts +0 -170
  313. package/plugins/acryl-router/tests/helpers.spec.ts +0 -172
  314. package/plugins/acryl-router/tests/middleware.spec.ts +0 -183
  315. package/plugins/acryl-router/tests/table.spec.ts +0 -47
@@ -1,13 +1,13 @@
1
1
  /**
2
- * The router as a Cordis service (spec 046, M1): the shared conformance suite through a real `Context`, then the
3
- * lifecycle mechanics the repo protocol requires: PENDING while the provider is missing, reactivation when it appears,
4
- * provider replacement without duplicate registrations, disposal, repeated mount and reload, and in-flight abort.
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
5
  */
6
6
 
7
7
  import { Context, type FiberState } from '@deepseek-ai/cordis'
8
8
  import { afterEach, describe, expect, it } from 'vitest'
9
- import { apply, Config, name, type Router } from '../src/index.ts'
10
- import { conformance, type Candidate } from './conformance.ts'
9
+ import { streamSSE } from '../src/helpers/index.ts'
10
+ import { apply, App, Config, connectionOf, HTTPException, name, type Router } from '../src/index.ts'
11
11
 
12
12
  const PENDING = 0 as FiberState.PENDING
13
13
  const ACTIVE = 2 as FiberState.ACTIVE
@@ -36,30 +36,93 @@ afterEach(async () => {
36
36
  while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
37
37
  })
38
38
 
39
- conformance('the Router service in a Cordis Context', async () => {
40
- const { router } = await mountRouter()
41
- const candidate: Candidate = {
42
- route: (method, path, handler) => router.route(method, path, handler),
43
- use: (path, middleware) => router.use(path, middleware),
44
- fetch: request => router.fetch(request),
45
- size: () => router.size,
46
- }
47
- return candidate
48
- })
49
-
50
- /** A consumer plugin that needs `router` and registers one route, the way the market will. */
39
+ /** A consumer plugin that needs `router` and mounts one route, the way the Market does. */
51
40
  function consumer(path: string, label: string) {
52
41
  return {
53
42
  name: `consumer${label}`,
54
43
  inject: ['router'],
55
44
  apply(ctx: Context): void {
56
- ctx.router.get(path, c => c.text(label))
45
+ ctx.router.route('/', new App().get(path, c => c.text(label)))
57
46
  },
58
47
  }
59
48
  }
60
49
 
61
50
  const get = (router: Router, path: string): Promise<Response> => router.fetch(new Request(`http://acryl.test${path}`))
62
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 App()
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 App().get('/a', c => c.text('a')))
74
+ router.route('/', new App().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 App().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 App().use('/guarded/*', async c => c.text('denied', 401)))
87
+ router.route('/', new App().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 App().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 App().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 App().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
+
63
126
  describe('acryl-router lifecycle through Cordis', () => {
64
127
  it('leaves a consumer PENDING while router is missing and activates it when router appears', async () => {
65
128
  const ctx = root()
@@ -74,7 +137,7 @@ describe('acryl-router lifecycle through Cordis', () => {
74
137
  expect(ctx.router.size).toBe(1)
75
138
  })
76
139
 
77
- it('removes a consumer\'s routes when the consumer unloads and leaves other consumers alone', async () => {
140
+ it('removes a consumer\'s mounts when the consumer unloads and leaves other consumers alone', async () => {
78
141
  const { ctx, router } = await mountRouter()
79
142
  const a = ctx.plugin(consumer('/a', 'A'))
80
143
  ctx.plugin(consumer('/b', 'B'))
@@ -110,6 +173,54 @@ describe('acryl-router lifecycle through Cordis', () => {
110
173
  expect(await (await get(ctx.router, '/c')).text()).toBe('A')
111
174
  })
112
175
 
176
+ it('serves the routes an app had when it was mounted, whatever else changes later', async () => {
177
+ const { ctx, router } = await mountRouter()
178
+ const app = new App().get('/early', c => c.text('early'))
179
+ router.route('/', app)
180
+ app.get('/late', c => c.text('late'))
181
+ expect((await get(router, '/early')).status).toBe(200)
182
+ expect((await get(router, '/late')).status).toBe(404)
183
+ // Another plugin's mount recomposes the router; the late route still is not served.
184
+ ctx.plugin(consumer('/other', 'other'))
185
+ await settle()
186
+ expect((await get(router, '/other')).status).toBe(200)
187
+ expect((await get(router, '/late')).status).toBe(404)
188
+ })
189
+
190
+ it('serves two plugins on the same path in mount order, and hands the path to the other when the first unloads', async () => {
191
+ const { ctx, router } = await mountRouter()
192
+ const first = ctx.plugin(consumer('/same', 'first'))
193
+ await settle()
194
+ ctx.plugin(consumer('/same', 'second'))
195
+ await settle()
196
+ expect(await (await get(router, '/same')).text()).toBe('first')
197
+ await first.dispose()
198
+ await settle()
199
+ expect(await (await get(router, '/same')).text()).toBe('second')
200
+ })
201
+
202
+ it('runs a reloaded plugin\'s handler once per request: reloading leaves no duplicate route behind, ten times', async () => {
203
+ const { ctx, router } = await mountRouter()
204
+ let calls = 0
205
+ const counting = {
206
+ name: 'counting',
207
+ inject: ['router'],
208
+ apply(inner: Context): void {
209
+ inner.router.route('/', new App().use(async (_c, next) => { calls += 1; await next() }).get('/count', c => c.text(String(calls))))
210
+ },
211
+ }
212
+ for (let round = 0; round < 10; round += 1) {
213
+ const fiber = ctx.plugin(counting)
214
+ await settle()
215
+ expect(router.size).toBe(1)
216
+ calls = 0
217
+ expect(await (await get(router, '/count')).text()).toBe('1')
218
+ await fiber.dispose()
219
+ await settle()
220
+ expect(router.size).toBe(0)
221
+ }
222
+ })
223
+
113
224
  it('returns to its baseline after fifty consumer mount and unload cycles', async () => {
114
225
  const { ctx, router } = await mountRouter()
115
226
  ctx.plugin(consumer('/stay', 'S'))
@@ -119,10 +230,12 @@ describe('acryl-router lifecycle through Cordis', () => {
119
230
  const fiber = ctx.plugin(consumer(`/cycle/${String(i)}`, `C${String(i)}`))
120
231
  await settle()
121
232
  expect(router.size).toBe(baseline + 1)
233
+ expect(await (await get(router, `/cycle/${String(i)}`)).text()).toBe(`C${String(i)}`)
122
234
  await fiber.dispose()
123
235
  await settle()
124
236
  }
125
237
  expect(router.size).toBe(baseline)
238
+ expect((await get(router, '/cycle/0')).status).toBe(404)
126
239
  })
127
240
 
128
241
  it('aborts an in-flight request and waits for it when the router is disposed, then answers 503', async () => {
@@ -131,81 +244,166 @@ describe('acryl-router lifecycle through Cordis', () => {
131
244
  await settle()
132
245
  const router = ctx.router
133
246
  let aborted = false
134
- router.get('/slow', async c => {
135
- await new Promise<void>((_resolve, reject) => { c.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
247
+ router.route('/', new App().get('/slow', async c => {
248
+ await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
136
249
  return c.text('late')
137
- })
138
- const pending = router.fetch(new Request('http://acryl.test/slow'))
250
+ }))
251
+ const pending = get(router, '/slow')
139
252
  await settle()
140
253
 
141
254
  await fiber.dispose()
142
255
  await settle()
143
256
  expect(aborted).toBe(true)
144
257
  expect((await pending).status).toBe(499)
145
- expect((await router.fetch(new Request('http://acryl.test/slow'))).status).toBe(503)
258
+ expect((await get(router, '/slow')).status).toBe(503)
146
259
  })
147
260
 
148
- it('hands the transport\'s connection info to the handler and gives an in-process call an empty one', async () => {
261
+ it('forwards the caller\'s own abort to the handler signal', async () => {
149
262
  const { router } = await mountRouter()
150
- router.get('/who', c => c.json({ remote: c.connection.remoteAddress ?? null }))
151
- const over = await router.fetch(new Request('http://acryl.test/who'), { remoteAddress: '127.0.0.1' })
152
- expect(await over.json()).toEqual({ remote: '127.0.0.1' })
153
- const inProcess = await router.fetch(new Request('http://acryl.test/who'))
154
- expect(await inProcess.json()).toEqual({ remote: null })
263
+ let seen = false
264
+ router.route('/', new App().get('/slow', async c => {
265
+ await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { seen = true; reject(new Error('aborted')) }, { once: true }) })
266
+ return c.text('x')
267
+ }))
268
+ const controller = new AbortController()
269
+ const pending = router.fetch(new Request('http://acryl.test/slow', { signal: controller.signal }))
270
+ await settle()
271
+ controller.abort()
272
+ expect((await pending).status).toBe(499)
273
+ expect(seen).toBe(true)
155
274
  })
156
275
 
157
- it('keeps a streaming request in flight through waitUntil: disposal aborts it and waits until it has ended', async () => {
276
+ 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 () => {
158
277
  const ctx = root()
159
278
  const fiber = ctx.plugin(provider, config)
160
279
  await settle()
161
- const router = ctx.router
162
- let ended = false
163
- router.get('/stream', c => {
164
- const body = new ReadableStream<Uint8Array>({
165
- start(controller) {
166
- const done = new Promise<void>(resolve => {
167
- c.signal.addEventListener('abort', () => { ended = true; controller.close(); resolve() }, { once: true })
168
- })
169
- c.waitUntil(done)
170
- controller.enqueue(new TextEncoder().encode('first'))
171
- },
172
- })
173
- return new Response(body)
174
- })
175
- const response = await router.fetch(new Request('http://acryl.test/stream'))
176
- expect(response.status).toBe(200)
280
+ let cleaned = false
281
+ ctx.router.route('/', new App().get('/events', c => streamSSE(c, async stream => {
282
+ stream.onAbort(() => { cleaned = true })
283
+ await stream.writeSSE({ data: 'first' })
284
+ while (!stream.aborted) await stream.sleep(10)
285
+ })))
286
+ const response = await get(ctx.router, '/events')
287
+ expect(response.headers.get('content-type')).toBe('text/event-stream')
177
288
  const reader = response.body!.getReader()
178
- expect(new TextDecoder().decode((await reader.read()).value)).toBe('first')
289
+ expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
179
290
 
180
291
  await fiber.dispose()
181
- expect(ended).toBe(true)
292
+ expect(cleaned).toBe(true)
182
293
  await expect(reader.read()).resolves.toMatchObject({ done: true })
183
294
  })
184
295
 
296
+ it('ends the stream a plugin opened when that plugin unloads, and leaves another plugin\'s request running', async () => {
297
+ const { ctx, router } = await mountRouter()
298
+ let cleaned = false
299
+ let otherAborted = false
300
+ let release!: () => void
301
+ const streaming = ctx.plugin({
302
+ name: 'streaming',
303
+ inject: ['router'],
304
+ apply(inner: Context): void {
305
+ inner.router.route('/', new App().get('/events', c => streamSSE(c, async stream => {
306
+ stream.onAbort(() => { cleaned = true })
307
+ await stream.writeSSE({ data: 'first' })
308
+ while (!stream.aborted) await stream.sleep(10)
309
+ })))
310
+ },
311
+ })
312
+ ctx.plugin({
313
+ name: 'other',
314
+ inject: ['router'],
315
+ apply(inner: Context): void {
316
+ inner.router.route('/', new App().get('/slow', async c => {
317
+ c.req.raw.signal.addEventListener('abort', () => { otherAborted = true }, { once: true })
318
+ await new Promise<void>(resolve => { release = resolve })
319
+ return c.text('done')
320
+ }))
321
+ },
322
+ })
323
+ await settle()
324
+ const response = await get(router, '/events')
325
+ const reader = response.body!.getReader()
326
+ expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
327
+ const slow = get(router, '/slow')
328
+ await settle()
329
+
330
+ await streaming.dispose()
331
+ expect(cleaned).toBe(true)
332
+ await expect(reader.read()).resolves.toMatchObject({ done: true })
333
+ expect(otherAborted).toBe(false)
334
+ release()
335
+ expect(await (await slow).text()).toBe('done')
336
+ })
337
+
338
+ it('aborts a request still inside an unloading plugin\'s middleware, but not one that middleware already passed on', async () => {
339
+ const { ctx, router } = await mountRouter()
340
+ let guardAborted = false
341
+ let handlerAborted = false
342
+ let release!: () => void
343
+ const guard = ctx.plugin({
344
+ name: 'guard',
345
+ inject: ['router'],
346
+ apply(inner: Context): void {
347
+ inner.router.route('/', new App()
348
+ .use('/held/*', async (c, next) => {
349
+ c.req.raw.signal.addEventListener('abort', () => { guardAborted = true }, { once: true })
350
+ await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true }) })
351
+ await next()
352
+ })
353
+ .use('/passed/*', async (_c, next) => { await next() }))
354
+ },
355
+ })
356
+ ctx.plugin({
357
+ name: 'handler',
358
+ inject: ['router'],
359
+ apply(inner: Context): void {
360
+ inner.router.route('/', new App()
361
+ .get('/held/x', c => c.text('held'))
362
+ .get('/passed/x', c => streamSSE(c, async stream => {
363
+ stream.onAbort(() => { handlerAborted = true })
364
+ await stream.writeSSE({ data: 'open' })
365
+ await new Promise<void>(resolve => { release = resolve })
366
+ })))
367
+ },
368
+ })
369
+ await settle()
370
+ const held = get(router, '/held/x')
371
+ const passed = await get(router, '/passed/x')
372
+ const reader = passed.body!.getReader()
373
+ await reader.read()
374
+
375
+ await guard.dispose()
376
+ expect(guardAborted).toBe(true)
377
+ expect((await held).status).toBe(499)
378
+ expect(handlerAborted).toBe(false)
379
+ release()
380
+ await expect(reader.read()).resolves.toMatchObject({ done: true })
381
+ })
382
+
383
+ it('keeps a request in flight through waitUntil until its task settles', async () => {
384
+ const ctx = root()
385
+ const fiber = ctx.plugin(provider, config)
386
+ await settle()
387
+ let finished = false
388
+ ctx.router.route('/', new App().get('/later', c => {
389
+ c.executionCtx.waitUntil(new Promise<void>(resolve => { setTimeout(() => { finished = true; resolve() }, 30) }))
390
+ return c.text('now')
391
+ }))
392
+ expect(await (await get(ctx.router, '/later')).text()).toBe('now')
393
+ expect(finished).toBe(false)
394
+ await fiber.dispose()
395
+ expect(finished).toBe(true)
396
+ })
397
+
185
398
  it('gives up on a request that ignores the abort after the grace period instead of hanging disposal', async () => {
186
399
  const ctx = root()
187
400
  const fiber = ctx.plugin(provider, { trailingSlash: 'strict', disposeGraceMs: 30 })
188
401
  await settle()
189
- ctx.router.get('/stuck', c => { c.waitUntil(new Promise(() => undefined)); return c.text('x') })
190
- await ctx.router.fetch(new Request('http://acryl.test/stuck'))
402
+ ctx.router.route('/', new App().get('/stuck', c => { c.executionCtx.waitUntil(new Promise(() => undefined)); return c.text('x') }))
403
+ await get(ctx.router, '/stuck')
191
404
  const started = Date.now()
192
405
  await fiber.dispose()
193
406
  expect(Date.now() - started).toBeLessThan(1000)
194
407
  expect(fiber.state).toBe(DISPOSED)
195
408
  })
196
-
197
- it('forwards the caller\'s own abort to the handler signal', async () => {
198
- const { router } = await mountRouter()
199
- let seen = false
200
- router.get('/slow', async c => {
201
- await new Promise<void>((_resolve, reject) => { c.signal.addEventListener('abort', () => { seen = true; reject(new Error('aborted')) }, { once: true }) })
202
- return c.text('x')
203
- })
204
- const controller = new AbortController()
205
- const pending = router.fetch(new Request('http://acryl.test/slow', { signal: controller.signal }))
206
- await settle()
207
- controller.abort()
208
- expect((await pending).status).toBe(499)
209
- expect(seen).toBe(true)
210
- })
211
409
  })
@@ -0,0 +1,79 @@
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
+ })
@@ -2,7 +2,7 @@
2
2
  "source": {
3
3
  "repo": "acryldev/acryl",
4
4
  "path": "plugins/acryl-router",
5
- "commit": "11fed295161c0282d144b5ac62616988b58d8189"
5
+ "commit": "df79b188398b6d4f8cceda35a72b88300a10a9ce"
6
6
  },
7
7
  "vendored": [
8
8
  "src",
@@ -10,7 +10,7 @@
10
10
  "LICENSE"
11
11
  ],
12
12
  "rewrittenSpecifiers": 0,
13
- "digest": "sha256:3873e59cdc36ab606be983680dc2f338141563ebafb13b36215c6b7de7c80d42",
13
+ "digest": "sha256:6e15f8ce0499b94c95649342f6fe984bc9b2aec0f5a142e8853a5badcc3b85f5",
14
14
  "testsNotRun": {
15
15
  "tests/loader.spec.ts": "loads the built lib/ entry through the real Loader; the library ships source and has no build"
16
16
  },
@@ -7,5 +7,6 @@
7
7
  "dependencies": {
8
8
  "@deepseek-ai/cordis": "4.0.4"
9
9
  },
10
- "mount": "not mounted: it is the library behind acryl-node-server"
10
+ "mount": "not mounted: it is the library behind acryl-node-server",
11
+ "omit": ["**/*.test.*", "**/*.case.ts"]
11
12
  }
@@ -25,7 +25,9 @@ export function toRequest(req: IncomingMessage, signal: AbortSignal): Request {
25
25
  }
26
26
  const method = (req.method ?? 'GET').toUpperCase()
27
27
  if (method === 'GET' || method === 'HEAD') return new Request(url, { method, headers, signal })
28
- return new Request(url, { method, headers, signal, body: Readable.toWeb(req) as ReadableStream, duplex: 'half' })
28
+ // Node's fetch requires `duplex: 'half'` for a streamed body; the DOM lib's `RequestInit` does not declare the field yet.
29
+ const init: RequestInit & { duplex: 'half' } = { method, headers, signal, body: Readable.toWeb(req) as ReadableStream, duplex: 'half' }
30
+ return new Request(url, init)
29
31
  }
30
32
 
31
33
  /**
@@ -8,7 +8,8 @@ import { Context, type FiberState } from '@deepseek-ai/cordis'
8
8
  import WebServer from '@deepseek-ai/dsh-host-webserver'
9
9
  import { apply as routerApply, name as routerName } from '../../acryl-router/src/index.ts'
10
10
  import { afterEach, describe, expect, it } from 'vitest'
11
- import type { RouterHttpService } from '../../acryl-router/src/index.ts'
11
+ import { App, connectionOf, type RouterHttpService } from '../../acryl-router/src/index.ts'
12
+ import { methodNotAllowed } from '../../acryl-router/src/middleware/index.ts'
12
13
  import { apply, inject, name } from '../src/index.ts'
13
14
 
14
15
  const PENDING = 0 as FiberState.PENDING
@@ -39,25 +40,28 @@ async function stack(): Promise<{ ctx: Context, base: () => string }> {
39
40
  return { ctx, base: () => `http://127.0.0.1:${String((ctx as unknown as { webServer: { port: number } }).webServer.port)}` }
40
41
  }
41
42
 
42
- function demo(prefix: string, extra?: (ctx: Context) => void) {
43
+ function demo(prefix: string, extra?: (app: App) => void) {
43
44
  return {
44
45
  name: `demo${prefix.replaceAll('/', '-')}`,
45
46
  inject: ['router', 'routerHttp'],
46
47
  apply(ctx: Context): void {
47
48
  ctx.routerHttp.mount(prefix)
48
- ctx.router.get(`${prefix}/hello/:who`, c => c.json({ hello: c.params['who'], q: c.url.searchParams.get('q'), host: c.req.headers.get('host') !== null }))
49
- ctx.router.post(`${prefix}/echo`, async c => c.json({ got: await c.req.text(), type: c.req.headers.get('content-type') }, 201))
50
- ctx.router.get(`${prefix}/cookies`, () => {
49
+ const app = new App()
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', () => {
51
54
  const headers = new Headers()
52
55
  headers.append('set-cookie', 'a=1; Path=/')
53
56
  headers.append('set-cookie', 'b=2; Path=/')
54
57
  return new Response('ok', { headers })
55
58
  })
56
- ctx.router.get(`${prefix}/stream`, () => new Response(new ReadableStream({
59
+ app.get('/stream', () => new Response(new ReadableStream({
57
60
  start(controller) { controller.enqueue(new TextEncoder().encode('one,')); controller.enqueue(new TextEncoder().encode('two')); controller.close() },
58
61
  })))
59
- ctx.router.get(`${prefix}/peer`, c => c.json({ remote: c.connection.remoteAddress ?? null, localPort: c.connection.localPort ?? null }))
60
- extra?.(ctx)
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)
61
65
  },
62
66
  }
63
67
  }
@@ -106,7 +110,7 @@ describe('acryl-router-http against a real webServer', () => {
106
110
  await settle(20)
107
111
  const missing = await fetch(`${base()}/api/demo/none`)
108
112
  expect(missing.status).toBe(404)
109
- expect(await missing.text()).toBe('Not Found')
113
+ expect(await missing.text()).toBe('404 Not Found')
110
114
  expect((await fetch(`${base()}/api/demo/echo`)).status).toBe(405)
111
115
  const head = await fetch(`${base()}/api/demo/hello/a`, { method: 'HEAD' })
112
116
  expect(head.status).toBe(200)
@@ -117,9 +121,9 @@ describe('acryl-router-http against a real webServer', () => {
117
121
  it('aborts the handler signal when the HTTP client disconnects', async () => {
118
122
  const { ctx, base } = await stack()
119
123
  let aborted = false
120
- ctx.plugin(demo('/api/demo', c => {
121
- c.router.get('/api/demo/slow', async r => {
122
- await new Promise<void>((_resolve, reject) => { r.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
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 }) })
123
127
  return r.text('never')
124
128
  })
125
129
  }))
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { Context } from '@deepseek-ai/cordis'
9
9
  import WebServer from '@deepseek-ai/dsh-host-webserver'
10
- import { apply as routerApply, name as routerName } from '../../acryl-router/src/index.ts'
10
+ import { App, apply as routerApply, name as routerName } from '../../acryl-router/src/index.ts'
11
11
  import { afterEach, describe, expect, it } from 'vitest'
12
12
  import { apply as httpApply, inject as httpInject, name as httpName } from '../src/index.ts'
13
13
 
@@ -28,15 +28,17 @@ const notes = {
28
28
  inject: ['router'],
29
29
  apply(ctx: Context): void {
30
30
  const store = new Map<string, string>()
31
- ctx.router.post('/api/notes/:id', async c => {
32
- const text = await c.req.text()
33
- store.set(c.params['id'] ?? '', text)
34
- return c.json({ id: c.params['id'], saved: text.length }, 201)
35
- })
36
- ctx.router.get('/api/notes/:id', c => {
37
- const text = store.get(c.params['id'] ?? '')
38
- return text === undefined ? c.json({ error: 'not found' }, 404) : c.json({ id: c.params['id'], text })
39
- })
31
+ const app = new App()
32
+ .post('/:id', async c => {
33
+ const text = await c.req.text()
34
+ store.set(c.req.param('id'), text)
35
+ return c.json({ id: c.req.param('id'), saved: text.length }, 201)
36
+ })
37
+ .get('/:id', c => {
38
+ const text = store.get(c.req.param('id'))
39
+ return text === undefined ? c.json({ error: 'not found' }, 404) : c.json({ id: c.req.param('id'), text })
40
+ })
41
+ ctx.router.route('/api/notes', app)
40
42
  ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/api/notes') })
41
43
  },
42
44
  }