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.
- package/dist/index.d.ts +6 -1
- package/dist/index.js +28 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/plugins/acryl-inspect/plugin.json +2 -1
- package/plugins/acryl-inspect/src/report.ts +16 -13
- package/plugins/acryl-inspect/src/service.ts +9 -7
- package/plugins/acryl-inspect/tests/report.spec.ts +7 -8
- package/plugins/acryl-inspect/tests/service.spec.ts +11 -10
- package/plugins/acryl-inspect/upstream.json +2 -2
- package/plugins/acryl-node-server/LICENSE +26 -0
- package/plugins/acryl-node-server/plugin.json +2 -1
- package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
- package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
- package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
- package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
- package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
- package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
- package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
- package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
- package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
- package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
- package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
- package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
- package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
- package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
- package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
- package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
- package/plugins/acryl-node-server/src/index.ts +19 -6
- package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
- package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
- package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
- package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
- package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
- package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
- package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
- package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
- package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
- package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
- package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
- package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
- package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
- package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
- package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
- package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
- package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
- package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
- package/plugins/acryl-node-server/tests/server.spec.ts +44 -8
- package/plugins/acryl-node-server/upstream.json +3 -3
- package/plugins/acryl-router/LICENSE +26 -0
- package/plugins/acryl-router/plugin.json +2 -1
- package/plugins/acryl-router/src/client.ts +2 -0
- package/plugins/acryl-router/src/connection.ts +42 -0
- package/plugins/acryl-router/src/core/app-base.ts +544 -0
- package/plugins/acryl-router/src/core/app.test.ts +3833 -0
- package/plugins/acryl-router/src/core/app.ts +34 -0
- package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
- package/plugins/acryl-router/src/core/client/client.ts +249 -0
- package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
- package/plugins/acryl-router/src/core/client/index.ts +17 -0
- package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
- package/plugins/acryl-router/src/core/client/types.ts +393 -0
- package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
- package/plugins/acryl-router/src/core/client/utils.ts +117 -0
- package/plugins/acryl-router/src/core/compose.test.ts +751 -0
- package/plugins/acryl-router/src/core/compose.ts +73 -0
- package/plugins/acryl-router/src/core/context.test.ts +629 -0
- package/plugins/acryl-router/src/core/context.ts +781 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
- package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
- package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
- package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
- package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
- package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
- package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
- package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
- package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
- package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
- package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
- package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
- package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
- package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
- package/plugins/acryl-router/src/core/http-exception.ts +77 -0
- package/plugins/acryl-router/src/core/index.ts +53 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
- package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
- package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
- package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
- package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
- package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
- package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
- package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
- package/plugins/acryl-router/src/core/request/constants.ts +1 -0
- package/plugins/acryl-router/src/core/request.test.ts +751 -0
- package/plugins/acryl-router/src/core/request.ts +502 -0
- package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
- package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
- package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
- package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
- package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
- package/plugins/acryl-router/src/core/router/utils.ts +1 -0
- package/plugins/acryl-router/src/core/router.ts +103 -0
- package/plugins/acryl-router/src/core/types.test.ts +4134 -0
- package/plugins/acryl-router/src/core/types.ts +2782 -0
- package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
- package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
- package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
- package/plugins/acryl-router/src/core/utils/body.ts +263 -0
- package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
- package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
- package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
- package/plugins/acryl-router/src/core/utils/color.ts +56 -0
- package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
- package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
- package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
- package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
- package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
- package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
- package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
- package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
- package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
- package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
- package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
- package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
- package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
- package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
- package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
- package/plugins/acryl-router/src/core/utils/html.ts +182 -0
- package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
- package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
- package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
- package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
- package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
- package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
- package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
- package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
- package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
- package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
- package/plugins/acryl-router/src/core/utils/types.ts +123 -0
- package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
- package/plugins/acryl-router/src/core/utils/url.ts +337 -0
- package/plugins/acryl-router/src/core/validator/index.ts +8 -0
- package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
- package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
- package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
- package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
- package/plugins/acryl-router/src/helpers/index.ts +13 -3
- package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
- package/plugins/acryl-router/src/index.ts +6 -6
- package/plugins/acryl-router/src/middleware/index.ts +25 -6
- package/plugins/acryl-router/src/middleware/loopback-only.ts +19 -13
- package/plugins/acryl-router/src/service.ts +256 -64
- package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
- package/plugins/acryl-router/tests/live-install.spec.ts +2 -2
- package/plugins/acryl-router/tests/loader.spec.ts +3 -2
- package/plugins/acryl-router/tests/service.spec.ts +264 -66
- package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
- package/plugins/acryl-router/upstream.json +2 -2
- package/plugins/acryl-router-http/plugin.json +2 -1
- package/plugins/acryl-router-http/src/bridge.ts +3 -1
- package/plugins/acryl-router-http/tests/adapter.spec.ts +16 -12
- package/plugins/acryl-router-http/tests/surfaces.spec.ts +12 -10
- package/plugins/acryl-router-http/upstream.json +3 -3
- package/plugins/acryl-state/NOTICE +30 -0
- package/plugins/acryl-state/plugin.json +1 -1
- package/plugins/acryl-state/src/cell.ts +1 -1
- package/plugins/acryl-state/src/service.ts +33 -15
- package/plugins/acryl-state/upstream.json +6 -4
- package/plugins/builder/script.ts +4 -7
- package/plugins/extensions/index.ts +2 -2
- package/plugins/extensions/resolve.ts +28 -3
- package/plugins/openai/prompt.ts +9 -7
- package/plugins/tsconfig.json +5 -4
- package/templates/agent/src/routes.ts +37 -27
- package/templates/agent/test/model.test.ts +2 -1
- package/templates/agent/tsconfig.json +8 -5
- package/templates/blank/src/routes.ts +6 -4
- package/templates/blank/tsconfig.json +2 -4
- package/templates/todo/src/page.ts +1 -1
- package/templates/todo/src/todo.ts +56 -29
- package/templates/todo/tsconfig.json +2 -4
- package/templates/todo/upstream.json +2 -2
- package/plugins/acryl-router/src/compose.ts +0 -21
- package/plugins/acryl-router/src/context.ts +0 -59
- package/plugins/acryl-router/src/helpers/read-body.ts +0 -59
- package/plugins/acryl-router/src/helpers/sse.ts +0 -83
- package/plugins/acryl-router/src/helpers/validate.ts +0 -44
- package/plugins/acryl-router/src/http-error.ts +0 -21
- package/plugins/acryl-router/src/middleware/body-limit.ts +0 -25
- package/plugins/acryl-router/src/middleware/cors.ts +0 -46
- package/plugins/acryl-router/src/middleware/logger.ts +0 -21
- package/plugins/acryl-router/src/middleware/request-id.ts +0 -24
- package/plugins/acryl-router/src/middleware/secure-headers.ts +0 -23
- package/plugins/acryl-router/src/pattern.ts +0 -89
- package/plugins/acryl-router/src/registrar.ts +0 -90
- package/plugins/acryl-router/src/table.ts +0 -156
- package/plugins/acryl-router/src/types.ts +0 -73
- package/plugins/acryl-router/src/util.ts +0 -23
- package/plugins/acryl-router/tests/api.spec.ts +0 -223
- package/plugins/acryl-router/tests/conformance.ts +0 -170
- package/plugins/acryl-router/tests/helpers.spec.ts +0 -172
- package/plugins/acryl-router/tests/middleware.spec.ts +0 -183
- package/plugins/acryl-router/tests/table.spec.ts +0 -47
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The ACRYL router service (`ctx.router`): plugin-owned
|
|
2
|
+
* The ACRYL router service (`ctx.router`): plugin-owned `App` mounts on Web-standard Request and Response.
|
|
3
3
|
*
|
|
4
|
-
* A route
|
|
5
|
-
* runs a request in process: Web
|
|
6
|
-
* directly, so one handler serves every surface without opening a socket. Disposal aborts in-flight
|
|
7
|
-
*
|
|
4
|
+
* A plugin builds an ordinary `App` and mounts it with `ctx.router.route(path, app)` from its own context, so the mount disappears when that
|
|
5
|
+
* plugin unloads. `fetch(request)` runs a request in process: Web and Desktop serve it behind HTTP through an adapter, the terminal UI calls
|
|
6
|
+
* it directly, so one handler serves every surface without opening a socket. Disposal refuses new requests, aborts in-flight ones, ends any
|
|
7
|
+
* response body still streaming and waits for all of it to settle.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
11
11
|
import z from '@deepseek-ai/schemastery'
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import type {
|
|
12
|
+
import type { ConnectionInfo, RouterBindings } from './connection.ts'
|
|
13
|
+
import { App } from './core/app.ts'
|
|
14
|
+
import type { ExecutionContext } from './core/context.ts'
|
|
15
|
+
import { HTTPException } from './core/http-exception.ts'
|
|
16
|
+
import { TrieRouter } from './core/router/trie-router/index.ts'
|
|
17
|
+
import { COMPOSED_HANDLER } from './core/utils/constants.ts'
|
|
18
|
+
import { mergePath } from './core/utils/url.ts'
|
|
19
|
+
import type { ErrorHandler, H, NotFoundHandler } from './core/types.ts'
|
|
15
20
|
|
|
16
21
|
const DEFAULT_DISPOSE_GRACE_MS = 2000
|
|
17
22
|
|
|
23
|
+
/** How a trailing slash is read: `strict` keeps `/a` and `/a/` distinct, `ignore` treats them as one path. */
|
|
24
|
+
export type TrailingSlash = 'strict' | 'ignore'
|
|
25
|
+
|
|
18
26
|
/** Validated plugin configuration. */
|
|
19
27
|
export interface Config {
|
|
20
28
|
trailingSlash: TrailingSlash
|
|
21
29
|
disposeGraceMs: number
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
/** Every field has a default, so a caller passes only what it changes: `ctx.plugin(plugin, { ... })` takes the partial shape. */
|
|
33
|
+
export const Config: z<Partial<Config>, Config> = z.object({
|
|
25
34
|
trailingSlash: z.union(['strict', 'ignore']).default('strict').description('Whether `/a` and `/a/` are different paths.'),
|
|
26
|
-
disposeGraceMs: z.number().min(0).default(DEFAULT_DISPOSE_GRACE_MS).description('How long disposal waits for aborted requests
|
|
35
|
+
disposeGraceMs: z.number().min(0).default(DEFAULT_DISPOSE_GRACE_MS).description('How long disposal waits for aborted requests, their response bodies and their waitUntil tasks before it gives up on them.'),
|
|
27
36
|
})
|
|
28
37
|
|
|
29
38
|
/**
|
|
@@ -45,84 +54,170 @@ declare module '@deepseek-ai/cordis' {
|
|
|
45
54
|
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
46
55
|
export const name = 'acryl-router'
|
|
47
56
|
|
|
57
|
+
// Any `App`, whatever its Env and schema; the router only reads its routes.
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
export type MountableApp = App<any, any, any>
|
|
60
|
+
|
|
61
|
+
interface Mount {
|
|
62
|
+
readonly path: string
|
|
63
|
+
readonly app: MountableApp
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface RouterState {
|
|
67
|
+
readonly mounts: Set<Mount>
|
|
68
|
+
errorHandler: ErrorHandler | undefined
|
|
69
|
+
notFoundHandler: NotFoundHandler | undefined
|
|
70
|
+
/** The app composed from the live mounts; dropped on every change and composed again by the next request. */
|
|
71
|
+
composed: App | undefined
|
|
72
|
+
closing: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
48
75
|
interface Flight {
|
|
49
76
|
readonly controller: AbortController
|
|
50
|
-
/**
|
|
77
|
+
/** How many of each mount's handlers are running for this request right now (a middleware counts until its `next()` returns). */
|
|
78
|
+
readonly running: Map<Mount, number>
|
|
79
|
+
/** The mount whose handler produced the response; its body is that mount's code still running. */
|
|
80
|
+
producer: Mount | undefined
|
|
81
|
+
/** Ends the response body early (disposal); a no-op once the body is done or when there is none. */
|
|
82
|
+
readonly endBody: () => void
|
|
83
|
+
/** Settles when the handler is done, its body has been read or cancelled, and every `waitUntil` task it registered has settled. */
|
|
51
84
|
readonly settled: Promise<unknown>
|
|
52
85
|
}
|
|
53
86
|
|
|
87
|
+
/** Wait for `tasks`, including any added while waiting (a stream may register more after the handler returned). */
|
|
88
|
+
async function drain(tasks: readonly Promise<unknown>[]): Promise<void> {
|
|
89
|
+
let seen = 0
|
|
90
|
+
while (seen < tasks.length) {
|
|
91
|
+
const batch = tasks.slice(seen)
|
|
92
|
+
seen = tasks.length
|
|
93
|
+
await Promise.allSettled(batch)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
54
97
|
export class Router extends Service {
|
|
55
|
-
|
|
98
|
+
/** Everything that changes after construction, kept in one object so the service's live state is in one place. */
|
|
99
|
+
private readonly state: RouterState = { mounts: new Set(), errorHandler: undefined, notFoundHandler: undefined, composed: undefined, closing: false }
|
|
56
100
|
private readonly flights = new Set<Flight>()
|
|
57
|
-
|
|
101
|
+
/** The flight of each request the router dispatched, keyed by the request its handlers see as `c.req.raw`. */
|
|
102
|
+
private readonly flightOf = new WeakMap<Request, Flight>()
|
|
103
|
+
private readonly strict: boolean
|
|
58
104
|
private readonly graceMs: number
|
|
105
|
+
private readonly logError: ErrorHandler
|
|
59
106
|
|
|
60
107
|
constructor(ctx: Context, config: Partial<Config> = {}) {
|
|
61
108
|
super(ctx, 'router')
|
|
62
109
|
// The schema supplies these defaults when the Loader mounts the package; a plugin object mounted by hand skips the schema.
|
|
63
110
|
const { trailingSlash = 'strict', disposeGraceMs = DEFAULT_DISPOSE_GRACE_MS } = config
|
|
111
|
+
this.strict = trailingSlash === 'strict'
|
|
64
112
|
this.graceMs = disposeGraceMs
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
113
|
+
this.logError = (error, c) => {
|
|
114
|
+
if (error instanceof HTTPException) return error.getResponse()
|
|
115
|
+
// A handler that gave up because its caller left or the router is closing is not a failure worth logging.
|
|
116
|
+
if (c.req.raw.signal.aborted) return new Response('Client Closed Request', { status: 499 })
|
|
117
|
+
ctx.logger.error(`acryl-router: handler failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
118
|
+
return c.text('Internal Server Error', 500)
|
|
119
|
+
}
|
|
69
120
|
ctx.effect(() => async () => {
|
|
70
|
-
this.closing = true
|
|
71
|
-
|
|
72
|
-
for (const flight of open) flight.controller.abort()
|
|
73
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
74
|
-
const gaveUp = new Promise<'gave-up'>(resolve => { timer = setTimeout(() => { resolve('gave-up') }, this.graceMs) })
|
|
75
|
-
const outcome = await Promise.race([Promise.allSettled(open.map(flight => flight.settled)).then(() => 'settled' as const), gaveUp])
|
|
76
|
-
clearTimeout(timer)
|
|
77
|
-
if (outcome === 'gave-up') ctx.logger.warn(`acryl-router: ${String(this.flights.size)} request(s) did not finish ${String(this.graceMs)} ms after abort; disposal continued without them`)
|
|
121
|
+
this.state.closing = true
|
|
122
|
+
await this.end([...this.flights], 'disposal')
|
|
78
123
|
}, 'acryl-router: abort and settle in-flight requests')
|
|
79
124
|
}
|
|
80
125
|
|
|
81
|
-
/**
|
|
126
|
+
/** Registrations currently held (mounts, and the router-wide error and not-found handlers); a leak check for reload tests. */
|
|
82
127
|
get size(): number {
|
|
83
|
-
return this.
|
|
128
|
+
return this.state.mounts.size + (this.state.errorHandler === undefined ? 0 : 1) + (this.state.notFoundHandler === undefined ? 0 : 1)
|
|
84
129
|
}
|
|
85
130
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Serve `app` under `path` (`'/'` for its own paths as they are). The mount belongs to the calling plugin and goes away when it unloads;
|
|
133
|
+
* the returned function removes it sooner. Mounts are composed in the order they were made. Like the core's own `app.route`, the routes
|
|
134
|
+
* are copied now: one the app registers after this call is not served (mount again, or finish the app first).
|
|
135
|
+
*/
|
|
136
|
+
route(path: string, app: MountableApp): () => void {
|
|
137
|
+
const mount = this.snapshot(path, app)
|
|
138
|
+
this.state.mounts.add(mount)
|
|
139
|
+
this.state.composed = undefined
|
|
140
|
+
// Removing a mount ends the requests its code is still serving: their signals abort, a body it produced is cancelled, and the
|
|
141
|
+
// plugin's unload waits for them (bounded by `disposeGraceMs`), so nothing a plugin started outlives it.
|
|
142
|
+
const remove = (): Promise<void> => {
|
|
143
|
+
if (!this.state.mounts.delete(mount)) return Promise.resolve()
|
|
144
|
+
this.state.composed = undefined
|
|
145
|
+
return this.end([...this.flights].filter(flight => (flight.running.get(mount) ?? 0) > 0 || flight.producer === mount), `removing ${path}`)
|
|
146
|
+
}
|
|
147
|
+
// The label lists what the mount serves, so the inspector can report routes without asking the router (`ALL` is middleware or `all`).
|
|
148
|
+
const served = app.routes.map(route => `${route.method} ${mergePath(path, route.path)}`).join(', ')
|
|
149
|
+
this.ctx.effect(() => remove, `acryl-router: route ${path} (${served})`)
|
|
150
|
+
return () => { void remove() }
|
|
97
151
|
}
|
|
98
152
|
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
153
|
+
/**
|
|
154
|
+
* The mount's own copy of `app`'s routes as they are now (like the core's `app.route`), each handler wrapped so the router knows which
|
|
155
|
+
* mount's code a request is in. The core's `app.route` runs first so a sub-app's own `onError` still applies to its routes.
|
|
156
|
+
*/
|
|
157
|
+
private snapshot(path: string, app: MountableApp): Mount {
|
|
158
|
+
const copy = new App().route('/', app)
|
|
159
|
+
const tagged = new App()
|
|
160
|
+
const mount: Mount = { path, app: tagged }
|
|
161
|
+
for (const route of copy.routes) {
|
|
162
|
+
const inner = route.handler
|
|
163
|
+
const handler: H = async (c, next) => {
|
|
164
|
+
const flight = this.flightOf.get(c.req.raw)
|
|
165
|
+
if (flight === undefined) return inner(c, next)
|
|
166
|
+
let calledNext = false
|
|
167
|
+
flight.running.set(mount, (flight.running.get(mount) ?? 0) + 1)
|
|
168
|
+
try {
|
|
169
|
+
const result = await inner(c, () => { calledNext = true; return next() })
|
|
170
|
+
if (!calledNext && result instanceof Response) flight.producer = mount
|
|
171
|
+
return result
|
|
172
|
+
} finally {
|
|
173
|
+
flight.running.set(mount, (flight.running.get(mount) ?? 1) - 1)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
;(handler as unknown as Record<string, unknown>)[COMPOSED_HANDLER] = inner
|
|
177
|
+
tagged.on(route.method, route.path, handler)
|
|
178
|
+
}
|
|
179
|
+
return mount
|
|
102
180
|
}
|
|
103
181
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
182
|
+
/** Abort `flights`, cut their bodies short and wait for them to settle, at most `disposeGraceMs`. */
|
|
183
|
+
private async end(flights: readonly Flight[], why: string): Promise<void> {
|
|
184
|
+
if (flights.length === 0) return
|
|
185
|
+
for (const flight of flights) {
|
|
186
|
+
flight.controller.abort()
|
|
187
|
+
flight.endBody()
|
|
188
|
+
}
|
|
189
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
190
|
+
const gaveUp = new Promise<'gave-up'>(resolve => { timer = setTimeout(() => { resolve('gave-up') }, this.graceMs) })
|
|
191
|
+
const outcome = await Promise.race([Promise.allSettled(flights.map(flight => flight.settled)).then(() => 'settled' as const), gaveUp])
|
|
192
|
+
clearTimeout(timer)
|
|
193
|
+
if (outcome === 'gave-up') this.ctx.logger.warn(`acryl-router: ${String(flights.length)} request(s) did not finish ${String(this.graceMs)} ms after abort (${why}); continued without them`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Turn errors no mounted app handled into responses. One handler per router; the calling plugin owns it. */
|
|
116
197
|
onError(handler: ErrorHandler): () => void {
|
|
117
|
-
|
|
118
|
-
this.
|
|
198
|
+
if (this.state.errorHandler !== undefined) throw new Error('router: an error handler is already registered')
|
|
199
|
+
this.state.errorHandler = handler
|
|
200
|
+
this.state.composed = undefined
|
|
201
|
+
const remove = (): void => {
|
|
202
|
+
if (this.state.errorHandler !== handler) return
|
|
203
|
+
this.state.errorHandler = undefined
|
|
204
|
+
this.state.composed = undefined
|
|
205
|
+
}
|
|
206
|
+
this.ctx.effect(() => remove, 'acryl-router: onError')
|
|
119
207
|
return remove
|
|
120
208
|
}
|
|
121
209
|
|
|
122
210
|
/** Answer requests no route claimed. One handler per router; the calling plugin owns it. */
|
|
123
211
|
notFound(handler: NotFoundHandler): () => void {
|
|
124
|
-
|
|
125
|
-
this.
|
|
212
|
+
if (this.state.notFoundHandler !== undefined) throw new Error('router: a not-found handler is already registered')
|
|
213
|
+
this.state.notFoundHandler = handler
|
|
214
|
+
this.state.composed = undefined
|
|
215
|
+
const remove = (): void => {
|
|
216
|
+
if (this.state.notFoundHandler !== handler) return
|
|
217
|
+
this.state.notFoundHandler = undefined
|
|
218
|
+
this.state.composed = undefined
|
|
219
|
+
}
|
|
220
|
+
this.ctx.effect(() => remove, 'acryl-router: notFound')
|
|
126
221
|
return remove
|
|
127
222
|
}
|
|
128
223
|
|
|
@@ -136,20 +231,43 @@ export class Router extends Service {
|
|
|
136
231
|
}
|
|
137
232
|
|
|
138
233
|
/**
|
|
139
|
-
* Run a request in process. Never throws: failures become a 500, a disposed router answers 503. `connection` describes
|
|
140
|
-
*
|
|
234
|
+
* Run a request in process. Never throws: failures become a 500, a disposed router answers 503. `connection` describes the transport
|
|
235
|
+
* (an HTTP adapter passes the socket's peer address and local port); an in-process caller omits it. Handlers read it with
|
|
236
|
+
* `connectionOf(c)`, abort with `c.req.raw.signal` and keep the request in flight with `c.executionCtx.waitUntil(task)`.
|
|
141
237
|
*/
|
|
142
238
|
fetch(request: Request, connection: ConnectionInfo = {}): Promise<Response> {
|
|
143
|
-
if (this.closing) return Promise.resolve(new Response('Service Unavailable', { status: 503 }))
|
|
239
|
+
if (this.state.closing) return Promise.resolve(new Response('Service Unavailable', { status: 503 }))
|
|
144
240
|
const controller = new AbortController()
|
|
145
241
|
const forward = (): void => controller.abort()
|
|
146
242
|
if (request.signal.aborted) controller.abort()
|
|
147
243
|
else request.signal.addEventListener('abort', forward, { once: true })
|
|
148
244
|
const tasks: Promise<unknown>[] = []
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
245
|
+
const executionCtx: ExecutionContext = {
|
|
246
|
+
waitUntil: task => { tasks.push(task) },
|
|
247
|
+
passThroughOnException: () => {},
|
|
248
|
+
props: {},
|
|
249
|
+
}
|
|
250
|
+
const env: RouterBindings = { connection }
|
|
251
|
+
let bodyDone: Promise<void> = Promise.resolve()
|
|
252
|
+
let endBody = (): void => {}
|
|
253
|
+
let resolveSettled!: (value: Promise<unknown>) => void
|
|
254
|
+
const flight: Flight = { controller, running: new Map(), producer: undefined, endBody: () => { endBody() }, settled: new Promise(resolve => { resolveSettled = resolve }) }
|
|
255
|
+
const inner = new Request(request, { signal: controller.signal })
|
|
256
|
+
this.flightOf.set(inner, flight)
|
|
257
|
+
// The caller gets the Response the moment the handler returns it; the flight lasts until its body ends and every waitUntil task settles.
|
|
258
|
+
const response = this.dispatch(inner, env, executionCtx).then(raw => {
|
|
259
|
+
const tracked = this.track(raw)
|
|
260
|
+
bodyDone = tracked.done
|
|
261
|
+
endBody = tracked.end
|
|
262
|
+
// Aborted before the handler answered (disposal, or the caller left): nobody is going to read this body, so end it now.
|
|
263
|
+
if (controller.signal.aborted) tracked.end()
|
|
264
|
+
return tracked.response
|
|
265
|
+
})
|
|
266
|
+
const settled = response.then(async () => {
|
|
267
|
+
await bodyDone
|
|
268
|
+
await drain(tasks)
|
|
269
|
+
})
|
|
270
|
+
resolveSettled(settled)
|
|
153
271
|
this.flights.add(flight)
|
|
154
272
|
void settled.finally(() => {
|
|
155
273
|
request.signal.removeEventListener('abort', forward)
|
|
@@ -157,6 +275,80 @@ export class Router extends Service {
|
|
|
157
275
|
})
|
|
158
276
|
return response
|
|
159
277
|
}
|
|
278
|
+
|
|
279
|
+
private async dispatch(request: Request, env: RouterBindings, executionCtx: ExecutionContext): Promise<Response> {
|
|
280
|
+
try {
|
|
281
|
+
return await this.app().fetch(request, env, executionCtx)
|
|
282
|
+
} catch (error) {
|
|
283
|
+
this.ctx.logger.error(`acryl-router: request failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
284
|
+
return new Response('Internal Server Error', { status: 500 })
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private app(): App {
|
|
289
|
+
if (this.state.composed !== undefined) return this.state.composed
|
|
290
|
+
// The trie router, because the composed app is rebuilt after every mount change: the default matcher compiles one regular expression
|
|
291
|
+
// over all routes on its first request, which grows faster than the route count (measured: 22 ms at 1000 routes, 163 ms at 2000),
|
|
292
|
+
// while the trie rebuilds in about 1 ms per 1000 routes and matches as fast (scripts/bench-remount.mjs).
|
|
293
|
+
const root = new App({ strict: this.strict, router: new TrieRouter() })
|
|
294
|
+
for (const mount of this.state.mounts) root.route(mount.path, mount.app)
|
|
295
|
+
root.onError(this.state.errorHandler ?? this.logError)
|
|
296
|
+
if (this.state.notFoundHandler !== undefined) root.notFound(this.state.notFoundHandler)
|
|
297
|
+
this.state.composed = root
|
|
298
|
+
return root
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Hand the caller a body the router can end. Reading it passes the handler's chunks through; disposal cancels the handler's stream (which runs
|
|
303
|
+
* the stream helpers' own abort cleanup) and closes the caller's side, so a stream never outlives the plugin that served it.
|
|
304
|
+
*/
|
|
305
|
+
private track(response: Response): { response: Response, done: Promise<void>, end: () => void } {
|
|
306
|
+
const source = response.body
|
|
307
|
+
if (source === null) return { response, done: Promise.resolve(), end: () => {} }
|
|
308
|
+
const reader = source.getReader()
|
|
309
|
+
let finish!: () => void
|
|
310
|
+
const done = new Promise<void>(resolve => { finish = resolve })
|
|
311
|
+
let ended = false
|
|
312
|
+
let sink: ReadableStreamDefaultController<Uint8Array> | undefined
|
|
313
|
+
const end = (): void => {
|
|
314
|
+
if (ended) return
|
|
315
|
+
ended = true
|
|
316
|
+
finish()
|
|
317
|
+
}
|
|
318
|
+
const endEarly = (): void => {
|
|
319
|
+
if (ended) return
|
|
320
|
+
reader.cancel(new Error('acryl-router: disposed')).catch(() => {})
|
|
321
|
+
try { sink?.close() } catch { /* already closed */ }
|
|
322
|
+
end()
|
|
323
|
+
}
|
|
324
|
+
const body = new ReadableStream<Uint8Array>({
|
|
325
|
+
start: controller => { sink = controller },
|
|
326
|
+
pull: async controller => {
|
|
327
|
+
try {
|
|
328
|
+
const chunk = await reader.read()
|
|
329
|
+
if (ended) return
|
|
330
|
+
if (chunk.done) {
|
|
331
|
+
controller.close()
|
|
332
|
+
end()
|
|
333
|
+
} else {
|
|
334
|
+
controller.enqueue(chunk.value)
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (!ended) controller.error(error)
|
|
338
|
+
end()
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
cancel: async reason => {
|
|
342
|
+
end()
|
|
343
|
+
await reader.cancel(reason)
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
return {
|
|
347
|
+
response: new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers }),
|
|
348
|
+
done,
|
|
349
|
+
end: endEarly,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
160
352
|
}
|
|
161
353
|
|
|
162
354
|
export function apply(ctx: Context, config: Partial<Config> = {}): void {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The examples `docs/framework/` relies on, run as written: if one of these fails, the page that shows it is wrong.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
6
|
+
import z from '@deepseek-ai/schemastery'
|
|
7
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
8
|
+
import { createMiddleware, schemaValidator, testClient } from '../src/helpers/index.ts'
|
|
9
|
+
import { App, HTTPException, apply, name } from '../src/index.ts'
|
|
10
|
+
import { methodNotAllowed } from '../src/middleware/index.ts'
|
|
11
|
+
|
|
12
|
+
const contexts: Context[] = []
|
|
13
|
+
const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
async function withPlugin(plugin: { name: string, inject: string[], apply(ctx: Context): void }): Promise<Context> {
|
|
19
|
+
const ctx = new Context()
|
|
20
|
+
contexts.push(ctx)
|
|
21
|
+
ctx.plugin({ name, apply })
|
|
22
|
+
ctx.plugin(plugin)
|
|
23
|
+
await settle()
|
|
24
|
+
return ctx
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('getting-started.md', () => {
|
|
28
|
+
it('serves the minimal plugin as the route table says', async () => {
|
|
29
|
+
const ctx = await withPlugin({
|
|
30
|
+
name: 'todo',
|
|
31
|
+
inject: ['router'],
|
|
32
|
+
apply(inner) {
|
|
33
|
+
const app = new App()
|
|
34
|
+
.get('/', c => c.json({ ok: true }))
|
|
35
|
+
.get('/todos/:id', c => c.json({ id: c.req.param('id') }))
|
|
36
|
+
inner.router.route('/todo', app)
|
|
37
|
+
inner.inject(['routerHttp'], deeper => { deeper.routerHttp.mount('/todo') })
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
expect(await (await ctx.router.request('/todo')).json()).toEqual({ ok: true })
|
|
41
|
+
expect(await (await ctx.router.request('/todo/todos/42')).json()).toEqual({ id: '42' })
|
|
42
|
+
expect((await ctx.router.request('/todo/')).status).toBe(404)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('routing.md', () => {
|
|
47
|
+
it('serves the routes an app had at the mount, not ones added later', async () => {
|
|
48
|
+
const app = new App().get('/a', c => c.text('a'))
|
|
49
|
+
const ctx = await withPlugin({ name: 'late', inject: ['router'], apply(inner) { inner.router.route('/x', app) } })
|
|
50
|
+
app.get('/b', c => c.text('b'))
|
|
51
|
+
expect((await ctx.router.request('/x/a')).status).toBe(200)
|
|
52
|
+
expect((await ctx.router.request('/x/b')).status).toBe(404)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('answers a wrong method with 404, or 405 and Allow with methodNotAllowed', async () => {
|
|
56
|
+
const plain = new App().get('/item', c => c.text('item'))
|
|
57
|
+
expect((await plain.request('/item', { method: 'POST' })).status).toBe(404)
|
|
58
|
+
const strict = new App()
|
|
59
|
+
strict.use(methodNotAllowed({ app: strict })).get('/item', c => c.text('item'))
|
|
60
|
+
const response = await strict.request('/item', { method: 'POST' })
|
|
61
|
+
expect(response.status).toBe(405)
|
|
62
|
+
expect(response.headers.get('allow')).toContain('GET')
|
|
63
|
+
|
|
64
|
+
const ctx = await withPlugin({ name: 'items', inject: ['router'], apply(inner) { inner.router.route('/shop', strict) } })
|
|
65
|
+
const mounted = await ctx.router.request('/shop/item', { method: 'POST' })
|
|
66
|
+
expect(mounted.status).toBe(405)
|
|
67
|
+
expect(mounted.headers.get('allow')).toContain('GET')
|
|
68
|
+
expect((await ctx.router.request('/shop/item')).status).toBe(200)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('uses a mounted app\'s onError for its routes', async () => {
|
|
72
|
+
const app = new App().get('/boom', () => { throw new Error('boom') })
|
|
73
|
+
app.onError((_error, c) => c.json({ error: 'internal_error' }, 500))
|
|
74
|
+
const ctx = await withPlugin({ name: 'errors', inject: ['router'], apply(inner) { inner.router.route('/', app) } })
|
|
75
|
+
expect(await (await ctx.router.request('/boom')).json()).toEqual({ error: 'internal_error' })
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('context.md and middleware.md', () => {
|
|
80
|
+
it('passes typed variables from middleware to the handler', async () => {
|
|
81
|
+
const withUser = createMiddleware<{ Variables: { user: { name: string } } }>(async (c, next) => {
|
|
82
|
+
c.set('user', { name: 'ada' })
|
|
83
|
+
await next()
|
|
84
|
+
})
|
|
85
|
+
const app = new App().use(withUser).get('/me', c => c.json(c.var.user))
|
|
86
|
+
expect(await (await app.request('/me')).json()).toEqual({ name: 'ada' })
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('never throws from next(): the error is already a response and is left in c.error', async () => {
|
|
90
|
+
let caught = false
|
|
91
|
+
let seen: unknown
|
|
92
|
+
const app = new App()
|
|
93
|
+
.use(async (c, next) => {
|
|
94
|
+
try {
|
|
95
|
+
await next()
|
|
96
|
+
} catch {
|
|
97
|
+
caught = true
|
|
98
|
+
}
|
|
99
|
+
seen = c.error
|
|
100
|
+
})
|
|
101
|
+
.get('/', () => { throw new Error('boom') })
|
|
102
|
+
expect((await app.request('/')).status).toBe(500)
|
|
103
|
+
expect(caught).toBe(false)
|
|
104
|
+
expect(seen).toBeInstanceOf(Error)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('turns an HTTPException into its own response', async () => {
|
|
108
|
+
const app = new App().get('/', () => { throw new HTTPException(403, { message: 'not yours' }) })
|
|
109
|
+
const response = await app.request('/')
|
|
110
|
+
expect(response.status).toBe(403)
|
|
111
|
+
expect(await response.text()).toBe('not yours')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('validation.md', () => {
|
|
116
|
+
const NewTodo = z.object({
|
|
117
|
+
title: z.string().required(),
|
|
118
|
+
done: z.boolean().default(false),
|
|
119
|
+
})
|
|
120
|
+
const app = new App().post('/todos', schemaValidator('json', NewTodo), c => c.json({ todo: c.req.valid('json') }, 201))
|
|
121
|
+
const post = (body: string, type?: string): Promise<Response> =>
|
|
122
|
+
Promise.resolve(app.request('/todos', { method: 'POST', body, headers: type === undefined ? {} : { 'content-type': type } }))
|
|
123
|
+
|
|
124
|
+
it('hands the handler the schema output', async () => {
|
|
125
|
+
const response = await post('{"title":"hello"}', 'application/json')
|
|
126
|
+
expect(response.status).toBe(201)
|
|
127
|
+
expect(await response.json()).toEqual({ todo: { title: 'hello', done: false } })
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('answers 400 with the issues the page shows', async () => {
|
|
131
|
+
const response = await post('{"title":1}', 'application/json')
|
|
132
|
+
expect(response.status).toBe(400)
|
|
133
|
+
expect(await response.json()).toEqual({ error: 'invalid json', issues: [{ message: '$.title expected string but got 1', path: 'title' }] })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('validates {} when the request is not JSON, so a required field fails', async () => {
|
|
137
|
+
expect((await post('{"title":"hello"}')).status).toBe(400)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('answers 400 for a body that is not JSON', async () => {
|
|
141
|
+
expect((await post('{', 'application/json')).status).toBe(400)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('helpers.md', () => {
|
|
146
|
+
it('calls a chained app through the typed test client', async () => {
|
|
147
|
+
const app = new App().get('/search', c => c.json({ query: c.req.query('q') }))
|
|
148
|
+
const client = testClient(app)
|
|
149
|
+
const response = await client.search.$get({ query: { q: 'cordis' } })
|
|
150
|
+
expect(await response.json()).toEqual({ query: 'cordis' })
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
9
9
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
10
|
-
import { apply, name } from '../src/index.ts'
|
|
10
|
+
import { App, apply, name } from '../src/index.ts'
|
|
11
11
|
|
|
12
12
|
const contexts: Context[] = []
|
|
13
13
|
const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
|
|
@@ -46,7 +46,7 @@ const extension = (): object => ({
|
|
|
46
46
|
name: 'live-ext',
|
|
47
47
|
inject: ['router'],
|
|
48
48
|
apply(ctx: Context): void {
|
|
49
|
-
ctx.router.get('/live', c => c.json({ ok: true }))
|
|
49
|
+
ctx.router.route('/', new App().get('/live', c => c.json({ ok: true })))
|
|
50
50
|
},
|
|
51
51
|
})
|
|
52
52
|
|
|
@@ -11,6 +11,7 @@ import { join } from 'node:path'
|
|
|
11
11
|
import { Context, type FiberState } from '@deepseek-ai/cordis'
|
|
12
12
|
import { Loader } from '@deepseek-ai/cordis-plugin-loader'
|
|
13
13
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
14
|
+
import { App } from '../src/index.ts'
|
|
14
15
|
|
|
15
16
|
const ACTIVE = 2 as FiberState.ACTIVE
|
|
16
17
|
const DISPOSED = 4 as FiberState.DISPOSED
|
|
@@ -45,7 +46,7 @@ describe('acryl-router through a real Loader', () => {
|
|
|
45
46
|
it('activates from the built entry and serves a request in process', async () => {
|
|
46
47
|
const { ctx, entryId } = await mount()
|
|
47
48
|
expect(entryOf(ctx, entryId).fiber?.state).toBe(ACTIVE)
|
|
48
|
-
ctx.router.get('/ping', c => c.json({ ok: true }))
|
|
49
|
+
ctx.router.route('/', new App().get('/ping', c => c.json({ ok: true })))
|
|
49
50
|
const response = await ctx.router.fetch(new Request('http://acryl.test/ping'))
|
|
50
51
|
expect(await response.json()).toEqual({ ok: true })
|
|
51
52
|
})
|
|
@@ -65,7 +66,7 @@ describe('acryl-router through a real Loader', () => {
|
|
|
65
66
|
expect(ctx.router).not.toBe(first)
|
|
66
67
|
expect(ctx.router.size).toBe(0)
|
|
67
68
|
expect((await first.fetch(new Request('http://acryl.test/x'))).status).toBe(503)
|
|
68
|
-
ctx.router.get('/again', c => c.text('ok'))
|
|
69
|
+
ctx.router.route('/', new App().get('/again', c => c.text('ok')))
|
|
69
70
|
expect(await (await ctx.router.fetch(new Request('http://acryl.test/again'))).text()).toBe('ok')
|
|
70
71
|
})
|
|
71
72
|
})
|