resume
Resumes a stateful saga from its journal using the executor's pre-wired journal configuration.
M9 fix — F003 spec-conformant 2-arg API
The F003 spec declares suspend fun StatefulSagaExecutor<C, R, S>.resume(runId, context). The previous implementation required a 7-arg overload because journal, reducer, terminalDecoder, and terminalEncoder are typed with the payload type P which is not a type parameter of StatefulSagaExecutor.
This implementation delegates to an opaque closure (StatefulSagaExecutorImpl.internalResumeFn) wired by the sagaExecutor(journal, ...) factory. The closure captures P at construction time, so the public API signature remains clean.
For executors built without a journal (no internalResumeFn), this method throws UnsupportedOperationException with a clear message.
Return
ResumeOutcome discriminated by journal state.
Parameters
The run identifier to look up.
The saga context (forwarded to any continuation path).
Resumes a stateful saga from its journal.
This extension is the entry point for F003 saga resume & replay.
Behaviour (T014 scaffold — foundation only)
| Journal state | Outcome |
|---|---|
| No entries for runId | ResumeOutcome.NotFound |
| Seq gap in entries | ResumeOutcome.CorruptJournal |
| Journal read error | ResumeOutcome.CorruptJournal |
| Entries present (all other cases) | throw NotImplementedError until T015–T019 |
The NotImplementedError paths will be replaced by T015 (Terminal short-circuit), T016 (state replay), T017 (effect-key short-circuit), T018/T019 (compensation resume).
Usage
val journal = InMemorySagaJournal<OrderEvent>()
val executor = sagaExecutor<OrderCtx, OrderResult, OrderState, OrderEvent>(
initialState = OrderState.fresh(),
journal = journal,
) {
step("reserve") { completes with "reserved" }
}
when (val outcome = executor.resume(runId, ctx, journal)) {
is ResumeOutcome.Resumed -> process(outcome.result)
is ResumeOutcome.NotFound -> startFresh()
is ResumeOutcome.CorruptJournal -> quarantine(outcome.reason)
is ResumeOutcome.Indeterminate -> alert(outcome.reason)
}Return
ResumeOutcome discriminated by journal state.
Parameters
The run identifier to look up.
The saga context (used in T015+ to continue execution).
The journal to read from.
Resumes a stateless saga from its journal.
State-less variant of StatefulSagaExecutor.resume. Since SagaExecutor carries no saga state (S = Unit), ResumeOutcome.Resumed.state is always Unit and StateReducer is not needed.
Behaviour (T014 scaffold — foundation only)
| Journal state | Outcome |
|---|---|
| No entries for runId | ResumeOutcome.NotFound |
| Seq gap in entries | ResumeOutcome.CorruptJournal |
| Journal read error | ResumeOutcome.CorruptJournal |
| Entries present (all other cases) | throw NotImplementedError until T015–T019 |
Return
ResumeOutcome with S = Unit.
Parameters
The run identifier to look up.
The saga context.
The journal to read from.
Resumes a stateful saga from its journal when state reconstruction and Terminal decoding are required.
Behaviour (T015)
| Journal state | Outcome |
|---|---|
| No entries for runId | ResumeOutcome.NotFound |
| Seq gap in entries | ResumeOutcome.CorruptJournal |
| Journal read error | ResumeOutcome.CorruptJournal |
| StateReducer throws | ResumeOutcome.CorruptJournal with reason = "reducer threw: <msg>" |
| Last entry is Terminal + decoder succeeds + outcome is Committed/Aborted | ResumeOutcome.Resumed |
| Last entry is Terminal + decoded outcome is Indeterminate | ResumeOutcome.Indeterminate (normalization rule — T017 depends on this) |
| Terminal payload cannot be decoded (decoder returns null or throws) | ResumeOutcome.CorruptJournal at terminal entry seq |
| Entries present without Terminal (all other cases) | throw NotImplementedError until T016–T019 |
Two-pass algorithm
State pass: fold reducer over ALL entries (including non-Effect phases — the reducer contract requires handling every ca.acendas.kstate.saga.journal.EntryPhase).
Terminal pass: if the last entry is ca.acendas.kstate.saga.journal.EntryPhase.Terminal, decode its payload via terminalDecoder to extract
(result: R, TerminalOutcome).
Indeterminate normalization (load-bearing for T017)
When the decoded Terminal payload's outcome is TerminalOutcome.Indeterminate, this function returns ResumeOutcome.Indeterminate — NOT Resumed(terminal = Indeterminate). This ensures that the second resume() call on an Indeterminate run returns the same stable shape as the first resume produced.
No-mutation guarantee
This path never writes to the journal. Idempotency is free: calling resume() twice on a Terminal-present journal produces the same outcome.
Intent-without-effectKey (T017)
When the last entry has phase ca.acendas.kstate.saga.journal.EntryPhase.Intent AND its metadata["effect-key"] is absent, the outcome of the side effect cannot be determined. This function:
Replays state via the reducer fold over all entries.
If terminalEncoder is provided, appends a
Terminal(seq=last+1, payload=encoded(Indeterminate))entry to the journal so that subsequent resumes hit T015's Terminal-short-circuit path and return the same stable ResumeOutcome.Indeterminate shape (idempotent re-resume). If terminalEncoder isnull, the journal is left unchanged (pure-read mode).Returns ResumeOutcome.Indeterminate with
reason = "no-effect-key".
Return
ResumeOutcome discriminated by journal state and Terminal payload.
Parameters
The run identifier to look up.
The saga context (forwarded to T016+ continuation — unused on this path).
The journal to read from.
Starting state for the StateReducer fold.
User-supplied fold function — determines the reconstructed S.
Decodes the Terminal entry's payload into (R, TerminalOutcome).
Optional encoder for writing a synthetic Terminal entry on Indeterminate detection. When null, the journal is not written (pure-read).