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,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The router as a Cordis service
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
10
|
-
import {
|
|
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
|
-
|
|
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
|
|
140
|
+
it('removes a consumer\'s mounts when the consumer unloads and leaves other consumers alone', async () => {
|
|
78
141
|
const { ctx, router } = await mountRouter()
|
|
79
142
|
const a = ctx.plugin(consumer('/a', 'A'))
|
|
80
143
|
ctx.plugin(consumer('/b', 'B'))
|
|
@@ -110,6 +173,54 @@ describe('acryl-router lifecycle through Cordis', () => {
|
|
|
110
173
|
expect(await (await get(ctx.router, '/c')).text()).toBe('A')
|
|
111
174
|
})
|
|
112
175
|
|
|
176
|
+
it('serves the routes an app had when it was mounted, whatever else changes later', async () => {
|
|
177
|
+
const { ctx, router } = await mountRouter()
|
|
178
|
+
const app = new App().get('/early', c => c.text('early'))
|
|
179
|
+
router.route('/', app)
|
|
180
|
+
app.get('/late', c => c.text('late'))
|
|
181
|
+
expect((await get(router, '/early')).status).toBe(200)
|
|
182
|
+
expect((await get(router, '/late')).status).toBe(404)
|
|
183
|
+
// Another plugin's mount recomposes the router; the late route still is not served.
|
|
184
|
+
ctx.plugin(consumer('/other', 'other'))
|
|
185
|
+
await settle()
|
|
186
|
+
expect((await get(router, '/other')).status).toBe(200)
|
|
187
|
+
expect((await get(router, '/late')).status).toBe(404)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('serves two plugins on the same path in mount order, and hands the path to the other when the first unloads', async () => {
|
|
191
|
+
const { ctx, router } = await mountRouter()
|
|
192
|
+
const first = ctx.plugin(consumer('/same', 'first'))
|
|
193
|
+
await settle()
|
|
194
|
+
ctx.plugin(consumer('/same', 'second'))
|
|
195
|
+
await settle()
|
|
196
|
+
expect(await (await get(router, '/same')).text()).toBe('first')
|
|
197
|
+
await first.dispose()
|
|
198
|
+
await settle()
|
|
199
|
+
expect(await (await get(router, '/same')).text()).toBe('second')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('runs a reloaded plugin\'s handler once per request: reloading leaves no duplicate route behind, ten times', async () => {
|
|
203
|
+
const { ctx, router } = await mountRouter()
|
|
204
|
+
let calls = 0
|
|
205
|
+
const counting = {
|
|
206
|
+
name: 'counting',
|
|
207
|
+
inject: ['router'],
|
|
208
|
+
apply(inner: Context): void {
|
|
209
|
+
inner.router.route('/', new App().use(async (_c, next) => { calls += 1; await next() }).get('/count', c => c.text(String(calls))))
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
for (let round = 0; round < 10; round += 1) {
|
|
213
|
+
const fiber = ctx.plugin(counting)
|
|
214
|
+
await settle()
|
|
215
|
+
expect(router.size).toBe(1)
|
|
216
|
+
calls = 0
|
|
217
|
+
expect(await (await get(router, '/count')).text()).toBe('1')
|
|
218
|
+
await fiber.dispose()
|
|
219
|
+
await settle()
|
|
220
|
+
expect(router.size).toBe(0)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
113
224
|
it('returns to its baseline after fifty consumer mount and unload cycles', async () => {
|
|
114
225
|
const { ctx, router } = await mountRouter()
|
|
115
226
|
ctx.plugin(consumer('/stay', 'S'))
|
|
@@ -119,10 +230,12 @@ describe('acryl-router lifecycle through Cordis', () => {
|
|
|
119
230
|
const fiber = ctx.plugin(consumer(`/cycle/${String(i)}`, `C${String(i)}`))
|
|
120
231
|
await settle()
|
|
121
232
|
expect(router.size).toBe(baseline + 1)
|
|
233
|
+
expect(await (await get(router, `/cycle/${String(i)}`)).text()).toBe(`C${String(i)}`)
|
|
122
234
|
await fiber.dispose()
|
|
123
235
|
await settle()
|
|
124
236
|
}
|
|
125
237
|
expect(router.size).toBe(baseline)
|
|
238
|
+
expect((await get(router, '/cycle/0')).status).toBe(404)
|
|
126
239
|
})
|
|
127
240
|
|
|
128
241
|
it('aborts an in-flight request and waits for it when the router is disposed, then answers 503', async () => {
|
|
@@ -131,81 +244,166 @@ describe('acryl-router lifecycle through Cordis', () => {
|
|
|
131
244
|
await settle()
|
|
132
245
|
const router = ctx.router
|
|
133
246
|
let aborted = false
|
|
134
|
-
router.get('/slow', async c => {
|
|
135
|
-
await new Promise<void>((_resolve, reject) => { c.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
|
|
247
|
+
router.route('/', new App().get('/slow', async c => {
|
|
248
|
+
await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
|
|
136
249
|
return c.text('late')
|
|
137
|
-
})
|
|
138
|
-
const pending = router
|
|
250
|
+
}))
|
|
251
|
+
const pending = get(router, '/slow')
|
|
139
252
|
await settle()
|
|
140
253
|
|
|
141
254
|
await fiber.dispose()
|
|
142
255
|
await settle()
|
|
143
256
|
expect(aborted).toBe(true)
|
|
144
257
|
expect((await pending).status).toBe(499)
|
|
145
|
-
expect((await router
|
|
258
|
+
expect((await get(router, '/slow')).status).toBe(503)
|
|
146
259
|
})
|
|
147
260
|
|
|
148
|
-
it('
|
|
261
|
+
it('forwards the caller\'s own abort to the handler signal', async () => {
|
|
149
262
|
const { router } = await mountRouter()
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
263
|
+
let seen = false
|
|
264
|
+
router.route('/', new App().get('/slow', async c => {
|
|
265
|
+
await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { seen = true; reject(new Error('aborted')) }, { once: true }) })
|
|
266
|
+
return c.text('x')
|
|
267
|
+
}))
|
|
268
|
+
const controller = new AbortController()
|
|
269
|
+
const pending = router.fetch(new Request('http://acryl.test/slow', { signal: controller.signal }))
|
|
270
|
+
await settle()
|
|
271
|
+
controller.abort()
|
|
272
|
+
expect((await pending).status).toBe(499)
|
|
273
|
+
expect(seen).toBe(true)
|
|
155
274
|
})
|
|
156
275
|
|
|
157
|
-
it('
|
|
276
|
+
it('ends a server-sent-event stream when the router is disposed: the stream\'s abort cleanup runs and the reader sees the end', async () => {
|
|
158
277
|
const ctx = root()
|
|
159
278
|
const fiber = ctx.plugin(provider, config)
|
|
160
279
|
await settle()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
c.waitUntil(done)
|
|
170
|
-
controller.enqueue(new TextEncoder().encode('first'))
|
|
171
|
-
},
|
|
172
|
-
})
|
|
173
|
-
return new Response(body)
|
|
174
|
-
})
|
|
175
|
-
const response = await router.fetch(new Request('http://acryl.test/stream'))
|
|
176
|
-
expect(response.status).toBe(200)
|
|
280
|
+
let cleaned = false
|
|
281
|
+
ctx.router.route('/', new App().get('/events', c => streamSSE(c, async stream => {
|
|
282
|
+
stream.onAbort(() => { cleaned = true })
|
|
283
|
+
await stream.writeSSE({ data: 'first' })
|
|
284
|
+
while (!stream.aborted) await stream.sleep(10)
|
|
285
|
+
})))
|
|
286
|
+
const response = await get(ctx.router, '/events')
|
|
287
|
+
expect(response.headers.get('content-type')).toBe('text/event-stream')
|
|
177
288
|
const reader = response.body!.getReader()
|
|
178
|
-
expect(new TextDecoder().decode((await reader.read()).value)).toBe('first')
|
|
289
|
+
expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
|
|
179
290
|
|
|
180
291
|
await fiber.dispose()
|
|
181
|
-
expect(
|
|
292
|
+
expect(cleaned).toBe(true)
|
|
182
293
|
await expect(reader.read()).resolves.toMatchObject({ done: true })
|
|
183
294
|
})
|
|
184
295
|
|
|
296
|
+
it('ends the stream a plugin opened when that plugin unloads, and leaves another plugin\'s request running', async () => {
|
|
297
|
+
const { ctx, router } = await mountRouter()
|
|
298
|
+
let cleaned = false
|
|
299
|
+
let otherAborted = false
|
|
300
|
+
let release!: () => void
|
|
301
|
+
const streaming = ctx.plugin({
|
|
302
|
+
name: 'streaming',
|
|
303
|
+
inject: ['router'],
|
|
304
|
+
apply(inner: Context): void {
|
|
305
|
+
inner.router.route('/', new App().get('/events', c => streamSSE(c, async stream => {
|
|
306
|
+
stream.onAbort(() => { cleaned = true })
|
|
307
|
+
await stream.writeSSE({ data: 'first' })
|
|
308
|
+
while (!stream.aborted) await stream.sleep(10)
|
|
309
|
+
})))
|
|
310
|
+
},
|
|
311
|
+
})
|
|
312
|
+
ctx.plugin({
|
|
313
|
+
name: 'other',
|
|
314
|
+
inject: ['router'],
|
|
315
|
+
apply(inner: Context): void {
|
|
316
|
+
inner.router.route('/', new App().get('/slow', async c => {
|
|
317
|
+
c.req.raw.signal.addEventListener('abort', () => { otherAborted = true }, { once: true })
|
|
318
|
+
await new Promise<void>(resolve => { release = resolve })
|
|
319
|
+
return c.text('done')
|
|
320
|
+
}))
|
|
321
|
+
},
|
|
322
|
+
})
|
|
323
|
+
await settle()
|
|
324
|
+
const response = await get(router, '/events')
|
|
325
|
+
const reader = response.body!.getReader()
|
|
326
|
+
expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
|
|
327
|
+
const slow = get(router, '/slow')
|
|
328
|
+
await settle()
|
|
329
|
+
|
|
330
|
+
await streaming.dispose()
|
|
331
|
+
expect(cleaned).toBe(true)
|
|
332
|
+
await expect(reader.read()).resolves.toMatchObject({ done: true })
|
|
333
|
+
expect(otherAborted).toBe(false)
|
|
334
|
+
release()
|
|
335
|
+
expect(await (await slow).text()).toBe('done')
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('aborts a request still inside an unloading plugin\'s middleware, but not one that middleware already passed on', async () => {
|
|
339
|
+
const { ctx, router } = await mountRouter()
|
|
340
|
+
let guardAborted = false
|
|
341
|
+
let handlerAborted = false
|
|
342
|
+
let release!: () => void
|
|
343
|
+
const guard = ctx.plugin({
|
|
344
|
+
name: 'guard',
|
|
345
|
+
inject: ['router'],
|
|
346
|
+
apply(inner: Context): void {
|
|
347
|
+
inner.router.route('/', new App()
|
|
348
|
+
.use('/held/*', async (c, next) => {
|
|
349
|
+
c.req.raw.signal.addEventListener('abort', () => { guardAborted = true }, { once: true })
|
|
350
|
+
await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true }) })
|
|
351
|
+
await next()
|
|
352
|
+
})
|
|
353
|
+
.use('/passed/*', async (_c, next) => { await next() }))
|
|
354
|
+
},
|
|
355
|
+
})
|
|
356
|
+
ctx.plugin({
|
|
357
|
+
name: 'handler',
|
|
358
|
+
inject: ['router'],
|
|
359
|
+
apply(inner: Context): void {
|
|
360
|
+
inner.router.route('/', new App()
|
|
361
|
+
.get('/held/x', c => c.text('held'))
|
|
362
|
+
.get('/passed/x', c => streamSSE(c, async stream => {
|
|
363
|
+
stream.onAbort(() => { handlerAborted = true })
|
|
364
|
+
await stream.writeSSE({ data: 'open' })
|
|
365
|
+
await new Promise<void>(resolve => { release = resolve })
|
|
366
|
+
})))
|
|
367
|
+
},
|
|
368
|
+
})
|
|
369
|
+
await settle()
|
|
370
|
+
const held = get(router, '/held/x')
|
|
371
|
+
const passed = await get(router, '/passed/x')
|
|
372
|
+
const reader = passed.body!.getReader()
|
|
373
|
+
await reader.read()
|
|
374
|
+
|
|
375
|
+
await guard.dispose()
|
|
376
|
+
expect(guardAborted).toBe(true)
|
|
377
|
+
expect((await held).status).toBe(499)
|
|
378
|
+
expect(handlerAborted).toBe(false)
|
|
379
|
+
release()
|
|
380
|
+
await expect(reader.read()).resolves.toMatchObject({ done: true })
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('keeps a request in flight through waitUntil until its task settles', async () => {
|
|
384
|
+
const ctx = root()
|
|
385
|
+
const fiber = ctx.plugin(provider, config)
|
|
386
|
+
await settle()
|
|
387
|
+
let finished = false
|
|
388
|
+
ctx.router.route('/', new App().get('/later', c => {
|
|
389
|
+
c.executionCtx.waitUntil(new Promise<void>(resolve => { setTimeout(() => { finished = true; resolve() }, 30) }))
|
|
390
|
+
return c.text('now')
|
|
391
|
+
}))
|
|
392
|
+
expect(await (await get(ctx.router, '/later')).text()).toBe('now')
|
|
393
|
+
expect(finished).toBe(false)
|
|
394
|
+
await fiber.dispose()
|
|
395
|
+
expect(finished).toBe(true)
|
|
396
|
+
})
|
|
397
|
+
|
|
185
398
|
it('gives up on a request that ignores the abort after the grace period instead of hanging disposal', async () => {
|
|
186
399
|
const ctx = root()
|
|
187
400
|
const fiber = ctx.plugin(provider, { trailingSlash: 'strict', disposeGraceMs: 30 })
|
|
188
401
|
await settle()
|
|
189
|
-
ctx.router.get('/stuck', c => { c.waitUntil(new Promise(() => undefined)); return c.text('x') })
|
|
190
|
-
await ctx.router
|
|
402
|
+
ctx.router.route('/', new App().get('/stuck', c => { c.executionCtx.waitUntil(new Promise(() => undefined)); return c.text('x') }))
|
|
403
|
+
await get(ctx.router, '/stuck')
|
|
191
404
|
const started = Date.now()
|
|
192
405
|
await fiber.dispose()
|
|
193
406
|
expect(Date.now() - started).toBeLessThan(1000)
|
|
194
407
|
expect(fiber.state).toBe(DISPOSED)
|
|
195
408
|
})
|
|
196
|
-
|
|
197
|
-
it('forwards the caller\'s own abort to the handler signal', async () => {
|
|
198
|
-
const { router } = await mountRouter()
|
|
199
|
-
let seen = false
|
|
200
|
-
router.get('/slow', async c => {
|
|
201
|
-
await new Promise<void>((_resolve, reject) => { c.signal.addEventListener('abort', () => { seen = true; reject(new Error('aborted')) }, { once: true }) })
|
|
202
|
-
return c.text('x')
|
|
203
|
-
})
|
|
204
|
-
const controller = new AbortController()
|
|
205
|
-
const pending = router.fetch(new Request('http://acryl.test/slow', { signal: controller.signal }))
|
|
206
|
-
await settle()
|
|
207
|
-
controller.abort()
|
|
208
|
-
expect((await pending).status).toBe(499)
|
|
209
|
-
expect(seen).toBe(true)
|
|
210
|
-
})
|
|
211
409
|
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Service's fields look like to the plugins that call it. A plugin reaches a service through its own shadow of the instance
|
|
3
|
+
* (`ctx.counter` in plugin A and in plugin B are two different objects over one service), but reads and writes of fields pass through to
|
|
4
|
+
* the one instance: a field assigned in a method is seen by every caller, as is state kept in an object that is only mutated. What the
|
|
5
|
+
* shadow does change is `this.ctx` inside a method, which `live-install.spec.ts` covers.
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Service } from '@deepseek-ai/cordis'
|
|
8
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
9
|
+
|
|
10
|
+
declare module '@deepseek-ai/cordis' {
|
|
11
|
+
interface Context {
|
|
12
|
+
counter: Counter
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const contexts: Context[] = []
|
|
17
|
+
const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
class Counter extends Service {
|
|
23
|
+
/** Reassigned by `assign`. */
|
|
24
|
+
assigned = 0
|
|
25
|
+
/** Created once, only mutated. */
|
|
26
|
+
private readonly state = { count: 0 }
|
|
27
|
+
|
|
28
|
+
constructor(ctx: Context) {
|
|
29
|
+
super(ctx, 'counter')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
assign(): void {
|
|
33
|
+
this.assigned += 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
mutate(): void {
|
|
37
|
+
this.state.count += 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get count(): number {
|
|
41
|
+
return this.state.count
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function twoCallers(): Promise<{ a: Context, b: Context }> {
|
|
46
|
+
const root = new Context()
|
|
47
|
+
contexts.push(root)
|
|
48
|
+
root.plugin({ name: 'counter', apply: (ctx: Context) => { new Counter(ctx) } })
|
|
49
|
+
const callers: Context[] = []
|
|
50
|
+
for (const name of ['a', 'b']) root.plugin({ name, inject: ['counter'], apply: (ctx: Context) => { callers.push(ctx) } })
|
|
51
|
+
await settle()
|
|
52
|
+
const [a, b] = callers
|
|
53
|
+
if (a === undefined || b === undefined) throw new Error('both callers should be active')
|
|
54
|
+
return { a, b }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('service state and the shadow context', () => {
|
|
58
|
+
it('shows a field assigned in a method through one caller to every caller', async () => {
|
|
59
|
+
const { a, b } = await twoCallers()
|
|
60
|
+
a.counter.assign()
|
|
61
|
+
b.counter.assign()
|
|
62
|
+
expect(a.counter.assigned).toBe(2)
|
|
63
|
+
expect(b.counter.assigned).toBe(2)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('shows a field assigned from outside through one caller to every caller', async () => {
|
|
67
|
+
const { a, b } = await twoCallers()
|
|
68
|
+
a.counter.assigned = 5
|
|
69
|
+
expect(b.counter.assigned).toBe(5)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('shares state kept in one object that is only mutated', async () => {
|
|
73
|
+
const { a, b } = await twoCallers()
|
|
74
|
+
a.counter.mutate()
|
|
75
|
+
b.counter.mutate()
|
|
76
|
+
expect(a.counter.count).toBe(2)
|
|
77
|
+
expect(b.counter.count).toBe(2)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"source": {
|
|
3
3
|
"repo": "acryldev/acryl",
|
|
4
4
|
"path": "plugins/acryl-router",
|
|
5
|
-
"commit": "
|
|
5
|
+
"commit": "df79b188398b6d4f8cceda35a72b88300a10a9ce"
|
|
6
6
|
},
|
|
7
7
|
"vendored": [
|
|
8
8
|
"src",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"LICENSE"
|
|
11
11
|
],
|
|
12
12
|
"rewrittenSpecifiers": 0,
|
|
13
|
-
"digest": "sha256:
|
|
13
|
+
"digest": "sha256:6e15f8ce0499b94c95649342f6fe984bc9b2aec0f5a142e8853a5badcc3b85f5",
|
|
14
14
|
"testsNotRun": {
|
|
15
15
|
"tests/loader.spec.ts": "loads the built lib/ entry through the real Loader; the library ships source and has no build"
|
|
16
16
|
},
|
|
@@ -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
|
-
|
|
28
|
+
// Node's fetch requires `duplex: 'half'` for a streamed body; the DOM lib's `RequestInit` does not declare the field yet.
|
|
29
|
+
const init: RequestInit & { duplex: 'half' } = { method, headers, signal, body: Readable.toWeb(req) as ReadableStream, duplex: 'half' }
|
|
30
|
+
return new Request(url, init)
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -8,7 +8,8 @@ import { Context, type FiberState } from '@deepseek-ai/cordis'
|
|
|
8
8
|
import WebServer from '@deepseek-ai/dsh-host-webserver'
|
|
9
9
|
import { apply as routerApply, name as routerName } from '../../acryl-router/src/index.ts'
|
|
10
10
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
11
|
-
import type
|
|
11
|
+
import { App, connectionOf, type RouterHttpService } from '../../acryl-router/src/index.ts'
|
|
12
|
+
import { methodNotAllowed } from '../../acryl-router/src/middleware/index.ts'
|
|
12
13
|
import { apply, inject, name } from '../src/index.ts'
|
|
13
14
|
|
|
14
15
|
const PENDING = 0 as FiberState.PENDING
|
|
@@ -39,25 +40,28 @@ async function stack(): Promise<{ ctx: Context, base: () => string }> {
|
|
|
39
40
|
return { ctx, base: () => `http://127.0.0.1:${String((ctx as unknown as { webServer: { port: number } }).webServer.port)}` }
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function demo(prefix: string, extra?: (
|
|
43
|
+
function demo(prefix: string, extra?: (app: App) => void) {
|
|
43
44
|
return {
|
|
44
45
|
name: `demo${prefix.replaceAll('/', '-')}`,
|
|
45
46
|
inject: ['router', 'routerHttp'],
|
|
46
47
|
apply(ctx: Context): void {
|
|
47
48
|
ctx.routerHttp.mount(prefix)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const app = new App()
|
|
50
|
+
app.use(methodNotAllowed({ app }))
|
|
51
|
+
app.get('/hello/:who', c => c.json({ hello: c.req.param('who'), q: c.req.query('q') ?? null, host: c.req.header('host') !== undefined }))
|
|
52
|
+
app.post('/echo', async c => c.json({ got: await c.req.text(), type: c.req.header('content-type') ?? null }, 201))
|
|
53
|
+
app.get('/cookies', () => {
|
|
51
54
|
const headers = new Headers()
|
|
52
55
|
headers.append('set-cookie', 'a=1; Path=/')
|
|
53
56
|
headers.append('set-cookie', 'b=2; Path=/')
|
|
54
57
|
return new Response('ok', { headers })
|
|
55
58
|
})
|
|
56
|
-
|
|
59
|
+
app.get('/stream', () => new Response(new ReadableStream({
|
|
57
60
|
start(controller) { controller.enqueue(new TextEncoder().encode('one,')); controller.enqueue(new TextEncoder().encode('two')); controller.close() },
|
|
58
61
|
})))
|
|
59
|
-
|
|
60
|
-
extra?.(
|
|
62
|
+
app.get('/peer', c => c.json({ remote: connectionOf(c).remoteAddress ?? null, localPort: connectionOf(c).localPort ?? null }))
|
|
63
|
+
extra?.(app)
|
|
64
|
+
ctx.router.route(prefix, app)
|
|
61
65
|
},
|
|
62
66
|
}
|
|
63
67
|
}
|
|
@@ -106,7 +110,7 @@ describe('acryl-router-http against a real webServer', () => {
|
|
|
106
110
|
await settle(20)
|
|
107
111
|
const missing = await fetch(`${base()}/api/demo/none`)
|
|
108
112
|
expect(missing.status).toBe(404)
|
|
109
|
-
expect(await missing.text()).toBe('Not Found')
|
|
113
|
+
expect(await missing.text()).toBe('404 Not Found')
|
|
110
114
|
expect((await fetch(`${base()}/api/demo/echo`)).status).toBe(405)
|
|
111
115
|
const head = await fetch(`${base()}/api/demo/hello/a`, { method: 'HEAD' })
|
|
112
116
|
expect(head.status).toBe(200)
|
|
@@ -117,9 +121,9 @@ describe('acryl-router-http against a real webServer', () => {
|
|
|
117
121
|
it('aborts the handler signal when the HTTP client disconnects', async () => {
|
|
118
122
|
const { ctx, base } = await stack()
|
|
119
123
|
let aborted = false
|
|
120
|
-
ctx.plugin(demo('/api/demo',
|
|
121
|
-
|
|
122
|
-
await new Promise<void>((_resolve, reject) => { r.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
|
|
124
|
+
ctx.plugin(demo('/api/demo', app => {
|
|
125
|
+
app.get('/slow', async r => {
|
|
126
|
+
await new Promise<void>((_resolve, reject) => { r.req.raw.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
|
|
123
127
|
return r.text('never')
|
|
124
128
|
})
|
|
125
129
|
}))
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { Context } from '@deepseek-ai/cordis'
|
|
9
9
|
import WebServer from '@deepseek-ai/dsh-host-webserver'
|
|
10
|
-
import { apply as routerApply, name as routerName } from '../../acryl-router/src/index.ts'
|
|
10
|
+
import { App, apply as routerApply, name as routerName } from '../../acryl-router/src/index.ts'
|
|
11
11
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
12
12
|
import { apply as httpApply, inject as httpInject, name as httpName } from '../src/index.ts'
|
|
13
13
|
|
|
@@ -28,15 +28,17 @@ const notes = {
|
|
|
28
28
|
inject: ['router'],
|
|
29
29
|
apply(ctx: Context): void {
|
|
30
30
|
const store = new Map<string, string>()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
const app = new App()
|
|
32
|
+
.post('/:id', async c => {
|
|
33
|
+
const text = await c.req.text()
|
|
34
|
+
store.set(c.req.param('id'), text)
|
|
35
|
+
return c.json({ id: c.req.param('id'), saved: text.length }, 201)
|
|
36
|
+
})
|
|
37
|
+
.get('/:id', c => {
|
|
38
|
+
const text = store.get(c.req.param('id'))
|
|
39
|
+
return text === undefined ? c.json({ error: 'not found' }, 404) : c.json({ id: c.req.param('id'), text })
|
|
40
|
+
})
|
|
41
|
+
ctx.router.route('/api/notes', app)
|
|
40
42
|
ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/api/notes') })
|
|
41
43
|
},
|
|
42
44
|
}
|