Skip to content

Commit f29aa1e

Browse files
YunchuWangCopilot
andcommitted
fix(durable-functions): thread entity state type through EntityContext<TState>
The v3-compat EntityContext<TState>/EntityHandler<TState> aliases accepted a state type parameter but ignored it, so a bare context.df.getState() returned unknown. Genuine v3 entity code (e.g. the ported BasicNode ClassBasedEntities sample) relies on getState() returning the context's TState, so it failed to compile against the compat package. Make DurableEntityContext generic over TState and default the getState<T = TState> method generic to it, so a bare getState() returns TState | undefined (matching v3) while an explicit getState<X>() still overrides. Because TState only appears as an erased method-generic default, DurableEntityContext<A> and <B> stay structurally identical, so app.entity() registration still accepts specifically typed handlers (verified: full compat unit suite green). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e5be22b-b727-4ccf-8668-60397bb403a0
1 parent 2982f34 commit f29aa1e

3 files changed

Lines changed: 20 additions & 12 deletions

File tree

packages/azure-functions-durable/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ Initial gRPC-consolidated Azure Functions Durable provider, built on `@microsoft
2323
`df` plus replay-safe log helpers (no `invocationId` / `functionName` / `extraInputs`), and the
2424
classic entity context exposes only `{ df }`. Rationale: reading `InvocationContext` members such as
2525
`invocationId` / `extraInputs` inside an orchestrator is replay-nondeterministic and was never
26-
recommended, so they are intentionally not surfaced.
26+
recommended, so they are intentionally not surfaced. The classic entity context stays generic over
27+
its state type (`EntityContext<TState>`), so a bare `context.df.getState()` returns `TState | undefined`
28+
as in v3; the per-call `getState<T>()` generic still overrides it.
2729
- Task result shape follows the core SDK: use `isComplete` / `isFailed` / `getResult()` (v3 used
2830
`isCompleted` / `isFaulted` / `result`). `context.df.createTimer(...)` returns a cancelable
2931
`TimerTask`, so the timeout-race pattern (`Task.any` then `timer.cancel()`) keeps working.

packages/azure-functions-durable/src/entity-context.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { EntityFactory, EntityInstanceId, ITaskEntity, TaskEntityOperation } fro
1212
* written against the legacy `durable-functions` `context.df.*` API run unchanged on the
1313
* gRPC/durabletask engine.
1414
*/
15-
export class DurableEntityContext {
15+
export class DurableEntityContext<TState = unknown> {
1616
private _result: unknown;
1717
private _resultSet = false;
1818
private readonly _isNewlyConstructed: boolean;
@@ -59,9 +59,13 @@ export class DurableEntityContext {
5959
/**
6060
* Gets the current entity state.
6161
*
62+
* @remarks
63+
* When the context is typed `EntityContext<TState>` (v3 style), a bare `getState()` returns
64+
* `TState | undefined`. The per-call type parameter can still override this (e.g. `getState<X>()`).
65+
*
6266
* @param initializer - Optional zero-argument callable providing the initial state when none exists.
6367
*/
64-
getState<T = unknown>(initializer?: () => T): T | undefined {
68+
getState<T = TState>(initializer?: () => T): T | undefined {
6569
const defaultValue = typeof initializer === "function" ? initializer() : undefined;
6670
return this._operation.state.getState<T>(defaultValue);
6771
}
@@ -106,15 +110,17 @@ export class DurableEntityContext {
106110
* Breaking change from `durable-functions` v3, where the entity context extended `InvocationContext`.
107111
* This context exposes only `df`; entity code that read `InvocationContext` members must be updated.
108112
*/
109-
export interface ClassicEntityContext {
110-
df: DurableEntityContext;
113+
export interface ClassicEntityContext<TState = unknown> {
114+
df: DurableEntityContext<TState>;
111115
}
112116

113117
/**
114118
* A classic Durable Functions (v3) entity: a single-argument function that reads and mutates state
115119
* through `context.df.*`.
116120
*/
117-
export type ClassicEntity = (context: ClassicEntityContext) => unknown | Promise<unknown>;
121+
export type ClassicEntity<TState = unknown> = (
122+
context: ClassicEntityContext<TState>,
123+
) => unknown | Promise<unknown>;
118124

119125
/**
120126
* Adapts an entity handler for registration on the core worker.

packages/azure-functions-durable/src/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ export type {
8383
/** v3-compat: client returned by {@link getClient}. */
8484
export type DurableClient = DurableFunctionsClient;
8585
/**
86-
* v3-compat: entity context. `TState` is accepted for source compatibility with the legacy generic
87-
* API; the underlying `df.getState<T>()`/`getInput<T>()` are per-call generic, so the type param is
88-
* intentionally ignored here.
86+
* v3-compat: entity context. When typed `EntityContext<TState>`, a bare `context.df.getState()`
87+
* returns `TState | undefined` (matching the v3 generic entity context). The per-call generics on
88+
* `df.getState<T>()`/`df.getInput<T>()` still allow overriding the type at the call site.
8989
*/
90-
export type EntityContext<_TState = unknown> = ClassicEntityContext;
91-
/** v3-compat: entity handler. `TState` accepted for source compatibility (see {@link EntityContext}). */
92-
export type EntityHandler<_TState = unknown> = ClassicEntity;
90+
export type EntityContext<TState = unknown> = ClassicEntityContext<TState>;
91+
/** v3-compat: entity handler. `TState` threads through to `context.df` (see {@link EntityContext}). */
92+
export type EntityHandler<TState = unknown> = ClassicEntity<TState>;

0 commit comments

Comments
 (0)