rivetkit 2.0.2 → 2.0.4-rc.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/README.md +3 -5
- package/dist/browser/client.d.ts +2485 -0
- package/dist/browser/client.js +5182 -0
- package/dist/browser/client.js.map +1 -0
- package/dist/browser/inspector/client.d.ts +130 -0
- package/dist/browser/inspector/client.js +2854 -0
- package/dist/browser/inspector/client.js.map +1 -0
- package/dist/browser/v3-DnYObHH3.d.ts +279 -0
- package/dist/inspector.tar.gz +0 -0
- package/dist/schemas/actor-inspector/v1.ts +784 -0
- package/dist/schemas/actor-inspector/v2.ts +796 -0
- package/dist/schemas/actor-inspector/v3.ts +899 -0
- package/dist/schemas/actor-persist/v1.ts +225 -0
- package/dist/schemas/actor-persist/v2.ts +268 -0
- package/dist/schemas/actor-persist/v3.ts +280 -0
- package/dist/schemas/actor-persist/v4.ts +406 -0
- package/dist/schemas/client-protocol/v1.ts +441 -0
- package/dist/schemas/client-protocol/v2.ts +438 -0
- package/dist/schemas/client-protocol/v3.ts +554 -0
- package/dist/schemas/file-system-driver/v1.ts +108 -0
- package/dist/schemas/file-system-driver/v2.ts +142 -0
- package/dist/schemas/file-system-driver/v3.ts +167 -0
- package/dist/schemas/persist/v1.ts +781 -0
- package/dist/schemas/transport/v1.ts +697 -0
- package/dist/tsup/actor/errors.cjs +106 -0
- package/dist/tsup/actor/errors.cjs.map +1 -0
- package/dist/tsup/actor/errors.d.cts +188 -0
- package/dist/tsup/actor/errors.d.ts +188 -0
- package/dist/tsup/actor/errors.js +106 -0
- package/dist/tsup/actor/errors.js.map +1 -0
- package/dist/tsup/actor-router-consts-D29T1Z-K.d.cts +24 -0
- package/dist/tsup/actor-router-consts-D29T1Z-K.d.ts +24 -0
- package/dist/tsup/chunk-325TLXJT.js +1060 -0
- package/dist/tsup/chunk-325TLXJT.js.map +1 -0
- package/dist/tsup/chunk-424PT5DM.js +23 -0
- package/dist/tsup/chunk-424PT5DM.js.map +1 -0
- package/dist/tsup/chunk-4JVIG3SS.cjs +6289 -0
- package/dist/tsup/chunk-4JVIG3SS.cjs.map +1 -0
- package/dist/tsup/chunk-6LJAZ5R4.cjs +96 -0
- package/dist/tsup/chunk-6LJAZ5R4.cjs.map +1 -0
- package/dist/tsup/chunk-6XU3FMCB.cjs +534 -0
- package/dist/tsup/chunk-6XU3FMCB.cjs.map +1 -0
- package/dist/tsup/chunk-7HTNH26M.js +509 -0
- package/dist/tsup/chunk-7HTNH26M.js.map +1 -0
- package/dist/tsup/chunk-AUVH72RE.cjs +5977 -0
- package/dist/tsup/chunk-AUVH72RE.cjs.map +1 -0
- package/dist/tsup/chunk-D4BYUPNQ.js +645 -0
- package/dist/tsup/chunk-D4BYUPNQ.js.map +1 -0
- package/dist/tsup/chunk-HDQ2JUQT.cjs +23 -0
- package/dist/tsup/chunk-HDQ2JUQT.cjs.map +1 -0
- package/dist/tsup/chunk-HHXX2VRM.js +6289 -0
- package/dist/tsup/chunk-HHXX2VRM.js.map +1 -0
- package/dist/tsup/chunk-JEAEA2PB.js +49 -0
- package/dist/tsup/chunk-JEAEA2PB.js.map +1 -0
- package/dist/tsup/chunk-JYSEG3VF.cjs +642 -0
- package/dist/tsup/chunk-JYSEG3VF.cjs.map +1 -0
- package/dist/tsup/chunk-K6DGYILQ.js +2657 -0
- package/dist/tsup/chunk-K6DGYILQ.js.map +1 -0
- package/dist/tsup/chunk-KJSYAUOM.js +96 -0
- package/dist/tsup/chunk-KJSYAUOM.js.map +1 -0
- package/dist/tsup/chunk-L47L3ZWJ.cjs +509 -0
- package/dist/tsup/chunk-L47L3ZWJ.cjs.map +1 -0
- package/dist/tsup/chunk-LXUQ667X.js +2006 -0
- package/dist/tsup/chunk-LXUQ667X.js.map +1 -0
- package/dist/tsup/chunk-MXNPAB5W.js +5977 -0
- package/dist/tsup/chunk-MXNPAB5W.js.map +1 -0
- package/dist/tsup/chunk-N4KRDJ56.js +72 -0
- package/dist/tsup/chunk-N4KRDJ56.js.map +1 -0
- package/dist/tsup/chunk-NIYZDWMW.cjs +2006 -0
- package/dist/tsup/chunk-NIYZDWMW.cjs.map +1 -0
- package/dist/tsup/chunk-PQZHDKRW.cjs +1060 -0
- package/dist/tsup/chunk-PQZHDKRW.cjs.map +1 -0
- package/dist/tsup/chunk-PVOE6BU7.cjs +1050 -0
- package/dist/tsup/chunk-PVOE6BU7.cjs.map +1 -0
- package/dist/tsup/chunk-Q4UD2GA4.cjs +1810 -0
- package/dist/tsup/chunk-Q4UD2GA4.cjs.map +1 -0
- package/dist/tsup/chunk-QUD664YZ.js +1810 -0
- package/dist/tsup/chunk-QUD664YZ.js.map +1 -0
- package/dist/tsup/chunk-RTOCTWME.js +1050 -0
- package/dist/tsup/chunk-RTOCTWME.js.map +1 -0
- package/dist/tsup/chunk-SAZZ4SB2.cjs +2657 -0
- package/dist/tsup/chunk-SAZZ4SB2.cjs.map +1 -0
- package/dist/tsup/chunk-SR3KQE7Q.cjs +72 -0
- package/dist/tsup/chunk-SR3KQE7Q.cjs.map +1 -0
- package/dist/tsup/chunk-V2GHLYC6.cjs +49 -0
- package/dist/tsup/chunk-V2GHLYC6.cjs.map +1 -0
- package/dist/tsup/chunk-V3WG7XTW.cjs +645 -0
- package/dist/tsup/chunk-V3WG7XTW.cjs.map +1 -0
- package/dist/tsup/chunk-VKVNIQRQ.js +257 -0
- package/dist/tsup/chunk-VKVNIQRQ.js.map +1 -0
- package/dist/tsup/chunk-WMPW7JYC.js +642 -0
- package/dist/tsup/chunk-WMPW7JYC.js.map +1 -0
- package/dist/tsup/chunk-Z7HNQ2WF.js +534 -0
- package/dist/tsup/chunk-Z7HNQ2WF.js.map +1 -0
- package/dist/tsup/chunk-ZFY5J2EP.cjs +257 -0
- package/dist/tsup/chunk-ZFY5J2EP.cjs.map +1 -0
- package/dist/tsup/client/mod.cjs +33 -0
- package/dist/tsup/client/mod.cjs.map +1 -0
- package/dist/tsup/client/mod.d.cts +64 -0
- package/dist/tsup/client/mod.d.ts +64 -0
- package/dist/tsup/client/mod.js +33 -0
- package/dist/tsup/client/mod.js.map +1 -0
- package/dist/tsup/common/log.cjs +21 -0
- package/dist/tsup/common/log.cjs.map +1 -0
- package/dist/tsup/common/log.d.cts +34 -0
- package/dist/tsup/common/log.d.ts +34 -0
- package/dist/tsup/common/log.js +21 -0
- package/dist/tsup/common/log.js.map +1 -0
- package/dist/tsup/common/websocket.cjs +10 -0
- package/dist/tsup/common/websocket.cjs.map +1 -0
- package/dist/tsup/common/websocket.d.cts +3 -0
- package/dist/tsup/common/websocket.d.ts +3 -0
- package/dist/tsup/common/websocket.js +10 -0
- package/dist/tsup/common/websocket.js.map +1 -0
- package/dist/tsup/config-BiNoIHRs.d.cts +80 -0
- package/dist/tsup/config-BiNoIHRs.d.ts +80 -0
- package/dist/tsup/config-P3XujgRr.d.ts +2594 -0
- package/dist/tsup/config-_gfywqqI.d.cts +2594 -0
- package/dist/tsup/context-Bxd8Cx4H.d.cts +75 -0
- package/dist/tsup/context-uNA4TRn3.d.ts +75 -0
- package/dist/tsup/db/drizzle/mod.cjs +49 -0
- package/dist/tsup/db/drizzle/mod.cjs.map +1 -0
- package/dist/tsup/db/drizzle/mod.d.cts +17 -0
- package/dist/tsup/db/drizzle/mod.d.ts +17 -0
- package/dist/tsup/db/drizzle/mod.js +49 -0
- package/dist/tsup/db/drizzle/mod.js.map +1 -0
- package/dist/tsup/db/mod.cjs +9 -0
- package/dist/tsup/db/mod.cjs.map +1 -0
- package/dist/tsup/db/mod.d.cts +9 -0
- package/dist/tsup/db/mod.d.ts +9 -0
- package/dist/tsup/db/mod.js +9 -0
- package/dist/tsup/db/mod.js.map +1 -0
- package/dist/tsup/driver-BcLvZcKl.d.cts +13 -0
- package/dist/tsup/driver-CPGHKXyh.d.ts +13 -0
- package/dist/tsup/driver-helpers/mod.cjs +53 -0
- package/dist/tsup/driver-helpers/mod.cjs.map +1 -0
- package/dist/tsup/driver-helpers/mod.d.cts +47 -0
- package/dist/tsup/driver-helpers/mod.d.ts +47 -0
- package/dist/tsup/driver-helpers/mod.js +53 -0
- package/dist/tsup/driver-helpers/mod.js.map +1 -0
- package/dist/tsup/driver-test-suite/mod.cjs +4974 -0
- package/dist/tsup/driver-test-suite/mod.cjs.map +1 -0
- package/dist/tsup/driver-test-suite/mod.d.cts +73 -0
- package/dist/tsup/driver-test-suite/mod.d.ts +73 -0
- package/dist/tsup/driver-test-suite/mod.js +4974 -0
- package/dist/tsup/driver-test-suite/mod.js.map +1 -0
- package/dist/tsup/inspector/mod.cjs +164 -0
- package/dist/tsup/inspector/mod.cjs.map +1 -0
- package/dist/tsup/inspector/mod.d.cts +130 -0
- package/dist/tsup/inspector/mod.d.ts +130 -0
- package/dist/tsup/inspector/mod.js +164 -0
- package/dist/tsup/inspector/mod.js.map +1 -0
- package/dist/tsup/keys-CydblqMh.d.cts +13 -0
- package/dist/tsup/keys-CydblqMh.d.ts +13 -0
- package/dist/tsup/mod.cjs +82 -0
- package/dist/tsup/mod.cjs.map +1 -0
- package/dist/tsup/mod.d.cts +126 -0
- package/dist/tsup/mod.d.ts +126 -0
- package/dist/tsup/mod.js +82 -0
- package/dist/tsup/mod.js.map +1 -0
- package/dist/tsup/serve-test-suite/mod.cjs +2601 -0
- package/dist/tsup/serve-test-suite/mod.cjs.map +1 -0
- package/dist/tsup/serve-test-suite/mod.d.cts +9 -0
- package/dist/tsup/serve-test-suite/mod.d.ts +9 -0
- package/dist/tsup/serve-test-suite/mod.js +2601 -0
- package/dist/tsup/serve-test-suite/mod.js.map +1 -0
- package/dist/tsup/test/mod.cjs +90 -0
- package/dist/tsup/test/mod.cjs.map +1 -0
- package/dist/tsup/test/mod.d.cts +26 -0
- package/dist/tsup/test/mod.d.ts +26 -0
- package/dist/tsup/test/mod.js +90 -0
- package/dist/tsup/test/mod.js.map +1 -0
- package/dist/tsup/utils-fwx3o3K9.d.cts +18 -0
- package/dist/tsup/utils-fwx3o3K9.d.ts +18 -0
- package/dist/tsup/utils.cjs +43 -0
- package/dist/tsup/utils.cjs.map +1 -0
- package/dist/tsup/utils.d.cts +148 -0
- package/dist/tsup/utils.d.ts +148 -0
- package/dist/tsup/utils.js +43 -0
- package/dist/tsup/utils.js.map +1 -0
- package/dist/tsup/v3-DnYObHH3.d.cts +279 -0
- package/dist/tsup/v3-DnYObHH3.d.ts +279 -0
- package/dist/tsup/workflow/mod.cjs +16 -0
- package/dist/tsup/workflow/mod.cjs.map +1 -0
- package/dist/tsup/workflow/mod.d.cts +25 -0
- package/dist/tsup/workflow/mod.d.ts +25 -0
- package/dist/tsup/workflow/mod.js +16 -0
- package/dist/tsup/workflow/mod.js.map +1 -0
- package/package.json +293 -5
- package/src/actor/config.ts +1221 -0
- package/src/actor/conn/driver.ts +61 -0
- package/src/actor/conn/drivers/http.ts +17 -0
- package/src/actor/conn/drivers/raw-request.ts +24 -0
- package/src/actor/conn/drivers/raw-websocket.ts +65 -0
- package/src/actor/conn/drivers/websocket.ts +144 -0
- package/src/actor/conn/mod.ts +288 -0
- package/src/actor/conn/persisted.ts +81 -0
- package/src/actor/conn/state-manager.ts +196 -0
- package/src/actor/contexts/action.ts +47 -0
- package/src/actor/contexts/base/actor.ts +347 -0
- package/src/actor/contexts/base/conn-init.ts +68 -0
- package/src/actor/contexts/base/conn.ts +73 -0
- package/src/actor/contexts/before-action-response.ts +42 -0
- package/src/actor/contexts/before-connect.ts +31 -0
- package/src/actor/contexts/connect.ts +42 -0
- package/src/actor/contexts/create-conn-state.ts +32 -0
- package/src/actor/contexts/create-vars.ts +39 -0
- package/src/actor/contexts/create.ts +39 -0
- package/src/actor/contexts/destroy.ts +42 -0
- package/src/actor/contexts/disconnect.ts +43 -0
- package/src/actor/contexts/index.ts +33 -0
- package/src/actor/contexts/request.ts +80 -0
- package/src/actor/contexts/run.ts +47 -0
- package/src/actor/contexts/sleep.ts +42 -0
- package/src/actor/contexts/state-change.ts +42 -0
- package/src/actor/contexts/wake.ts +42 -0
- package/src/actor/contexts/websocket.ts +80 -0
- package/src/actor/database.ts +13 -0
- package/src/actor/definition.ts +64 -0
- package/src/actor/driver.ts +114 -0
- package/src/actor/errors.ts +556 -0
- package/src/actor/instance/connection-manager.ts +574 -0
- package/src/actor/instance/event-manager.ts +314 -0
- package/src/actor/instance/keys.ts +146 -0
- package/src/actor/instance/kv.ts +241 -0
- package/src/actor/instance/mod.ts +1658 -0
- package/src/actor/instance/persisted.ts +67 -0
- package/src/actor/instance/queue-manager.ts +603 -0
- package/src/actor/instance/queue.ts +345 -0
- package/src/actor/instance/schedule-manager.ts +392 -0
- package/src/actor/instance/state-manager.ts +542 -0
- package/src/actor/instance/traces-driver.ts +128 -0
- package/src/actor/keys.test.ts +275 -0
- package/src/actor/keys.ts +89 -0
- package/src/actor/log.ts +6 -0
- package/src/actor/mod.ts +110 -0
- package/src/actor/protocol/old.ts +416 -0
- package/src/actor/protocol/serde.ts +222 -0
- package/src/actor/router-endpoints.ts +400 -0
- package/src/actor/router-websocket-endpoints.test.ts +54 -0
- package/src/actor/router-websocket-endpoints.ts +405 -0
- package/src/actor/router.ts +380 -0
- package/src/actor/schedule.ts +17 -0
- package/src/actor/schema.ts +291 -0
- package/src/actor/utils.test.ts +48 -0
- package/src/actor/utils.ts +158 -0
- package/src/client/actor-common.ts +32 -0
- package/src/client/actor-conn.ts +1262 -0
- package/src/client/actor-handle.ts +344 -0
- package/src/client/actor-query.ts +112 -0
- package/src/client/client.ts +558 -0
- package/src/client/config.ts +151 -0
- package/src/client/errors.ts +76 -0
- package/src/client/log.ts +5 -0
- package/src/client/mod.browser.ts +2 -0
- package/src/client/mod.ts +70 -0
- package/src/client/queue.ts +146 -0
- package/src/client/raw-utils.ts +149 -0
- package/src/client/test.ts +44 -0
- package/src/client/utils.ts +252 -0
- package/src/common/actor-router-consts.ts +59 -0
- package/src/common/cors.ts +57 -0
- package/src/common/eventsource-interface.ts +47 -0
- package/src/common/eventsource.ts +44 -0
- package/src/common/inline-websocket-adapter.ts +154 -0
- package/src/common/log-levels.ts +27 -0
- package/src/common/log.ts +229 -0
- package/src/common/logfmt.ts +221 -0
- package/src/common/network.ts +2 -0
- package/src/common/router.ts +174 -0
- package/src/common/utils.ts +339 -0
- package/src/common/websocket-interface.ts +7 -0
- package/src/common/websocket.ts +43 -0
- package/src/db/config.ts +100 -0
- package/src/db/drizzle/mod.ts +226 -0
- package/src/db/drizzle/sqlite-core.ts +22 -0
- package/src/db/mod.ts +125 -0
- package/src/db/shared.ts +92 -0
- package/src/db/sqlite-vfs.ts +12 -0
- package/src/devtools-loader/index.ts +33 -0
- package/src/devtools-loader/log.ts +5 -0
- package/src/driver-helpers/mod.ts +33 -0
- package/src/driver-helpers/utils.ts +54 -0
- package/src/driver-test-suite/log.ts +5 -0
- package/src/driver-test-suite/mod.ts +293 -0
- package/src/driver-test-suite/test-inline-client-driver.ts +307 -0
- package/src/driver-test-suite/tests/access-control.ts +218 -0
- package/src/driver-test-suite/tests/action-features.ts +203 -0
- package/src/driver-test-suite/tests/actor-conn-hibernation.ts +152 -0
- package/src/driver-test-suite/tests/actor-conn-state.ts +300 -0
- package/src/driver-test-suite/tests/actor-conn.ts +596 -0
- package/src/driver-test-suite/tests/actor-db-raw.ts +73 -0
- package/src/driver-test-suite/tests/actor-db.ts +477 -0
- package/src/driver-test-suite/tests/actor-destroy.ts +294 -0
- package/src/driver-test-suite/tests/actor-driver.ts +18 -0
- package/src/driver-test-suite/tests/actor-error-handling.ts +150 -0
- package/src/driver-test-suite/tests/actor-handle.ts +312 -0
- package/src/driver-test-suite/tests/actor-inline-client.ts +163 -0
- package/src/driver-test-suite/tests/actor-inspector.ts +264 -0
- package/src/driver-test-suite/tests/actor-kv.ts +65 -0
- package/src/driver-test-suite/tests/actor-metadata.ts +116 -0
- package/src/driver-test-suite/tests/actor-onstatechange.ts +95 -0
- package/src/driver-test-suite/tests/actor-queue.ts +325 -0
- package/src/driver-test-suite/tests/actor-run.ts +181 -0
- package/src/driver-test-suite/tests/actor-schedule.ts +97 -0
- package/src/driver-test-suite/tests/actor-sleep.ts +415 -0
- package/src/driver-test-suite/tests/actor-state.ts +54 -0
- package/src/driver-test-suite/tests/actor-stateless.ts +70 -0
- package/src/driver-test-suite/tests/actor-vars.ts +97 -0
- package/src/driver-test-suite/tests/actor-workflow.ts +118 -0
- package/src/driver-test-suite/tests/manager-driver.ts +388 -0
- package/src/driver-test-suite/tests/raw-http-direct-registry.ts +227 -0
- package/src/driver-test-suite/tests/raw-http-request-properties.ts +454 -0
- package/src/driver-test-suite/tests/raw-http.ts +359 -0
- package/src/driver-test-suite/tests/raw-websocket-direct-registry.ts +393 -0
- package/src/driver-test-suite/tests/raw-websocket.ts +513 -0
- package/src/driver-test-suite/tests/request-access.ts +240 -0
- package/src/driver-test-suite/utils.ts +80 -0
- package/src/drivers/default.ts +38 -0
- package/src/drivers/engine/actor-driver.ts +1027 -0
- package/src/drivers/engine/config.ts +43 -0
- package/src/drivers/engine/log.ts +5 -0
- package/src/drivers/engine/mod.ts +36 -0
- package/src/drivers/file-system/actor.ts +102 -0
- package/src/drivers/file-system/global-state.ts +1445 -0
- package/src/drivers/file-system/kv-limits.ts +70 -0
- package/src/drivers/file-system/log.ts +5 -0
- package/src/drivers/file-system/manager.ts +300 -0
- package/src/drivers/file-system/mod.ts +78 -0
- package/src/drivers/file-system/sqlite-runtime.ts +210 -0
- package/src/drivers/file-system/utils.ts +125 -0
- package/src/engine-process/constants.ts +2 -0
- package/src/engine-process/log.ts +5 -0
- package/src/engine-process/mod.ts +464 -0
- package/src/globals.d.ts +35 -0
- package/src/inspector/actor-inspector.ts +352 -0
- package/src/inspector/config.ts +49 -0
- package/src/inspector/handler.ts +273 -0
- package/src/inspector/log.ts +5 -0
- package/src/inspector/mod.browser.ts +8 -0
- package/src/inspector/mod.ts +4 -0
- package/src/inspector/serve-ui.ts +40 -0
- package/src/inspector/transport.ts +18 -0
- package/src/inspector/utils.ts +32 -0
- package/src/manager/driver.ts +106 -0
- package/src/manager/gateway.ts +668 -0
- package/src/manager/log.ts +5 -0
- package/src/manager/mod.ts +2 -0
- package/src/manager/protocol/mod.ts +22 -0
- package/src/manager/protocol/query.ts +85 -0
- package/src/manager/router-schema.ts +22 -0
- package/src/manager/router.ts +660 -0
- package/src/manager-api/actors.ts +83 -0
- package/src/manager-api/common.ts +4 -0
- package/src/mod.ts +24 -0
- package/src/registry/config/driver.ts +21 -0
- package/src/registry/config/index.ts +510 -0
- package/src/registry/config/legacy-runner.ts +157 -0
- package/src/registry/config/runner.ts +21 -0
- package/src/registry/config/serverless.ts +94 -0
- package/src/registry/index.ts +194 -0
- package/src/registry/log.ts +5 -0
- package/src/remote-manager-driver/actor-http-client.ts +84 -0
- package/src/remote-manager-driver/actor-websocket-client.ts +81 -0
- package/src/remote-manager-driver/api-endpoints.ts +159 -0
- package/src/remote-manager-driver/api-utils.ts +69 -0
- package/src/remote-manager-driver/log.ts +5 -0
- package/src/remote-manager-driver/metadata.ts +64 -0
- package/src/remote-manager-driver/mod.ts +414 -0
- package/src/remote-manager-driver/ws-proxy.ts +189 -0
- package/src/schemas/actor-inspector/mod.ts +1 -0
- package/src/schemas/actor-inspector/versioned.ts +233 -0
- package/src/schemas/actor-persist/mod.ts +1 -0
- package/src/schemas/actor-persist/versioned.ts +217 -0
- package/src/schemas/client-protocol/mod.ts +1 -0
- package/src/schemas/client-protocol/versioned.ts +330 -0
- package/src/schemas/client-protocol-zod/mod.ts +118 -0
- package/src/schemas/file-system-driver/mod.ts +1 -0
- package/src/schemas/file-system-driver/versioned.ts +135 -0
- package/src/schemas/persist/mod.ts +1 -0
- package/src/schemas/transport/mod.ts +1 -0
- package/src/serde.ts +138 -0
- package/src/serve-test-suite/mod.ts +148 -0
- package/src/serverless/configure.ts +82 -0
- package/src/serverless/log.ts +5 -0
- package/src/serverless/router.test.ts +299 -0
- package/src/serverless/router.ts +215 -0
- package/src/test/log.ts +5 -0
- package/src/test/mod.ts +99 -0
- package/src/utils/crypto.ts +24 -0
- package/src/utils/endpoint-parser.test.ts +202 -0
- package/src/utils/endpoint-parser.ts +124 -0
- package/src/utils/env-vars.ts +78 -0
- package/src/utils/node.ts +178 -0
- package/src/utils/router.ts +83 -0
- package/src/utils/serve.ts +212 -0
- package/src/utils.test.ts +34 -0
- package/src/utils.ts +437 -0
- package/src/workflow/constants.ts +2 -0
- package/src/workflow/context.ts +597 -0
- package/src/workflow/driver.ts +194 -0
- package/src/workflow/inspector.ts +268 -0
- package/src/workflow/mod.ts +128 -0
|
@@ -0,0 +1,1445 @@
|
|
|
1
|
+
import invariant from "invariant";
|
|
2
|
+
import { lookupInRegistry } from "@/actor/definition";
|
|
3
|
+
import { ActorDuplicateKey } from "@/actor/errors";
|
|
4
|
+
import type { AnyActorInstance } from "@/actor/instance/mod";
|
|
5
|
+
import type { ActorKey } from "@/actor/mod";
|
|
6
|
+
import type { AnyClient } from "@/client/client";
|
|
7
|
+
import { type ActorDriver, getInitialActorKvState } from "@/driver-helpers/mod";
|
|
8
|
+
import type { RegistryConfig } from "@/registry/config";
|
|
9
|
+
import type * as schema from "@/schemas/file-system-driver/mod";
|
|
10
|
+
import {
|
|
11
|
+
ACTOR_ALARM_VERSIONED,
|
|
12
|
+
ACTOR_STATE_VERSIONED,
|
|
13
|
+
CURRENT_VERSION as FILE_SYSTEM_DRIVER_CURRENT_VERSION,
|
|
14
|
+
} from "@/schemas/file-system-driver/versioned";
|
|
15
|
+
import {
|
|
16
|
+
type LongTimeoutHandle,
|
|
17
|
+
promiseWithResolvers,
|
|
18
|
+
setLongTimeout,
|
|
19
|
+
stringifyError,
|
|
20
|
+
} from "@/utils";
|
|
21
|
+
import {
|
|
22
|
+
getNodeCrypto,
|
|
23
|
+
getNodeFs,
|
|
24
|
+
getNodeFsSync,
|
|
25
|
+
getNodePath,
|
|
26
|
+
} from "@/utils/node";
|
|
27
|
+
import { logger } from "./log";
|
|
28
|
+
import {
|
|
29
|
+
ensureDirectoryExists,
|
|
30
|
+
ensureDirectoryExistsSync,
|
|
31
|
+
getStoragePath,
|
|
32
|
+
} from "./utils";
|
|
33
|
+
import {
|
|
34
|
+
computePrefixUpperBound,
|
|
35
|
+
ensureUint8Array,
|
|
36
|
+
loadSqliteRuntime,
|
|
37
|
+
type SqliteRuntime,
|
|
38
|
+
type SqliteRuntimeDatabase,
|
|
39
|
+
} from "./sqlite-runtime";
|
|
40
|
+
import {
|
|
41
|
+
estimateKvSize,
|
|
42
|
+
validateKvEntries,
|
|
43
|
+
validateKvKey,
|
|
44
|
+
validateKvKeys,
|
|
45
|
+
} from "./kv-limits";
|
|
46
|
+
|
|
47
|
+
// Actor handler to track running instances
|
|
48
|
+
|
|
49
|
+
enum ActorLifecycleState {
|
|
50
|
+
NONEXISTENT, // Entry exists but actor not yet created
|
|
51
|
+
AWAKE, // Actor is running normally
|
|
52
|
+
STARTING_SLEEP, // Actor is being put to sleep
|
|
53
|
+
STARTING_DESTROY, // Actor is being destroyed
|
|
54
|
+
DESTROYED, // Actor was destroyed, should not be recreated
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ActorEntry {
|
|
58
|
+
id: string;
|
|
59
|
+
|
|
60
|
+
state?: schema.ActorState;
|
|
61
|
+
|
|
62
|
+
/** Promise for loading the actor state. */
|
|
63
|
+
loadPromise?: Promise<ActorEntry>;
|
|
64
|
+
|
|
65
|
+
actor?: AnyActorInstance;
|
|
66
|
+
/** Promise for starting the actor. */
|
|
67
|
+
startPromise?: ReturnType<typeof promiseWithResolvers<void>>;
|
|
68
|
+
/** Promise for stopping the actor. */
|
|
69
|
+
stopPromise?: PromiseWithResolvers<void>;
|
|
70
|
+
|
|
71
|
+
alarmTimeout?: LongTimeoutHandle;
|
|
72
|
+
/** The timestamp currently scheduled for this actor's alarm (ms since epoch). */
|
|
73
|
+
alarmTimestamp?: number;
|
|
74
|
+
|
|
75
|
+
/** Resolver for pending write operations that need to be notified when any write completes */
|
|
76
|
+
pendingWriteResolver?: PromiseWithResolvers<void>;
|
|
77
|
+
|
|
78
|
+
lifecycleState: ActorLifecycleState;
|
|
79
|
+
|
|
80
|
+
// TODO: This might make sense to move in to actorstate, but we have a
|
|
81
|
+
// single reader/writer so it's not an issue
|
|
82
|
+
/** Generation of this actor when creating/destroying. */
|
|
83
|
+
generation: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface FileSystemDriverOptions {
|
|
87
|
+
/** Whether to persist data to disk */
|
|
88
|
+
persist?: boolean;
|
|
89
|
+
/** Custom path for storage */
|
|
90
|
+
customPath?: string;
|
|
91
|
+
/** Deprecated option retained for explicit migration to sqlite-only KV. */
|
|
92
|
+
useNativeSqlite?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Global state for the file system driver
|
|
97
|
+
*/
|
|
98
|
+
export class FileSystemGlobalState {
|
|
99
|
+
#storagePath: string;
|
|
100
|
+
#stateDir: string;
|
|
101
|
+
#dbsDir: string;
|
|
102
|
+
#alarmsDir: string;
|
|
103
|
+
|
|
104
|
+
#persist: boolean;
|
|
105
|
+
#sqliteRuntime: SqliteRuntime;
|
|
106
|
+
#actorKvDatabases = new Map<string, SqliteRuntimeDatabase>();
|
|
107
|
+
|
|
108
|
+
// IMPORTANT: Never delete from this map. Doing so will result in race
|
|
109
|
+
// conditions since the actor generation will cease to be tracked
|
|
110
|
+
// correctly. Always increment generation if a new actor is created.
|
|
111
|
+
#actors = new Map<string, ActorEntry>();
|
|
112
|
+
|
|
113
|
+
#actorCountOnStartup: number = 0;
|
|
114
|
+
|
|
115
|
+
#runnerParams?: {
|
|
116
|
+
config: RegistryConfig;
|
|
117
|
+
inlineClient: AnyClient;
|
|
118
|
+
actorDriver: ActorDriver;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
get persist(): boolean {
|
|
122
|
+
return this.#persist;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
get storagePath() {
|
|
126
|
+
return this.#storagePath;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get actorCountOnStartup() {
|
|
130
|
+
return this.#actorCountOnStartup;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
constructor(options: FileSystemDriverOptions = {}) {
|
|
134
|
+
const { persist = true, customPath, useNativeSqlite = true } = options;
|
|
135
|
+
if (!useNativeSqlite) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"File-system driver no longer supports non-SQLite KV storage.",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
this.#persist = persist;
|
|
141
|
+
this.#sqliteRuntime = loadSqliteRuntime();
|
|
142
|
+
this.#storagePath = persist ? (customPath ?? getStoragePath()) : "/tmp";
|
|
143
|
+
const path = getNodePath();
|
|
144
|
+
this.#stateDir = path.join(this.#storagePath, "state");
|
|
145
|
+
this.#dbsDir = path.join(this.#storagePath, "databases");
|
|
146
|
+
this.#alarmsDir = path.join(this.#storagePath, "alarms");
|
|
147
|
+
|
|
148
|
+
if (this.#persist) {
|
|
149
|
+
// Ensure storage directories exist synchronously during initialization
|
|
150
|
+
ensureDirectoryExistsSync(this.#stateDir);
|
|
151
|
+
ensureDirectoryExistsSync(this.#dbsDir);
|
|
152
|
+
ensureDirectoryExistsSync(this.#alarmsDir);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const fsSync = getNodeFsSync();
|
|
156
|
+
const actorIds = fsSync.readdirSync(this.#stateDir);
|
|
157
|
+
this.#actorCountOnStartup = actorIds.length;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logger().error({ msg: "failed to count actors", error });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
logger().debug({
|
|
163
|
+
msg: "file system driver ready",
|
|
164
|
+
dir: this.#storagePath,
|
|
165
|
+
actorCount: this.#actorCountOnStartup,
|
|
166
|
+
sqliteRuntime: this.#sqliteRuntime.kind,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Cleanup stale temp files on startup
|
|
170
|
+
try {
|
|
171
|
+
this.#cleanupTempFilesSync();
|
|
172
|
+
} catch (err) {
|
|
173
|
+
logger().error({
|
|
174
|
+
msg: "failed to cleanup temp files",
|
|
175
|
+
error: err,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
this.#migrateLegacyKvToSqliteOnStartupSync();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger().error({
|
|
183
|
+
msg: "failed legacy kv startup migration",
|
|
184
|
+
error,
|
|
185
|
+
});
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
logger().debug({
|
|
190
|
+
msg: "memory driver ready",
|
|
191
|
+
sqliteRuntime: this.#sqliteRuntime.kind,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
getActorStatePath(actorId: string): string {
|
|
197
|
+
return getNodePath().join(this.#stateDir, actorId);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getActorDbPath(actorId: string): string {
|
|
201
|
+
return getNodePath().join(this.#dbsDir, `${actorId}.db`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getActorAlarmPath(actorId: string): string {
|
|
205
|
+
return getNodePath().join(this.#alarmsDir, actorId);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#getActorKvDatabasePath(actorId: string): string {
|
|
209
|
+
if (this.#persist) {
|
|
210
|
+
return this.getActorDbPath(actorId);
|
|
211
|
+
}
|
|
212
|
+
return ":memory:";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#ensureActorKvTables(db: SqliteRuntimeDatabase): void {
|
|
216
|
+
db.exec(`
|
|
217
|
+
CREATE TABLE IF NOT EXISTS kv (
|
|
218
|
+
key BLOB PRIMARY KEY NOT NULL,
|
|
219
|
+
value BLOB NOT NULL
|
|
220
|
+
)
|
|
221
|
+
`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#getOrCreateActorKvDatabase(actorId: string): SqliteRuntimeDatabase {
|
|
225
|
+
const existing = this.#actorKvDatabases.get(actorId);
|
|
226
|
+
if (existing) {
|
|
227
|
+
return existing;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const dbPath = this.#getActorKvDatabasePath(actorId);
|
|
231
|
+
if (this.#persist) {
|
|
232
|
+
const path = getNodePath();
|
|
233
|
+
ensureDirectoryExistsSync(path.dirname(dbPath));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let db: SqliteRuntimeDatabase;
|
|
237
|
+
try {
|
|
238
|
+
db = this.#sqliteRuntime.open(dbPath);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`failed to open actor kv database for actor ${actorId} at ${dbPath}: ${error}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.#ensureActorKvTables(db);
|
|
246
|
+
this.#actorKvDatabases.set(actorId, db);
|
|
247
|
+
return db;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#closeActorKvDatabase(actorId: string): void {
|
|
251
|
+
const db = this.#actorKvDatabases.get(actorId);
|
|
252
|
+
if (!db) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
db.close();
|
|
258
|
+
} finally {
|
|
259
|
+
this.#actorKvDatabases.delete(actorId);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#putKvEntriesInDb(
|
|
264
|
+
db: SqliteRuntimeDatabase,
|
|
265
|
+
entries: [Uint8Array, Uint8Array][],
|
|
266
|
+
): void {
|
|
267
|
+
if (entries.length === 0) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
db.exec("BEGIN");
|
|
272
|
+
try {
|
|
273
|
+
for (const [key, value] of entries) {
|
|
274
|
+
db.run("INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)", [
|
|
275
|
+
key,
|
|
276
|
+
value,
|
|
277
|
+
]);
|
|
278
|
+
}
|
|
279
|
+
db.exec("COMMIT");
|
|
280
|
+
} catch (error) {
|
|
281
|
+
try {
|
|
282
|
+
db.exec("ROLLBACK");
|
|
283
|
+
} catch {
|
|
284
|
+
// Ignore rollback errors, original error is more actionable.
|
|
285
|
+
}
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#isKvDbPopulated(db: SqliteRuntimeDatabase): boolean {
|
|
291
|
+
const row = db.get<{ count: number | bigint }>(
|
|
292
|
+
"SELECT COUNT(*) AS count FROM kv",
|
|
293
|
+
);
|
|
294
|
+
const count = row ? Number(row.count) : 0;
|
|
295
|
+
return count > 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#migrateLegacyKvToSqliteOnStartupSync(): void {
|
|
299
|
+
const fsSync = getNodeFsSync();
|
|
300
|
+
if (!fsSync.existsSync(this.#stateDir)) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const actorIds = fsSync
|
|
305
|
+
.readdirSync(this.#stateDir)
|
|
306
|
+
.filter((id) => !id.includes(".tmp."));
|
|
307
|
+
|
|
308
|
+
for (const actorId of actorIds) {
|
|
309
|
+
const statePath = this.getActorStatePath(actorId);
|
|
310
|
+
let state: schema.ActorState;
|
|
311
|
+
try {
|
|
312
|
+
const stateBytes = fsSync.readFileSync(statePath);
|
|
313
|
+
state = ACTOR_STATE_VERSIONED.deserializeWithEmbeddedVersion(
|
|
314
|
+
new Uint8Array(stateBytes),
|
|
315
|
+
);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
logger().warn({
|
|
318
|
+
msg: "failed to parse actor state during startup migration",
|
|
319
|
+
actorId,
|
|
320
|
+
error,
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!state.kvStorage || state.kvStorage.length === 0) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const dbPath = this.getActorDbPath(actorId);
|
|
330
|
+
const path = getNodePath();
|
|
331
|
+
ensureDirectoryExistsSync(path.dirname(dbPath));
|
|
332
|
+
const db = this.#sqliteRuntime.open(dbPath);
|
|
333
|
+
try {
|
|
334
|
+
this.#ensureActorKvTables(db);
|
|
335
|
+
if (this.#isKvDbPopulated(db)) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const legacyEntries = state.kvStorage.map((entry) => [
|
|
340
|
+
new Uint8Array(entry.key),
|
|
341
|
+
new Uint8Array(entry.value),
|
|
342
|
+
]) as [Uint8Array, Uint8Array][];
|
|
343
|
+
this.#putKvEntriesInDb(db, legacyEntries);
|
|
344
|
+
|
|
345
|
+
logger().info({
|
|
346
|
+
msg: "migrated legacy actor kv storage to sqlite",
|
|
347
|
+
actorId,
|
|
348
|
+
entryCount: legacyEntries.length,
|
|
349
|
+
});
|
|
350
|
+
} finally {
|
|
351
|
+
db.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async *getActorsIterator(params: {
|
|
357
|
+
cursor?: string;
|
|
358
|
+
}): AsyncGenerator<schema.ActorState> {
|
|
359
|
+
let actorIds = Array.from(this.#actors.keys()).sort();
|
|
360
|
+
|
|
361
|
+
// Check if state directory exists first
|
|
362
|
+
const fsSync = getNodeFsSync();
|
|
363
|
+
if (fsSync.existsSync(this.#stateDir)) {
|
|
364
|
+
actorIds = fsSync
|
|
365
|
+
.readdirSync(this.#stateDir)
|
|
366
|
+
.filter((id) => !id.includes(".tmp"))
|
|
367
|
+
.sort();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const startIndex = params.cursor
|
|
371
|
+
? actorIds.indexOf(params.cursor) + 1
|
|
372
|
+
: 0;
|
|
373
|
+
|
|
374
|
+
for (let i = startIndex; i < actorIds.length; i++) {
|
|
375
|
+
const actorId = actorIds[i];
|
|
376
|
+
if (!actorId) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const state = await this.loadActorStateOrError(actorId);
|
|
382
|
+
yield state;
|
|
383
|
+
} catch (error) {
|
|
384
|
+
logger().error({
|
|
385
|
+
msg: "failed to load actor state",
|
|
386
|
+
actorId,
|
|
387
|
+
error,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Ensures an entry exists for this actor.
|
|
395
|
+
*
|
|
396
|
+
* Used for #createActor and #loadActor.
|
|
397
|
+
*/
|
|
398
|
+
#upsertEntry(actorId: string): ActorEntry {
|
|
399
|
+
let entry = this.#actors.get(actorId);
|
|
400
|
+
if (entry) {
|
|
401
|
+
return entry;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
entry = {
|
|
405
|
+
id: actorId,
|
|
406
|
+
lifecycleState: ActorLifecycleState.NONEXISTENT,
|
|
407
|
+
generation: crypto.randomUUID(),
|
|
408
|
+
};
|
|
409
|
+
this.#actors.set(actorId, entry);
|
|
410
|
+
return entry;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Creates a new actor and writes to file system.
|
|
415
|
+
*/
|
|
416
|
+
async createActor(
|
|
417
|
+
actorId: string,
|
|
418
|
+
name: string,
|
|
419
|
+
key: ActorKey,
|
|
420
|
+
input: unknown | undefined,
|
|
421
|
+
): Promise<ActorEntry> {
|
|
422
|
+
// TODO: Does not check if actor already exists on fs
|
|
423
|
+
|
|
424
|
+
await this.#waitForActorStop(actorId);
|
|
425
|
+
let entry = this.#upsertEntry(actorId);
|
|
426
|
+
|
|
427
|
+
// Check if actor already exists (has state or is being stopped)
|
|
428
|
+
if (entry.state) {
|
|
429
|
+
throw new ActorDuplicateKey(name, key);
|
|
430
|
+
}
|
|
431
|
+
if (this.isActorStopping(actorId)) {
|
|
432
|
+
await this.#waitForActorStop(actorId);
|
|
433
|
+
entry = this.#upsertEntry(actorId);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// If actor was destroyed, reset to NONEXISTENT and increment generation
|
|
437
|
+
if (entry.lifecycleState === ActorLifecycleState.DESTROYED) {
|
|
438
|
+
entry.lifecycleState = ActorLifecycleState.NONEXISTENT;
|
|
439
|
+
entry.generation = crypto.randomUUID();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Initialize storage (runtime KV is stored in SQLite; state.kvStorage is legacy-only)
|
|
443
|
+
const initialKvState = getInitialActorKvState(input);
|
|
444
|
+
|
|
445
|
+
// Initialize metadata
|
|
446
|
+
await this.#withActorWrite(actorId, async (lockedEntry) => {
|
|
447
|
+
lockedEntry.state = {
|
|
448
|
+
actorId,
|
|
449
|
+
name,
|
|
450
|
+
key,
|
|
451
|
+
createdAt: BigInt(Date.now()),
|
|
452
|
+
kvStorage: [],
|
|
453
|
+
startTs: null,
|
|
454
|
+
connectableTs: null,
|
|
455
|
+
sleepTs: null,
|
|
456
|
+
destroyTs: null,
|
|
457
|
+
};
|
|
458
|
+
lockedEntry.lifecycleState = ActorLifecycleState.AWAKE;
|
|
459
|
+
if (this.#persist) {
|
|
460
|
+
await this.#performWrite(
|
|
461
|
+
actorId,
|
|
462
|
+
lockedEntry.generation,
|
|
463
|
+
lockedEntry.state,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
if (initialKvState.length > 0) {
|
|
467
|
+
const db = this.#getOrCreateActorKvDatabase(actorId);
|
|
468
|
+
this.#putKvEntriesInDb(db, initialKvState);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return entry;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Loads the actor from disk or returns the existing actor entry. This will return an entry even if the actor does not actually exist.
|
|
477
|
+
*/
|
|
478
|
+
async loadActor(actorId: string): Promise<ActorEntry> {
|
|
479
|
+
const entry = this.#upsertEntry(actorId);
|
|
480
|
+
|
|
481
|
+
// Check if destroyed - don't load from disk
|
|
482
|
+
if (entry.lifecycleState === ActorLifecycleState.DESTROYED) {
|
|
483
|
+
return entry;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Check if already loaded
|
|
487
|
+
if (entry.state) {
|
|
488
|
+
return entry;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// If not persisted, then don't load from FS
|
|
492
|
+
if (!this.#persist) {
|
|
493
|
+
return entry;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// If state is currently being loaded, wait for it
|
|
497
|
+
if (entry.loadPromise) {
|
|
498
|
+
await entry.loadPromise;
|
|
499
|
+
return entry;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Start loading state
|
|
503
|
+
entry.loadPromise = this.loadActorState(entry);
|
|
504
|
+
return entry.loadPromise;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private async loadActorState(entry: ActorEntry) {
|
|
508
|
+
const stateFilePath = this.getActorStatePath(entry.id);
|
|
509
|
+
|
|
510
|
+
// Read & parse file
|
|
511
|
+
try {
|
|
512
|
+
const fs = getNodeFs();
|
|
513
|
+
const stateData = await fs.readFile(stateFilePath);
|
|
514
|
+
|
|
515
|
+
const loadedState =
|
|
516
|
+
ACTOR_STATE_VERSIONED.deserializeWithEmbeddedVersion(
|
|
517
|
+
new Uint8Array(stateData),
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
// Runtime reads/writes are SQLite-only; legacy kvStorage is for one-time startup migration.
|
|
521
|
+
entry.state = {
|
|
522
|
+
...loadedState,
|
|
523
|
+
kvStorage: [],
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
return entry;
|
|
527
|
+
} catch (innerError: any) {
|
|
528
|
+
// File does not exist, meaning the actor does not exist
|
|
529
|
+
if (innerError.code === "ENOENT") {
|
|
530
|
+
entry.loadPromise = undefined;
|
|
531
|
+
return entry;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// For other errors, throw
|
|
535
|
+
const error = new Error(
|
|
536
|
+
`Failed to load actor state: ${innerError}`,
|
|
537
|
+
);
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async loadOrCreateActor(
|
|
543
|
+
actorId: string,
|
|
544
|
+
name: string,
|
|
545
|
+
key: ActorKey,
|
|
546
|
+
input: unknown | undefined,
|
|
547
|
+
): Promise<ActorEntry> {
|
|
548
|
+
await this.#waitForActorStop(actorId);
|
|
549
|
+
|
|
550
|
+
// Attempt to load actor
|
|
551
|
+
const entry = await this.loadActor(actorId);
|
|
552
|
+
|
|
553
|
+
// If no state for this actor, then create & write state
|
|
554
|
+
if (!entry.state) {
|
|
555
|
+
if (this.isActorStopping(actorId)) {
|
|
556
|
+
await this.#waitForActorStop(actorId);
|
|
557
|
+
return await this.loadOrCreateActor(actorId, name, key, input);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// If actor was destroyed, reset to NONEXISTENT and increment generation
|
|
561
|
+
if (entry.lifecycleState === ActorLifecycleState.DESTROYED) {
|
|
562
|
+
entry.lifecycleState = ActorLifecycleState.NONEXISTENT;
|
|
563
|
+
entry.generation = crypto.randomUUID();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Initialize storage (runtime KV is stored in SQLite; state.kvStorage is legacy-only)
|
|
567
|
+
const initialKvState = getInitialActorKvState(input);
|
|
568
|
+
|
|
569
|
+
await this.#withActorWrite(actorId, async (lockedEntry) => {
|
|
570
|
+
lockedEntry.state = {
|
|
571
|
+
actorId,
|
|
572
|
+
name,
|
|
573
|
+
key: key as readonly string[],
|
|
574
|
+
createdAt: BigInt(Date.now()),
|
|
575
|
+
kvStorage: [],
|
|
576
|
+
startTs: null,
|
|
577
|
+
connectableTs: null,
|
|
578
|
+
sleepTs: null,
|
|
579
|
+
destroyTs: null,
|
|
580
|
+
};
|
|
581
|
+
if (this.#persist) {
|
|
582
|
+
await this.#performWrite(
|
|
583
|
+
actorId,
|
|
584
|
+
lockedEntry.generation,
|
|
585
|
+
lockedEntry.state,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (initialKvState.length > 0) {
|
|
589
|
+
const db = this.#getOrCreateActorKvDatabase(actorId);
|
|
590
|
+
this.#putKvEntriesInDb(db, initialKvState);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return entry;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async sleepActor(actorId: string) {
|
|
598
|
+
invariant(
|
|
599
|
+
this.#persist,
|
|
600
|
+
"cannot sleep actor with memory driver, must use file system driver",
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
// Get the actor. We upsert it even though we're about to destroy it so we have a lock on flagging `destroying` as true.
|
|
604
|
+
const actor = this.#upsertEntry(actorId);
|
|
605
|
+
invariant(actor, `tried to sleep ${actorId}, does not exist`);
|
|
606
|
+
|
|
607
|
+
// Check if already destroying
|
|
608
|
+
if (this.isActorStopping(actorId)) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
actor.lifecycleState = ActorLifecycleState.STARTING_SLEEP;
|
|
612
|
+
actor.stopPromise = promiseWithResolvers((reason) => logger().warn({ msg: "unhandled actor sleep stop promise rejection", reason }));
|
|
613
|
+
|
|
614
|
+
// Wait for actor to fully start before stopping it to avoid race conditions
|
|
615
|
+
if (actor.loadPromise) await actor.loadPromise.catch();
|
|
616
|
+
if (actor.startPromise?.promise)
|
|
617
|
+
await actor.startPromise.promise.catch();
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
// Update state with sleep timestamp
|
|
621
|
+
if (actor.state) {
|
|
622
|
+
await this.#withActorWrite(actorId, async (lockedEntry) => {
|
|
623
|
+
if (!lockedEntry.state) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
lockedEntry.state = {
|
|
627
|
+
...lockedEntry.state,
|
|
628
|
+
sleepTs: BigInt(Date.now()),
|
|
629
|
+
};
|
|
630
|
+
if (this.#persist) {
|
|
631
|
+
await this.#performWrite(
|
|
632
|
+
actorId,
|
|
633
|
+
lockedEntry.generation,
|
|
634
|
+
lockedEntry.state,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Stop actor
|
|
641
|
+
invariant(actor.actor, "actor should be loaded");
|
|
642
|
+
await actor.actor.onStop("sleep");
|
|
643
|
+
} finally {
|
|
644
|
+
// Ensure any pending KV writes finish before removing the entry.
|
|
645
|
+
await this.#withActorWrite(actorId, async () => {});
|
|
646
|
+
this.#closeActorKvDatabase(actorId);
|
|
647
|
+
actor.stopPromise?.resolve();
|
|
648
|
+
actor.stopPromise = undefined;
|
|
649
|
+
|
|
650
|
+
// Remove from map after stop is complete
|
|
651
|
+
this.#actors.delete(actorId);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async destroyActor(actorId: string) {
|
|
656
|
+
// Get the actor. We upsert it even though we're about to destroy it so we have a lock on flagging `destroying` as true.
|
|
657
|
+
const actor = this.#upsertEntry(actorId);
|
|
658
|
+
|
|
659
|
+
// If actor is loaded, stop it first
|
|
660
|
+
// Check if already destroying
|
|
661
|
+
if (this.isActorStopping(actorId)) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
actor.lifecycleState = ActorLifecycleState.STARTING_DESTROY;
|
|
665
|
+
actor.stopPromise = promiseWithResolvers((reason) => logger().warn({ msg: "unhandled actor destroy stop promise rejection", reason }));
|
|
666
|
+
|
|
667
|
+
// Wait for actor to fully start before stopping it to avoid race conditions
|
|
668
|
+
if (actor.loadPromise) await actor.loadPromise.catch();
|
|
669
|
+
if (actor.startPromise?.promise)
|
|
670
|
+
await actor.startPromise.promise.catch();
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
// Update state with destroy timestamp
|
|
674
|
+
if (actor.state) {
|
|
675
|
+
await this.#withActorWrite(actorId, async (lockedEntry) => {
|
|
676
|
+
if (!lockedEntry.state) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
lockedEntry.state = {
|
|
680
|
+
...lockedEntry.state,
|
|
681
|
+
destroyTs: BigInt(Date.now()),
|
|
682
|
+
};
|
|
683
|
+
if (this.#persist) {
|
|
684
|
+
await this.#performWrite(
|
|
685
|
+
actorId,
|
|
686
|
+
lockedEntry.generation,
|
|
687
|
+
lockedEntry.state,
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Stop actor if it's running
|
|
694
|
+
if (actor.actor) {
|
|
695
|
+
await actor.actor.onStop("destroy");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Ensure any pending KV writes finish before deleting files.
|
|
699
|
+
await this.#withActorWrite(actorId, async () => {});
|
|
700
|
+
this.#closeActorKvDatabase(actorId);
|
|
701
|
+
|
|
702
|
+
// Clear alarm timeout if exists
|
|
703
|
+
if (actor.alarmTimeout) {
|
|
704
|
+
actor.alarmTimeout.abort();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Delete persisted files if using file system driver
|
|
708
|
+
if (this.#persist) {
|
|
709
|
+
const fs = getNodeFs();
|
|
710
|
+
|
|
711
|
+
// Delete all actor files in parallel
|
|
712
|
+
await Promise.all([
|
|
713
|
+
// Delete actor state file
|
|
714
|
+
(async () => {
|
|
715
|
+
try {
|
|
716
|
+
await fs.unlink(this.getActorStatePath(actorId));
|
|
717
|
+
} catch (err: any) {
|
|
718
|
+
if (err?.code !== "ENOENT") {
|
|
719
|
+
logger().error({
|
|
720
|
+
msg: "failed to delete actor state file",
|
|
721
|
+
actorId,
|
|
722
|
+
error: stringifyError(err),
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
})(),
|
|
727
|
+
// Delete actor database file
|
|
728
|
+
(async () => {
|
|
729
|
+
try {
|
|
730
|
+
await fs.unlink(this.getActorDbPath(actorId));
|
|
731
|
+
} catch (err: any) {
|
|
732
|
+
if (err?.code !== "ENOENT") {
|
|
733
|
+
logger().error({
|
|
734
|
+
msg: "failed to delete actor database file",
|
|
735
|
+
actorId,
|
|
736
|
+
error: stringifyError(err),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
})(),
|
|
741
|
+
// Delete actor alarm file
|
|
742
|
+
(async () => {
|
|
743
|
+
try {
|
|
744
|
+
await fs.unlink(this.getActorAlarmPath(actorId));
|
|
745
|
+
} catch (err: any) {
|
|
746
|
+
if (err?.code !== "ENOENT") {
|
|
747
|
+
logger().error({
|
|
748
|
+
msg: "failed to delete actor alarm file",
|
|
749
|
+
actorId,
|
|
750
|
+
error: stringifyError(err),
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
})(),
|
|
755
|
+
]);
|
|
756
|
+
}
|
|
757
|
+
} finally {
|
|
758
|
+
// Ensure any pending KV writes finish before clearing the entry.
|
|
759
|
+
await this.#withActorWrite(actorId, async () => {});
|
|
760
|
+
actor.stopPromise?.resolve();
|
|
761
|
+
actor.stopPromise = undefined;
|
|
762
|
+
|
|
763
|
+
// Reset the entry
|
|
764
|
+
//
|
|
765
|
+
// Do not remove entry in order to avoid race condition with
|
|
766
|
+
// destroying. Next actor creation will increment the generation.
|
|
767
|
+
actor.state = undefined;
|
|
768
|
+
actor.loadPromise = undefined;
|
|
769
|
+
actor.actor = undefined;
|
|
770
|
+
actor.startPromise = undefined;
|
|
771
|
+
actor.alarmTimeout = undefined;
|
|
772
|
+
actor.alarmTimeout = undefined;
|
|
773
|
+
actor.pendingWriteResolver = undefined;
|
|
774
|
+
actor.lifecycleState = ActorLifecycleState.DESTROYED;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Save actor state to disk.
|
|
780
|
+
*/
|
|
781
|
+
async writeActor(
|
|
782
|
+
actorId: string,
|
|
783
|
+
generation: string,
|
|
784
|
+
state: schema.ActorState,
|
|
785
|
+
): Promise<void> {
|
|
786
|
+
if (!this.#persist) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
await this.#withActorWrite(actorId, async () => {
|
|
791
|
+
await this.#performWrite(actorId, generation, state);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
isGenerationCurrentAndNotDestroyed(
|
|
796
|
+
actorId: string,
|
|
797
|
+
generation: string,
|
|
798
|
+
): boolean {
|
|
799
|
+
const entry = this.#upsertEntry(actorId);
|
|
800
|
+
if (!entry) return false;
|
|
801
|
+
return (
|
|
802
|
+
entry.generation === generation &&
|
|
803
|
+
entry.lifecycleState !== ActorLifecycleState.STARTING_DESTROY
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
isActorStopping(actorId: string) {
|
|
808
|
+
const entry = this.#upsertEntry(actorId);
|
|
809
|
+
if (!entry) return false;
|
|
810
|
+
return (
|
|
811
|
+
entry.lifecycleState === ActorLifecycleState.STARTING_SLEEP ||
|
|
812
|
+
entry.lifecycleState === ActorLifecycleState.STARTING_DESTROY
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async #waitForActorStop(actorId: string): Promise<void> {
|
|
817
|
+
while (true) {
|
|
818
|
+
const entry = this.#actors.get(actorId);
|
|
819
|
+
if (!entry?.stopPromise) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
await entry.stopPromise.promise;
|
|
824
|
+
} catch {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async #withActorWrite<T>(
|
|
831
|
+
actorId: string,
|
|
832
|
+
fn: (entry: ActorEntry) => Promise<T>,
|
|
833
|
+
): Promise<T> {
|
|
834
|
+
const entry = this.#actors.get(actorId);
|
|
835
|
+
invariant(entry, "actor entry does not exist");
|
|
836
|
+
|
|
837
|
+
const previousWrite = entry.pendingWriteResolver;
|
|
838
|
+
const currentWrite = promiseWithResolvers<void>((reason) => logger().warn({ msg: "unhandled kv write promise rejection", reason }));
|
|
839
|
+
entry.pendingWriteResolver = currentWrite;
|
|
840
|
+
|
|
841
|
+
if (previousWrite) {
|
|
842
|
+
try {
|
|
843
|
+
await previousWrite.promise;
|
|
844
|
+
} catch {
|
|
845
|
+
// Ignore failed previous writes so later writes can proceed.
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
try {
|
|
850
|
+
return await fn(entry);
|
|
851
|
+
} finally {
|
|
852
|
+
currentWrite.resolve();
|
|
853
|
+
if (entry.pendingWriteResolver === currentWrite) {
|
|
854
|
+
entry.pendingWriteResolver = undefined;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async #waitForPendingWrite(actorId: string): Promise<void> {
|
|
860
|
+
const entry = this.#actors.get(actorId);
|
|
861
|
+
if (!entry?.pendingWriteResolver) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
while (entry.pendingWriteResolver) {
|
|
866
|
+
const pending = entry.pendingWriteResolver;
|
|
867
|
+
try {
|
|
868
|
+
await pending.promise;
|
|
869
|
+
} catch {
|
|
870
|
+
// Ignore write failures to avoid blocking reads forever.
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async setActorAlarm(actorId: string, timestamp: number) {
|
|
876
|
+
const entry = this.#actors.get(actorId);
|
|
877
|
+
invariant(entry, "actor entry does not exist");
|
|
878
|
+
|
|
879
|
+
// Track generation of the actor when the write started to detect
|
|
880
|
+
// destroy/create race condition
|
|
881
|
+
const writeGeneration = entry.generation;
|
|
882
|
+
if (this.isActorStopping(actorId)) {
|
|
883
|
+
logger().info("skipping set alarm since actor stopping");
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Persist alarm to disk
|
|
888
|
+
if (this.#persist) {
|
|
889
|
+
const alarmPath = this.getActorAlarmPath(actorId);
|
|
890
|
+
const crypto = getNodeCrypto();
|
|
891
|
+
const tempPath = `${alarmPath}.tmp.${crypto.randomUUID()}`;
|
|
892
|
+
try {
|
|
893
|
+
const path = getNodePath();
|
|
894
|
+
await ensureDirectoryExists(path.dirname(alarmPath));
|
|
895
|
+
const alarmData: schema.ActorAlarm = {
|
|
896
|
+
actorId,
|
|
897
|
+
timestamp: BigInt(timestamp),
|
|
898
|
+
};
|
|
899
|
+
const data = ACTOR_ALARM_VERSIONED.serializeWithEmbeddedVersion(
|
|
900
|
+
alarmData,
|
|
901
|
+
FILE_SYSTEM_DRIVER_CURRENT_VERSION,
|
|
902
|
+
);
|
|
903
|
+
const fs = getNodeFs();
|
|
904
|
+
await fs.writeFile(tempPath, data);
|
|
905
|
+
|
|
906
|
+
if (
|
|
907
|
+
!this.isGenerationCurrentAndNotDestroyed(
|
|
908
|
+
actorId,
|
|
909
|
+
writeGeneration,
|
|
910
|
+
)
|
|
911
|
+
) {
|
|
912
|
+
logger().debug(
|
|
913
|
+
"skipping writing alarm since actor destroying or new generation",
|
|
914
|
+
);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
await fs.rename(tempPath, alarmPath);
|
|
919
|
+
} catch (error) {
|
|
920
|
+
try {
|
|
921
|
+
const fs = getNodeFs();
|
|
922
|
+
await fs.unlink(tempPath);
|
|
923
|
+
} catch {}
|
|
924
|
+
logger().error({
|
|
925
|
+
msg: "failed to write alarm",
|
|
926
|
+
actorId,
|
|
927
|
+
error,
|
|
928
|
+
});
|
|
929
|
+
throw new Error(`Failed to write alarm: ${error}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Schedule timeout
|
|
934
|
+
this.#scheduleAlarmTimeout(actorId, timestamp);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Perform the actual write operation with atomic writes
|
|
939
|
+
*/
|
|
940
|
+
async #performWrite(
|
|
941
|
+
actorId: string,
|
|
942
|
+
generation: string,
|
|
943
|
+
state: schema.ActorState,
|
|
944
|
+
): Promise<void> {
|
|
945
|
+
const dataPath = this.getActorStatePath(actorId);
|
|
946
|
+
// Generate unique temp filename to prevent any race conditions
|
|
947
|
+
const crypto = getNodeCrypto();
|
|
948
|
+
const tempPath = `${dataPath}.tmp.${crypto.randomUUID()}`;
|
|
949
|
+
|
|
950
|
+
try {
|
|
951
|
+
// Create directory if needed
|
|
952
|
+
const path = getNodePath();
|
|
953
|
+
await ensureDirectoryExists(path.dirname(dataPath));
|
|
954
|
+
|
|
955
|
+
// Convert to BARE types for serialization
|
|
956
|
+
const bareState: schema.ActorState = {
|
|
957
|
+
actorId: state.actorId,
|
|
958
|
+
name: state.name,
|
|
959
|
+
key: state.key,
|
|
960
|
+
createdAt: state.createdAt,
|
|
961
|
+
kvStorage: state.kvStorage,
|
|
962
|
+
startTs: state.startTs,
|
|
963
|
+
connectableTs: state.connectableTs,
|
|
964
|
+
sleepTs: state.sleepTs,
|
|
965
|
+
destroyTs: state.destroyTs,
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// Perform atomic write
|
|
969
|
+
const serializedState =
|
|
970
|
+
ACTOR_STATE_VERSIONED.serializeWithEmbeddedVersion(
|
|
971
|
+
bareState,
|
|
972
|
+
FILE_SYSTEM_DRIVER_CURRENT_VERSION,
|
|
973
|
+
);
|
|
974
|
+
const fs = getNodeFs();
|
|
975
|
+
await fs.writeFile(tempPath, serializedState);
|
|
976
|
+
|
|
977
|
+
if (!this.isGenerationCurrentAndNotDestroyed(actorId, generation)) {
|
|
978
|
+
logger().debug(
|
|
979
|
+
"skipping writing alarm since actor destroying or new generation",
|
|
980
|
+
);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
await fs.rename(tempPath, dataPath);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
// Cleanup temp file on error
|
|
987
|
+
try {
|
|
988
|
+
const fs = getNodeFs();
|
|
989
|
+
await fs.unlink(tempPath);
|
|
990
|
+
} catch {
|
|
991
|
+
// Ignore cleanup errors
|
|
992
|
+
}
|
|
993
|
+
logger().error({
|
|
994
|
+
msg: "failed to save actor state",
|
|
995
|
+
actorId,
|
|
996
|
+
error,
|
|
997
|
+
});
|
|
998
|
+
throw new Error(`Failed to save actor state: ${error}`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Call this method after the actor driver has been initiated.
|
|
1004
|
+
*
|
|
1005
|
+
* This will trigger all initial alarms from the file system.
|
|
1006
|
+
*
|
|
1007
|
+
* This needs to be sync since DriverConfig.actor is sync
|
|
1008
|
+
*/
|
|
1009
|
+
onRunnerStart(
|
|
1010
|
+
config: RegistryConfig,
|
|
1011
|
+
inlineClient: AnyClient,
|
|
1012
|
+
actorDriver: ActorDriver,
|
|
1013
|
+
) {
|
|
1014
|
+
if (this.#runnerParams) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Save runner params for future use
|
|
1019
|
+
this.#runnerParams = {
|
|
1020
|
+
config: config,
|
|
1021
|
+
inlineClient,
|
|
1022
|
+
actorDriver,
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
// Load alarms from disk and schedule timeouts
|
|
1026
|
+
try {
|
|
1027
|
+
this.#loadAlarmsSync();
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
logger().error({
|
|
1030
|
+
msg: "failed to load alarms on startup",
|
|
1031
|
+
error: err,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async startActor(
|
|
1037
|
+
config: RegistryConfig,
|
|
1038
|
+
inlineClient: AnyClient,
|
|
1039
|
+
actorDriver: ActorDriver,
|
|
1040
|
+
actorId: string,
|
|
1041
|
+
): Promise<AnyActorInstance> {
|
|
1042
|
+
await this.#waitForActorStop(actorId);
|
|
1043
|
+
|
|
1044
|
+
// Get the actor metadata
|
|
1045
|
+
let entry = await this.loadActor(actorId);
|
|
1046
|
+
if (!entry.state) {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`Actor does not exist and cannot be started: "${actorId}"`,
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Actor already starting
|
|
1053
|
+
if (entry.startPromise) {
|
|
1054
|
+
await entry.startPromise.promise;
|
|
1055
|
+
invariant(entry.actor, "actor should have loaded");
|
|
1056
|
+
return entry.actor;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Actor already loaded
|
|
1060
|
+
if (entry.actor) {
|
|
1061
|
+
if (entry.actor.isStopping || this.isActorStopping(actorId)) {
|
|
1062
|
+
await this.#waitForActorStop(actorId);
|
|
1063
|
+
entry = await this.loadActor(actorId);
|
|
1064
|
+
if (!entry.state) {
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
`Actor does not exist and cannot be started: "${actorId}"`,
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
} else {
|
|
1070
|
+
return entry.actor;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Create start promise
|
|
1075
|
+
entry.startPromise = promiseWithResolvers((reason) => logger().warn({ msg: "unhandled actor start promise rejection", reason }));
|
|
1076
|
+
|
|
1077
|
+
try {
|
|
1078
|
+
// Create actor
|
|
1079
|
+
const definition = lookupInRegistry(config, entry.state.name);
|
|
1080
|
+
entry.actor = await definition.instantiate();
|
|
1081
|
+
entry.lifecycleState = ActorLifecycleState.AWAKE;
|
|
1082
|
+
|
|
1083
|
+
// Start actor
|
|
1084
|
+
await entry.actor.start(
|
|
1085
|
+
actorDriver,
|
|
1086
|
+
inlineClient,
|
|
1087
|
+
actorId,
|
|
1088
|
+
entry.state.name,
|
|
1089
|
+
entry.state.key as string[],
|
|
1090
|
+
"unknown",
|
|
1091
|
+
);
|
|
1092
|
+
|
|
1093
|
+
// Update state with start timestamp
|
|
1094
|
+
// NOTE: connectableTs is always in sync with startTs since actors become connectable immediately after starting
|
|
1095
|
+
const now = BigInt(Date.now());
|
|
1096
|
+
await this.#withActorWrite(actorId, async (lockedEntry) => {
|
|
1097
|
+
if (!lockedEntry.state) {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`Actor does not exist and cannot be started: "${actorId}"`,
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
lockedEntry.state = {
|
|
1103
|
+
...lockedEntry.state,
|
|
1104
|
+
startTs: now,
|
|
1105
|
+
connectableTs: now,
|
|
1106
|
+
sleepTs: null, // Clear sleep timestamp when actor wakes up
|
|
1107
|
+
};
|
|
1108
|
+
if (this.#persist) {
|
|
1109
|
+
await this.#performWrite(
|
|
1110
|
+
actorId,
|
|
1111
|
+
lockedEntry.generation,
|
|
1112
|
+
lockedEntry.state,
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
// Finish
|
|
1118
|
+
entry.startPromise.resolve();
|
|
1119
|
+
entry.startPromise = undefined;
|
|
1120
|
+
|
|
1121
|
+
return entry.actor;
|
|
1122
|
+
} catch (innerError) {
|
|
1123
|
+
const error = new Error(
|
|
1124
|
+
`Failed to start actor ${actorId}: ${innerError}`,
|
|
1125
|
+
{ cause: innerError },
|
|
1126
|
+
);
|
|
1127
|
+
entry.startPromise?.reject(error);
|
|
1128
|
+
entry.startPromise = undefined;
|
|
1129
|
+
throw error;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
async loadActorStateOrError(actorId: string): Promise<schema.ActorState> {
|
|
1134
|
+
const state = (await this.loadActor(actorId)).state;
|
|
1135
|
+
if (!state) throw new Error(`Actor does not exist: ${actorId}`);
|
|
1136
|
+
return state;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
getActorOrError(actorId: string): ActorEntry {
|
|
1140
|
+
const entry = this.#actors.get(actorId);
|
|
1141
|
+
if (!entry) throw new Error(`No entry for actor: ${actorId}`);
|
|
1142
|
+
return entry;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
async createDatabase(actorId: string): Promise<string | undefined> {
|
|
1146
|
+
return this.getActorDbPath(actorId);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Load all persisted alarms from disk and schedule their timers.
|
|
1151
|
+
*/
|
|
1152
|
+
#loadAlarmsSync(): void {
|
|
1153
|
+
try {
|
|
1154
|
+
const fsSync = getNodeFsSync();
|
|
1155
|
+
const files = fsSync.existsSync(this.#alarmsDir)
|
|
1156
|
+
? fsSync.readdirSync(this.#alarmsDir)
|
|
1157
|
+
: [];
|
|
1158
|
+
for (const file of files) {
|
|
1159
|
+
// Skip temp files
|
|
1160
|
+
if (file.includes(".tmp.")) continue;
|
|
1161
|
+
const path = getNodePath();
|
|
1162
|
+
const fullPath = path.join(this.#alarmsDir, file);
|
|
1163
|
+
try {
|
|
1164
|
+
const buf = fsSync.readFileSync(fullPath);
|
|
1165
|
+
const alarmData =
|
|
1166
|
+
ACTOR_ALARM_VERSIONED.deserializeWithEmbeddedVersion(
|
|
1167
|
+
new Uint8Array(buf),
|
|
1168
|
+
);
|
|
1169
|
+
const timestamp = Number(alarmData.timestamp);
|
|
1170
|
+
if (Number.isFinite(timestamp)) {
|
|
1171
|
+
this.#scheduleAlarmTimeout(
|
|
1172
|
+
alarmData.actorId,
|
|
1173
|
+
timestamp,
|
|
1174
|
+
);
|
|
1175
|
+
} else {
|
|
1176
|
+
logger().debug({
|
|
1177
|
+
msg: "invalid alarm file contents",
|
|
1178
|
+
file,
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
logger().error({
|
|
1183
|
+
msg: "failed to read alarm file",
|
|
1184
|
+
file,
|
|
1185
|
+
error: stringifyError(err),
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
logger().error({
|
|
1191
|
+
msg: "failed to list alarms directory",
|
|
1192
|
+
error: err,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Schedule an alarm timer for an actor without writing to disk.
|
|
1199
|
+
*/
|
|
1200
|
+
#scheduleAlarmTimeout(actorId: string, timestamp: number) {
|
|
1201
|
+
const entry = this.#upsertEntry(actorId);
|
|
1202
|
+
|
|
1203
|
+
// If there's already an earlier alarm scheduled, do not override it.
|
|
1204
|
+
if (
|
|
1205
|
+
entry.alarmTimestamp !== undefined &&
|
|
1206
|
+
timestamp >= entry.alarmTimestamp
|
|
1207
|
+
) {
|
|
1208
|
+
logger().debug({
|
|
1209
|
+
msg: "skipping alarm schedule (later than existing)",
|
|
1210
|
+
actorId,
|
|
1211
|
+
timestamp,
|
|
1212
|
+
current: entry.alarmTimestamp,
|
|
1213
|
+
});
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
logger().debug({ msg: "scheduling alarm", actorId, timestamp });
|
|
1218
|
+
|
|
1219
|
+
// Cancel existing timeout and update the current scheduled timestamp
|
|
1220
|
+
entry.alarmTimeout?.abort();
|
|
1221
|
+
entry.alarmTimestamp = timestamp;
|
|
1222
|
+
|
|
1223
|
+
const delay = Math.max(0, timestamp - Date.now());
|
|
1224
|
+
entry.alarmTimeout = setLongTimeout(async () => {
|
|
1225
|
+
// Clear currently scheduled timestamp as this alarm is firing now
|
|
1226
|
+
entry.alarmTimestamp = undefined;
|
|
1227
|
+
// On trigger: remove persisted alarm file
|
|
1228
|
+
if (this.#persist) {
|
|
1229
|
+
try {
|
|
1230
|
+
const fs = getNodeFs();
|
|
1231
|
+
await fs.unlink(this.getActorAlarmPath(actorId));
|
|
1232
|
+
} catch (err: any) {
|
|
1233
|
+
if (err?.code !== "ENOENT") {
|
|
1234
|
+
logger().debug({
|
|
1235
|
+
msg: "failed to remove alarm file",
|
|
1236
|
+
actorId,
|
|
1237
|
+
error: stringifyError(err),
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
try {
|
|
1244
|
+
logger().debug({ msg: "triggering alarm", actorId, timestamp });
|
|
1245
|
+
|
|
1246
|
+
// Ensure actor state exists and start actor if needed
|
|
1247
|
+
const loaded = await this.loadActor(actorId);
|
|
1248
|
+
if (!loaded.state)
|
|
1249
|
+
throw new Error(`Actor does not exist: ${actorId}`);
|
|
1250
|
+
|
|
1251
|
+
// Start actor if not already running
|
|
1252
|
+
const runnerParams = this.#runnerParams;
|
|
1253
|
+
invariant(runnerParams, "missing runner params");
|
|
1254
|
+
if (!loaded.actor) {
|
|
1255
|
+
await this.startActor(
|
|
1256
|
+
runnerParams.config,
|
|
1257
|
+
runnerParams.inlineClient,
|
|
1258
|
+
runnerParams.actorDriver,
|
|
1259
|
+
actorId,
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
invariant(loaded.actor, "actor should be loaded after wake");
|
|
1264
|
+
await loaded.actor.onAlarm();
|
|
1265
|
+
} catch (err) {
|
|
1266
|
+
logger().error({
|
|
1267
|
+
msg: "failed to handle alarm",
|
|
1268
|
+
actorId,
|
|
1269
|
+
error: stringifyError(err),
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}, delay);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Cleanup stale temp files on startup (synchronous)
|
|
1277
|
+
*/
|
|
1278
|
+
#cleanupTempFilesSync(): void {
|
|
1279
|
+
try {
|
|
1280
|
+
const fsSync = getNodeFsSync();
|
|
1281
|
+
const files = fsSync.readdirSync(this.#stateDir);
|
|
1282
|
+
const tempFiles = files.filter((f) => f.includes(".tmp."));
|
|
1283
|
+
|
|
1284
|
+
const oneHourAgo = Date.now() - 3600000; // 1 hour in ms
|
|
1285
|
+
|
|
1286
|
+
for (const tempFile of tempFiles) {
|
|
1287
|
+
try {
|
|
1288
|
+
const path = getNodePath();
|
|
1289
|
+
const fullPath = path.join(this.#stateDir, tempFile);
|
|
1290
|
+
const stat = fsSync.statSync(fullPath);
|
|
1291
|
+
|
|
1292
|
+
// Remove if older than 1 hour
|
|
1293
|
+
if (stat.mtimeMs < oneHourAgo) {
|
|
1294
|
+
fsSync.unlinkSync(fullPath);
|
|
1295
|
+
logger().info({
|
|
1296
|
+
msg: "cleaned up stale temp file",
|
|
1297
|
+
file: tempFile,
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
logger().debug({
|
|
1302
|
+
msg: "failed to cleanup temp file",
|
|
1303
|
+
file: tempFile,
|
|
1304
|
+
error: err,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
logger().error({
|
|
1310
|
+
msg: "failed to read actors directory for cleanup",
|
|
1311
|
+
error: err,
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Batch put KV entries for an actor.
|
|
1318
|
+
*/
|
|
1319
|
+
async kvBatchPut(
|
|
1320
|
+
actorId: string,
|
|
1321
|
+
entries: [Uint8Array, Uint8Array][],
|
|
1322
|
+
): Promise<void> {
|
|
1323
|
+
await this.loadActor(actorId);
|
|
1324
|
+
await this.#withActorWrite(actorId, async (entry) => {
|
|
1325
|
+
if (!entry.state) {
|
|
1326
|
+
if (this.isActorStopping(actorId)) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
throw new Error(`Actor ${actorId} state not loaded`);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const db = this.#getOrCreateActorKvDatabase(actorId);
|
|
1333
|
+
const totalSize = estimateKvSize(db);
|
|
1334
|
+
validateKvEntries(entries, totalSize);
|
|
1335
|
+
this.#putKvEntriesInDb(db, entries);
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Batch get KV entries for an actor.
|
|
1341
|
+
*/
|
|
1342
|
+
async kvBatchGet(
|
|
1343
|
+
actorId: string,
|
|
1344
|
+
keys: Uint8Array[],
|
|
1345
|
+
): Promise<(Uint8Array | null)[]> {
|
|
1346
|
+
const entry = await this.loadActor(actorId);
|
|
1347
|
+
await this.#waitForPendingWrite(actorId);
|
|
1348
|
+
if (!entry.state) {
|
|
1349
|
+
if (this.isActorStopping(actorId)) {
|
|
1350
|
+
throw new Error(`Actor ${actorId} is stopping`);
|
|
1351
|
+
} else {
|
|
1352
|
+
throw new Error(`Actor ${actorId} state not loaded`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
validateKvKeys(keys);
|
|
1357
|
+
|
|
1358
|
+
const db = this.#getOrCreateActorKvDatabase(actorId);
|
|
1359
|
+
const results: (Uint8Array | null)[] = [];
|
|
1360
|
+
for (const key of keys) {
|
|
1361
|
+
const row = db.get<{ value: Uint8Array | ArrayBuffer }>(
|
|
1362
|
+
"SELECT value FROM kv WHERE key = ?",
|
|
1363
|
+
[key],
|
|
1364
|
+
);
|
|
1365
|
+
if (!row) {
|
|
1366
|
+
results.push(null);
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
results.push(ensureUint8Array(row.value, "value"));
|
|
1370
|
+
}
|
|
1371
|
+
return results;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* Batch delete KV entries for an actor.
|
|
1376
|
+
*/
|
|
1377
|
+
async kvBatchDelete(actorId: string, keys: Uint8Array[]): Promise<void> {
|
|
1378
|
+
await this.loadActor(actorId);
|
|
1379
|
+
await this.#withActorWrite(actorId, async (entry) => {
|
|
1380
|
+
if (!entry.state) {
|
|
1381
|
+
if (this.isActorStopping(actorId)) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
throw new Error(`Actor ${actorId} state not loaded`);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
if (keys.length === 0) {
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
validateKvKeys(keys);
|
|
1391
|
+
|
|
1392
|
+
const db = this.#getOrCreateActorKvDatabase(actorId);
|
|
1393
|
+
db.exec("BEGIN");
|
|
1394
|
+
try {
|
|
1395
|
+
for (const key of keys) {
|
|
1396
|
+
db.run("DELETE FROM kv WHERE key = ?", [key]);
|
|
1397
|
+
}
|
|
1398
|
+
db.exec("COMMIT");
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
try {
|
|
1401
|
+
db.exec("ROLLBACK");
|
|
1402
|
+
} catch {
|
|
1403
|
+
// Ignore rollback errors, original error is more actionable.
|
|
1404
|
+
}
|
|
1405
|
+
throw error;
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* List KV entries with a given prefix for an actor.
|
|
1412
|
+
*/
|
|
1413
|
+
async kvListPrefix(
|
|
1414
|
+
actorId: string,
|
|
1415
|
+
prefix: Uint8Array,
|
|
1416
|
+
): Promise<[Uint8Array, Uint8Array][]> {
|
|
1417
|
+
const entry = await this.loadActor(actorId);
|
|
1418
|
+
await this.#waitForPendingWrite(actorId);
|
|
1419
|
+
if (!entry.state) {
|
|
1420
|
+
if (this.isActorStopping(actorId)) {
|
|
1421
|
+
throw new Error(`Actor ${actorId} is destroying`);
|
|
1422
|
+
} else {
|
|
1423
|
+
throw new Error(`Actor ${actorId} state not loaded`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
validateKvKey(prefix, "prefix key");
|
|
1427
|
+
|
|
1428
|
+
const db = this.#getOrCreateActorKvDatabase(actorId);
|
|
1429
|
+
const upperBound = computePrefixUpperBound(prefix);
|
|
1430
|
+
const rows = upperBound
|
|
1431
|
+
? db.all<{ key: Uint8Array | ArrayBuffer; value: Uint8Array | ArrayBuffer }>(
|
|
1432
|
+
"SELECT key, value FROM kv WHERE key >= ? AND key < ? ORDER BY key ASC",
|
|
1433
|
+
[prefix, upperBound],
|
|
1434
|
+
)
|
|
1435
|
+
: db.all<{ key: Uint8Array | ArrayBuffer; value: Uint8Array | ArrayBuffer }>(
|
|
1436
|
+
"SELECT key, value FROM kv WHERE key >= ? ORDER BY key ASC",
|
|
1437
|
+
[prefix],
|
|
1438
|
+
);
|
|
1439
|
+
|
|
1440
|
+
return rows.map((row) => [
|
|
1441
|
+
ensureUint8Array(row.key, "key"),
|
|
1442
|
+
ensureUint8Array(row.value, "value"),
|
|
1443
|
+
]);
|
|
1444
|
+
}
|
|
1445
|
+
}
|