acrylblends 0.2.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 +22 -16
- package/dist/index.js +65 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/plugins/acryl-inspect/LICENSE +21 -0
- package/plugins/acryl-inspect/plugin.json +12 -0
- package/plugins/acryl-inspect/src/index.ts +3 -0
- package/plugins/acryl-inspect/src/report.ts +126 -0
- package/plugins/acryl-inspect/src/service.ts +68 -0
- package/plugins/acryl-inspect/tests/report.spec.ts +63 -0
- package/plugins/acryl-inspect/tests/service.spec.ts +121 -0
- package/plugins/acryl-inspect/upstream.json +18 -0
- package/plugins/acryl-node-server/LICENSE +47 -0
- package/plugins/acryl-node-server/plugin.json +13 -0
- 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 +107 -0
- 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 +141 -0
- package/plugins/acryl-node-server/upstream.json +18 -0
- package/plugins/acryl-router/LICENSE +47 -0
- package/plugins/acryl-router/plugin.json +13 -0
- 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 -0
- package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
- package/plugins/acryl-router/src/index.ts +6 -0
- package/plugins/acryl-router/src/middleware/index.ts +25 -0
- package/plugins/acryl-router/src/middleware/loopback-only.ts +88 -0
- package/plugins/acryl-router/src/service.ts +356 -0
- package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
- package/plugins/acryl-router/tests/live-install.spec.ts +64 -0
- package/plugins/acryl-router/tests/loader.spec.ts +72 -0
- package/plugins/acryl-router/tests/service.spec.ts +409 -0
- package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
- package/plugins/acryl-router/upstream.json +20 -0
- package/plugins/acryl-router-http/LICENSE +21 -0
- package/plugins/acryl-router-http/plugin.json +12 -0
- package/plugins/acryl-router-http/src/bridge.ts +54 -0
- package/plugins/acryl-router-http/src/index.ts +63 -0
- package/plugins/acryl-router-http/src/serve.ts +24 -0
- package/plugins/acryl-router-http/tests/adapter.spec.ts +174 -0
- package/plugins/acryl-router-http/tests/stream.spec.ts +133 -0
- package/plugins/acryl-router-http/tests/surfaces.spec.ts +101 -0
- package/plugins/acryl-router-http/upstream.json +22 -0
- package/plugins/acryl-state/LICENSE +21 -0
- package/plugins/acryl-state/NOTICE +30 -0
- package/plugins/acryl-state/plugin.json +12 -0
- package/plugins/acryl-state/src/cell.ts +67 -0
- package/plugins/acryl-state/src/index.ts +3 -0
- package/plugins/acryl-state/src/react.ts +11 -0
- package/plugins/acryl-state/src/service.ts +119 -0
- package/plugins/acryl-state/tests/cell.spec.ts +71 -0
- package/plugins/acryl-state/tests/react.spec.ts +39 -0
- package/plugins/acryl-state/tests/service.spec.ts +132 -0
- package/plugins/acryl-state/upstream.json +22 -0
- package/plugins/builder/script.ts +6 -10
- package/plugins/extensions/index.ts +20 -3
- package/plugins/extensions/resolve.ts +28 -3
- package/plugins/openai/prompt.ts +12 -6
- package/plugins/tasks/index.ts +14 -0
- package/plugins/tsconfig.json +14 -5
- package/templates/agent/blend.yaml +5 -7
- package/templates/agent/src/app.ts +8 -8
- package/templates/agent/src/routes.ts +39 -38
- package/templates/agent/template.json +3 -3
- package/templates/agent/test/builder.test.ts +22 -14
- package/templates/agent/test/model.test.ts +3 -3
- package/templates/agent/tsconfig.json +8 -5
- package/templates/{app → blank}/blend.yaml +3 -3
- package/templates/{app → blank}/src/app.ts +5 -5
- package/templates/blank/src/routes.ts +16 -0
- package/templates/blank/template.json +1 -0
- package/templates/{app → blank}/test/app.test.ts +1 -1
- package/templates/{app → blank}/tsconfig.json +2 -4
- package/templates/todo/README.md +27 -0
- package/templates/todo/blend.yaml +25 -0
- package/templates/todo/gitignore +2 -0
- package/templates/todo/package.json +23 -0
- package/templates/todo/src/app.ts +71 -0
- package/templates/todo/src/main.ts +19 -0
- package/templates/todo/src/page.ts +28 -0
- package/templates/todo/src/todo.ts +123 -0
- package/templates/todo/template.json +1 -0
- package/templates/todo/test/todo.test.ts +145 -0
- package/templates/todo/tsconfig.json +17 -0
- package/templates/todo/upstream.json +12 -0
- package/plugins/events/index.ts +0 -42
- package/plugins/events/plugin.json +0 -9
- package/plugins/http/index.ts +0 -64
- package/plugins/http/plugin.json +0 -10
- package/plugins/server/index.ts +0 -138
- package/plugins/server/plugin.json +0 -10
- package/templates/app/src/routes.ts +0 -14
- package/templates/app/template.json +0 -1
- /package/templates/{app → blank}/README.md +0 -0
- /package/templates/{app → blank}/gitignore +0 -0
- /package/templates/{app → blank}/package.json +0 -0
- /package/templates/{app → blank}/src/main.ts +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reference app of the ACRYL Framework: a todo list in one plugin. It needs `router` and `state`, keeps the list in a cell, serves
|
|
3
|
+
* JSON CRUD plus a page, and writes the list to a file so it survives a restart. Nothing else.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { dirname } from 'node:path'
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
9
|
+
import { App, HTTPException } from './plugins/acryl-router/src/index.ts'
|
|
10
|
+
import { bodyLimit, loopbackOnly } from './plugins/acryl-router/src/middleware/index.ts'
|
|
11
|
+
import { schemaValidator, streamSSE, type StandardSchema } from './plugins/acryl-router/src/helpers/index.ts'
|
|
12
|
+
import type {} from './plugins/acryl-state/src/index.ts'
|
|
13
|
+
import { PAGE } from './page.ts'
|
|
14
|
+
|
|
15
|
+
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
16
|
+
export const name = 'acryl-todo'
|
|
17
|
+
export const inject = ['router', 'state']
|
|
18
|
+
|
|
19
|
+
export interface Todo { id: number, title: string, done: boolean }
|
|
20
|
+
export interface Config { file?: string }
|
|
21
|
+
|
|
22
|
+
/** Standard Schema for the body of a new todo, written by hand so the example has no schema library. */
|
|
23
|
+
const NewTodo: StandardSchema<{ title: string }> = {
|
|
24
|
+
'~standard': {
|
|
25
|
+
validate: value => {
|
|
26
|
+
const title = (value as { title?: unknown } | null)?.title
|
|
27
|
+
return typeof title === 'string' && title.trim() !== '' && title.length <= 200
|
|
28
|
+
? { value: { title: title.trim() } }
|
|
29
|
+
: { issues: [{ message: 'title must be 1 to 200 characters', path: ['title'] }] }
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Standard Schema for a change to a todo. */
|
|
35
|
+
const TodoChange: StandardSchema<{ done: boolean }> = {
|
|
36
|
+
'~standard': {
|
|
37
|
+
validate: value => {
|
|
38
|
+
const done = (value as { done?: unknown } | null)?.done
|
|
39
|
+
return typeof done === 'boolean' ? { value: { done } } : { issues: [{ message: 'done must be true or false', path: ['done'] }] }
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A todo body is small; anything bigger is refused before it is read. */
|
|
45
|
+
const MAX_BODY_BYTES = 16 * 1024
|
|
46
|
+
|
|
47
|
+
/** A missing file is a new list. Any other failure (unreadable, corrupt, half-written) refuses to start: starting empty would overwrite the user's real list on the first save. */
|
|
48
|
+
function load(file: string | undefined): Todo[] {
|
|
49
|
+
if (file === undefined) return []
|
|
50
|
+
let text: string
|
|
51
|
+
try {
|
|
52
|
+
text = readFileSync(file, 'utf8')
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []
|
|
55
|
+
throw new Error(`acryl-todo: cannot read ${file}: ${(error as Error).message}`, { cause: error })
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const parsed: unknown = JSON.parse(text)
|
|
59
|
+
if (!Array.isArray(parsed)) throw new Error('not a list')
|
|
60
|
+
return parsed as Todo[]
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new Error(`acryl-todo: ${file} is not a valid todo list (${(error as Error).message}); fix or remove it, it was left untouched`, { cause: error })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function save(file: string, todos: readonly Todo[]): void {
|
|
67
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
68
|
+
const temporary = `${file}.tmp`
|
|
69
|
+
writeFileSync(temporary, JSON.stringify(todos))
|
|
70
|
+
renameSync(temporary, file)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function apply(ctx: Context, config: Config = {}): void {
|
|
74
|
+
const todos = ctx.state.cell<readonly Todo[]>('todo.list', load(config.file))
|
|
75
|
+
if (config.file !== undefined) {
|
|
76
|
+
const file = config.file
|
|
77
|
+
ctx.effect(() => todos.subscribe(list => { save(file, list) }), 'acryl-todo: save to file')
|
|
78
|
+
}
|
|
79
|
+
const nextId = (): number => todos.get().reduce((max, todo) => Math.max(max, todo.id), 0) + 1
|
|
80
|
+
|
|
81
|
+
const find = (id: number): Todo => {
|
|
82
|
+
const todo = todos.get().find(item => item.id === id)
|
|
83
|
+
if (todo === undefined) throw new HTTPException(404, { message: 'no such todo' })
|
|
84
|
+
return todo
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const api = new App()
|
|
88
|
+
.use(loopbackOnly({ inProcess: 'allow' }), bodyLimit({ maxSize: MAX_BODY_BYTES }))
|
|
89
|
+
.get('/', c => c.html(PAGE))
|
|
90
|
+
.get('/api', c => c.json(todos.get()))
|
|
91
|
+
.post('/api', schemaValidator('json', NewTodo), c => {
|
|
92
|
+
const todo: Todo = { id: nextId(), title: c.req.valid('json').title, done: false }
|
|
93
|
+
todos.update(list => [...list, todo])
|
|
94
|
+
return c.json(todo, 201)
|
|
95
|
+
})
|
|
96
|
+
.patch('/api/:id', schemaValidator('json', TodoChange), c => {
|
|
97
|
+
const { id } = find(Number(c.req.param('id')))
|
|
98
|
+
const { done } = c.req.valid('json')
|
|
99
|
+
todos.update(list => list.map(todo => todo.id === id ? { ...todo, done } : todo))
|
|
100
|
+
return c.json(find(id))
|
|
101
|
+
})
|
|
102
|
+
.delete('/api/:id', c => {
|
|
103
|
+
const { id } = find(Number(c.req.param('id')))
|
|
104
|
+
todos.update(list => list.filter(todo => todo.id !== id))
|
|
105
|
+
return c.body(null, 204)
|
|
106
|
+
})
|
|
107
|
+
.get('/events', c => streamSSE(c, async stream => {
|
|
108
|
+
const ended = new Promise<void>(resolve => { stream.onAbort(resolve) })
|
|
109
|
+
// Writes are asynchronous; chaining them keeps events in the order the list changed.
|
|
110
|
+
let queue: Promise<unknown> = Promise.resolve()
|
|
111
|
+
const send = (list: readonly Todo[]): void => { queue = queue.then(() => stream.writeSSE({ data: JSON.stringify(list) })).catch(() => {}) }
|
|
112
|
+
send(todos.get())
|
|
113
|
+
const unsubscribe = todos.subscribe(send)
|
|
114
|
+
try {
|
|
115
|
+
await ended
|
|
116
|
+
} finally {
|
|
117
|
+
unsubscribe()
|
|
118
|
+
}
|
|
119
|
+
}))
|
|
120
|
+
ctx.router.route('/todo', api)
|
|
121
|
+
|
|
122
|
+
ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/todo') })
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "plugins": ["instance", "acryl-router", "acryl-state", "acryl-node-server", "manifest", "save"] }
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { test } from 'node:test'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { type App, createApp, readManifest } from '../src/app.ts'
|
|
8
|
+
import { pinnedInstance, withPort } from '../src/plugins/instance/index.ts'
|
|
9
|
+
|
|
10
|
+
const projectRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
11
|
+
|
|
12
|
+
/** Boot the app on any free port with its data in `home`, run the test, always stop it. */
|
|
13
|
+
async function withApp(home: string, run: (app: App) => Promise<void>) {
|
|
14
|
+
const app = await createApp(withPort(pinnedInstance(home, { root: projectRoot }), 0))
|
|
15
|
+
try {
|
|
16
|
+
await run(app)
|
|
17
|
+
} finally {
|
|
18
|
+
await app.stop()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function withHome(run: (home: string) => Promise<void>) {
|
|
23
|
+
const home = await mkdtemp(join(tmpdir(), 'todo-test-'))
|
|
24
|
+
try {
|
|
25
|
+
await run(home)
|
|
26
|
+
} finally {
|
|
27
|
+
await rm(home, { recursive: true, force: true })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const send = (app: App, method: string, path: string, body?: unknown) =>
|
|
32
|
+
fetch(`${app.url}${path}`, {
|
|
33
|
+
method,
|
|
34
|
+
headers: { 'content-type': 'application/json' },
|
|
35
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('creates, completes, lists and deletes', () =>
|
|
39
|
+
withHome((home) =>
|
|
40
|
+
withApp(home, async (app) => {
|
|
41
|
+
const created = await send(app, 'POST', '/todo/api', { title: ' buy milk ' })
|
|
42
|
+
assert.equal(created.status, 201)
|
|
43
|
+
assert.deepEqual(await created.json(), { id: 1, title: 'buy milk', done: false })
|
|
44
|
+
|
|
45
|
+
const done = await send(app, 'PATCH', '/todo/api/1', { done: true })
|
|
46
|
+
assert.deepEqual(await done.json(), { id: 1, title: 'buy milk', done: true })
|
|
47
|
+
assert.deepEqual(await (await send(app, 'GET', '/todo/api')).json(), [
|
|
48
|
+
{ id: 1, title: 'buy milk', done: true },
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
assert.equal((await send(app, 'DELETE', '/todo/api/1')).status, 204)
|
|
52
|
+
assert.deepEqual(await (await send(app, 'GET', '/todo/api')).json(), [])
|
|
53
|
+
}),
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
test('a change from a foreign origin is refused; a plain request from this machine works', () =>
|
|
57
|
+
withHome((home) =>
|
|
58
|
+
withApp(home, async (app) => {
|
|
59
|
+
const post = (headers: Record<string, string>) =>
|
|
60
|
+
fetch(`${app.url}/todo/api`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
63
|
+
body: JSON.stringify({ title: 'x' }),
|
|
64
|
+
})
|
|
65
|
+
assert.equal((await post({ origin: 'http://evil.example' })).status, 403)
|
|
66
|
+
assert.equal((await post({ origin: 'null' })).status, 403)
|
|
67
|
+
assert.equal((await post({})).status, 201)
|
|
68
|
+
assert.equal((await post({ origin: new URL(app.url).origin })).status, 201)
|
|
69
|
+
}),
|
|
70
|
+
))
|
|
71
|
+
|
|
72
|
+
test('rejects bad input and unknown ids', () =>
|
|
73
|
+
withHome((home) =>
|
|
74
|
+
withApp(home, async (app) => {
|
|
75
|
+
assert.equal((await send(app, 'POST', '/todo/api', { title: '' })).status, 400)
|
|
76
|
+
assert.equal((await send(app, 'POST', '/todo/api', { title: 42 })).status, 400)
|
|
77
|
+
assert.equal((await send(app, 'DELETE', '/todo/api/9')).status, 404)
|
|
78
|
+
assert.equal((await send(app, 'PATCH', '/todo/api/9', { done: true })).status, 404)
|
|
79
|
+
assert.equal((await send(app, 'GET', '/nope')).status, 404)
|
|
80
|
+
}),
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
test('the list survives a restart: a second app on the same home sees it', () =>
|
|
84
|
+
withHome(async (home) => {
|
|
85
|
+
await withApp(home, async (app) => {
|
|
86
|
+
await send(app, 'POST', '/todo/api', { title: 'first' })
|
|
87
|
+
await send(app, 'POST', '/todo/api', { title: 'second' })
|
|
88
|
+
await send(app, 'PATCH', '/todo/api/2', { done: true })
|
|
89
|
+
})
|
|
90
|
+
await withApp(home, async (app) => {
|
|
91
|
+
const list = (await (await send(app, 'GET', '/todo/api')).json()) as Array<{
|
|
92
|
+
id: number
|
|
93
|
+
done: boolean
|
|
94
|
+
}>
|
|
95
|
+
assert.deepEqual(
|
|
96
|
+
list.map((todo) => [todo.id, todo.done]),
|
|
97
|
+
[
|
|
98
|
+
[1, false],
|
|
99
|
+
[2, true],
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
const created = (await (await send(app, 'POST', '/todo/api', { title: 'third' })).json()) as {
|
|
103
|
+
id: number
|
|
104
|
+
}
|
|
105
|
+
assert.equal(created.id, 3)
|
|
106
|
+
})
|
|
107
|
+
}))
|
|
108
|
+
|
|
109
|
+
test('serves the page, and streams the list: now, and again on every change', () =>
|
|
110
|
+
withHome((home) =>
|
|
111
|
+
withApp(home, async (app) => {
|
|
112
|
+
const page = await send(app, 'GET', '/todo')
|
|
113
|
+
assert.equal(page.status, 200)
|
|
114
|
+
assert.match(await page.text(), /<title>Todo<\/title>/)
|
|
115
|
+
|
|
116
|
+
const controller = new AbortController()
|
|
117
|
+
const stream = await fetch(`${app.url}/todo/events`, { signal: controller.signal })
|
|
118
|
+
const reader = stream.body!.getReader()
|
|
119
|
+
const decoder = new TextDecoder()
|
|
120
|
+
let text = ''
|
|
121
|
+
const until = async (what: RegExp) => {
|
|
122
|
+
for (let waited = 0; !what.test(text) && waited < 3000; waited += 25) {
|
|
123
|
+
const next = await Promise.race([
|
|
124
|
+
reader.read(),
|
|
125
|
+
new Promise<{ done: true; value: undefined }>((done) =>
|
|
126
|
+
setTimeout(() => done({ done: true, value: undefined }), 25),
|
|
127
|
+
),
|
|
128
|
+
])
|
|
129
|
+
if (next.value) text += decoder.decode(next.value, { stream: true })
|
|
130
|
+
}
|
|
131
|
+
assert.match(text, what)
|
|
132
|
+
}
|
|
133
|
+
await until(/data: \[\]/)
|
|
134
|
+
await send(app, 'POST', '/todo/api', { title: 'streamed' })
|
|
135
|
+
await until(/"title":"streamed"/)
|
|
136
|
+
controller.abort()
|
|
137
|
+
}),
|
|
138
|
+
))
|
|
139
|
+
|
|
140
|
+
test('the manifest decides what is mounted', () => {
|
|
141
|
+
assert.deepEqual(
|
|
142
|
+
readManifest().plugins.map((row) => row.id),
|
|
143
|
+
['router', 'state', 'todo', 'server'],
|
|
144
|
+
)
|
|
145
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noImplicitOverride": true,
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"erasableSyntaxOnly": true,
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"types": ["node"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src", "test"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": {
|
|
3
|
+
"repo": "acryldev/acryl",
|
|
4
|
+
"files": {
|
|
5
|
+
"examples/acryl-todo/src/index.ts": "src/todo.ts",
|
|
6
|
+
"examples/acryl-todo/src/page.ts": "src/page.ts"
|
|
7
|
+
},
|
|
8
|
+
"commit": "df79b188398b6d4f8cceda35a72b88300a10a9ce"
|
|
9
|
+
},
|
|
10
|
+
"rewrittenSpecifiers": 4,
|
|
11
|
+
"digest": "sha256:c4efede39387681003c6d2f0f2858e157a84bcaaf9ae94ebe23d10f72a538ffd"
|
|
12
|
+
}
|
package/plugins/events/index.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Events as a stream (`GET /events`, server-sent events). A client connects once and receives every change as it happens: the
|
|
3
|
-
* event name is the Cordis event (`tasks/changed`, `extensions/changed`, any you list), the data is `{ "args": [...] }` with the
|
|
4
|
-
* event's arguments as JSON. Desktop, web and terminal surfaces can all render the same stream (the Pi Durable "one place to
|
|
5
|
-
* observe" idea); nothing here knows about tasks or extensions. Each connection's listeners are removed when the client leaves.
|
|
6
|
-
*/
|
|
7
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
8
|
-
import type {} from '../http/index.ts'
|
|
9
|
-
|
|
10
|
-
export interface EventsConfig {
|
|
11
|
-
/** Cordis events to stream. */
|
|
12
|
-
topics?: readonly string[]
|
|
13
|
-
/** Where the stream is served. Default `/events`. */
|
|
14
|
-
path?: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const events = {
|
|
18
|
-
name: 'events',
|
|
19
|
-
inject: ['http'],
|
|
20
|
-
apply(ctx: Context, config?: EventsConfig) {
|
|
21
|
-
const topics = config?.topics ?? []
|
|
22
|
-
const path = config?.path ?? '/events'
|
|
23
|
-
const listen = ctx.on as unknown as (
|
|
24
|
-
topic: string,
|
|
25
|
-
listener: (...args: unknown[]) => void,
|
|
26
|
-
) => () => void
|
|
27
|
-
ctx.effect(() =>
|
|
28
|
-
ctx.http.route('GET', path, () => ({
|
|
29
|
-
body: null,
|
|
30
|
-
stream: (send) => {
|
|
31
|
-
const leaves = topics.map((topic) =>
|
|
32
|
-
listen.call(ctx, topic, (...args: unknown[]) => send(topic, { args })),
|
|
33
|
-
)
|
|
34
|
-
send('connected', { topics })
|
|
35
|
-
return () => {
|
|
36
|
-
for (const leave of leaves) leave()
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
})),
|
|
40
|
-
)
|
|
41
|
-
},
|
|
42
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "events",
|
|
3
|
-
"description": "A live stream of Cordis events over server-sent events, so a surface renders state instead of polling.",
|
|
4
|
-
"requires": ["http"],
|
|
5
|
-
"inject": ["http"],
|
|
6
|
-
"provides": [],
|
|
7
|
-
"dependencies": { "@deepseek-ai/cordis": "4.0.4" },
|
|
8
|
-
"mount": "add a row to blend.yaml: events, with config { topics: ['tasks/changed', 'extensions/changed'] }"
|
|
9
|
-
}
|
package/plugins/http/index.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
-
|
|
3
|
-
/** A request as routes see it: the method, the path and the parsed JSON body, if any. */
|
|
4
|
-
export interface RouteRequest {
|
|
5
|
-
readonly method: string
|
|
6
|
-
readonly path: string
|
|
7
|
-
readonly body: unknown
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** What a route answers. The status defaults to 200; the body is sent as JSON. */
|
|
11
|
-
/** Sends one server-sent event: `topic` is the event name, `data` is sent as JSON. */
|
|
12
|
-
export type Send = (topic: string, data: unknown) => void
|
|
13
|
-
|
|
14
|
-
/** A live stream: called when a client connects, it returns what to run when the client leaves. */
|
|
15
|
-
export type Stream = (send: Send) => () => void
|
|
16
|
-
|
|
17
|
-
export interface Reply {
|
|
18
|
-
readonly status?: number
|
|
19
|
-
readonly body: unknown
|
|
20
|
-
/** Answer with a stream of server-sent events instead of a JSON body. */
|
|
21
|
-
readonly stream?: Stream
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type Handler = (request: RouteRequest) => Reply | Promise<Reply>
|
|
25
|
-
|
|
26
|
-
/** The router service. Plugins add routes; the server asks it to answer requests. */
|
|
27
|
-
export interface Http {
|
|
28
|
-
/** Add a route. Returns the function that removes it, so it can be the disposer of a `ctx.effect`. */
|
|
29
|
-
route(method: string, path: string, handler: Handler): () => void
|
|
30
|
-
/** Answer a request from the registered routes, 404 when none matches. */
|
|
31
|
-
handle(request: RouteRequest): Promise<{ status: number; body: unknown; stream?: Stream }>
|
|
32
|
-
/** The registered routes, as `METHOD /path`. */
|
|
33
|
-
list(): string[]
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
declare module '@deepseek-ai/cordis' {
|
|
37
|
-
interface Context {
|
|
38
|
-
http: Http
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Provides the `http` router. Nothing listens here: see the `server` plugin. */
|
|
43
|
-
export function http(ctx: Context) {
|
|
44
|
-
const routes = new Map<string, Handler>()
|
|
45
|
-
ctx.provide('http', {
|
|
46
|
-
route(method, path, handler) {
|
|
47
|
-
const key = `${method.toUpperCase()} ${path}`
|
|
48
|
-
if (routes.has(key)) throw new Error(`route ${key} is already registered`)
|
|
49
|
-
routes.set(key, handler)
|
|
50
|
-
return () => void routes.delete(key)
|
|
51
|
-
},
|
|
52
|
-
async handle(request) {
|
|
53
|
-
const handler = routes.get(`${request.method.toUpperCase()} ${request.path}`)
|
|
54
|
-
if (!handler) return { status: 404, body: { error: 'not found' } }
|
|
55
|
-
const reply = await handler(request)
|
|
56
|
-
return {
|
|
57
|
-
status: reply.status ?? 200,
|
|
58
|
-
body: reply.body,
|
|
59
|
-
...(reply.stream ? { stream: reply.stream } : {}),
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
list: () => [...routes.keys()].sort(),
|
|
63
|
-
} satisfies Http)
|
|
64
|
-
}
|
package/plugins/http/plugin.json
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "http",
|
|
3
|
-
"description": "The router service: plugins add routes inside ctx.effect, so a route disappears with the plugin that added it.",
|
|
4
|
-
"requires": [],
|
|
5
|
-
"inject": [],
|
|
6
|
-
"provides": ["http"],
|
|
7
|
-
"dependencies": {
|
|
8
|
-
"@deepseek-ai/cordis": "4.0.4"
|
|
9
|
-
}
|
|
10
|
-
}
|
package/plugins/server/index.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createServer,
|
|
3
|
-
type IncomingMessage,
|
|
4
|
-
type Server as NodeServer,
|
|
5
|
-
type ServerResponse,
|
|
6
|
-
} from 'node:http'
|
|
7
|
-
import type { AddressInfo } from 'node:net'
|
|
8
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
9
|
-
import type { Send, Stream } from '../http/index.ts'
|
|
10
|
-
|
|
11
|
-
/** Where the server listens. Passed as this plugin's Cordis config. */
|
|
12
|
-
export interface ServerConfig {
|
|
13
|
-
readonly port: number
|
|
14
|
-
readonly hostname?: string
|
|
15
|
-
/** Try the next port when this one is taken (up to 20 more). */
|
|
16
|
-
readonly scan?: boolean
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** The running server, provided once it listens. */
|
|
20
|
-
export interface Server {
|
|
21
|
-
readonly url: string
|
|
22
|
-
readonly port: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
declare module '@deepseek-ai/cordis' {
|
|
26
|
-
interface Context {
|
|
27
|
-
server: Server
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const SCAN_LIMIT = 20
|
|
32
|
-
|
|
33
|
-
async function readJson(
|
|
34
|
-
request: IncomingMessage,
|
|
35
|
-
): Promise<{ ok: true; body: unknown } | { ok: false }> {
|
|
36
|
-
const chunks: Buffer[] = []
|
|
37
|
-
for await (const chunk of request) chunks.push(chunk as Buffer)
|
|
38
|
-
if (chunks.length === 0) return { ok: true, body: undefined }
|
|
39
|
-
try {
|
|
40
|
-
return { ok: true, body: JSON.parse(Buffer.concat(chunks).toString('utf8')) }
|
|
41
|
-
} catch {
|
|
42
|
-
return { ok: false }
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function listen(listener: NodeServer, port: number, hostname: string): Promise<void> {
|
|
47
|
-
return new Promise((resolve, reject) => {
|
|
48
|
-
const onError = (error: Error) => {
|
|
49
|
-
listener.off('listening', onListening)
|
|
50
|
-
reject(error)
|
|
51
|
-
}
|
|
52
|
-
const onListening = () => {
|
|
53
|
-
listener.off('error', onError)
|
|
54
|
-
resolve()
|
|
55
|
-
}
|
|
56
|
-
listener.once('error', onError)
|
|
57
|
-
listener.once('listening', onListening)
|
|
58
|
-
listener.listen(port, hostname)
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const KEEPALIVE_MS = 15_000
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Answer with server-sent events. Each event has a rising `id`, a `topic` as its event name and JSON `data`; a comment line every 15 s
|
|
66
|
-
* keeps proxies from closing an idle stream. When the client leaves (or the server stops) the stream's own cleanup runs.
|
|
67
|
-
*/
|
|
68
|
-
function openStream(request: IncomingMessage, response: ServerResponse, stream: Stream): void {
|
|
69
|
-
response.writeHead(200, {
|
|
70
|
-
'content-type': 'text/event-stream',
|
|
71
|
-
'cache-control': 'no-cache',
|
|
72
|
-
connection: 'keep-alive',
|
|
73
|
-
})
|
|
74
|
-
response.write(': connected\n\n')
|
|
75
|
-
let id = 0
|
|
76
|
-
let open = true
|
|
77
|
-
const send: Send = (topic, data) => {
|
|
78
|
-
if (!open) return
|
|
79
|
-
id += 1
|
|
80
|
-
response.write(`id: ${id}\nevent: ${topic}\ndata: ${JSON.stringify(data)}\n\n`)
|
|
81
|
-
}
|
|
82
|
-
const leave = stream(send)
|
|
83
|
-
const keepalive = setInterval(() => response.write(': keepalive\n\n'), KEEPALIVE_MS)
|
|
84
|
-
const close = () => {
|
|
85
|
-
if (!open) return
|
|
86
|
-
open = false
|
|
87
|
-
clearInterval(keepalive)
|
|
88
|
-
leave()
|
|
89
|
-
}
|
|
90
|
-
request.on('close', close)
|
|
91
|
-
response.on('close', close)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Serves the `http` router with Node's own `node:http`. The returned disposer closes the server. */
|
|
95
|
-
export const server = {
|
|
96
|
-
name: 'server',
|
|
97
|
-
inject: ['http'],
|
|
98
|
-
async apply(ctx: Context, config: ServerConfig) {
|
|
99
|
-
const hostname = config.hostname ?? '127.0.0.1'
|
|
100
|
-
const listener = createServer(async (request, response) => {
|
|
101
|
-
const send = (status: number, body: unknown) => {
|
|
102
|
-
response.writeHead(status, { 'content-type': 'application/json' })
|
|
103
|
-
response.end(JSON.stringify(body))
|
|
104
|
-
}
|
|
105
|
-
try {
|
|
106
|
-
const parsed = await readJson(request)
|
|
107
|
-
if (!parsed.ok) return send(400, { error: 'invalid JSON' })
|
|
108
|
-
const path = new URL(request.url ?? '/', 'http://localhost').pathname
|
|
109
|
-
const reply = await ctx.http.handle({
|
|
110
|
-
method: request.method ?? 'GET',
|
|
111
|
-
path,
|
|
112
|
-
body: parsed.body,
|
|
113
|
-
})
|
|
114
|
-
if (reply.stream) return openStream(request, response, reply.stream)
|
|
115
|
-
send(reply.status, reply.body)
|
|
116
|
-
} catch (error) {
|
|
117
|
-
send(500, { error: error instanceof Error ? error.message : String(error) })
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
const attempts = config.scan && config.port !== 0 ? SCAN_LIMIT + 1 : 1
|
|
121
|
-
for (let attempt = 0; ; attempt++) {
|
|
122
|
-
try {
|
|
123
|
-
await listen(listener, config.port + attempt, hostname)
|
|
124
|
-
break
|
|
125
|
-
} catch (error) {
|
|
126
|
-
const inUse = (error as NodeJS.ErrnoException).code === 'EADDRINUSE'
|
|
127
|
-
if (!inUse || attempt + 1 >= attempts) throw error
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const { port } = listener.address() as AddressInfo
|
|
131
|
-
ctx.provide('server', { url: `http://${hostname}:${port}`, port } satisfies Server)
|
|
132
|
-
return () =>
|
|
133
|
-
new Promise<void>((done) => {
|
|
134
|
-
listener.close(() => done())
|
|
135
|
-
listener.closeAllConnections()
|
|
136
|
-
})
|
|
137
|
-
},
|
|
138
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "server",
|
|
3
|
-
"description": "Serves the http router over node:http; moves to the next free port when asked; closes on stop.",
|
|
4
|
-
"requires": ["http"],
|
|
5
|
-
"inject": ["http"],
|
|
6
|
-
"provides": ["server"],
|
|
7
|
-
"dependencies": {
|
|
8
|
-
"@deepseek-ai/cordis": "4.0.4"
|
|
9
|
-
}
|
|
10
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
-
import type {} from './plugins/http/index.ts'
|
|
3
|
-
|
|
4
|
-
export const APP_NAME = '__APP_NAME__'
|
|
5
|
-
|
|
6
|
-
/** The app's own routes. Each one lives in a `ctx.effect`, so it is removed when this plugin is disposed. */
|
|
7
|
-
export const routes = {
|
|
8
|
-
name: 'routes',
|
|
9
|
-
inject: ['http'],
|
|
10
|
-
apply(ctx: Context) {
|
|
11
|
-
ctx.effect(() => ctx.http.route('GET', '/', () => ({ body: { app: APP_NAME, ok: true } })))
|
|
12
|
-
ctx.effect(() => ctx.http.route('GET', '/health', () => ({ body: { ok: true } })))
|
|
13
|
-
},
|
|
14
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "plugins": ["instance", "http", "server", "manifest", "save"] }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|