@shware/workflow 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +154 -2
- package/dist/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/apns-relay.cjs +105 -0
- package/dist/apns-relay.cjs.map +1 -0
- package/dist/apns-relay.d.cts +28 -0
- package/dist/apns-relay.d.cts.map +1 -0
- package/dist/apns-relay.d.mts +28 -0
- package/dist/apns-relay.d.mts.map +1 -0
- package/dist/apns-relay.mjs +104 -0
- package/dist/apns-relay.mjs.map +1 -0
- package/dist/apns.cjs +121 -0
- package/dist/apns.cjs.map +1 -0
- package/dist/apns.d.cts +63 -0
- package/dist/apns.d.cts.map +1 -0
- package/dist/apns.d.mts +63 -0
- package/dist/apns.d.mts.map +1 -0
- package/dist/apns.mjs +116 -0
- package/dist/apns.mjs.map +1 -0
- package/dist/client.cjs +125 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +108 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.mts +108 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +122 -0
- package/dist/client.mjs.map +1 -0
- package/dist/cloudflare/bindings.cjs +28 -0
- package/dist/cloudflare/bindings.cjs.map +1 -0
- package/dist/cloudflare/bindings.d.cts +144 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -0
- package/dist/cloudflare/bindings.d.mts +144 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -0
- package/dist/cloudflare/bindings.mjs +25 -0
- package/dist/cloudflare/bindings.mjs.map +1 -0
- package/dist/cloudflare/channels.cjs +164 -0
- package/dist/cloudflare/channels.cjs.map +1 -0
- package/dist/cloudflare/channels.d.cts +29 -0
- package/dist/cloudflare/channels.d.cts.map +1 -0
- package/dist/cloudflare/channels.d.mts +29 -0
- package/dist/cloudflare/channels.d.mts.map +1 -0
- package/dist/cloudflare/channels.mjs +161 -0
- package/dist/cloudflare/channels.mjs.map +1 -0
- package/dist/cloudflare/d1-env.cjs +17 -0
- package/dist/cloudflare/d1-env.cjs.map +1 -0
- package/dist/cloudflare/d1-env.d.cts +12 -0
- package/dist/cloudflare/d1-env.d.cts.map +1 -0
- package/dist/cloudflare/d1-env.d.mts +12 -0
- package/dist/cloudflare/d1-env.d.mts.map +1 -0
- package/dist/cloudflare/d1-env.mjs +16 -0
- package/dist/cloudflare/d1-env.mjs.map +1 -0
- package/dist/cloudflare/index.cjs +54 -0
- package/dist/cloudflare/index.d.cts +19 -0
- package/dist/cloudflare/index.d.mts +19 -0
- package/dist/cloudflare/index.mjs +16 -0
- package/dist/cloudflare/router.cjs +516 -0
- package/dist/cloudflare/router.cjs.map +1 -0
- package/dist/cloudflare/router.d.cts +157 -0
- package/dist/cloudflare/router.d.cts.map +1 -0
- package/dist/cloudflare/router.d.mts +157 -0
- package/dist/cloudflare/router.d.mts.map +1 -0
- package/dist/cloudflare/router.mjs +503 -0
- package/dist/cloudflare/router.mjs.map +1 -0
- package/dist/cloudflare/runner.cjs +225 -0
- package/dist/cloudflare/runner.cjs.map +1 -0
- package/dist/cloudflare/runner.d.cts +73 -0
- package/dist/cloudflare/runner.d.cts.map +1 -0
- package/dist/cloudflare/runner.d.mts +73 -0
- package/dist/cloudflare/runner.d.mts.map +1 -0
- package/dist/cloudflare/runner.mjs +224 -0
- package/dist/cloudflare/runner.mjs.map +1 -0
- package/dist/cloudflare/schema.sql +127 -0
- package/dist/cloudflare/senders.cjs +114 -0
- package/dist/cloudflare/senders.cjs.map +1 -0
- package/dist/cloudflare/senders.d.cts +68 -0
- package/dist/cloudflare/senders.d.cts.map +1 -0
- package/dist/cloudflare/senders.d.mts +68 -0
- package/dist/cloudflare/senders.d.mts.map +1 -0
- package/dist/cloudflare/senders.mjs +108 -0
- package/dist/cloudflare/senders.mjs.map +1 -0
- package/dist/cloudflare/worker.cjs +228 -0
- package/dist/cloudflare/worker.cjs.map +1 -0
- package/dist/cloudflare/worker.d.cts +92 -0
- package/dist/cloudflare/worker.d.cts.map +1 -0
- package/dist/cloudflare/worker.d.mts +92 -0
- package/dist/cloudflare/worker.d.mts.map +1 -0
- package/dist/cloudflare/worker.mjs +224 -0
- package/dist/cloudflare/worker.mjs.map +1 -0
- package/dist/cloudflare/workers.d.cjs +0 -0
- package/dist/cloudflare/workers.d.cts +44 -0
- package/dist/cloudflare/workers.d.cts.map +1 -0
- package/dist/cloudflare/workers.d.mts +44 -0
- package/dist/cloudflare/workers.d.mts.map +1 -0
- package/dist/config.cjs +151 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +149 -0
- package/dist/config.d.cts.map +1 -0
- package/dist/config.d.mts +149 -0
- package/dist/config.d.mts.map +1 -0
- package/dist/config.mjs +143 -0
- package/dist/config.mjs.map +1 -0
- package/dist/contract.cjs +212 -0
- package/dist/contract.cjs.map +1 -0
- package/dist/contract.d.cts +90 -0
- package/dist/contract.d.cts.map +1 -0
- package/dist/contract.d.mts +90 -0
- package/dist/contract.d.mts.map +1 -0
- package/dist/contract.mjs +206 -0
- package/dist/contract.mjs.map +1 -0
- package/dist/dsl/action.cjs +29 -0
- package/dist/dsl/action.cjs.map +1 -0
- package/dist/dsl/action.d.cts +64 -0
- package/dist/dsl/action.d.cts.map +1 -0
- package/dist/dsl/action.d.mts +64 -0
- package/dist/dsl/action.d.mts.map +1 -0
- package/dist/dsl/action.mjs +28 -0
- package/dist/dsl/action.mjs.map +1 -0
- package/dist/dsl/base.cjs +34 -0
- package/dist/dsl/base.cjs.map +1 -0
- package/dist/dsl/base.d.cts +45 -0
- package/dist/dsl/base.d.cts.map +1 -0
- package/dist/dsl/base.d.mts +45 -0
- package/dist/dsl/base.d.mts.map +1 -0
- package/dist/dsl/base.mjs +30 -0
- package/dist/dsl/base.mjs.map +1 -0
- package/dist/dsl/bundle.cjs +207 -0
- package/dist/dsl/bundle.cjs.map +1 -0
- package/dist/dsl/bundle.d.cts +19 -0
- package/dist/dsl/bundle.d.cts.map +1 -0
- package/dist/dsl/bundle.d.mts +19 -0
- package/dist/dsl/bundle.d.mts.map +1 -0
- package/dist/dsl/bundle.mjs +206 -0
- package/dist/dsl/bundle.mjs.map +1 -0
- package/dist/dsl/condition.cjs +177 -0
- package/dist/dsl/condition.cjs.map +1 -0
- package/dist/dsl/condition.d.cts +140 -0
- package/dist/dsl/condition.d.cts.map +1 -0
- package/dist/dsl/condition.d.mts +140 -0
- package/dist/dsl/condition.d.mts.map +1 -0
- package/dist/dsl/condition.mjs +156 -0
- package/dist/dsl/condition.mjs.map +1 -0
- package/dist/dsl/flow.cjs +228 -0
- package/dist/dsl/flow.cjs.map +1 -0
- package/dist/dsl/flow.d.cts +223 -0
- package/dist/dsl/flow.d.cts.map +1 -0
- package/dist/dsl/flow.d.mts +223 -0
- package/dist/dsl/flow.d.mts.map +1 -0
- package/dist/dsl/flow.mjs +226 -0
- package/dist/dsl/flow.mjs.map +1 -0
- package/dist/dsl/index.cjs +38 -0
- package/dist/dsl/index.d.cts +10 -0
- package/dist/dsl/index.d.mts +10 -0
- package/dist/dsl/index.mjs +9 -0
- package/dist/dsl/refs.cjs +59 -0
- package/dist/dsl/refs.cjs.map +1 -0
- package/dist/dsl/refs.d.cts +88 -0
- package/dist/dsl/refs.d.cts.map +1 -0
- package/dist/dsl/refs.d.mts +88 -0
- package/dist/dsl/refs.d.mts.map +1 -0
- package/dist/dsl/refs.mjs +55 -0
- package/dist/dsl/refs.mjs.map +1 -0
- package/dist/dsl/template.cjs +65 -0
- package/dist/dsl/template.cjs.map +1 -0
- package/dist/dsl/template.d.cts +75 -0
- package/dist/dsl/template.d.cts.map +1 -0
- package/dist/dsl/template.d.mts +75 -0
- package/dist/dsl/template.d.mts.map +1 -0
- package/dist/dsl/template.mjs +63 -0
- package/dist/dsl/template.mjs.map +1 -0
- package/dist/dsl/trigger.cjs +40 -0
- package/dist/dsl/trigger.cjs.map +1 -0
- package/dist/dsl/trigger.d.cts +65 -0
- package/dist/dsl/trigger.d.cts.map +1 -0
- package/dist/dsl/trigger.d.mts +65 -0
- package/dist/dsl/trigger.d.mts.map +1 -0
- package/dist/dsl/trigger.mjs +39 -0
- package/dist/dsl/trigger.mjs.map +1 -0
- package/dist/dsl/workflow.cjs +121 -0
- package/dist/dsl/workflow.cjs.map +1 -0
- package/dist/dsl/workflow.d.cts +59 -0
- package/dist/dsl/workflow.d.cts.map +1 -0
- package/dist/dsl/workflow.d.mts +59 -0
- package/dist/dsl/workflow.d.mts.map +1 -0
- package/dist/dsl/workflow.mjs +120 -0
- package/dist/dsl/workflow.mjs.map +1 -0
- package/dist/engine/actions.cjs +27 -0
- package/dist/engine/actions.cjs.map +1 -0
- package/dist/engine/actions.d.cts +40 -0
- package/dist/engine/actions.d.cts.map +1 -0
- package/dist/engine/actions.d.mts +40 -0
- package/dist/engine/actions.d.mts.map +1 -0
- package/dist/engine/actions.mjs +26 -0
- package/dist/engine/actions.mjs.map +1 -0
- package/dist/engine/bucket.cjs +72 -0
- package/dist/engine/bucket.cjs.map +1 -0
- package/dist/engine/bucket.d.cts +32 -0
- package/dist/engine/bucket.d.cts.map +1 -0
- package/dist/engine/bucket.d.mts +32 -0
- package/dist/engine/bucket.d.mts.map +1 -0
- package/dist/engine/bucket.mjs +69 -0
- package/dist/engine/bucket.mjs.map +1 -0
- package/dist/engine/condition.cjs +166 -0
- package/dist/engine/condition.cjs.map +1 -0
- package/dist/engine/condition.d.cts +52 -0
- package/dist/engine/condition.d.cts.map +1 -0
- package/dist/engine/condition.d.mts +52 -0
- package/dist/engine/condition.d.mts.map +1 -0
- package/dist/engine/condition.mjs +161 -0
- package/dist/engine/condition.mjs.map +1 -0
- package/dist/engine/index.cjs +20 -0
- package/dist/engine/index.d.cts +8 -0
- package/dist/engine/index.d.mts +8 -0
- package/dist/engine/index.mjs +7 -0
- package/dist/engine/interpreter.cjs +428 -0
- package/dist/engine/interpreter.cjs.map +1 -0
- package/dist/engine/interpreter.d.cts +6 -0
- package/dist/engine/interpreter.d.cts.map +1 -0
- package/dist/engine/interpreter.d.mts +6 -0
- package/dist/engine/interpreter.d.mts.map +1 -0
- package/dist/engine/interpreter.mjs +427 -0
- package/dist/engine/interpreter.mjs.map +1 -0
- package/dist/engine/ports.cjs +0 -0
- package/dist/engine/ports.d.cts +252 -0
- package/dist/engine/ports.d.cts.map +1 -0
- package/dist/engine/ports.d.mts +252 -0
- package/dist/engine/ports.d.mts.map +1 -0
- package/dist/engine/ports.mjs +1 -0
- package/dist/engine/recipient.cjs +23 -0
- package/dist/engine/recipient.cjs.map +1 -0
- package/dist/engine/recipient.d.cts +13 -0
- package/dist/engine/recipient.d.cts.map +1 -0
- package/dist/engine/recipient.d.mts +13 -0
- package/dist/engine/recipient.d.mts.map +1 -0
- package/dist/engine/recipient.mjs +22 -0
- package/dist/engine/recipient.mjs.map +1 -0
- package/dist/engine/time-scale.cjs +54 -0
- package/dist/engine/time-scale.cjs.map +1 -0
- package/dist/engine/time-scale.d.cts +31 -0
- package/dist/engine/time-scale.d.cts.map +1 -0
- package/dist/engine/time-scale.d.mts +31 -0
- package/dist/engine/time-scale.d.mts.map +1 -0
- package/dist/engine/time-scale.mjs +52 -0
- package/dist/engine/time-scale.mjs.map +1 -0
- package/dist/engine/time-window.cjs +149 -0
- package/dist/engine/time-window.cjs.map +1 -0
- package/dist/engine/time-window.d.cts +22 -0
- package/dist/engine/time-window.d.cts.map +1 -0
- package/dist/engine/time-window.d.mts +22 -0
- package/dist/engine/time-window.d.mts.map +1 -0
- package/dist/engine/time-window.mjs +147 -0
- package/dist/engine/time-window.mjs.map +1 -0
- package/dist/fcm.cjs +125 -0
- package/dist/fcm.cjs.map +1 -0
- package/dist/fcm.d.cts +48 -0
- package/dist/fcm.d.cts.map +1 -0
- package/dist/fcm.d.mts +48 -0
- package/dist/fcm.d.mts.map +1 -0
- package/dist/fcm.mjs +121 -0
- package/dist/fcm.mjs.map +1 -0
- package/dist/hash.cjs +245 -0
- package/dist/hash.cjs.map +1 -0
- package/dist/hash.d.cts +30 -0
- package/dist/hash.d.cts.map +1 -0
- package/dist/hash.d.mts +30 -0
- package/dist/hash.d.mts.map +1 -0
- package/dist/hash.mjs +239 -0
- package/dist/hash.mjs.map +1 -0
- package/dist/index.cjs +100 -0
- package/dist/index.d.cts +18 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +17 -0
- package/dist/ir.cjs +366 -0
- package/dist/ir.cjs.map +1 -0
- package/dist/ir.d.cts +486 -0
- package/dist/ir.d.cts.map +1 -0
- package/dist/ir.d.mts +486 -0
- package/dist/ir.d.mts.map +1 -0
- package/dist/ir.mjs +344 -0
- package/dist/ir.mjs.map +1 -0
- package/dist/jwt.cjs +22 -0
- package/dist/jwt.cjs.map +1 -0
- package/dist/jwt.d.cts +9 -0
- package/dist/jwt.d.cts.map +1 -0
- package/dist/jwt.d.mts +9 -0
- package/dist/jwt.d.mts.map +1 -0
- package/dist/jwt.mjs +20 -0
- package/dist/jwt.mjs.map +1 -0
- package/dist/plan.cjs +223 -0
- package/dist/plan.cjs.map +1 -0
- package/dist/plan.d.cts +68 -0
- package/dist/plan.d.cts.map +1 -0
- package/dist/plan.d.mts +68 -0
- package/dist/plan.d.mts.map +1 -0
- package/dist/plan.mjs +221 -0
- package/dist/plan.mjs.map +1 -0
- package/dist/postgres-js.cjs +42 -0
- package/dist/postgres-js.cjs.map +1 -0
- package/dist/postgres-js.d.cts +28 -0
- package/dist/postgres-js.d.cts.map +1 -0
- package/dist/postgres-js.d.mts +28 -0
- package/dist/postgres-js.d.mts.map +1 -0
- package/dist/postgres-js.mjs +38 -0
- package/dist/postgres-js.mjs.map +1 -0
- package/dist/provenance.cjs +67 -0
- package/dist/provenance.cjs.map +1 -0
- package/dist/provenance.d.cts +41 -0
- package/dist/provenance.d.cts.map +1 -0
- package/dist/provenance.d.mts +41 -0
- package/dist/provenance.d.mts.map +1 -0
- package/dist/provenance.mjs +65 -0
- package/dist/provenance.mjs.map +1 -0
- package/dist/push.cjs +14 -0
- package/dist/push.cjs.map +1 -0
- package/dist/push.d.cts +16 -0
- package/dist/push.d.cts.map +1 -0
- package/dist/push.d.mts +16 -0
- package/dist/push.d.mts.map +1 -0
- package/dist/push.mjs +13 -0
- package/dist/push.mjs.map +1 -0
- package/dist/react-email.cjs +48 -0
- package/dist/react-email.cjs.map +1 -0
- package/dist/react-email.d.cts +7 -0
- package/dist/react-email.d.cts.map +1 -0
- package/dist/react-email.d.mts +7 -0
- package/dist/react-email.d.mts.map +1 -0
- package/dist/react-email.mjs +47 -0
- package/dist/react-email.mjs.map +1 -0
- package/dist/reporting/classify.cjs +42 -0
- package/dist/reporting/classify.cjs.map +1 -0
- package/dist/reporting/classify.d.cts +60 -0
- package/dist/reporting/classify.d.cts.map +1 -0
- package/dist/reporting/classify.d.mts +60 -0
- package/dist/reporting/classify.d.mts.map +1 -0
- package/dist/reporting/classify.mjs +38 -0
- package/dist/reporting/classify.mjs.map +1 -0
- package/dist/reporting/observer.cjs +63 -0
- package/dist/reporting/observer.cjs.map +1 -0
- package/dist/reporting/observer.d.cts +18 -0
- package/dist/reporting/observer.d.cts.map +1 -0
- package/dist/reporting/observer.d.mts +18 -0
- package/dist/reporting/observer.d.mts.map +1 -0
- package/dist/reporting/observer.mjs +62 -0
- package/dist/reporting/observer.mjs.map +1 -0
- package/dist/reporting/tracking.cjs +153 -0
- package/dist/reporting/tracking.cjs.map +1 -0
- package/dist/reporting/tracking.d.cts +43 -0
- package/dist/reporting/tracking.d.cts.map +1 -0
- package/dist/reporting/tracking.d.mts +43 -0
- package/dist/reporting/tracking.d.mts.map +1 -0
- package/dist/reporting/tracking.mjs +151 -0
- package/dist/reporting/tracking.mjs.map +1 -0
- package/dist/runtime.cjs +19 -0
- package/dist/runtime.cjs.map +1 -0
- package/dist/runtime.d.cts +30 -0
- package/dist/runtime.d.cts.map +1 -0
- package/dist/runtime.d.mts +30 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +18 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/store/d1.cjs +185 -0
- package/dist/store/d1.cjs.map +1 -0
- package/dist/store/d1.d.cts +55 -0
- package/dist/store/d1.d.cts.map +1 -0
- package/dist/store/d1.d.mts +55 -0
- package/dist/store/d1.d.mts.map +1 -0
- package/dist/store/d1.mjs +184 -0
- package/dist/store/d1.mjs.map +1 -0
- package/dist/store/facts.cjs +60 -0
- package/dist/store/facts.cjs.map +1 -0
- package/dist/store/facts.d.cts +38 -0
- package/dist/store/facts.d.cts.map +1 -0
- package/dist/store/facts.d.mts +38 -0
- package/dist/store/facts.d.mts.map +1 -0
- package/dist/store/facts.mjs +58 -0
- package/dist/store/facts.mjs.map +1 -0
- package/dist/store/index.cjs +31 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.cts +291 -0
- package/dist/store/index.d.cts.map +1 -0
- package/dist/store/index.d.mts +291 -0
- package/dist/store/index.d.mts.map +1 -0
- package/dist/store/index.mjs +22 -0
- package/dist/store/index.mjs.map +1 -0
- package/dist/store/postgres-reporting.cjs +213 -0
- package/dist/store/postgres-reporting.cjs.map +1 -0
- package/dist/store/postgres-reporting.d.cts +63 -0
- package/dist/store/postgres-reporting.d.cts.map +1 -0
- package/dist/store/postgres-reporting.d.mts +63 -0
- package/dist/store/postgres-reporting.d.mts.map +1 -0
- package/dist/store/postgres-reporting.mjs +212 -0
- package/dist/store/postgres-reporting.mjs.map +1 -0
- package/dist/store/postgres.cjs +309 -0
- package/dist/store/postgres.cjs.map +1 -0
- package/dist/store/postgres.d.cts +77 -0
- package/dist/store/postgres.d.cts.map +1 -0
- package/dist/store/postgres.d.mts +77 -0
- package/dist/store/postgres.d.mts.map +1 -0
- package/dist/store/postgres.mjs +306 -0
- package/dist/store/postgres.mjs.map +1 -0
- package/dist/store/sql.cjs +24 -0
- package/dist/store/sql.cjs.map +1 -0
- package/dist/store/sql.d.cts +25 -0
- package/dist/store/sql.d.cts.map +1 -0
- package/dist/store/sql.d.mts +25 -0
- package/dist/store/sql.d.mts.map +1 -0
- package/dist/store/sql.mjs +23 -0
- package/dist/store/sql.mjs.map +1 -0
- package/dist/store/tables.cjs +404 -0
- package/dist/store/tables.cjs.map +1 -0
- package/dist/store/tables.d.cts +81 -0
- package/dist/store/tables.d.cts.map +1 -0
- package/dist/store/tables.d.mts +81 -0
- package/dist/store/tables.d.mts.map +1 -0
- package/dist/store/tables.mjs +399 -0
- package/dist/store/tables.mjs.map +1 -0
- package/dist/templates.cjs +247 -0
- package/dist/templates.cjs.map +1 -0
- package/dist/templates.d.cts +187 -0
- package/dist/templates.d.cts.map +1 -0
- package/dist/templates.d.mts +187 -0
- package/dist/templates.d.mts.map +1 -0
- package/dist/templates.mjs +230 -0
- package/dist/templates.mjs.map +1 -0
- package/dist/webhooks.cjs +96 -0
- package/dist/webhooks.cjs.map +1 -0
- package/dist/webhooks.d.cts +25 -0
- package/dist/webhooks.d.cts.map +1 -0
- package/dist/webhooks.d.mts +25 -0
- package/dist/webhooks.d.mts.map +1 -0
- package/dist/webhooks.mjs +94 -0
- package/dist/webhooks.mjs.map +1 -0
- package/dist/webpush.cjs +180 -0
- package/dist/webpush.cjs.map +1 -0
- package/dist/webpush.d.cts +53 -0
- package/dist/webpush.d.cts.map +1 -0
- package/dist/webpush.d.mts +53 -0
- package/dist/webpush.d.mts.map +1 -0
- package/dist/webpush.mjs +175 -0
- package/dist/webpush.mjs.map +1 -0
- package/package.json +189 -3
- package/src/cloudflare/schema.sql +127 -0
- package/src/cloudflare/workers.d.ts +44 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-scale.cjs","names":[],"sources":["../../src/engine/time-scale.ts"],"sourcesContent":["/**\n * Debug time scale: divide every duration in a workflow's IR by a factor so a\n * journey written in days plays out in seconds — with a factor of 3600, a\n * `delay('2 days')` sleeps 48 s, a `waitUntil(..., { timeout: '3 days' })`\n * gives up after 72 s, and a goal `within: '30 days'` closes after 12 min.\n *\n * Everything that is a Duration scales together (delays, random delays, wait\n * timeouts, goal and `performed(... within)` windows inside the workflow), so\n * the journey keeps its shape, just faster. What does not scale: wall-clock\n * `timeWindow`s (they are times of day, not durations). Segment definitions\n * live in the store rather than the workflow body; `scaleSegmentWindows`\n * scales them as they are read, so a segment's `within` keeps pace too.\n *\n * Durations are recognised structurally — an object whose only keys are\n * `value` (the source text) and `ms` — which is the DurationIR shape and\n * nothing else in the IR. The text is kept so the studio still shows what was\n * written.\n */\nexport function scaleDurations<T>(ir: T, factor: number): T {\n if (!(factor > 0) || factor === 1) return ir;\n return walk(ir, factor) as T;\n}\n\nfunction walk(value: unknown, factor: number): unknown {\n if (Array.isArray(value)) return value.map((item) => walk(item, factor));\n if (value === null || typeof value !== 'object') return value;\n const record = value as Record<string, unknown>;\n const keys = Object.keys(record);\n if (keys.length === 2 && typeof record.value === 'string' && typeof record.ms === 'number') {\n return { value: record.value, ms: Math.max(0, Math.round(record.ms / factor)) };\n }\n return Object.fromEntries(keys.map((key) => [key, walk(record[key], factor)]));\n}\n\n/**\n * A store whose segment definitions come back scaled by `factor` — what journeyWorker reads\n * through in fast-forward, so a segment's windows (`performed(e.login, { within: '7 days' })`)\n * shrink with the journeys that reference it, and with the segment triggers it routes. Every\n * other method is the store's own.\n */\nexport function scaleSegmentWindows<\n S extends { getSegmentCondition(name: string): Promise<unknown> },\n>(store: S, factor: number): S {\n if (!(factor > 0) || factor === 1) return store;\n return new Proxy(store, {\n get(target, property) {\n if (property === 'getSegmentCondition') {\n return async (name: string) =>\n scaleDurations(await target.getSegmentCondition(name), factor);\n }\n const value: unknown = Reflect.get(target, property, target);\n return typeof value === 'function' ? value.bind(target) : value;\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAkBA,SAAgB,eAAkB,IAAO,QAAmB;CAC1D,IAAI,EAAE,SAAS,MAAM,WAAW,GAAG,OAAO;CAC1C,OAAO,KAAK,IAAI,MAAM;AACxB;AAEA,SAAS,KAAK,OAAgB,QAAyB;CACrD,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,KAAK,SAAS,KAAK,MAAM,MAAM,CAAC;CACvE,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,MAAM,SAAS;CACf,MAAM,OAAO,OAAO,KAAK,MAAM;CAC/B,IAAI,KAAK,WAAW,KAAK,OAAO,OAAO,UAAU,YAAY,OAAO,OAAO,OAAO,UAChF,OAAO;EAAE,OAAO,OAAO;EAAO,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,KAAK,MAAM,CAAC;CAAE;CAEhF,OAAO,OAAO,YAAY,KAAK,KAAK,QAAQ,CAAC,KAAK,KAAK,OAAO,MAAM,MAAM,CAAC,CAAC,CAAC;AAC/E;;;;;;;AAQA,SAAgB,oBAEd,OAAU,QAAmB;CAC7B,IAAI,EAAE,SAAS,MAAM,WAAW,GAAG,OAAO;CAC1C,OAAO,IAAI,MAAM,OAAO,EACtB,IAAI,QAAQ,UAAU;EACpB,IAAI,aAAa,uBACf,OAAO,OAAO,SACZ,eAAe,MAAM,OAAO,oBAAoB,IAAI,GAAG,MAAM;EAEjE,MAAM,QAAiB,QAAQ,IAAI,QAAQ,UAAU,MAAM;EAC3D,OAAO,OAAO,UAAU,aAAa,MAAM,KAAK,MAAM,IAAI;CAC5D,EACF,CAAC;AACH"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/engine/time-scale.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Debug time scale: divide every duration in a workflow's IR by a factor so a
|
|
4
|
+
* journey written in days plays out in seconds — with a factor of 3600, a
|
|
5
|
+
* `delay('2 days')` sleeps 48 s, a `waitUntil(..., { timeout: '3 days' })`
|
|
6
|
+
* gives up after 72 s, and a goal `within: '30 days'` closes after 12 min.
|
|
7
|
+
*
|
|
8
|
+
* Everything that is a Duration scales together (delays, random delays, wait
|
|
9
|
+
* timeouts, goal and `performed(... within)` windows inside the workflow), so
|
|
10
|
+
* the journey keeps its shape, just faster. What does not scale: wall-clock
|
|
11
|
+
* `timeWindow`s (they are times of day, not durations). Segment definitions
|
|
12
|
+
* live in the store rather than the workflow body; `scaleSegmentWindows`
|
|
13
|
+
* scales them as they are read, so a segment's `within` keeps pace too.
|
|
14
|
+
*
|
|
15
|
+
* Durations are recognised structurally — an object whose only keys are
|
|
16
|
+
* `value` (the source text) and `ms` — which is the DurationIR shape and
|
|
17
|
+
* nothing else in the IR. The text is kept so the studio still shows what was
|
|
18
|
+
* written.
|
|
19
|
+
*/
|
|
20
|
+
export declare function scaleDurations<T>(ir: T, factor: number): T;
|
|
21
|
+
/**
|
|
22
|
+
* A store whose segment definitions come back scaled by `factor` — what journeyWorker reads
|
|
23
|
+
* through in fast-forward, so a segment's windows (`performed(e.login, { within: '7 days' })`)
|
|
24
|
+
* shrink with the journeys that reference it, and with the segment triggers it routes. Every
|
|
25
|
+
* other method is the store's own.
|
|
26
|
+
*/
|
|
27
|
+
export declare function scaleSegmentWindows<S extends {
|
|
28
|
+
getSegmentCondition(name: string): Promise<unknown>;
|
|
29
|
+
}>(store: S, factor: number): S;
|
|
30
|
+
//#endregion
|
|
31
|
+
//# sourceMappingURL=time-scale.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-scale.d.cts","names":[],"sources":["../../src/engine/time-scale.ts"],"mappings":";;;;;;;;;;;;;;;;;;;wBAkBgB,eAAe,GAAG,IAAI,GAAG,iBAAiB;;;;;;;wBAsB1C,oBACd;EAAY,oBAAoB,eAAe;GAC/C,OAAO,GAAG,iBAAiB"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/engine/time-scale.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Debug time scale: divide every duration in a workflow's IR by a factor so a
|
|
4
|
+
* journey written in days plays out in seconds — with a factor of 3600, a
|
|
5
|
+
* `delay('2 days')` sleeps 48 s, a `waitUntil(..., { timeout: '3 days' })`
|
|
6
|
+
* gives up after 72 s, and a goal `within: '30 days'` closes after 12 min.
|
|
7
|
+
*
|
|
8
|
+
* Everything that is a Duration scales together (delays, random delays, wait
|
|
9
|
+
* timeouts, goal and `performed(... within)` windows inside the workflow), so
|
|
10
|
+
* the journey keeps its shape, just faster. What does not scale: wall-clock
|
|
11
|
+
* `timeWindow`s (they are times of day, not durations). Segment definitions
|
|
12
|
+
* live in the store rather than the workflow body; `scaleSegmentWindows`
|
|
13
|
+
* scales them as they are read, so a segment's `within` keeps pace too.
|
|
14
|
+
*
|
|
15
|
+
* Durations are recognised structurally — an object whose only keys are
|
|
16
|
+
* `value` (the source text) and `ms` — which is the DurationIR shape and
|
|
17
|
+
* nothing else in the IR. The text is kept so the studio still shows what was
|
|
18
|
+
* written.
|
|
19
|
+
*/
|
|
20
|
+
export declare function scaleDurations<T>(ir: T, factor: number): T;
|
|
21
|
+
/**
|
|
22
|
+
* A store whose segment definitions come back scaled by `factor` — what journeyWorker reads
|
|
23
|
+
* through in fast-forward, so a segment's windows (`performed(e.login, { within: '7 days' })`)
|
|
24
|
+
* shrink with the journeys that reference it, and with the segment triggers it routes. Every
|
|
25
|
+
* other method is the store's own.
|
|
26
|
+
*/
|
|
27
|
+
export declare function scaleSegmentWindows<S extends {
|
|
28
|
+
getSegmentCondition(name: string): Promise<unknown>;
|
|
29
|
+
}>(store: S, factor: number): S;
|
|
30
|
+
//#endregion
|
|
31
|
+
//# sourceMappingURL=time-scale.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-scale.d.mts","names":[],"sources":["../../src/engine/time-scale.ts"],"mappings":";;;;;;;;;;;;;;;;;;;wBAkBgB,eAAe,GAAG,IAAI,GAAG,iBAAiB;;;;;;;wBAsB1C,oBACd;EAAY,oBAAoB,eAAe;GAC/C,OAAO,GAAG,iBAAiB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/engine/time-scale.ts
|
|
2
|
+
/**
|
|
3
|
+
* Debug time scale: divide every duration in a workflow's IR by a factor so a
|
|
4
|
+
* journey written in days plays out in seconds — with a factor of 3600, a
|
|
5
|
+
* `delay('2 days')` sleeps 48 s, a `waitUntil(..., { timeout: '3 days' })`
|
|
6
|
+
* gives up after 72 s, and a goal `within: '30 days'` closes after 12 min.
|
|
7
|
+
*
|
|
8
|
+
* Everything that is a Duration scales together (delays, random delays, wait
|
|
9
|
+
* timeouts, goal and `performed(... within)` windows inside the workflow), so
|
|
10
|
+
* the journey keeps its shape, just faster. What does not scale: wall-clock
|
|
11
|
+
* `timeWindow`s (they are times of day, not durations). Segment definitions
|
|
12
|
+
* live in the store rather than the workflow body; `scaleSegmentWindows`
|
|
13
|
+
* scales them as they are read, so a segment's `within` keeps pace too.
|
|
14
|
+
*
|
|
15
|
+
* Durations are recognised structurally — an object whose only keys are
|
|
16
|
+
* `value` (the source text) and `ms` — which is the DurationIR shape and
|
|
17
|
+
* nothing else in the IR. The text is kept so the studio still shows what was
|
|
18
|
+
* written.
|
|
19
|
+
*/
|
|
20
|
+
function scaleDurations(ir, factor) {
|
|
21
|
+
if (!(factor > 0) || factor === 1) return ir;
|
|
22
|
+
return walk(ir, factor);
|
|
23
|
+
}
|
|
24
|
+
function walk(value, factor) {
|
|
25
|
+
if (Array.isArray(value)) return value.map((item) => walk(item, factor));
|
|
26
|
+
if (value === null || typeof value !== "object") return value;
|
|
27
|
+
const record = value;
|
|
28
|
+
const keys = Object.keys(record);
|
|
29
|
+
if (keys.length === 2 && typeof record.value === "string" && typeof record.ms === "number") return {
|
|
30
|
+
value: record.value,
|
|
31
|
+
ms: Math.max(0, Math.round(record.ms / factor))
|
|
32
|
+
};
|
|
33
|
+
return Object.fromEntries(keys.map((key) => [key, walk(record[key], factor)]));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A store whose segment definitions come back scaled by `factor` — what journeyWorker reads
|
|
37
|
+
* through in fast-forward, so a segment's windows (`performed(e.login, { within: '7 days' })`)
|
|
38
|
+
* shrink with the journeys that reference it, and with the segment triggers it routes. Every
|
|
39
|
+
* other method is the store's own.
|
|
40
|
+
*/
|
|
41
|
+
function scaleSegmentWindows(store, factor) {
|
|
42
|
+
if (!(factor > 0) || factor === 1) return store;
|
|
43
|
+
return new Proxy(store, { get(target, property) {
|
|
44
|
+
if (property === "getSegmentCondition") return async (name) => scaleDurations(await target.getSegmentCondition(name), factor);
|
|
45
|
+
const value = Reflect.get(target, property, target);
|
|
46
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
47
|
+
} });
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { scaleDurations, scaleSegmentWindows };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=time-scale.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-scale.mjs","names":[],"sources":["../../src/engine/time-scale.ts"],"sourcesContent":["/**\n * Debug time scale: divide every duration in a workflow's IR by a factor so a\n * journey written in days plays out in seconds — with a factor of 3600, a\n * `delay('2 days')` sleeps 48 s, a `waitUntil(..., { timeout: '3 days' })`\n * gives up after 72 s, and a goal `within: '30 days'` closes after 12 min.\n *\n * Everything that is a Duration scales together (delays, random delays, wait\n * timeouts, goal and `performed(... within)` windows inside the workflow), so\n * the journey keeps its shape, just faster. What does not scale: wall-clock\n * `timeWindow`s (they are times of day, not durations). Segment definitions\n * live in the store rather than the workflow body; `scaleSegmentWindows`\n * scales them as they are read, so a segment's `within` keeps pace too.\n *\n * Durations are recognised structurally — an object whose only keys are\n * `value` (the source text) and `ms` — which is the DurationIR shape and\n * nothing else in the IR. The text is kept so the studio still shows what was\n * written.\n */\nexport function scaleDurations<T>(ir: T, factor: number): T {\n if (!(factor > 0) || factor === 1) return ir;\n return walk(ir, factor) as T;\n}\n\nfunction walk(value: unknown, factor: number): unknown {\n if (Array.isArray(value)) return value.map((item) => walk(item, factor));\n if (value === null || typeof value !== 'object') return value;\n const record = value as Record<string, unknown>;\n const keys = Object.keys(record);\n if (keys.length === 2 && typeof record.value === 'string' && typeof record.ms === 'number') {\n return { value: record.value, ms: Math.max(0, Math.round(record.ms / factor)) };\n }\n return Object.fromEntries(keys.map((key) => [key, walk(record[key], factor)]));\n}\n\n/**\n * A store whose segment definitions come back scaled by `factor` — what journeyWorker reads\n * through in fast-forward, so a segment's windows (`performed(e.login, { within: '7 days' })`)\n * shrink with the journeys that reference it, and with the segment triggers it routes. Every\n * other method is the store's own.\n */\nexport function scaleSegmentWindows<\n S extends { getSegmentCondition(name: string): Promise<unknown> },\n>(store: S, factor: number): S {\n if (!(factor > 0) || factor === 1) return store;\n return new Proxy(store, {\n get(target, property) {\n if (property === 'getSegmentCondition') {\n return async (name: string) =>\n scaleDurations(await target.getSegmentCondition(name), factor);\n }\n const value: unknown = Reflect.get(target, property, target);\n return typeof value === 'function' ? value.bind(target) : value;\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,SAAgB,eAAkB,IAAO,QAAmB;CAC1D,IAAI,EAAE,SAAS,MAAM,WAAW,GAAG,OAAO;CAC1C,OAAO,KAAK,IAAI,MAAM;AACxB;AAEA,SAAS,KAAK,OAAgB,QAAyB;CACrD,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,KAAK,SAAS,KAAK,MAAM,MAAM,CAAC;CACvE,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,MAAM,SAAS;CACf,MAAM,OAAO,OAAO,KAAK,MAAM;CAC/B,IAAI,KAAK,WAAW,KAAK,OAAO,OAAO,UAAU,YAAY,OAAO,OAAO,OAAO,UAChF,OAAO;EAAE,OAAO,OAAO;EAAO,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,KAAK,MAAM,CAAC;CAAE;CAEhF,OAAO,OAAO,YAAY,KAAK,KAAK,QAAQ,CAAC,KAAK,KAAK,OAAO,MAAM,MAAM,CAAC,CAAC,CAAC;AAC/E;;;;;;;AAQA,SAAgB,oBAEd,OAAU,QAAmB;CAC7B,IAAI,EAAE,SAAS,MAAM,WAAW,GAAG,OAAO;CAC1C,OAAO,IAAI,MAAM,OAAO,EACtB,IAAI,QAAQ,UAAU;EACpB,IAAI,aAAa,uBACf,OAAO,OAAO,SACZ,eAAe,MAAM,OAAO,oBAAoB,IAAI,GAAG,MAAM;EAEjE,MAAM,QAAiB,QAAQ,IAAI,QAAQ,UAAU,MAAM;EAC3D,OAAO,OAAO,UAAU,aAAa,MAAM,KAAK,MAAM,IAAI;CAC5D,EACF,CAAC;AACH"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/engine/time-window.ts
|
|
3
|
+
/**
|
|
4
|
+
* Time-window scheduling — when does the next send window open?
|
|
5
|
+
*
|
|
6
|
+
* Pure functions of (nowMs, days, between, tz): the interpreter calls them
|
|
7
|
+
* inside a durable step, so the answer is checkpointed and replay-safe.
|
|
8
|
+
* Timezone math goes through Intl.DateTimeFormat (available in Node, workerd
|
|
9
|
+
* and browsers alike) — no tz database dependency. An unknown timezone falls
|
|
10
|
+
* back to UTC rather than failing the journey.
|
|
11
|
+
*/
|
|
12
|
+
const DAY_MS = 864e5;
|
|
13
|
+
/** Indexed by Date#getUTCDay, matching WeekdayIR's lowercase three-letter form. */
|
|
14
|
+
const WEEKDAYS = [
|
|
15
|
+
"sun",
|
|
16
|
+
"mon",
|
|
17
|
+
"tue",
|
|
18
|
+
"wed",
|
|
19
|
+
"thu",
|
|
20
|
+
"fri",
|
|
21
|
+
"sat"
|
|
22
|
+
];
|
|
23
|
+
const formatters = /* @__PURE__ */ new Map();
|
|
24
|
+
/** Cached per-timezone formatter; throws on an invalid timezone id. */
|
|
25
|
+
function formatterFor(tz) {
|
|
26
|
+
let dtf = formatters.get(tz);
|
|
27
|
+
if (dtf === void 0) {
|
|
28
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
29
|
+
timeZone: tz,
|
|
30
|
+
hourCycle: "h23",
|
|
31
|
+
year: "numeric",
|
|
32
|
+
month: "numeric",
|
|
33
|
+
day: "numeric",
|
|
34
|
+
hour: "numeric",
|
|
35
|
+
minute: "numeric"
|
|
36
|
+
});
|
|
37
|
+
formatters.set(tz, dtf);
|
|
38
|
+
}
|
|
39
|
+
return dtf;
|
|
40
|
+
}
|
|
41
|
+
/** Validate a timezone id, falling back to UTC — a bad profile value must not wedge the journey. */
|
|
42
|
+
function resolveTimeZone(tz) {
|
|
43
|
+
try {
|
|
44
|
+
formatterFor(tz);
|
|
45
|
+
return tz;
|
|
46
|
+
} catch {
|
|
47
|
+
return "UTC";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function localParts(ms, tz) {
|
|
51
|
+
const parts = {};
|
|
52
|
+
for (const part of formatterFor(tz).formatToParts(new Date(ms))) parts[part.type] = part.value;
|
|
53
|
+
return {
|
|
54
|
+
year: Number(parts.year),
|
|
55
|
+
month: Number(parts.month),
|
|
56
|
+
day: Number(parts.day),
|
|
57
|
+
hour: Number(parts.hour),
|
|
58
|
+
minute: Number(parts.minute)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** The zone's UTC offset at an instant, as (wall clock read as if it were UTC) − instant. */
|
|
62
|
+
function offsetMsAt(ts, tz) {
|
|
63
|
+
const p = localParts(ts, tz);
|
|
64
|
+
return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute) - ts;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The first instant at or after `before` whose offset differs from the one
|
|
68
|
+
* there — i.e. the DST transition separating the two bounds. Bisects on the
|
|
69
|
+
* minute grid, the finest granularity any tz rule uses, so the loop lands on
|
|
70
|
+
* the transition itself rather than merely near it.
|
|
71
|
+
*/
|
|
72
|
+
function transitionBetween(before, after, tz) {
|
|
73
|
+
const startOffset = offsetMsAt(before, tz);
|
|
74
|
+
let lo = before;
|
|
75
|
+
let hi = after;
|
|
76
|
+
while (hi - lo > 6e4) {
|
|
77
|
+
const mid = lo + Math.floor((hi - lo) / 12e4) * 6e4;
|
|
78
|
+
if (offsetMsAt(mid, tz) === startOffset) lo = mid;
|
|
79
|
+
else hi = mid;
|
|
80
|
+
}
|
|
81
|
+
return hi;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The UTC instant at which the wall clock in `tz` reads y-m-d hh:mm.
|
|
85
|
+
*
|
|
86
|
+
* Guess the UTC value from the offset at the guess, read it back through the
|
|
87
|
+
* timezone and correct by the difference; a second pass settles every wall
|
|
88
|
+
* time that exists, and picks the earlier of the two when a fall-back repeats
|
|
89
|
+
* an hour.
|
|
90
|
+
*
|
|
91
|
+
* A wall time inside a spring-forward gap exists on no clock, so the two
|
|
92
|
+
* corrections straddle the transition and neither reads back as asked. The
|
|
93
|
+
* convention here is the usual one: move forward to the first instant that
|
|
94
|
+
* does exist, i.e. the transition itself — 02:30 on a US spring-forward day
|
|
95
|
+
* resolves to 03:00. Without that branch the correction bottoms out on the low
|
|
96
|
+
* side and the window opens an hour *before* its configured start.
|
|
97
|
+
*/
|
|
98
|
+
function zonedInstant(year, month, day, hour, minute, tz) {
|
|
99
|
+
const target = Date.UTC(year, month - 1, day, hour, minute);
|
|
100
|
+
const first = target - offsetMsAt(target, tz);
|
|
101
|
+
if (offsetMsAt(first, tz) === target - first) return first;
|
|
102
|
+
const second = target - offsetMsAt(first, tz);
|
|
103
|
+
if (offsetMsAt(second, tz) === target - second) return second;
|
|
104
|
+
return transitionBetween(Math.min(first, second), Math.max(first, second), tz);
|
|
105
|
+
}
|
|
106
|
+
const TIME = /^(\d{1,2}):(\d{2})$/;
|
|
107
|
+
function parseTime(value) {
|
|
108
|
+
const match = TIME.exec(value);
|
|
109
|
+
if (!match) return null;
|
|
110
|
+
const hh = Number(match[1]);
|
|
111
|
+
const mm = Number(match[2]);
|
|
112
|
+
if (hh > 23 || mm > 59) return null;
|
|
113
|
+
return {
|
|
114
|
+
hh,
|
|
115
|
+
mm
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The instant the next [start, end) window opens in `tz`, or null when nowMs
|
|
120
|
+
* is already inside a window. Scans nine calendar days so every weekday is
|
|
121
|
+
* reachable. Malformed times (the DSL validates its own input, but IR may come
|
|
122
|
+
* from elsewhere) disable the window — better to send now than to hold a user
|
|
123
|
+
* forever on a config typo.
|
|
124
|
+
*/
|
|
125
|
+
function nextWindowStart(nowMs, days, between, tz) {
|
|
126
|
+
const start = parseTime(between[0]);
|
|
127
|
+
const end = parseTime(between[1]);
|
|
128
|
+
if (start === null || end === null) return null;
|
|
129
|
+
const zone = resolveTimeZone(tz);
|
|
130
|
+
const today = localParts(nowMs, zone);
|
|
131
|
+
const firstDay = Date.UTC(today.year, today.month - 1, today.day);
|
|
132
|
+
for (let offset = 0; offset < 9; offset++) {
|
|
133
|
+
const date = new Date(firstDay + offset * DAY_MS);
|
|
134
|
+
if (!days.includes(WEEKDAYS[date.getUTCDay()])) continue;
|
|
135
|
+
const year = date.getUTCFullYear();
|
|
136
|
+
const month = date.getUTCMonth() + 1;
|
|
137
|
+
const day = date.getUTCDate();
|
|
138
|
+
const opensAt = zonedInstant(year, month, day, start.hh, start.mm, zone);
|
|
139
|
+
const closesAt = zonedInstant(year, month, day, end.hh, end.mm, zone);
|
|
140
|
+
if (nowMs < opensAt) return opensAt;
|
|
141
|
+
if (nowMs < closesAt) return null;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
exports.nextWindowStart = nextWindowStart;
|
|
147
|
+
exports.resolveTimeZone = resolveTimeZone;
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=time-window.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-window.cjs","names":[],"sources":["../../src/engine/time-window.ts"],"sourcesContent":["/**\n * Time-window scheduling — when does the next send window open?\n *\n * Pure functions of (nowMs, days, between, tz): the interpreter calls them\n * inside a durable step, so the answer is checkpointed and replay-safe.\n * Timezone math goes through Intl.DateTimeFormat (available in Node, workerd\n * and browsers alike) — no tz database dependency. An unknown timezone falls\n * back to UTC rather than failing the journey.\n */\n\nconst DAY_MS = 86_400_000;\n\n/** Indexed by Date#getUTCDay, matching WeekdayIR's lowercase three-letter form. */\nconst WEEKDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];\n\ninterface LocalParts {\n year: number;\n month: number;\n day: number;\n hour: number;\n minute: number;\n}\n\nconst formatters = new Map<string, Intl.DateTimeFormat>();\n\n/** Cached per-timezone formatter; throws on an invalid timezone id. */\nfunction formatterFor(tz: string): Intl.DateTimeFormat {\n let dtf = formatters.get(tz);\n if (dtf === undefined) {\n dtf = new Intl.DateTimeFormat('en-US', {\n timeZone: tz,\n hourCycle: 'h23',\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n });\n formatters.set(tz, dtf);\n }\n return dtf;\n}\n\n/** Validate a timezone id, falling back to UTC — a bad profile value must not wedge the journey. */\nexport function resolveTimeZone(tz: string): string {\n try {\n formatterFor(tz);\n return tz;\n } catch {\n return 'UTC';\n }\n}\n\nfunction localParts(ms: number, tz: string): LocalParts {\n const parts: Partial<Record<string, string>> = {};\n for (const part of formatterFor(tz).formatToParts(new Date(ms))) {\n parts[part.type] = part.value;\n }\n return {\n year: Number(parts.year),\n month: Number(parts.month),\n day: Number(parts.day),\n hour: Number(parts.hour),\n minute: Number(parts.minute),\n };\n}\n\n/** The zone's UTC offset at an instant, as (wall clock read as if it were UTC) − instant. */\nfunction offsetMsAt(ts: number, tz: string): number {\n const p = localParts(ts, tz);\n return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute) - ts;\n}\n\n/**\n * The first instant at or after `before` whose offset differs from the one\n * there — i.e. the DST transition separating the two bounds. Bisects on the\n * minute grid, the finest granularity any tz rule uses, so the loop lands on\n * the transition itself rather than merely near it.\n */\nfunction transitionBetween(before: number, after: number, tz: string): number {\n const startOffset = offsetMsAt(before, tz);\n let lo = before;\n let hi = after;\n while (hi - lo > 60_000) {\n // Halve on whole minutes: both bounds stay minute-aligned, so mid is\n // strictly inside (lo, hi) and the loop cannot stall.\n const mid = lo + Math.floor((hi - lo) / 120_000) * 60_000;\n if (offsetMsAt(mid, tz) === startOffset) lo = mid;\n else hi = mid;\n }\n return hi;\n}\n\n/**\n * The UTC instant at which the wall clock in `tz` reads y-m-d hh:mm.\n *\n * Guess the UTC value from the offset at the guess, read it back through the\n * timezone and correct by the difference; a second pass settles every wall\n * time that exists, and picks the earlier of the two when a fall-back repeats\n * an hour.\n *\n * A wall time inside a spring-forward gap exists on no clock, so the two\n * corrections straddle the transition and neither reads back as asked. The\n * convention here is the usual one: move forward to the first instant that\n * does exist, i.e. the transition itself — 02:30 on a US spring-forward day\n * resolves to 03:00. Without that branch the correction bottoms out on the low\n * side and the window opens an hour *before* its configured start.\n */\nfunction zonedInstant(\n year: number,\n month: number,\n day: number,\n hour: number,\n minute: number,\n tz: string\n): number {\n const target = Date.UTC(year, month - 1, day, hour, minute);\n const first = target - offsetMsAt(target, tz);\n if (offsetMsAt(first, tz) === target - first) return first;\n const second = target - offsetMsAt(first, tz);\n if (offsetMsAt(second, tz) === target - second) return second;\n return transitionBetween(Math.min(first, second), Math.max(first, second), tz);\n}\n\nconst TIME = /^(\\d{1,2}):(\\d{2})$/;\n\nfunction parseTime(value: string): { hh: number; mm: number } | null {\n const match = TIME.exec(value);\n if (!match) return null;\n const hh = Number(match[1]);\n const mm = Number(match[2]);\n if (hh > 23 || mm > 59) return null;\n return { hh, mm };\n}\n\n/**\n * The instant the next [start, end) window opens in `tz`, or null when nowMs\n * is already inside a window. Scans nine calendar days so every weekday is\n * reachable. Malformed times (the DSL validates its own input, but IR may come\n * from elsewhere) disable the window — better to send now than to hold a user\n * forever on a config typo.\n */\nexport function nextWindowStart(\n nowMs: number,\n days: readonly string[],\n between: readonly [string, string],\n tz: string\n): number | null {\n const start = parseTime(between[0]);\n const end = parseTime(between[1]);\n if (start === null || end === null) return null;\n const zone = resolveTimeZone(tz);\n\n // Step the local *calendar*, not the absolute clock: a spring-forward day is\n // 23 hours long, so nowMs + n·24h crosses two local midnights and skips that\n // date entirely — the send would be held until the weekday came round again.\n // Date.UTC arithmetic over the civil date has no DST to trip on, and\n // getUTCDay names the same weekday the zone does.\n const today = localParts(nowMs, zone);\n const firstDay = Date.UTC(today.year, today.month - 1, today.day);\n\n for (let offset = 0; offset < 9; offset++) {\n const date = new Date(firstDay + offset * DAY_MS);\n if (!days.includes(WEEKDAYS[date.getUTCDay()])) continue;\n const year = date.getUTCFullYear();\n const month = date.getUTCMonth() + 1;\n const day = date.getUTCDate();\n const opensAt = zonedInstant(year, month, day, start.hh, start.mm, zone);\n const closesAt = zonedInstant(year, month, day, end.hh, end.mm, zone);\n if (nowMs < opensAt) return opensAt;\n if (nowMs < closesAt) return null;\n // Past this day's window (e.g. 23:00 against 09:00–17:00) → keep scanning forward\n }\n return null; // no allowed day in range (an empty days list) → do not wait\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/engine/time-window.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Time-window scheduling — when does the next send window open?
|
|
4
|
+
*
|
|
5
|
+
* Pure functions of (nowMs, days, between, tz): the interpreter calls them
|
|
6
|
+
* inside a durable step, so the answer is checkpointed and replay-safe.
|
|
7
|
+
* Timezone math goes through Intl.DateTimeFormat (available in Node, workerd
|
|
8
|
+
* and browsers alike) — no tz database dependency. An unknown timezone falls
|
|
9
|
+
* back to UTC rather than failing the journey.
|
|
10
|
+
*/
|
|
11
|
+
/** Validate a timezone id, falling back to UTC — a bad profile value must not wedge the journey. */
|
|
12
|
+
export declare function resolveTimeZone(tz: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* The instant the next [start, end) window opens in `tz`, or null when nowMs
|
|
15
|
+
* is already inside a window. Scans nine calendar days so every weekday is
|
|
16
|
+
* reachable. Malformed times (the DSL validates its own input, but IR may come
|
|
17
|
+
* from elsewhere) disable the window — better to send now than to hold a user
|
|
18
|
+
* forever on a config typo.
|
|
19
|
+
*/
|
|
20
|
+
export declare function nextWindowStart(nowMs: number, days: readonly string[], between: readonly [string, string], tz: string): number | null;
|
|
21
|
+
//#endregion
|
|
22
|
+
//# sourceMappingURL=time-window.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-window.d.cts","names":[],"sources":["../../src/engine/time-window.ts"],"mappings":";;;;;;;;;;;wBA4CgB,gBAAgB;;;;;;;;wBAkGhB,gBACd,eACA,yBACA,oCACA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/engine/time-window.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Time-window scheduling — when does the next send window open?
|
|
4
|
+
*
|
|
5
|
+
* Pure functions of (nowMs, days, between, tz): the interpreter calls them
|
|
6
|
+
* inside a durable step, so the answer is checkpointed and replay-safe.
|
|
7
|
+
* Timezone math goes through Intl.DateTimeFormat (available in Node, workerd
|
|
8
|
+
* and browsers alike) — no tz database dependency. An unknown timezone falls
|
|
9
|
+
* back to UTC rather than failing the journey.
|
|
10
|
+
*/
|
|
11
|
+
/** Validate a timezone id, falling back to UTC — a bad profile value must not wedge the journey. */
|
|
12
|
+
export declare function resolveTimeZone(tz: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* The instant the next [start, end) window opens in `tz`, or null when nowMs
|
|
15
|
+
* is already inside a window. Scans nine calendar days so every weekday is
|
|
16
|
+
* reachable. Malformed times (the DSL validates its own input, but IR may come
|
|
17
|
+
* from elsewhere) disable the window — better to send now than to hold a user
|
|
18
|
+
* forever on a config typo.
|
|
19
|
+
*/
|
|
20
|
+
export declare function nextWindowStart(nowMs: number, days: readonly string[], between: readonly [string, string], tz: string): number | null;
|
|
21
|
+
//#endregion
|
|
22
|
+
//# sourceMappingURL=time-window.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-window.d.mts","names":[],"sources":["../../src/engine/time-window.ts"],"mappings":";;;;;;;;;;;wBA4CgB,gBAAgB;;;;;;;;wBAkGhB,gBACd,eACA,yBACA,oCACA"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
//#region src/engine/time-window.ts
|
|
2
|
+
/**
|
|
3
|
+
* Time-window scheduling — when does the next send window open?
|
|
4
|
+
*
|
|
5
|
+
* Pure functions of (nowMs, days, between, tz): the interpreter calls them
|
|
6
|
+
* inside a durable step, so the answer is checkpointed and replay-safe.
|
|
7
|
+
* Timezone math goes through Intl.DateTimeFormat (available in Node, workerd
|
|
8
|
+
* and browsers alike) — no tz database dependency. An unknown timezone falls
|
|
9
|
+
* back to UTC rather than failing the journey.
|
|
10
|
+
*/
|
|
11
|
+
const DAY_MS = 864e5;
|
|
12
|
+
/** Indexed by Date#getUTCDay, matching WeekdayIR's lowercase three-letter form. */
|
|
13
|
+
const WEEKDAYS = [
|
|
14
|
+
"sun",
|
|
15
|
+
"mon",
|
|
16
|
+
"tue",
|
|
17
|
+
"wed",
|
|
18
|
+
"thu",
|
|
19
|
+
"fri",
|
|
20
|
+
"sat"
|
|
21
|
+
];
|
|
22
|
+
const formatters = /* @__PURE__ */ new Map();
|
|
23
|
+
/** Cached per-timezone formatter; throws on an invalid timezone id. */
|
|
24
|
+
function formatterFor(tz) {
|
|
25
|
+
let dtf = formatters.get(tz);
|
|
26
|
+
if (dtf === void 0) {
|
|
27
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
28
|
+
timeZone: tz,
|
|
29
|
+
hourCycle: "h23",
|
|
30
|
+
year: "numeric",
|
|
31
|
+
month: "numeric",
|
|
32
|
+
day: "numeric",
|
|
33
|
+
hour: "numeric",
|
|
34
|
+
minute: "numeric"
|
|
35
|
+
});
|
|
36
|
+
formatters.set(tz, dtf);
|
|
37
|
+
}
|
|
38
|
+
return dtf;
|
|
39
|
+
}
|
|
40
|
+
/** Validate a timezone id, falling back to UTC — a bad profile value must not wedge the journey. */
|
|
41
|
+
function resolveTimeZone(tz) {
|
|
42
|
+
try {
|
|
43
|
+
formatterFor(tz);
|
|
44
|
+
return tz;
|
|
45
|
+
} catch {
|
|
46
|
+
return "UTC";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function localParts(ms, tz) {
|
|
50
|
+
const parts = {};
|
|
51
|
+
for (const part of formatterFor(tz).formatToParts(new Date(ms))) parts[part.type] = part.value;
|
|
52
|
+
return {
|
|
53
|
+
year: Number(parts.year),
|
|
54
|
+
month: Number(parts.month),
|
|
55
|
+
day: Number(parts.day),
|
|
56
|
+
hour: Number(parts.hour),
|
|
57
|
+
minute: Number(parts.minute)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** The zone's UTC offset at an instant, as (wall clock read as if it were UTC) − instant. */
|
|
61
|
+
function offsetMsAt(ts, tz) {
|
|
62
|
+
const p = localParts(ts, tz);
|
|
63
|
+
return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute) - ts;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The first instant at or after `before` whose offset differs from the one
|
|
67
|
+
* there — i.e. the DST transition separating the two bounds. Bisects on the
|
|
68
|
+
* minute grid, the finest granularity any tz rule uses, so the loop lands on
|
|
69
|
+
* the transition itself rather than merely near it.
|
|
70
|
+
*/
|
|
71
|
+
function transitionBetween(before, after, tz) {
|
|
72
|
+
const startOffset = offsetMsAt(before, tz);
|
|
73
|
+
let lo = before;
|
|
74
|
+
let hi = after;
|
|
75
|
+
while (hi - lo > 6e4) {
|
|
76
|
+
const mid = lo + Math.floor((hi - lo) / 12e4) * 6e4;
|
|
77
|
+
if (offsetMsAt(mid, tz) === startOffset) lo = mid;
|
|
78
|
+
else hi = mid;
|
|
79
|
+
}
|
|
80
|
+
return hi;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The UTC instant at which the wall clock in `tz` reads y-m-d hh:mm.
|
|
84
|
+
*
|
|
85
|
+
* Guess the UTC value from the offset at the guess, read it back through the
|
|
86
|
+
* timezone and correct by the difference; a second pass settles every wall
|
|
87
|
+
* time that exists, and picks the earlier of the two when a fall-back repeats
|
|
88
|
+
* an hour.
|
|
89
|
+
*
|
|
90
|
+
* A wall time inside a spring-forward gap exists on no clock, so the two
|
|
91
|
+
* corrections straddle the transition and neither reads back as asked. The
|
|
92
|
+
* convention here is the usual one: move forward to the first instant that
|
|
93
|
+
* does exist, i.e. the transition itself — 02:30 on a US spring-forward day
|
|
94
|
+
* resolves to 03:00. Without that branch the correction bottoms out on the low
|
|
95
|
+
* side and the window opens an hour *before* its configured start.
|
|
96
|
+
*/
|
|
97
|
+
function zonedInstant(year, month, day, hour, minute, tz) {
|
|
98
|
+
const target = Date.UTC(year, month - 1, day, hour, minute);
|
|
99
|
+
const first = target - offsetMsAt(target, tz);
|
|
100
|
+
if (offsetMsAt(first, tz) === target - first) return first;
|
|
101
|
+
const second = target - offsetMsAt(first, tz);
|
|
102
|
+
if (offsetMsAt(second, tz) === target - second) return second;
|
|
103
|
+
return transitionBetween(Math.min(first, second), Math.max(first, second), tz);
|
|
104
|
+
}
|
|
105
|
+
const TIME = /^(\d{1,2}):(\d{2})$/;
|
|
106
|
+
function parseTime(value) {
|
|
107
|
+
const match = TIME.exec(value);
|
|
108
|
+
if (!match) return null;
|
|
109
|
+
const hh = Number(match[1]);
|
|
110
|
+
const mm = Number(match[2]);
|
|
111
|
+
if (hh > 23 || mm > 59) return null;
|
|
112
|
+
return {
|
|
113
|
+
hh,
|
|
114
|
+
mm
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The instant the next [start, end) window opens in `tz`, or null when nowMs
|
|
119
|
+
* is already inside a window. Scans nine calendar days so every weekday is
|
|
120
|
+
* reachable. Malformed times (the DSL validates its own input, but IR may come
|
|
121
|
+
* from elsewhere) disable the window — better to send now than to hold a user
|
|
122
|
+
* forever on a config typo.
|
|
123
|
+
*/
|
|
124
|
+
function nextWindowStart(nowMs, days, between, tz) {
|
|
125
|
+
const start = parseTime(between[0]);
|
|
126
|
+
const end = parseTime(between[1]);
|
|
127
|
+
if (start === null || end === null) return null;
|
|
128
|
+
const zone = resolveTimeZone(tz);
|
|
129
|
+
const today = localParts(nowMs, zone);
|
|
130
|
+
const firstDay = Date.UTC(today.year, today.month - 1, today.day);
|
|
131
|
+
for (let offset = 0; offset < 9; offset++) {
|
|
132
|
+
const date = new Date(firstDay + offset * DAY_MS);
|
|
133
|
+
if (!days.includes(WEEKDAYS[date.getUTCDay()])) continue;
|
|
134
|
+
const year = date.getUTCFullYear();
|
|
135
|
+
const month = date.getUTCMonth() + 1;
|
|
136
|
+
const day = date.getUTCDate();
|
|
137
|
+
const opensAt = zonedInstant(year, month, day, start.hh, start.mm, zone);
|
|
138
|
+
const closesAt = zonedInstant(year, month, day, end.hh, end.mm, zone);
|
|
139
|
+
if (nowMs < opensAt) return opensAt;
|
|
140
|
+
if (nowMs < closesAt) return null;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { nextWindowStart, resolveTimeZone };
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=time-window.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-window.mjs","names":[],"sources":["../../src/engine/time-window.ts"],"sourcesContent":["/**\n * Time-window scheduling — when does the next send window open?\n *\n * Pure functions of (nowMs, days, between, tz): the interpreter calls them\n * inside a durable step, so the answer is checkpointed and replay-safe.\n * Timezone math goes through Intl.DateTimeFormat (available in Node, workerd\n * and browsers alike) — no tz database dependency. An unknown timezone falls\n * back to UTC rather than failing the journey.\n */\n\nconst DAY_MS = 86_400_000;\n\n/** Indexed by Date#getUTCDay, matching WeekdayIR's lowercase three-letter form. */\nconst WEEKDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];\n\ninterface LocalParts {\n year: number;\n month: number;\n day: number;\n hour: number;\n minute: number;\n}\n\nconst formatters = new Map<string, Intl.DateTimeFormat>();\n\n/** Cached per-timezone formatter; throws on an invalid timezone id. */\nfunction formatterFor(tz: string): Intl.DateTimeFormat {\n let dtf = formatters.get(tz);\n if (dtf === undefined) {\n dtf = new Intl.DateTimeFormat('en-US', {\n timeZone: tz,\n hourCycle: 'h23',\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n });\n formatters.set(tz, dtf);\n }\n return dtf;\n}\n\n/** Validate a timezone id, falling back to UTC — a bad profile value must not wedge the journey. */\nexport function resolveTimeZone(tz: string): string {\n try {\n formatterFor(tz);\n return tz;\n } catch {\n return 'UTC';\n }\n}\n\nfunction localParts(ms: number, tz: string): LocalParts {\n const parts: Partial<Record<string, string>> = {};\n for (const part of formatterFor(tz).formatToParts(new Date(ms))) {\n parts[part.type] = part.value;\n }\n return {\n year: Number(parts.year),\n month: Number(parts.month),\n day: Number(parts.day),\n hour: Number(parts.hour),\n minute: Number(parts.minute),\n };\n}\n\n/** The zone's UTC offset at an instant, as (wall clock read as if it were UTC) − instant. */\nfunction offsetMsAt(ts: number, tz: string): number {\n const p = localParts(ts, tz);\n return Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute) - ts;\n}\n\n/**\n * The first instant at or after `before` whose offset differs from the one\n * there — i.e. the DST transition separating the two bounds. Bisects on the\n * minute grid, the finest granularity any tz rule uses, so the loop lands on\n * the transition itself rather than merely near it.\n */\nfunction transitionBetween(before: number, after: number, tz: string): number {\n const startOffset = offsetMsAt(before, tz);\n let lo = before;\n let hi = after;\n while (hi - lo > 60_000) {\n // Halve on whole minutes: both bounds stay minute-aligned, so mid is\n // strictly inside (lo, hi) and the loop cannot stall.\n const mid = lo + Math.floor((hi - lo) / 120_000) * 60_000;\n if (offsetMsAt(mid, tz) === startOffset) lo = mid;\n else hi = mid;\n }\n return hi;\n}\n\n/**\n * The UTC instant at which the wall clock in `tz` reads y-m-d hh:mm.\n *\n * Guess the UTC value from the offset at the guess, read it back through the\n * timezone and correct by the difference; a second pass settles every wall\n * time that exists, and picks the earlier of the two when a fall-back repeats\n * an hour.\n *\n * A wall time inside a spring-forward gap exists on no clock, so the two\n * corrections straddle the transition and neither reads back as asked. The\n * convention here is the usual one: move forward to the first instant that\n * does exist, i.e. the transition itself — 02:30 on a US spring-forward day\n * resolves to 03:00. Without that branch the correction bottoms out on the low\n * side and the window opens an hour *before* its configured start.\n */\nfunction zonedInstant(\n year: number,\n month: number,\n day: number,\n hour: number,\n minute: number,\n tz: string\n): number {\n const target = Date.UTC(year, month - 1, day, hour, minute);\n const first = target - offsetMsAt(target, tz);\n if (offsetMsAt(first, tz) === target - first) return first;\n const second = target - offsetMsAt(first, tz);\n if (offsetMsAt(second, tz) === target - second) return second;\n return transitionBetween(Math.min(first, second), Math.max(first, second), tz);\n}\n\nconst TIME = /^(\\d{1,2}):(\\d{2})$/;\n\nfunction parseTime(value: string): { hh: number; mm: number } | null {\n const match = TIME.exec(value);\n if (!match) return null;\n const hh = Number(match[1]);\n const mm = Number(match[2]);\n if (hh > 23 || mm > 59) return null;\n return { hh, mm };\n}\n\n/**\n * The instant the next [start, end) window opens in `tz`, or null when nowMs\n * is already inside a window. Scans nine calendar days so every weekday is\n * reachable. Malformed times (the DSL validates its own input, but IR may come\n * from elsewhere) disable the window — better to send now than to hold a user\n * forever on a config typo.\n */\nexport function nextWindowStart(\n nowMs: number,\n days: readonly string[],\n between: readonly [string, string],\n tz: string\n): number | null {\n const start = parseTime(between[0]);\n const end = parseTime(between[1]);\n if (start === null || end === null) return null;\n const zone = resolveTimeZone(tz);\n\n // Step the local *calendar*, not the absolute clock: a spring-forward day is\n // 23 hours long, so nowMs + n·24h crosses two local midnights and skips that\n // date entirely — the send would be held until the weekday came round again.\n // Date.UTC arithmetic over the civil date has no DST to trip on, and\n // getUTCDay names the same weekday the zone does.\n const today = localParts(nowMs, zone);\n const firstDay = Date.UTC(today.year, today.month - 1, today.day);\n\n for (let offset = 0; offset < 9; offset++) {\n const date = new Date(firstDay + offset * DAY_MS);\n if (!days.includes(WEEKDAYS[date.getUTCDay()])) continue;\n const year = date.getUTCFullYear();\n const month = date.getUTCMonth() + 1;\n const day = date.getUTCDate();\n const opensAt = zonedInstant(year, month, day, start.hh, start.mm, zone);\n const closesAt = zonedInstant(year, month, day, end.hh, end.mm, zone);\n if (nowMs < opensAt) return opensAt;\n if (nowMs < closesAt) return null;\n // Past this day's window (e.g. 23:00 against 09:00–17:00) → keep scanning forward\n }\n return null; // no allowed day in range (an empty days list) → do not wait\n}\n"],"mappings":"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"}
|