Add durable-functions@4.0.0 — Azure Functions Durable provider on the gRPC core (+ core host helpers, E2E CI, and release pipeline)#282
Conversation
Expose low-level protobuf codecs and single work-item execution helpers for Azure Functions Durable JS gRPC consolidation without adding Functions metadata support. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expose stable byte-oriented worker processing methods and endpoint/taskHub client options for Azure Functions Durable JS gRPC consolidation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the work-item-executor extraction and base64 protobuf helpers with a minimal, Python-aligned implementation. processOrchestratorRequest and processEntityBatchRequest now reuse the existing worker execution path via an in-process capturing stub (mirroring durabletask-python PR #155's null-stub pattern) instead of refactoring the worker. Removed the base64 helper module, the object-level execute* methods, the V2 EntityRequest host path, and the related exports and tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a minimal “single work-item” execution surface to the Durable Task JS worker so host integrations (e.g., Azure Functions) can execute one orchestrator/entity batch request per invocation without running the long-lived gRPC streaming loop. This aligns with the SDK’s role as a low-level Durable Task Scheduler (sidecar) client/worker implementation.
Changes:
- Added byte-level helper methods on
TaskHubGrpcWorkerto process serializedOrchestratorRequest/EntityBatchRequestpayloads and return serialized responses. - Introduced an in-process
CapturingSidecarStubthat captures completion payloads instead of sending them over gRPC. - Added end-to-end tests and README documentation for the new host-integration surface.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| README.md | Documents the new low-level host integration APIs and clarifies the non–Durable Functions programming model scope. |
| packages/durabletask-js/test/functions-grpc-support.spec.ts | Adds e2e-style tests covering the new byte processors for orchestrations and entities. |
| packages/durabletask-js/src/worker/task-hub-grpc-worker.ts | Implements the new public byte-processing methods and the capturing sidecar stub. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t glue Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ease pipeline - Add DurableOrchestrationContext/DurableEntityContext (context.df.* adapters) + wrapOrchestrator/wrapEntity - Add RetryOptions, callHttp (throws), parentInstanceId; align newGuid/callSubOrchestrator with v3 signatures - Add v3 client query-return types (DurableOrchestrationStatus, OrchestrationRuntimeStatus, EntityStateResponse, PurgeHistoryResult) - Add client getStatus/getStatusAll/getStatusBy/readEntityState/purgeInstanceHistory/startNew/waitForCompletionOrCreateCheckStatusResponse - Add deprecated DurableOrchestrationClient alias - Wire host-provided maxGrpcMessageSizeInBytes into gRPC channel options (Python parity) - Remove dead CapturingSidecarStub.abandonRequest field (keep required no-op method) - Add durable-functions package to build/release pipelines
Drop rootDir, outDir, include, and exclude since they are inherited from ./tsconfig.json. Keep only the baseUrl + paths override that points @microsoft/durabletask-js imports at the built dist output. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ment call style and update tests
…to DurableFunctionsClient
…m/microsoft/durabletask-js into pr/282/yunchuwang/functions-grpc-support
… Functions - Introduced `EntityId` class for classic v3 entity identifiers. - Enhanced `DurableEntityContext` to include `isNewlyConstructed` and `entityId` properties. - Implemented `signalEntity` method in `DurableEntityContext` for signaling other entities. - Updated `DurableOrchestrationContext` to track and set custom status. - Added unit tests for new features and behaviors.
…ayload in DurableFunctionsClient
Collapse the 4.0.0-alpha.0 changelog to a single bullet and rewrite the README to focus on what the package supports and why it is needed, dropping the implementation-plan / phase-status / open-questions sections. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…onContext type aliases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rator Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… context.log to classic orchestration context Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…3-compatible) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
durable-functions@0.4.0 — Azure Functions Durable provider on the gRPC core (+ core host helpers, E2E CI, and release pipeline)durable-functions@4.0.0 — Azure Functions Durable provider on the gRPC core (+ core host helpers, E2E CI, and release pipeline)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 57 out of 59 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- test/e2e-functions/test-app/package-lock.json: Generated file
Comments suppressed due to low confidence (2)
packages/azure-functions-durable/src/client.ts:540
- When
createHttpManagementPayload(instanceId)is used (no request),getInstanceStatusUrlfalls back tobaseUrl, butbaseUrldefaults to an empty string. That yields relative URLs like/instances/<id>(and then/instances/<id>/terminate?...), which is likely unusable for callers and silently wrong. Consider throwing a clear error whenbaseUrlis missing and the request-less call style is used.
// No request (classic Durable Functions v3 single-argument call): fall back to the base URL
// supplied in the client binding configuration.
const trimmedBaseUrl = baseUrl.replace(/\/+$/, "");
return `${trimmedBaseUrl}/instances/${encodedInstanceId}`;
}
test/e2e-functions/test-app/package.json:5
- The
comment_durable-functionstext says the compat package pins core to a published version that predates the Functions worker helpers, butdurable-functionsdepends on@microsoft/durabletask-js: 0.4.0(the same version that introduces those helpers in this PR). The override is still useful for ensuring the test-app uses the in-repo core build, but the rationale here reads inaccurate and may confuse future maintainers.
- terminate/suspend/resume no-op on terminal instances (v3 HTTP 410 parity) [#14] - readEntityState(entityId, options?: TaskHubOptions) restores v3 signature; rejects unsupported taskHubName/connectionName routing on the gRPC path [#15] - getStatus not-found message restores literal "HTTP 404" substring [#16] - drop dead prebuild/src/version.ts generation [#13] - CHANGELOG: correct removed-exports list (verified vs v3 public surface); document entity-lock removal + core lockEntities/isInCriticalSection migration; document callHttp throwing [#11, #17, #18] Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e5be22b-b727-4ccf-8668-60397bb403a0
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 57 out of 59 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- test/e2e-functions/test-app/package-lock.json: Generated file
Comments suppressed due to low confidence (1)
packages/azure-functions-durable/src/get-client.ts:23
getClientassumescontext.options.extraInputsis always present. If it isundefined(e.g., a function that didn't register any extra inputs), this will throw aTypeErrorbefore you can surface the intended friendly error message.
export function getClient(context: InvocationContext): DurableFunctionsClient {
const clientInput = context.options.extraInputs.find(isDurableClientInput);
if (!clientInput) {
…bot review comments - client.ts: restore the closing brace of getInstanceStatusUrl that an external Copilot Autofix commit dropped (branch no longer compiled); simplify the redundant trailing-slash regex (/\/+$|\/$/ -> /\/+$/). Keep the autofix's baseUrl guard. - get-client.ts: guard context.options.extraInputs with ?. so a missing collection yields the friendly "no durable client input binding" error instead of a raw TypeError [bot #5]; cache one DurableFunctionsClient per distinct binding config so the underlying gRPC channel is reused across invocations instead of leaking a new channel each call [bot #7]. - get-client.spec.ts: isolate the string/object-shape tests with distinct configs and add a caching-contract test (same binding -> same client; different binding -> distinct client). - test-app/package.json: correct the stale @microsoft/durabletask-js override rationale (0.4.0 is the on-branch, not-yet-published core that introduces the worker helpers) [bot #1]. - README.md: point readers to the in-repo durable-functions provider package and clarify the core package only exposes low-level gRPC helpers [bot #3]. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e5be22b-b727-4ccf-8668-60397bb403a0
… exists (v3 parity) DurableEntityContext.getState called the initializer on every invocation, so an expensive or side-effecting initializer ran even when the entity already had state. Durable Functions v3 (Azure/azure-functions-durable-js Entity.getState) only uses the initializer as a fallback when there is no existing state. Use the core state object's hasState flag to short-circuit: return the stored state when present, otherwise fall back to the initializer. Adds a regression test asserting the initializer is not called when state exists. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e5be22b-b727-4ccf-8668-60397bb403a0
…+ evict stopped clients from getClient cache P0 (PR #282 functions-e2e CI RED on node 22.x): terminate of a TERMINAL instance returned 400 instead of the v3 no-op 200. Reproduced locally against the Preview extension bundle and captured the real gRPC codes: terminate-of-terminal surfaces FAILED_PRECONDITION(9) ("...because instance is in the <State> state."), NOT UNKNOWN(2) (suspend/resume) nor NOT_FOUND(5) (a genuinely missing instance). No transport code alone separates terminal / non-terminal-wrong-state / missing, so _mapControlPlaneError now consults getStatus for the control-plane verbs on any of UNKNOWN | NOT_FOUND | FAILED_PRECONDITION and keys the outcome off the instance's actual runtime state: terminal -> no-op (v3 410 parity, 200); non-terminal -> "Cannot <verb> orchestration instance in the <State> state."; missing (getStatus finds nothing) -> "No instance with ID '<id>' found.". raiseEvent (no verb) keeps its NOT_FOUND -> not-found mapping. Bot #B (client.ts): mapped control-plane throws now preserve the original gRPC error as { cause: error }. Bot #A (get-client.ts): getClient's per-binding client cache could hand back a stopped client whose gRPC channel is closed. The cached client's stop() is now wrapped to evict its cache entry, so the next getClient with the same binding builds a fresh client instead of reusing a dead channel. Verified: build 0; compat unit 96/96 (+ terminate FAILED_PRECONDITION no-op test, terminate NOT_FOUND missing test, getClient evict-on-stop test); functions-e2e full suite 35 passed / 0 failed / 10 skipped (the 2 terminate-terminal tests now 200; suspend-resume + external-event unaffected). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e5be22b-b727-4ccf-8668-60397bb403a0
TL;DR for reviewers
This PR began as "add two Functions gRPC helpers to the core worker" (the original title), but it now delivers the full Azure Functions Durable provider on the gRPC core. The headline change is a new npm package
durable-functions@4.0.0(packages/azure-functions-durable/, ~30 source files — does not exist onmain) that re-implements the classic Durable Functions programming model on top of@microsoft/durabletask-js. It is the gRPC-core successor to the classic v3 (extension‑HTTP)durable-functionspackage and targets the@azure/functionsv4 programming model. The core worker helpers, the E2E suite enablement, and the release pipeline are the supporting cast.Scope: 58 files, +4,648 / −523, across four areas:
durable-functions@4.0.0on the gRPC core — v3‑compatible programming modelpackages/azure-functions-durable/**TaskHubGrpcWorkerpackages/durabletask-js/src/worker/task-hub-grpc-worker.ts,src/index.ts.github/workflows/functions-e2e-tests.yaml,test/e2e-functions/**eng/templates/build.yml,eng/ci/release.yml1. New package:
durable-functions@4.0.0(packages/azure-functions-durable/)A v3‑API‑compatible Azure Functions Durable provider built on the gRPC core, so existing v3 orchestrator/activity/entity/client code type‑checks and runs against the consolidated gRPC path. Added whole in this PR (not on
main).app.ts,app-client.ts,trigger.ts,input.ts,worker.ts):app.orchestration/app.activity/app.entity, theapp.client.*durable‑client starter helpers (HTTP / timer / blob / queue / service‑bus / event‑hub / event‑grid / cosmos), and trigger + input bindings.client.ts, 616 lines):DurableFunctionsClient/DurableOrchestrationClient— start, terminate, suspend, resume, raiseEvent, getStatus, purge, rewind, restart. Includes_mapControlPlaneError, which translates opaque gRPC control‑plane errors back to the v3 error surface (see Error fidelity below).orchestration-context.ts,entity-context.ts):DurableOrchestrationContext/DurableEntityContextpluswrapOrchestrator/wrapEntityadapters bridging the classic generator model to the core.orchestration-status.ts,orchestration-filter.ts,metadata.ts,retry-options.ts,entity-id.ts,entity-state-response.ts,purge-history-result.ts,http-management-payload.ts,get-client.ts,durable-grpc.ts,user-agent.ts.TaskFailedErrorfrom core forinstanceofguards; type aliases (DurableClient,EntityContext<T>,OrchestrationHandler, …) so v3 code compiles unchanged.README.md+CHANGELOG.mddocument migration notes and intentional omissions (DurableError/AggregatedError).2. Core host helpers on
TaskHubGrpcWorker(original PR scope)Two methods that let a host drive one durable work item per invocation without the long‑lived gRPC worker loop — mirroring the Python consolidation in durabletask-python #155:
processOrchestratorRequest(request: Uint8Array): Promise<Uint8Array>processEntityBatchRequest(request: Uint8Array): Promise<Uint8Array>Each deserializes a
TaskHubSidecarServiceprotobuf, executes one work item via the worker's existing internal path (_executeOrchestratorInternal/_executeEntityInternal) using an in‑processCapturingSidecarStub(null‑stub pattern, same as Python'sAzureFunctionsNullStub), and returns the serialized response. Transport encoding (e.g. base64) stays in the host, out of core. Covered bypackages/durabletask-js/test/functions-grpc-support.spec.ts.3. E2E suite + CI enablement
The Functions host E2E suite already exists on
main; this PR makes it actually run — and pass — against the new provider:.github/workflows/functions-e2e-tests.yaml): runs onpull_requestfor the Functions surface (test/e2e-functions/**,packages/azure-functions-durable/**, the workflow file) plusworkflow_dispatch; adds--skipApiVersionCheckto Azurite; fixes build ordering; and fixes a latent invalid‑YAML step name that was blocking the workflow entirely (including manual dispatch).test/e2e-functions/test-app/host.json): targets the Preview extension bundle[4.29.0, 5.0.0)(gRPC path) and setsmaxGrpcMessageSizeInBytes: 2147483647to lift the host's default 4 MiB gRPC receive cap (which otherwise fails large‑output / get‑all‑instances). See Cross‑language note.harness.tsexpected‑error strings aligned to the v4 gRPC‑core fidelity;suspend-resume.spec.ts/terminate.spec.tsupdated, with 3 terminal‑instance control‑plane cases skipped pending Functions v4 client: terminal/invalid-state control-plane ops surface opaque2 UNKNOWN(HTTP 400) instead of a meaningful status #315.README.md,test/e2e-functions/README.md,scripts/test-e2e-functions.sh.2 UNKNOWN(HTTP 400) instead of a meaningful status #315 skips + pre‑existing environment/feature skips).4. Build & release pipeline
eng/templates/build.yml: packdurable-functionsand stage theazure-functions-durableartifact.eng/ci/release.yml: newdurable_functionsESRP release job to publish the package.Error fidelity (
_mapControlPlaneError)The consolidated gRPC path collapses control‑plane failures into two opaque codes; the compat layer translates them back to the v3 surface:
NOT_FOUND(5) →No instance with ID '<id>' found.(matches v3).UNKNOWN(2) on a non‑terminal instance → looks up runtime status and throwsCannot <op> orchestration instance in the <State> state.(matches host wording).UNKNOWN(2) on a terminal instance → detail is not recoverable from the opaque error; the v4 status policy (200‑swallow vs 409/410, ideally a server‑side fix) is deferred to Functions v4 client: terminal/invalid-state control-plane ops surface opaque2 UNKNOWN(HTTP 400) instead of a meaningful status #315, so those 3 specs are skipped.Activity/entity failure text now surfaces the v4‑core shape (
TaskFailedError: Activity task #N failed: …,EntityOperationFailedException: …) rather than v3'sActivity function '<name>' failed:. The core error does not carry the function name, and reconstructing the exact v3 string would require a replay‑risky change in the shared core, so the E2E expectations were aligned to the v4‑native shape. (Name‑in‑error is a possible future core enhancement benefiting all SDKs.)Cross‑language note
The 4 MiB gRPC cap is an extension‑defined default (.NET
DurableTaskOptions.MaxGrpcMessageSizeInBytes = 4194304), sent to and honored consistently by every SDK worker. Raising it viamaxGrpcMessageSizeInBytesinhost.jsonis the same knob .NET‑isolated and Python use — no SDK‑side divergence.Follow‑ups
2 UNKNOWN(HTTP 400) instead of a meaningful status #315 — terminal/invalid‑state control‑plane ops return an opaque2 UNKNOWN; decide the v4 status policy (server‑side gRPC status‑detail fix preferred). 3 E2E specs skipped pending this.Testing
npm run test:unit -w durable-functions).functions-grpc-support.spec.tsgreen.npm run build -w durable-functionsclean (core + compat).