@effect-app/infra 4.0.0-beta.26 → 4.0.0-beta.261
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/CHANGELOG.md +1973 -0
- package/_check.sh +1 -1
- package/dist/CUPS.d.ts +30 -11
- package/dist/CUPS.d.ts.map +1 -1
- package/dist/CUPS.js +35 -14
- package/dist/ClusterCosmos.d.ts +64 -0
- package/dist/ClusterCosmos.d.ts.map +1 -0
- package/dist/ClusterCosmos.js +501 -0
- package/dist/ClusterServiceBus.d.ts +67 -0
- package/dist/ClusterServiceBus.d.ts.map +1 -0
- package/dist/ClusterServiceBus.js +82 -0
- package/dist/ContextProvider.d.ts +34 -0
- package/dist/ContextProvider.d.ts.map +1 -0
- package/dist/ContextProvider.js +40 -0
- package/dist/Emailer/Sendgrid.d.ts +111 -147
- package/dist/Emailer/Sendgrid.d.ts.map +1 -1
- package/dist/Emailer/Sendgrid.js +24 -19
- package/dist/Emailer/fake.d.ts +2 -2
- package/dist/Emailer/fake.d.ts.map +1 -1
- package/dist/Emailer/fake.js +4 -4
- package/dist/MainFiberSet.d.ts +12 -9
- package/dist/MainFiberSet.d.ts.map +1 -1
- package/dist/MainFiberSet.js +10 -6
- package/dist/QueueMaker/SQLQueue.d.ts +8 -9
- package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
- package/dist/QueueMaker/SQLQueue.js +138 -120
- package/dist/QueueMaker/errors.d.ts +5 -3
- package/dist/QueueMaker/errors.d.ts.map +1 -1
- package/dist/QueueMaker/errors.js +4 -2
- package/dist/QueueMaker/memQueue.d.ts +10 -6
- package/dist/QueueMaker/memQueue.d.ts.map +1 -1
- package/dist/QueueMaker/memQueue.js +84 -68
- package/dist/QueueMaker/sbqueue.d.ts +9 -5
- package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
- package/dist/QueueMaker/sbqueue.js +60 -58
- package/dist/RequestFiberSet.d.ts +10 -7
- package/dist/RequestFiberSet.d.ts.map +1 -1
- package/dist/RequestFiberSet.js +13 -8
- package/dist/SQL/Model.d.ts +468 -0
- package/dist/SQL/Model.d.ts.map +1 -0
- package/dist/SQL/Model.js +469 -0
- package/dist/SQL.d.ts +2 -0
- package/dist/SQL.d.ts.map +1 -0
- package/dist/{adapters/SQL.js → SQL.js} +1 -1
- package/dist/ServiceBus.d.ts +61 -0
- package/dist/ServiceBus.d.ts.map +1 -0
- package/dist/ServiceBus.js +108 -0
- package/dist/Store/Cosmos/query.d.ts +15 -4
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +179 -41
- package/dist/Store/Cosmos.d.ts +3 -3
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +344 -246
- package/dist/Store/Disk.d.ts +5 -5
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +78 -38
- package/dist/Store/Memory.d.ts +7 -10
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +326 -66
- package/dist/Store/SQL/Pg.d.ts +4 -0
- package/dist/Store/SQL/Pg.d.ts.map +1 -0
- package/dist/Store/SQL/Pg.js +232 -0
- package/dist/Store/SQL/query.d.ts +49 -0
- package/dist/Store/SQL/query.d.ts.map +1 -0
- package/dist/Store/SQL/query.js +527 -0
- package/dist/Store/SQL.d.ts +21 -0
- package/dist/Store/SQL.d.ts.map +1 -0
- package/dist/Store/SQL.js +449 -0
- package/dist/Store/codeFilter.d.ts +5 -5
- package/dist/Store/codeFilter.d.ts.map +1 -1
- package/dist/Store/codeFilter.js +6 -3
- package/dist/Store/index.d.ts +7 -5
- package/dist/Store/index.d.ts.map +1 -1
- package/dist/Store/index.js +18 -5
- package/dist/Store/utils.d.ts +4 -3
- package/dist/Store/utils.d.ts.map +1 -1
- package/dist/Store/utils.js +5 -5
- package/dist/WorkflowEngineCosmos.d.ts +29 -0
- package/dist/WorkflowEngineCosmos.d.ts.map +1 -0
- package/dist/WorkflowEngineCosmos.js +521 -0
- package/dist/WorkflowEngineSqlite.d.ts +24 -0
- package/dist/WorkflowEngineSqlite.d.ts.map +1 -0
- package/dist/WorkflowEngineSqlite.js +550 -0
- package/dist/arbs.d.ts +2 -2
- package/dist/arbs.d.ts.map +1 -1
- package/dist/arbs.js +5 -3
- package/dist/codec.d.ts +5 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +5 -0
- package/dist/cosmos-client.d.ts +16 -0
- package/dist/cosmos-client.d.ts.map +1 -0
- package/dist/cosmos-client.js +11 -0
- package/dist/errorReporter.d.ts +7 -5
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +23 -27
- package/dist/errors.d.ts +1 -1
- package/dist/fileUtil.d.ts +2 -2
- package/dist/fileUtil.d.ts.map +1 -1
- package/dist/fileUtil.js +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/internal/RequestContextMiddleware.d.ts +5 -0
- package/dist/internal/RequestContextMiddleware.d.ts.map +1 -0
- package/dist/internal/RequestContextMiddleware.js +45 -0
- package/dist/internal/auth.d.ts +53 -0
- package/dist/internal/auth.d.ts.map +1 -0
- package/dist/internal/auth.js +180 -0
- package/dist/internal/events.d.ts +11 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +49 -0
- package/dist/internal/health.d.ts +3 -0
- package/dist/internal/health.d.ts.map +1 -0
- package/dist/internal/health.js +5 -0
- package/dist/layerUtils.d.ts +32 -0
- package/dist/layerUtils.d.ts.map +1 -0
- package/dist/layerUtils.js +17 -0
- package/dist/logger/jsonLogger.d.ts +2 -2
- package/dist/logger/jsonLogger.d.ts.map +1 -1
- package/dist/logger/jsonLogger.js +5 -3
- package/dist/logger/logFmtLogger.d.ts +2 -2
- package/dist/logger/logFmtLogger.d.ts.map +1 -1
- package/dist/logger/logFmtLogger.js +3 -3
- package/dist/logger/shared.d.ts +3 -3
- package/dist/logger/shared.d.ts.map +1 -1
- package/dist/logger/shared.js +5 -5
- package/dist/logger.d.ts +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/memQueue.d.ts +15 -0
- package/dist/memQueue.d.ts.map +1 -0
- package/dist/memQueue.js +21 -0
- package/dist/middlewares.d.ts +10 -0
- package/dist/middlewares.d.ts.map +1 -0
- package/dist/{api/middlewares.js → middlewares.js} +1 -1
- package/dist/mongo-client.d.ts +11 -0
- package/dist/mongo-client.d.ts.map +1 -0
- package/dist/mongo-client.js +15 -0
- package/dist/otel.d.ts +75 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +65 -0
- package/dist/rateLimit.d.ts +12 -4
- package/dist/rateLimit.d.ts.map +1 -1
- package/dist/rateLimit.js +7 -12
- package/dist/redis-client.d.ts +42 -0
- package/dist/redis-client.d.ts.map +1 -0
- package/dist/redis-client.js +98 -0
- package/dist/reportError.d.ts +4 -0
- package/dist/reportError.d.ts.map +1 -0
- package/dist/reportError.js +28 -0
- package/dist/routing/middleware/RouterMiddleware.d.ts +16 -0
- package/dist/routing/middleware/RouterMiddleware.d.ts.map +1 -0
- package/dist/{api/routing → routing}/middleware/RouterMiddleware.js +1 -1
- package/dist/routing/middleware/middleware.d.ts +48 -0
- package/dist/routing/middleware/middleware.d.ts.map +1 -0
- package/dist/routing/middleware/middleware.js +128 -0
- package/dist/routing/middleware.d.ts +3 -0
- package/dist/routing/middleware.d.ts.map +1 -0
- package/dist/{api/routing → routing}/middleware.js +1 -2
- package/dist/routing/schema/jwt.d.ts +4 -0
- package/dist/routing/schema/jwt.d.ts.map +1 -0
- package/dist/routing/schema/jwt.js +13 -0
- package/dist/routing/tsort.d.ts +8 -0
- package/dist/routing/tsort.d.ts.map +1 -0
- package/dist/routing/tsort.js +51 -0
- package/dist/routing/utils.d.ts +19 -0
- package/dist/routing/utils.d.ts.map +1 -0
- package/dist/routing/utils.js +45 -0
- package/dist/routing.d.ts +184 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +236 -0
- package/dist/test.d.ts +3 -3
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +2 -2
- package/dist/util.d.ts +3 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +14 -0
- package/dist/vitest.d.ts +1 -1
- package/docs/cluster-storage.md +26 -0
- package/docs/workflow-engine.md +262 -0
- package/examples/query.ts +47 -39
- package/package.json +31 -345
- package/run.sh +1 -0
- package/src/CUPS.ts +52 -13
- package/src/ClusterCosmos.ts +984 -0
- package/src/ClusterServiceBus.ts +242 -0
- package/src/{api/ContextProvider.ts → ContextProvider.ts} +19 -16
- package/src/Emailer/Sendgrid.ts +82 -59
- package/src/Emailer/fake.ts +3 -3
- package/src/MainFiberSet.ts +12 -10
- package/src/QueueMaker/SQLQueue.ts +153 -156
- package/src/QueueMaker/errors.ts +3 -1
- package/src/QueueMaker/memQueue.ts +113 -107
- package/src/QueueMaker/sbqueue.ts +78 -90
- package/src/RequestFiberSet.ts +13 -8
- package/src/{adapters/SQL → SQL}/Model.ts +42 -41
- package/src/ServiceBus.ts +219 -0
- package/src/Store/Cosmos/query.ts +216 -52
- package/src/Store/Cosmos.ts +493 -353
- package/src/Store/Disk.ts +109 -69
- package/src/Store/Memory.ts +365 -96
- package/src/Store/SQL/Pg.ts +363 -0
- package/src/Store/SQL/query.ts +603 -0
- package/src/Store/SQL.ts +735 -0
- package/src/Store/codeFilter.ts +8 -5
- package/src/Store/index.ts +21 -6
- package/src/Store/utils.ts +26 -24
- package/src/WorkflowEngineCosmos.ts +719 -0
- package/src/WorkflowEngineSqlite.ts +813 -0
- package/src/arbs.ts +5 -3
- package/src/{adapters/cosmos-client.ts → cosmos-client.ts} +5 -3
- package/src/errorReporter.ts +66 -76
- package/src/fileUtil.ts +1 -1
- package/src/index.ts +2 -1
- package/src/{api/internal → internal}/RequestContextMiddleware.ts +23 -6
- package/src/internal/auth.ts +272 -0
- package/src/{api/internal → internal}/events.ts +22 -13
- package/src/{api/layerUtils.ts → layerUtils.ts} +14 -10
- package/src/logger/jsonLogger.ts +4 -2
- package/src/logger/logFmtLogger.ts +2 -2
- package/src/logger/shared.ts +5 -4
- package/src/{adapters/memQueue.ts → memQueue.ts} +5 -4
- package/src/{adapters/mongo-client.ts → mongo-client.ts} +4 -2
- package/src/otel.ts +152 -0
- package/src/rateLimit.ts +34 -23
- package/src/{adapters/redis-client.ts → redis-client.ts} +7 -3
- package/src/{api/reportError.ts → reportError.ts} +3 -2
- package/src/{api/routing → routing}/middleware/RouterMiddleware.ts +5 -4
- package/src/{api/routing → routing}/middleware/middleware.ts +62 -17
- package/src/routing/middleware.ts +4 -0
- package/src/{api/routing → routing}/schema/jwt.ts +2 -1
- package/src/{api/routing → routing}/utils.ts +2 -1
- package/src/routing.ts +768 -0
- package/src/test.ts +2 -2
- package/test/auth.test.ts +101 -0
- package/test/cluster-cosmos.test.ts +590 -0
- package/test/cluster-servicebus.test.ts +180 -0
- package/test/cluster-sqlite.test.ts +207 -0
- package/test/contextProvider.test.ts +15 -12
- package/test/controller.test.ts +28 -32
- package/test/cosmos-query.test.ts +159 -0
- package/test/dist/_check-agg-infer.test-d.d.ts +2 -0
- package/test/dist/_check-agg-infer.test-d.d.ts.map +1 -0
- package/test/dist/_check-agg-infer.test-d.js +19 -0
- package/test/dist/_check-proj-infer.test-d.d.ts +2 -0
- package/test/dist/_check-proj-infer.test-d.d.ts.map +1 -0
- package/test/dist/_check-proj-infer.test-d.js +16 -0
- package/test/dist/_check-tighten.test-d.d.ts +2 -0
- package/test/dist/_check-tighten.test-d.d.ts.map +1 -0
- package/test/dist/_check-tighten.test-d.js +21 -0
- package/test/dist/auth.test.d.ts.map +1 -0
- package/test/dist/cluster-cosmos.test.d.ts.map +1 -0
- package/test/dist/cluster-servicebus.test.d.ts.map +1 -0
- package/test/dist/cluster-sqlite.test.d.ts.map +1 -0
- package/test/dist/contextProvider.test.d.ts.map +1 -1
- package/test/dist/controller.test.d.ts.map +1 -1
- package/test/dist/cosmos-query.test.d.ts.map +1 -0
- package/test/dist/date-query.test.d.ts.map +1 -0
- package/test/dist/fixtures.d.ts +30 -12
- package/test/dist/fixtures.d.ts.map +1 -1
- package/test/dist/fixtures.js +17 -10
- package/test/dist/query.test.d.ts.map +1 -1
- package/test/dist/rawQuery.test.d.ts.map +1 -1
- package/test/dist/repository-ext.test.d.ts.map +1 -0
- package/test/dist/requires.test.d.ts.map +1 -1
- package/test/dist/router-generator.test.d.ts.map +1 -0
- package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
- package/test/dist/rpc-context-map-streaming.test.d.ts.map +1 -0
- package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
- package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
- package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
- package/test/dist/sql-store.test.d.ts.map +1 -0
- package/test/dist/workflow-engine-cosmos.test.d.ts.map +1 -0
- package/test/dist/workflow-engine-sqlite.test.d.ts.map +1 -0
- package/test/fixtures.ts +16 -9
- package/test/layerUtils.test.ts +2 -2
- package/test/query.test.ts +905 -40
- package/test/rawQuery.test.ts +340 -22
- package/test/repository-ext.test.ts +62 -0
- package/test/requires.test.ts +10 -5
- package/test/router-generator.test.ts +187 -0
- package/test/routing-interruptibility.test.ts +66 -0
- package/test/rpc-context-map-streaming.test.ts +262 -0
- package/test/rpc-e2e-invalidation.test.ts +256 -0
- package/test/rpc-multi-middleware.test.ts +85 -10
- package/test/rpc-stream-fullstack.test.ts +304 -0
- package/test/sql-store.test.ts +1711 -0
- package/test/validateSample.test.ts +26 -14
- package/test/workflow-engine-cosmos.test.ts +354 -0
- package/test/workflow-engine-sqlite.test.ts +299 -0
- package/tsconfig.examples.json +1 -1
- package/tsconfig.json +2 -1
- package/tsconfig.json.bak +2 -2
- package/tsconfig.src.json +35 -35
- package/tsconfig.test.json +2 -2
- package/dist/Emailer/service.d.ts +0 -55
- package/dist/Emailer/service.d.ts.map +0 -1
- package/dist/Emailer/service.js +0 -6
- package/dist/Emailer.d.ts +0 -2
- package/dist/Emailer.d.ts.map +0 -1
- package/dist/Emailer.js +0 -2
- package/dist/Model/Repository/ext.d.ts +0 -41
- package/dist/Model/Repository/ext.d.ts.map +0 -1
- package/dist/Model/Repository/ext.js +0 -65
- package/dist/Model/Repository/internal/internal.d.ts +0 -59
- package/dist/Model/Repository/internal/internal.d.ts.map +0 -1
- package/dist/Model/Repository/internal/internal.js +0 -316
- package/dist/Model/Repository/legacy.d.ts +0 -19
- package/dist/Model/Repository/legacy.d.ts.map +0 -1
- package/dist/Model/Repository/legacy.js +0 -2
- package/dist/Model/Repository/makeRepo.d.ts +0 -49
- package/dist/Model/Repository/makeRepo.d.ts.map +0 -1
- package/dist/Model/Repository/makeRepo.js +0 -24
- package/dist/Model/Repository/service.d.ts +0 -89
- package/dist/Model/Repository/service.d.ts.map +0 -1
- package/dist/Model/Repository/service.js +0 -2
- package/dist/Model/Repository/validation.d.ts +0 -42
- package/dist/Model/Repository/validation.d.ts.map +0 -1
- package/dist/Model/Repository/validation.js +0 -32
- package/dist/Model/Repository.d.ts +0 -6
- package/dist/Model/Repository.d.ts.map +0 -1
- package/dist/Model/Repository.js +0 -6
- package/dist/Model/dsl.d.ts +0 -32
- package/dist/Model/dsl.d.ts.map +0 -1
- package/dist/Model/dsl.js +0 -44
- package/dist/Model/filter/filterApi.d.ts +0 -30
- package/dist/Model/filter/filterApi.d.ts.map +0 -1
- package/dist/Model/filter/filterApi.js +0 -2
- package/dist/Model/filter/types/errors.d.ts +0 -29
- package/dist/Model/filter/types/errors.d.ts.map +0 -1
- package/dist/Model/filter/types/errors.js +0 -2
- package/dist/Model/filter/types/fields.d.ts +0 -15
- package/dist/Model/filter/types/fields.d.ts.map +0 -1
- package/dist/Model/filter/types/fields.js +0 -2
- package/dist/Model/filter/types/path/common.d.ts +0 -316
- package/dist/Model/filter/types/path/common.d.ts.map +0 -1
- package/dist/Model/filter/types/path/common.js +0 -2
- package/dist/Model/filter/types/path/eager.d.ts +0 -95
- package/dist/Model/filter/types/path/eager.d.ts.map +0 -1
- package/dist/Model/filter/types/path/eager.js +0 -31
- package/dist/Model/filter/types/path/index.d.ts +0 -4
- package/dist/Model/filter/types/path/index.d.ts.map +0 -1
- package/dist/Model/filter/types/path/index.js +0 -3
- package/dist/Model/filter/types/utils.d.ts +0 -79
- package/dist/Model/filter/types/utils.d.ts.map +0 -1
- package/dist/Model/filter/types/utils.js +0 -2
- package/dist/Model/filter/types/validator.d.ts +0 -30
- package/dist/Model/filter/types/validator.d.ts.map +0 -1
- package/dist/Model/filter/types/validator.js +0 -2
- package/dist/Model/filter/types.d.ts +0 -5
- package/dist/Model/filter/types.d.ts.map +0 -1
- package/dist/Model/filter/types.js +0 -7
- package/dist/Model/query/dsl.d.ts +0 -248
- package/dist/Model/query/dsl.d.ts.map +0 -1
- package/dist/Model/query/dsl.js +0 -104
- package/dist/Model/query/new-kid-interpreter.d.ts +0 -28
- package/dist/Model/query/new-kid-interpreter.d.ts.map +0 -1
- package/dist/Model/query/new-kid-interpreter.js +0 -165
- package/dist/Model/query.d.ts +0 -15
- package/dist/Model/query.d.ts.map +0 -1
- package/dist/Model/query.js +0 -3
- package/dist/Model.d.ts +0 -4
- package/dist/Model.d.ts.map +0 -1
- package/dist/Model.js +0 -4
- package/dist/Operations.d.ts +0 -55
- package/dist/Operations.d.ts.map +0 -1
- package/dist/Operations.js +0 -102
- package/dist/OperationsRepo.d.ts +0 -41
- package/dist/OperationsRepo.d.ts.map +0 -1
- package/dist/OperationsRepo.js +0 -14
- package/dist/QueueMaker/service.d.ts +0 -11
- package/dist/QueueMaker/service.d.ts.map +0 -1
- package/dist/QueueMaker/service.js +0 -4
- package/dist/RequestContext.d.ts +0 -63
- package/dist/RequestContext.d.ts.map +0 -1
- package/dist/RequestContext.js +0 -49
- package/dist/Store/ContextMapContainer.d.ts +0 -14
- package/dist/Store/ContextMapContainer.d.ts.map +0 -1
- package/dist/Store/ContextMapContainer.js +0 -16
- package/dist/Store/service.d.ts +0 -108
- package/dist/Store/service.d.ts.map +0 -1
- package/dist/Store/service.js +0 -71
- package/dist/Store.d.ts +0 -2
- package/dist/Store.d.ts.map +0 -1
- package/dist/Store.js +0 -2
- package/dist/adapters/SQL/Model.d.ts +0 -479
- package/dist/adapters/SQL/Model.d.ts.map +0 -1
- package/dist/adapters/SQL/Model.js +0 -478
- package/dist/adapters/SQL.d.ts +0 -2
- package/dist/adapters/SQL.d.ts.map +0 -1
- package/dist/adapters/ServiceBus.d.ts +0 -58
- package/dist/adapters/ServiceBus.d.ts.map +0 -1
- package/dist/adapters/ServiceBus.js +0 -99
- package/dist/adapters/cosmos-client.d.ts +0 -14
- package/dist/adapters/cosmos-client.d.ts.map +0 -1
- package/dist/adapters/cosmos-client.js +0 -9
- package/dist/adapters/index.d.ts +0 -2
- package/dist/adapters/index.d.ts.map +0 -1
- package/dist/adapters/index.js +0 -2
- package/dist/adapters/logger.d.ts +0 -9
- package/dist/adapters/logger.d.ts.map +0 -1
- package/dist/adapters/logger.js +0 -3
- package/dist/adapters/memQueue.d.ts +0 -13
- package/dist/adapters/memQueue.d.ts.map +0 -1
- package/dist/adapters/memQueue.js +0 -20
- package/dist/adapters/mongo-client.d.ts +0 -10
- package/dist/adapters/mongo-client.d.ts.map +0 -1
- package/dist/adapters/mongo-client.js +0 -13
- package/dist/adapters/redis-client.d.ts +0 -39
- package/dist/adapters/redis-client.d.ts.map +0 -1
- package/dist/adapters/redis-client.js +0 -94
- package/dist/api/ContextProvider.d.ts +0 -31
- package/dist/api/ContextProvider.d.ts.map +0 -1
- package/dist/api/ContextProvider.js +0 -38
- package/dist/api/codec.d.ts +0 -5
- package/dist/api/codec.d.ts.map +0 -1
- package/dist/api/codec.js +0 -5
- package/dist/api/internal/RequestContextMiddleware.d.ts +0 -5
- package/dist/api/internal/RequestContextMiddleware.d.ts.map +0 -1
- package/dist/api/internal/RequestContextMiddleware.js +0 -35
- package/dist/api/internal/auth.d.ts +0 -15
- package/dist/api/internal/auth.d.ts.map +0 -1
- package/dist/api/internal/auth.js +0 -47
- package/dist/api/internal/events.d.ts +0 -9
- package/dist/api/internal/events.d.ts.map +0 -1
- package/dist/api/internal/events.js +0 -42
- package/dist/api/internal/health.d.ts +0 -3
- package/dist/api/internal/health.d.ts.map +0 -1
- package/dist/api/internal/health.js +0 -5
- package/dist/api/layerUtils.d.ts +0 -24
- package/dist/api/layerUtils.d.ts.map +0 -1
- package/dist/api/layerUtils.js +0 -16
- package/dist/api/middlewares.d.ts +0 -10
- package/dist/api/middlewares.d.ts.map +0 -1
- package/dist/api/reportError.d.ts +0 -4
- package/dist/api/reportError.d.ts.map +0 -1
- package/dist/api/reportError.js +0 -27
- package/dist/api/routing/middleware/RouterMiddleware.d.ts +0 -15
- package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +0 -1
- package/dist/api/routing/middleware/middleware.d.ts +0 -9
- package/dist/api/routing/middleware/middleware.d.ts.map +0 -1
- package/dist/api/routing/middleware/middleware.js +0 -92
- package/dist/api/routing/middleware.d.ts +0 -4
- package/dist/api/routing/middleware.d.ts.map +0 -1
- package/dist/api/routing/schema/jwt.d.ts +0 -4
- package/dist/api/routing/schema/jwt.d.ts.map +0 -1
- package/dist/api/routing/schema/jwt.js +0 -12
- package/dist/api/routing/tsort.d.ts +0 -8
- package/dist/api/routing/tsort.d.ts.map +0 -1
- package/dist/api/routing/tsort.js +0 -51
- package/dist/api/routing/utils.d.ts +0 -19
- package/dist/api/routing/utils.d.ts.map +0 -1
- package/dist/api/routing/utils.js +0 -44
- package/dist/api/routing.d.ts +0 -138
- package/dist/api/routing.d.ts.map +0 -1
- package/dist/api/routing.js +0 -166
- package/dist/api/setupRequest.d.ts +0 -12
- package/dist/api/setupRequest.d.ts.map +0 -1
- package/dist/api/setupRequest.js +0 -44
- package/dist/api/util.d.ts +0 -3
- package/dist/api/util.d.ts.map +0 -1
- package/dist/api/util.js +0 -14
- package/eslint.config.mjs +0 -24
- package/src/Emailer/service.ts +0 -52
- package/src/Emailer.ts +0 -1
- package/src/Model/Repository/ext.ts +0 -283
- package/src/Model/Repository/internal/internal.ts +0 -577
- package/src/Model/Repository/legacy.ts +0 -27
- package/src/Model/Repository/makeRepo.ts +0 -139
- package/src/Model/Repository/service.ts +0 -627
- package/src/Model/Repository/validation.ts +0 -31
- package/src/Model/Repository.ts +0 -5
- package/src/Model/dsl.ts +0 -128
- package/src/Model/filter/filterApi.ts +0 -60
- package/src/Model/filter/types/errors.ts +0 -47
- package/src/Model/filter/types/fields.ts +0 -50
- package/src/Model/filter/types/path/common.ts +0 -404
- package/src/Model/filter/types/path/eager.ts +0 -298
- package/src/Model/filter/types/path/index.ts +0 -4
- package/src/Model/filter/types/utils.ts +0 -128
- package/src/Model/filter/types/validator.ts +0 -46
- package/src/Model/filter/types.ts +0 -6
- package/src/Model/query/dsl.ts +0 -2110
- package/src/Model/query/new-kid-interpreter.ts +0 -210
- package/src/Model/query.ts +0 -13
- package/src/Model.ts +0 -3
- package/src/Operations.ts +0 -235
- package/src/OperationsRepo.ts +0 -16
- package/src/QueueMaker/service.ts +0 -17
- package/src/RequestContext.ts +0 -63
- package/src/Store/ContextMapContainer.ts +0 -20
- package/src/Store/service.ts +0 -184
- package/src/Store.ts +0 -1
- package/src/adapters/ServiceBus.ts +0 -209
- package/src/adapters/index.ts +0 -0
- package/src/adapters/logger.ts +0 -3
- package/src/api/internal/auth.ts +0 -68
- package/src/api/routing/middleware.ts +0 -6
- package/src/api/routing.ts +0 -598
- package/src/api/setupRequest.ts +0 -84
- /package/src/{adapters/SQL.ts → SQL.ts} +0 -0
- /package/src/{api/codec.ts → codec.ts} +0 -0
- /package/src/{api/internal → internal}/health.ts +0 -0
- /package/src/{api/middlewares.ts → middlewares.ts} +0 -0
- /package/src/{api/routing → routing}/tsort.ts +0 -0
- /package/src/{api/util.ts → util.ts} +0 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite backed {@link WorkflowEngine} implementation.
|
|
3
|
+
*
|
|
4
|
+
* Persists workflow state across four tables:
|
|
5
|
+
* - `workflow_exec` — one row per execution; tracks status, lease, etag
|
|
6
|
+
* - `workflow_activity` — recorded activity results keyed by (exec, name, attempt)
|
|
7
|
+
* - `workflow_deferred` — durable deferred completions keyed by (exec, name)
|
|
8
|
+
* - `workflow_clock` — scheduled clocks with `fire_at`
|
|
9
|
+
*
|
|
10
|
+
* Atomicity: multi-statement operations are wrapped in `sql.withTransaction`
|
|
11
|
+
* (BEGIN/COMMIT) so concurrent writers do not observe partial state.
|
|
12
|
+
*
|
|
13
|
+
* Optimistic concurrency:
|
|
14
|
+
* - exec state transitions use `UPDATE ... WHERE etag = ? RETURNING etag`;
|
|
15
|
+
* a zero-row result is an `OptimisticConcurrencyException`.
|
|
16
|
+
* - activity / deferred / clock inserts use `INSERT ... ON CONFLICT DO
|
|
17
|
+
* NOTHING RETURNING ...` for first-writer-wins semantics across drivers.
|
|
18
|
+
*
|
|
19
|
+
* Durability — everything that crosses the storage boundary is round-tripped
|
|
20
|
+
* through schema codecs (`S.fromJsonString(S.toCodecJson(...))`), exactly like
|
|
21
|
+
* the cluster engine:
|
|
22
|
+
*
|
|
23
|
+
* - The workflow payload and the top-level `Workflow.Result` are encoded with
|
|
24
|
+
* the workflow's own `payloadSchema` / `successSchema` / `errorSchema`, so
|
|
25
|
+
* typed values (dates, branded ids, schema classes) survive a restart.
|
|
26
|
+
* - Activity results flow through the engine already encoded, so they are
|
|
27
|
+
* persisted with an opaque `Workflow.Result({ success: AnyOrVoid, error:
|
|
28
|
+
* AnyOrVoid })` codec — same trick the cluster `ActivityRpc` uses.
|
|
29
|
+
* - Durable-deferred exits use an opaque `Exit` codec.
|
|
30
|
+
*
|
|
31
|
+
* Crash recovery: each driver holds a time-bound lease on the exec row,
|
|
32
|
+
* renewed by a heartbeat fiber. A scope-bound recovery poller re-drives any
|
|
33
|
+
* exec whose lease has lapsed. A clock poller fires due clocks even when
|
|
34
|
+
* the in-process timer is missing (e.g. after a restart).
|
|
35
|
+
*/
|
|
36
|
+
import * as Effect from "effect-app/Effect"
|
|
37
|
+
import * as Layer from "effect-app/Layer"
|
|
38
|
+
import * as Option from "effect-app/Option"
|
|
39
|
+
import * as S from "effect-app/Schema"
|
|
40
|
+
import * as Duration from "effect/Duration"
|
|
41
|
+
import * as Exit from "effect/Exit"
|
|
42
|
+
import * as Fiber from "effect/Fiber"
|
|
43
|
+
import * as FiberMap from "effect/FiberMap"
|
|
44
|
+
import * as Schedule from "effect/Schedule"
|
|
45
|
+
import type * as Scope from "effect/Scope"
|
|
46
|
+
import { SqlClient } from "effect/unstable/sql"
|
|
47
|
+
import * as Workflow from "effect/unstable/workflow/Workflow"
|
|
48
|
+
import { type Encoded, makeUnsafe, WorkflowEngine, WorkflowInstance } from "effect/unstable/workflow/WorkflowEngine"
|
|
49
|
+
import { randomUUID } from "node:crypto"
|
|
50
|
+
import { OptimisticConcurrencyException } from "./errors.js"
|
|
51
|
+
import { annotateDb } from "./otel.js"
|
|
52
|
+
|
|
53
|
+
export interface WorkflowEngineSqliteConfig {
|
|
54
|
+
/** Optional prefix for table names (e.g. `tenant_`). */
|
|
55
|
+
readonly prefix?: string
|
|
56
|
+
/** Lease duration before a claim is considered stale. Default 30s. */
|
|
57
|
+
readonly leaseTtl?: Duration.Duration
|
|
58
|
+
/** Renewal cadence — should be < leaseTtl. Default 10s. */
|
|
59
|
+
readonly heartbeatInterval?: Duration.Duration
|
|
60
|
+
/** Cadence for scanning stale leases. Default 15s. Set to `Duration.zero` to disable. */
|
|
61
|
+
readonly recoveryInterval?: Duration.Duration
|
|
62
|
+
/** Cadence for scanning due clocks. Default 5s. Set to `Duration.zero` to disable. */
|
|
63
|
+
readonly clockPollInterval?: Duration.Duration
|
|
64
|
+
/** Stable worker identity; defaults to a random UUID per process. */
|
|
65
|
+
readonly workerId?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type ExecStatus = "running" | "complete" | "interrupted"
|
|
69
|
+
|
|
70
|
+
interface ExecRow {
|
|
71
|
+
readonly execution_id: string
|
|
72
|
+
readonly workflow_name: string
|
|
73
|
+
/** Schema-encoded (JSON string) workflow payload. */
|
|
74
|
+
readonly payload: string
|
|
75
|
+
readonly parent: string | null
|
|
76
|
+
readonly status: ExecStatus
|
|
77
|
+
readonly suspended: number
|
|
78
|
+
readonly interrupted: number
|
|
79
|
+
/** Schema-encoded (JSON string) top-level `Workflow.Result`, set on completion. */
|
|
80
|
+
readonly completed_result: string | null
|
|
81
|
+
readonly worker: string | null
|
|
82
|
+
readonly lease_expires_at: number | null
|
|
83
|
+
readonly etag: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ExecState {
|
|
87
|
+
readonly executionId: string
|
|
88
|
+
readonly workflowName: string
|
|
89
|
+
readonly payload: string
|
|
90
|
+
readonly parent: string | undefined
|
|
91
|
+
readonly status: ExecStatus
|
|
92
|
+
readonly suspended: boolean
|
|
93
|
+
readonly interrupted: boolean
|
|
94
|
+
readonly completedResult: string | undefined
|
|
95
|
+
readonly worker: string | undefined
|
|
96
|
+
readonly leaseExpiresAt: number | undefined
|
|
97
|
+
readonly etag: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const parseExec = (row: ExecRow): ExecState => ({
|
|
101
|
+
executionId: row.execution_id,
|
|
102
|
+
workflowName: row.workflow_name,
|
|
103
|
+
payload: row.payload,
|
|
104
|
+
parent: row.parent ?? undefined,
|
|
105
|
+
status: row.status,
|
|
106
|
+
suspended: row.suspended !== 0,
|
|
107
|
+
interrupted: row.interrupted !== 0,
|
|
108
|
+
completedResult: row.completed_result ?? undefined,
|
|
109
|
+
worker: row.worker ?? undefined,
|
|
110
|
+
leaseExpiresAt: row.lease_expires_at ?? undefined,
|
|
111
|
+
etag: row.etag
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// --- Storage codecs ---------------------------------------------------------
|
|
115
|
+
// Values flowing through the engine's activity / deferred boundary are already
|
|
116
|
+
// schema-encoded, so the structure is round-tripped while the payload stays
|
|
117
|
+
// opaque (mirrors the cluster engine's `AnyOrVoid` usage).
|
|
118
|
+
const AnyOrVoid = S.Union([S.Any, S.Void])
|
|
119
|
+
const ActivityResultCodec = S.fromJsonString(S.toCodecJson(Workflow.Result({ success: AnyOrVoid, error: AnyOrVoid })))
|
|
120
|
+
const DeferredExitCodec = S.fromJsonString(S.toCodecJson(S.Exit(AnyOrVoid, AnyOrVoid, S.Defect)))
|
|
121
|
+
|
|
122
|
+
const encodeActivityResult = (r: Workflow.Result<unknown, unknown>) =>
|
|
123
|
+
Effect.orDie(S.encodeEffect(ActivityResultCodec)(r))
|
|
124
|
+
const decodeActivityResult = (s: string) => Effect.orDie(S.decodeEffect(ActivityResultCodec)(s))
|
|
125
|
+
const encodeDeferredExit = (e: Exit.Exit<unknown, unknown>) => Effect.orDie(S.encodeEffect(DeferredExitCodec)(e))
|
|
126
|
+
const decodeDeferredExit = (s: string) => Effect.orDie(S.decodeEffect(DeferredExitCodec)(s))
|
|
127
|
+
|
|
128
|
+
const makeSqliteWorkflowEngine = Effect.fnUntraced(function*(cfg: WorkflowEngineSqliteConfig) {
|
|
129
|
+
const sql = yield* SqlClient.SqlClient
|
|
130
|
+
const scope = yield* Effect.scope
|
|
131
|
+
const prefix = cfg.prefix ?? ""
|
|
132
|
+
const execTable = `${prefix}workflow_exec`
|
|
133
|
+
const activityTable = `${prefix}workflow_activity`
|
|
134
|
+
const deferredTable = `${prefix}workflow_deferred`
|
|
135
|
+
const clockTable = `${prefix}workflow_clock`
|
|
136
|
+
|
|
137
|
+
const workerId = cfg.workerId ?? randomUUID()
|
|
138
|
+
const leaseTtl = cfg.leaseTtl ?? Duration.seconds(30)
|
|
139
|
+
const heartbeatInterval = cfg.heartbeatInterval ?? Duration.seconds(10)
|
|
140
|
+
const recoveryInterval = cfg.recoveryInterval ?? Duration.seconds(15)
|
|
141
|
+
const clockPollInterval = cfg.clockPollInterval ?? Duration.seconds(5)
|
|
142
|
+
|
|
143
|
+
const annotate = (operation: string, executionId?: string) =>
|
|
144
|
+
annotateDb({
|
|
145
|
+
operation,
|
|
146
|
+
system: "sqlite",
|
|
147
|
+
collection: execTable,
|
|
148
|
+
entity: "workflow",
|
|
149
|
+
extra: executionId !== undefined ? { "app.entity.id": executionId } : undefined
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const exec = (query: string, params: ReadonlyArray<unknown> = []) =>
|
|
153
|
+
sql.unsafe(query, params as Array<any>).pipe(Effect.orDie)
|
|
154
|
+
|
|
155
|
+
// --- Schema -----------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
yield* exec(
|
|
158
|
+
`CREATE TABLE IF NOT EXISTS "${execTable}" (
|
|
159
|
+
execution_id TEXT PRIMARY KEY,
|
|
160
|
+
workflow_name TEXT NOT NULL,
|
|
161
|
+
payload TEXT NOT NULL,
|
|
162
|
+
parent TEXT,
|
|
163
|
+
status TEXT NOT NULL,
|
|
164
|
+
suspended INTEGER NOT NULL DEFAULT 0,
|
|
165
|
+
interrupted INTEGER NOT NULL DEFAULT 0,
|
|
166
|
+
completed_result TEXT,
|
|
167
|
+
worker TEXT,
|
|
168
|
+
lease_expires_at INTEGER,
|
|
169
|
+
etag TEXT NOT NULL
|
|
170
|
+
)`
|
|
171
|
+
)
|
|
172
|
+
yield* exec(
|
|
173
|
+
`CREATE INDEX IF NOT EXISTS "${execTable}_recovery" ON "${execTable}" (status, lease_expires_at)`
|
|
174
|
+
)
|
|
175
|
+
yield* exec(
|
|
176
|
+
`CREATE TABLE IF NOT EXISTS "${activityTable}" (
|
|
177
|
+
execution_id TEXT NOT NULL,
|
|
178
|
+
name TEXT NOT NULL,
|
|
179
|
+
attempt INTEGER NOT NULL,
|
|
180
|
+
result TEXT NOT NULL,
|
|
181
|
+
PRIMARY KEY (execution_id, name, attempt)
|
|
182
|
+
)`
|
|
183
|
+
)
|
|
184
|
+
yield* exec(
|
|
185
|
+
`CREATE TABLE IF NOT EXISTS "${deferredTable}" (
|
|
186
|
+
execution_id TEXT NOT NULL,
|
|
187
|
+
name TEXT NOT NULL,
|
|
188
|
+
exit TEXT NOT NULL,
|
|
189
|
+
PRIMARY KEY (execution_id, name)
|
|
190
|
+
)`
|
|
191
|
+
)
|
|
192
|
+
yield* exec(
|
|
193
|
+
`CREATE TABLE IF NOT EXISTS "${clockTable}" (
|
|
194
|
+
execution_id TEXT NOT NULL,
|
|
195
|
+
name TEXT NOT NULL,
|
|
196
|
+
workflow_name TEXT NOT NULL,
|
|
197
|
+
deferred_name TEXT NOT NULL,
|
|
198
|
+
fire_at INTEGER NOT NULL,
|
|
199
|
+
PRIMARY KEY (execution_id, name)
|
|
200
|
+
)`
|
|
201
|
+
)
|
|
202
|
+
yield* exec(
|
|
203
|
+
`CREATE INDEX IF NOT EXISTS "${clockTable}_due" ON "${clockTable}" (fire_at)`
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// --- In-process bookkeeping -------------------------------------------
|
|
207
|
+
|
|
208
|
+
type Registered = {
|
|
209
|
+
readonly workflow: Workflow.Any
|
|
210
|
+
readonly execute: (
|
|
211
|
+
payload: object,
|
|
212
|
+
executionId: string
|
|
213
|
+
) => Effect.Effect<unknown, unknown, WorkflowInstance | WorkflowEngine>
|
|
214
|
+
readonly scope: Scope.Scope
|
|
215
|
+
}
|
|
216
|
+
const workflows = new Map<string, Registered>()
|
|
217
|
+
|
|
218
|
+
type LocalExec = {
|
|
219
|
+
instance: WorkflowInstance["Service"]
|
|
220
|
+
fiber: Fiber.Fiber<Workflow.Result<unknown, unknown>> | undefined
|
|
221
|
+
parent: string | undefined
|
|
222
|
+
}
|
|
223
|
+
const locals = new Map<string, LocalExec>()
|
|
224
|
+
const clocks = yield* FiberMap.make<string>()
|
|
225
|
+
|
|
226
|
+
// Per-workflow codecs for the typed payload + top-level result. Cached by
|
|
227
|
+
// workflow name; derived from the workflow's own schemas so typed values
|
|
228
|
+
// (dates, branded ids, schema classes) survive the storage round-trip.
|
|
229
|
+
const makePayloadCodec = (workflow: Workflow.Any) => S.fromJsonString(S.toCodecJson(workflow.payloadSchema))
|
|
230
|
+
const payloadCodecCache = new Map<string, ReturnType<typeof makePayloadCodec>>()
|
|
231
|
+
const payloadCodecFor = (workflow: Workflow.Any) => {
|
|
232
|
+
let c = payloadCodecCache.get(workflow.name)
|
|
233
|
+
if (!c) {
|
|
234
|
+
c = makePayloadCodec(workflow)
|
|
235
|
+
payloadCodecCache.set(workflow.name, c)
|
|
236
|
+
}
|
|
237
|
+
return c
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const makeResultCodec = (workflow: Workflow.Any) =>
|
|
241
|
+
S.fromJsonString(S.toCodecJson(Workflow.Result({ success: workflow.successSchema, error: workflow.errorSchema })))
|
|
242
|
+
const resultCodecCache = new Map<string, ReturnType<typeof makeResultCodec>>()
|
|
243
|
+
const resultCodecFor = (workflow: Workflow.Any) => {
|
|
244
|
+
let c = resultCodecCache.get(workflow.name)
|
|
245
|
+
if (!c) {
|
|
246
|
+
c = makeResultCodec(workflow)
|
|
247
|
+
resultCodecCache.set(workflow.name, c)
|
|
248
|
+
}
|
|
249
|
+
return c
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const encodePayload = (workflow: Workflow.Any, payload: object) =>
|
|
253
|
+
Effect.orDie(S.encodeEffect(payloadCodecFor(workflow))(payload)) as Effect.Effect<string>
|
|
254
|
+
const decodePayload = (workflow: Workflow.Any, s: string) =>
|
|
255
|
+
Effect.orDie(S.decodeEffect(payloadCodecFor(workflow))(s)) as Effect.Effect<object>
|
|
256
|
+
const encodeResult = (workflow: Workflow.Any, r: Workflow.Result<unknown, unknown>) =>
|
|
257
|
+
Effect.orDie(S.encodeEffect(resultCodecFor(workflow))(r)) as Effect.Effect<string>
|
|
258
|
+
const decodeResult = (workflow: Workflow.Any, s: string) =>
|
|
259
|
+
Effect.orDie(S.decodeEffect(resultCodecFor(workflow))(s)) as Effect.Effect<Workflow.Result<unknown, unknown>>
|
|
260
|
+
|
|
261
|
+
// --- Core SQL operations ----------------------------------------------
|
|
262
|
+
|
|
263
|
+
const readExec = (executionId: string): Effect.Effect<Option.Option<ExecState>> =>
|
|
264
|
+
exec(
|
|
265
|
+
`SELECT * FROM "${execTable}" WHERE execution_id = ?`,
|
|
266
|
+
[executionId]
|
|
267
|
+
)
|
|
268
|
+
.pipe(
|
|
269
|
+
Effect.map((rows) => {
|
|
270
|
+
const r = (rows as ReadonlyArray<ExecRow>)[0]
|
|
271
|
+
return r ? Option.some(parseExec(r)) : Option.none<ExecState>()
|
|
272
|
+
}),
|
|
273
|
+
annotate("readExec", executionId)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* OCC-guarded write. Generates a fresh etag on success; returns
|
|
278
|
+
* `OptimisticConcurrencyException` when no row matches the prior etag.
|
|
279
|
+
*/
|
|
280
|
+
const replaceExec = (
|
|
281
|
+
state: ExecState,
|
|
282
|
+
next: Partial<Omit<ExecState, "executionId" | "etag" | "workflowName" | "payload" | "parent">>
|
|
283
|
+
) =>
|
|
284
|
+
Effect
|
|
285
|
+
.gen(function*() {
|
|
286
|
+
const newEtag = randomUUID()
|
|
287
|
+
const merged = { ...state, ...next, etag: newEtag }
|
|
288
|
+
const rows = yield* exec(
|
|
289
|
+
`UPDATE "${execTable}"
|
|
290
|
+
SET status = ?,
|
|
291
|
+
suspended = ?,
|
|
292
|
+
interrupted = ?,
|
|
293
|
+
completed_result = ?,
|
|
294
|
+
worker = ?,
|
|
295
|
+
lease_expires_at = ?,
|
|
296
|
+
etag = ?
|
|
297
|
+
WHERE execution_id = ? AND etag = ?
|
|
298
|
+
RETURNING etag`,
|
|
299
|
+
[
|
|
300
|
+
merged.status,
|
|
301
|
+
merged.suspended ? 1 : 0,
|
|
302
|
+
merged.interrupted ? 1 : 0,
|
|
303
|
+
merged.completedResult ?? null,
|
|
304
|
+
merged.worker ?? null,
|
|
305
|
+
merged.leaseExpiresAt ?? null,
|
|
306
|
+
newEtag,
|
|
307
|
+
state.executionId,
|
|
308
|
+
state.etag
|
|
309
|
+
]
|
|
310
|
+
)
|
|
311
|
+
if ((rows as ReadonlyArray<unknown>).length === 0) {
|
|
312
|
+
return yield* new OptimisticConcurrencyException({
|
|
313
|
+
type: "workflow.exec",
|
|
314
|
+
id: state.executionId,
|
|
315
|
+
code: 412
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
return merged
|
|
319
|
+
})
|
|
320
|
+
.pipe(annotate("replaceExec", state.executionId))
|
|
321
|
+
|
|
322
|
+
const createExec = (initial: ExecState): Effect.Effect<boolean> =>
|
|
323
|
+
exec(
|
|
324
|
+
`INSERT INTO "${execTable}"
|
|
325
|
+
(execution_id, workflow_name, payload, parent, status, suspended, interrupted, etag)
|
|
326
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
327
|
+
ON CONFLICT DO NOTHING
|
|
328
|
+
RETURNING execution_id`,
|
|
329
|
+
[
|
|
330
|
+
initial.executionId,
|
|
331
|
+
initial.workflowName,
|
|
332
|
+
initial.payload,
|
|
333
|
+
initial.parent ?? null,
|
|
334
|
+
initial.status,
|
|
335
|
+
initial.suspended ? 1 : 0,
|
|
336
|
+
initial.interrupted ? 1 : 0,
|
|
337
|
+
initial.etag
|
|
338
|
+
]
|
|
339
|
+
)
|
|
340
|
+
.pipe(
|
|
341
|
+
Effect.map((rows) => (rows as ReadonlyArray<unknown>).length > 0),
|
|
342
|
+
annotate("createExec", initial.executionId)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
// First-writer-wins persistence of an activity result; returns true if this
|
|
346
|
+
// call won, false if another writer beat us to the (exec, name, attempt) row.
|
|
347
|
+
const createActivity = (
|
|
348
|
+
executionId: string,
|
|
349
|
+
name: string,
|
|
350
|
+
attempt: number,
|
|
351
|
+
encoded: string
|
|
352
|
+
): Effect.Effect<boolean> =>
|
|
353
|
+
exec(
|
|
354
|
+
`INSERT INTO "${activityTable}" (execution_id, name, attempt, result)
|
|
355
|
+
VALUES (?, ?, ?, ?)
|
|
356
|
+
ON CONFLICT DO NOTHING
|
|
357
|
+
RETURNING execution_id`,
|
|
358
|
+
[executionId, name, attempt, encoded]
|
|
359
|
+
)
|
|
360
|
+
.pipe(Effect.map((rows) => (rows as ReadonlyArray<unknown>).length > 0))
|
|
361
|
+
|
|
362
|
+
// Overwrites a previously persisted *suspended* activity result so the next
|
|
363
|
+
// attempt can record its real outcome.
|
|
364
|
+
const upsertActivity = (
|
|
365
|
+
executionId: string,
|
|
366
|
+
name: string,
|
|
367
|
+
attempt: number,
|
|
368
|
+
encoded: string
|
|
369
|
+
): Effect.Effect<void> =>
|
|
370
|
+
exec(
|
|
371
|
+
`INSERT INTO "${activityTable}" (execution_id, name, attempt, result)
|
|
372
|
+
VALUES (?, ?, ?, ?)
|
|
373
|
+
ON CONFLICT(execution_id, name, attempt) DO UPDATE SET result = excluded.result`,
|
|
374
|
+
[executionId, name, attempt, encoded]
|
|
375
|
+
)
|
|
376
|
+
.pipe(Effect.asVoid)
|
|
377
|
+
|
|
378
|
+
const readActivity = (
|
|
379
|
+
executionId: string,
|
|
380
|
+
name: string,
|
|
381
|
+
attempt: number
|
|
382
|
+
): Effect.Effect<Option.Option<string>> =>
|
|
383
|
+
exec(
|
|
384
|
+
`SELECT result FROM "${activityTable}" WHERE execution_id = ? AND name = ? AND attempt = ?`,
|
|
385
|
+
[executionId, name, attempt]
|
|
386
|
+
)
|
|
387
|
+
.pipe(
|
|
388
|
+
Effect.map((rows) => {
|
|
389
|
+
const r = (rows as ReadonlyArray<{ result: string }>)[0]
|
|
390
|
+
return r ? Option.some(r.result) : Option.none<string>()
|
|
391
|
+
})
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
const createDeferred = (
|
|
395
|
+
executionId: string,
|
|
396
|
+
name: string,
|
|
397
|
+
encoded: string
|
|
398
|
+
): Effect.Effect<boolean> =>
|
|
399
|
+
exec(
|
|
400
|
+
`INSERT INTO "${deferredTable}" (execution_id, name, exit)
|
|
401
|
+
VALUES (?, ?, ?)
|
|
402
|
+
ON CONFLICT DO NOTHING
|
|
403
|
+
RETURNING execution_id`,
|
|
404
|
+
[executionId, name, encoded]
|
|
405
|
+
)
|
|
406
|
+
.pipe(Effect.map((rows) => (rows as ReadonlyArray<unknown>).length > 0))
|
|
407
|
+
|
|
408
|
+
const readDeferred = (
|
|
409
|
+
executionId: string,
|
|
410
|
+
name: string
|
|
411
|
+
): Effect.Effect<Option.Option<string>> =>
|
|
412
|
+
exec(
|
|
413
|
+
`SELECT exit FROM "${deferredTable}" WHERE execution_id = ? AND name = ?`,
|
|
414
|
+
[executionId, name]
|
|
415
|
+
)
|
|
416
|
+
.pipe(
|
|
417
|
+
Effect.map((rows) => {
|
|
418
|
+
const r = (rows as ReadonlyArray<{ exit: string }>)[0]
|
|
419
|
+
return r ? Option.some(r.exit) : Option.none<string>()
|
|
420
|
+
})
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
const insertClock = (
|
|
424
|
+
executionId: string,
|
|
425
|
+
name: string,
|
|
426
|
+
workflowName: string,
|
|
427
|
+
deferredName: string,
|
|
428
|
+
fireAt: number
|
|
429
|
+
): Effect.Effect<boolean> =>
|
|
430
|
+
exec(
|
|
431
|
+
`INSERT INTO "${clockTable}" (execution_id, name, workflow_name, deferred_name, fire_at)
|
|
432
|
+
VALUES (?, ?, ?, ?, ?)
|
|
433
|
+
ON CONFLICT DO NOTHING
|
|
434
|
+
RETURNING execution_id`,
|
|
435
|
+
[executionId, name, workflowName, deferredName, fireAt]
|
|
436
|
+
)
|
|
437
|
+
.pipe(Effect.map((rows) => (rows as ReadonlyArray<unknown>).length > 0))
|
|
438
|
+
|
|
439
|
+
const deleteClock = (executionId: string, name: string) =>
|
|
440
|
+
exec(
|
|
441
|
+
`DELETE FROM "${clockTable}" WHERE execution_id = ? AND name = ?`,
|
|
442
|
+
[executionId, name]
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
// --- Workflow result helpers ------------------------------------------
|
|
446
|
+
|
|
447
|
+
const completeResult = (
|
|
448
|
+
workflow: Workflow.Any,
|
|
449
|
+
state: ExecState
|
|
450
|
+
): Effect.Effect<Option.Option<Workflow.Result<unknown, unknown>>> =>
|
|
451
|
+
state.status === "complete" && state.completedResult
|
|
452
|
+
? Effect.map(decodeResult(workflow, state.completedResult), Option.some)
|
|
453
|
+
: Effect.succeedNone
|
|
454
|
+
|
|
455
|
+
// --- Lease / claim ----------------------------------------------------
|
|
456
|
+
|
|
457
|
+
const leaseActive = (state: ExecState, now: number): boolean =>
|
|
458
|
+
state.worker !== undefined
|
|
459
|
+
&& state.worker !== workerId
|
|
460
|
+
&& state.leaseExpiresAt !== undefined
|
|
461
|
+
&& state.leaseExpiresAt > now
|
|
462
|
+
|
|
463
|
+
const tryClaim = (state: ExecState): Effect.Effect<Option.Option<ExecState>> =>
|
|
464
|
+
Effect.gen(function*() {
|
|
465
|
+
const now = Date.now()
|
|
466
|
+
if (leaseActive(state, now)) return Option.none<ExecState>()
|
|
467
|
+
return yield* replaceExec(state, {
|
|
468
|
+
worker: workerId,
|
|
469
|
+
leaseExpiresAt: now + Duration.toMillis(leaseTtl)
|
|
470
|
+
})
|
|
471
|
+
.pipe(
|
|
472
|
+
Effect.map(Option.some),
|
|
473
|
+
Effect.catchTag("OptimisticConcurrencyException", () => Effect.succeed(Option.none<ExecState>()))
|
|
474
|
+
)
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
const heartbeat = (executionId: string): Effect.Effect<void> =>
|
|
478
|
+
Effect.gen(function*() {
|
|
479
|
+
while (true) {
|
|
480
|
+
yield* Effect.sleep(heartbeatInterval)
|
|
481
|
+
const local = locals.get(executionId)
|
|
482
|
+
const polled = local?.fiber?.pollUnsafe()
|
|
483
|
+
if (!local?.fiber || polled) return
|
|
484
|
+
const cur = yield* readExec(executionId).pipe(
|
|
485
|
+
Effect.catchCause(() => Effect.succeed(Option.none<ExecState>()))
|
|
486
|
+
)
|
|
487
|
+
if (Option.isNone(cur)) continue
|
|
488
|
+
const state = cur.value
|
|
489
|
+
if (state.status === "complete" || state.worker !== workerId) return
|
|
490
|
+
yield* replaceExec(state, {
|
|
491
|
+
leaseExpiresAt: Date.now() + Duration.toMillis(leaseTtl)
|
|
492
|
+
})
|
|
493
|
+
.pipe(
|
|
494
|
+
Effect.catchTag("OptimisticConcurrencyException", () => Effect.void),
|
|
495
|
+
Effect.catchCause(() => Effect.void)
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
// --- Drive logic ------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
const drive = (
|
|
503
|
+
executionId: string,
|
|
504
|
+
payload: object,
|
|
505
|
+
parent: string | undefined,
|
|
506
|
+
entry: Registered
|
|
507
|
+
): Effect.Effect<void> =>
|
|
508
|
+
Effect.gen(function*() {
|
|
509
|
+
let local = locals.get(executionId)
|
|
510
|
+
if (local?.fiber) {
|
|
511
|
+
const polled = local.fiber.pollUnsafe()
|
|
512
|
+
const stillRunning = !polled
|
|
513
|
+
const completedNotResume = polled && polled._tag === "Success" && polled.value._tag === "Complete"
|
|
514
|
+
if (stillRunning || completedNotResume) return
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const stateOpt = yield* readExec(executionId)
|
|
518
|
+
if (Option.isNone(stateOpt) || stateOpt.value.status === "complete") return
|
|
519
|
+
|
|
520
|
+
const claimed = yield* tryClaim(stateOpt.value)
|
|
521
|
+
const state = Option.isSome(claimed) ? claimed.value : stateOpt.value
|
|
522
|
+
|
|
523
|
+
const instance = WorkflowInstance.initial(entry.workflow, executionId)
|
|
524
|
+
instance.interrupted = state.interrupted
|
|
525
|
+
if (!local) {
|
|
526
|
+
local = { instance, fiber: undefined, parent }
|
|
527
|
+
locals.set(executionId, local)
|
|
528
|
+
} else {
|
|
529
|
+
local.instance = instance
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const onComplete = Effect.fnUntraced(function*(result: Workflow.Result<unknown, unknown>) {
|
|
533
|
+
const current = yield* readExec(executionId)
|
|
534
|
+
if (Option.isNone(current) || current.value.status === "complete") return
|
|
535
|
+
const isComplete = result._tag === "Complete"
|
|
536
|
+
const completedResult = isComplete ? yield* encodeResult(entry.workflow, result) : undefined
|
|
537
|
+
yield* replaceExec(current.value, {
|
|
538
|
+
status: isComplete ? "complete" : current.value.status,
|
|
539
|
+
suspended: result._tag === "Suspended",
|
|
540
|
+
interrupted: instance.interrupted,
|
|
541
|
+
completedResult,
|
|
542
|
+
worker: isComplete ? undefined : current.value.worker,
|
|
543
|
+
leaseExpiresAt: isComplete ? undefined : current.value.leaseExpiresAt
|
|
544
|
+
})
|
|
545
|
+
.pipe(Effect.catchTag("OptimisticConcurrencyException", () => Effect.void))
|
|
546
|
+
if (parent && isComplete) {
|
|
547
|
+
yield* Effect.forkIn(driveById(parent), scope)
|
|
548
|
+
}
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
local.fiber = yield* entry.execute(payload, executionId).pipe(
|
|
552
|
+
Effect.onExit(() => {
|
|
553
|
+
if (!instance.interrupted) return Effect.void
|
|
554
|
+
instance.suspended = false
|
|
555
|
+
return Effect.withFiber((fiber) => Effect.interruptible(Fiber.interrupt(fiber)))
|
|
556
|
+
}),
|
|
557
|
+
Workflow.intoResult,
|
|
558
|
+
Effect.provideService(WorkflowInstance, instance),
|
|
559
|
+
Effect.provideService(WorkflowEngine, engine),
|
|
560
|
+
Effect.tap(onComplete),
|
|
561
|
+
Effect.forkIn(entry.scope)
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
if (Option.isSome(claimed)) {
|
|
565
|
+
yield* Effect.forkIn(heartbeat(executionId), scope)
|
|
566
|
+
}
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
const driveById = (executionId: string): Effect.Effect<void> =>
|
|
570
|
+
Effect.gen(function*() {
|
|
571
|
+
const stateOpt = yield* readExec(executionId)
|
|
572
|
+
if (Option.isNone(stateOpt)) return
|
|
573
|
+
const state = stateOpt.value
|
|
574
|
+
const entry = workflows.get(state.workflowName)
|
|
575
|
+
if (!entry) return
|
|
576
|
+
const payload = yield* decodePayload(entry.workflow, state.payload)
|
|
577
|
+
yield* drive(executionId, payload, state.parent, entry)
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
// --- Clock firing -----------------------------------------------------
|
|
581
|
+
|
|
582
|
+
const fireClock = (
|
|
583
|
+
executionId: string,
|
|
584
|
+
name: string,
|
|
585
|
+
deferredName: string
|
|
586
|
+
): Effect.Effect<void> =>
|
|
587
|
+
Effect
|
|
588
|
+
.gen(function*() {
|
|
589
|
+
const encoded = yield* encodeDeferredExit(Exit.void)
|
|
590
|
+
const inserted = yield* sql
|
|
591
|
+
.withTransaction(Effect.gen(function*() {
|
|
592
|
+
const got = yield* createDeferred(executionId, deferredName, encoded)
|
|
593
|
+
yield* deleteClock(executionId, name)
|
|
594
|
+
return got
|
|
595
|
+
}))
|
|
596
|
+
.pipe(Effect.orDie)
|
|
597
|
+
if (inserted) yield* driveById(executionId)
|
|
598
|
+
})
|
|
599
|
+
.pipe(annotate("clockFire", executionId))
|
|
600
|
+
|
|
601
|
+
// --- Encoded engine ---------------------------------------------------
|
|
602
|
+
|
|
603
|
+
const encoded: Encoded = {
|
|
604
|
+
register: Effect.fnUntraced(function*(workflow, execute) {
|
|
605
|
+
workflows.set(workflow.name, {
|
|
606
|
+
workflow,
|
|
607
|
+
execute,
|
|
608
|
+
scope: yield* Effect.scope
|
|
609
|
+
})
|
|
610
|
+
}),
|
|
611
|
+
execute: Effect.fnUntraced(function*(workflow, options) {
|
|
612
|
+
const entry = workflows.get(workflow.name)
|
|
613
|
+
if (!entry) {
|
|
614
|
+
return yield* Effect.orDie(Effect.fail(`Workflow ${workflow.name} is not registered`))
|
|
615
|
+
}
|
|
616
|
+
const initial: ExecState = {
|
|
617
|
+
executionId: options.executionId,
|
|
618
|
+
workflowName: workflow.name,
|
|
619
|
+
payload: yield* encodePayload(workflow, options.payload),
|
|
620
|
+
parent: options.parent?.executionId,
|
|
621
|
+
status: "running",
|
|
622
|
+
suspended: false,
|
|
623
|
+
interrupted: false,
|
|
624
|
+
completedResult: undefined,
|
|
625
|
+
worker: undefined,
|
|
626
|
+
leaseExpiresAt: undefined,
|
|
627
|
+
etag: randomUUID()
|
|
628
|
+
}
|
|
629
|
+
yield* createExec(initial)
|
|
630
|
+
yield* drive(options.executionId, options.payload, options.parent?.executionId, entry)
|
|
631
|
+
if (options.discard) return undefined as any
|
|
632
|
+
const local = locals.get(options.executionId)
|
|
633
|
+
if (local?.fiber) {
|
|
634
|
+
return (yield* Fiber.join(local.fiber)) as any
|
|
635
|
+
}
|
|
636
|
+
// Foreign-driver fallback: poll the persisted result until completion.
|
|
637
|
+
while (true) {
|
|
638
|
+
const cur = yield* readExec(options.executionId)
|
|
639
|
+
if (Option.isSome(cur)) {
|
|
640
|
+
const r = yield* completeResult(workflow, cur.value)
|
|
641
|
+
if (Option.isSome(r)) return r.value as any
|
|
642
|
+
}
|
|
643
|
+
yield* Effect.sleep(Duration.millis(500))
|
|
644
|
+
}
|
|
645
|
+
}),
|
|
646
|
+
poll: (workflow, executionId) =>
|
|
647
|
+
Effect.gen(function*() {
|
|
648
|
+
const local = locals.get(executionId)
|
|
649
|
+
if (local?.fiber) {
|
|
650
|
+
const exitVal = local.fiber.pollUnsafe()
|
|
651
|
+
if (!exitVal) return Option.none<Workflow.Result<unknown, unknown>>()
|
|
652
|
+
if (exitVal._tag !== "Success") return yield* Effect.die(exitVal.cause)
|
|
653
|
+
return Option.some(exitVal.value)
|
|
654
|
+
}
|
|
655
|
+
const state = yield* readExec(executionId)
|
|
656
|
+
if (Option.isNone(state)) return Option.none<Workflow.Result<unknown, unknown>>()
|
|
657
|
+
return yield* completeResult(workflow, state.value)
|
|
658
|
+
}),
|
|
659
|
+
interrupt: Effect.fnUntraced(function*(_workflow, executionId) {
|
|
660
|
+
const local = locals.get(executionId)
|
|
661
|
+
if (local) local.instance.interrupted = true
|
|
662
|
+
const current = yield* readExec(executionId)
|
|
663
|
+
if (Option.isNone(current) || current.value.status === "complete") return
|
|
664
|
+
yield* replaceExec(current.value, { interrupted: true }).pipe(
|
|
665
|
+
Effect.catchTag("OptimisticConcurrencyException", () => Effect.void)
|
|
666
|
+
)
|
|
667
|
+
yield* driveById(executionId)
|
|
668
|
+
}),
|
|
669
|
+
interruptUnsafe: Effect.fnUntraced(function*(_workflow, executionId) {
|
|
670
|
+
const local = locals.get(executionId)
|
|
671
|
+
if (local) local.instance.interrupted = true
|
|
672
|
+
const current = yield* readExec(executionId)
|
|
673
|
+
if (Option.isSome(current) && current.value.status !== "complete") {
|
|
674
|
+
yield* replaceExec(current.value, { interrupted: true }).pipe(
|
|
675
|
+
Effect.catchTag("OptimisticConcurrencyException", () => Effect.void)
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
if (local?.fiber) yield* Fiber.interrupt(local.fiber)
|
|
679
|
+
}),
|
|
680
|
+
resume: (_workflow, executionId) => driveById(executionId),
|
|
681
|
+
activityExecute: Effect.fnUntraced(function*(activity, attempt) {
|
|
682
|
+
const instance = yield* WorkflowInstance
|
|
683
|
+
const existing = yield* readActivity(instance.executionId, activity.name, attempt)
|
|
684
|
+
if (Option.isSome(existing)) {
|
|
685
|
+
const prev = yield* decodeActivityResult(existing.value)
|
|
686
|
+
// A completed activity is replayed from its persisted result; a
|
|
687
|
+
// suspended one must re-run (it parked on a clock/deferred).
|
|
688
|
+
if (prev._tag === "Complete") return prev
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const activityInstance = WorkflowInstance.initial(instance.workflow, instance.executionId)
|
|
692
|
+
activityInstance.interrupted = instance.interrupted
|
|
693
|
+
|
|
694
|
+
const result = yield* activity.executeEncoded.pipe(
|
|
695
|
+
Workflow.intoResult,
|
|
696
|
+
Effect.provideService(WorkflowInstance, activityInstance)
|
|
697
|
+
)
|
|
698
|
+
const encodedResult = yield* encodeActivityResult(result)
|
|
699
|
+
|
|
700
|
+
if (Option.isSome(existing)) {
|
|
701
|
+
// Overwrite the previously persisted *suspended* result.
|
|
702
|
+
yield* upsertActivity(instance.executionId, activity.name, attempt, encodedResult)
|
|
703
|
+
return result
|
|
704
|
+
}
|
|
705
|
+
// First-writer-wins: if persistence loses the race, use the persisted result.
|
|
706
|
+
const persisted = yield* createActivity(instance.executionId, activity.name, attempt, encodedResult)
|
|
707
|
+
if (persisted) return result
|
|
708
|
+
const winner = yield* readActivity(instance.executionId, activity.name, attempt)
|
|
709
|
+
if (Option.isSome(winner)) {
|
|
710
|
+
const w = yield* decodeActivityResult(winner.value)
|
|
711
|
+
if (w._tag === "Complete") return w
|
|
712
|
+
}
|
|
713
|
+
return result
|
|
714
|
+
}),
|
|
715
|
+
deferredResult: Effect.fnUntraced(function*(deferred) {
|
|
716
|
+
const instance = yield* WorkflowInstance
|
|
717
|
+
const got = yield* readDeferred(instance.executionId, deferred.name)
|
|
718
|
+
if (Option.isNone(got)) return Option.none<Exit.Exit<unknown, unknown>>()
|
|
719
|
+
return Option.some(yield* decodeDeferredExit(got.value))
|
|
720
|
+
}),
|
|
721
|
+
deferredDone: Effect.fnUntraced(function*(options) {
|
|
722
|
+
const encoded = yield* encodeDeferredExit(options.exit)
|
|
723
|
+
const inserted = yield* createDeferred(options.executionId, options.deferredName, encoded)
|
|
724
|
+
if (!inserted) return
|
|
725
|
+
yield* driveById(options.executionId)
|
|
726
|
+
}),
|
|
727
|
+
scheduleClock: (workflow, options) => {
|
|
728
|
+
const fireAt = Date.now() + Duration.toMillis(options.clock.duration)
|
|
729
|
+
return Effect.gen(function*() {
|
|
730
|
+
yield* insertClock(
|
|
731
|
+
options.executionId,
|
|
732
|
+
options.clock.name,
|
|
733
|
+
workflow.name,
|
|
734
|
+
options.clock.deferred.name,
|
|
735
|
+
fireAt
|
|
736
|
+
)
|
|
737
|
+
yield* fireClock(options.executionId, options.clock.name, options.clock.deferred.name).pipe(
|
|
738
|
+
Effect.delay(options.clock.duration),
|
|
739
|
+
FiberMap.run(clocks, `${options.executionId}/${options.clock.name}`, { onlyIfMissing: true }),
|
|
740
|
+
Effect.asVoid
|
|
741
|
+
)
|
|
742
|
+
})
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const engine = makeUnsafe(encoded)
|
|
747
|
+
|
|
748
|
+
// --- Recovery poller --------------------------------------------------
|
|
749
|
+
|
|
750
|
+
if (Duration.toMillis(recoveryInterval) > 0) {
|
|
751
|
+
const recoverStep = Effect
|
|
752
|
+
.gen(function*() {
|
|
753
|
+
const rows = yield* exec(
|
|
754
|
+
`SELECT execution_id, workflow_name FROM "${execTable}"
|
|
755
|
+
WHERE status = 'running' AND (lease_expires_at IS NULL OR lease_expires_at <= ?)
|
|
756
|
+
LIMIT 100`,
|
|
757
|
+
[Date.now()]
|
|
758
|
+
)
|
|
759
|
+
for (const row of rows as ReadonlyArray<{ execution_id: string; workflow_name: string }>) {
|
|
760
|
+
if (!workflows.has(row.workflow_name)) continue
|
|
761
|
+
const local = locals.get(row.execution_id)
|
|
762
|
+
if (local?.fiber && !local.fiber.pollUnsafe()) continue
|
|
763
|
+
yield* Effect.forkIn(driveById(row.execution_id), scope)
|
|
764
|
+
}
|
|
765
|
+
})
|
|
766
|
+
.pipe(annotate("recoveryScan"), Effect.catchCause(() => Effect.void))
|
|
767
|
+
|
|
768
|
+
yield* recoverStep.pipe(
|
|
769
|
+
Effect.repeat(Schedule.spaced(recoveryInterval)),
|
|
770
|
+
Effect.forkIn(scope)
|
|
771
|
+
)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// --- Clock poller -----------------------------------------------------
|
|
775
|
+
|
|
776
|
+
if (Duration.toMillis(clockPollInterval) > 0) {
|
|
777
|
+
const clockStep = Effect
|
|
778
|
+
.gen(function*() {
|
|
779
|
+
const rows = yield* exec(
|
|
780
|
+
`SELECT execution_id, name, deferred_name FROM "${clockTable}"
|
|
781
|
+
WHERE fire_at <= ?
|
|
782
|
+
LIMIT 100`,
|
|
783
|
+
[Date.now()]
|
|
784
|
+
)
|
|
785
|
+
for (
|
|
786
|
+
const row of rows as ReadonlyArray<{
|
|
787
|
+
execution_id: string
|
|
788
|
+
name: string
|
|
789
|
+
deferred_name: string
|
|
790
|
+
}>
|
|
791
|
+
) {
|
|
792
|
+
yield* Effect.forkIn(fireClock(row.execution_id, row.name, row.deferred_name), scope)
|
|
793
|
+
}
|
|
794
|
+
})
|
|
795
|
+
.pipe(annotate("clockScan"), Effect.catchCause(() => Effect.void))
|
|
796
|
+
|
|
797
|
+
yield* clockStep.pipe(
|
|
798
|
+
Effect.repeat(Schedule.spaced(clockPollInterval)),
|
|
799
|
+
Effect.forkIn(scope)
|
|
800
|
+
)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return engine
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* SQLite backed `WorkflowEngine` layer. Requires an ambient `SqlClient`
|
|
808
|
+
* (`@effect/sql-sqlite-node` or a compatible client).
|
|
809
|
+
*/
|
|
810
|
+
export const layerSqlite = (
|
|
811
|
+
cfg: WorkflowEngineSqliteConfig = {}
|
|
812
|
+
): Layer.Layer<WorkflowEngine, never, SqlClient.SqlClient> =>
|
|
813
|
+
Layer.effect(WorkflowEngine)(makeSqliteWorkflowEngine(cfg))
|