acrylblends 0.3.0 → 0.4.1

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 (341) hide show
  1. package/dist/index.d.ts +6 -1
  2. package/dist/index.js +84 -5
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/LICENSE +23 -0
  5. package/docs/framework/README.md +63 -0
  6. package/docs/framework/UPSTREAM.md +3 -0
  7. package/docs/framework/api-summary.md +138 -0
  8. package/docs/framework/concepts.md +105 -0
  9. package/docs/framework/context.md +129 -0
  10. package/docs/framework/getting-started.md +146 -0
  11. package/docs/framework/helpers.md +118 -0
  12. package/docs/framework/middleware.md +154 -0
  13. package/docs/framework/migration-from-0-3.md +108 -0
  14. package/docs/framework/migration-from-dsh-webserver.md +103 -0
  15. package/docs/framework/node-server.md +75 -0
  16. package/docs/framework/plugin-lifecycle.md +111 -0
  17. package/docs/framework/request.md +106 -0
  18. package/docs/framework/routing.md +188 -0
  19. package/docs/framework/surfaces.md +71 -0
  20. package/docs/framework/upstream.json +9 -0
  21. package/docs/framework/validation.md +135 -0
  22. package/package.json +4 -2
  23. package/plugins/acryl-inspect/plugin.json +2 -1
  24. package/plugins/acryl-inspect/src/report.ts +21 -13
  25. package/plugins/acryl-inspect/src/service.ts +9 -7
  26. package/plugins/acryl-inspect/tests/report.spec.ts +13 -8
  27. package/plugins/acryl-inspect/tests/service.spec.ts +11 -10
  28. package/plugins/acryl-inspect/upstream.json +2 -2
  29. package/plugins/acryl-node-server/LICENSE +26 -0
  30. package/plugins/acryl-node-server/plugin.json +2 -1
  31. package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
  32. package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
  33. package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
  34. package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
  35. package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
  36. package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
  37. package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
  38. package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
  39. package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
  40. package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
  41. package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
  42. package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
  43. package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
  44. package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
  45. package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
  46. package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
  47. package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
  48. package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
  49. package/plugins/acryl-node-server/src/index.ts +19 -6
  50. package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
  51. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
  52. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  53. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
  54. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
  55. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
  56. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
  57. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
  58. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  59. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
  60. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
  61. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
  62. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
  63. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
  64. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
  65. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
  66. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
  67. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
  68. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
  69. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
  70. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
  71. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
  72. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
  73. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
  74. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
  75. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
  76. package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
  77. package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
  78. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
  79. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
  80. package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
  81. package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
  82. package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
  83. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
  84. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
  85. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
  86. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
  87. package/plugins/acryl-node-server/tests/server.spec.ts +44 -8
  88. package/plugins/acryl-node-server/upstream.json +3 -3
  89. package/plugins/acryl-router/LICENSE +26 -0
  90. package/plugins/acryl-router/plugin.json +2 -1
  91. package/plugins/acryl-router/src/client.ts +2 -0
  92. package/plugins/acryl-router/src/connection.ts +42 -0
  93. package/plugins/acryl-router/src/core/app-base.ts +544 -0
  94. package/plugins/acryl-router/src/core/app.test.ts +3833 -0
  95. package/plugins/acryl-router/src/core/app.ts +34 -0
  96. package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
  97. package/plugins/acryl-router/src/core/client/client.ts +249 -0
  98. package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
  99. package/plugins/acryl-router/src/core/client/index.ts +17 -0
  100. package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
  101. package/plugins/acryl-router/src/core/client/types.ts +393 -0
  102. package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
  103. package/plugins/acryl-router/src/core/client/utils.ts +117 -0
  104. package/plugins/acryl-router/src/core/compose.test.ts +751 -0
  105. package/plugins/acryl-router/src/core/compose.ts +73 -0
  106. package/plugins/acryl-router/src/core/context.test.ts +629 -0
  107. package/plugins/acryl-router/src/core/context.ts +781 -0
  108. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
  109. package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
  110. package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
  111. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
  112. package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
  113. package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
  114. package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
  115. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
  116. package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
  117. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
  118. package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
  119. package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
  120. package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
  121. package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
  122. package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
  123. package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
  124. package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
  125. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
  126. package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
  127. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
  128. package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
  129. package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
  130. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
  131. package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
  132. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
  133. package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
  134. package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
  135. package/plugins/acryl-router/src/core/http-exception.ts +77 -0
  136. package/plugins/acryl-router/src/core/index.ts +53 -0
  137. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
  138. package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
  139. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
  140. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
  141. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
  142. package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
  143. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
  144. package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
  145. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
  146. package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
  147. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
  148. package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
  149. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
  150. package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
  151. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
  152. package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
  153. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
  154. package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
  155. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
  156. package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
  157. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
  158. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
  159. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
  160. package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
  161. package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
  162. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
  163. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
  164. package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
  165. package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
  166. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
  167. package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
  168. package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
  169. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
  170. package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
  171. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
  172. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
  173. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
  174. package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
  175. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
  176. package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
  177. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
  178. package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
  179. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
  180. package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
  181. package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
  182. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
  183. package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
  184. package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
  185. package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
  186. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
  187. package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
  188. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
  189. package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
  190. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
  191. package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
  192. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
  193. package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
  194. package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
  195. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
  196. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
  197. package/plugins/acryl-router/src/core/request/constants.ts +1 -0
  198. package/plugins/acryl-router/src/core/request.test.ts +751 -0
  199. package/plugins/acryl-router/src/core/request.ts +502 -0
  200. package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
  201. package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
  202. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
  203. package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
  204. package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
  205. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
  206. package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
  207. package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
  208. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
  209. package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
  210. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
  211. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
  212. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
  213. package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
  214. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
  215. package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
  216. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
  217. package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
  218. package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
  219. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
  220. package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
  221. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
  222. package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
  223. package/plugins/acryl-router/src/core/router/utils.ts +1 -0
  224. package/plugins/acryl-router/src/core/router.ts +103 -0
  225. package/plugins/acryl-router/src/core/types.test.ts +4134 -0
  226. package/plugins/acryl-router/src/core/types.ts +2782 -0
  227. package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
  228. package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
  229. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
  230. package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
  231. package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
  232. package/plugins/acryl-router/src/core/utils/body.ts +263 -0
  233. package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
  234. package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
  235. package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
  236. package/plugins/acryl-router/src/core/utils/color.ts +56 -0
  237. package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
  238. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
  239. package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
  240. package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
  241. package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
  242. package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
  243. package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
  244. package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
  245. package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
  246. package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
  247. package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
  248. package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
  249. package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
  250. package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
  251. package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
  252. package/plugins/acryl-router/src/core/utils/html.ts +182 -0
  253. package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
  254. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
  255. package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
  256. package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
  257. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
  258. package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
  259. package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
  260. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
  261. package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
  262. package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
  263. package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
  264. package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
  265. package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
  266. package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
  267. package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
  268. package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
  269. package/plugins/acryl-router/src/core/utils/types.ts +123 -0
  270. package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
  271. package/plugins/acryl-router/src/core/utils/url.ts +337 -0
  272. package/plugins/acryl-router/src/core/validator/index.ts +8 -0
  273. package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
  274. package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
  275. package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
  276. package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
  277. package/plugins/acryl-router/src/helpers/index.ts +13 -3
  278. package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
  279. package/plugins/acryl-router/src/index.ts +6 -6
  280. package/plugins/acryl-router/src/middleware/index.ts +25 -6
  281. package/plugins/acryl-router/src/middleware/loopback-only.ts +53 -14
  282. package/plugins/acryl-router/src/service.ts +262 -64
  283. package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
  284. package/plugins/acryl-router/tests/live-install.spec.ts +2 -2
  285. package/plugins/acryl-router/tests/loader.spec.ts +3 -2
  286. package/plugins/acryl-router/tests/loopback-only.spec.ts +80 -0
  287. package/plugins/acryl-router/tests/service.spec.ts +281 -66
  288. package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
  289. package/plugins/acryl-router/upstream.json +2 -2
  290. package/plugins/acryl-router-http/plugin.json +2 -1
  291. package/plugins/acryl-router-http/src/bridge.ts +3 -1
  292. package/plugins/acryl-router-http/tests/adapter.spec.ts +16 -12
  293. package/plugins/acryl-router-http/tests/surfaces.spec.ts +12 -10
  294. package/plugins/acryl-router-http/upstream.json +3 -3
  295. package/plugins/acryl-state/NOTICE +30 -0
  296. package/plugins/acryl-state/plugin.json +1 -1
  297. package/plugins/acryl-state/src/cell.ts +1 -1
  298. package/plugins/acryl-state/src/service.ts +33 -15
  299. package/plugins/acryl-state/upstream.json +6 -4
  300. package/plugins/builder/script.ts +4 -7
  301. package/plugins/extensions/index.ts +6 -5
  302. package/plugins/extensions/resolve.ts +28 -3
  303. package/plugins/openai/prompt.ts +9 -7
  304. package/plugins/tsconfig.json +5 -4
  305. package/shared/AGENTS.core.md +54 -0
  306. package/templates/agent/AGENTS.md +6 -0
  307. package/templates/agent/README.md +2 -2
  308. package/templates/agent/blend.yaml +1 -1
  309. package/templates/agent/src/routes.ts +37 -27
  310. package/templates/agent/test/model.test.ts +2 -1
  311. package/templates/agent/tsconfig.json +8 -5
  312. package/templates/blank/AGENTS.md +5 -0
  313. package/templates/blank/README.md +1 -1
  314. package/templates/blank/src/routes.ts +6 -4
  315. package/templates/blank/tsconfig.json +2 -4
  316. package/templates/todo/AGENTS.md +5 -0
  317. package/templates/todo/src/page.ts +1 -1
  318. package/templates/todo/src/todo.ts +58 -29
  319. package/templates/todo/tsconfig.json +2 -4
  320. package/templates/todo/upstream.json +2 -2
  321. package/plugins/acryl-router/src/compose.ts +0 -21
  322. package/plugins/acryl-router/src/context.ts +0 -59
  323. package/plugins/acryl-router/src/helpers/read-body.ts +0 -59
  324. package/plugins/acryl-router/src/helpers/sse.ts +0 -83
  325. package/plugins/acryl-router/src/helpers/validate.ts +0 -44
  326. package/plugins/acryl-router/src/http-error.ts +0 -21
  327. package/plugins/acryl-router/src/middleware/body-limit.ts +0 -25
  328. package/plugins/acryl-router/src/middleware/cors.ts +0 -46
  329. package/plugins/acryl-router/src/middleware/logger.ts +0 -21
  330. package/plugins/acryl-router/src/middleware/request-id.ts +0 -24
  331. package/plugins/acryl-router/src/middleware/secure-headers.ts +0 -23
  332. package/plugins/acryl-router/src/pattern.ts +0 -89
  333. package/plugins/acryl-router/src/registrar.ts +0 -90
  334. package/plugins/acryl-router/src/table.ts +0 -156
  335. package/plugins/acryl-router/src/types.ts +0 -73
  336. package/plugins/acryl-router/src/util.ts +0 -23
  337. package/plugins/acryl-router/tests/api.spec.ts +0 -223
  338. package/plugins/acryl-router/tests/conformance.ts +0 -170
  339. package/plugins/acryl-router/tests/helpers.spec.ts +0 -172
  340. package/plugins/acryl-router/tests/middleware.spec.ts +0 -183
  341. package/plugins/acryl-router/tests/table.spec.ts +0 -47
@@ -0,0 +1,80 @@
1
+ /**
2
+ * `loopbackOnly`, the guard of a local app's own routes: the peer and port come from the transport, the `Host` names this app, and the
3
+ * `callers` rule decides whether any local program or only the app's own page may call.
4
+ */
5
+
6
+ import { describe, expect, it } from 'vitest'
7
+ import { App, type ConnectionInfo } from '../src/index.ts'
8
+ import { loopbackOnly, type LoopbackOptions } from '../src/middleware/index.ts'
9
+
10
+ const PORT = 43_120
11
+ const SELF = `http://127.0.0.1:${String(PORT)}`
12
+ const LOCAL: ConnectionInfo = { remoteAddress: '127.0.0.1', localPort: PORT }
13
+
14
+ function guarded(options: LoopbackOptions = {}): App {
15
+ return new App().use(loopbackOnly(options)).get('/x', c => c.text('read')).post('/x', c => c.text('changed'))
16
+ }
17
+
18
+ async function status(
19
+ app: App,
20
+ method: string,
21
+ headers: Record<string, string>,
22
+ connection: ConnectionInfo = LOCAL,
23
+ ): Promise<number> {
24
+ const response = await app.fetch(new Request(`${SELF}/x`, { method, headers: { host: `127.0.0.1:${String(PORT)}`, ...headers } }), { connection })
25
+ return response.status
26
+ }
27
+
28
+ describe('loopbackOnly, callers: local (the default)', () => {
29
+ const app = guarded()
30
+
31
+ it('lets a local program read and change, and a page of this app change', async () => {
32
+ expect(await status(app, 'GET', {})).toBe(200)
33
+ expect(await status(app, 'POST', {})).toBe(200)
34
+ expect(await status(app, 'POST', { origin: SELF })).toBe(200)
35
+ })
36
+
37
+ it('refuses another origin\'s change, a foreign peer, a wrong port, a foreign Host and an in-process call', async () => {
38
+ expect(await status(app, 'POST', { origin: 'http://evil.test' })).toBe(403)
39
+ expect(await status(app, 'GET', {}, { remoteAddress: '192.168.1.5', localPort: PORT })).toBe(403)
40
+ expect(await status(app, 'GET', {}, { remoteAddress: '127.0.0.1', localPort: PORT + 1 })).toBe(403)
41
+ expect(await status(app, 'GET', { host: 'evil.test' })).toBe(403)
42
+ expect(await status(app, 'GET', {}, {})).toBe(403)
43
+ })
44
+
45
+ it('lets an in-process call through only when the host says so', async () => {
46
+ expect(await status(guarded({ inProcess: 'allow' }), 'GET', {}, {})).toBe(200)
47
+ })
48
+ })
49
+
50
+ describe('loopbackOnly, callers: own-page', () => {
51
+ const app = guarded({ callers: 'own-page', hostnames: ['127.0.0.1'] })
52
+
53
+ it('lets this app\'s page read and change with its exact Origin', async () => {
54
+ expect(await status(app, 'GET', { origin: SELF, 'sec-fetch-site': 'same-origin' })).toBe(200)
55
+ expect(await status(app, 'POST', { origin: SELF, 'sec-fetch-site': 'same-origin' })).toBe(200)
56
+ expect(await status(app, 'POST', { origin: SELF })).toBe(200)
57
+ })
58
+
59
+ it('lets a same-origin read without Origin through by its Referer, as browsers send it', async () => {
60
+ expect(await status(app, 'GET', { 'sec-fetch-site': 'same-origin', referer: `${SELF}/page?tab=1` })).toBe(200)
61
+ })
62
+
63
+ it('refuses a local program (no Origin), even for a read', async () => {
64
+ expect(await status(app, 'GET', {})).toBe(403)
65
+ expect(await status(app, 'POST', {})).toBe(403)
66
+ })
67
+
68
+ it('refuses a change that only has the read evidence, and Origin forms that are not exact', async () => {
69
+ expect(await status(app, 'POST', { 'sec-fetch-site': 'same-origin', referer: `${SELF}/` })).toBe(403)
70
+ expect(await status(app, 'POST', { origin: `${SELF}/` })).toBe(403)
71
+ expect(await status(app, 'POST', { origin: 'null' })).toBe(403)
72
+ expect(await status(app, 'POST', { origin: SELF, 'sec-fetch-site': 'same-site' })).toBe(403)
73
+ })
74
+
75
+ it('refuses another page, a Referer from elsewhere and a Host this app does not answer to', async () => {
76
+ expect(await status(app, 'GET', { origin: 'http://evil.test', 'sec-fetch-site': 'cross-site' })).toBe(403)
77
+ expect(await status(app, 'GET', { 'sec-fetch-site': 'same-origin', referer: 'http://evil.test/' })).toBe(403)
78
+ expect(await status(app, 'GET', { host: `localhost:${String(PORT)}`, origin: `http://localhost:${String(PORT)}` })).toBe(403)
79
+ })
80
+ })
@@ -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'))
@@ -89,6 +152,23 @@ describe('acryl-router lifecycle through Cordis', () => {
89
152
  expect(router.size).toBe(1)
90
153
  })
91
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 App().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
+
92
172
  it('replaces the provider without leaving doubled registrations: consumers re-register exactly once', async () => {
93
173
  const ctx = root()
94
174
  const first = ctx.plugin(provider, config)
@@ -110,6 +190,54 @@ describe('acryl-router lifecycle through Cordis', () => {
110
190
  expect(await (await get(ctx.router, '/c')).text()).toBe('A')
111
191
  })
112
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 App().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 App().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
+
113
241
  it('returns to its baseline after fifty consumer mount and unload cycles', async () => {
114
242
  const { ctx, router } = await mountRouter()
115
243
  ctx.plugin(consumer('/stay', 'S'))
@@ -119,10 +247,12 @@ describe('acryl-router lifecycle through Cordis', () => {
119
247
  const fiber = ctx.plugin(consumer(`/cycle/${String(i)}`, `C${String(i)}`))
120
248
  await settle()
121
249
  expect(router.size).toBe(baseline + 1)
250
+ expect(await (await get(router, `/cycle/${String(i)}`)).text()).toBe(`C${String(i)}`)
122
251
  await fiber.dispose()
123
252
  await settle()
124
253
  }
125
254
  expect(router.size).toBe(baseline)
255
+ expect((await get(router, '/cycle/0')).status).toBe(404)
126
256
  })
127
257
 
128
258
  it('aborts an in-flight request and waits for it when the router is disposed, then answers 503', async () => {
@@ -131,81 +261,166 @@ describe('acryl-router lifecycle through Cordis', () => {
131
261
  await settle()
132
262
  const router = ctx.router
133
263
  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 }) })
264
+ router.route('/', new App().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 }) })
136
266
  return c.text('late')
137
- })
138
- const pending = router.fetch(new Request('http://acryl.test/slow'))
267
+ }))
268
+ const pending = get(router, '/slow')
139
269
  await settle()
140
270
 
141
271
  await fiber.dispose()
142
272
  await settle()
143
273
  expect(aborted).toBe(true)
144
274
  expect((await pending).status).toBe(499)
145
- expect((await router.fetch(new Request('http://acryl.test/slow'))).status).toBe(503)
275
+ expect((await get(router, '/slow')).status).toBe(503)
146
276
  })
147
277
 
148
- it('hands the transport\'s connection info to the handler and gives an in-process call an empty one', async () => {
278
+ it('forwards the caller\'s own abort to the handler signal', async () => {
149
279
  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 })
280
+ let seen = false
281
+ router.route('/', new App().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)
155
291
  })
156
292
 
157
- it('keeps a streaming request in flight through waitUntil: disposal aborts it and waits until it has ended', async () => {
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 () => {
158
294
  const ctx = root()
159
295
  const fiber = ctx.plugin(provider, config)
160
296
  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)
297
+ let cleaned = false
298
+ ctx.router.route('/', new App().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')
177
305
  const reader = response.body!.getReader()
178
- expect(new TextDecoder().decode((await reader.read()).value)).toBe('first')
306
+ expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
179
307
 
180
308
  await fiber.dispose()
181
- expect(ended).toBe(true)
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 App().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 App().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 App()
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 App()
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()
182
397
  await expect(reader.read()).resolves.toMatchObject({ done: true })
183
398
  })
184
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 App().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
+
185
415
  it('gives up on a request that ignores the abort after the grace period instead of hanging disposal', async () => {
186
416
  const ctx = root()
187
417
  const fiber = ctx.plugin(provider, { trailingSlash: 'strict', disposeGraceMs: 30 })
188
418
  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'))
419
+ ctx.router.route('/', new App().get('/stuck', c => { c.executionCtx.waitUntil(new Promise(() => undefined)); return c.text('x') }))
420
+ await get(ctx.router, '/stuck')
191
421
  const started = Date.now()
192
422
  await fiber.dispose()
193
423
  expect(Date.now() - started).toBeLessThan(1000)
194
424
  expect(fiber.state).toBe(DISPOSED)
195
425
  })
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
426
  })
@@ -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": "64e7c9d2e787f74b2da82492644fbbb46a601074"
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:9ad75d259a74b2a07fe1d8a269e7c83c30061acc90f8a1630e6049b68ba10aa3",
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
  /**