Skip to content

feat: add serialization context to data conversion interfaces#1950

Open
THardy98 wants to merge 11 commits intomainfrom
feature/ts-serialization-context
Open

feat: add serialization context to data conversion interfaces#1950
THardy98 wants to merge 11 commits intomainfrom
feature/ts-serialization-context

Conversation

@THardy98
Copy link
Contributor

@THardy98 THardy98 commented Feb 24, 2026

Summary

Closes: #1661

Add serialization context to the data conversion interfaces (PayloadConverter, FailureConverter, PayloadCodec), enabling converters to vary behavior based on which workflow or activity owns the data being serialized. Brings the TypeScript SDK to parity with the Python SDK's serialization context support.

How it works

Converters can optionally implement withContext(context: SerializationContext) to receive a context-bound copy. If not implemented, the converter is used as-is.

Context is threaded at every serialization boundary:

  • client → server (workflow start, queries, signals, updates, schedule actions, async completion)
  • worker → activity (args, results, failures, heartbeats)
  • workflow isolate (activity commands, child workflows, external signals/cancels).

Seq-based context tracking:

Both the workflow activator (internals.ts) and the WorkflowCodecRunner maintain parallel maps from command sequence number → serialization context. When a command is issued (e.g. scheduleActivity), the context is stored keyed by seq. When the corresponding resolve job arrives (e.g. resolveActivity), the stored context is retrieved, used for deserialization, and deleted. This is necessary because scheduling and resolution happen in separate activation cycles.

What to pay attention to

  • Two parallel seq maps — the activator tracks contexts for the PayloadConverter/FailureConverter (workflow isolate side), while the codec runner tracks contexts for PayloadCodec (worker side). They follow the same lifecycle but operate independently.
  • AsyncCompletionClient.withContext() — the only way to bind context. By-ID and by-token paths without it use the bare converter.

Test plan

  • test-serialization-context.ts — Integration tests using a Tracer type that accumulates context tags at each serialization hop. Full trace arrays are asserted with t.deepEqual to catch false positives. Covers: workflow start, activity, child workflow, query, update, signal, external signal/cancel, schedule actions, async completion (by-id, bound-token, unbound-token).
  • test-workflow-codec-runner.ts — 9 unit tests verifying seq map contents and codec context binding for: activity, local activity, child workflow round-trips, workflow-level commands, signal/cancel independence, context-free fallback, forgetRun.
pnpm run build
pnpm -C packages/test exec ava ./lib/test-workflow-codec-runner.js
pnpm -C packages/test exec ava ./lib/test-serialization-context.js
RUN_INTEGRATION_TESTS=1 pnpm -C packages/test exec ava

🤖 Generated with Claude Code

THardy98 and others added 7 commits February 23, 2026 11:08
Simplify the serialization context plumbing across the SDK:

- AsyncCompletionClient: remove auto-derivation fallback and context
  constructor param; only withContext() sets context
- Activity: collapse duplicate contextDataConverter param into the
  existing dataConverter param
- Worker: remove ActivitySerializationContextInput indirection, derive
  context from ActivityInfo at call site
- WorkflowCodecRunner: remove WeakMap cache, 8 wrapper methods, and
  dead ?? workflowContext fallbacks; inline codecsForContext() at call
  sites using boolean-flag pattern
- Workflow internals: remove auto-derivation fallback in resolveActivity,
  remove unused workflowId parameter from errorToFailure/failureToError,
  remove incorrect self-workflow fallback in signal context
- Workflow handlers: deduplicate context construction (create once, reuse)
- Schedule helpers: simplify context derivation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@THardy98 THardy98 requested a review from a team as a code owner February 24, 2026 07:52
@THardy98 THardy98 force-pushed the feature/ts-serialization-context branch from a02fbf0 to 5d11fee Compare February 24, 2026 08:00
@THardy98 THardy98 marked this pull request as draft February 24, 2026 08:01
@THardy98 THardy98 marked this pull request as ready for review February 24, 2026 17:52
@THardy98 THardy98 marked this pull request as draft February 24, 2026 17:53
@THardy98 THardy98 marked this pull request as ready for review February 24, 2026 18:20
@THardy98
Copy link
Contributor Author

THardy98 commented Feb 25, 2026

Sorry I don't know how to lessen the diff for workflow-codec-runner.ts.

Open to suggestions as the diff looks worse than it actually is

Add integration tests verifying full serialization trace for every
code path: workflow start, activity, child workflow, query, update,
signal, external signal/cancel, schedule actions, and async completion.
Uses a Tracer type that accumulates context tags at each serialization
boundary, with t.deepEqual on full trace arrays to prevent false
positives.
@THardy98 THardy98 force-pushed the feature/ts-serialization-context branch from 53340c9 to 638ce22 Compare February 25, 2026 20:39
Copy link
Member

@chris-olszewski chris-olszewski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Serializing some initial comments

this.options.namespace,
this.options.taskQueue
);
contextDataConverter = withSerializationContext(this.options.loadedDataConverter, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to be concerned about not reaching this line and using the data converter without context in the catch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current behavior should be correct — if we fail, it's because either:

  1. IllegalStateError — we don't have meaningful context to apply
  2. extractActivityInfo threw — we don't have valid info to build context from

In both cases, context-free encoding is the only option since the information needed to construct the context doesn't exist yet

/** Parent workflow type when this activity is associated with a workflow. */
workflowType?: string;
/** Whether the activity is a local activity started from a workflow. */
isLocal: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought: with that information on hand, that means we (i.e. the sdk, not the user), could now skip codecs on local activities's input args, as those are never transferred through the network/persisted to history anyway. That optimization was not possible until now.

return appendTraceStep(decoded, `from:${contextTag(this.context)}`) as T;
}

withContext(context: SerializationContext): PayloadConverter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of disappointing that users must systematically write this function (which will basically always be the same code if they are context aware) and that they must make toPayload and fromPayload (or similar in other types of serializers) deal with the possibility of being called without a context, which we know should never happen if they support context and using a recent sdk.

Copy link
Contributor

@mjameswh mjameswh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't merge yet. I want to discuss DX a bit more before committing to this API.

@mjameswh
Copy link
Contributor

I'll continue review another day. Please hold.

- Add discriminant field (type: 'workflow' | 'activity') to context types
  for clean union narrowing without 'isLocal' in context checks
- Add optional workflowType to WorkflowSerializationContext, populated
  where available
- Simplify conditional spread patterns in worker.ts to direct assignment
- Extract contextDataConverter getter in AsyncCompletionClient
- Fix type-only import in workflow.ts
- Move test assertions outside worker.runUntil
- Add JSDoc on withSerializationContext (worker-only, === optimization)
- Replace unit tests with integration coverage: remove 10 implementation-
  coupled unit tests, keep forgetRun leak guard, add local activity
  integration test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@THardy98
Copy link
Contributor Author

THardy98 commented Mar 6, 2026

Addressed PR feedback and reduced the test code (removed unit tests, not enough signal for them to be worth keeping)

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.

[Feature Request] Serialization context for codecs and converters

3 participants