@adonis-agora/durable 1.0.0-rc.2 → 1.0.0-rc.21
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 +204 -0
- package/dist/assets/spa/assets/index-BgoNV3TR.css +1 -0
- package/dist/assets/spa/assets/index-DzXygyDm.js +57 -0
- package/dist/assets/spa/assets/index-DzXygyDm.js.map +1 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/dist/assets/spa/assets/sora-latin-400-normal-CRt88UEn.woff2 +0 -0
- package/dist/assets/spa/assets/sora-latin-400-normal-OW7qkl5a.woff +0 -0
- package/dist/assets/spa/assets/sora-latin-500-normal-01eiPEn0.woff2 +0 -0
- package/dist/assets/spa/assets/sora-latin-500-normal-w58xtEt9.woff +0 -0
- package/dist/assets/spa/assets/sora-latin-600-normal-1_7fyUAY.woff +0 -0
- package/dist/assets/spa/assets/sora-latin-600-normal-Cdg4DaK0.woff2 +0 -0
- package/dist/assets/spa/assets/sora-latin-700-normal-9waGdLWo.woff2 +0 -0
- package/dist/assets/spa/assets/sora-latin-700-normal-BKPfQAnC.woff +0 -0
- package/dist/assets/spa/index.html +2 -8
- package/dist/commands/agora_doctor.d.ts +23 -0
- package/dist/commands/agora_doctor.d.ts.map +1 -0
- package/dist/commands/agora_doctor.js +110 -0
- package/dist/commands/agora_doctor.js.map +1 -0
- package/dist/commands/durable_doctor.d.ts +8 -0
- package/dist/commands/durable_doctor.d.ts.map +1 -0
- package/dist/commands/durable_doctor.js +8 -0
- package/dist/commands/durable_doctor.js.map +1 -0
- package/dist/commands/durable_worker.d.ts +3 -3
- package/dist/commands/durable_worker.js +7 -7
- package/dist/commands/durable_worker.js.map +1 -1
- package/dist/commands/health.d.ts +12 -0
- package/dist/commands/health.d.ts.map +1 -0
- package/dist/commands/health.js +34 -0
- package/dist/commands/health.js.map +1 -0
- package/dist/commands/lifecycle_republish.d.ts +8 -9
- package/dist/commands/lifecycle_republish.d.ts.map +1 -1
- package/dist/commands/lifecycle_republish.js +29 -16
- package/dist/commands/lifecycle_republish.js.map +1 -1
- package/dist/commands/main.d.ts +25 -5
- package/dist/commands/main.d.ts.map +1 -1
- package/dist/commands/main.js +54 -7
- package/dist/commands/main.js.map +1 -1
- package/dist/commands/runs.d.ts +9 -16
- package/dist/commands/runs.d.ts.map +1 -1
- package/dist/commands/runs.js +60 -47
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/runs_bulk.d.ts +17 -0
- package/dist/commands/runs_bulk.d.ts.map +1 -0
- package/dist/commands/runs_bulk.js +76 -0
- package/dist/commands/runs_bulk.js.map +1 -0
- package/dist/commands/runs_cancel.d.ts +15 -0
- package/dist/commands/runs_cancel.d.ts.map +1 -0
- package/dist/commands/runs_cancel.js +39 -0
- package/dist/commands/runs_cancel.js.map +1 -0
- package/dist/commands/runs_continue.d.ts +12 -0
- package/dist/commands/runs_continue.d.ts.map +1 -0
- package/dist/commands/runs_continue.js +28 -0
- package/dist/commands/runs_continue.js.map +1 -0
- package/dist/commands/runs_event.d.ts +13 -0
- package/dist/commands/runs_event.d.ts.map +1 -0
- package/dist/commands/runs_event.js +28 -0
- package/dist/commands/runs_event.js.map +1 -0
- package/dist/commands/runs_fan.d.ts +16 -0
- package/dist/commands/runs_fan.d.ts.map +1 -0
- package/dist/commands/runs_fan.js +58 -0
- package/dist/commands/runs_fan.js.map +1 -0
- package/dist/commands/runs_redispatch.d.ts +14 -0
- package/dist/commands/runs_redispatch.d.ts.map +1 -0
- package/dist/commands/runs_redispatch.js +35 -0
- package/dist/commands/runs_redispatch.js.map +1 -0
- package/dist/commands/runs_replay.d.ts +14 -0
- package/dist/commands/runs_replay.d.ts.map +1 -0
- package/dist/commands/runs_replay.js +38 -0
- package/dist/commands/runs_replay.js.map +1 -0
- package/dist/commands/runs_retry.d.ts +13 -0
- package/dist/commands/runs_retry.d.ts.map +1 -0
- package/dist/commands/runs_retry.js +29 -0
- package/dist/commands/runs_retry.js.map +1 -0
- package/dist/commands/runs_show.d.ts +14 -0
- package/dist/commands/runs_show.d.ts.map +1 -0
- package/dist/commands/runs_show.js +96 -0
- package/dist/commands/runs_show.js.map +1 -0
- package/dist/commands/runs_signal.d.ts +17 -0
- package/dist/commands/runs_signal.d.ts.map +1 -0
- package/dist/commands/runs_signal.js +49 -0
- package/dist/commands/runs_signal.js.map +1 -0
- package/dist/commands/runs_update.d.ts +15 -0
- package/dist/commands/runs_update.d.ts.map +1 -0
- package/dist/commands/runs_update.js +39 -0
- package/dist/commands/runs_update.js.map +1 -0
- package/dist/commands/runs_values.d.ts +15 -0
- package/dist/commands/runs_values.d.ts.map +1 -0
- package/dist/commands/runs_values.js +45 -0
- package/dist/commands/runs_values.js.map +1 -0
- package/dist/commands/schedules.d.ts +18 -0
- package/dist/commands/schedules.d.ts.map +1 -0
- package/dist/commands/schedules.js +70 -0
- package/dist/commands/schedules.js.map +1 -0
- package/dist/commands/schedules_pause.d.ts +16 -0
- package/dist/commands/schedules_pause.d.ts.map +1 -0
- package/dist/commands/schedules_pause.js +40 -0
- package/dist/commands/schedules_pause.js.map +1 -0
- package/dist/commands/schedules_resume.d.ts +8 -0
- package/dist/commands/schedules_resume.d.ts.map +1 -0
- package/dist/commands/schedules_resume.js +8 -0
- package/dist/commands/schedules_resume.js.map +1 -0
- package/dist/commands/schedules_trigger.d.ts +13 -0
- package/dist/commands/schedules_trigger.d.ts.map +1 -0
- package/dist/commands/schedules_trigger.js +32 -0
- package/dist/commands/schedules_trigger.js.map +1 -0
- package/dist/commands/tasks_complete.d.ts +15 -0
- package/dist/commands/tasks_complete.d.ts.map +1 -0
- package/dist/commands/tasks_complete.js +41 -0
- package/dist/commands/tasks_complete.js.map +1 -0
- package/dist/commands/tasks_fail.d.ts +16 -0
- package/dist/commands/tasks_fail.d.ts.map +1 -0
- package/dist/commands/tasks_fail.js +47 -0
- package/dist/commands/tasks_fail.js.map +1 -0
- package/dist/commands/workers.d.ts +11 -0
- package/dist/commands/workers.d.ts.map +1 -0
- package/dist/commands/workers.js +35 -0
- package/dist/commands/workers.js.map +1 -0
- package/dist/commands/workflows_lifecycle.d.ts +12 -0
- package/dist/commands/workflows_lifecycle.d.ts.map +1 -0
- package/dist/commands/workflows_lifecycle.js +37 -0
- package/dist/commands/workflows_lifecycle.js.map +1 -0
- package/dist/configure.d.ts +2 -2
- package/dist/configure.js +2 -2
- package/dist/providers/dashboard_provider.d.ts +57 -54
- package/dist/providers/dashboard_provider.d.ts.map +1 -1
- package/dist/providers/dashboard_provider.js +399 -288
- package/dist/providers/dashboard_provider.js.map +1 -1
- package/dist/providers/durable_provider.d.ts.map +1 -1
- package/dist/providers/durable_provider.js +67 -37
- package/dist/providers/durable_provider.js.map +1 -1
- package/dist/src/commands/console_command.d.ts +82 -0
- package/dist/src/commands/console_command.d.ts.map +1 -0
- package/dist/src/commands/console_command.js +238 -0
- package/dist/src/commands/console_command.js.map +1 -0
- package/dist/src/commands/runs.js +2 -2
- package/dist/src/commands/runs.js.map +1 -1
- package/dist/src/concurrency.d.ts.map +1 -1
- package/dist/src/concurrency.js +1 -0
- package/dist/src/concurrency.js.map +1 -1
- package/dist/src/config_types.d.ts +79 -26
- package/dist/src/config_types.d.ts.map +1 -1
- package/dist/src/dashboard/access_denied_page.d.ts +5 -7
- package/dist/src/dashboard/access_denied_page.d.ts.map +1 -1
- package/dist/src/dashboard/access_denied_page.js +2 -6
- package/dist/src/dashboard/access_denied_page.js.map +1 -1
- package/dist/src/dashboard/audit.d.ts +14 -0
- package/dist/src/dashboard/audit.d.ts.map +1 -0
- package/dist/src/dashboard/audit.js +36 -0
- package/dist/src/dashboard/audit.js.map +1 -0
- package/dist/src/dashboard/console_authorize.d.ts +98 -0
- package/dist/src/dashboard/console_authorize.d.ts.map +1 -0
- package/dist/src/dashboard/console_authorize.js +163 -0
- package/dist/src/dashboard/console_authorize.js.map +1 -0
- package/dist/src/dashboard/console_shell.d.ts +45 -0
- package/dist/src/dashboard/console_shell.d.ts.map +1 -0
- package/dist/src/dashboard/console_shell.js +125 -0
- package/dist/src/dashboard/console_shell.js.map +1 -0
- package/dist/src/dashboard/console_switcher.d.ts +132 -0
- package/dist/src/dashboard/console_switcher.d.ts.map +1 -0
- package/dist/src/dashboard/console_switcher.js +201 -0
- package/dist/src/dashboard/console_switcher.js.map +1 -0
- package/dist/src/dashboard/define_config.d.ts +68 -81
- package/dist/src/dashboard/define_config.d.ts.map +1 -1
- package/dist/src/dashboard/define_config.js +18 -112
- package/dist/src/dashboard/define_config.js.map +1 -1
- package/dist/src/dashboard/dev_tools.d.ts +102 -0
- package/dist/src/dashboard/dev_tools.d.ts.map +1 -0
- package/dist/src/dashboard/dev_tools.js +502 -0
- package/dist/src/dashboard/dev_tools.js.map +1 -0
- package/dist/src/dashboard/diagnostics-recorder.d.ts +1 -1
- package/dist/src/dashboard/diagnostics-recorder.js +1 -1
- package/dist/src/dashboard/gateway-adapter.d.ts +6 -6
- package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -1
- package/dist/src/dashboard/gateway-adapter.js +5 -5
- package/dist/src/dashboard/gateway-adapter.js.map +1 -1
- package/dist/src/dashboard/handlers.d.ts +48 -39
- package/dist/src/dashboard/handlers.d.ts.map +1 -1
- package/dist/src/dashboard/handlers.js +181 -83
- package/dist/src/dashboard/handlers.js.map +1 -1
- package/dist/src/dashboard/index.d.ts +12 -9
- package/dist/src/dashboard/index.d.ts.map +1 -1
- package/dist/src/dashboard/index.js +10 -7
- package/dist/src/dashboard/index.js.map +1 -1
- package/dist/src/dashboard/list-filters.d.ts +27 -0
- package/dist/src/dashboard/list-filters.d.ts.map +1 -0
- package/dist/src/dashboard/list-filters.js +69 -0
- package/dist/src/dashboard/list-filters.js.map +1 -0
- package/dist/src/dashboard/openapi.d.ts.map +1 -1
- package/dist/src/dashboard/openapi.js +127 -52
- package/dist/src/dashboard/openapi.js.map +1 -1
- package/dist/src/dashboard/run-filter.d.ts +18 -15
- package/dist/src/dashboard/run-filter.d.ts.map +1 -1
- package/dist/src/dashboard/run-filter.js +30 -22
- package/dist/src/dashboard/run-filter.js.map +1 -1
- package/dist/src/dashboard/spa.d.ts +26 -1
- package/dist/src/dashboard/spa.d.ts.map +1 -1
- package/dist/src/dashboard/spa.js +9 -2
- package/dist/src/dashboard/spa.js.map +1 -1
- package/dist/src/dashboard/tenant_scope.d.ts +38 -0
- package/dist/src/dashboard/tenant_scope.d.ts.map +1 -0
- package/dist/src/dashboard/tenant_scope.js +166 -0
- package/dist/src/dashboard/tenant_scope.js.map +1 -0
- package/dist/src/define_config.d.ts +27 -12
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js +9 -4
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/diagnostics/index.d.ts +11 -0
- package/dist/src/diagnostics/index.d.ts.map +1 -0
- package/dist/src/diagnostics/index.js +6 -0
- package/dist/src/diagnostics/index.js.map +1 -0
- package/dist/src/diagnostics/transport.d.ts +71 -0
- package/dist/src/diagnostics/transport.d.ts.map +1 -0
- package/dist/src/diagnostics/transport.js +188 -0
- package/dist/src/diagnostics/transport.js.map +1 -0
- package/dist/src/diagnostics-bridge.d.ts +2 -1
- package/dist/src/diagnostics-bridge.d.ts.map +1 -1
- package/dist/src/diagnostics-bridge.js +7 -1
- package/dist/src/diagnostics-bridge.js.map +1 -1
- package/dist/src/doctor/agora_doctor.d.ts +116 -0
- package/dist/src/doctor/agora_doctor.d.ts.map +1 -0
- package/dist/src/doctor/agora_doctor.js +685 -0
- package/dist/src/doctor/agora_doctor.js.map +1 -0
- package/dist/src/doctor/checks.d.ts +28 -0
- package/dist/src/doctor/checks.d.ts.map +1 -0
- package/dist/src/doctor/checks.js +288 -0
- package/dist/src/doctor/checks.js.map +1 -0
- package/dist/src/doctor/index.d.ts +9 -0
- package/dist/src/doctor/index.d.ts.map +1 -0
- package/dist/src/doctor/index.js +16 -0
- package/dist/src/doctor/index.js.map +1 -0
- package/dist/src/engine.d.ts +55 -9
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +333 -84
- package/dist/src/engine.js.map +1 -1
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +13 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/event-trigger-bridge.d.ts +5 -14
- package/dist/src/event-trigger-bridge.d.ts.map +1 -1
- package/dist/src/event-trigger-bridge.js +11 -99
- package/dist/src/event-trigger-bridge.js.map +1 -1
- package/dist/src/index.d.ts +8 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/interfaces.d.ts +53 -9
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/interfaces.js +8 -0
- package/dist/src/interfaces.js.map +1 -1
- package/dist/src/origin.js +3 -3
- package/dist/src/role_bindings.d.ts +8 -8
- package/dist/src/role_bindings.js +8 -8
- package/dist/src/run-gateway/interface.d.ts +4 -3
- package/dist/src/run-gateway/interface.d.ts.map +1 -1
- package/dist/src/run-gateway/proxy-run-gateway.d.ts +4 -2
- package/dist/src/run-gateway/proxy-run-gateway.d.ts.map +1 -1
- package/dist/src/run-gateway/proxy-run-gateway.js +21 -6
- package/dist/src/run-gateway/proxy-run-gateway.js.map +1 -1
- package/dist/src/run-gateway/run-request-responder.d.ts +6 -1
- package/dist/src/run-gateway/run-request-responder.d.ts.map +1 -1
- package/dist/src/run-gateway/run-request-responder.js +30 -3
- package/dist/src/run-gateway/run-request-responder.js.map +1 -1
- package/dist/src/run-gateway/store-run-gateway.d.ts +4 -4
- package/dist/src/run-gateway/store-run-gateway.d.ts.map +1 -1
- package/dist/src/run-gateway/store-run-gateway.js +4 -4
- package/dist/src/run-gateway/store-run-gateway.js.map +1 -1
- package/dist/src/run-gateway/tenant-auth.d.ts +19 -1
- package/dist/src/run-gateway/tenant-auth.d.ts.map +1 -1
- package/dist/src/run-gateway/tenant-auth.js +133 -0
- package/dist/src/run-gateway/tenant-auth.js.map +1 -1
- package/dist/src/run-gateway/tenant-scoped-gateway.d.ts +10 -0
- package/dist/src/run-gateway/tenant-scoped-gateway.d.ts.map +1 -0
- package/dist/src/run-gateway/tenant-scoped-gateway.js +107 -0
- package/dist/src/run-gateway/tenant-scoped-gateway.js.map +1 -0
- package/dist/src/run-value-facets.d.ts +1 -1
- package/dist/src/run-value-facets.d.ts.map +1 -1
- package/dist/src/run-value-facets.js +5 -1
- package/dist/src/run-value-facets.js.map +1 -1
- package/dist/src/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler.js +2 -1
- package/dist/src/scheduler.js.map +1 -1
- package/dist/src/schedules.d.ts +16 -1
- package/dist/src/schedules.d.ts.map +1 -1
- package/dist/src/schedules.js +29 -4
- package/dist/src/schedules.js.map +1 -1
- package/dist/src/services/main.d.ts +3 -3
- package/dist/src/services/main.js +6 -6
- package/dist/src/services/main.js.map +1 -1
- package/dist/src/singleton-admission.d.ts.map +1 -1
- package/dist/src/singleton-admission.js +2 -0
- package/dist/src/singleton-admission.js.map +1 -1
- package/dist/src/singleton-gate.d.ts +1 -1
- package/dist/src/singleton-gate.d.ts.map +1 -1
- package/dist/src/singleton-gate.js +4 -3
- package/dist/src/singleton-gate.js.map +1 -1
- package/dist/src/sql-schema.d.ts +54 -0
- package/dist/src/sql-schema.d.ts.map +1 -0
- package/dist/src/sql-schema.js +145 -0
- package/dist/src/sql-schema.js.map +1 -0
- package/dist/src/stores/factory.d.ts +6 -0
- package/dist/src/stores/factory.d.ts.map +1 -1
- package/dist/src/stores/factory.js +3 -0
- package/dist/src/stores/factory.js.map +1 -1
- package/dist/src/stores/lucid-mappers.d.ts +3 -0
- package/dist/src/stores/lucid-mappers.d.ts.map +1 -1
- package/dist/src/stores/lucid-mappers.js +12 -0
- package/dist/src/stores/lucid-mappers.js.map +1 -1
- package/dist/src/stores/lucid-pg-hot-path.d.ts +6 -2
- package/dist/src/stores/lucid-pg-hot-path.d.ts.map +1 -1
- package/dist/src/stores/lucid-pg-hot-path.js +7 -3
- package/dist/src/stores/lucid-pg-hot-path.js.map +1 -1
- package/dist/src/stores/lucid-schema.d.ts +18 -4
- package/dist/src/stores/lucid-schema.d.ts.map +1 -1
- package/dist/src/stores/lucid-schema.js +106 -61
- package/dist/src/stores/lucid-schema.js.map +1 -1
- package/dist/src/stores/lucid.d.ts +42 -2
- package/dist/src/stores/lucid.d.ts.map +1 -1
- package/dist/src/stores/lucid.js +450 -231
- package/dist/src/stores/lucid.js.map +1 -1
- package/dist/src/tenancy.d.ts +104 -0
- package/dist/src/tenancy.d.ts.map +1 -0
- package/dist/src/tenancy.js +91 -0
- package/dist/src/tenancy.js.map +1 -0
- package/dist/src/testing/in-memory-state-store.d.ts.map +1 -1
- package/dist/src/testing/in-memory-state-store.js +9 -3
- package/dist/src/testing/in-memory-state-store.js.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.js +59 -0
- package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
- package/dist/src/testing-kit/transport-conformance.d.ts.map +1 -1
- package/dist/src/testing-kit/transport-conformance.js +5 -1
- package/dist/src/testing-kit/transport-conformance.js.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.d.ts +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.js +14 -3
- package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
- package/dist/src/transports/db-schema.d.ts +13 -5
- package/dist/src/transports/db-schema.d.ts.map +1 -1
- package/dist/src/transports/db-schema.js +52 -27
- package/dist/src/transports/db-schema.js.map +1 -1
- package/dist/src/transports/db.d.ts +6 -0
- package/dist/src/transports/db.d.ts.map +1 -1
- package/dist/src/transports/db.js +91 -42
- package/dist/src/transports/db.js.map +1 -1
- package/dist/src/transports/factory.d.ts +16 -0
- package/dist/src/transports/factory.d.ts.map +1 -1
- package/dist/src/transports/factory.js +6 -1
- package/dist/src/transports/factory.js.map +1 -1
- package/dist/src/transports/queue.d.ts +2 -2
- package/dist/src/transports/queue.d.ts.map +1 -1
- package/dist/src/transports/queue.js +49 -8
- package/dist/src/transports/queue.js.map +1 -1
- package/dist/src/transports/worker-keys-db.d.ts +5 -0
- package/dist/src/transports/worker-keys-db.d.ts.map +1 -1
- package/dist/src/transports/worker-keys-db.js +10 -8
- package/dist/src/transports/worker-keys-db.js.map +1 -1
- package/dist/src/workflow-discovery.d.ts +1 -1
- package/dist/src/workflow-discovery.d.ts.map +1 -1
- package/dist/src/workflow-discovery.js +4 -7
- package/dist/src/workflow-discovery.js.map +1 -1
- package/dist/src/workflow-events.d.ts +20 -51
- package/dist/src/workflow-events.d.ts.map +1 -1
- package/dist/src/workflow-events.js +12 -30
- package/dist/src/workflow-events.js.map +1 -1
- package/dist/src/workflow-ref.d.ts +1 -1
- package/dist/stubs/config/durable_dashboard.stub +9 -29
- package/dist/stubs/database/migrations/create_durable_tables.stub +12 -2
- package/dist/stubs/database/migrations/create_durable_transport_messages.stub +34 -5
- package/dist/stubs/database/migrations/create_durable_transport_tables.stub +30 -8
- package/dist/stubs/database/migrations/create_durable_transport_worker_keys.stub +24 -2
- package/package.json +25 -7
- package/skills/durable-cluster/SKILL.md +43 -23
- package/skills/durable-reliability/SKILL.md +13 -7
- package/dist/assets/spa/assets/index-70hGzOnT.css +0 -1
- package/dist/assets/spa/assets/index-CJjMAUau.js +0 -16
- package/dist/assets/spa/assets/index-CJjMAUau.js.map +0 -1
- package/dist/commands/retry.d.ts +0 -16
- package/dist/commands/retry.d.ts.map +0 -1
- package/dist/commands/retry.js +0 -34
- package/dist/commands/retry.js.map +0 -1
- package/dist/src/dashboard/auth.d.ts +0 -134
- package/dist/src/dashboard/auth.d.ts.map +0 -1
- package/dist/src/dashboard/auth.js +0 -156
- package/dist/src/dashboard/auth.js.map +0 -1
- package/dist/src/dashboard/login_page.d.ts +0 -36
- package/dist/src/dashboard/login_page.d.ts.map +0 -1
- package/dist/src/dashboard/login_page.js +0 -158
- package/dist/src/dashboard/login_page.js.map +0 -1
- package/dist/src/dashboard/session_cookie.d.ts +0 -45
- package/dist/src/dashboard/session_cookie.d.ts.map +0 -1
- package/dist/src/dashboard/session_cookie.js +0 -97
- package/dist/src/dashboard/session_cookie.js.map +0 -1
package/dist/src/stores/lucid.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { RunAlreadyExistsError } from '../errors.js';
|
|
3
|
+
import { RECOVERABLE_RUN_STATUSES, SETTLEABLE_RUN_STATUSES } from '../interfaces.js';
|
|
2
4
|
import { namespaceFilterValues, normalizeNamespace } from '../namespaces.js';
|
|
3
5
|
import { mergeRunFacetRows } from '../run-facets.js';
|
|
4
6
|
import { runPageWindow } from '../run-pagination.js';
|
|
5
7
|
import { axisIsRunColumn, mergeRunValueFacetRows, RUN_VALUE_FACET_LIMIT, RUN_VALUE_FACET_SCAN, runValueFacetsFromRuns, } from '../run-value-facets.js';
|
|
6
8
|
import { attributeColumnFor, attributeOperand, attributeOperands, normalizeAttributeRows, sqlComparator, } from '../search-attributes.js';
|
|
7
9
|
import { SINGLETON_ACTIVE_STATUSES, SINGLETON_ADMITTED_TAG, settlesTerminally, singletonAdmissionDenialPatch, singletonAdmissionTags, withoutAdmission, } from '../singleton-admission.js';
|
|
10
|
+
import { assertSchemaSupported, hasColumn, hasTable, qualify, qualifyAll, schemaExists as schemaExistsIn, validateSchemaName, } from '../sql-schema.js';
|
|
8
11
|
import { CHECKPOINT_COLUMNS_BUT_INPUT, checkpointToRow, parseJsonColumn, rowToCheckpoint, rowToRun, rowToSignalWaiter, runPatchToRow, runToRow, SETTLED_CHECKPOINT_COLUMNS, settlementToRow, } from './lucid-mappers.js';
|
|
9
12
|
import { LucidPgHotPath, quote } from './lucid-pg-hot-path.js';
|
|
10
|
-
import { createDurableTables, DURABLE_TABLES } from './lucid-schema.js';
|
|
13
|
+
import { createDurableTables, DURABLE_SCHEMA_META_TABLE, DURABLE_TABLES } from './lucid-schema.js';
|
|
11
14
|
/**
|
|
12
15
|
* A production-grade, persistent `StateStore` backed by AdonisJS **Lucid** (Knex). Runs, checkpoints,
|
|
13
16
|
* timers, signal waiters, run attributes and recovery leases live in SQL, so durable state survives
|
|
@@ -25,11 +28,22 @@ export class LucidStateStore {
|
|
|
25
28
|
options;
|
|
26
29
|
/** The namespace run reads are fenced to, or undefined for the operator view. */
|
|
27
30
|
scopeNamespace;
|
|
31
|
+
/** The schema the tables live in, or undefined for the connection's current one. */
|
|
32
|
+
schema;
|
|
33
|
+
/** The durable table names, qualified with {@link schema}, for the query builder. */
|
|
34
|
+
t;
|
|
35
|
+
/** The Postgres hot path's statements over {@link t}. */
|
|
36
|
+
sql;
|
|
28
37
|
constructor(db, options = {}) {
|
|
29
38
|
this.db = db;
|
|
30
39
|
this.connectionName = options.connectionName;
|
|
31
40
|
this.options = options;
|
|
32
41
|
this.scopeNamespace = options.scope?.namespace;
|
|
42
|
+
this.schema = validateSchemaName(options.schema, 'stores.lucid');
|
|
43
|
+
if (this.schema !== undefined)
|
|
44
|
+
assertSchemaSupported(this.client(), this.schema, 'stores.lucid');
|
|
45
|
+
this.t = qualifyAll(DURABLE_TABLES, this.schema);
|
|
46
|
+
this.sql = hotStatements(this.schema);
|
|
33
47
|
}
|
|
34
48
|
/**
|
|
35
49
|
* A store over the same `Database` whose run reads are confined to `scope.namespace` (see
|
|
@@ -51,19 +65,108 @@ export class LucidStateStore {
|
|
|
51
65
|
}
|
|
52
66
|
/**
|
|
53
67
|
* The Postgres hot path (see `lucid-pg-hot-path.ts`): the statements a turn and a step result run
|
|
54
|
-
* on every step, as constant SQL. `undefined` on any other dialect,
|
|
55
|
-
* Resolved per call, like {@link client}
|
|
68
|
+
* on every step, as constant SQL. `undefined` on any other dialect, with Lucid's `debug` on, or
|
|
69
|
+
* under a (global) transaction. Resolved per call, like {@link client}.
|
|
56
70
|
*/
|
|
57
71
|
hot() {
|
|
72
|
+
// Not on a transaction (an app's global one): see `LucidPgHotPath.for`.
|
|
58
73
|
return LucidPgHotPath.for(this.client(), {
|
|
59
74
|
preparedStatements: this.options.preparedStatements,
|
|
60
75
|
});
|
|
61
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The hot reads with explicit column lists: each table's base columns plus the later-wave ones it
|
|
79
|
+
* has, probed once (an older schema without them keeps working) and again after this store's own
|
|
80
|
+
* {@link ensureSchema}. A column another process adds later is not read until the next probe, and a
|
|
81
|
+
* statement already prepared keeps its shape.
|
|
82
|
+
*/
|
|
83
|
+
hotReadStatements;
|
|
84
|
+
hotReads(hot) {
|
|
85
|
+
this.hotReadStatements ??= hot
|
|
86
|
+
.rows(COLUMNS_PRESENT_SQL, [
|
|
87
|
+
this.schema ?? null,
|
|
88
|
+
[DURABLE_TABLES.runs, DURABLE_TABLES.checkpoints],
|
|
89
|
+
[...RUN_OPTIONAL_COLUMNS, ...CHECKPOINT_OPTIONAL_COLUMNS],
|
|
90
|
+
])
|
|
91
|
+
.then((rows) => {
|
|
92
|
+
const present = new Set(rows.map((r) => `${String(r.table_name)}.${String(r.column_name)}`));
|
|
93
|
+
const has = (table) => (c) => present.has(`${table}.${c}`);
|
|
94
|
+
const runCols = [
|
|
95
|
+
...RUN_BASE_COLUMNS,
|
|
96
|
+
...RUN_OPTIONAL_COLUMNS.filter(has(DURABLE_TABLES.runs)),
|
|
97
|
+
];
|
|
98
|
+
const checkpointCols = [
|
|
99
|
+
...CHECKPOINT_BASE_COLUMNS,
|
|
100
|
+
...CHECKPOINT_OPTIONAL_COLUMNS.filter(has(DURABLE_TABLES.checkpoints)),
|
|
101
|
+
];
|
|
102
|
+
return this.sql.reads(runCols.map(quote).join(', '), checkpointCols.map(quote).join(', '));
|
|
103
|
+
})
|
|
104
|
+
.catch((error) => {
|
|
105
|
+
this.hotReadStatements = undefined; // a failed probe is retried, never cached
|
|
106
|
+
throw error;
|
|
107
|
+
});
|
|
108
|
+
return this.hotReadStatements;
|
|
109
|
+
}
|
|
62
110
|
/** Idempotently provision the durable tables (called on boot when `autoSchema` is on). */
|
|
63
111
|
async ensureSchema() {
|
|
64
|
-
await createDurableTables(this.db, this.connectionName,
|
|
112
|
+
await createDurableTables(this.db, this.connectionName, {
|
|
113
|
+
...(this.options.collation !== undefined ? { collation: this.options.collation } : {}),
|
|
114
|
+
...(this.schema !== undefined ? { schema: this.schema } : {}),
|
|
115
|
+
});
|
|
116
|
+
// The tables may have gained columns: the next hot read probes them again.
|
|
117
|
+
this.hotReadStatements = undefined;
|
|
118
|
+
}
|
|
119
|
+
/** The database schema the store's tables live in, or `undefined` for the connection's current one. */
|
|
120
|
+
get schemaName() {
|
|
121
|
+
return this.schema;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* What `agora:doctor` reports for a store with a `schema`: whether the schema exists, which durable
|
|
125
|
+
* tables are missing from it, which of `optionalTables` (tables other pieces create when they first
|
|
126
|
+
* run, like the db transport's) are in it, and whether the connection can write there (a probe row
|
|
127
|
+
* inserted into the schema marker table, in a transaction that is rolled back).
|
|
128
|
+
*/
|
|
129
|
+
async inspectSchema(optionalTables = []) {
|
|
130
|
+
const client = this.client();
|
|
131
|
+
const schema = this.schema;
|
|
132
|
+
const schemaExists = schema === undefined || (await schemaExistsIn(client, schema));
|
|
133
|
+
const missingTables = [];
|
|
134
|
+
for (const table of [...Object.values(DURABLE_TABLES), DURABLE_SCHEMA_META_TABLE]) {
|
|
135
|
+
if (!schemaExists || !(await hasTable(client, schema, table)))
|
|
136
|
+
missingTables.push(table);
|
|
137
|
+
}
|
|
138
|
+
const optionalPresent = [];
|
|
139
|
+
if (schemaExists) {
|
|
140
|
+
for (const table of optionalTables) {
|
|
141
|
+
if (await hasTable(client, schema, table))
|
|
142
|
+
optionalPresent.push(table);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
let writeError;
|
|
146
|
+
if (!missingTables.includes(DURABLE_SCHEMA_META_TABLE)) {
|
|
147
|
+
const rollback = new Error('durable doctor probe rollback');
|
|
148
|
+
try {
|
|
149
|
+
await client.transaction(async (trx) => {
|
|
150
|
+
await trx
|
|
151
|
+
.insertQuery()
|
|
152
|
+
.table(qualify(schema, DURABLE_SCHEMA_META_TABLE))
|
|
153
|
+
.insert({ id: '__doctor_probe__', fingerprint: 'probe', applied_at: Date.now() });
|
|
154
|
+
throw rollback;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error !== rollback)
|
|
159
|
+
writeError = error instanceof Error ? error.message : String(error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { schema, schemaExists, missingTables, optionalPresent, writeError };
|
|
65
163
|
}
|
|
66
164
|
// --- runs ---------------------------------------------------------------
|
|
165
|
+
/**
|
|
166
|
+
* Insert the run, or reject with {@link RunAlreadyExistsError} when its id is taken. The insert
|
|
167
|
+
* writes nothing on a conflict instead of failing, so a run that already exists never aborts a
|
|
168
|
+
* surrounding transaction (an app's test `wrapInGlobalTransaction`, on Postgres).
|
|
169
|
+
*/
|
|
67
170
|
async createRun(run) {
|
|
68
171
|
if (normalizeAttributeRows(run.id, run.searchAttributes).length === 0) {
|
|
69
172
|
// Nothing to index: the insert, then clear any attribute rows a deleted run of this id left,
|
|
@@ -72,17 +175,23 @@ export class LucidStateStore {
|
|
|
72
175
|
if (hot) {
|
|
73
176
|
const row = runToRow(run);
|
|
74
177
|
const keys = Object.keys(row);
|
|
75
|
-
await hot.
|
|
76
|
-
|
|
178
|
+
const inserted = await hot.rows(this.sql.insertRunIfAbsent(keys), keys.map((k) => row[k]));
|
|
179
|
+
if (inserted.length === 0)
|
|
180
|
+
throw new RunAlreadyExistsError(run.id);
|
|
181
|
+
await hot.run(this.sql.deleteAttributes, [run.id]);
|
|
77
182
|
return;
|
|
78
183
|
}
|
|
79
184
|
const client = this.client();
|
|
80
|
-
await client.
|
|
81
|
-
|
|
185
|
+
if (!(await insertIfAbsent(client, this.t.runs, runToRow(run), ['id']))) {
|
|
186
|
+
throw new RunAlreadyExistsError(run.id);
|
|
187
|
+
}
|
|
188
|
+
await client.from(this.t.attributes).where('run_id', run.id).delete();
|
|
82
189
|
return;
|
|
83
190
|
}
|
|
84
191
|
await this.client().transaction(async (trx) => {
|
|
85
|
-
await trx.
|
|
192
|
+
if (!(await insertIfAbsent(trx, this.t.runs, runToRow(run), ['id']))) {
|
|
193
|
+
throw new RunAlreadyExistsError(run.id);
|
|
194
|
+
}
|
|
86
195
|
await this.reindexAttributes(trx, run.id, run.searchAttributes);
|
|
87
196
|
});
|
|
88
197
|
}
|
|
@@ -102,7 +211,7 @@ export class LucidStateStore {
|
|
|
102
211
|
const row = await this.releaseSlot(this.client(), runId, patch);
|
|
103
212
|
const keys = Object.keys(row);
|
|
104
213
|
if (keys.length) {
|
|
105
|
-
await hot.run(
|
|
214
|
+
await hot.run(this.sql.updateRun(keys, false), [...keys.map((k) => row[k]), runId]);
|
|
106
215
|
}
|
|
107
216
|
return;
|
|
108
217
|
}
|
|
@@ -110,7 +219,7 @@ export class LucidStateStore {
|
|
|
110
219
|
const row = await this.releaseSlot(trx, runId, patch);
|
|
111
220
|
// Knex throws on an empty `.update({})`; skip the UPDATE when no mapped column changed.
|
|
112
221
|
if (Object.keys(row).length) {
|
|
113
|
-
await trx.from(
|
|
222
|
+
await trx.from(this.t.runs).where('id', runId).update(row);
|
|
114
223
|
}
|
|
115
224
|
if ('searchAttributes' in patch) {
|
|
116
225
|
await this.reindexAttributes(trx, runId, patch.searchAttributes);
|
|
@@ -123,10 +232,10 @@ export class LucidStateStore {
|
|
|
123
232
|
const row = await this.releaseSlot(this.client(), runId, patch);
|
|
124
233
|
const keys = Object.keys(row);
|
|
125
234
|
if (keys.length === 0) {
|
|
126
|
-
const found = await hot.rows(
|
|
235
|
+
const found = await hot.rows(this.sql.runInStatuses, [runId, expectedStatuses]);
|
|
127
236
|
return found.length > 0;
|
|
128
237
|
}
|
|
129
|
-
const { count } = await hot.run(
|
|
238
|
+
const { count } = await hot.run(this.sql.updateRun(keys, true), [
|
|
130
239
|
...keys.map((k) => row[k]),
|
|
131
240
|
runId,
|
|
132
241
|
expectedStatuses,
|
|
@@ -141,7 +250,7 @@ export class LucidStateStore {
|
|
|
141
250
|
// structure — the database arbitrates, not a prior JS read. Two racing writers can never both
|
|
142
251
|
// report `true` for the same row.
|
|
143
252
|
const affected = await trx
|
|
144
|
-
.from(
|
|
253
|
+
.from(this.t.runs)
|
|
145
254
|
.where('id', runId)
|
|
146
255
|
.whereIn('status', expectedStatuses)
|
|
147
256
|
.update(row);
|
|
@@ -151,7 +260,7 @@ export class LucidStateStore {
|
|
|
151
260
|
// Knex throws on an empty `.update({})`. Nothing to write, but the predicate's outcome is
|
|
152
261
|
// still observable: report whether it would have matched, without mutating anything.
|
|
153
262
|
const current = await trx
|
|
154
|
-
.from(
|
|
263
|
+
.from(this.t.runs)
|
|
155
264
|
.where('id', runId)
|
|
156
265
|
.whereIn('status', expectedStatuses)
|
|
157
266
|
.first();
|
|
@@ -178,7 +287,7 @@ export class LucidStateStore {
|
|
|
178
287
|
// Sorted, so two batches sharing runs take their row locks in the same order.
|
|
179
288
|
const ids = [...new Set(runIds)].sort();
|
|
180
289
|
await this.client()
|
|
181
|
-
.from(
|
|
290
|
+
.from(this.t.runs)
|
|
182
291
|
.whereIn('id', ids)
|
|
183
292
|
.where('status', 'suspended')
|
|
184
293
|
.andWhere((w) => w.whereNull('wake_at').orWhere('wake_at', '>', wakeAt))
|
|
@@ -188,9 +297,7 @@ export class LucidStateStore {
|
|
|
188
297
|
decisionMarkerColumn;
|
|
189
298
|
hasDecisionMarkerColumn(trx) {
|
|
190
299
|
// Probed on the caller's transaction: a single-connection pool (SQLite) has no other to use.
|
|
191
|
-
this.decisionMarkerColumn ??= trx.schema
|
|
192
|
-
.hasColumn(DURABLE_TABLES.runs, 'awaiting_decision_task_id')
|
|
193
|
-
.catch(() => {
|
|
300
|
+
this.decisionMarkerColumn ??= hasColumn(trx, this.schema, DURABLE_TABLES.runs, 'awaiting_decision_task_id').catch(() => {
|
|
194
301
|
this.decisionMarkerColumn = undefined; // a failed probe is retried, never cached
|
|
195
302
|
return true;
|
|
196
303
|
});
|
|
@@ -225,7 +332,7 @@ export class LucidStateStore {
|
|
|
225
332
|
tags = patch.tags;
|
|
226
333
|
}
|
|
227
334
|
else {
|
|
228
|
-
const q = trx.from(
|
|
335
|
+
const q = trx.from(this.t.runs).where('id', runId).select('tags');
|
|
229
336
|
if (locksRows(trx))
|
|
230
337
|
q.forUpdate();
|
|
231
338
|
const current = (await q.first());
|
|
@@ -239,7 +346,7 @@ export class LucidStateStore {
|
|
|
239
346
|
// Every active run sharing the key. The tag LIKE may over-match (it is only a lock-scope
|
|
240
347
|
// narrowing); the decision re-filters exactly.
|
|
241
348
|
const sharing = () => trx
|
|
242
|
-
.from(
|
|
349
|
+
.from(this.t.runs)
|
|
243
350
|
.where('workflow', workflow)
|
|
244
351
|
.whereIn('status', SINGLETON_ACTIVE_STATUSES)
|
|
245
352
|
.whereRaw(`tags like ? escape '!'`, [`%"${escapeLike(tag)}"%`]);
|
|
@@ -263,13 +370,13 @@ export class LucidStateStore {
|
|
|
263
370
|
if (!tags) {
|
|
264
371
|
const parked = singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt);
|
|
265
372
|
if (parked) {
|
|
266
|
-
await trx.from(
|
|
373
|
+
await trx.from(this.t.runs).where('id', runId).update(runPatchToRow(parked));
|
|
267
374
|
}
|
|
268
375
|
return false;
|
|
269
376
|
}
|
|
270
377
|
// Already a holder (a resumed turn): nothing to write.
|
|
271
378
|
if (!runs.find((r) => r.id === runId)?.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
272
|
-
await trx.from(
|
|
379
|
+
await trx.from(this.t.runs).where('id', runId).update(runPatchToRow({ tags }));
|
|
273
380
|
}
|
|
274
381
|
return true;
|
|
275
382
|
});
|
|
@@ -277,19 +384,20 @@ export class LucidStateStore {
|
|
|
277
384
|
async getRun(runId) {
|
|
278
385
|
const hot = this.hot();
|
|
279
386
|
if (hot) {
|
|
387
|
+
const sql = await this.hotReads(hot);
|
|
280
388
|
const rows = this.scopeNamespace === undefined
|
|
281
|
-
? await hot.rows(
|
|
282
|
-
: await hot.rows(
|
|
389
|
+
? await hot.rows(sql.get, [runId])
|
|
390
|
+
: await hot.rows(sql.getScoped, [runId, namespaceFilterValues(this.scopeNamespace)]);
|
|
283
391
|
return rows[0] ? rowToRun(rows[0]) : null;
|
|
284
392
|
}
|
|
285
|
-
const row = await this.fenced(this.client().from(
|
|
393
|
+
const row = await this.fenced(this.client().from(this.t.runs).where('id', runId)).first();
|
|
286
394
|
return row ? rowToRun(row) : null;
|
|
287
395
|
}
|
|
288
396
|
async getRuns(runIds) {
|
|
289
397
|
const ids = [...new Set(runIds)];
|
|
290
398
|
if (ids.length === 0)
|
|
291
399
|
return [];
|
|
292
|
-
const rows = await this.fenced(this.client().from(
|
|
400
|
+
const rows = await this.fenced(this.client().from(this.t.runs).whereIn('id', ids));
|
|
293
401
|
return rows.map(rowToRun);
|
|
294
402
|
}
|
|
295
403
|
async deleteRun(runId) {
|
|
@@ -297,10 +405,10 @@ export class LucidStateStore {
|
|
|
297
405
|
// transaction so a crash can't leave a half-deleted run. (Buffered signals are token-keyed,
|
|
298
406
|
// not run-scoped, so they are not swept here.)
|
|
299
407
|
await this.client().transaction(async (trx) => {
|
|
300
|
-
await trx.from(
|
|
301
|
-
await trx.from(
|
|
302
|
-
await trx.from(
|
|
303
|
-
await trx.from(
|
|
408
|
+
await trx.from(this.t.checkpoints).where('run_id', runId).delete();
|
|
409
|
+
await trx.from(this.t.signalWaiters).where('run_id', runId).delete();
|
|
410
|
+
await trx.from(this.t.attributes).where('run_id', runId).delete();
|
|
411
|
+
await trx.from(this.t.runs).where('id', runId).delete();
|
|
304
412
|
});
|
|
305
413
|
}
|
|
306
414
|
async deleteRuns(runIds) {
|
|
@@ -312,10 +420,10 @@ export class LucidStateStore {
|
|
|
312
420
|
await this.client().transaction(async (trx) => {
|
|
313
421
|
for (let at = 0; at < ids.length; at += DELETE_RUNS_CHUNK) {
|
|
314
422
|
const chunk = ids.slice(at, at + DELETE_RUNS_CHUNK);
|
|
315
|
-
await trx.from(
|
|
316
|
-
await trx.from(
|
|
317
|
-
await trx.from(
|
|
318
|
-
await trx.from(
|
|
423
|
+
await trx.from(this.t.checkpoints).whereIn('run_id', chunk).delete();
|
|
424
|
+
await trx.from(this.t.signalWaiters).whereIn('run_id', chunk).delete();
|
|
425
|
+
await trx.from(this.t.attributes).whereIn('run_id', chunk).delete();
|
|
426
|
+
await trx.from(this.t.runs).whereIn('id', chunk).delete();
|
|
319
427
|
}
|
|
320
428
|
});
|
|
321
429
|
}
|
|
@@ -323,11 +431,11 @@ export class LucidStateStore {
|
|
|
323
431
|
async getCheckpoint(runId, seq) {
|
|
324
432
|
const hot = this.hot();
|
|
325
433
|
if (hot) {
|
|
326
|
-
const rows = await hot.rows(
|
|
434
|
+
const rows = await hot.rows((await this.hotReads(hot)).getCheckpoint, [runId, seq]);
|
|
327
435
|
return rows[0] ? rowToCheckpoint(rows[0]) : null;
|
|
328
436
|
}
|
|
329
437
|
const row = await this.client()
|
|
330
|
-
.from(
|
|
438
|
+
.from(this.t.checkpoints)
|
|
331
439
|
.where('run_id', runId)
|
|
332
440
|
.andWhere('seq', seq)
|
|
333
441
|
.first();
|
|
@@ -338,7 +446,7 @@ export class LucidStateStore {
|
|
|
338
446
|
const out = [];
|
|
339
447
|
for (let at = 0; at < unique.length; at += CHECKPOINT_BATCH) {
|
|
340
448
|
const rows = (await this.client()
|
|
341
|
-
.from(
|
|
449
|
+
.from(this.t.checkpoints)
|
|
342
450
|
.where('run_id', runId)
|
|
343
451
|
.whereIn('seq', unique.slice(at, at + CHECKPOINT_BATCH)));
|
|
344
452
|
out.push(...rows.map(rowToCheckpoint));
|
|
@@ -349,7 +457,7 @@ export class LucidStateStore {
|
|
|
349
457
|
const unique = [...new Set(seqs)];
|
|
350
458
|
const hot = this.hot();
|
|
351
459
|
if (hot && unique.length <= CHECKPOINT_BATCH) {
|
|
352
|
-
const rows = await hot.rows(
|
|
460
|
+
const rows = await hot.rows(this.sql.checkpointsForSettle, [runId, unique]);
|
|
353
461
|
return rows
|
|
354
462
|
.map(rowToCheckpoint)
|
|
355
463
|
.sort((a, b) => a.seq - b.seq);
|
|
@@ -357,7 +465,7 @@ export class LucidStateStore {
|
|
|
357
465
|
const out = [];
|
|
358
466
|
for (let at = 0; at < unique.length; at += CHECKPOINT_BATCH) {
|
|
359
467
|
const rows = (await this.client()
|
|
360
|
-
.from(
|
|
468
|
+
.from(this.t.checkpoints)
|
|
361
469
|
.where('run_id', runId)
|
|
362
470
|
.whereIn('seq', unique.slice(at, at + CHECKPOINT_BATCH))
|
|
363
471
|
.select(...CHECKPOINT_COLUMNS_BUT_INPUT));
|
|
@@ -374,18 +482,18 @@ export class LucidStateStore {
|
|
|
374
482
|
// One multi-row upsert per chunk, in one transaction: a rejection writes nothing.
|
|
375
483
|
await this.client().transaction(async (trx) => {
|
|
376
484
|
for (let at = 0; at < checkpoints.length; at += CHECKPOINT_UPSERT_BATCH) {
|
|
377
|
-
await upsertCheckpointRows(trx, checkpoints.slice(at, at + CHECKPOINT_UPSERT_BATCH));
|
|
485
|
+
await upsertCheckpointRows(trx, this.t.checkpoints, checkpoints.slice(at, at + CHECKPOINT_UPSERT_BATCH));
|
|
378
486
|
}
|
|
379
487
|
});
|
|
380
488
|
}
|
|
381
489
|
async saveCheckpointIfPending(checkpoint) {
|
|
382
|
-
return updateCheckpointIfPending(this.client(), checkpoint);
|
|
490
|
+
return updateCheckpointIfPending(this.client(), this.t.checkpoints, checkpoint);
|
|
383
491
|
}
|
|
384
492
|
async renewCheckpointLease(runId, seq, expectedWakeAt, wakeAt) {
|
|
385
493
|
// One guarded UPDATE: the row-level compare-and-set is atomic on every dialect, so a result or a
|
|
386
494
|
// re-dispatch committed since the caller's read makes this match nothing.
|
|
387
495
|
const query = this.client()
|
|
388
|
-
.from(
|
|
496
|
+
.from(this.t.checkpoints)
|
|
389
497
|
.where('run_id', runId)
|
|
390
498
|
.andWhere('seq', seq)
|
|
391
499
|
.andWhere('status', 'pending');
|
|
@@ -403,7 +511,7 @@ export class LucidStateStore {
|
|
|
403
511
|
// for a row is compared against the first one's write), then write every winner with one
|
|
404
512
|
// multi-row upsert per chunk.
|
|
405
513
|
return this.client().transaction(async (trx) => {
|
|
406
|
-
const pending = await lockPendingCheckpoints(trx, checkpoints, ['seq']);
|
|
514
|
+
const pending = await lockPendingCheckpoints(trx, this.t.checkpoints, checkpoints, ['seq']);
|
|
407
515
|
const wrote = [];
|
|
408
516
|
const writes = new Map();
|
|
409
517
|
for (const cp of checkpoints) {
|
|
@@ -419,7 +527,7 @@ export class LucidStateStore {
|
|
|
419
527
|
}
|
|
420
528
|
const rows = [...writes.values()];
|
|
421
529
|
for (let at = 0; at < rows.length; at += CHECKPOINT_UPSERT_BATCH) {
|
|
422
|
-
await upsertCheckpointRows(trx, rows.slice(at, at + CHECKPOINT_UPSERT_BATCH));
|
|
530
|
+
await upsertCheckpointRows(trx, this.t.checkpoints, rows.slice(at, at + CHECKPOINT_UPSERT_BATCH));
|
|
423
531
|
}
|
|
424
532
|
return wrote;
|
|
425
533
|
});
|
|
@@ -430,7 +538,7 @@ export class LucidStateStore {
|
|
|
430
538
|
return [];
|
|
431
539
|
if (settlements.length === 1) {
|
|
432
540
|
const affected = await this.client()
|
|
433
|
-
.from(
|
|
541
|
+
.from(this.t.checkpoints)
|
|
434
542
|
.where('run_id', only.runId)
|
|
435
543
|
.andWhere('seq', only.seq)
|
|
436
544
|
.andWhere('status', 'pending')
|
|
@@ -443,7 +551,7 @@ export class LucidStateStore {
|
|
|
443
551
|
// locked, so the upsert never inserts: its row carries just the columns an insert would require,
|
|
444
552
|
// and `input` is neither sent nor rewritten.
|
|
445
553
|
return this.client().transaction(async (trx) => {
|
|
446
|
-
const pending = await lockPendingCheckpoints(trx, settlements, [
|
|
554
|
+
const pending = await lockPendingCheckpoints(trx, this.t.checkpoints, settlements, [
|
|
447
555
|
'seq',
|
|
448
556
|
'name',
|
|
449
557
|
'kind',
|
|
@@ -472,7 +580,7 @@ export class LucidStateStore {
|
|
|
472
580
|
for (let at = 0; at < rows.length; at += CHECKPOINT_UPSERT_BATCH) {
|
|
473
581
|
await trx
|
|
474
582
|
.insertQuery()
|
|
475
|
-
.table(
|
|
583
|
+
.table(this.t.checkpoints)
|
|
476
584
|
.knexQuery.insert(rows.slice(at, at + CHECKPOINT_UPSERT_BATCH))
|
|
477
585
|
.onConflict(['run_id', 'seq'])
|
|
478
586
|
.merge([...SETTLED_CHECKPOINT_COLUMNS]);
|
|
@@ -495,7 +603,7 @@ export class LucidStateStore {
|
|
|
495
603
|
const live = results.filter((_, i) => first[i]);
|
|
496
604
|
const client = this.client();
|
|
497
605
|
const settled = client.dialect.name === 'postgres'
|
|
498
|
-
? await settleResultsReturningPg(client, this.hot(), live)
|
|
606
|
+
? await settleResultsReturningPg(client, this.hot(), this.schema, live)
|
|
499
607
|
: await this.settleResultsPlain(live);
|
|
500
608
|
const out = [];
|
|
501
609
|
let k = 0;
|
|
@@ -520,7 +628,7 @@ export class LucidStateStore {
|
|
|
520
628
|
const out = new Map();
|
|
521
629
|
const client = this.client();
|
|
522
630
|
const runRows = (await client
|
|
523
|
-
.from(
|
|
631
|
+
.from(this.t.runs)
|
|
524
632
|
.whereIn('id', [...new Set(results.map((r) => r.runId))])
|
|
525
633
|
.select('id', 'status', 'wake_at'));
|
|
526
634
|
const runs = new Map(runRows.map((row) => [row.id, row]));
|
|
@@ -585,36 +693,16 @@ export class LucidStateStore {
|
|
|
585
693
|
if (hot) {
|
|
586
694
|
const keys = Object.keys(row);
|
|
587
695
|
const values = keys.map((k) => row[k] ?? null);
|
|
588
|
-
return (await hot.rows(
|
|
589
|
-
}
|
|
590
|
-
if (client.dialect.name === 'postgres') {
|
|
591
|
-
const inserted = (await client
|
|
592
|
-
.insertQuery()
|
|
593
|
-
.table(DURABLE_TABLES.checkpoints)
|
|
594
|
-
.knexQuery.insert(row)
|
|
595
|
-
.onConflict(['run_id', 'seq'])
|
|
596
|
-
.ignore()
|
|
597
|
-
.returning('seq'));
|
|
598
|
-
return inserted.length > 0;
|
|
599
|
-
}
|
|
600
|
-
// MySQL and SQLite: a plain INSERT, whose duplicate-key error is the "already there" answer (an
|
|
601
|
-
// `INSERT IGNORE` would also swallow every other error, and reports no row count through Knex).
|
|
602
|
-
try {
|
|
603
|
-
await client.table(DURABLE_TABLES.checkpoints).insert(row);
|
|
604
|
-
return true;
|
|
605
|
-
}
|
|
606
|
-
catch (err) {
|
|
607
|
-
if (isDuplicateKey(err))
|
|
608
|
-
return false;
|
|
609
|
-
throw err;
|
|
696
|
+
return (await hot.rows(this.sql.insertCheckpointIfAbsent(keys), values)).length > 0;
|
|
610
697
|
}
|
|
698
|
+
return insertIfAbsent(client, this.t.checkpoints, row, ['run_id', 'seq']);
|
|
611
699
|
}
|
|
612
700
|
/** Persist the latest worker heartbeat on the step's checkpoint row (see the interface doc).
|
|
613
701
|
* A targeted two-column UPDATE — never an upsert: a beat racing the checkpoint's own insert (or
|
|
614
702
|
* outliving a deleted run) matches zero rows and is a silent no-op, by design. */
|
|
615
703
|
async recordStepHeartbeat(runId, seq, at, progress) {
|
|
616
704
|
await this.client()
|
|
617
|
-
.from(
|
|
705
|
+
.from(this.t.checkpoints)
|
|
618
706
|
.where('run_id', runId)
|
|
619
707
|
.andWhere('seq', seq)
|
|
620
708
|
.update({
|
|
@@ -632,14 +720,16 @@ export class LucidStateStore {
|
|
|
632
720
|
const row = checkpointToRow(checkpoint);
|
|
633
721
|
await client
|
|
634
722
|
.insertQuery()
|
|
635
|
-
.table(
|
|
723
|
+
.table(this.t.checkpoints)
|
|
636
724
|
.knexQuery.insert(row)
|
|
637
725
|
.onConflict(['run_id', 'seq'])
|
|
638
726
|
.merge();
|
|
639
727
|
}
|
|
640
728
|
// --- recovery / dispatch queries ----------------------------------------
|
|
641
729
|
async listIncompleteRuns(namespace) {
|
|
642
|
-
const q = this.fenced(this.client()
|
|
730
|
+
const q = this.fenced(this.client()
|
|
731
|
+
.from(this.t.runs)
|
|
732
|
+
.whereIn('status', [...RECOVERABLE_RUN_STATUSES]));
|
|
643
733
|
if (namespace !== undefined)
|
|
644
734
|
q.whereIn('namespace', namespaceFilterValues(namespace));
|
|
645
735
|
const rows = await q;
|
|
@@ -651,8 +741,8 @@ export class LucidStateStore {
|
|
|
651
741
|
// running run — and issues ~zero lock-probe UPDATEs. `owner` adds the runs still leased to that
|
|
652
742
|
// instance id (a rebooted instance's own leftovers; the engine filters what it is executing).
|
|
653
743
|
const q = this.fenced(this.client()
|
|
654
|
-
.from(
|
|
655
|
-
.
|
|
744
|
+
.from(this.t.runs)
|
|
745
|
+
.whereIn('status', [...RECOVERABLE_RUN_STATUSES])
|
|
656
746
|
.andWhere((sub) => {
|
|
657
747
|
sub.whereNull('locked_until').orWhere('locked_until', '<=', nowMs);
|
|
658
748
|
if (owner !== undefined)
|
|
@@ -664,7 +754,7 @@ export class LucidStateStore {
|
|
|
664
754
|
return rows.map(rowToRun);
|
|
665
755
|
}
|
|
666
756
|
async listPendingRuns(limit, namespace) {
|
|
667
|
-
const q = this.fenced(this.client().from(
|
|
757
|
+
const q = this.fenced(this.client().from(this.t.runs).where('status', 'pending'));
|
|
668
758
|
if (namespace !== undefined)
|
|
669
759
|
q.whereIn('namespace', namespaceFilterValues(namespace));
|
|
670
760
|
const rows = await q
|
|
@@ -691,9 +781,9 @@ export class LucidStateStore {
|
|
|
691
781
|
if (dialect === 'postgres') {
|
|
692
782
|
// One statement: pick the oldest free pending rows, skipping any another claimer holds locked
|
|
693
783
|
// right now, and lease them. Several pollers at once each get a different slice.
|
|
694
|
-
const pick = oldest(client.from(
|
|
784
|
+
const pick = oldest(client.from(this.t.runs)).forUpdate().skipLocked();
|
|
695
785
|
rows = (await client
|
|
696
|
-
.from(
|
|
786
|
+
.from(this.t.runs)
|
|
697
787
|
.whereIn('id', pick)
|
|
698
788
|
.update({ locked_by: owner, locked_until: leaseUntilMs })
|
|
699
789
|
.returning('*'));
|
|
@@ -702,20 +792,20 @@ export class LucidStateStore {
|
|
|
702
792
|
// No UPDATE … RETURNING: lock the batch (skipping rows another claimer holds), lease it and read
|
|
703
793
|
// it back, in one transaction.
|
|
704
794
|
rows = await client.transaction(async (trx) => {
|
|
705
|
-
const ids = (await oldest(trx.from(
|
|
795
|
+
const ids = (await oldest(trx.from(this.t.runs)).forUpdate().skipLocked()).map((r) => r.id);
|
|
706
796
|
if (ids.length === 0)
|
|
707
797
|
return [];
|
|
708
798
|
await trx
|
|
709
|
-
.from(
|
|
799
|
+
.from(this.t.runs)
|
|
710
800
|
.whereIn('id', ids)
|
|
711
801
|
.update({ locked_by: owner, locked_until: leaseUntilMs });
|
|
712
|
-
return (await trx.from(
|
|
802
|
+
return (await trx.from(this.t.runs).whereIn('id', ids));
|
|
713
803
|
});
|
|
714
804
|
}
|
|
715
805
|
else {
|
|
716
806
|
// SQLite has no row locks to skip: each run is taken with the lease's own compare-and-set.
|
|
717
807
|
const claimed = [];
|
|
718
|
-
const candidates = (await free(client.from(
|
|
808
|
+
const candidates = (await free(client.from(this.t.runs))
|
|
719
809
|
.orderBy('created_at', 'asc')
|
|
720
810
|
.orderBy('id', 'asc')
|
|
721
811
|
.limit(limit));
|
|
@@ -733,7 +823,7 @@ export class LucidStateStore {
|
|
|
733
823
|
async listDueTimers(nowMs, namespace, limit) {
|
|
734
824
|
const marker = await this.hasWakeMarkerColumn();
|
|
735
825
|
const q = this.fenced(this.client()
|
|
736
|
-
.from(
|
|
826
|
+
.from(this.t.runs)
|
|
737
827
|
.where('status', 'suspended')
|
|
738
828
|
.where((w) => {
|
|
739
829
|
w.where((d) => d.whereNotNull('wake_at').andWhere('wake_at', '<=', nowMs));
|
|
@@ -756,7 +846,7 @@ export class LucidStateStore {
|
|
|
756
846
|
// Atomic compare-and-set: acquire ONLY if currently unlocked or the lease has expired. The single
|
|
757
847
|
// conditional UPDATE is the concurrency guard — two instances racing can never both report `true`.
|
|
758
848
|
const affected = await this.client()
|
|
759
|
-
.from(
|
|
849
|
+
.from(this.t.runs)
|
|
760
850
|
.where('id', runId)
|
|
761
851
|
.andWhere((q) => {
|
|
762
852
|
q.whereNull('locked_until').orWhere('locked_until', '<=', nowMs);
|
|
@@ -767,9 +857,10 @@ export class LucidStateStore {
|
|
|
767
857
|
async tryLockRunAndGet(runId, owner, leaseUntilMs, nowMs) {
|
|
768
858
|
const hot = this.hot();
|
|
769
859
|
if (hot) {
|
|
860
|
+
const sql = await this.hotReads(hot);
|
|
770
861
|
const rows = this.scopeNamespace === undefined
|
|
771
|
-
? await hot.rows(
|
|
772
|
-
: await hot.rows(
|
|
862
|
+
? await hot.rows(sql.lockAndGet, [owner, leaseUntilMs, runId, nowMs])
|
|
863
|
+
: await hot.rows(sql.lockAndGetScoped, [
|
|
773
864
|
owner,
|
|
774
865
|
leaseUntilMs,
|
|
775
866
|
runId,
|
|
@@ -783,7 +874,7 @@ export class LucidStateStore {
|
|
|
783
874
|
// One statement: the lease's compare-and-set and the read of the row it locked. The scope
|
|
784
875
|
// fence sits in the WHERE clause, so a run outside it is neither locked nor read.
|
|
785
876
|
const rows = (await this.fenced(client
|
|
786
|
-
.from(
|
|
877
|
+
.from(this.t.runs)
|
|
787
878
|
.where('id', runId)
|
|
788
879
|
.andWhere((q) => {
|
|
789
880
|
q.whereNull('locked_until').orWhere('locked_until', '<=', nowMs);
|
|
@@ -804,15 +895,15 @@ export class LucidStateStore {
|
|
|
804
895
|
const hot = this.hot();
|
|
805
896
|
if (hot) {
|
|
806
897
|
if (owner === undefined)
|
|
807
|
-
await hot.run(
|
|
898
|
+
await hot.run(this.sql.releaseLock, [runId]);
|
|
808
899
|
else
|
|
809
|
-
await hot.run(
|
|
900
|
+
await hot.run(this.sql.releaseOwnLock, [runId, owner]);
|
|
810
901
|
return;
|
|
811
902
|
}
|
|
812
903
|
// Owner-scoped release (when `owner` is given): the `locked_by` predicate lives in the UPDATE's
|
|
813
904
|
// WHERE clause, so a stale executor whose lease was taken over matches zero rows and cannot
|
|
814
905
|
// wipe the new owner's lease — same conditional-write shape as renewRunLock.
|
|
815
|
-
const q = this.client().from(
|
|
906
|
+
const q = this.client().from(this.t.runs).where('id', runId);
|
|
816
907
|
if (owner !== undefined)
|
|
817
908
|
q.andWhere('locked_by', owner);
|
|
818
909
|
await q.update({ locked_by: null, locked_until: null });
|
|
@@ -823,19 +914,13 @@ export class LucidStateStore {
|
|
|
823
914
|
const marker = await this.hasWakeMarkerColumn();
|
|
824
915
|
const hot = this.hot();
|
|
825
916
|
if (hot) {
|
|
826
|
-
const released = await hot.run(marker ?
|
|
827
|
-
runId,
|
|
828
|
-
owner,
|
|
829
|
-
]);
|
|
917
|
+
const released = await hot.run(marker ? this.sql.releaseUnlessWake : this.sql.releaseOwnLock, [runId, owner]);
|
|
830
918
|
if (released.count === 1)
|
|
831
919
|
return true;
|
|
832
|
-
const rows = await hot.rows(
|
|
920
|
+
const rows = await hot.rows(this.sql.lockedBy, [runId]);
|
|
833
921
|
return rows[0]?.locked_by !== owner;
|
|
834
922
|
}
|
|
835
|
-
const q = this.client()
|
|
836
|
-
.from(DURABLE_TABLES.runs)
|
|
837
|
-
.where('id', runId)
|
|
838
|
-
.andWhere('locked_by', owner);
|
|
923
|
+
const q = this.client().from(this.t.runs).where('id', runId).andWhere('locked_by', owner);
|
|
839
924
|
if (marker)
|
|
840
925
|
q.whereNull('wake_requested_at');
|
|
841
926
|
const affected = await q.update({ locked_by: null, locked_until: null });
|
|
@@ -843,7 +928,7 @@ export class LucidStateStore {
|
|
|
843
928
|
return true;
|
|
844
929
|
// Nothing matched: the lease is not ours (any more), or a turn was asked for.
|
|
845
930
|
const row = (await this.client()
|
|
846
|
-
.from(
|
|
931
|
+
.from(this.t.runs)
|
|
847
932
|
.where('id', runId)
|
|
848
933
|
.select('locked_by')
|
|
849
934
|
.first());
|
|
@@ -851,9 +936,7 @@ export class LucidStateStore {
|
|
|
851
936
|
}
|
|
852
937
|
/** {@link hasWakeMarkerColumn} on the caller's transaction (a single-connection pool has no other). */
|
|
853
938
|
hasWakeMarkerColumnIn(trx) {
|
|
854
|
-
this.wakeMarkerColumn ??= trx.schema
|
|
855
|
-
.hasColumn(DURABLE_TABLES.runs, 'wake_requested_at')
|
|
856
|
-
.catch(() => {
|
|
939
|
+
this.wakeMarkerColumn ??= hasColumn(trx, this.schema, DURABLE_TABLES.runs, 'wake_requested_at').catch(() => {
|
|
857
940
|
this.wakeMarkerColumn = undefined;
|
|
858
941
|
return false;
|
|
859
942
|
});
|
|
@@ -862,9 +945,7 @@ export class LucidStateStore {
|
|
|
862
945
|
/** Whether the runs table has `wake_requested_at`, probed once per store and cached. */
|
|
863
946
|
wakeMarkerColumn;
|
|
864
947
|
hasWakeMarkerColumn() {
|
|
865
|
-
this.wakeMarkerColumn ??= this.client()
|
|
866
|
-
.schema.hasColumn(DURABLE_TABLES.runs, 'wake_requested_at')
|
|
867
|
-
.catch(() => {
|
|
948
|
+
this.wakeMarkerColumn ??= hasColumn(this.client(), this.schema, DURABLE_TABLES.runs, 'wake_requested_at').catch(() => {
|
|
868
949
|
this.wakeMarkerColumn = undefined;
|
|
869
950
|
return false;
|
|
870
951
|
});
|
|
@@ -873,10 +954,10 @@ export class LucidStateStore {
|
|
|
873
954
|
async renewRunLock(runId, owner, leaseUntilMs) {
|
|
874
955
|
const hot = this.hot();
|
|
875
956
|
if (hot)
|
|
876
|
-
return (await hot.run(
|
|
957
|
+
return (await hot.run(this.sql.renewLock, [leaseUntilMs, runId, owner])).count === 1;
|
|
877
958
|
// Extend ONLY if `owner` still holds the lease — a dead worker's lease still expires & is reclaimed.
|
|
878
959
|
const affected = await this.client()
|
|
879
|
-
.from(
|
|
960
|
+
.from(this.t.runs)
|
|
880
961
|
.where('id', runId)
|
|
881
962
|
.andWhere('locked_by', owner)
|
|
882
963
|
.update({ locked_until: leaseUntilMs });
|
|
@@ -888,7 +969,7 @@ export class LucidStateStore {
|
|
|
888
969
|
// runs once per wait registration, i.e. on every signal/child/event suspension).
|
|
889
970
|
await this.client()
|
|
890
971
|
.insertQuery()
|
|
891
|
-
.table(
|
|
972
|
+
.table(this.t.signalWaiters)
|
|
892
973
|
.knexQuery.insert({
|
|
893
974
|
token: waiter.token,
|
|
894
975
|
run_id: waiter.runId,
|
|
@@ -903,10 +984,10 @@ export class LucidStateStore {
|
|
|
903
984
|
// the same waiter to two callers. The DELETE arbitrates, so no transaction is needed around it,
|
|
904
985
|
// and a token with no waiter (the common case) costs one read.
|
|
905
986
|
const client = this.client();
|
|
906
|
-
const row = await client.from(
|
|
987
|
+
const row = await client.from(this.t.signalWaiters).where('token', token).first();
|
|
907
988
|
if (!row)
|
|
908
989
|
return null;
|
|
909
|
-
const deleted = await client.from(
|
|
990
|
+
const deleted = await client.from(this.t.signalWaiters).where('token', token).delete();
|
|
910
991
|
if (rowsAffected(deleted) !== 1)
|
|
911
992
|
return null;
|
|
912
993
|
return rowToSignalWaiter(row);
|
|
@@ -915,14 +996,14 @@ export class LucidStateStore {
|
|
|
915
996
|
// The prefix embeds caller-supplied names (event names via eventPrefix) — escape their LIKE
|
|
916
997
|
// metacharacters so a name containing `%`/`_` can't scan (or match) other tokens.
|
|
917
998
|
const rows = await this.client()
|
|
918
|
-
.from(
|
|
999
|
+
.from(this.t.signalWaiters)
|
|
919
1000
|
.whereRaw(`token like ? escape '!'`, [`${escapeLike(prefix)}%`]);
|
|
920
1001
|
return rows.map(rowToSignalWaiter);
|
|
921
1002
|
}
|
|
922
1003
|
async listSignalWaitersByRunIds(runIds) {
|
|
923
1004
|
if (runIds.length === 0)
|
|
924
1005
|
return [];
|
|
925
|
-
const rows = await this.client().from(
|
|
1006
|
+
const rows = await this.client().from(this.t.signalWaiters).whereIn('run_id', runIds);
|
|
926
1007
|
return rows.map(rowToSignalWaiter);
|
|
927
1008
|
}
|
|
928
1009
|
async hasSignalWaiter(runId, tokenPrefix) {
|
|
@@ -930,7 +1011,7 @@ export class LucidStateStore {
|
|
|
930
1011
|
// MySQL's default collations ignore case), so a hit is confirmed in JS. Only a match that is not
|
|
931
1012
|
// a real prefix match pays for reading the run's whole matching set.
|
|
932
1013
|
const matching = () => this.client()
|
|
933
|
-
.from(
|
|
1014
|
+
.from(this.t.signalWaiters)
|
|
934
1015
|
.select('token')
|
|
935
1016
|
.where('run_id', runId)
|
|
936
1017
|
.andWhereRaw(`token like ? escape '!'`, [`${escapeLike(tokenPrefix)}%`]);
|
|
@@ -953,7 +1034,7 @@ export class LucidStateStore {
|
|
|
953
1034
|
const chunk = entries.slice(at, at + CHILD_WAIT_ROWS);
|
|
954
1035
|
await trx
|
|
955
1036
|
.insertQuery()
|
|
956
|
-
.table(
|
|
1037
|
+
.table(this.t.signalWaiters)
|
|
957
1038
|
.knexQuery.insert(chunk.map(({ waiter }) => ({
|
|
958
1039
|
token: waiter.token,
|
|
959
1040
|
run_id: waiter.runId,
|
|
@@ -967,7 +1048,7 @@ export class LucidStateStore {
|
|
|
967
1048
|
const chunk = entries.slice(at, at + CHILD_WAIT_ROWS);
|
|
968
1049
|
await trx
|
|
969
1050
|
.insertQuery()
|
|
970
|
-
.table(
|
|
1051
|
+
.table(this.t.checkpoints)
|
|
971
1052
|
.knexQuery.insert(chunk.map(({ checkpoint }) => checkpointToRow(checkpoint)))
|
|
972
1053
|
.onConflict(['run_id', 'seq'])
|
|
973
1054
|
.merge();
|
|
@@ -977,7 +1058,7 @@ export class LucidStateStore {
|
|
|
977
1058
|
async compactCheckpoints(runId, fromSeq, toSeq, into) {
|
|
978
1059
|
await this.client().transaction(async (trx) => {
|
|
979
1060
|
await trx
|
|
980
|
-
.from(
|
|
1061
|
+
.from(this.t.checkpoints)
|
|
981
1062
|
.where('run_id', runId)
|
|
982
1063
|
.andWhere('seq', '>=', fromSeq)
|
|
983
1064
|
.andWhere('seq', '<=', toSeq)
|
|
@@ -990,7 +1071,7 @@ export class LucidStateStore {
|
|
|
990
1071
|
// primary key, so a later putSignalWaiter for the same token may have replaced the row with a
|
|
991
1072
|
// different run's waiter — the run_id/seq predicate leaves that one untouched. No-op if absent.
|
|
992
1073
|
await this.client()
|
|
993
|
-
.from(
|
|
1074
|
+
.from(this.t.signalWaiters)
|
|
994
1075
|
.where('token', waiter.token)
|
|
995
1076
|
.andWhere('run_id', waiter.runId)
|
|
996
1077
|
.andWhere('seq', waiter.seq)
|
|
@@ -999,12 +1080,12 @@ export class LucidStateStore {
|
|
|
999
1080
|
// --- buffered signals (FIFO per token) ----------------------------------
|
|
1000
1081
|
async bufferSignal(token, payload) {
|
|
1001
1082
|
await this.client()
|
|
1002
|
-
.table(
|
|
1083
|
+
.table(this.t.bufferedSignals)
|
|
1003
1084
|
.insert({ token, payload: payload === undefined ? null : JSON.stringify(payload) });
|
|
1004
1085
|
}
|
|
1005
1086
|
async listBufferedSignalTokens(prefix, limit) {
|
|
1006
1087
|
const rows = (await this.client()
|
|
1007
|
-
.from(
|
|
1088
|
+
.from(this.t.bufferedSignals)
|
|
1008
1089
|
.whereRaw(`token like ? escape '!'`, [`${escapeLike(prefix)}%`])
|
|
1009
1090
|
.distinct('token')
|
|
1010
1091
|
.orderBy('token', 'asc')
|
|
@@ -1013,7 +1094,7 @@ export class LucidStateStore {
|
|
|
1013
1094
|
}
|
|
1014
1095
|
async listBufferedSignals(tokenPrefix, afterId, limit) {
|
|
1015
1096
|
const q = this.client()
|
|
1016
|
-
.from(
|
|
1097
|
+
.from(this.t.bufferedSignals)
|
|
1017
1098
|
.whereRaw(`token like ? escape '!'`, [`${escapeLike(tokenPrefix)}%`]);
|
|
1018
1099
|
if (afterId !== undefined)
|
|
1019
1100
|
q.where('id', '>', Number(afterId));
|
|
@@ -1029,7 +1110,7 @@ export class LucidStateStore {
|
|
|
1029
1110
|
async deleteBufferedSignals(ids) {
|
|
1030
1111
|
for (let at = 0; at < ids.length; at += 500) {
|
|
1031
1112
|
await this.client()
|
|
1032
|
-
.from(
|
|
1113
|
+
.from(this.t.bufferedSignals)
|
|
1033
1114
|
.whereIn('id', ids.slice(at, at + 500).map((id) => Number(id)))
|
|
1034
1115
|
.delete();
|
|
1035
1116
|
}
|
|
@@ -1040,9 +1121,9 @@ export class LucidStateStore {
|
|
|
1040
1121
|
const client = this.client();
|
|
1041
1122
|
const hot = this.hot();
|
|
1042
1123
|
const row = hot
|
|
1043
|
-
? (await hot.rows(
|
|
1124
|
+
? (await hot.rows(this.sql.oldestBufferedSignal, [token]))[0]
|
|
1044
1125
|
: await client
|
|
1045
|
-
.from(
|
|
1126
|
+
.from(this.t.bufferedSignals)
|
|
1046
1127
|
.where('token', token)
|
|
1047
1128
|
.orderBy('id', 'asc')
|
|
1048
1129
|
.first();
|
|
@@ -1050,8 +1131,8 @@ export class LucidStateStore {
|
|
|
1050
1131
|
return null;
|
|
1051
1132
|
const r = row;
|
|
1052
1133
|
const deleted = hot
|
|
1053
|
-
? (await hot.run(
|
|
1054
|
-
: rowsAffected(await client.from(
|
|
1134
|
+
? (await hot.run(this.sql.deleteBufferedSignal, [r.id])).count
|
|
1135
|
+
: rowsAffected(await client.from(this.t.bufferedSignals).where('id', r.id).delete());
|
|
1055
1136
|
if (deleted !== 1)
|
|
1056
1137
|
return null;
|
|
1057
1138
|
return {
|
|
@@ -1061,7 +1142,7 @@ export class LucidStateStore {
|
|
|
1061
1142
|
// --- buffered events (name-keyed, match-based consumption) ---------------
|
|
1062
1143
|
async bufferEvent(input) {
|
|
1063
1144
|
await this.client()
|
|
1064
|
-
.table(
|
|
1145
|
+
.table(this.t.bufferedEvents)
|
|
1065
1146
|
.insert({
|
|
1066
1147
|
id: input.id,
|
|
1067
1148
|
name: input.name,
|
|
@@ -1072,9 +1153,9 @@ export class LucidStateStore {
|
|
|
1072
1153
|
async listBufferedEvents(name, limit) {
|
|
1073
1154
|
const hot = this.hot();
|
|
1074
1155
|
const rows = hot
|
|
1075
|
-
? await hot.rows(
|
|
1156
|
+
? await hot.rows(this.sql.bufferedEvents, [name, limit])
|
|
1076
1157
|
: await this.client()
|
|
1077
|
-
.from(
|
|
1158
|
+
.from(this.t.bufferedEvents)
|
|
1078
1159
|
.where('name', name)
|
|
1079
1160
|
.orderBy('published_at', 'asc') // oldest first
|
|
1080
1161
|
.limit(limit);
|
|
@@ -1091,7 +1172,7 @@ export class LucidStateStore {
|
|
|
1091
1172
|
// IN subquery. The age predicate is repeated on the DELETE so a row re-published under the same
|
|
1092
1173
|
// id meanwhile is left alone.
|
|
1093
1174
|
const rows = (await this.client()
|
|
1094
|
-
.from(
|
|
1175
|
+
.from(this.t.bufferedEvents)
|
|
1095
1176
|
.select('id')
|
|
1096
1177
|
.where('published_at', '<', publishedBefore)
|
|
1097
1178
|
.orderBy('published_at', 'asc')
|
|
@@ -1099,7 +1180,7 @@ export class LucidStateStore {
|
|
|
1099
1180
|
if (rows.length === 0)
|
|
1100
1181
|
return 0;
|
|
1101
1182
|
const deleted = await this.client()
|
|
1102
|
-
.from(
|
|
1183
|
+
.from(this.t.bufferedEvents)
|
|
1103
1184
|
.whereIn('id', rows.map((r) => r.id))
|
|
1104
1185
|
.andWhere('published_at', '<', publishedBefore)
|
|
1105
1186
|
.delete();
|
|
@@ -1108,10 +1189,7 @@ export class LucidStateStore {
|
|
|
1108
1189
|
async removeBufferedEvent(id) {
|
|
1109
1190
|
// Atomic claim: whoever's delete affects the row (returns 1) is the one that gets to deliver;
|
|
1110
1191
|
// a concurrent claimant sees 0 and backs off. `id` is the primary key.
|
|
1111
|
-
const deleted = await this.client()
|
|
1112
|
-
.from(DURABLE_TABLES.bufferedEvents)
|
|
1113
|
-
.where('id', id)
|
|
1114
|
-
.delete();
|
|
1192
|
+
const deleted = await this.client().from(this.t.bufferedEvents).where('id', id).delete();
|
|
1115
1193
|
return rowsAffected(deleted) === 1;
|
|
1116
1194
|
}
|
|
1117
1195
|
// --- transaction (exactly-once business write + checkpoint) -------------
|
|
@@ -1143,13 +1221,13 @@ export class LucidStateStore {
|
|
|
1143
1221
|
const row = scheduleToRow(record);
|
|
1144
1222
|
await this.client()
|
|
1145
1223
|
.insertQuery()
|
|
1146
|
-
.table(
|
|
1224
|
+
.table(this.t.schedules)
|
|
1147
1225
|
.knexQuery.insert(row)
|
|
1148
1226
|
.onConflict('id')
|
|
1149
1227
|
.merge();
|
|
1150
1228
|
}
|
|
1151
1229
|
async getSchedule(id) {
|
|
1152
|
-
const row = await this.client().from(
|
|
1230
|
+
const row = await this.client().from(this.t.schedules).where('id', id).first();
|
|
1153
1231
|
return row ? rowToSchedule(row) : null;
|
|
1154
1232
|
}
|
|
1155
1233
|
/** One conditional UPDATE: the compare-and-set on `next_fire_at` lives in its WHERE clause. */
|
|
@@ -1173,7 +1251,7 @@ export class LucidStateStore {
|
|
|
1173
1251
|
set.updated_at = patch.updatedAt.getTime();
|
|
1174
1252
|
if (!Object.keys(set).length)
|
|
1175
1253
|
return (await this.getSchedule(id)) !== null;
|
|
1176
|
-
const q = this.client().from(
|
|
1254
|
+
const q = this.client().from(this.t.schedules).where('id', id);
|
|
1177
1255
|
if (expectedNextFireAt === null)
|
|
1178
1256
|
q.whereNull('next_fire_at');
|
|
1179
1257
|
else if (expectedNextFireAt !== undefined)
|
|
@@ -1181,11 +1259,11 @@ export class LucidStateStore {
|
|
|
1181
1259
|
return rowsAffected(await q.update(set)) > 0;
|
|
1182
1260
|
}
|
|
1183
1261
|
async deleteSchedule(id) {
|
|
1184
|
-
const affected = await this.client().from(
|
|
1262
|
+
const affected = await this.client().from(this.t.schedules).where('id', id).delete();
|
|
1185
1263
|
return rowsAffected(affected) > 0;
|
|
1186
1264
|
}
|
|
1187
1265
|
async listSchedules(query) {
|
|
1188
|
-
const q = this.client().from(
|
|
1266
|
+
const q = this.client().from(this.t.schedules);
|
|
1189
1267
|
if (query.namespace !== undefined) {
|
|
1190
1268
|
q.whereIn('namespace', namespaceFilterValues(query.namespace));
|
|
1191
1269
|
}
|
|
@@ -1246,7 +1324,7 @@ export class LucidStateStore {
|
|
|
1246
1324
|
q.limit(scan);
|
|
1247
1325
|
return runValueFacetsFromRuns((await q).map(rowToRun), axis, opts);
|
|
1248
1326
|
}
|
|
1249
|
-
const column = axis.field;
|
|
1327
|
+
const column = axis.field === 'tenant' ? 'tenant_id' : axis.field;
|
|
1250
1328
|
const needle = opts?.search?.trim().toLowerCase();
|
|
1251
1329
|
// One pass over the matching runs (the shape `runFacets` uses), not a self semi-join on `id`.
|
|
1252
1330
|
const q = this.scopedRuns(query)
|
|
@@ -1268,7 +1346,7 @@ export class LucidStateStore {
|
|
|
1268
1346
|
* exactly the set the list would show.
|
|
1269
1347
|
*/
|
|
1270
1348
|
scopedRuns(query) {
|
|
1271
|
-
const q = this.fenced(this.client().from(
|
|
1349
|
+
const q = this.fenced(this.client().from(this.t.runs));
|
|
1272
1350
|
if (query.workflow)
|
|
1273
1351
|
q.where('workflow', query.workflow);
|
|
1274
1352
|
if (query.workflows) {
|
|
@@ -1292,6 +1370,23 @@ export class LucidStateStore {
|
|
|
1292
1370
|
}
|
|
1293
1371
|
if (query.origin)
|
|
1294
1372
|
q.where('origin', query.origin);
|
|
1373
|
+
if (query.tenantId !== undefined)
|
|
1374
|
+
q.where('tenant_id', query.tenantId);
|
|
1375
|
+
if (query.tenantIds) {
|
|
1376
|
+
// `tenant_id IN (...)`, with `null` standing for "no tenant"; an empty set matches nothing.
|
|
1377
|
+
const ids = query.tenantIds.filter((id) => id !== null);
|
|
1378
|
+
const withNull = query.tenantIds.includes(null);
|
|
1379
|
+
if (ids.length === 0 && !withNull)
|
|
1380
|
+
q.whereRaw('1 = 0');
|
|
1381
|
+
else {
|
|
1382
|
+
q.andWhere((or) => {
|
|
1383
|
+
if (ids.length)
|
|
1384
|
+
or.orWhereIn('tenant_id', ids);
|
|
1385
|
+
if (withNull)
|
|
1386
|
+
or.orWhereNull('tenant_id');
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1295
1390
|
if (query.status)
|
|
1296
1391
|
q.where('status', query.status);
|
|
1297
1392
|
if (query.statuses) {
|
|
@@ -1355,10 +1450,10 @@ export class LucidStateStore {
|
|
|
1355
1450
|
}
|
|
1356
1451
|
q.whereExists((sub) => {
|
|
1357
1452
|
sub
|
|
1358
|
-
.from(
|
|
1359
|
-
.whereRaw(`${
|
|
1360
|
-
.andWhere(`${
|
|
1361
|
-
.whereIn(`${
|
|
1453
|
+
.from(this.t.attributes)
|
|
1454
|
+
.whereRaw('?? = ??', [`${this.t.attributes}.run_id`, `${this.t.runs}.id`])
|
|
1455
|
+
.andWhere(`${this.t.attributes}.key`, f.key)
|
|
1456
|
+
.whereIn(`${this.t.attributes}.${col}`, operands);
|
|
1362
1457
|
});
|
|
1363
1458
|
return;
|
|
1364
1459
|
}
|
|
@@ -1366,15 +1461,15 @@ export class LucidStateStore {
|
|
|
1366
1461
|
const operand = attributeOperand(f);
|
|
1367
1462
|
q.whereExists((sub) => {
|
|
1368
1463
|
sub
|
|
1369
|
-
.from(
|
|
1370
|
-
.whereRaw(`${
|
|
1371
|
-
.andWhere(`${
|
|
1372
|
-
.andWhere(`${
|
|
1464
|
+
.from(this.t.attributes)
|
|
1465
|
+
.whereRaw('?? = ??', [`${this.t.attributes}.run_id`, `${this.t.runs}.id`])
|
|
1466
|
+
.andWhere(`${this.t.attributes}.key`, f.key)
|
|
1467
|
+
.andWhere(`${this.t.attributes}.${col}`, cmp, operand);
|
|
1373
1468
|
});
|
|
1374
1469
|
}
|
|
1375
1470
|
async listCheckpoints(runId) {
|
|
1376
1471
|
const rows = await this.client()
|
|
1377
|
-
.from(
|
|
1472
|
+
.from(this.t.checkpoints)
|
|
1378
1473
|
.where('run_id', runId)
|
|
1379
1474
|
.orderBy('seq', 'asc');
|
|
1380
1475
|
return rows.map(rowToCheckpoint);
|
|
@@ -1382,9 +1477,9 @@ export class LucidStateStore {
|
|
|
1382
1477
|
async listCheckpointsForReplay(runId) {
|
|
1383
1478
|
const hot = this.hot();
|
|
1384
1479
|
const rows = (hot
|
|
1385
|
-
? await hot.rows(
|
|
1480
|
+
? await hot.rows(this.sql.replayCheckpoints, [runId])
|
|
1386
1481
|
: await this.client()
|
|
1387
|
-
.from(
|
|
1482
|
+
.from(this.t.checkpoints)
|
|
1388
1483
|
.where('run_id', runId)
|
|
1389
1484
|
.orderBy('seq', 'asc')
|
|
1390
1485
|
.select(...CHECKPOINT_COLUMNS_BUT_INPUT));
|
|
@@ -1397,7 +1492,7 @@ export class LucidStateStore {
|
|
|
1397
1492
|
const inputs = new Map();
|
|
1398
1493
|
for (let at = 0; at < unsettled.length; at += CHECKPOINT_BATCH) {
|
|
1399
1494
|
const found = (await this.client()
|
|
1400
|
-
.from(
|
|
1495
|
+
.from(this.t.checkpoints)
|
|
1401
1496
|
.where('run_id', runId)
|
|
1402
1497
|
.whereIn('seq', unsettled.slice(at, at + CHECKPOINT_BATCH))
|
|
1403
1498
|
.select('seq', 'input'));
|
|
@@ -1413,7 +1508,7 @@ export class LucidStateStore {
|
|
|
1413
1508
|
}
|
|
1414
1509
|
async getLatestCheckpointByName(runId, name) {
|
|
1415
1510
|
const row = await this.client()
|
|
1416
|
-
.from(
|
|
1511
|
+
.from(this.t.checkpoints)
|
|
1417
1512
|
.where('run_id', runId)
|
|
1418
1513
|
.andWhere('name', name)
|
|
1419
1514
|
.orderBy('seq', 'desc')
|
|
@@ -1424,7 +1519,7 @@ export class LucidStateStore {
|
|
|
1424
1519
|
if (prefixes.length === 0)
|
|
1425
1520
|
return [];
|
|
1426
1521
|
const rows = await this.client()
|
|
1427
|
-
.from(
|
|
1522
|
+
.from(this.t.checkpoints)
|
|
1428
1523
|
.where('run_id', runId)
|
|
1429
1524
|
.andWhere((q) => {
|
|
1430
1525
|
for (const p of prefixes)
|
|
@@ -1438,11 +1533,11 @@ export class LucidStateStore {
|
|
|
1438
1533
|
return 0;
|
|
1439
1534
|
const hot = this.hot();
|
|
1440
1535
|
if (hot) {
|
|
1441
|
-
const rows = await hot.rows(
|
|
1536
|
+
const rows = await hot.rows(this.sql.countCheckpoints, [runId, fromSeq, toSeq, statuses]);
|
|
1442
1537
|
return Number(rows[0]?.count ?? 0);
|
|
1443
1538
|
}
|
|
1444
1539
|
const rows = await this.client()
|
|
1445
|
-
.from(
|
|
1540
|
+
.from(this.t.checkpoints)
|
|
1446
1541
|
.where('run_id', runId)
|
|
1447
1542
|
.andWhere('seq', '>=', fromSeq)
|
|
1448
1543
|
.andWhere('seq', '<=', toSeq)
|
|
@@ -1455,7 +1550,7 @@ export class LucidStateStore {
|
|
|
1455
1550
|
if (statuses.length === 0)
|
|
1456
1551
|
return false;
|
|
1457
1552
|
const row = await this.client()
|
|
1458
|
-
.from(
|
|
1553
|
+
.from(this.t.checkpoints)
|
|
1459
1554
|
.select('seq')
|
|
1460
1555
|
.where('run_id', runId)
|
|
1461
1556
|
.andWhere('seq', '>=', fromSeq)
|
|
@@ -1475,7 +1570,7 @@ export class LucidStateStore {
|
|
|
1475
1570
|
const client = this.client();
|
|
1476
1571
|
const kind = nameKindSql(client.dialect.name);
|
|
1477
1572
|
const q = client
|
|
1478
|
-
.from(
|
|
1573
|
+
.from(this.t.checkpoints)
|
|
1479
1574
|
.select('parallel_group as grp', 'status')
|
|
1480
1575
|
.select(client.raw(`${kind} as name_kind`))
|
|
1481
1576
|
.count('* as n')
|
|
@@ -1521,7 +1616,7 @@ export class LucidStateStore {
|
|
|
1521
1616
|
if (query.statuses?.length === 0 || query.namePrefixes?.length === 0)
|
|
1522
1617
|
return [];
|
|
1523
1618
|
const client = this.client();
|
|
1524
|
-
const q = client.from(
|
|
1619
|
+
const q = client.from(this.t.checkpoints).where('run_id', runId);
|
|
1525
1620
|
if (query.group !== undefined)
|
|
1526
1621
|
q.andWhere('parallel_group', query.group);
|
|
1527
1622
|
if (query.excludeGroups?.length) {
|
|
@@ -1556,10 +1651,10 @@ export class LucidStateStore {
|
|
|
1556
1651
|
/** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
|
|
1557
1652
|
* the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
|
|
1558
1653
|
async reindexAttributes(client, runId, attributes) {
|
|
1559
|
-
await client.from(
|
|
1654
|
+
await client.from(this.t.attributes).where('run_id', runId).delete();
|
|
1560
1655
|
const rows = normalizeAttributeRows(runId, attributes);
|
|
1561
1656
|
if (rows.length) {
|
|
1562
|
-
await client.table(
|
|
1657
|
+
await client.table(this.t.attributes).multiInsert(rows.map((r) => ({
|
|
1563
1658
|
run_id: r.runId,
|
|
1564
1659
|
key: r.key,
|
|
1565
1660
|
str_value: r.strValue,
|
|
@@ -1568,6 +1663,43 @@ export class LucidStateStore {
|
|
|
1568
1663
|
}
|
|
1569
1664
|
}
|
|
1570
1665
|
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Serialize the store's work on a transaction client. A transaction is ONE connection: two store
|
|
1668
|
+
* calls in flight at once (a background run pickup next to a request's own write, under an app's test
|
|
1669
|
+
* `wrapInGlobalTransaction`) would send overlapping queries down it, which `pg` deprecates (and 9.0
|
|
1670
|
+
* removes), and a statement could land inside a savepoint the other call is rolling back. So while
|
|
1671
|
+
* the store's client is a transaction, each call waits for the previous one on that transaction to
|
|
1672
|
+
* finish. Re-entrant: a store method calling another one in the same call does not wait for itself.
|
|
1673
|
+
* A store on a plain connection is untouched (its pool hands each query its own connection).
|
|
1674
|
+
*/
|
|
1675
|
+
const transactionTails = new WeakMap();
|
|
1676
|
+
const holdingTransaction = new AsyncLocalStorage();
|
|
1677
|
+
function serializeOnTransactions(proto) {
|
|
1678
|
+
const AsyncFunction = (async () => { }).constructor;
|
|
1679
|
+
for (const name of Object.getOwnPropertyNames(proto)) {
|
|
1680
|
+
if (name === 'constructor')
|
|
1681
|
+
continue;
|
|
1682
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, name);
|
|
1683
|
+
const original = descriptor?.value;
|
|
1684
|
+
if (typeof original !== 'function' || !(original instanceof AsyncFunction))
|
|
1685
|
+
continue;
|
|
1686
|
+
const method = original;
|
|
1687
|
+
Object.defineProperty(proto, name, {
|
|
1688
|
+
...descriptor,
|
|
1689
|
+
value: function (...args) {
|
|
1690
|
+
const client = this.client();
|
|
1691
|
+
if (!client.isTransaction || holdingTransaction.getStore() === client) {
|
|
1692
|
+
return method.apply(this, args);
|
|
1693
|
+
}
|
|
1694
|
+
const previous = transactionTails.get(client) ?? Promise.resolve();
|
|
1695
|
+
const run = previous.then(() => holdingTransaction.run(client, () => method.apply(this, args)), () => holdingTransaction.run(client, () => method.apply(this, args)));
|
|
1696
|
+
transactionTails.set(client, run.catch(() => undefined));
|
|
1697
|
+
return run;
|
|
1698
|
+
},
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
serializeOnTransactions(LucidStateStore.prototype);
|
|
1571
1703
|
/**
|
|
1572
1704
|
* Escape LIKE metacharacters (`%`, `_`) — and the escape character itself — using `!` as the ESCAPE
|
|
1573
1705
|
* char. `!` because a backslash inside a SQL string literal is dialect-divergent (MySQL treats it as
|
|
@@ -1618,20 +1750,20 @@ const CHECKPOINT_BATCH = 500;
|
|
|
1618
1750
|
/** Most checkpoint rows one multi-row upsert writes (kept under every dialect's bind limit). */
|
|
1619
1751
|
const CHECKPOINT_UPSERT_BATCH = 200;
|
|
1620
1752
|
/** One multi-row `INSERT … ON CONFLICT (run_id, seq) DO UPDATE` (MySQL: `ON DUPLICATE KEY`). */
|
|
1621
|
-
async function upsertCheckpointRows(client, checkpoints) {
|
|
1753
|
+
async function upsertCheckpointRows(client, checkpointsTable, checkpoints) {
|
|
1622
1754
|
if (checkpoints.length === 0)
|
|
1623
1755
|
return;
|
|
1624
1756
|
await client
|
|
1625
1757
|
.insertQuery()
|
|
1626
|
-
.table(
|
|
1758
|
+
.table(checkpointsTable)
|
|
1627
1759
|
.knexQuery.insert(checkpoints.map(checkpointToRow))
|
|
1628
1760
|
.onConflict(['run_id', 'seq'])
|
|
1629
1761
|
.merge();
|
|
1630
1762
|
}
|
|
1631
|
-
async function updateCheckpointIfPending(client, checkpoint) {
|
|
1763
|
+
async function updateCheckpointIfPending(client, checkpointsTable, checkpoint) {
|
|
1632
1764
|
const { run_id: _runId, seq: _seq, ...row } = checkpointToRow(checkpoint);
|
|
1633
1765
|
const affected = await client
|
|
1634
|
-
.from(
|
|
1766
|
+
.from(checkpointsTable)
|
|
1635
1767
|
.where('run_id', checkpoint.runId)
|
|
1636
1768
|
.andWhere('seq', checkpoint.seq)
|
|
1637
1769
|
.andWhere('status', 'pending')
|
|
@@ -1661,55 +1793,114 @@ function escapeLike(value) {
|
|
|
1661
1793
|
return value.replace(/[!%_]/g, (c) => `!${c}`);
|
|
1662
1794
|
}
|
|
1663
1795
|
// --- the Postgres hot path's statements (see `lucid-pg-hot-path.ts`) --------------------------------
|
|
1664
|
-
const RUNS_T = quote(DURABLE_TABLES.runs);
|
|
1665
|
-
const CHECKPOINTS_T = quote(DURABLE_TABLES.checkpoints);
|
|
1666
1796
|
const CHECKPOINT_LIST = CHECKPOINT_COLUMNS_BUT_INPUT.map(quote).join(', ');
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1797
|
+
/**
|
|
1798
|
+
* The runs columns every schema has. The hot path names its columns instead of `*`: a named prepared
|
|
1799
|
+
* statement's result type is fixed when it is parsed, so a `*` statement fails ("cached plan must
|
|
1800
|
+
* not change result type") once a column is added under it — another pod's `ensureSchema` during a
|
|
1801
|
+
* rolling upgrade.
|
|
1802
|
+
*/
|
|
1803
|
+
const RUN_BASE_COLUMNS = [
|
|
1804
|
+
'id',
|
|
1805
|
+
'workflow',
|
|
1806
|
+
'workflow_version',
|
|
1807
|
+
'status',
|
|
1808
|
+
'namespace',
|
|
1809
|
+
'input',
|
|
1810
|
+
'output',
|
|
1811
|
+
'error',
|
|
1812
|
+
'wake_at',
|
|
1813
|
+
'locked_by',
|
|
1814
|
+
'locked_until',
|
|
1815
|
+
'recovery_attempts',
|
|
1816
|
+
'tags',
|
|
1817
|
+
'search_attributes',
|
|
1818
|
+
'priority',
|
|
1819
|
+
'created_at',
|
|
1820
|
+
'updated_at',
|
|
1821
|
+
];
|
|
1822
|
+
/** Runs columns that arrived in later schema waves: read only when the table has them. */
|
|
1823
|
+
const RUN_OPTIONAL_COLUMNS = [
|
|
1824
|
+
'origin',
|
|
1825
|
+
'awaiting_decision_task_id',
|
|
1826
|
+
'wake_requested_at',
|
|
1827
|
+
'tenant_id',
|
|
1828
|
+
'tenant_kind',
|
|
1829
|
+
];
|
|
1830
|
+
/** Checkpoint columns that arrived in later schema waves: read only when the table has them. */
|
|
1831
|
+
const CHECKPOINT_OPTIONAL_COLUMNS = [
|
|
1832
|
+
'queue',
|
|
1833
|
+
'parallel_group',
|
|
1834
|
+
'last_heartbeat_at',
|
|
1835
|
+
'heartbeat_progress',
|
|
1836
|
+
];
|
|
1837
|
+
const CHECKPOINT_BASE_COLUMNS = [
|
|
1838
|
+
...CHECKPOINT_COLUMNS_BUT_INPUT.filter((c) => !CHECKPOINT_OPTIONAL_COLUMNS.includes(c)),
|
|
1839
|
+
'input',
|
|
1840
|
+
];
|
|
1841
|
+
/** The later-wave columns present, in the configured schema (`?` first) or the current one. */
|
|
1842
|
+
const COLUMNS_PRESENT_SQL = `select "table_name", "column_name" from information_schema.columns where "table_schema" = coalesce(?, current_schema()) and "table_name" = any(?) and "column_name" = any(?)`;
|
|
1843
|
+
const BUFFERED_SIGNAL_LIST = ['id', 'token', 'payload'].map(quote).join(', ');
|
|
1844
|
+
const BUFFERED_EVENT_LIST = ['id', 'name', 'payload', 'published_at'].map(quote).join(', ');
|
|
1845
|
+
/** A quoted, schema-qualified table name for raw SQL: `"schema"."table"`, or `"table"`. */
|
|
1846
|
+
function quotedTable(schema, table) {
|
|
1847
|
+
return schema ? `${quote(schema)}.${quote(table)}` : quote(table);
|
|
1705
1848
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1849
|
+
const hotStatementsBySchema = new Map();
|
|
1850
|
+
/** {@link HotStatements} for `schema` (the connection's current schema when unset). */
|
|
1851
|
+
function hotStatements(schema) {
|
|
1852
|
+
const cacheKey = schema ?? '';
|
|
1853
|
+
const cached = hotStatementsBySchema.get(cacheKey);
|
|
1854
|
+
if (cached)
|
|
1855
|
+
return cached;
|
|
1856
|
+
const RUNS_T = quotedTable(schema, DURABLE_TABLES.runs);
|
|
1857
|
+
const CHECKPOINTS_T = quotedTable(schema, DURABLE_TABLES.checkpoints);
|
|
1858
|
+
const ATTRIBUTES_T = quotedTable(schema, DURABLE_TABLES.attributes);
|
|
1859
|
+
const SIGNALS_T = quotedTable(schema, DURABLE_TABLES.bufferedSignals);
|
|
1860
|
+
const EVENTS_T = quotedTable(schema, DURABLE_TABLES.bufferedEvents);
|
|
1861
|
+
/** Statements whose column list is the row's own keys, built once per key set. */
|
|
1862
|
+
const shaped = new Map();
|
|
1863
|
+
const shapedSql = (key, build) => {
|
|
1864
|
+
let text = shaped.get(key);
|
|
1865
|
+
if (text === undefined) {
|
|
1866
|
+
text = build();
|
|
1867
|
+
shaped.set(key, text);
|
|
1868
|
+
}
|
|
1869
|
+
return text;
|
|
1870
|
+
};
|
|
1871
|
+
/** `INSERT` of one row of `table` (quoted) with the columns `keys`. */
|
|
1872
|
+
const insertSql = (table, keys) => `insert into ${table} (${keys.map(quote).join(', ')}) values (${keys.map(() => '?').join(', ')})`;
|
|
1873
|
+
const statements = {
|
|
1874
|
+
reads: (cols, checkpointCols) => ({
|
|
1875
|
+
getCheckpoint: shapedSql(`checkpoint-get:${checkpointCols}`, () => `select ${checkpointCols} from ${CHECKPOINTS_T} where "run_id" = ? and "seq" = ? limit 1`),
|
|
1876
|
+
get: shapedSql(`run-get:${cols}`, () => `select ${cols} from ${RUNS_T} where "id" = ? limit 1`),
|
|
1877
|
+
getScoped: shapedSql(`run-get-scoped:${cols}`, () => `select ${cols} from ${RUNS_T} where "id" = ? and "namespace" = any(?) limit 1`),
|
|
1878
|
+
lockAndGet: shapedSql(`run-lock:${cols}`, () => `update ${RUNS_T} set "locked_by" = ?, "locked_until" = ? where "id" = ? and ("locked_until" is null or "locked_until" <= ?) returning ${cols}`),
|
|
1879
|
+
lockAndGetScoped: shapedSql(`run-lock-scoped:${cols}`, () => `update ${RUNS_T} set "locked_by" = ?, "locked_until" = ? where "id" = ? and ("locked_until" is null or "locked_until" <= ?) and "namespace" = any(?) returning ${cols}`),
|
|
1880
|
+
}),
|
|
1881
|
+
runInStatuses: `select 1 from ${RUNS_T} where "id" = ? and "status" = any(?) limit 1`,
|
|
1882
|
+
releaseLock: `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ?`,
|
|
1883
|
+
releaseOwnLock: `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ? and "locked_by" = ?`,
|
|
1884
|
+
releaseUnlessWake: `update ${RUNS_T} set "locked_by" = null, "locked_until" = null where "id" = ? and "locked_by" = ? and "wake_requested_at" is null`,
|
|
1885
|
+
lockedBy: `select "locked_by" from ${RUNS_T} where "id" = ? limit 1`,
|
|
1886
|
+
renewLock: `update ${RUNS_T} set "locked_until" = ? where "id" = ? and "locked_by" = ?`,
|
|
1887
|
+
deleteAttributes: `delete from ${ATTRIBUTES_T} where "run_id" = ?`,
|
|
1888
|
+
checkpointsForSettle: `select ${CHECKPOINT_LIST} from ${CHECKPOINTS_T} where "run_id" = ? and "seq" = any(?)`,
|
|
1889
|
+
replayCheckpoints: `select ${CHECKPOINT_LIST} from ${CHECKPOINTS_T} where "run_id" = ? order by "seq" asc`,
|
|
1890
|
+
countCheckpoints: `select count(*) as "count" from ${CHECKPOINTS_T} where "run_id" = ? and "seq" >= ? and "seq" <= ? and "status" = any(?)`,
|
|
1891
|
+
oldestBufferedSignal: `select ${BUFFERED_SIGNAL_LIST} from ${SIGNALS_T} where "token" = ? order by "id" asc limit 1`,
|
|
1892
|
+
deleteBufferedSignal: `delete from ${SIGNALS_T} where "id" = ?`,
|
|
1893
|
+
bufferedEvents: `select ${BUFFERED_EVENT_LIST} from ${EVENTS_T} where "name" = ? order by "published_at" asc limit ?`,
|
|
1894
|
+
insertRunIfAbsent: (keys) => shapedSql(`insert-run-if-absent:${keys.join(',')}`, () => `${insertSql(RUNS_T, keys)} on conflict ("id") do nothing returning "id"`),
|
|
1895
|
+
insertCheckpointIfAbsent: (keys) => shapedSql(`insert-if-absent:${keys.join(',')}`, () => `${insertSql(CHECKPOINTS_T, keys)} on conflict ("run_id", "seq") do nothing returning "seq"`),
|
|
1896
|
+
updateRun: (keys, conditional) => shapedSql(`update-run:${conditional}:${keys.join(',')}`, () => {
|
|
1897
|
+
const set = keys.map((k) => `${quote(k)} = ?`).join(', ');
|
|
1898
|
+
const where = conditional ? `"id" = ? and "status" = any(?)` : `"id" = ?`;
|
|
1899
|
+
return `update ${RUNS_T} set ${set} where ${where}`;
|
|
1900
|
+
}),
|
|
1901
|
+
};
|
|
1902
|
+
hotStatementsBySchema.set(cacheKey, statements);
|
|
1903
|
+
return statements;
|
|
1713
1904
|
}
|
|
1714
1905
|
/**
|
|
1715
1906
|
* {@link LucidStateStore.settleResultsIfPending} on Postgres: one `UPDATE … FROM (VALUES …) …
|
|
@@ -1717,7 +1908,7 @@ function updateRunSql(keys, conditional) {
|
|
|
1717
1908
|
* in the same statement, and read back with its `wake_at`. What a result does not carry keeps its
|
|
1718
1909
|
* stored value: `output` unless it completed, `events` and `started_at` unless present.
|
|
1719
1910
|
*/
|
|
1720
|
-
async function settleResultsReturningPg(client, hot, results) {
|
|
1911
|
+
async function settleResultsReturningPg(client, hot, schema, results) {
|
|
1721
1912
|
const values = [];
|
|
1722
1913
|
const bindings = [];
|
|
1723
1914
|
for (const r of results) {
|
|
@@ -1731,7 +1922,7 @@ async function settleResultsReturningPg(client, hot, results) {
|
|
|
1731
1922
|
const cols = CHECKPOINT_COLUMNS_BUT_INPUT.map((c) => `c.${c}`).join(', ');
|
|
1732
1923
|
const statuses = SETTLEABLE_RUN_STATUSES.map(() => '?').join(', ');
|
|
1733
1924
|
bindings.push(...SETTLEABLE_RUN_STATUSES);
|
|
1734
|
-
const sql = `update ${DURABLE_TABLES.checkpoints} as c set
|
|
1925
|
+
const sql = `update ${quotedTable(schema, DURABLE_TABLES.checkpoints)} as c set
|
|
1735
1926
|
status = v.status,
|
|
1736
1927
|
output = case when v.status = 'completed' then v.output else c.output end,
|
|
1737
1928
|
error = v.error,
|
|
@@ -1740,7 +1931,7 @@ async function settleResultsReturningPg(client, hot, results) {
|
|
|
1740
1931
|
finished_at = v.finished_at
|
|
1741
1932
|
from (values ${values.join(', ')})
|
|
1742
1933
|
as v(run_id, seq, status, output, error, has_events, events, started_at, finished_at),
|
|
1743
|
-
${DURABLE_TABLES.runs} as r
|
|
1934
|
+
${quotedTable(schema, DURABLE_TABLES.runs)} as r
|
|
1744
1935
|
where c.run_id = v.run_id and c.seq = v.seq and c.status = 'pending'
|
|
1745
1936
|
and r.id = c.run_id and r.status in (${statuses})
|
|
1746
1937
|
returning ${cols}, r.status as run_status, r.wake_at as run_wake_at`;
|
|
@@ -1758,14 +1949,36 @@ async function settleResultsReturningPg(client, hot, results) {
|
|
|
1758
1949
|
}
|
|
1759
1950
|
return out;
|
|
1760
1951
|
}
|
|
1761
|
-
/**
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1952
|
+
/**
|
|
1953
|
+
* Insert `row` into `table` unless a row with the same `key` exists, as a statement that never
|
|
1954
|
+
* fails on the conflict, and say whether it wrote. A failing statement would abort a surrounding
|
|
1955
|
+
* Postgres transaction, so the conflict is never detected by catching a duplicate-key error.
|
|
1956
|
+
*
|
|
1957
|
+
* - Postgres and SQLite: `INSERT … ON CONFLICT (key) DO NOTHING RETURNING key`, a row back when it wrote.
|
|
1958
|
+
* - MySQL: `INSERT … ON DUPLICATE KEY UPDATE key = IF(LAST_INSERT_ID(1), key, key)`, which leaves an
|
|
1959
|
+
* existing row as it is and reports `insertId` 1 for it, 0 for a row it wrote (`table` has no
|
|
1960
|
+
* AUTO_INCREMENT). The affected-row count cannot tell them apart (mysql2 counts found rows), and
|
|
1961
|
+
* `INSERT IGNORE` would turn a value the column cannot hold into a silent truncation.
|
|
1962
|
+
*/
|
|
1963
|
+
async function insertIfAbsent(client, table, row, key) {
|
|
1964
|
+
if (client.dialect.name === 'mysql') {
|
|
1965
|
+
const first = key[0];
|
|
1966
|
+
const knex = client.getWriteClient();
|
|
1967
|
+
const { sql, bindings } = client
|
|
1968
|
+
.insertQuery()
|
|
1969
|
+
.table(table)
|
|
1970
|
+
.knexQuery.insert(row)
|
|
1971
|
+
.onConflict(key)
|
|
1972
|
+
.merge({ [first]: knex.raw('if(last_insert_id(1), ??, ??)', [first, first]) })
|
|
1973
|
+
.toSQL()
|
|
1974
|
+
.toNative();
|
|
1975
|
+
const result = (await client.rawQuery(sql, bindings));
|
|
1976
|
+
const header = (Array.isArray(result) ? result[0] : result);
|
|
1977
|
+
return Number(header?.insertId ?? 0) === 0;
|
|
1978
|
+
}
|
|
1979
|
+
const insert = client.insertQuery().table(table).knexQuery.insert(row).onConflict(key);
|
|
1980
|
+
const inserted = (await insert.ignore().returning(key[0]));
|
|
1981
|
+
return inserted.length > 0;
|
|
1769
1982
|
}
|
|
1770
1983
|
function checkpointKey(runId, seq) {
|
|
1771
1984
|
return `${runId}\u0000${seq}`;
|
|
@@ -1776,11 +1989,11 @@ function checkpointKey(runId, seq) {
|
|
|
1776
1989
|
* so the database write lock is taken FIRST with a no-op write, and no other connection or process
|
|
1777
1990
|
* settles a row between the read and the caller's writes.
|
|
1778
1991
|
*/
|
|
1779
|
-
async function lockPendingCheckpoints(trx, entries, columns) {
|
|
1992
|
+
async function lockPendingCheckpoints(trx, checkpointsTable, entries, columns) {
|
|
1780
1993
|
const first = entries[0];
|
|
1781
1994
|
if (first && !locksRows(trx)) {
|
|
1782
1995
|
await trx
|
|
1783
|
-
.from(
|
|
1996
|
+
.from(checkpointsTable)
|
|
1784
1997
|
.where('run_id', first.runId)
|
|
1785
1998
|
.andWhere('seq', first.seq)
|
|
1786
1999
|
.andWhere('status', 'pending')
|
|
@@ -1797,7 +2010,7 @@ async function lockPendingCheckpoints(trx, entries, columns) {
|
|
|
1797
2010
|
const seqs = [...seqSet];
|
|
1798
2011
|
for (let at = 0; at < seqs.length; at += CHECKPOINT_BATCH) {
|
|
1799
2012
|
const q = trx
|
|
1800
|
-
.from(
|
|
2013
|
+
.from(checkpointsTable)
|
|
1801
2014
|
.where('run_id', runId)
|
|
1802
2015
|
.whereIn('seq', seqs.slice(at, at + CHECKPOINT_BATCH))
|
|
1803
2016
|
.andWhere('status', 'pending')
|
|
@@ -1834,6 +2047,10 @@ function scheduleToRow(r) {
|
|
|
1834
2047
|
tags: r.tags?.length ? JSON.stringify(r.tags) : null,
|
|
1835
2048
|
spec: JSON.stringify(r.spec),
|
|
1836
2049
|
state: JSON.stringify(r.state),
|
|
2050
|
+
// Only when set: a schedule with no tenant saves on a schema without the columns.
|
|
2051
|
+
...(r.tenantId !== undefined
|
|
2052
|
+
? { tenant_id: r.tenantId, tenant_kind: r.tenantKind ?? null }
|
|
2053
|
+
: {}),
|
|
1837
2054
|
created_at: r.createdAt.getTime(),
|
|
1838
2055
|
updated_at: r.updatedAt.getTime(),
|
|
1839
2056
|
};
|
|
@@ -1850,6 +2067,8 @@ function rowToSchedule(row) {
|
|
|
1850
2067
|
paused: row.paused === true || row.paused === 1 || row.paused === '1' || row.paused === 'true',
|
|
1851
2068
|
nextFireAt: row.next_fire_at == null ? null : Number(row.next_fire_at),
|
|
1852
2069
|
...(tags?.length ? { tags } : {}),
|
|
2070
|
+
...(row.tenant_id != null ? { tenantId: row.tenant_id } : {}),
|
|
2071
|
+
...(row.tenant_kind != null ? { tenantKind: row.tenant_kind } : {}),
|
|
1853
2072
|
spec: parseJsonColumn(row.spec, `${DURABLE_TABLES.schedules}.spec`, row.id) ?? {},
|
|
1854
2073
|
state: parseJsonColumn(row.state, `${DURABLE_TABLES.schedules}.state`, row.id) ?? {},
|
|
1855
2074
|
createdAt: new Date(Number(row.created_at)),
|