@shware/workflow 6.1.1 → 7.0.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.
Files changed (108) hide show
  1. package/README.md +11 -1
  2. package/dist/aws/index.cjs +3 -2
  3. package/dist/aws/index.cjs.map +1 -1
  4. package/dist/aws/index.mjs +3 -2
  5. package/dist/aws/index.mjs.map +1 -1
  6. package/dist/cloudflare/worker.cjs +1 -1
  7. package/dist/cloudflare/worker.cjs.map +1 -1
  8. package/dist/cloudflare/worker.mjs +1 -1
  9. package/dist/cloudflare/worker.mjs.map +1 -1
  10. package/dist/email-sender.cjs +8 -1
  11. package/dist/email-sender.cjs.map +1 -1
  12. package/dist/email-sender.d.cts +2 -2
  13. package/dist/email-sender.d.cts.map +1 -1
  14. package/dist/email-sender.d.mts +2 -2
  15. package/dist/email-sender.d.mts.map +1 -1
  16. package/dist/email-sender.mjs +9 -2
  17. package/dist/email-sender.mjs.map +1 -1
  18. package/dist/email.cjs +5 -0
  19. package/dist/email.cjs.map +1 -1
  20. package/dist/email.d.cts +2 -0
  21. package/dist/email.d.cts.map +1 -1
  22. package/dist/email.d.mts +2 -0
  23. package/dist/email.d.mts.map +1 -1
  24. package/dist/email.mjs +5 -1
  25. package/dist/email.mjs.map +1 -1
  26. package/dist/engine/condition.cjs +4 -2
  27. package/dist/engine/condition.cjs.map +1 -1
  28. package/dist/engine/condition.d.cts.map +1 -1
  29. package/dist/engine/condition.d.mts.map +1 -1
  30. package/dist/engine/condition.mjs +4 -2
  31. package/dist/engine/condition.mjs.map +1 -1
  32. package/dist/engine/interpreter.cjs +2 -1
  33. package/dist/engine/interpreter.cjs.map +1 -1
  34. package/dist/engine/interpreter.mjs +2 -1
  35. package/dist/engine/interpreter.mjs.map +1 -1
  36. package/dist/engine/ports.d.cts +19 -3
  37. package/dist/engine/ports.d.cts.map +1 -1
  38. package/dist/engine/ports.d.mts +19 -3
  39. package/dist/engine/ports.d.mts.map +1 -1
  40. package/dist/host/http.cjs +1 -1
  41. package/dist/host/http.cjs.map +1 -1
  42. package/dist/host/http.d.cts +3 -1
  43. package/dist/host/http.d.cts.map +1 -1
  44. package/dist/host/http.d.mts +3 -1
  45. package/dist/host/http.d.mts.map +1 -1
  46. package/dist/host/http.mjs +1 -1
  47. package/dist/host/http.mjs.map +1 -1
  48. package/dist/host/ingest.cjs +108 -12
  49. package/dist/host/ingest.cjs.map +1 -1
  50. package/dist/host/ingest.d.cts +21 -2
  51. package/dist/host/ingest.d.cts.map +1 -1
  52. package/dist/host/ingest.d.mts +21 -2
  53. package/dist/host/ingest.d.mts.map +1 -1
  54. package/dist/host/ingest.mjs +107 -13
  55. package/dist/host/ingest.mjs.map +1 -1
  56. package/dist/host/journey-host.cjs +2 -1
  57. package/dist/host/journey-host.cjs.map +1 -1
  58. package/dist/host/journey-host.d.cts +5 -2
  59. package/dist/host/journey-host.d.cts.map +1 -1
  60. package/dist/host/journey-host.d.mts +5 -2
  61. package/dist/host/journey-host.d.mts.map +1 -1
  62. package/dist/host/journey-host.mjs +2 -1
  63. package/dist/host/journey-host.mjs.map +1 -1
  64. package/dist/index.cjs +1 -0
  65. package/dist/index.d.cts +2 -2
  66. package/dist/index.d.mts +2 -2
  67. package/dist/index.mjs +2 -2
  68. package/dist/reporting/observer.cjs +1 -0
  69. package/dist/reporting/observer.cjs.map +1 -1
  70. package/dist/reporting/observer.mjs +1 -0
  71. package/dist/reporting/observer.mjs.map +1 -1
  72. package/dist/store/facts.cjs +18 -2
  73. package/dist/store/facts.cjs.map +1 -1
  74. package/dist/store/facts.d.cts +1 -0
  75. package/dist/store/facts.d.cts.map +1 -1
  76. package/dist/store/facts.d.mts +1 -0
  77. package/dist/store/facts.d.mts.map +1 -1
  78. package/dist/store/facts.mjs +18 -2
  79. package/dist/store/facts.mjs.map +1 -1
  80. package/dist/store/index.cjs.map +1 -1
  81. package/dist/store/index.d.cts +70 -10
  82. package/dist/store/index.d.cts.map +1 -1
  83. package/dist/store/index.d.mts +70 -10
  84. package/dist/store/index.d.mts.map +1 -1
  85. package/dist/store/index.mjs.map +1 -1
  86. package/dist/store/postgres-reporting.cjs +4 -0
  87. package/dist/store/postgres-reporting.cjs.map +1 -1
  88. package/dist/store/postgres-reporting.d.cts +1 -0
  89. package/dist/store/postgres-reporting.d.cts.map +1 -1
  90. package/dist/store/postgres-reporting.d.mts +1 -0
  91. package/dist/store/postgres-reporting.d.mts.map +1 -1
  92. package/dist/store/postgres-reporting.mjs +4 -0
  93. package/dist/store/postgres-reporting.mjs.map +1 -1
  94. package/dist/store/postgres.cjs +65 -11
  95. package/dist/store/postgres.cjs.map +1 -1
  96. package/dist/store/postgres.d.cts +13 -4
  97. package/dist/store/postgres.d.cts.map +1 -1
  98. package/dist/store/postgres.d.mts +13 -4
  99. package/dist/store/postgres.d.mts.map +1 -1
  100. package/dist/store/postgres.mjs +65 -11
  101. package/dist/store/postgres.mjs.map +1 -1
  102. package/dist/store/tables.cjs +4 -3
  103. package/dist/store/tables.cjs.map +1 -1
  104. package/dist/store/tables.d.cts.map +1 -1
  105. package/dist/store/tables.d.mts.map +1 -1
  106. package/dist/store/tables.mjs +4 -3
  107. package/dist/store/tables.mjs.map +1 -1
  108. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -10,10 +10,10 @@ import { GoalOptions, WorkflowBuilder, WorkflowDefinition, workflow } from "./ds
10
10
  import { compileBundle } from "./dsl/bundle.cjs";
11
11
  import "./dsl/index.cjs";
12
12
  import { Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DeclaredOutput, EventPayload, ProfilePatch, SubscriptionsDefinition, Topic, checkEvent, defineContract, isContract } from "./contract.cjs";
13
- import { EmailAddress, EmailConfig, EmailRenderOptions, PREVIEW_MESSAGE_ID, ResolvedEmail, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertTemplateTopic, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, mimeAddress, preferencesLink, resolveEmail } from "./email.cjs";
13
+ import { EmailAddress, EmailConfig, EmailRenderOptions, PREVIEW_MESSAGE_ID, ResolvedEmail, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertTemplateTopic, assertUnsubscribeUrl, formatEmailAddress, isEmailAddress, isUnsubscribeLink, mimeAddress, preferencesLink, resolveEmail } from "./email.cjs";
14
14
  import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailMetadata, EmailTemplate, EmailTemplateBase, EmailTemplateDefinition, EmailTemplateTopic, PropsSchema, PushContent, PushTemplate, PushTemplateBase, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateTopic, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate, templatesFor } from "./templates.cjs";
15
15
  import { JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, resolveRuntime } from "./runtime.cjs";
16
16
  import { ApnsConfig, ConfigCompileError, DatabaseConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, isConfig } from "./config.cjs";
17
17
  import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.cjs";
18
18
  import { BundlePlan, ChangeStatus, NodeChange, ResourceChange, WorkflowChange, isSemanticChange, plan } from "./plan.cjs";
19
- export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailMetadata, EmailRenderOptions, EmailTemplate, EmailTemplateBase, EmailTemplateDefinition, EmailTemplateTopic, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, PREVIEW_MESSAGE_ID, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateBase, PushTemplateDefinition, ResolvedEmail, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, SubscriptionsDefinition, SubscriptionsIR, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateTopic, TemplateValidationError, type TimeOfDay, Topic, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertSupportedSchema, assertTemplateTopic, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, mimeAddress, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, preferencesLink, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, templatesFor, trigger, user, workflow };
19
+ export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailMetadata, EmailRenderOptions, EmailTemplate, EmailTemplateBase, EmailTemplateDefinition, EmailTemplateTopic, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, PREVIEW_MESSAGE_ID, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateBase, PushTemplateDefinition, ResolvedEmail, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, SubscriptionsDefinition, SubscriptionsIR, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateTopic, TemplateValidationError, type TimeOfDay, Topic, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertSupportedSchema, assertTemplateTopic, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailAddress, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, mimeAddress, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, preferencesLink, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, templatesFor, trigger, user, workflow };
package/dist/index.d.mts CHANGED
@@ -10,10 +10,10 @@ import { GoalOptions, WorkflowBuilder, WorkflowDefinition, workflow } from "./ds
10
10
  import { compileBundle } from "./dsl/bundle.mjs";
11
11
  import "./dsl/index.mjs";
12
12
  import { Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DeclaredOutput, EventPayload, ProfilePatch, SubscriptionsDefinition, Topic, checkEvent, defineContract, isContract } from "./contract.mjs";
13
- import { EmailAddress, EmailConfig, EmailRenderOptions, PREVIEW_MESSAGE_ID, ResolvedEmail, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertTemplateTopic, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, mimeAddress, preferencesLink, resolveEmail } from "./email.mjs";
13
+ import { EmailAddress, EmailConfig, EmailRenderOptions, PREVIEW_MESSAGE_ID, ResolvedEmail, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertTemplateTopic, assertUnsubscribeUrl, formatEmailAddress, isEmailAddress, isUnsubscribeLink, mimeAddress, preferencesLink, resolveEmail } from "./email.mjs";
14
14
  import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailMetadata, EmailTemplate, EmailTemplateBase, EmailTemplateDefinition, EmailTemplateTopic, PropsSchema, PushContent, PushTemplate, PushTemplateBase, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateTopic, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate, templatesFor } from "./templates.mjs";
15
15
  import { JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, resolveRuntime } from "./runtime.mjs";
16
16
  import { ApnsConfig, ConfigCompileError, DatabaseConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, isConfig } from "./config.mjs";
17
17
  import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.mjs";
18
18
  import { BundlePlan, ChangeStatus, NodeChange, ResourceChange, WorkflowChange, isSemanticChange, plan } from "./plan.mjs";
19
- export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailMetadata, EmailRenderOptions, EmailTemplate, EmailTemplateBase, EmailTemplateDefinition, EmailTemplateTopic, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, PREVIEW_MESSAGE_ID, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateBase, PushTemplateDefinition, ResolvedEmail, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, SubscriptionsDefinition, SubscriptionsIR, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateTopic, TemplateValidationError, type TimeOfDay, Topic, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertSupportedSchema, assertTemplateTopic, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, mimeAddress, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, preferencesLink, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, templatesFor, trigger, user, workflow };
19
+ export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailMetadata, EmailRenderOptions, EmailTemplate, EmailTemplateBase, EmailTemplateDefinition, EmailTemplateTopic, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, PREVIEW_MESSAGE_ID, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateBase, PushTemplateDefinition, ResolvedEmail, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, SubscriptionsDefinition, SubscriptionsIR, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateTopic, TemplateValidationError, type TimeOfDay, Topic, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertSupportedSchema, assertTemplateTopic, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailAddress, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, mimeAddress, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, preferencesLink, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, templatesFor, trigger, user, workflow };
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import { action } from "./dsl/action.mjs";
4
4
  import { event, user } from "./dsl/refs.mjs";
5
5
  import { template } from "./dsl/template.mjs";
6
6
  import { and, between, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment } from "./dsl/condition.mjs";
7
- import { PREVIEW_MESSAGE_ID, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertTemplateTopic, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, mimeAddress, preferencesLink, resolveEmail } from "./email.mjs";
7
+ import { PREVIEW_MESSAGE_ID, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertTemplateTopic, assertUnsubscribeUrl, formatEmailAddress, isEmailAddress, isUnsubscribeLink, mimeAddress, preferencesLink, resolveEmail } from "./email.mjs";
8
8
  import { CHANNEL_OF, TemplateValidationError, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate, templatesFor } from "./templates.mjs";
9
9
  import { flow } from "./dsl/flow.mjs";
10
10
  import { trigger } from "./dsl/trigger.mjs";
@@ -15,4 +15,4 @@ import { ConfigCompileError, apnsEnvironment, compileConfig, defineConfig, isCon
15
15
  import { ContractValidationError, checkEvent, defineContract, isContract } from "./contract.mjs";
16
16
  import { isSemanticChange, plan } from "./plan.mjs";
17
17
  import { resolveRuntime } from "./runtime.mjs";
18
- export { ActionIR, BundleIR, CHANNEL_OF, ChannelIR, ConditionIR, ConfigCompileError, ContractValidationError, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PREVIEW_MESSAGE_ID, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, SubscriptionsIR, TemplateIR, TemplateValidationError, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertSupportedSchema, assertTemplateTopic, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, mimeAddress, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, preferencesLink, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, templatesFor, trigger, user, workflow };
18
+ export { ActionIR, BundleIR, CHANNEL_OF, ChannelIR, ConditionIR, ConfigCompileError, ContractValidationError, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PREVIEW_MESSAGE_ID, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, SubscriptionsIR, TemplateIR, TemplateValidationError, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertHttpUrl, assertSupportedSchema, assertTemplateTopic, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailAddress, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, mimeAddress, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, preferencesLink, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, templatesFor, trigger, user, workflow };
@@ -18,6 +18,7 @@ function reportingObserver(reporting, origin) {
18
18
  return {
19
19
  started: (event) => reporting.instanceStarted(event),
20
20
  nodeReached: (event) => reporting.nodeReached(event),
21
+ messageSettled: (message) => reporting.messageSettled(message.idempotencyKey),
21
22
  async messagePrepared(message) {
22
23
  const id = await reporting.recordMessage({
23
24
  idempotencyKey: message.idempotencyKey,
@@ -1 +1 @@
1
- {"version":3,"file":"observer.cjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (served by the Worker, ./tracking.ts); the renderer gets the id too, for the email's\n * preferences link (`email.preferencesUrl?m=<message>`), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n topic:\n message.channel === 'email' || message.channel === 'push'\n ? (message.topic ?? null)\n : null,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n messageId: id,\n link: (url, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,OACE,QAAQ,YAAY,WAAW,QAAQ,YAAY,SAC9C,QAAQ,SAAS,OAClB;IACN,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,WAAW;KACX,OAAO,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}
1
+ {"version":3,"file":"observer.cjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (served by the Worker, ./tracking.ts); the renderer gets the id too, for the email's\n * preferences link (`email.preferencesUrl?m=<message>`), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n messageSettled: (message) => reporting.messageSettled(message.idempotencyKey),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n topic:\n message.channel === 'email' || message.channel === 'push'\n ? (message.topic ?? null)\n : null,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n messageId: id,\n link: (url, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,iBAAiB,YAAY,UAAU,eAAe,QAAQ,cAAc;EAE5E,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,OACE,QAAQ,YAAY,WAAW,QAAQ,YAAY,SAC9C,QAAQ,SAAS,OAClB;IACN,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,WAAW;KACX,OAAO,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}
@@ -17,6 +17,7 @@ function reportingObserver(reporting, origin) {
17
17
  return {
18
18
  started: (event) => reporting.instanceStarted(event),
19
19
  nodeReached: (event) => reporting.nodeReached(event),
20
+ messageSettled: (message) => reporting.messageSettled(message.idempotencyKey),
20
21
  async messagePrepared(message) {
21
22
  const id = await reporting.recordMessage({
22
23
  idempotencyKey: message.idempotencyKey,
@@ -1 +1 @@
1
- {"version":3,"file":"observer.mjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (served by the Worker, ./tracking.ts); the renderer gets the id too, for the email's\n * preferences link (`email.preferencesUrl?m=<message>`), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n topic:\n message.channel === 'email' || message.channel === 'push'\n ? (message.topic ?? null)\n : null,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n messageId: id,\n link: (url, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;;;AAgBA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,OACE,QAAQ,YAAY,WAAW,QAAQ,YAAY,SAC9C,QAAQ,SAAS,OAClB;IACN,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,WAAW;KACX,OAAO,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}
1
+ {"version":3,"file":"observer.mjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (served by the Worker, ./tracking.ts); the renderer gets the id too, for the email's\n * preferences link (`email.preferencesUrl?m=<message>`), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n messageSettled: (message) => reporting.messageSettled(message.idempotencyKey),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n topic:\n message.channel === 'email' || message.channel === 'push'\n ? (message.topic ?? null)\n : null,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n messageId: id,\n link: (url, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;;;AAgBA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,iBAAiB,YAAY,UAAU,eAAe,QAAQ,cAAc;EAE5E,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,OACE,QAAQ,YAAY,WAAW,QAAQ,YAAY,SAC9C,QAAQ,SAAS,OAClB;IACN,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,WAAW;KACX,OAAO,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}
@@ -3,6 +3,8 @@ const require_engine_condition = require("../engine/condition.cjs");
3
3
  // src/store/facts.ts
4
4
  /** The profile path that reads the user's own id (`u.userId`). */
5
5
  const USER_ID_PROPERTY = "userId";
6
+ /** Payloads per round trip when a `where` filters events in JS. */
7
+ const PAYLOAD_PAGE = 500;
6
8
  /** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */
7
9
  var JourneyFactSource = class {
8
10
  store;
@@ -16,9 +18,23 @@ var JourneyFactSource = class {
16
18
  ...opts?.sinceMs === void 0 ? {} : { sinceMs: opts.sinceMs },
17
19
  ...opts?.untilMs === void 0 ? {} : { untilMs: opts.untilMs }
18
20
  };
19
- if (opts?.where === void 0) return this.store.countEvents(this.userId, event, window);
21
+ const atLeast = opts?.atLeast;
22
+ if (opts?.where === void 0) return this.store.countEvents(this.userId, event, window, atLeast);
20
23
  const where = opts.where;
21
- return (await this.store.listEventPayloads(this.userId, event, window)).filter((payload) => require_engine_condition.matchesWhere(payload, where)).length;
24
+ let matched = 0;
25
+ let after;
26
+ for (;;) {
27
+ const page = await this.store.pageEventPayloads(this.userId, event, window, {
28
+ limit: PAYLOAD_PAGE,
29
+ after
30
+ });
31
+ for (const payload of page.payloads) {
32
+ if (require_engine_condition.matchesWhere(payload, where)) matched += 1;
33
+ if (atLeast !== void 0 && matched >= atLeast) return matched;
34
+ }
35
+ if (page.next === void 0) return matched;
36
+ after = page.next;
37
+ }
22
38
  }
23
39
  /**
24
40
  * The scalar boundary. Everything downstream — comparisons, subject
@@ -1 +1 @@
1
- {"version":3,"file":"facts.cjs","names":["matchesWhere"],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n if (opts?.where === undefined) return this.store.countEvents(this.userId, event, window);\n // Payload filtering happens in JS via the shared evaluator — one set of\n // where semantics with the in-memory FactSource; per-user row counts are small.\n const where = opts.where;\n const payloads = await this.store.listEventPayloads(this.userId, event, window);\n return payloads.filter((payload) => matchesWhere(payload, where)).length;\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /identify validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n if (path === USER_ID_PROPERTY) return this.userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(this.userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /identify\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's opt-outs: the channel's, or with a topic that topic's as well. */\n async isSuppressed(channel: string, topic?: string): Promise<boolean> {\n return this.store.isSuppressed(this.userId, channel, topic);\n }\n\n listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(this.userId);\n }\n}\n"],"mappings":";;;;AAMA,MAAa,mBAAmB;;AAGhC,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAFnB,YACE,OACA,QACA;EAFiB,KAAA,QAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,KAAK,MAAM,YAAY,KAAK,QAAQ,OAAO,MAAM;EAGvF,MAAM,QAAQ,KAAK;EAEnB,QAAO,MADgB,KAAK,MAAM,kBAAkB,KAAK,QAAQ,OAAO,MAAM,EAAA,CAC9D,QAAQ,YAAYA,yBAAAA,aAAa,SAAS,KAAK,CAAC,CAAC,CAAC;CACpE;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,IAAI,SAAA,UAA2B,OAAO,KAAK;EAC3C,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM;EAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAiB,OAAkC;EACpE,OAAO,KAAK,MAAM,aAAa,KAAK,QAAQ,SAAS,KAAK;CAC5D;CAEA,cAAqC;EACnC,OAAO,KAAK,MAAM,YAAY,KAAK,MAAM;CAC3C;AACF"}
1
+ {"version":3,"file":"facts.cjs","names":["matchesWhere"],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { EventCursor, JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Payloads per round trip when a `where` filters events in JS. */\nconst PAYLOAD_PAGE = 500;\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR; atLeast?: number }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n const atLeast = opts?.atLeast;\n if (opts?.where === undefined) {\n return this.store.countEvents(this.userId, event, window, atLeast);\n }\n // Payload filtering stays in JS through the shared evaluator — one set of where semantics\n // with the in-memory FactSource — but a page at a time, stopping as soon as enough events\n // match. A heavy user's history is no longer loaded whole on every evaluation; only a user\n // with no match left to find is read to the end, and then in bounded pages.\n const where = opts.where;\n let matched = 0;\n let after: EventCursor | undefined;\n for (;;) {\n const page = await this.store.pageEventPayloads(this.userId, event, window, {\n limit: PAYLOAD_PAGE,\n after,\n });\n for (const payload of page.payloads) {\n if (matchesWhere(payload, where)) matched += 1;\n if (atLeast !== undefined && matched >= atLeast) return matched;\n }\n if (page.next === undefined) return matched;\n after = page.next;\n }\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /identify validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n if (path === USER_ID_PROPERTY) return this.userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(this.userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /identify\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's opt-outs: the channel's, or with a topic that topic's as well. */\n async isSuppressed(channel: string, topic?: string): Promise<boolean> {\n return this.store.isSuppressed(this.userId, channel, topic);\n }\n\n listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(this.userId);\n }\n}\n"],"mappings":";;;;AAMA,MAAa,mBAAmB;;AAGhC,MAAM,eAAe;;AAGrB,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAFnB,YACE,OACA,QACA;EAFiB,KAAA,QAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,MAAM,UAAU,MAAM;EACtB,IAAI,MAAM,UAAU,KAAA,GAClB,OAAO,KAAK,MAAM,YAAY,KAAK,QAAQ,OAAO,QAAQ,OAAO;EAMnE,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU;EACd,IAAI;EACJ,SAAS;GACP,MAAM,OAAO,MAAM,KAAK,MAAM,kBAAkB,KAAK,QAAQ,OAAO,QAAQ;IAC1E,OAAO;IACP;GACF,CAAC;GACD,KAAK,MAAM,WAAW,KAAK,UAAU;IACnC,IAAIA,yBAAAA,aAAa,SAAS,KAAK,GAAG,WAAW;IAC7C,IAAI,YAAY,KAAA,KAAa,WAAW,SAAS,OAAO;GAC1D;GACA,IAAI,KAAK,SAAS,KAAA,GAAW,OAAO;GACpC,QAAQ,KAAK;EACf;CACF;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,IAAI,SAAA,UAA2B,OAAO,KAAK;EAC3C,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM;EAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAiB,OAAkC;EACpE,OAAO,KAAK,MAAM,aAAa,KAAK,QAAQ,SAAS,KAAK;CAC5D;CAEA,cAAqC;EACnC,OAAO,KAAK,MAAM,YAAY,KAAK,MAAM;CAC3C;AACF"}
@@ -12,6 +12,7 @@ export declare class JourneyFactSource implements FactSource {
12
12
  sinceMs?: number;
13
13
  untilMs?: number;
14
14
  where?: ConditionIR;
15
+ atLeast?: number;
15
16
  }): Promise<number>;
16
17
  /**
17
18
  * The scalar boundary. Everything downstream — comparisons, subject
@@ -1 +1 @@
1
- {"version":3,"file":"facts.d.cts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;qBAMa;;qBAGA,6BAA6B;mBAErB;mBACA;EAFnB,YACmB,OAAO,cACP;EAGb,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;;;;;;;;;;;;;;;EA2BG,YAAY,eAAe,QAAQ;EAoBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,iBAAiB,iBAAiB;EAIrD,eAAe,QAAQ"}
1
+ {"version":3,"file":"facts.d.cts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;qBAMa;;qBAMA,6BAA6B;mBAErB;mBACA;EAFnB,YACmB,OAAO,cACP;EAGb,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;IAAa;MACjE;;;;;;;;;;;;;;;EA4CG,YAAY,eAAe,QAAQ;EAoBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,iBAAiB,iBAAiB;EAIrD,eAAe,QAAQ"}
@@ -12,6 +12,7 @@ export declare class JourneyFactSource implements FactSource {
12
12
  sinceMs?: number;
13
13
  untilMs?: number;
14
14
  where?: ConditionIR;
15
+ atLeast?: number;
15
16
  }): Promise<number>;
16
17
  /**
17
18
  * The scalar boundary. Everything downstream — comparisons, subject
@@ -1 +1 @@
1
- {"version":3,"file":"facts.d.mts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;qBAMa;;qBAGA,6BAA6B;mBAErB;mBACA;EAFnB,YACmB,OAAO,cACP;EAGb,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;;;;;;;;;;;;;;;EA2BG,YAAY,eAAe,QAAQ;EAoBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,iBAAiB,iBAAiB;EAIrD,eAAe,QAAQ"}
1
+ {"version":3,"file":"facts.d.mts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;qBAMa;;qBAMA,6BAA6B;mBAErB;mBACA;EAFnB,YACmB,OAAO,cACP;EAGb,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;IAAa;MACjE;;;;;;;;;;;;;;;EA4CG,YAAY,eAAe,QAAQ;EAoBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,iBAAiB,iBAAiB;EAIrD,eAAe,QAAQ"}
@@ -2,6 +2,8 @@ import { matchesWhere } from "../engine/condition.mjs";
2
2
  // src/store/facts.ts
3
3
  /** The profile path that reads the user's own id (`u.userId`). */
4
4
  const USER_ID_PROPERTY = "userId";
5
+ /** Payloads per round trip when a `where` filters events in JS. */
6
+ const PAYLOAD_PAGE = 500;
5
7
  /** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */
6
8
  var JourneyFactSource = class {
7
9
  store;
@@ -15,9 +17,23 @@ var JourneyFactSource = class {
15
17
  ...opts?.sinceMs === void 0 ? {} : { sinceMs: opts.sinceMs },
16
18
  ...opts?.untilMs === void 0 ? {} : { untilMs: opts.untilMs }
17
19
  };
18
- if (opts?.where === void 0) return this.store.countEvents(this.userId, event, window);
20
+ const atLeast = opts?.atLeast;
21
+ if (opts?.where === void 0) return this.store.countEvents(this.userId, event, window, atLeast);
19
22
  const where = opts.where;
20
- return (await this.store.listEventPayloads(this.userId, event, window)).filter((payload) => matchesWhere(payload, where)).length;
23
+ let matched = 0;
24
+ let after;
25
+ for (;;) {
26
+ const page = await this.store.pageEventPayloads(this.userId, event, window, {
27
+ limit: PAYLOAD_PAGE,
28
+ after
29
+ });
30
+ for (const payload of page.payloads) {
31
+ if (matchesWhere(payload, where)) matched += 1;
32
+ if (atLeast !== void 0 && matched >= atLeast) return matched;
33
+ }
34
+ if (page.next === void 0) return matched;
35
+ after = page.next;
36
+ }
21
37
  }
22
38
  /**
23
39
  * The scalar boundary. Everything downstream — comparisons, subject
@@ -1 +1 @@
1
- {"version":3,"file":"facts.mjs","names":[],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n if (opts?.where === undefined) return this.store.countEvents(this.userId, event, window);\n // Payload filtering happens in JS via the shared evaluator — one set of\n // where semantics with the in-memory FactSource; per-user row counts are small.\n const where = opts.where;\n const payloads = await this.store.listEventPayloads(this.userId, event, window);\n return payloads.filter((payload) => matchesWhere(payload, where)).length;\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /identify validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n if (path === USER_ID_PROPERTY) return this.userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(this.userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /identify\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's opt-outs: the channel's, or with a topic that topic's as well. */\n async isSuppressed(channel: string, topic?: string): Promise<boolean> {\n return this.store.isSuppressed(this.userId, channel, topic);\n }\n\n listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(this.userId);\n }\n}\n"],"mappings":";;;AAMA,MAAa,mBAAmB;;AAGhC,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAFnB,YACE,OACA,QACA;EAFiB,KAAA,QAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,KAAK,MAAM,YAAY,KAAK,QAAQ,OAAO,MAAM;EAGvF,MAAM,QAAQ,KAAK;EAEnB,QAAO,MADgB,KAAK,MAAM,kBAAkB,KAAK,QAAQ,OAAO,MAAM,EAAA,CAC9D,QAAQ,YAAY,aAAa,SAAS,KAAK,CAAC,CAAC,CAAC;CACpE;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,IAAI,SAAA,UAA2B,OAAO,KAAK;EAC3C,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM;EAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAiB,OAAkC;EACpE,OAAO,KAAK,MAAM,aAAa,KAAK,QAAQ,SAAS,KAAK;CAC5D;CAEA,cAAqC;EACnC,OAAO,KAAK,MAAM,YAAY,KAAK,MAAM;CAC3C;AACF"}
1
+ {"version":3,"file":"facts.mjs","names":[],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { EventCursor, JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Payloads per round trip when a `where` filters events in JS. */\nconst PAYLOAD_PAGE = 500;\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR; atLeast?: number }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n const atLeast = opts?.atLeast;\n if (opts?.where === undefined) {\n return this.store.countEvents(this.userId, event, window, atLeast);\n }\n // Payload filtering stays in JS through the shared evaluator — one set of where semantics\n // with the in-memory FactSource — but a page at a time, stopping as soon as enough events\n // match. A heavy user's history is no longer loaded whole on every evaluation; only a user\n // with no match left to find is read to the end, and then in bounded pages.\n const where = opts.where;\n let matched = 0;\n let after: EventCursor | undefined;\n for (;;) {\n const page = await this.store.pageEventPayloads(this.userId, event, window, {\n limit: PAYLOAD_PAGE,\n after,\n });\n for (const payload of page.payloads) {\n if (matchesWhere(payload, where)) matched += 1;\n if (atLeast !== undefined && matched >= atLeast) return matched;\n }\n if (page.next === undefined) return matched;\n after = page.next;\n }\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /identify validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n if (path === USER_ID_PROPERTY) return this.userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(this.userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /identify\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's opt-outs: the channel's, or with a topic that topic's as well. */\n async isSuppressed(channel: string, topic?: string): Promise<boolean> {\n return this.store.isSuppressed(this.userId, channel, topic);\n }\n\n listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(this.userId);\n }\n}\n"],"mappings":";;;AAMA,MAAa,mBAAmB;;AAGhC,MAAM,eAAe;;AAGrB,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAFnB,YACE,OACA,QACA;EAFiB,KAAA,QAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,MAAM,UAAU,MAAM;EACtB,IAAI,MAAM,UAAU,KAAA,GAClB,OAAO,KAAK,MAAM,YAAY,KAAK,QAAQ,OAAO,QAAQ,OAAO;EAMnE,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU;EACd,IAAI;EACJ,SAAS;GACP,MAAM,OAAO,MAAM,KAAK,MAAM,kBAAkB,KAAK,QAAQ,OAAO,QAAQ;IAC1E,OAAO;IACP;GACF,CAAC;GACD,KAAK,MAAM,WAAW,KAAK,UAAU;IACnC,IAAI,aAAa,SAAS,KAAK,GAAG,WAAW;IAC7C,IAAI,YAAY,KAAA,KAAa,WAAW,SAAS,OAAO;GAC1D;GACA,IAAI,KAAK,SAAS,KAAA,GAAW,OAAO;GACpC,QAAQ,KAAK;EACf;CACF;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,IAAI,SAAA,UAA2B,OAAO,KAAK;EAC3C,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM;EAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAiB,OAAkC;EACpE,OAAO,KAAK,MAAM,aAAa,KAAK,QAAQ,SAAS,KAAK;CAC5D;CAEA,cAAqC;EACnC,OAAO,KAAK,MAAM,YAAY,KAAK,MAAM;CAC3C;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/store/index.ts"],"sourcesContent":["import type { PushDevice } from '../engine/ports';\nimport type { BundleIR, ConditionIR, ScalarIR, WorkflowIR } from '../ir';\n\n/**\n * The journey engine's data plane. Postgres is the store (`PostgresJourneyStore`); this\n * interface is the port the host, the runner and the fact source use to persist or look\n * anything up, and what a test stub implements in its place. Execution (durable steps,\n * sleeps, wake-ups) is the workflow runtime's job and stays out of here.\n *\n * Timestamps are epoch milliseconds throughout; a store may persist them in\n * its native type as long as `sinceMs` comparisons behave.\n */\n\n/**\n * A profile patch or a stored profile. On the way in, `null` means \"remove\n * this property\" (RFC 7386 merge-patch, the only way the API has to unset\n * one); a stored profile never contains null.\n */\nexport type ProfileProps = Record<string, ScalarIR | null | undefined>;\n\nexport interface EventInput {\n /** The occurrence's identity, chosen by the sender; a second insert with the same id is a no-op. */\n id: string;\n userId: string;\n event: string;\n ts: number;\n payload: Record<string, unknown>;\n}\n\nexport interface TriggerRoute {\n workflow: string;\n hash: string;\n /** Payload gate (payload-only condition tree), or null when the trigger has none. */\n where: ConditionIR | null;\n /** Profile gate, or null. */\n filter: ConditionIR | null;\n /**\n * The code version that applied these routes (`replaceRoutesIfNewer`); absent after a forced\n * deploy. Read in the same row as the hash, so the pair is always one deploy's.\n */\n version?: string | undefined;\n}\n\nexport interface SegmentTriggerRoute {\n workflow: string;\n hash: string;\n segment: string;\n /** As `TriggerRoute.version`. */\n version?: string | undefined;\n}\n\n/**\n * A Worker version, as Cloudflare's version metadata binding names it: the bundle compiled into\n * that version goes live only over an older one (`replaceRoutesIfNewer`).\n */\nexport interface BundleVersion {\n id: string;\n /** When the version was created (epoch ms): later versions are newer. */\n createdAt: number;\n}\n\nexport interface EntryInput {\n workflow: string;\n userId: string;\n instanceId: string;\n hash: string;\n ts: number;\n /**\n * The instance's start, recorded in the same transaction as the entry — for a runtime whose\n * start is an asynchronous submit (AWS) that may have to be retried.\n */\n execution?: ExecutionStart | undefined;\n}\n\n/** How an instance is started on a runtime that submits it: replayed as is on every retry. */\nexport interface ExecutionStart {\n /** The runtime's idempotency key (the durable execution name). */\n name: string;\n /** What is invoked: a qualified function ARN, its version included. */\n target: string;\n /** The exact payload: a retried submit must send the same bytes, or the runtime refuses it. */\n input: string;\n}\n\nexport interface ExecutionRecord extends ExecutionStart {\n instanceId: string;\n workflow: string;\n userId: string;\n createdAt: number;\n submittedAt?: number | undefined;\n arn?: string | undefined;\n unresolvedAt?: number | undefined;\n callbackId?: string | undefined;\n /** Bumped by every wait the instance arms: a wake or a cleanup of an older one leaves it alone. */\n generation: number;\n deadlineAt?: number | undefined;\n checkedAt?: number | undefined;\n stoppedAt?: number | undefined;\n}\n\n/**\n * What a runtime that submits its starts and parks waits on one-shot callbacks (AWS) keeps\n * beside the entry ledger. The runtime holds the execution history; this holds only the\n * mapping from the engine's instance id to it.\n */\nexport interface ExecutionRegistry {\n get(instanceId: string): Promise<ExecutionRecord | undefined>;\n /**\n * Starts not yet confirmed and not given up on, oldest first: of one (workflow, user) entry\n * still running, or every one created before `createdBefore`.\n */\n pendingStarts(filter: {\n workflow?: string;\n userId?: string;\n createdBefore?: number;\n limit: number;\n }): Promise<ExecutionRecord[]>;\n markSubmitted(instanceId: string, arn: string | undefined, at: number): Promise<void>;\n markUnresolved(instanceId: string, at: number): Promise<void>;\n setArn(instanceId: string, arn: string): Promise<void>;\n /** Record the callback a wait parks on; false when a newer generation is already armed. */\n armCallback(\n instanceId: string,\n callback: { id: string; generation: number; deadlineAt?: number | undefined }\n ): Promise<boolean>;\n /** Forget the callback — only if it is still that generation's. */\n clearCallback(instanceId: string, generation: number): Promise<void>;\n /** Submitted executions whose entry is still running, least recently checked first. */\n toReconcile(limit: number, checkedBefore: number): Promise<ExecutionRecord[]>;\n markChecked(instanceId: string, at: number): Promise<void>;\n /** Record the first stop; a later one keeps it. */\n markStopped(instanceId: string, at: number): Promise<void>;\n}\n\n/**\n * How an entry attempt ended: a fresh row, a 'failed' row of a dead instance\n * taken over (the once policy does not bar a user over one outage), or\n * refused because the pair already entered and is still alive.\n */\nexport type EntryOutcome = 'entered' | 'reclaimed' | null;\n\n/** A device registered for pushes: its provider token belongs to the user who registered it last. */\nexport interface DeviceInput {\n userId: string;\n token: string;\n /** `ios` (APNs) or `android` (FCM): which provider the token belongs to. */\n platform: string;\n ts: number;\n}\n\n/** An instance of a user still in the ledger, and whether its runner was admitted (see `admitInstance`). */\nexport interface RunningInstance {\n instanceId: string;\n /** False until the runner admitted itself: nothing has run, and the tombstone keeps it that way. */\n admitted: boolean;\n}\n\n/** A store plus how to release what opening it acquired (a connection, say). */\nexport interface StoreLease {\n store: JourneyStore;\n close?: () => Promise<void>;\n}\n\nimport type { ErasureState } from './erasure';\n\nexport { type ErasureState, UserErasedError } from './erasure';\n\n/**\n * The live database lacks tables or columns the engine reads (an SDK upgrade added them, or the\n * host never ran the migration). Raised before a deploy writes anything.\n */\nexport class SchemaMismatchError extends Error {\n override readonly name = 'SchemaMismatchError';\n\n constructor(readonly problems: readonly string[]) {\n super(\n `the database does not match @shware/workflow's tables — regenerate the drizzle schema (workflow-ui generate) and run your migrations:\\n ${problems.join('\\n ')}`\n );\n }\n}\n\n/** A message copy as reporting records it, before it goes out. */\nexport interface MessageRecord {\n idempotencyKey: string;\n workflow: string;\n instanceId: string;\n nodeId: string;\n template: string;\n /** Where the copy lands: `email`, `push_ios`, `push_android`. */\n channel: string;\n userId: string;\n /** An email's subscription topic; null for a transactional email and for every other channel. */\n topic: string | null;\n sentAt: number;\n}\n\n/** What is known of a copy without its engagement: whose it is, where it went, its topic. */\nexport interface MessageSummary {\n userId: string;\n channel: string;\n topic: string | null;\n}\n\n/** One opt-out to set or clear: a topic of the channel, or the whole channel (`topic: null`). */\nexport interface SuppressionChange {\n topic: string | null;\n suppressed: boolean;\n}\n\n/** What a click is judged by. */\nexport interface ClickContext {\n url: string;\n deliveredAt: number;\n linkCount: number;\n /** The copy's clicks in the window before this one. */\n recent: { position: number; at: number }[];\n}\n\n/** An open or a click, with the verdict on who made it and where it came from. */\nexport interface Engagement {\n at: number;\n machine: boolean;\n /** The rule that called it a machine; null for a person. */\n reason: string | null;\n userAgent: string | null;\n /** The network the request came from (Cloudflare's `cf.asn`, `cf.asOrganization`, `cf.country`), never the address. */\n asn: number | null;\n organization: string | null;\n country: string | null;\n}\n\n/**\n * Journey reporting: where each instance stands, every copy and how it was engaged with, segment\n * sizes. The store exposes it as `reporting`; the host records it and serves email tracking.\n * Every write is idempotent — a retried step reports again.\n */\n/** Bounds on when an event happened, both inclusive (epoch ms); an absent side is open. */\nexport interface EventWindow {\n sinceMs?: number;\n untilMs?: number;\n}\n\nexport interface JourneyReporting {\n /** The instance entered: its row exists from here, so one a guard ends at once still counts. */\n instanceStarted(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n at: number;\n }): Promise<void>;\n nodeReached(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n nodeId: string;\n at: number;\n }): Promise<void>;\n instanceFinished(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n status: string;\n at: number;\n convertedAt?: number;\n }): Promise<void>;\n /** The copy's row id; the row a retry already wrote, when there is one. */\n recordMessage(message: MessageRecord): Promise<string>;\n settleMessage(\n idempotencyKey: string,\n result: { delivered: true } | { delivered: false; reason: string },\n at: number\n ): Promise<void>;\n /** An email's tracked links, in document order. */\n recordLinks(messageId: string, urls: readonly string[]): Promise<void>;\n /**\n * Record an open of the copy, judged by `judge` against the copy (its recipient and when it was\n * delivered — when it was sent, before the transport answered); `undefined` from the judge\n * records nothing. Read and written in one transaction. False for an unknown copy.\n */\n recordOpen(\n messageId: string,\n judge: (message: { userId: string; deliveredAt: number }) => Engagement | undefined\n ): Promise<boolean>;\n /**\n * Record a click on the copy's link at `position`, judged by `judge` against the link, how\n * many links the copy has and its clicks of the last `windowMs`. The copy is locked for the\n * transaction, so clicks arriving together (a scanner following every link at once) are\n * judged one after another, each seeing the ones before. When the judge asks to `backdate`,\n * the window's earlier clicks are reclassified with this one's verdict. A click implies an\n * open. Undefined for an unknown link.\n */\n recordClick(\n messageId: string,\n position: number,\n at: number,\n windowMs: number,\n judge: (link: ClickContext) => { engagement: Engagement; backdate: boolean }\n ): Promise<{ url: string; engagement: Engagement } | undefined>;\n /**\n * The email copy's recipient opted out: of the copy's topic, or with `scope: 'all'` of every\n * email. Recorded on the copy and as a suppression (the topic's row, or the channel-wide\n * null-topic row), in one transaction, which also yields who the recipient is and the topic —\n * so the caller acts on the unsubscribe without a second read. A transactional copy\n * (`topic: null`) has nothing to opt out of: nothing is recorded, and the answer says so.\n * Undefined for an unknown copy.\n */\n recordUnsubscribe(\n messageId: string,\n at: number,\n scope?: 'topic' | 'all'\n ): Promise<{ userId: string; topic: string | null } | undefined>;\n /** The copy's owner, channel and topic; undefined for an unknown copy. */\n lookupMessage(messageId: string): Promise<MessageSummary | undefined>;\n /** A push copy the device reported it received. False for an unknown copy. */\n recordDeviceDelivered(messageId: string, at: number): Promise<boolean>;\n /** Drop members of segments no longer deployed, then record every deployed segment's size. */\n snapshotSegments(at: number): Promise<{ segment: string; size: number }[]>;\n}\n\nexport interface JourneyStore {\n /** Where instances stand, every copy and its engagement, segment sizes. */\n readonly reporting: JourneyReporting;\n\n /**\n * Record that the user's data must go (a deleted account): the tombstone every write about\n * the user is conditional on, from here on and for good. Committed before anything is\n * stopped or erased, so an entry that got in before it is found by the erasure and none gets\n * in after it. Idempotent: a second request, or one for a user already erased, changes nothing.\n */\n requestErasure(userId: string, at: number): Promise<void>;\n /** Users whose erasure was requested and not carried out yet (`'erasing'`), oldest first. */\n pendingErasures(): Promise<string[]>;\n /**\n * Where the user stands: `'none'` (no tombstone), `'erasing'` (requested, their instances\n * being stopped, data still there) or `'erased'` (`forgetUser` went through). Both\n * tombstone states refuse every write about the user; the entry points read this for a fast\n * answer, and the store's conditional writes are the gate itself.\n */\n erasureState(userId: string): Promise<ErasureState>;\n /** The user's journey instances still running, by the entry ledger. */\n runningInstances(userId: string): Promise<RunningInstance[]>;\n /**\n * The runner's admission, before its first observable effect: records that the instance runs\n * (`admitted_at`, kept on replay) — unless the user has an erasure tombstone, in which case\n * nothing is recorded and the runner must not run. Mutually exclusive with `requestErasure`\n * and `forgetUser` for that user (Postgres: the user's advisory lock), so an erasure either\n * finds the instance admitted and stops it, or commits its tombstone first and the admission\n * is refused — never neither.\n *\n * A first admission needs the entry running. An instance already admitted is admitted again\n * whatever the entry's status — the legitimate replay of an instance whose finalisation was\n * cut short after the entry reached its terminal status, which resumes on its own checkpoint\n * and finishes what is left. Never a new attempt's way in: false for a terminal entry never\n * admitted, for an entry no longer in the ledger, for another user's entry, and for an\n * instance whose entry was reclaimed since (`enterJourney` resets the admission with the\n * instance id, so a superseded attempt cannot pass under the id it lost).\n */\n admitInstance(instanceId: string, userId: string): Promise<boolean>;\n /**\n * Erase everything kept about the user — profile, devices, events, wake subscriptions, segment\n * memberships, entries and reporting — and mark the tombstone erased (`erased_at`), which\n * stays: a write that was in flight when the data went cannot bring the user back, and the\n * same id is never reactivated by ingest — which is also why the suppressions go with the\n * rest: nothing is ever sent to the id again, so no opt-out needs keeping. Run only once no\n * instance of theirs runs any more, or one could write some of it back. Idempotent. Mutually\n * exclusive with every write about the user (the user's advisory lock), so the data is gone\n * when this returns — nothing of theirs lands afterwards.\n */\n forgetUser(userId: string, at: number): Promise<void>;\n\n /**\n * The user opted out of the channel (recorded by an unsubscribe, or set through\n * `/subscriptions`): with a `topic`, an opt-out of that topic or of the whole channel; without\n * one, only the whole channel.\n */\n isSuppressed(userId: string, channel: string, topic?: string): Promise<boolean>;\n /** Every opt-out of the user on the channel: the topics, and `null` for the whole channel. */\n listSuppressions(userId: string, channel: string): Promise<(string | null)[]>;\n /**\n * Set or clear opt-outs, all in one transaction: a suppressed change inserts the row (keeping\n * the time of one already there), an unsuppressed one deletes it. Written whatever the user's\n * erasure state — the caller refuses an erased user first — since an opt-out is never refused.\n */\n updateSuppressions(\n userId: string,\n channel: string,\n changes: readonly SuppressionChange[],\n at: number\n ): Promise<void>;\n\n /**\n * What the live database lacks against the engine's tables (read-only); a deploy refuses to\n * run while it lists anything.\n */\n checkSchema(): Promise<string[]>;\n\n /* ---------------------------------- events --------------------------------- */\n /**\n * Append the occurrence; false when its id was already in the log (nothing written). Throws\n * `UserErasedError` for a user with a tombstone, like every write about a user below.\n */\n insertEvent(input: EventInput): Promise<boolean>;\n /** Occurrences of `event` for the user, optionally only those within the window. */\n countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;\n /** The payloads of those occurrences — the caller filters them with the shared evaluator. */\n listEventPayloads(\n userId: string,\n event: string,\n window?: EventWindow\n ): Promise<Record<string, unknown>[]>;\n\n /* --------------------------------- profiles -------------------------------- */\n getProfile(userId: string): Promise<ProfileProps | undefined>;\n /**\n * Merge-patch `props` into the stored profile in one statement (a null\n * removes the property) and return the result. One statement, because a\n * read-modify-write loses one of two concurrent identifies.\n */\n mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;\n\n /**\n * Every user the engine knows (a profile or an event): who the segment sweep evaluates. Ids\n * come in ascending order; `after` resumes past the last id of the previous page, and `limit`\n * bounds the page (the whole list without one).\n */\n listUserIds(page?: { after?: string | undefined; limit?: number | undefined }): Promise<string[]>;\n\n /* ---------------------------------- devices --------------------------------- */\n /** The user's push devices, most recently registered first. */\n listDevices(userId: string): Promise<PushDevice[]>;\n /**\n * Register a device: one row per token, so a token registered again moves to the user who\n * registered it last (a phone signed into another account) and to the front of their list.\n */\n upsertDevice(device: DeviceInput): Promise<void>;\n /** Forget a token (reported dead by APNs / FCM); the user it belonged to, if any. */\n removeDevice(token: string): Promise<string | undefined>;\n /**\n * The user signed out on the device: the token is kept, bound to nobody, until the next sign-in\n * binds it again. The user it belonged to, if any.\n */\n unbindDevice(token: string): Promise<string | undefined>;\n\n /* ---------------------------- deployed definitions -------------------------- */\n getSegmentCondition(id: string): Promise<ConditionIR | undefined>;\n /** Every deployed segment with its definition: what the segment sweep evaluates. */\n listSegments(): Promise<{ id: string; condition: ConditionIR }[]>;\n findTriggers(event: string): Promise<TriggerRoute[]>;\n listSegmentTriggers(): Promise<SegmentTriggerRoute[]>;\n putWorkflowIR(workflow: WorkflowIR): Promise<void>;\n getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined>;\n /**\n * Swap the routing tables (segments, event triggers, segment triggers) for\n * the bundle's — atomically, so a concurrent ingest never sees them half\n * empty. Workflow bodies are stored separately (`putWorkflowIR`) because they\n * are content-addressed and outlive the routes that point at them.\n */\n replaceRoutes(bundle: BundleIR): Promise<void>;\n /**\n * The Worker version whose bundle the routes came from, when one put them there through\n * `replaceRoutesIfNewer` (a forced deploy leaves it as it was). Read from the database itself,\n * not a cache in front of it.\n */\n liveBundleVersion(): Promise<BundleVersion | undefined>;\n /**\n * `replaceRoutes`, only when `version` is newer than the live one, recorded in the same\n * transaction: two versions serving side by side during a rollout cannot leave the older one's\n * routes in force. False, with nothing written, when a version as new or newer is live.\n */\n replaceRoutesIfNewer(bundle: BundleIR, version: BundleVersion): Promise<boolean>;\n\n /* ----------------------------- segment membership --------------------------- */\n isSegmentMember(segment: string, userId: string): Promise<boolean>;\n /** Record the membership, with the workflows its entry has to start (see segment_start). */\n addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void>;\n /** The workflows the member's entry has not started yet. */\n pendingSegmentStarts(segment: string, userId: string): Promise<string[]>;\n completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void>;\n /** Drop the membership and any start it still owed. */\n removeSegmentMember(segment: string, userId: string): Promise<void>;\n\n /* ------------------------------- entry ledger ------------------------------- */\n /**\n * Record the entry (the once policy). Atomic under concurrent ingests: of\n * two racing attempts exactly one is 'entered' (or 'reclaimed', when the\n * pair's previous instance is recorded as 'failed') and the other null.\n */\n enterJourney(entry: EntryInput): Promise<EntryOutcome>;\n removeEntry(instanceId: string): Promise<void>;\n setEntryStatus(instanceId: string, status: string): Promise<void>;\n /** Execution starts and wake callbacks, for a runtime that submits its starts (AWS). */\n readonly executions: ExecutionRegistry;\n\n /* ------------------------------ wake subscriptions --------------------------- */\n /** Distinct wake handles registered for (user, event). */\n findWakeHandles(userId: string, event: string): Promise<string[]>;\n /** Register the handle for each event; repeated registrations are idempotent. */\n subscribe(handle: string, userId: string, events: readonly string[], ts: number): Promise<void>;\n /** Drop every registration the handle holds. */\n unsubscribe(handle: string): Promise<void>;\n}\n\nexport { JourneyFactSource } from './facts';\nexport { PostgresJourneyStore } from './postgres';\nexport { PostgresReporting } from './postgres-reporting';\nexport {\n DEFAULT_SCHEMA,\n TABLES,\n renderDrizzleSchema,\n renderPostgresDdl,\n type TableSpec,\n type UniqueConstraintSpec,\n} from './tables';\nexport type { PostgresJourneyStoreOptions, SqlClientLike } from './postgres';\n"],"mappings":";;;;;;;;;;;AA2KA,IAAa,sBAAb,cAAyC,MAAM;CAGxB;CAFrB,OAAyB;CAEzB,YAAY,UAAsC;EAChD,MACE,4IAA4I,SAAS,KAAK,MAAM,GAClK;EAHmB,KAAA,WAAA;CAIrB;AACF"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/store/index.ts"],"sourcesContent":["import type { PushDevice } from '../engine/ports';\nimport type { BundleIR, ConditionIR, ScalarIR, WorkflowIR } from '../ir';\n\n/**\n * The journey engine's data plane. Postgres is the store (`PostgresJourneyStore`); this\n * interface is the port the host, the runner and the fact source use to persist or look\n * anything up, and what a test stub implements in its place. Execution (durable steps,\n * sleeps, wake-ups) is the workflow runtime's job and stays out of here.\n *\n * Timestamps are epoch milliseconds throughout; a store may persist them in\n * its native type as long as `sinceMs` comparisons behave.\n */\n\n/**\n * A profile patch or a stored profile. On the way in, `null` means \"remove\n * this property\" (RFC 7386 merge-patch, the only way the API has to unset\n * one); a stored profile never contains null.\n */\nexport type ProfileProps = Record<string, ScalarIR | null | undefined>;\n\nexport interface EventInput {\n /** The occurrence's identity, chosen by the sender; a second insert with the same id is a no-op. */\n id: string;\n userId: string;\n event: string;\n ts: number;\n payload: Record<string, unknown>;\n}\n\nexport interface TriggerRoute {\n workflow: string;\n hash: string;\n /** Payload gate (payload-only condition tree), or null when the trigger has none. */\n where: ConditionIR | null;\n /** Profile gate, or null. */\n filter: ConditionIR | null;\n /**\n * The code version that applied these routes; absent after a hand-posted `POST /deploy`. Read\n * in the same row as the hash, so the pair is always one deploy's.\n */\n version?: string | undefined;\n}\n\nexport interface SegmentTriggerRoute {\n workflow: string;\n hash: string;\n segment: string;\n /** As `TriggerRoute.version`. */\n version?: string | undefined;\n}\n\n/**\n * A Worker version, as Cloudflare's version metadata binding names it: the bundle compiled into\n * that version goes live only over an older one (`replaceRoutesIfNewer`).\n */\nexport interface BundleVersion {\n id: string;\n /** When the version was created (epoch ms): later versions are newer. */\n createdAt: number;\n}\n\nexport interface EntryInput {\n workflow: string;\n userId: string;\n instanceId: string;\n hash: string;\n ts: number;\n /**\n * The instance's start, recorded in the same transaction as the entry — for a runtime whose\n * start is an asynchronous submit (AWS) that may have to be retried.\n */\n execution?: ExecutionStart | undefined;\n}\n\n/** How an instance is started on a runtime that submits it: replayed as is on every retry. */\nexport interface ExecutionStart {\n /** The runtime's idempotency key (the durable execution name). */\n name: string;\n /** What is invoked: a qualified function ARN, its version included. */\n target: string;\n /** The exact payload: a retried submit must send the same bytes, or the runtime refuses it. */\n input: string;\n}\n\nexport interface ExecutionRecord extends ExecutionStart {\n instanceId: string;\n workflow: string;\n userId: string;\n createdAt: number;\n submittedAt?: number | undefined;\n arn?: string | undefined;\n unresolvedAt?: number | undefined;\n callbackId?: string | undefined;\n /** Bumped by every wait the instance arms: a wake or a cleanup of an older one leaves it alone. */\n generation: number;\n deadlineAt?: number | undefined;\n checkedAt?: number | undefined;\n stoppedAt?: number | undefined;\n}\n\n/**\n * What a runtime that submits its starts and parks waits on one-shot callbacks (AWS) keeps\n * beside the entry ledger. The runtime holds the execution history; this holds only the\n * mapping from the engine's instance id to it.\n */\nexport interface ExecutionRegistry {\n get(instanceId: string): Promise<ExecutionRecord | undefined>;\n /**\n * Starts not yet confirmed and not given up on, oldest first: of one (workflow, user) entry\n * still running, or every one created before `createdBefore`.\n */\n pendingStarts(filter: {\n workflow?: string;\n userId?: string;\n createdBefore?: number;\n limit: number;\n }): Promise<ExecutionRecord[]>;\n markSubmitted(instanceId: string, arn: string | undefined, at: number): Promise<void>;\n markUnresolved(instanceId: string, at: number): Promise<void>;\n setArn(instanceId: string, arn: string): Promise<void>;\n /** Record the callback a wait parks on; false when a newer generation is already armed. */\n armCallback(\n instanceId: string,\n callback: { id: string; generation: number; deadlineAt?: number | undefined }\n ): Promise<boolean>;\n /** Forget the callback — only if it is still that generation's. */\n clearCallback(instanceId: string, generation: number): Promise<void>;\n /** Submitted executions whose entry is still running, least recently checked first. */\n toReconcile(limit: number, checkedBefore: number): Promise<ExecutionRecord[]>;\n markChecked(instanceId: string, at: number): Promise<void>;\n /** Record the first stop; a later one keeps it. */\n markStopped(instanceId: string, at: number): Promise<void>;\n}\n\n/**\n * How an entry attempt ended: a fresh row, a 'failed' row of a dead instance\n * taken over (the once policy does not bar a user over one outage), or\n * refused because the pair already entered and is still alive.\n */\nexport type EntryOutcome = 'entered' | 'reclaimed' | null;\n\n/** A device registered for pushes: its provider token belongs to the user who registered it last. */\nexport interface DeviceInput {\n userId: string;\n token: string;\n /** `ios` (APNs) or `android` (FCM): which provider the token belongs to. */\n platform: string;\n ts: number;\n}\n\n/** An instance of a user still in the ledger, and whether its runner was admitted (see `admitInstance`). */\nexport interface RunningInstance {\n instanceId: string;\n /** False until the runner admitted itself: nothing has run, and the tombstone keeps it that way. */\n admitted: boolean;\n}\n\n/** One (workflow, user) entry: the attempt it records and how far that attempt got. */\nexport interface EntryState {\n instanceId: string;\n /** 'running' until the run finalizes it ('completed', 'goal', 'failed', …). */\n status: string;\n /** The runner admitted itself (`admitInstance`): the instance did reach the runtime. */\n admitted: boolean;\n}\n\n/** An entry still waiting for its first admission (`unadmittedEntries`). */\nexport interface UnadmittedEntry {\n instanceId: string;\n workflow: string;\n userId: string;\n}\n\n/** A store plus how to release what opening it acquired (a connection, say). */\nexport interface StoreLease {\n store: JourneyStore;\n close?: () => Promise<void>;\n}\n\nimport type { ErasureState } from './erasure';\n\nexport { type ErasureState, UserErasedError } from './erasure';\n\n/**\n * The live database lacks tables or columns the engine reads (an SDK upgrade added them, or the\n * host never ran the migration). Raised before a deploy writes anything.\n */\nexport class SchemaMismatchError extends Error {\n override readonly name = 'SchemaMismatchError';\n\n constructor(readonly problems: readonly string[]) {\n super(\n `the database does not match @shware/workflow's tables — regenerate the drizzle schema (workflow-ui generate) and run your migrations:\\n ${problems.join('\\n ')}`\n );\n }\n}\n\n/** A message copy as reporting records it, before it goes out. */\nexport interface MessageRecord {\n idempotencyKey: string;\n workflow: string;\n instanceId: string;\n nodeId: string;\n template: string;\n /** Where the copy lands: `email`, `push_ios`, `push_android`. */\n channel: string;\n userId: string;\n /** An email's subscription topic; null for a transactional email and for every other channel. */\n topic: string | null;\n sentAt: number;\n}\n\n/** What is known of a copy without its engagement: whose it is, where it went, its topic. */\nexport interface MessageSummary {\n userId: string;\n channel: string;\n topic: string | null;\n}\n\n/** One opt-out to set or clear: a topic of the channel, or the whole channel (`topic: null`). */\nexport interface SuppressionChange {\n topic: string | null;\n suppressed: boolean;\n}\n\n/** What a click is judged by. */\nexport interface ClickContext {\n url: string;\n deliveredAt: number;\n linkCount: number;\n /** The copy's clicks in the window before this one. */\n recent: { position: number; at: number }[];\n}\n\n/** An open or a click, with the verdict on who made it and where it came from. */\nexport interface Engagement {\n at: number;\n machine: boolean;\n /** The rule that called it a machine; null for a person. */\n reason: string | null;\n userAgent: string | null;\n /** The network the request came from (Cloudflare's `cf.asn`, `cf.asOrganization`, `cf.country`), never the address. */\n asn: number | null;\n organization: string | null;\n country: string | null;\n}\n\n/**\n * Journey reporting: where each instance stands, every copy and how it was engaged with, segment\n * sizes. The store exposes it as `reporting`; the host records it and serves email tracking.\n * Every write is idempotent — a retried step reports again.\n */\n/** Bounds on when an event happened, both inclusive (epoch ms); an absent side is open. */\nexport interface EventWindow {\n sinceMs?: number;\n untilMs?: number;\n}\n\n/**\n * Where a page of events ended: the last row's timestamp and id, the keyset `(ts, id)` the next\n * page starts after. Opaque to callers; `ts` is the store's own rendering of the timestamp, so a\n * round trip loses no precision.\n */\nexport interface EventCursor {\n ts: string;\n id: string;\n}\n\nexport interface JourneyReporting {\n /** The instance entered: its row exists from here, so one a guard ends at once still counts. */\n instanceStarted(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n at: number;\n }): Promise<void>;\n nodeReached(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n nodeId: string;\n at: number;\n }): Promise<void>;\n instanceFinished(input: {\n workflow: string;\n instanceId: string;\n userId: string;\n status: string;\n at: number;\n convertedAt?: number;\n }): Promise<void>;\n /** The copy's row id; the row a retry already wrote, when there is one. */\n recordMessage(message: MessageRecord): Promise<string>;\n /**\n * Whether the copy was settled (`settleMessage`: delivered, or settled undelivered) — read from\n * the database itself, so a replayed send step never trusts a cache that missed the settle.\n */\n messageSettled(idempotencyKey: string): Promise<boolean>;\n settleMessage(\n idempotencyKey: string,\n result: { delivered: true } | { delivered: false; reason: string },\n at: number\n ): Promise<void>;\n /** An email's tracked links, in document order. */\n recordLinks(messageId: string, urls: readonly string[]): Promise<void>;\n /**\n * Record an open of the copy, judged by `judge` against the copy (its recipient and when it was\n * delivered — when it was sent, before the transport answered); `undefined` from the judge\n * records nothing. Read and written in one transaction. False for an unknown copy.\n */\n recordOpen(\n messageId: string,\n judge: (message: { userId: string; deliveredAt: number }) => Engagement | undefined\n ): Promise<boolean>;\n /**\n * Record a click on the copy's link at `position`, judged by `judge` against the link, how\n * many links the copy has and its clicks of the last `windowMs`. The copy is locked for the\n * transaction, so clicks arriving together (a scanner following every link at once) are\n * judged one after another, each seeing the ones before. When the judge asks to `backdate`,\n * the window's earlier clicks are reclassified with this one's verdict. A click implies an\n * open. Undefined for an unknown link.\n */\n recordClick(\n messageId: string,\n position: number,\n at: number,\n windowMs: number,\n judge: (link: ClickContext) => { engagement: Engagement; backdate: boolean }\n ): Promise<{ url: string; engagement: Engagement } | undefined>;\n /**\n * The email copy's recipient opted out: of the copy's topic, or with `scope: 'all'` of every\n * email. Recorded on the copy and as a suppression (the topic's row, or the channel-wide\n * null-topic row), in one transaction, which also yields who the recipient is and the topic —\n * so the caller acts on the unsubscribe without a second read. A transactional copy\n * (`topic: null`) has nothing to opt out of: nothing is recorded, and the answer says so.\n * Undefined for an unknown copy.\n */\n recordUnsubscribe(\n messageId: string,\n at: number,\n scope?: 'topic' | 'all'\n ): Promise<{ userId: string; topic: string | null } | undefined>;\n /** The copy's owner, channel and topic; undefined for an unknown copy. */\n lookupMessage(messageId: string): Promise<MessageSummary | undefined>;\n /** A push copy the device reported it received. False for an unknown copy. */\n recordDeviceDelivered(messageId: string, at: number): Promise<boolean>;\n /** Drop members of segments no longer deployed, then record every deployed segment's size. */\n snapshotSegments(at: number): Promise<{ segment: string; size: number }[]>;\n}\n\nexport interface JourneyStore {\n /** Where instances stand, every copy and its engagement, segment sizes. */\n readonly reporting: JourneyReporting;\n\n /**\n * Record that the user's data must go (a deleted account): the tombstone every write about\n * the user is conditional on, from here on and for good. Committed before anything is\n * stopped or erased, so an entry that got in before it is found by the erasure and none gets\n * in after it. Idempotent: a second request, or one for a user already erased, changes nothing.\n */\n requestErasure(userId: string, at: number): Promise<void>;\n /** Users whose erasure was requested and not carried out yet (`'erasing'`), oldest first. */\n pendingErasures(): Promise<string[]>;\n /**\n * Where the user stands: `'none'` (no tombstone), `'erasing'` (requested, their instances\n * being stopped, data still there) or `'erased'` (`forgetUser` went through). Both\n * tombstone states refuse every write about the user; the entry points read this for a fast\n * answer, and the store's conditional writes are the gate itself.\n */\n erasureState(userId: string): Promise<ErasureState>;\n /** The user's journey instances still running, by the entry ledger. */\n runningInstances(userId: string): Promise<RunningInstance[]>;\n /**\n * The runner's admission, before its first observable effect: records that the instance runs\n * (`admitted_at`, kept on replay) — unless the user has an erasure tombstone, in which case\n * nothing is recorded and the runner must not run. Mutually exclusive with `requestErasure`\n * and `forgetUser` for that user (Postgres: the user's advisory lock), so an erasure either\n * finds the instance admitted and stops it, or commits its tombstone first and the admission\n * is refused — never neither.\n *\n * A first admission needs the entry running. An instance already admitted is admitted again\n * whatever the entry's status — the legitimate replay of an instance whose finalisation was\n * cut short after the entry reached its terminal status, which resumes on its own checkpoint\n * and finishes what is left. Never a new attempt's way in: false for a terminal entry never\n * admitted, for an entry no longer in the ledger, for another user's entry, and for an\n * instance whose entry was reclaimed since (`enterJourney` resets the admission with the\n * instance id, so a superseded attempt cannot pass under the id it lost).\n */\n admitInstance(instanceId: string, userId: string): Promise<boolean>;\n /**\n * Erase everything kept about the user — profile, devices, events, wake subscriptions, segment\n * memberships, entries and reporting — and mark the tombstone erased (`erased_at`), which\n * stays: a write that was in flight when the data went cannot bring the user back, and the\n * same id is never reactivated by ingest — which is also why the suppressions go with the\n * rest: nothing is ever sent to the id again, so no opt-out needs keeping. Run only once no\n * instance of theirs runs any more, or one could write some of it back. Idempotent. Mutually\n * exclusive with every write about the user (the user's advisory lock), so the data is gone\n * when this returns — nothing of theirs lands afterwards.\n */\n forgetUser(userId: string, at: number): Promise<void>;\n\n /**\n * The user opted out of the channel (recorded by an unsubscribe, or set through\n * `/subscriptions`): with a `topic`, an opt-out of that topic or of the whole channel; without\n * one, only the whole channel.\n */\n isSuppressed(userId: string, channel: string, topic?: string): Promise<boolean>;\n /** Every opt-out of the user on the channel: the topics, and `null` for the whole channel. */\n listSuppressions(userId: string, channel: string): Promise<(string | null)[]>;\n /**\n * Set or clear opt-outs, all in one transaction: a suppressed change inserts the row (keeping\n * the time of one already there), an unsuppressed one deletes it. Written whatever the user's\n * erasure state — the caller refuses an erased user first — since an opt-out is never refused.\n */\n updateSuppressions(\n userId: string,\n channel: string,\n changes: readonly SuppressionChange[],\n at: number\n ): Promise<void>;\n\n /**\n * What the live database lacks against the engine's tables (read-only); a deploy refuses to\n * run while it lists anything.\n */\n checkSchema(): Promise<string[]>;\n\n /* ---------------------------------- events --------------------------------- */\n /**\n * Append the occurrence; false when its id was already in the log (nothing written). Throws\n * `UserErasedError` for a user with a tombstone, like every write about a user below.\n */\n insertEvent(input: EventInput): Promise<boolean>;\n /**\n * Occurrences of `event` for the user, optionally only those within the window. With\n * `atLeast`, counting may stop there: the result is exact below it and some number ≥ it\n * otherwise — all a `performed(…, { count })` check needs, without counting a heavy user's\n * whole history.\n */\n countEvents(\n userId: string,\n event: string,\n window?: EventWindow,\n atLeast?: number\n ): Promise<number>;\n /**\n * One page of those occurrences' payloads, oldest first — the caller filters them with the\n * shared evaluator and pages on with `next` only while it still needs matches, so a `where`\n * never loads a user's whole history at once. `next` is absent on the last page.\n */\n pageEventPayloads(\n userId: string,\n event: string,\n window: EventWindow | undefined,\n page: { limit: number; after?: EventCursor | undefined }\n ): Promise<{ payloads: Record<string, unknown>[]; next?: EventCursor | undefined }>;\n\n /* --------------------------------- profiles -------------------------------- */\n getProfile(userId: string): Promise<ProfileProps | undefined>;\n /**\n * Merge-patch `props` into the stored profile in one statement (a null\n * removes the property) and return the result. One statement, because a\n * read-modify-write loses one of two concurrent identifies.\n */\n mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;\n\n /**\n * Every user the engine knows (a profile or an event): who the segment sweep evaluates. Ids\n * come in ascending order; `after` resumes past the last id of the previous page, and `limit`\n * bounds the page (the whole list without one).\n */\n listUserIds(page?: { after?: string | undefined; limit?: number | undefined }): Promise<string[]>;\n\n /* ---------------------------------- devices --------------------------------- */\n /** The user's push devices, most recently registered first. */\n listDevices(userId: string): Promise<PushDevice[]>;\n /**\n * Register a device: one row per token, so a token registered again moves to the user who\n * registered it last (a phone signed into another account) and to the front of their list.\n */\n upsertDevice(device: DeviceInput): Promise<void>;\n /** Forget a token (reported dead by APNs / FCM); the user it belonged to, if any. */\n removeDevice(token: string): Promise<string | undefined>;\n /**\n * The user signed out on the device: the token is kept, bound to nobody, until the next sign-in\n * binds it again. The user it belonged to, if any.\n */\n unbindDevice(token: string): Promise<string | undefined>;\n\n /* ---------------------------- deployed definitions -------------------------- */\n getSegmentCondition(id: string): Promise<ConditionIR | undefined>;\n /** Every deployed segment with its definition: what the segment sweep evaluates. */\n listSegments(): Promise<{ id: string; condition: ConditionIR }[]>;\n findTriggers(event: string): Promise<TriggerRoute[]>;\n listSegmentTriggers(): Promise<SegmentTriggerRoute[]>;\n putWorkflowIR(workflow: WorkflowIR): Promise<void>;\n getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined>;\n /**\n * Swap the routing tables (segments, event triggers, segment triggers) for\n * the bundle's — atomically, so a concurrent ingest never sees them half\n * empty. Workflow bodies are stored separately (`putWorkflowIR`) because they\n * are content-addressed and outlive the routes that point at them.\n *\n * With `version` — a forced apply of the code's own bundle (`POST /bundle/deploy`) — the\n * version is recorded too, unconditionally and in the same transaction, so the live version\n * always names the code the routes came from. Without it (a hand-posted `POST /deploy`) the\n * recorded version stays as it was: that bundle holds until a newer code version applies its own.\n */\n replaceRoutes(bundle: BundleIR, version?: BundleVersion): Promise<void>;\n /**\n * The Worker version whose bundle the routes came from, when one put them there (through\n * `replaceRoutesIfNewer`, or a forced `replaceRoutes` with a version). Read from the database\n * itself, not a cache in front of it.\n */\n liveBundleVersion(): Promise<BundleVersion | undefined>;\n /**\n * `replaceRoutes`, only when `version` is newer than the live one, recorded in the same\n * transaction: two versions serving side by side during a rollout cannot leave the older one's\n * routes in force. False, with nothing written, when a version as new or newer is live.\n */\n replaceRoutesIfNewer(bundle: BundleIR, version: BundleVersion): Promise<boolean>;\n\n /* ----------------------------- segment membership --------------------------- */\n isSegmentMember(segment: string, userId: string): Promise<boolean>;\n /** Record the membership, with the workflows its entry has to start (see segment_start). */\n addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void>;\n /** The workflows the member's entry has not started yet. */\n pendingSegmentStarts(segment: string, userId: string): Promise<string[]>;\n completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void>;\n /** Drop the membership and any start it still owed. */\n removeSegmentMember(segment: string, userId: string): Promise<void>;\n\n /* ------------------------------- entry ledger ------------------------------- */\n /**\n * Record the entry (the once policy). Atomic under concurrent ingests: of\n * two racing attempts exactly one is 'entered' (or 'reclaimed', when the\n * pair's previous instance is recorded as 'failed') and the other null.\n */\n enterJourney(entry: EntryInput): Promise<EntryOutcome>;\n removeEntry(instanceId: string): Promise<void>;\n setEntryStatus(instanceId: string, status: string): Promise<void>;\n /** The (workflow, user) entry as it stands, read from the database itself; undefined if none. */\n getEntry(workflow: string, userId: string): Promise<EntryState | undefined>;\n /**\n * Running entries no runner has admitted, claimed before `claimedBefore` (epoch ms), oldest\n * first: candidates for a start that was lost between the claim and the platform's create.\n */\n unadmittedEntries(claimedBefore: number, limit: number): Promise<UnadmittedEntry[]>;\n /**\n * Mark a lost start `failed`, so the pair's next trigger reclaims it. Only while the entry is\n * still running and unadmitted under this instance id; false when it moved on meanwhile.\n */\n failUnadmittedEntry(instanceId: string): Promise<boolean>;\n /** Execution starts and wake callbacks, for a runtime that submits its starts (AWS). */\n readonly executions: ExecutionRegistry;\n\n /* ------------------------------ wake subscriptions --------------------------- */\n /** Distinct wake handles registered for (user, event). */\n findWakeHandles(userId: string, event: string): Promise<string[]>;\n /** Register the handle for each event; repeated registrations are idempotent. */\n subscribe(handle: string, userId: string, events: readonly string[], ts: number): Promise<void>;\n /** Drop every registration the handle holds. */\n unsubscribe(handle: string): Promise<void>;\n}\n\nexport { JourneyFactSource } from './facts';\nexport { PostgresJourneyStore } from './postgres';\nexport { PostgresReporting } from './postgres-reporting';\nexport {\n DEFAULT_SCHEMA,\n TABLES,\n renderDrizzleSchema,\n renderPostgresDdl,\n type TableSpec,\n type UniqueConstraintSpec,\n} from './tables';\nexport type { PostgresJourneyStoreOptions, SqlClientLike } from './postgres';\n"],"mappings":";;;;;;;;;;;AA2LA,IAAa,sBAAb,cAAyC,MAAM;CAGxB;CAFrB,OAAyB;CAEzB,YAAY,UAAsC;EAChD,MACE,4IAA4I,SAAS,KAAK,MAAM,GAClK;EAHmB,KAAA,WAAA;CAIrB;AACF"}
@@ -37,8 +37,8 @@ export interface TriggerRoute {
37
37
  /** Profile gate, or null. */
38
38
  filter: ConditionIR | null;
39
39
  /**
40
- * The code version that applied these routes (`replaceRoutesIfNewer`); absent after a forced
41
- * deploy. Read in the same row as the hash, so the pair is always one deploy's.
40
+ * The code version that applied these routes; absent after a hand-posted `POST /deploy`. Read
41
+ * in the same row as the hash, so the pair is always one deploy's.
42
42
  */
43
43
  version?: string | undefined;
44
44
  }
@@ -148,6 +148,20 @@ export interface RunningInstance {
148
148
  /** False until the runner admitted itself: nothing has run, and the tombstone keeps it that way. */
149
149
  admitted: boolean;
150
150
  }
151
+ /** One (workflow, user) entry: the attempt it records and how far that attempt got. */
152
+ export interface EntryState {
153
+ instanceId: string;
154
+ /** 'running' until the run finalizes it ('completed', 'goal', 'failed', …). */
155
+ status: string;
156
+ /** The runner admitted itself (`admitInstance`): the instance did reach the runtime. */
157
+ admitted: boolean;
158
+ }
159
+ /** An entry still waiting for its first admission (`unadmittedEntries`). */
160
+ export interface UnadmittedEntry {
161
+ instanceId: string;
162
+ workflow: string;
163
+ userId: string;
164
+ }
151
165
  /** A store plus how to release what opening it acquired (a connection, say). */
152
166
  export interface StoreLease {
153
167
  store: JourneyStore;
@@ -220,6 +234,15 @@ export interface EventWindow {
220
234
  sinceMs?: number;
221
235
  untilMs?: number;
222
236
  }
237
+ /**
238
+ * Where a page of events ended: the last row's timestamp and id, the keyset `(ts, id)` the next
239
+ * page starts after. Opaque to callers; `ts` is the store's own rendering of the timestamp, so a
240
+ * round trip loses no precision.
241
+ */
242
+ export interface EventCursor {
243
+ ts: string;
244
+ id: string;
245
+ }
223
246
  export interface JourneyReporting {
224
247
  /** The instance entered: its row exists from here, so one a guard ends at once still counts. */
225
248
  instanceStarted(input: {
@@ -245,6 +268,11 @@ export interface JourneyReporting {
245
268
  }): Promise<void>;
246
269
  /** The copy's row id; the row a retry already wrote, when there is one. */
247
270
  recordMessage(message: MessageRecord): Promise<string>;
271
+ /**
272
+ * Whether the copy was settled (`settleMessage`: delivered, or settled undelivered) — read from
273
+ * the database itself, so a replayed send step never trusts a cache that missed the settle.
274
+ */
275
+ messageSettled(idempotencyKey: string): Promise<boolean>;
248
276
  settleMessage(idempotencyKey: string, result: {
249
277
  delivered: true;
250
278
  } | {
@@ -372,10 +400,25 @@ export interface JourneyStore {
372
400
  * `UserErasedError` for a user with a tombstone, like every write about a user below.
373
401
  */
374
402
  insertEvent(input: EventInput): Promise<boolean>;
375
- /** Occurrences of `event` for the user, optionally only those within the window. */
376
- countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;
377
- /** The payloads of those occurrences — the caller filters them with the shared evaluator. */
378
- listEventPayloads(userId: string, event: string, window?: EventWindow): Promise<Record<string, unknown>[]>;
403
+ /**
404
+ * Occurrences of `event` for the user, optionally only those within the window. With
405
+ * `atLeast`, counting may stop there: the result is exact below it and some number ≥ it
406
+ * otherwise — all a `performed(…, { count })` check needs, without counting a heavy user's
407
+ * whole history.
408
+ */
409
+ countEvents(userId: string, event: string, window?: EventWindow, atLeast?: number): Promise<number>;
410
+ /**
411
+ * One page of those occurrences' payloads, oldest first — the caller filters them with the
412
+ * shared evaluator and pages on with `next` only while it still needs matches, so a `where`
413
+ * never loads a user's whole history at once. `next` is absent on the last page.
414
+ */
415
+ pageEventPayloads(userId: string, event: string, window: EventWindow | undefined, page: {
416
+ limit: number;
417
+ after?: EventCursor | undefined;
418
+ }): Promise<{
419
+ payloads: Record<string, unknown>[];
420
+ next?: EventCursor | undefined;
421
+ }>;
379
422
  getProfile(userId: string): Promise<ProfileProps | undefined>;
380
423
  /**
381
424
  * Merge-patch `props` into the stored profile in one statement (a null
@@ -421,12 +464,17 @@ export interface JourneyStore {
421
464
  * the bundle's — atomically, so a concurrent ingest never sees them half
422
465
  * empty. Workflow bodies are stored separately (`putWorkflowIR`) because they
423
466
  * are content-addressed and outlive the routes that point at them.
467
+ *
468
+ * With `version` — a forced apply of the code's own bundle (`POST /bundle/deploy`) — the
469
+ * version is recorded too, unconditionally and in the same transaction, so the live version
470
+ * always names the code the routes came from. Without it (a hand-posted `POST /deploy`) the
471
+ * recorded version stays as it was: that bundle holds until a newer code version applies its own.
424
472
  */
425
- replaceRoutes(bundle: BundleIR): Promise<void>;
473
+ replaceRoutes(bundle: BundleIR, version?: BundleVersion): Promise<void>;
426
474
  /**
427
- * The Worker version whose bundle the routes came from, when one put them there through
428
- * `replaceRoutesIfNewer` (a forced deploy leaves it as it was). Read from the database itself,
429
- * not a cache in front of it.
475
+ * The Worker version whose bundle the routes came from, when one put them there (through
476
+ * `replaceRoutesIfNewer`, or a forced `replaceRoutes` with a version). Read from the database
477
+ * itself, not a cache in front of it.
430
478
  */
431
479
  liveBundleVersion(): Promise<BundleVersion | undefined>;
432
480
  /**
@@ -451,6 +499,18 @@ export interface JourneyStore {
451
499
  enterJourney(entry: EntryInput): Promise<EntryOutcome>;
452
500
  removeEntry(instanceId: string): Promise<void>;
453
501
  setEntryStatus(instanceId: string, status: string): Promise<void>;
502
+ /** The (workflow, user) entry as it stands, read from the database itself; undefined if none. */
503
+ getEntry(workflow: string, userId: string): Promise<EntryState | undefined>;
504
+ /**
505
+ * Running entries no runner has admitted, claimed before `claimedBefore` (epoch ms), oldest
506
+ * first: candidates for a start that was lost between the claim and the platform's create.
507
+ */
508
+ unadmittedEntries(claimedBefore: number, limit: number): Promise<UnadmittedEntry[]>;
509
+ /**
510
+ * Mark a lost start `failed`, so the pair's next trigger reclaims it. Only while the entry is
511
+ * still running and unadmitted under this instance id; false when it moved on meanwhile.
512
+ */
513
+ failUnadmittedEntry(instanceId: string): Promise<boolean>;
454
514
  /** Execution starts and wake callbacks, for a runtime that submits its starts (AWS). */
455
515
  readonly executions: ExecutionRegistry;
456
516
  /** Distinct wake handles registered for (user, event). */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/store/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/store/index.ts"],"mappings":"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"}