Result type that each branch reports.
Creates a Join definition for a PersistenceSchema.
Durable name of the Join and of its ChannelMap. It must be non-empty, must not
contain /, and must be unique among the Flow's persistence definitions. Changing it while
Flows are running loses their pending results.
Codec of the branch result type. Changing the encoding while Flows are running can make pending results undecodable.
ReadonlychannelThe ChannelMap that carries this Join's branch results, named Join.name.
Use it to read a round's pending results with the existing ChannelMap API. The instance key
of a round is its JoinRound.roundKey. For example, an RPC selects the round with
join.channelMap.loadMessages(round.roundKey) in its load options, then reads
join.channelMap.pendingMessages(context, round.roundKey) or
join.channelMap.size(context, round.roundKey). Pending results are the results that no join
or reset has consumed, such as late results after a first-branch join.
This is the exact definition the Flow registers when its PersistenceSchema lists the Join, so every API that checks for a registered ChannelMap accepts it.
Never publish to it or delete its messages directly. Report results only with Join.endBranch or Join.reportBranchResult. A direct publication or deletion breaks the rule that each branch adds exactly one result per round, so joins and resets count wrong.
class QuoteFlow implements Flow<never> {
// getFlowType, getSteps, and getPersistenceSchema register the Steps and quoteReceived.
@rpc()
public getLateQuotes(context: Context, round: JoinRound): RPCResult<readonly string[]> {
const messages = quoteReceived.channelMap.pendingMessages(context, round.roundKey);
return { output: messages.map((message) => message.value) };
}
}
const lateQuotes = await client.invokeRPCWithOptions(quoteFlow.getLateQuotes, "quote-42", round, {
loadChannelMapInstances: [quoteReceived.channelMap.loadMessages(round.roundKey)],
});
ReadonlycodecCodec of the branch result type. Changing the encoding while Flows are running can make pending results undecodable.
ReadonlynameDurable name of the Join and of its ChannelMap. It must be non-empty, must not
contain /, and must be unique among the Flow's persistence definitions. Changing it while
Flows are running loses their pending results.
Returns the results that the join Step's satisfied Condition consumed, in commit order.
Call it in the join Step's execute. It returns an empty array when the round's Condition
was not the one that made the Wait ready, such as when a Timer won a Wait.anyOf.
The consumed results in the order Dex committed them.
Reports this branch's result and ends the branch.
Return it from the execute of a branch's last Step. It stages the result like
Join.reportBranchResult and returns a dead-end decision. Dex commits the result only
when this execute call succeeds, so a retried attempt does not report twice.
A dead-end decision.
Creates a Condition that waits for the results of every branch of a round.
Return it from the join Step's waitFor. It consumes exactly round.branchCount results.
A round with 0 branches is ready immediately. Combine it with a Timer through Wait.anyOf
when a branch may never report. See Join for an example.
Round that the fork passed to the join Step.
OptionalconditionId: stringOptional stable Condition ID, required inside Wait.anyCombinationOf.
A Channel Condition on the round's ChannelMap instance.
Creates a Condition that waits for count results of a round.
Return it from the join Step's waitFor for a partial join. It consumes exactly count
results in the order Dex committed them. The reset of this round uses count as its
receivedCount. See Join for an example.
Round that the fork passed to the join Step.
Number of results to wait for, from 1 to round.branchCount.
OptionalconditionId: stringOptional stable Condition ID, required inside Wait.anyCombinationOf.
A Channel Condition on the round's ChannelMap instance.
Creates a Condition that waits for the first result of a round.
Return it from the join Step's waitFor to continue with the first branch that reports.
It consumes exactly one result, the earliest one Dex committed. Put the branch's identity in the
result when the join must know which branch won. The reset of this round uses a
receivedCount of 1. See Join for an example.
It behaves like forBranches(round, 1). A round with 0 branches has no first result and would
wait forever, so this method throws for it, which fails the waitFor attempt.
Round that the fork passed to the join Step.
OptionalconditionId: stringOptional stable Condition ID, required inside Wait.anyCombinationOf.
A Channel Condition on the round's ChannelMap instance.
Starts one round from a Step: creates its JoinRound and the branch and join movements.
Call it in the execute of the split Step and return the movements, for example
goToMany(...join.fork(context, branchBuilders, joinBuilder)). Add more movements to the same
decision when needed. The round key is the current Step execution ID, which stays the same
across retries. The branch count is branchBuilders.length, so the application never counts
branches. An empty list creates a round with 0 branches, whose Join.forAllBranches
Condition is ready immediately.
The fork calls each branch builder in order, then the join builder, and returns their movements in that order. It does not contact Dex. See Join for a complete example.
Context of the current Step waitFor or execute call.
One function per branch. Each receives the round and returns the movement that starts that branch.
Receives the round and returns the movement of the join Step.
The branch movements followed by the join movement.
When context belongs to an RPC, which has no Step execution ID. Use
Join.forkWithRoundKey there.
Starts one round with an application-supplied round key.
Use it in an RPC, which has no Step execution ID, or when the application already has a unique
key per round. It behaves like Join.fork otherwise. Return the movements as the RPC's
nextSteps or in a Step decision.
The key must be unique per round of this Join within the Flow run. Reusing a key mixes results
of different rounds. Steps started by an RPC are siblings of every Step started by any call of
the same RPC, so prefer withCancelingSteps over withCancelingSiblingSteps for these rounds.
When you migrate a hand-written ChannelMap join to a Join with the same name, pass the old
instance key to keep running Flows working.
ChannelMap instance key of the round. It must be non-empty and must not
contain /.
One function per branch. Each receives the round and returns the movement that starts that branch.
Receives the round and returns the movement of the join Step.
The branch movements followed by the join movement.
import {
Join,
StepList,
StepMovement,
Wait,
gracefulComplete,
joinRoundCodec,
rpc,
stringCodec,
type Context,
type Flow,
type JoinRound,
type PersistenceSchema,
type RPCResult,
type Step,
type StepDecision,
} from "@superdurable/dex";
const reviewCompleted = new Join("reviewCompleted", stringCodec);
interface ReviewRequest {
readonly requestId: string;
readonly reviewers: readonly string[];
}
interface ReviewerInput {
readonly reviewer: string;
readonly round: JoinRound;
}
class ReviewByReviewer implements Step<ReviewerInput> {
public getStepType(): string {
return "ReviewByReviewer";
}
public execute(context: Context, input: ReviewerInput): StepDecision {
return reviewCompleted.endBranch(context, input.round, `${input.reviewer}: approved`);
}
}
class CollectReviews implements Step<JoinRound> {
public readonly inputCodec = joinRoundCodec;
public getStepType(): string {
return "CollectReviews";
}
public waitFor(_context: Context, round: JoinRound): Wait {
return Wait.until(reviewCompleted.forAllBranches(round));
}
public execute(context: Context, round: JoinRound): StepDecision {
return gracefulComplete(reviewCompleted.branchResults(context, round));
}
}
export class ReviewFlow implements Flow<never> {
public getFlowType(): string {
return "ReviewFlow";
}
public getSteps() {
return StepList.withoutStartStep().otherSteps(new ReviewByReviewer(), new CollectReviews());
}
public getPersistenceSchema(): PersistenceSchema {
return { channels: [reviewCompleted] };
}
@rpc()
public requestReviews(_context: Context, request: ReviewRequest): RPCResult<void> {
return {
output: undefined,
nextSteps: reviewCompleted.forkWithRoundKey(
`review-${request.requestId}`,
request.reviewers.map((reviewer) => (round: JoinRound) =>
StepMovement.of(ReviewByReviewer, { reviewer, round })),
(round) => StepMovement.of(CollectReviews, round),
),
};
}
}
Creates a Condition that consumes the remaining results of a round, which resets it.
Return it from the waitFor of the next round's split Step, with the previous round from
that Step's input. It waits for the round.branchCount - receivedCount results that the join
did not consume. The next round therefore starts only after every branch of the previous round
has reported, and the previous round leaves no messages behind. When nothing remains, it is
ready immediately. See Join for an example.
A canceled branch never reports, so this Condition never becomes ready for a round whose
branches were canceled. Reset such a round only after an RPC reports a substitute result for
each canceled branch with Join.reportBranchResult. Never add canceled branches to
receivedCount.
Previous round, as the join Step received it.
Number of results the join consumed: the count of forBranches, 1 for
forFirstBranch, or round.branchCount for forAllBranches. It must be from 0 to
round.branchCount.
OptionalconditionId: stringOptional stable Condition ID, required inside Wait.anyCombinationOf.
A Channel Condition on the round's ChannelMap instance.
Returns the late results that Join.forRemainingBranches consumed, in commit order.
Call it in the execute of the Step that waited for the reset. These results arrived after
the join had fired, so they did not affect the join.
The consumed late results in the order Dex committed them.
Reports this branch's result without ending the branch.
Use it when the branch continues after it reports, or from an RPC that reports a substitute result for a branch it canceled. The result is staged with the current handler result and committed only when that handler succeeds. Each branch must report exactly once per round. See Join for an example.
Synchronizes parallel branches: a join Step waits until branches of the same round report results.
Use a Join when one Step starts several branches and a later Step must continue after all of them, the first of them, or any
nof them report a result. A Join is a ChannelMap named Join.name. Each round uses its own ChannelMap instance. Each branch publishes one message: the result valueTitself.Declare a Join once, at module level, and register it in the Flow's
getPersistenceSchema().channels. Its name must not match another Channel, ChannelMap, Join, Attribute, or Stream of the Flow. Dex Web shows it as a ChannelMap.Use it in four places:
goToMany(...join.fork(...)). Join.fork creates one JoinRound and passes it to every branch builder and to the join builder.waitForreturns Join.forAllBranches, Join.forFirstBranch, or Join.forBranches. Itsexecutereads the results with Join.branchResults.waitForreturns Join.forRemainingBranches. The next round then starts only after every branch of the previous round has reported.Rules and traps:
Wait.anyOf(join.forAllBranches(round), Timer.byDuration(...)), when a branch may never report.endBranchorreportBranchResult. It must not wait on a join Condition. Only the join Step that the fork started waits on a round; two waiting Steps would split the results between them.forkthrows there. UseforkWithRoundKeyin an RPC.receivedCountpassed toforRemainingBranchesmust equal the number of results the join consumed: thecountofforBranches, 1 forforFirstBranch, orbranchCountforforAllBranches.withCancelingSiblingSteps(decision, BranchA, BranchB).forkputs every branch and the join Step in one decision, so they are siblings. Sibling cancellation does not reach later Steps of a multi-Step branch; usewithCancelingStepsthere when only one round runs at a time. Steps started by an RPC are siblings of every Step started by any call of that RPC, so for a round forked by Join.forkWithRoundKey in an RPC, cancel the branches withwithCancelingSteps.forRemainingBrancheswaits for results that canceled branches never report. A cancelling join's round therefore resets only when an RPC reports a substitute result for each canceled branch. Without that, do not reset the round; its late results stay until the Flow ends.executecommits, what happens to those results depends on the server version. A server that requeues consumed messages on cancellation returns them to the front of the round's instance, unless the canceling decision closes the Flow unconditionally. An older server drops them, and the round can no longer fire. See the server change log.Example
For a runtime number of branches, map the inputs to branch builders, as the Join.forkWithRoundKey example does.
Since
1.44.0