Skip to content

TypeScript SDK: add the Join helper for parallel branches - #1552

Merged
longquanzheng merged 11 commits into
mainfrom
feat/join-helper-typescript
Nov 11, 2026
Merged

longquanzheng merged 11 commits into
mainfrom
feat/join-helper-typescript

Conversation

@longquanzheng

Copy link
Copy Markdown
Contributor

Summary

Adds the Join helper to the TypeScript SDK: a join construct for parallel branches, built only on the existing ChannelMap. No server, IDL, or persistent-format change besides the JoinRound JSON value.

  • Join<T> (listed in PersistenceSchema.channels, registered as its inner ChannelMap): fork / forkWithRoundKey start the branches and the join Step with one JoinRound; branches report with endBranch or reportBranchResult; the join waits with forAllBranches, forFirstBranch, or forBranches; the next round resets with forRemainingBranches; results are read with branchResults / remainingBranchResults.
  • JoinRound { roundKey, branchCount } and joinRoundCodec, whose JSON {"roundKey","branchCount"} is pinned by a contract test; decoding validates both fields and ignores unknown ones.
  • PersistenceSchema.channels widens to (Channel | ChannelMap | Join)[]; code that builds schemas is source compatible.
  • The round key defaults to the split Step's StepExecutionID; forkWithRoundKey takes an explicit key (for RPCs). Forking the same Join twice in one attempt fails. Invalid counts throw when the condition is built (the SDK's existing style), failing the WaitFor; forFirstBranch on a zero-branch round fails (shared rule for all five SDKs).
  • TSDoc with complete, compiling examples (split, branch, join, reset; and an RPC forkWithRoundKey example) and the rules: each branch reports exactly once; receivedCount counts only what the join consumed; a cancelling join's round is fully reset only if an RPC reports the cancelled branches; RPC forks use explicit cancellation. @since 1.41.0. Channel and ChannelMap docs point to Join.
  • Flow Definition Graph TypeScript analyzer: recognizes new Join(...), the movements built by fork / forkWithRoundKey (array literals, .map(...) as ×N), and endBranch as a branch end, with a golden fixture.
  • Changelog entries in docs/reference/typescript/changelog.mdx (1.41) and sdk-typescript/CHANGELOG.md.

Why

The owner approved the Join helper design (decisions D1-D6, 2026-11-10). Without it, an application implementing a join counts branches and round keys itself, so 14 Workflow Patterns are rated +/-; with a documented construct in all five SDKs they become +. This is the TypeScript part; Java is #1549, Rust #1550, Go and Python follow. Examples migrate after the SDK release because examples/typescript pins the released SDK.

doc-map: no change needed: SDK API, analyzer, and reference changelog only; docs, skills, and examples change after all five SDKs ship

Testing

  • npm run docs:check, npm run typecheck (Node 22), npm test (221).
  • test/integ/join.integration.test.ts (12, every scenario in the design's test table, including a gate Step proving results were committed before the join waited and an RPC read of the pending late result after sibling cancellation); full run-integration-tests.sh (142).
  • go test -tags=integration ./internal/flowviz/ and ./internal/command/ -run Visualize; make copyright-check; cd docs && npm run check.

Join synchronizes parallel branches through a ChannelMap of branch
results. fork keys a round by the split Step execution ID and counts the
branch builders; forkWithRoundKey takes an explicit key for RPCs.
Branches report with endBranch or reportBranchResult; the join Step
waits with forAllBranches, forFirstBranch, or forBranches; the next
round resets the previous one with forRemainingBranches.

JoinRound is the only new persistent shape: {roundKey, branchCount}.
PersistenceSchema.channels accepts a Join, which registers as its
ChannelMap. Adds the JoinRound contract test and integration tests.
The TypeScript analyzer now treats new Join(...) as a ChannelMap
resource, reads the branch and join movements of Join.fork and
Join.forkWithRoundKey, including through a local variable, and treats
Join.endBranch as a dead-end decision. Adds a join.ts fixture golden.
Make the Join example complete and compilable, document that a
cancelling join's round resets only after substitute results, recommend
explicit cancellation for RPC-forked rounds, and state that requeueing
consumed results on cancellation depends on the server version.

The all-branches test now starts the join Step only after both results
commit, and the first-branch test reads the pending results through an
RPC while the run waits.
…tBranch on empty rounds

The forkWithRoundKey example is now a complete, compilable Flow.
forFirstBranch on a round without branches fails waitFor, matching the
cross-SDK rule; the invalid-count integration scenario covers it.
Join.fork and forkWithRoundKey builders passed through a local variable
initialized with an array literal or a map call no longer report a
dynamic target. Builders pushed onto the local list count as repeated
branches. The join.ts fixture covers both forms.
Join.channelMap is the registered ChannelMap that carries the branch
results, so applications can load and read a round's pending results
with the ChannelMap API, for example in an RPC. Integration tests read
pending results through it; a contract test pins that it is the
registered definition.
A push adds Join branches only when it targets the same binding as the
fork's list and runs before the fork on its path. Pushes after the fork,
in the other branch of an if, or on a shadowing variable are ignored.
A conditional push or another use of the list reports a dynamic target.
Only pushes in loops or iteration callbacks are marked as repeated.
Only Array iteration callbacks count as loops; a push in any other
callback is undetermined and reports a dynamic target. Non-push uses of
a builder list follow the same position and path rule as pushes, so
uses after the fork are ignored. A movement list spread into goToMany
uses the same classification, so a single push is no longer repeated.
The next shared SDK release is 1.43.0, so the Join version markers move
from 1.41.0 to 1.43.0.
A movement list spread into goToMany again yields every pushed target,
including conditional pushes; only a push in a loop or a callback that
may repeat is marked repeated. Array literal elements count once. The
strict undetermined rule stays for Join branch builder lists. Iteration
callbacks count as loops only when their receiver is provably an array.
@codecov

codecov Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.35616% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
sdk-typescript/src/join.ts 98.32% 12 Missing ⚠️

📢 Thoughts on this report? Let us know!

@longquanzheng
longquanzheng merged commit 8aa8e89 into main Nov 11, 2026
35 checks passed
@longquanzheng
longquanzheng deleted the feat/join-helper-typescript branch November 11, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant