Documentation
¶
Index ¶
- Constants
- func Bind(engine ActivationEngine, deps func() memql.AuthoredRuntimeDeps)
- func BundleTitleFor(r Rule) string
- func ConstructNameFor(ruleID string) string
- func GenerateAutomation(r Rule) (string, error)
- func LaneFor(recipientMode string) string
- func NormalizedRoles(roles []string) []string
- type ActivationEngine
- type Activator
- type ConditionError
- type Engine
- type EngineAdapter
- func (a EngineAdapter) ActivateApprovedBundle(ctx context.Context, owner, bundleId string, deps memql.AuthoredRuntimeDeps) (memql.ActivationResult, error)
- func (a EngineAdapter) Execute(ctx context.Context, query string) (any, error)
- func (a EngineAdapter) RetireActiveBundle(ctx context.Context, owner, bundleId string, deps memql.AuthoredRuntimeDeps) error
- func (a EngineAdapter) SetAuthoredPauseListener(name string, listener memql.AuthoredPauseListener)
- type FireOutcome
- type Firer
- type FiringGate
- type FiringState
- type FirstFireClaim
- type FirstFireRelease
- type Integration
- type Result
- type Rule
- type RuleState
- type Store
- func (s *Store) AddRecipient(ctx context.Context, ...) error
- func (s *Store) AudienceByID(ctx context.Context, audienceID string) (bool, error)
- func (s *Store) AudienceConfirmation(ctx context.Context, id string) (string, error)
- func (s *Store) BundleStatus(ctx context.Context, bundleID string) (status string, found bool, err error)
- func (s *Store) BundleVersion(ctx context.Context, bundleID string) int
- func (s *Store) CreateBundle(ctx context.Context, bundleID, title, summary, supersedes string, version int) error
- func (s *Store) CreateConstruct(ctx context.Context, constructID, bundleID, name, namespace, source string) error
- func (s *Store) EachSendableRecipient(ctx context.Context, audienceID string, visit func(map[string]any) bool) error
- func (s *Store) FiringStateByID(ctx context.Context, ruleID string) (FiringState, bool, error)
- func (s *Store) MailboxMembers(ctx context.Context, audienceID, address string) (members []map[string]any, complete bool, err error)
- func (s *Store) RecipientsForAudience(ctx context.Context, audienceID string) ([]map[string]any, error)
- func (s *Store) RecordDryRun(ctx context.Context, bundleID, ruleID, lane string) error
- func (s *Store) RecordFiring(ctx context.Context, ruleID string, firedCount int, firedAt time.Time, ...) error
- func (s *Store) RecordGeneration(ctx context.Context, ruleID, status, bundleID, constructName, lastError string) error
- func (s *Store) RecordValidation(ctx context.Context, bundleID string, ok bool, diagnostics []string, ...) error
- func (s *Store) RuleByID(ctx context.Context, ruleID string) (Rule, bool, error)
- func (s *Store) RuleOfBundle(ctx context.Context, bundleID string) (ruleID, status string, ok bool, err error)
- func (s *Store) RuleStateByID(ctx context.Context, ruleID string) (RuleState, bool, error)
- func (s *Store) SendToRecipient(ctx context.Context, ...) (map[string]any, error)
- func (s *Store) SenderIdentityByID(ctx context.Context, identityID string) (map[string]any, bool, error)
- func (s *Store) StageOperationalEmail(ctx context.Context, ...) error
- func (s *Store) TemplateByID(ctx context.Context, templateID string) (map[string]any, bool, error)
- func (s *Store) UsersInRoles(ctx context.Context) ([]map[string]any, error)
Constants ¶
const ( ModeClusterRoles = "cluster_roles" // operational lane: the transactional outbox ModeAudience = "audience" // marketing lane: the campaign machinery ModeRowAddress = "row_address" // marketing lane, address off the triggering row )
The three recipient modes, and the two lanes they pick (program P5). The lane is a CONSEQUENCE of who receives, never a separate setting -- a person choosing "the people in this cluster" has chosen the operational lane whether or not they have ever heard the word.
const BreakerStoppedSentence = "Stopped after repeated failures. Fix the cause, then turn it on again."
BreakerStoppedSentence is what a rule a breaker stopped says on its row: what happened and what to do.
const IntegrationName = "emailrules"
IntegrationName is the DSL-visible namespace: integration.emailrules.*
const PreviousVersionStillRunning = "The previous version is still running."
PreviousVersionStillRunning ends the reason a refused edit records: the version that was live before the edit is still armed and still sending.
const RuleConcept = "v1:campaigns:emailRule"
RuleConcept is the email rule's concept id: what a write conditional on the rule row is fenced on.
Variables ¶
This section is empty.
Functions ¶
func Bind ¶
func Bind(engine ActivationEngine, deps func() memql.AuthoredRuntimeDeps)
Bind hands this package the concrete engine and the authored-runtime deps. Called once from the App during the engine phase, right after the authored runtime exists. Idempotent; a second call replaces the first, which is what a test that stands two engines up needs.
The same handoff registers, on an engine that takes one, the listener that marks a rule paused when this node records its breaker's pause (breaker_pause.go). It is registered by name, so binding again replaces it.
func BundleTitleFor ¶
BundleTitleFor is what an operator sees in the authoring surfaces. It names the rule rather than the construct, because the rule is the thing they made.
func ConstructNameFor ¶
ConstructNameFor derives the generated automation's name from the rule id.
DETERMINISTIC, because that is what makes a regeneration REPLACE rather than accumulate: the activation planner supersedes a prior bundle by matching what it registers, and two names for one rule would leave the old automation armed beside the new one -- a rule that had been edited would then send twice, once with each version, which is the failure mode a person reports as "it sent the old copy as well".
Construct names are unique tree-wide, so the prefix is not decoration.
func GenerateAutomation ¶
GenerateAutomation renders the rule's executable form.
Pure: a function of the Rule and nothing else, so its whole output is diffable in a test and a change to it is visible as a change to the expected string rather than as a behaviour somebody has to reproduce.
func LaneFor ¶
LaneFor reports which delivery lane a recipient mode implies. Operational mail must never consult or write the marketing suppression list: an unsubscribe from a newsletter that silenced a security notice would be a correctly-implemented disaster.
func NormalizedRoles ¶
NormalizedRoles returns the operational lane's recipient roles, lowercased, de-duplicated and ordered, so two spellings of one rule produce one answer.
Types ¶
type ActivationEngine ¶
type ActivationEngine interface {
Engine
ActivateApprovedBundle(ctx context.Context, owner, bundleId string, deps memql.AuthoredRuntimeDeps) (memql.ActivationResult, error)
RetireActiveBundle(ctx context.Context, owner, bundleId string, deps memql.AuthoredRuntimeDeps) error
}
ActivationEngine is the concrete engine surface the activation path needs. Narrow on purpose: this package has no business with the rest of the engine, and a narrow seam is one a test can stand in for.
type Activator ¶
type Activator struct {
// contains filtered or unexported fields
}
Activator arms and retires rules.
func NewActivator ¶
func NewActivator(engine ActivationEngine, deps func() memql.AuthoredRuntimeDeps) *Activator
func (*Activator) Activate ¶
Activate generates, validates, writes and arms a rule's construct.
`owner` is the AUTHENTICATED caller. It must equal the rule's owner, and the check is not decoration: the generated automation runs under the AUTHOR's envelope, so activating somebody else's rule would be running their reads under their identity on your say-so. `ActivateApprovedBundle` enforces the same rule at the bundle grain; this refuses earlier, with a sentence about rules rather than about bundles.
func (*Activator) Retire ¶
Retire tears a rule's construct down. The RULE row survives with its history: a rule somebody turned off is a different thing from one that never existed, and an app that deleted the row would have nothing to show for the mail it already sent.
It retires the bundle the rule row NAMES, never one derived again: a bundle keeps the id it was armed under, which for a rule armed before ids carried their owner is the old derivation (legacyBundleIDFor). The activation after it derives afresh, with the owner.
type ConditionError ¶ added in v0.22.0
type ConditionError struct{ Message string }
ConditionError is a refused condition, worded for the person who typed it: the condition is the one place an end user writes an expression in the product (the OS Campaigns app's "Only when" field), and this message is what that field shows. Plain words, and the fix in one sentence.
func (*ConditionError) Error ¶ added in v0.22.0
func (e *ConditionError) Error() string
type EngineAdapter ¶
type EngineAdapter struct {
Engine *memql.MemQLEngine
}
EngineAdapter fits the live *memql.MemQLEngine to ActivationEngine.
The two differ in exactly one thing: the engine returns a *ExecuteResult where this package's read helpers take `any` -- and MaterializeRows accepts either. So the adapter is one method wide, and it lives here rather than in app/ for the same reason CampaignsEngineAdapter lives beside its consumer: the shape being adapted is this package's, and a wrapper written at the call site is one that has to be rewritten at the next call site.
func (EngineAdapter) ActivateApprovedBundle ¶
func (a EngineAdapter) ActivateApprovedBundle(ctx context.Context, owner, bundleId string, deps memql.AuthoredRuntimeDeps) (memql.ActivationResult, error)
func (EngineAdapter) RetireActiveBundle ¶
func (a EngineAdapter) RetireActiveBundle(ctx context.Context, owner, bundleId string, deps memql.AuthoredRuntimeDeps) error
func (EngineAdapter) SetAuthoredPauseListener ¶ added in v0.26.0
func (a EngineAdapter) SetAuthoredPauseListener(name string, listener memql.AuthoredPauseListener)
SetAuthoredPauseListener hands the live engine a breaker-pause listener.
type FireOutcome ¶
type FireOutcome struct {
RuleID string
Lane string
Recipients int
Sent int
Deferred int
Skipped int
Refusals []string
// Duplicate is a created rule's second firing for one row -- two writes
// raced on a new row -- which sends nothing and is not counted as a firing.
Duplicate bool
// Busy: a send found the cluster's lock busy and, after a few short pauses,
// still busy. Nothing was attempted for that recipient, the rule's fault it is
// not, and a created rule's claim for the row was given back so the row can fire
// again.
Busy bool
}
FireOutcome is what one firing did, for the result node and the log line.
type Firer ¶
type Firer struct {
// contains filtered or unexported fields
}
Firer executes one rule against one triggering event.
func (*Firer) Fire ¶
func (f *Firer) Fire(ctx context.Context, ruleID, nodeID string, event map[string]any) (FireOutcome, error)
Fire resolves the rule, decides the lane from who receives, and sends.
`nodeId` and `event` come from the generated automation's one step. `event` is the whole envelope; its `payload` is the triggering row as the trigger delivered it.
func (*Firer) WithFiringGate ¶ added in v0.23.6
func (f *Firer) WithFiringGate(gate FiringGate) *Firer
WithFiringGate returns the Firer with the gate that records one rule's firings one at a time across every replica (firing_gate.go). Without one, a firing is recorded unlocked, and firings that overlap can lose increments.
func (*Firer) WithFirstFireClaim ¶ added in v0.22.0
func (f *Firer) WithFirstFireClaim(claim FirstFireClaim) *Firer
WithFirstFireClaim returns the Firer with the claim a created rule needs.
func (*Firer) WithFirstFireRelease ¶ added in v0.26.0
func (f *Firer) WithFirstFireRelease(release FirstFireRelease) *Firer
WithFirstFireRelease returns the Firer with the release that goes with the claim. Without one, a created rule's firing that was held up by a busy lock keeps its claim, and the row can never fire again.
type FiringGate ¶ added in v0.23.6
FiringGate runs record while no other firing of the same rule -- on this node or any other -- is inside its own record.
type FiringState ¶ added in v0.23.6
type FiringState struct {
FiredCount int
// VersionAt is the createdAt of the version read -- the version the
// record must land after (firedAtAfter). Zero when the read did not carry
// one, which degrades to stamping at the local clock.
VersionAt time.Time
}
FiringState is what a firing's record is derived from: the count it advances and the version that count was read from.
type FirstFireClaim ¶ added in v0.22.0
FirstFireClaim records that a created rule has fired for a row and reports whether THIS firing is the one that did: true for exactly one caller per (rule, row), on any replica. A false is a duplicate -- or a claim that could not be recorded, which is treated the same, because a created rule that cannot prove it is first must not send.
type FirstFireRelease ¶ added in v0.26.0
FirstFireRelease gives back the claim a created rule took for a row, so the row's firing can be made again. It is for a firing that ended with nothing attempted -- the cluster's lock was busy -- and only that: a firing that began a send keeps its claim, which is what keeps the row from sending twice.
type Integration ¶
type Integration struct {
// contains filtered or unexported fields
}
Integration is the provider the engine registers.
func (*Integration) Capabilities ¶
func (i *Integration) Capabilities() []memql.IntegrationCapability
func (*Integration) IntegrationName ¶
func (i *Integration) IntegrationName() string
type Result ¶
type Result struct {
RuleID string
Status string
BundleID string
ConstructName string
Lane string
Error string
}
Result is what the builtin hands back.
type Rule ¶
type Rule struct {
ID string
OwnerUserID string
Name string
Description string
TriggerConcept string
EventKind string
Condition string
TemplateID string
RecipientMode string
RecipientRoles []string
AudienceID string
RecipientField string
AccountID string
SenderIdentity string
}
Rule is the form half of v1:campaigns:emailRule -- everything the generator reads. The generated/refs half (bundleId, constructName, lastError, the counters) is deliberately absent: those are the ENGINE's account of what it made of the form, and a generator that read them could produce a construct whose content depended on its own past output.
func (Rule) Validate ¶
Validate checks the form for everything that can be decided without the live concept registry. What it deliberately does NOT check is whether the trigger concept exists and whether the recipient field resolves on it -- those need the registry, they belong to the activation path, and a generator that reached for a registry would stop being a pure function of the form.
type RuleState ¶
type RuleState struct {
Status string
BundleID string
ConstructName string
LastError string
FiredCount int
// VersionAt is the createdAt of the version read: the witness a record
// conditional on this read is fenced on (memql.ContextWithRowVersionFence).
// Zero when the read did not carry one.
VersionAt time.Time
}
RuleState is the generated half of the row.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
func (*Store) AddRecipient ¶
func (s *Store) AddRecipient(ctx context.Context, recipientID, audienceID, email, displayName, accountID, status string) error
AddRecipient enrols a row-derived address into the rule's audience, so it gains a membership, a subscription state and a working unsubscribe. `signup` is the honest source value: a product flow added it.
accountID is the rule's organization, as a BARE id -- the caller bare-ifies it once, because BareShortId is not idempotent (memql#2981). A recipient is an organization-scoped row and the send refuses one outside the campaign's organization, so an address enrolled without it is a recipient the very next step would refuse. A rule that names no organization (one that predates organizations) leaves it off the call, and addRecipient stamps "".
func (*Store) AudienceByID ¶
AudienceByID reads the audience a marketing rule mails. Used at ARM time, not only at fire time: see Activator.verifyReadable.
func (*Store) AudienceConfirmation ¶ added in v0.26.0
func (*Store) BundleStatus ¶ added in v0.26.0
func (s *Store) BundleStatus(ctx context.Context, bundleID string) (status string, found bool, err error)
BundleStatus reads an authoring bundle's status under the caller's own actor. found is false when the bundle is not readable as the caller.
func (*Store) BundleVersion ¶
BundleVersion reads an authoring bundle's version. Zero when the bundle is unreadable -- which is treated as "there is nothing to supersede" rather than as an error, because the alternative is refusing to arm a rule on the strength of a row the caller cannot see.
func (*Store) CreateBundle ¶
func (s *Store) CreateBundle(ctx context.Context, bundleID, title, summary, supersedes string, version int) error
CreateBundle writes a rule's bundle as a draft. createAuthoringBundle is @serverOnly (memql#6022), so the caller stamps internal origin -- see Activator.writeBundle, where it is done inline at the one call.
func (*Store) CreateConstruct ¶
func (*Store) EachSendableRecipient ¶ added in v0.26.0
func (s *Store) EachSendableRecipient(ctx context.Context, audienceID string, visit func(map[string]any) bool) error
EachSendableRecipient follows the engine cursor and refuses a repeated cursor.
func (*Store) FiringStateByID ¶ added in v0.23.6
FiringStateByID reads the rule's count FRESH: from the database, never from this node's result cache. It is read inside the rule's firing gate, and the previous holder's write reaches this node's cache only by an asynchronous broadcast -- a cached answer is the count the previous holder already advanced (firing_gate.go, memql#5431).
The same named read as RuleStateByID, deliberately: freshness belongs to this one call site, and emailRuleById stays cached everywhere else it is read.
func (*Store) MailboxMembers ¶ added in v0.26.0
func (s *Store) MailboxMembers(ctx context.Context, audienceID, address string) (members []map[string]any, complete bool, err error)
MailboxMembers returns every row of the rule's audience that holds the mailbox on the triggering row, in any subscription state, oldest first within each source -- and whether it can be sure that is all of them.
A row is found by the digest of the address (a read of the audience's rows that carry it) or, for the rows that carry no digest at all, by comparing its address (a page of them). Each row's OWN address is compared before the row is believed: a digest is a key, and a raw write can set one that is not its row's. This replaces a read that listed only the audience's subscribed members and stopped at the first hundred: it never saw anyone who had unsubscribed (so they were enrolled again as subscribed), and a mailbox beyond the hundred was enrolled a second time.
complete is false when EITHER page is full: there may be an opted-out row beyond it, so the caller must not treat the page as the whole mailbox. The campaigns package can fall back to the roster for rows without a digest; a rule has no roster read, so it refuses the firing and says why.
Every read here decides a write, so the caller's ctx must be a fresh read.
func (*Store) RecipientsForAudience ¶
func (s *Store) RecipientsForAudience(ctx context.Context, audienceID string) ([]map[string]any, error)
RecipientsForAudience reads the roster the marketing lane mails.
func (*Store) RecordDryRun ¶
RecordDryRun writes Gate 2's verdict.
THE GATE IS PASSED BY CONSTRUCTION, and saying why matters. Gate 2 exists to put a behavioural trace and a side-effect manifest in front of a person before an LLM-authored bundle is armed. This bundle was not authored by an LLM: it is one automation with one step, produced by a deterministic template from a form the operator filled in and already saw, and the side effect it can have is exactly the one the form describes. Running a synthetic trace over it would manufacture an artifact nobody reads to satisfy a gate whose question has already been answered -- so the report says plainly what it is, and the operator's approval is the form they saved.
func (*Store) RecordFiring ¶
func (s *Store) RecordFiring(ctx context.Context, ruleID string, firedCount int, firedAt time.Time, lastError string) error
RecordFiring stamps that a rule ran. Its two fields are LIVENESS -- they move on their own -- which is why the surfaces that render them are told not to fingerprint them for arrival cues.
firedAt is both lastFiredAt and the new version's createdAt: the mutation stamps the version at it, which is what lets the fire path place its increment after the version it read (firedAtAfter).
func (*Store) RecordGeneration ¶
func (s *Store) RecordGeneration(ctx context.Context, ruleID, status, bundleID, constructName, lastError string) error
RecordGeneration stamps what the engine made of the form. @serverOnly, so the caller must have stamped internal origin -- see activate.go, where it is done inline at the one Execute that needs it.
func (*Store) RecordValidation ¶
func (s *Store) RecordValidation(ctx context.Context, bundleID string, ok bool, diagnostics []string, failureReason string) error
RecordValidation writes Gate 1's verdict. The report is an object literal rather than a quoted blob: `validationReport` is declared `object!`, and handing it a string would be a type the mutation does not accept.
func (*Store) RuleByID ¶
RuleByID reads one rule under the caller's own actor. That read IS the authorization on every path in this package: a rule the caller cannot read is a rule they cannot activate, retire or fire.
func (*Store) RuleOfBundle ¶ added in v0.26.0
func (s *Store) RuleOfBundle(ctx context.Context, bundleID string) (ruleID, status string, ok bool, err error)
RuleOfBundle names the rule a generated bundle was built for, and the bundle's status, read under the caller's own actor. The bundle's dry-run report records the rule (RecordDryRun), and the bundle id is derived from the bundle's owner, that rule and the bundle's version (bundleIDFor) -- so the id is re-derived from what the row says, and a bundle whose row names a rule that does not derive it names none. A bundle armed before ids carried their owner is derived without it (legacyBundleIDFor), and is the rule's too.
func (*Store) RuleStateByID ¶
RuleStateByID returns the fields the generator deliberately does not read -- the engine's own account of what it last made of the form.
func (*Store) SendToRecipient ¶
func (s *Store) SendToRecipient(ctx context.Context, templateID, recipientID, senderIdentityID, ruleID, requestID string) (map[string]any, error)
SendToRecipient is the MARKETING lane's one call: suppression checked at the point of send, the RFC 8058 pair attached, the resolved sending identity applied, the outcome ledgered. Reached as a builtin rather than as a Go call so this package does not depend on component/campaigns' internals.
A kind-prefixed `builtin <name>(...)` string IS executable through the engine, and the precedent is shipped: dsl/memql/tools.memql declares `@handler(type="query", query="builtin help(name: args.name)")`, and a query handler renders exactly such a call from its arguments' values and hands it to Execute (component/memql/tool_handler_v1.go).
func (*Store) SenderIdentityByID ¶
func (s *Store) SenderIdentityByID(ctx context.Context, identityID string) (map[string]any, bool, error)
SenderIdentityByID reads the identity a rule sends as.
func (*Store) StageOperationalEmail ¶ added in v0.26.0
func (s *Store) StageOperationalEmail(ctx context.Context, requestID, templateID, to, displayName, dedupeKey, ruleID string) error
StageOperationalEmail renders the active rule's published template and stages it in the server outbox.
func (*Store) TemplateByID ¶
TemplateByID reads the copy both lanes send.