acrylblends 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +6 -1
- package/dist/index.js +84 -5
- package/dist/index.js.map +1 -1
- package/docs/framework/LICENSE +23 -0
- package/docs/framework/README.md +63 -0
- package/docs/framework/UPSTREAM.md +3 -0
- package/docs/framework/api-summary.md +138 -0
- package/docs/framework/concepts.md +105 -0
- package/docs/framework/context.md +129 -0
- package/docs/framework/getting-started.md +146 -0
- package/docs/framework/helpers.md +118 -0
- package/docs/framework/middleware.md +154 -0
- package/docs/framework/migration-from-0-3.md +108 -0
- package/docs/framework/migration-from-dsh-webserver.md +103 -0
- package/docs/framework/node-server.md +75 -0
- package/docs/framework/plugin-lifecycle.md +111 -0
- package/docs/framework/request.md +106 -0
- package/docs/framework/routing.md +188 -0
- package/docs/framework/surfaces.md +71 -0
- package/docs/framework/upstream.json +9 -0
- package/docs/framework/validation.md +135 -0
- package/package.json +4 -2
- package/plugins/acryl-inspect/plugin.json +2 -1
- package/plugins/acryl-inspect/src/report.ts +21 -13
- package/plugins/acryl-inspect/src/service.ts +9 -7
- package/plugins/acryl-inspect/tests/report.spec.ts +13 -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 +53 -14
- package/plugins/acryl-router/src/service.ts +262 -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/loopback-only.spec.ts +80 -0
- package/plugins/acryl-router/tests/service.spec.ts +281 -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 +6 -5
- package/plugins/extensions/resolve.ts +28 -3
- package/plugins/openai/prompt.ts +9 -7
- package/plugins/tsconfig.json +5 -4
- package/shared/AGENTS.core.md +54 -0
- package/templates/agent/AGENTS.md +6 -0
- package/templates/agent/README.md +2 -2
- package/templates/agent/blend.yaml +1 -1
- 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/AGENTS.md +5 -0
- package/templates/blank/README.md +1 -1
- package/templates/blank/src/routes.ts +6 -4
- package/templates/blank/tsconfig.json +2 -4
- package/templates/todo/AGENTS.md +5 -0
- package/templates/todo/src/page.ts +1 -1
- package/templates/todo/src/todo.ts +58 -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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `loopbackOnly`, the guard of a local app's own routes: the peer and port come from the transport, the `Host` names this app, and the
|
|
3
|
+
* `callers` rule decides whether any local program or only the app's own page may call.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { App, type ConnectionInfo } from '../src/index.ts'
|
|
8
|
+
import { loopbackOnly, type LoopbackOptions } from '../src/middleware/index.ts'
|
|
9
|
+
|
|
10
|
+
const PORT = 43_120
|
|
11
|
+
const SELF = `http://127.0.0.1:${String(PORT)}`
|
|
12
|
+
const LOCAL: ConnectionInfo = { remoteAddress: '127.0.0.1', localPort: PORT }
|
|
13
|
+
|
|
14
|
+
function guarded(options: LoopbackOptions = {}): App {
|
|
15
|
+
return new App().use(loopbackOnly(options)).get('/x', c => c.text('read')).post('/x', c => c.text('changed'))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function status(
|
|
19
|
+
app: App,
|
|
20
|
+
method: string,
|
|
21
|
+
headers: Record<string, string>,
|
|
22
|
+
connection: ConnectionInfo = LOCAL,
|
|
23
|
+
): Promise<number> {
|
|
24
|
+
const response = await app.fetch(new Request(`${SELF}/x`, { method, headers: { host: `127.0.0.1:${String(PORT)}`, ...headers } }), { connection })
|
|
25
|
+
return response.status
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('loopbackOnly, callers: local (the default)', () => {
|
|
29
|
+
const app = guarded()
|
|
30
|
+
|
|
31
|
+
it('lets a local program read and change, and a page of this app change', async () => {
|
|
32
|
+
expect(await status(app, 'GET', {})).toBe(200)
|
|
33
|
+
expect(await status(app, 'POST', {})).toBe(200)
|
|
34
|
+
expect(await status(app, 'POST', { origin: SELF })).toBe(200)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('refuses another origin\'s change, a foreign peer, a wrong port, a foreign Host and an in-process call', async () => {
|
|
38
|
+
expect(await status(app, 'POST', { origin: 'http://evil.test' })).toBe(403)
|
|
39
|
+
expect(await status(app, 'GET', {}, { remoteAddress: '192.168.1.5', localPort: PORT })).toBe(403)
|
|
40
|
+
expect(await status(app, 'GET', {}, { remoteAddress: '127.0.0.1', localPort: PORT + 1 })).toBe(403)
|
|
41
|
+
expect(await status(app, 'GET', { host: 'evil.test' })).toBe(403)
|
|
42
|
+
expect(await status(app, 'GET', {}, {})).toBe(403)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('lets an in-process call through only when the host says so', async () => {
|
|
46
|
+
expect(await status(guarded({ inProcess: 'allow' }), 'GET', {}, {})).toBe(200)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('loopbackOnly, callers: own-page', () => {
|
|
51
|
+
const app = guarded({ callers: 'own-page', hostnames: ['127.0.0.1'] })
|
|
52
|
+
|
|
53
|
+
it('lets this app\'s page read and change with its exact Origin', async () => {
|
|
54
|
+
expect(await status(app, 'GET', { origin: SELF, 'sec-fetch-site': 'same-origin' })).toBe(200)
|
|
55
|
+
expect(await status(app, 'POST', { origin: SELF, 'sec-fetch-site': 'same-origin' })).toBe(200)
|
|
56
|
+
expect(await status(app, 'POST', { origin: SELF })).toBe(200)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('lets a same-origin read without Origin through by its Referer, as browsers send it', async () => {
|
|
60
|
+
expect(await status(app, 'GET', { 'sec-fetch-site': 'same-origin', referer: `${SELF}/page?tab=1` })).toBe(200)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('refuses a local program (no Origin), even for a read', async () => {
|
|
64
|
+
expect(await status(app, 'GET', {})).toBe(403)
|
|
65
|
+
expect(await status(app, 'POST', {})).toBe(403)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('refuses a change that only has the read evidence, and Origin forms that are not exact', async () => {
|
|
69
|
+
expect(await status(app, 'POST', { 'sec-fetch-site': 'same-origin', referer: `${SELF}/` })).toBe(403)
|
|
70
|
+
expect(await status(app, 'POST', { origin: `${SELF}/` })).toBe(403)
|
|
71
|
+
expect(await status(app, 'POST', { origin: 'null' })).toBe(403)
|
|
72
|
+
expect(await status(app, 'POST', { origin: SELF, 'sec-fetch-site': 'same-site' })).toBe(403)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('refuses another page, a Referer from elsewhere and a Host this app does not answer to', async () => {
|
|
76
|
+
expect(await status(app, 'GET', { origin: 'http://evil.test', 'sec-fetch-site': 'cross-site' })).toBe(403)
|
|
77
|
+
expect(await status(app, 'GET', { 'sec-fetch-site': 'same-origin', referer: 'http://evil.test/' })).toBe(403)
|
|
78
|
+
expect(await status(app, 'GET', { host: `localhost:${String(PORT)}`, origin: `http://localhost:${String(PORT)}` })).toBe(403)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The router as a Cordis service
|
|
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'))
|
|
@@ -89,6 +152,23 @@ describe('acryl-router lifecycle through Cordis', () => {
|
|
|
89
152
|
expect(router.size).toBe(1)
|
|
90
153
|
})
|
|
91
154
|
|
|
155
|
+
it('labels a mount by its path with one child per route, so no label grows with the app (an inspector caps label length)', async () => {
|
|
156
|
+
const { ctx, router } = await mountRouter()
|
|
157
|
+
const big = new App().use('/api/*', async (_c, next) => { await next() })
|
|
158
|
+
for (let i = 0; i < 60; i += 1) big.get(`/api/a-fairly-long-resource-name-${String(i)}`, c => c.text('ok'))
|
|
159
|
+
const fiber = ctx.plugin({ name: 'big', inject: ['router'], apply: (inner: Context) => { inner.router.route('/', big) } })
|
|
160
|
+
await settle()
|
|
161
|
+
const [mount, ...rest] = fiber.getEffects()
|
|
162
|
+
expect(rest).toEqual([])
|
|
163
|
+
expect(mount?.label).toBe('acryl-router: route /')
|
|
164
|
+
expect(mount?.children).toHaveLength(61)
|
|
165
|
+
expect(mount?.children[0]?.label).toBe('acryl-router: serves ALL /api/*')
|
|
166
|
+
expect(mount?.children.every(child => child.label.length < 100 && child.children.length === 0)).toBe(true)
|
|
167
|
+
expect(router.size).toBe(1)
|
|
168
|
+
await fiber.dispose()
|
|
169
|
+
expect(router.size).toBe(0)
|
|
170
|
+
})
|
|
171
|
+
|
|
92
172
|
it('replaces the provider without leaving doubled registrations: consumers re-register exactly once', async () => {
|
|
93
173
|
const ctx = root()
|
|
94
174
|
const first = ctx.plugin(provider, config)
|
|
@@ -110,6 +190,54 @@ describe('acryl-router lifecycle through Cordis', () => {
|
|
|
110
190
|
expect(await (await get(ctx.router, '/c')).text()).toBe('A')
|
|
111
191
|
})
|
|
112
192
|
|
|
193
|
+
it('serves the routes an app had when it was mounted, whatever else changes later', async () => {
|
|
194
|
+
const { ctx, router } = await mountRouter()
|
|
195
|
+
const app = new App().get('/early', c => c.text('early'))
|
|
196
|
+
router.route('/', app)
|
|
197
|
+
app.get('/late', c => c.text('late'))
|
|
198
|
+
expect((await get(router, '/early')).status).toBe(200)
|
|
199
|
+
expect((await get(router, '/late')).status).toBe(404)
|
|
200
|
+
// Another plugin's mount recomposes the router; the late route still is not served.
|
|
201
|
+
ctx.plugin(consumer('/other', 'other'))
|
|
202
|
+
await settle()
|
|
203
|
+
expect((await get(router, '/other')).status).toBe(200)
|
|
204
|
+
expect((await get(router, '/late')).status).toBe(404)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('serves two plugins on the same path in mount order, and hands the path to the other when the first unloads', async () => {
|
|
208
|
+
const { ctx, router } = await mountRouter()
|
|
209
|
+
const first = ctx.plugin(consumer('/same', 'first'))
|
|
210
|
+
await settle()
|
|
211
|
+
ctx.plugin(consumer('/same', 'second'))
|
|
212
|
+
await settle()
|
|
213
|
+
expect(await (await get(router, '/same')).text()).toBe('first')
|
|
214
|
+
await first.dispose()
|
|
215
|
+
await settle()
|
|
216
|
+
expect(await (await get(router, '/same')).text()).toBe('second')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('runs a reloaded plugin\'s handler once per request: reloading leaves no duplicate route behind, ten times', async () => {
|
|
220
|
+
const { ctx, router } = await mountRouter()
|
|
221
|
+
let calls = 0
|
|
222
|
+
const counting = {
|
|
223
|
+
name: 'counting',
|
|
224
|
+
inject: ['router'],
|
|
225
|
+
apply(inner: Context): void {
|
|
226
|
+
inner.router.route('/', new App().use(async (_c, next) => { calls += 1; await next() }).get('/count', c => c.text(String(calls))))
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
for (let round = 0; round < 10; round += 1) {
|
|
230
|
+
const fiber = ctx.plugin(counting)
|
|
231
|
+
await settle()
|
|
232
|
+
expect(router.size).toBe(1)
|
|
233
|
+
calls = 0
|
|
234
|
+
expect(await (await get(router, '/count')).text()).toBe('1')
|
|
235
|
+
await fiber.dispose()
|
|
236
|
+
await settle()
|
|
237
|
+
expect(router.size).toBe(0)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
113
241
|
it('returns to its baseline after fifty consumer mount and unload cycles', async () => {
|
|
114
242
|
const { ctx, router } = await mountRouter()
|
|
115
243
|
ctx.plugin(consumer('/stay', 'S'))
|
|
@@ -119,10 +247,12 @@ describe('acryl-router lifecycle through Cordis', () => {
|
|
|
119
247
|
const fiber = ctx.plugin(consumer(`/cycle/${String(i)}`, `C${String(i)}`))
|
|
120
248
|
await settle()
|
|
121
249
|
expect(router.size).toBe(baseline + 1)
|
|
250
|
+
expect(await (await get(router, `/cycle/${String(i)}`)).text()).toBe(`C${String(i)}`)
|
|
122
251
|
await fiber.dispose()
|
|
123
252
|
await settle()
|
|
124
253
|
}
|
|
125
254
|
expect(router.size).toBe(baseline)
|
|
255
|
+
expect((await get(router, '/cycle/0')).status).toBe(404)
|
|
126
256
|
})
|
|
127
257
|
|
|
128
258
|
it('aborts an in-flight request and waits for it when the router is disposed, then answers 503', async () => {
|
|
@@ -131,81 +261,166 @@ describe('acryl-router lifecycle through Cordis', () => {
|
|
|
131
261
|
await settle()
|
|
132
262
|
const router = ctx.router
|
|
133
263
|
let aborted = false
|
|
134
|
-
router.get('/slow', async c => {
|
|
135
|
-
await new Promise<void>((_resolve, reject) => { c.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
|
|
264
|
+
router.route('/', new App().get('/slow', async c => {
|
|
265
|
+
await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
|
|
136
266
|
return c.text('late')
|
|
137
|
-
})
|
|
138
|
-
const pending = router
|
|
267
|
+
}))
|
|
268
|
+
const pending = get(router, '/slow')
|
|
139
269
|
await settle()
|
|
140
270
|
|
|
141
271
|
await fiber.dispose()
|
|
142
272
|
await settle()
|
|
143
273
|
expect(aborted).toBe(true)
|
|
144
274
|
expect((await pending).status).toBe(499)
|
|
145
|
-
expect((await router
|
|
275
|
+
expect((await get(router, '/slow')).status).toBe(503)
|
|
146
276
|
})
|
|
147
277
|
|
|
148
|
-
it('
|
|
278
|
+
it('forwards the caller\'s own abort to the handler signal', async () => {
|
|
149
279
|
const { router } = await mountRouter()
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
280
|
+
let seen = false
|
|
281
|
+
router.route('/', new App().get('/slow', async c => {
|
|
282
|
+
await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { seen = true; reject(new Error('aborted')) }, { once: true }) })
|
|
283
|
+
return c.text('x')
|
|
284
|
+
}))
|
|
285
|
+
const controller = new AbortController()
|
|
286
|
+
const pending = router.fetch(new Request('http://acryl.test/slow', { signal: controller.signal }))
|
|
287
|
+
await settle()
|
|
288
|
+
controller.abort()
|
|
289
|
+
expect((await pending).status).toBe(499)
|
|
290
|
+
expect(seen).toBe(true)
|
|
155
291
|
})
|
|
156
292
|
|
|
157
|
-
it('
|
|
293
|
+
it('ends a server-sent-event stream when the router is disposed: the stream\'s abort cleanup runs and the reader sees the end', async () => {
|
|
158
294
|
const ctx = root()
|
|
159
295
|
const fiber = ctx.plugin(provider, config)
|
|
160
296
|
await settle()
|
|
161
|
-
|
|
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)
|
|
297
|
+
let cleaned = false
|
|
298
|
+
ctx.router.route('/', new App().get('/events', c => streamSSE(c, async stream => {
|
|
299
|
+
stream.onAbort(() => { cleaned = true })
|
|
300
|
+
await stream.writeSSE({ data: 'first' })
|
|
301
|
+
while (!stream.aborted) await stream.sleep(10)
|
|
302
|
+
})))
|
|
303
|
+
const response = await get(ctx.router, '/events')
|
|
304
|
+
expect(response.headers.get('content-type')).toBe('text/event-stream')
|
|
177
305
|
const reader = response.body!.getReader()
|
|
178
|
-
expect(new TextDecoder().decode((await reader.read()).value)).toBe('first')
|
|
306
|
+
expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
|
|
179
307
|
|
|
180
308
|
await fiber.dispose()
|
|
181
|
-
expect(
|
|
309
|
+
expect(cleaned).toBe(true)
|
|
310
|
+
await expect(reader.read()).resolves.toMatchObject({ done: true })
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('ends the stream a plugin opened when that plugin unloads, and leaves another plugin\'s request running', async () => {
|
|
314
|
+
const { ctx, router } = await mountRouter()
|
|
315
|
+
let cleaned = false
|
|
316
|
+
let otherAborted = false
|
|
317
|
+
let release!: () => void
|
|
318
|
+
const streaming = ctx.plugin({
|
|
319
|
+
name: 'streaming',
|
|
320
|
+
inject: ['router'],
|
|
321
|
+
apply(inner: Context): void {
|
|
322
|
+
inner.router.route('/', new App().get('/events', c => streamSSE(c, async stream => {
|
|
323
|
+
stream.onAbort(() => { cleaned = true })
|
|
324
|
+
await stream.writeSSE({ data: 'first' })
|
|
325
|
+
while (!stream.aborted) await stream.sleep(10)
|
|
326
|
+
})))
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
ctx.plugin({
|
|
330
|
+
name: 'other',
|
|
331
|
+
inject: ['router'],
|
|
332
|
+
apply(inner: Context): void {
|
|
333
|
+
inner.router.route('/', new App().get('/slow', async c => {
|
|
334
|
+
c.req.raw.signal.addEventListener('abort', () => { otherAborted = true }, { once: true })
|
|
335
|
+
await new Promise<void>(resolve => { release = resolve })
|
|
336
|
+
return c.text('done')
|
|
337
|
+
}))
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
await settle()
|
|
341
|
+
const response = await get(router, '/events')
|
|
342
|
+
const reader = response.body!.getReader()
|
|
343
|
+
expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
|
|
344
|
+
const slow = get(router, '/slow')
|
|
345
|
+
await settle()
|
|
346
|
+
|
|
347
|
+
await streaming.dispose()
|
|
348
|
+
expect(cleaned).toBe(true)
|
|
349
|
+
await expect(reader.read()).resolves.toMatchObject({ done: true })
|
|
350
|
+
expect(otherAborted).toBe(false)
|
|
351
|
+
release()
|
|
352
|
+
expect(await (await slow).text()).toBe('done')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('aborts a request still inside an unloading plugin\'s middleware, but not one that middleware already passed on', async () => {
|
|
356
|
+
const { ctx, router } = await mountRouter()
|
|
357
|
+
let guardAborted = false
|
|
358
|
+
let handlerAborted = false
|
|
359
|
+
let release!: () => void
|
|
360
|
+
const guard = ctx.plugin({
|
|
361
|
+
name: 'guard',
|
|
362
|
+
inject: ['router'],
|
|
363
|
+
apply(inner: Context): void {
|
|
364
|
+
inner.router.route('/', new App()
|
|
365
|
+
.use('/held/*', async (c, next) => {
|
|
366
|
+
c.req.raw.signal.addEventListener('abort', () => { guardAborted = true }, { once: true })
|
|
367
|
+
await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true }) })
|
|
368
|
+
await next()
|
|
369
|
+
})
|
|
370
|
+
.use('/passed/*', async (_c, next) => { await next() }))
|
|
371
|
+
},
|
|
372
|
+
})
|
|
373
|
+
ctx.plugin({
|
|
374
|
+
name: 'handler',
|
|
375
|
+
inject: ['router'],
|
|
376
|
+
apply(inner: Context): void {
|
|
377
|
+
inner.router.route('/', new App()
|
|
378
|
+
.get('/held/x', c => c.text('held'))
|
|
379
|
+
.get('/passed/x', c => streamSSE(c, async stream => {
|
|
380
|
+
stream.onAbort(() => { handlerAborted = true })
|
|
381
|
+
await stream.writeSSE({ data: 'open' })
|
|
382
|
+
await new Promise<void>(resolve => { release = resolve })
|
|
383
|
+
})))
|
|
384
|
+
},
|
|
385
|
+
})
|
|
386
|
+
await settle()
|
|
387
|
+
const held = get(router, '/held/x')
|
|
388
|
+
const passed = await get(router, '/passed/x')
|
|
389
|
+
const reader = passed.body!.getReader()
|
|
390
|
+
await reader.read()
|
|
391
|
+
|
|
392
|
+
await guard.dispose()
|
|
393
|
+
expect(guardAborted).toBe(true)
|
|
394
|
+
expect((await held).status).toBe(499)
|
|
395
|
+
expect(handlerAborted).toBe(false)
|
|
396
|
+
release()
|
|
182
397
|
await expect(reader.read()).resolves.toMatchObject({ done: true })
|
|
183
398
|
})
|
|
184
399
|
|
|
400
|
+
it('keeps a request in flight through waitUntil until its task settles', async () => {
|
|
401
|
+
const ctx = root()
|
|
402
|
+
const fiber = ctx.plugin(provider, config)
|
|
403
|
+
await settle()
|
|
404
|
+
let finished = false
|
|
405
|
+
ctx.router.route('/', new App().get('/later', c => {
|
|
406
|
+
c.executionCtx.waitUntil(new Promise<void>(resolve => { setTimeout(() => { finished = true; resolve() }, 30) }))
|
|
407
|
+
return c.text('now')
|
|
408
|
+
}))
|
|
409
|
+
expect(await (await get(ctx.router, '/later')).text()).toBe('now')
|
|
410
|
+
expect(finished).toBe(false)
|
|
411
|
+
await fiber.dispose()
|
|
412
|
+
expect(finished).toBe(true)
|
|
413
|
+
})
|
|
414
|
+
|
|
185
415
|
it('gives up on a request that ignores the abort after the grace period instead of hanging disposal', async () => {
|
|
186
416
|
const ctx = root()
|
|
187
417
|
const fiber = ctx.plugin(provider, { trailingSlash: 'strict', disposeGraceMs: 30 })
|
|
188
418
|
await settle()
|
|
189
|
-
ctx.router.get('/stuck', c => { c.waitUntil(new Promise(() => undefined)); return c.text('x') })
|
|
190
|
-
await ctx.router
|
|
419
|
+
ctx.router.route('/', new App().get('/stuck', c => { c.executionCtx.waitUntil(new Promise(() => undefined)); return c.text('x') }))
|
|
420
|
+
await get(ctx.router, '/stuck')
|
|
191
421
|
const started = Date.now()
|
|
192
422
|
await fiber.dispose()
|
|
193
423
|
expect(Date.now() - started).toBeLessThan(1000)
|
|
194
424
|
expect(fiber.state).toBe(DISPOSED)
|
|
195
425
|
})
|
|
196
|
-
|
|
197
|
-
it('forwards the caller\'s own abort to the handler signal', async () => {
|
|
198
|
-
const { router } = await mountRouter()
|
|
199
|
-
let seen = false
|
|
200
|
-
router.get('/slow', async c => {
|
|
201
|
-
await new Promise<void>((_resolve, reject) => { c.signal.addEventListener('abort', () => { seen = true; reject(new Error('aborted')) }, { once: true }) })
|
|
202
|
-
return c.text('x')
|
|
203
|
-
})
|
|
204
|
-
const controller = new AbortController()
|
|
205
|
-
const pending = router.fetch(new Request('http://acryl.test/slow', { signal: controller.signal }))
|
|
206
|
-
await settle()
|
|
207
|
-
controller.abort()
|
|
208
|
-
expect((await pending).status).toBe(499)
|
|
209
|
-
expect(seen).toBe(true)
|
|
210
|
-
})
|
|
211
426
|
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Service's fields look like to the plugins that call it. A plugin reaches a service through its own shadow of the instance
|
|
3
|
+
* (`ctx.counter` in plugin A and in plugin B are two different objects over one service), but reads and writes of fields pass through to
|
|
4
|
+
* the one instance: a field assigned in a method is seen by every caller, as is state kept in an object that is only mutated. What the
|
|
5
|
+
* shadow does change is `this.ctx` inside a method, which `live-install.spec.ts` covers.
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Service } from '@deepseek-ai/cordis'
|
|
8
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
9
|
+
|
|
10
|
+
declare module '@deepseek-ai/cordis' {
|
|
11
|
+
interface Context {
|
|
12
|
+
counter: Counter
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const contexts: Context[] = []
|
|
17
|
+
const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
class Counter extends Service {
|
|
23
|
+
/** Reassigned by `assign`. */
|
|
24
|
+
assigned = 0
|
|
25
|
+
/** Created once, only mutated. */
|
|
26
|
+
private readonly state = { count: 0 }
|
|
27
|
+
|
|
28
|
+
constructor(ctx: Context) {
|
|
29
|
+
super(ctx, 'counter')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
assign(): void {
|
|
33
|
+
this.assigned += 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
mutate(): void {
|
|
37
|
+
this.state.count += 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get count(): number {
|
|
41
|
+
return this.state.count
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function twoCallers(): Promise<{ a: Context, b: Context }> {
|
|
46
|
+
const root = new Context()
|
|
47
|
+
contexts.push(root)
|
|
48
|
+
root.plugin({ name: 'counter', apply: (ctx: Context) => { new Counter(ctx) } })
|
|
49
|
+
const callers: Context[] = []
|
|
50
|
+
for (const name of ['a', 'b']) root.plugin({ name, inject: ['counter'], apply: (ctx: Context) => { callers.push(ctx) } })
|
|
51
|
+
await settle()
|
|
52
|
+
const [a, b] = callers
|
|
53
|
+
if (a === undefined || b === undefined) throw new Error('both callers should be active')
|
|
54
|
+
return { a, b }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('service state and the shadow context', () => {
|
|
58
|
+
it('shows a field assigned in a method through one caller to every caller', async () => {
|
|
59
|
+
const { a, b } = await twoCallers()
|
|
60
|
+
a.counter.assign()
|
|
61
|
+
b.counter.assign()
|
|
62
|
+
expect(a.counter.assigned).toBe(2)
|
|
63
|
+
expect(b.counter.assigned).toBe(2)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('shows a field assigned from outside through one caller to every caller', async () => {
|
|
67
|
+
const { a, b } = await twoCallers()
|
|
68
|
+
a.counter.assigned = 5
|
|
69
|
+
expect(b.counter.assigned).toBe(5)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('shares state kept in one object that is only mutated', async () => {
|
|
73
|
+
const { a, b } = await twoCallers()
|
|
74
|
+
a.counter.mutate()
|
|
75
|
+
b.counter.mutate()
|
|
76
|
+
expect(a.counter.count).toBe(2)
|
|
77
|
+
expect(b.counter.count).toBe(2)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"source": {
|
|
3
3
|
"repo": "acryldev/acryl",
|
|
4
4
|
"path": "plugins/acryl-router",
|
|
5
|
-
"commit": "
|
|
5
|
+
"commit": "64e7c9d2e787f74b2da82492644fbbb46a601074"
|
|
6
6
|
},
|
|
7
7
|
"vendored": [
|
|
8
8
|
"src",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"LICENSE"
|
|
11
11
|
],
|
|
12
12
|
"rewrittenSpecifiers": 0,
|
|
13
|
-
"digest": "sha256:
|
|
13
|
+
"digest": "sha256:9ad75d259a74b2a07fe1d8a269e7c83c30061acc90f8a1630e6049b68ba10aa3",
|
|
14
14
|
"testsNotRun": {
|
|
15
15
|
"tests/loader.spec.ts": "loads the built lib/ entry through the real Loader; the library ships source and has no build"
|
|
16
16
|
},
|
|
@@ -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
|
/**
|