From 5f826175c2010ece59c15a70fb36c15d08bbf5fc Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 3 Feb 2026 16:20:37 -0800 Subject: [PATCH 1/5] feat: add support for parent orchestration instance in sub-orchestrations - Introduced ParentOrchestrationInstance type to represent details of the parent orchestration. - Updated OrchestrationContext to include a parent property for accessing parent orchestration details. - Enhanced newExecutionStartedEvent function to accept parent instance information. - Modified OrchestrationExecutor to extract and set parent instance info during execution. - Added tests to verify parent orchestration info is correctly handled in sub-orchestrations. - Created FEATURE_PARITY.md to document feature comparison between durabletask-js and durabletask-dotnet. --- docs/FEATURE_PARITY.md | 716 ++++++++++++++++++ packages/durabletask-js/src/index.ts | 1 + .../src/task/context/orchestration-context.ts | 11 + .../parent-orchestration-instance.type.ts | 26 + .../src/utils/pb-helper.util.ts | 15 +- .../src/worker/orchestration-executor.ts | 11 + .../worker/runtime-orchestration-context.ts | 7 + .../parent-orchestration-instance.spec.ts | 253 +++++++ test/e2e-azuremanaged/orchestration.spec.ts | 73 ++ 9 files changed, 1112 insertions(+), 1 deletion(-) create mode 100644 docs/FEATURE_PARITY.md create mode 100644 packages/durabletask-js/src/types/parent-orchestration-instance.type.ts create mode 100644 packages/durabletask-js/test/parent-orchestration-instance.spec.ts diff --git a/docs/FEATURE_PARITY.md b/docs/FEATURE_PARITY.md new file mode 100644 index 0000000..c65e65e --- /dev/null +++ b/docs/FEATURE_PARITY.md @@ -0,0 +1,716 @@ +# Feature Parity: durabletask-js vs durabletask-dotnet + +This document provides a comprehensive comparison between the **durabletask-js** (JavaScript/TypeScript) SDK and the **durabletask-dotnet** (.NET) SDK, highlighting features that are missing or incomplete in the JavaScript implementation. + +> **Last Updated:** February 2026 +> **Comparison Basis:** `main` branches of both repositories + +--- + +## Table of Contents + +- [Summary](#summary) +- [Feature Comparison Matrix](#feature-comparison-matrix) +- [Detailed Feature Analysis](#detailed-feature-analysis) + - [1. Durable Entities](#1-durable-entities) + - [2. Scheduled Tasks](#2-scheduled-tasks) + - [3. Orchestration Context Features](#3-orchestration-context-features) + - [4. Activity Context Features](#4-activity-context-features) + - [5. Client Features](#5-client-features) + - [6. Retry Mechanisms](#6-retry-mechanisms) + - [7. StartOrchestrationOptions](#7-startorchestrationoptions) + - [8. OrchestrationMetadata / OrchestrationState](#8-orchestrationmetadata--orchestrationstate) + - [9. Terminate and Purge Options](#9-terminate-and-purge-options) + - [10. Data Serialization](#10-data-serialization) + - [11. Logging and Diagnostics](#11-logging-and-diagnostics) + - [12. Developer Experience](#12-developer-experience) + - [13. Testing Infrastructure](#13-testing-infrastructure) + - [14. Extensions](#14-extensions) +- [Implementation Priority Recommendations](#implementation-priority-recommendations) + +--- + +## Summary + +| Category | .NET Features | JS Features | Parity % | +|----------|--------------|-------------|----------| +| Core Orchestrations | 15 | 13 | ~87% | +| Durable Entities | 12 | 0 | 0% | +| Scheduled Tasks | 8 | 0 | 0% | +| Client APIs | 18 | 14 | ~78% | +| Retry Mechanisms | 4 | 1 | 25% | +| Developer Tools | 4 | 0 | 0% | +| **Overall** | **61** | **28** | **~46%** | + +--- + +## Feature Comparison Matrix + +### Legend +- ✅ Fully Implemented +- ⚠️ Partially Implemented +- ❌ Not Implemented +- N/A Not Applicable + +| Feature | .NET | JS | Notes | +|---------|------|----|----- | +| **Core Orchestration** | +| Orchestrations | ✅ | ✅ | | +| Activities | ✅ | ✅ | | +| Sub-orchestrations | ✅ | ✅ | | +| Durable Timers | ✅ | ✅ | | +| External Events | ✅ | ✅ | | +| ContinueAsNew | ✅ | ✅ | | +| Custom Status | ✅ | ✅ | | +| SendEvent (orch-to-orch) | ✅ | ✅ | | +| NewGuid (deterministic) | ✅ | ✅ | | +| IsReplaying flag | ✅ | ✅ | | +| CurrentUtcDateTime | ✅ | ✅ | | +| Timer Cancellation | ✅ | ❌ | No CancellationToken support | +| WaitForExternalEvent with Timeout | ✅ | ❌ | | +| Parent Orchestration Info | ✅ | ✅ | Implemented in v0.1.0-alpha.2 | +| Orchestration Version | ✅ | ❌ | | +| Context Properties | ✅ | ❌ | | +| Replay-Safe Logger | ✅ | ❌ | | +| **Durable Entities** | +| Entity Client | ✅ | ❌ | | +| TaskEntity / ITaskEntity | ✅ | ❌ | | +| TaskEntityContext | ✅ | ❌ | | +| SignalEntity | ✅ | ❌ | | +| CallEntity | ✅ | ❌ | | +| LockEntitiesAsync | ✅ | ❌ | | +| InCriticalSection | ✅ | ❌ | | +| EntityInstanceId | ✅ | ❌ | | +| EntityQuery | ✅ | ❌ | | +| CleanEntityStorage | ✅ | ❌ | | +| Entity from Orchestration | ✅ | ❌ | | +| Entity State Management | ✅ | ❌ | | +| **Scheduled Tasks** | +| ScheduledTaskClient | ✅ | ❌ | | +| ScheduleClient | ✅ | ❌ | | +| CreateSchedule | ✅ | ❌ | | +| UpdateSchedule | ✅ | ❌ | | +| DeleteSchedule | ✅ | ❌ | | +| PauseSchedule | ✅ | ❌ | | +| ResumeSchedule | ✅ | ❌ | | +| ListSchedules | ✅ | ❌ | | +| **Client APIs** | +| ScheduleNewOrchestration | ✅ | ✅ | | +| GetOrchestrationState | ✅ | ✅ | | +| WaitForInstanceStart | ✅ | ✅ | | +| WaitForInstanceCompletion | ✅ | ✅ | | +| RaiseEvent | ✅ | ✅ | | +| Terminate | ✅ | ✅ | | +| Suspend/Resume | ✅ | ✅ | | +| Purge Instance | ✅ | ✅ | | +| Query Instances | ✅ | ✅ | | +| List Instance IDs | ✅ | ✅ | | +| Restart Orchestration | ✅ | ✅ | | +| Rewind Instance | ✅ | ✅ | | +| Get Orchestration History | ✅ | ❌ | | +| Recursive Termination | ✅ | ❌ | | +| Recursive Purge | ✅ | ❌ | | +| CancellationToken Support | ✅ | ❌ | | +| Suspend/Resume with Reason | ✅ | ⚠️ | JS has no reason parameter | +| **Retry Mechanisms** | +| Declarative RetryPolicy | ✅ | ✅ | | +| RetryHandler (Custom) | ✅ | ❌ | | +| AsyncRetryHandler | ✅ | ❌ | | +| RetryContext | ✅ | ❌ | | +| HandleFailure Delegate | ✅ | ❌ | | +| **Data Serialization** | +| DataConverter (Abstract) | ✅ | ❌ | JS uses JSON.stringify only | +| JsonDataConverter | ✅ | ❌ | | +| Custom Serializers | ✅ | ❌ | | +| ReadInputAs | ✅ | ❌ | | +| ReadOutputAs | ✅ | ❌ | | +| ReadCustomStatusAs | ✅ | ❌ | | +| **Developer Tools** | +| Source Generators | ✅ | ❌ | | +| Roslyn Analyzers | ✅ | ❌ | | +| In-Process Test Host | ✅ | ❌ | | +| DI Integration | ✅ | ❌ | | +| **Extensions** | +| Azure Blob Payloads | ✅ | ❌ | | +| Export History | ✅ | ❌ | | +| Azure Functions Integration | ✅ | ❌ | Separate package exists | + +--- + +## Detailed Feature Analysis + +### 1. Durable Entities + +**Status: ❌ Not Implemented** + +Durable Entities provide a programming model for managing stateful objects in a distributed environment. This is a major feature gap. + +#### .NET Implementation + +```csharp +// Entity Definition +[DurableTask(nameof(Counter))] +public class Counter : TaskEntity +{ + public void Add(int amount) => this.State += amount; + public void Reset() => this.State = 0; + public int Get() => this.State; +} + +// From Orchestration +await context.Entities.CallEntityAsync( + new EntityInstanceId("Counter", "myCounter"), + "Add", + 5); + +// Entity Locking +await using (await context.Entities.LockEntitiesAsync(entity1, entity2)) +{ + // Critical section with locked entities +} +``` + +#### Missing in JS + +| Component | Description | +|-----------|-------------| +| `DurableEntityClient` | Client for signaling and querying entities | +| `TaskEntity` | Base class for entity implementations | +| `TaskEntityContext` | Context passed to entity operations | +| `EntityInstanceId` | Typed identifier for entities | +| `SignalEntityAsync` | Fire-and-forget entity operations | +| `CallEntityAsync` | Request-response entity operations | +| `LockEntitiesAsync` | Distributed locking via entities | +| `InCriticalSection` | Check if locks are held | +| `GetEntityAsync` | Query entity state | +| `GetAllEntitiesAsync` | Query multiple entities | +| `CleanEntityStorageAsync` | Cleanup orphaned entities | + +--- + +### 2. Scheduled Tasks + +**Status: ❌ Not Implemented** + +Scheduled Tasks provide CRON-based scheduling of orchestrations. + +#### .NET Implementation + +```csharp +// Create a schedule +var scheduleClient = scheduledTaskClient.GetScheduleClient("mySchedule"); +await scheduleClient.CreateAsync(new ScheduleCreationOptions +{ + OrchestrationName = "MyOrchestrator", + Schedule = "0 * * * *", // Every hour + Input = new { foo = "bar" } +}); + +// Manage schedule lifecycle +await scheduleClient.PauseAsync(); +await scheduleClient.ResumeAsync(); +await scheduleClient.DeleteAsync(); +``` + +#### Missing in JS + +| Component | Description | +|-----------|-------------| +| `ScheduledTaskClient` | Client for managing schedules | +| `ScheduleClient` | Handle to individual schedule | +| `ScheduleCreationOptions` | Options for creating schedules | +| `ScheduleDescription` | Metadata about a schedule | +| `ScheduleQuery` | Filter for listing schedules | +| CRUD Operations | Create, Read, Update, Delete schedules | +| Lifecycle Management | Pause, Resume schedules | + +--- + +### 3. Orchestration Context Features + +#### Missing Features + +##### 3.1 Timer Cancellation + +**Status: ❌ Not Implemented** + +```csharp +// .NET - Timer with cancellation +using var cts = new CancellationTokenSource(); +var timerTask = context.CreateTimer(TimeSpan.FromMinutes(5), cts.Token); +var eventTask = context.WaitForExternalEvent("approval"); + +var winner = await Task.WhenAny(timerTask, eventTask); +if (winner == eventTask) +{ + cts.Cancel(); // Cancel the timer +} +``` + +```typescript +// JS - No cancellation support +const timerTask = ctx.createTimer(Date.now() + 5 * 60 * 1000); +const eventTask = ctx.waitForExternalEvent("approval"); +// Cannot cancel timer if event arrives first +``` + +##### 3.2 WaitForExternalEvent with Timeout + +**Status: ❌ Not Implemented** + +```csharp +// .NET - Built-in timeout support +var result = await context.WaitForExternalEvent("approval", TimeSpan.FromMinutes(30)); +``` + +```typescript +// JS - Must implement manually with whenAny +const eventTask = ctx.waitForExternalEvent("approval"); +const timeoutTask = ctx.createTimer(Date.now() + 30 * 60 * 1000); +const winner = yield whenAny([eventTask, timeoutTask]); +``` + +##### 3.3 Parent Orchestration Instance + +**Status: ✅ Implemented (v0.1.0-alpha.2)** + +```csharp +// .NET +public record ParentOrchestrationInstance(TaskName Name, string InstanceId); + +// Access in orchestration +if (context.Parent != null) +{ + logger.LogInformation("Parent: {Name} ({Id})", context.Parent.Name, context.Parent.InstanceId); +} +``` + +```typescript +// JS - Now available! +import { ParentOrchestrationInstance, OrchestrationContext } from "@microsoft/durabletask-js"; + +const myOrchestrator = async (ctx: OrchestrationContext) => { + if (ctx.parent) { + console.log(`Parent: ${ctx.parent.name} (${ctx.parent.instanceId})`); + // ctx.parent.taskScheduledId is also available + } + return "done"; +}; +``` + +##### 3.4 Orchestration Version + +**Status: ❌ Not Implemented** + +```csharp +// .NET +public virtual string Version => string.Empty; + +// Usage +if (context.CompareVersionTo("2.0") < 0) +{ + // Legacy behavior for older versions +} +``` + +##### 3.5 Context Properties + +**Status: ❌ Not Implemented** + +```csharp +// .NET - Extensible properties dictionary +public virtual IReadOnlyDictionary Properties { get; } +``` + +##### 3.6 Replay-Safe Logger + +**Status: ❌ Not Implemented** + +```csharp +// .NET +var logger = context.CreateReplaySafeLogger(); +logger.LogInformation("This only logs when not replaying"); +``` + +```typescript +// JS - Manual check required +if (!ctx.isReplaying) { + console.log("This only logs when not replaying"); +} +``` + +--- + +### 4. Activity Context Features + +#### Missing Features + +| Feature | .NET | JS | Description | +|---------|------|----|----| +| `Name` | ✅ | ❌ | Name of the activity being executed | +| `InstanceId` | ✅ | ⚠️ | JS has `orchestrationId` instead | + +```csharp +// .NET TaskActivityContext +public abstract TaskName Name { get; } +public abstract string InstanceId { get; } +``` + +```typescript +// JS ActivityContext +get orchestrationId(): string; +get taskId(): number; +// Missing: name property +``` + +--- + +### 5. Client Features + +#### Missing Features + +##### 5.1 Get Orchestration History + +**Status: ❌ Not Implemented** + +```csharp +// .NET +IList history = await client.GetOrchestrationHistoryAsync(instanceId); +``` + +##### 5.2 Recursive Termination + +**Status: ❌ Not Implemented** + +```csharp +// .NET +await client.TerminateInstanceAsync(instanceId, new TerminateInstanceOptions +{ + Output = "Terminated by admin", + Recursive = true // Also terminate sub-orchestrations +}); +``` + +```typescript +// JS +await client.terminateOrchestration(instanceId, output); +// Cannot terminate sub-orchestrations recursively +``` + +##### 5.3 Suspend/Resume with Reason + +**Status: ⚠️ Partially Implemented** + +```csharp +// .NET +await client.SuspendInstanceAsync(instanceId, "Maintenance window"); +await client.ResumeInstanceAsync(instanceId, "Maintenance complete"); +``` + +```typescript +// JS - No reason parameter +await client.suspendOrchestration(instanceId); +await client.resumeOrchestration(instanceId); +``` + +##### 5.4 CancellationToken Support + +**Status: ❌ Not Implemented** + +All .NET client methods accept `CancellationToken` for request cancellation. JS has no equivalent (could use `AbortSignal`). + +--- + +### 6. Retry Mechanisms + +#### Missing Features + +##### 6.1 Custom Retry Handler + +**Status: ❌ Not Implemented** + +```csharp +// .NET - Full control over retry logic +public delegate bool RetryHandler(RetryContext retryContext); +public delegate Task AsyncRetryHandler(RetryContext retryContext); + +var options = TaskOptions.FromRetryHandler(ctx => +{ + if (ctx.LastFailure.ErrorType == "TransientError") + { + return ctx.LastAttemptNumber < 5; + } + return false; // Don't retry other errors +}); +``` + +##### 6.2 RetryContext + +**Status: ❌ Not Implemented** + +```csharp +// .NET +public record RetryContext( + TaskOrchestrationContext OrchestrationContext, + int LastAttemptNumber, + TaskFailureDetails LastFailure, + TimeSpan TotalRetryTime, + CancellationToken CancellationToken); +``` + +##### 6.3 HandleFailure Delegate + +**Status: ❌ Not Implemented** + +```csharp +// .NET - Filter retries based on failure details +var policy = new RetryPolicy(maxAttempts: 5, firstRetryInterval: TimeSpan.FromSeconds(1)) +{ + HandleFailure = failure => failure.ErrorType != "FatalError" +}; +``` + +--- + +### 7. StartOrchestrationOptions + +| Property | .NET | JS | Status | +|----------|------|----|----| +| `InstanceId` | ✅ | ✅ | ✅ Implemented | +| `StartAt` | ✅ | ✅ | ✅ Implemented | +| `Version` | ✅ | ❌ | ❌ Not Implemented | +| `Tags` | ✅ | ❌ | ❌ Not Implemented | +| `DedupeStatuses` | ✅ | ❌ | ❌ Not Implemented | + +```csharp +// .NET +public record StartOrchestrationOptions +{ + public string? InstanceId { get; init; } + public DateTimeOffset? StartAt { get; init; } + public TaskVersion? Version { get; init; } + public IReadOnlyDictionary Tags { get; init; } + public IReadOnlyList? DedupeStatuses { get; init; } +} +``` + +```typescript +// JS - Current implementation +interface StartOrchestrationOptions { + instanceId?: string; + startAt?: Date; + // Missing: version, tags, dedupeStatuses +} +``` + +--- + +### 8. OrchestrationMetadata / OrchestrationState + +| Property/Method | .NET | JS | Status | +|----------|------|----|----| +| `InstanceId` | ✅ | ✅ | ✅ | +| `Name` | ✅ | ✅ | ✅ | +| `RuntimeStatus` | ✅ | ✅ | ✅ | +| `CreatedAt` | ✅ | ✅ | ✅ | +| `LastUpdatedAt` | ✅ | ✅ | ✅ | +| `SerializedInput` | ✅ | ✅ | ✅ | +| `SerializedOutput` | ✅ | ✅ | ✅ | +| `SerializedCustomStatus` | ✅ | ✅ | ✅ | +| `FailureDetails` | ✅ | ✅ | ✅ | +| `Tags` | ✅ | ❌ | ❌ Not Implemented | +| `DataConverter` | ✅ | ❌ | ❌ | +| `IsRunning` | ✅ | ❌ | ❌ Helper property | +| `IsCompleted` | ✅ | ❌ | ❌ Helper property | +| `ReadInputAs()` | ✅ | ❌ | ❌ Type-safe deserialization | +| `ReadOutputAs()` | ✅ | ❌ | ❌ Type-safe deserialization | +| `ReadCustomStatusAs()` | ✅ | ❌ | ❌ Type-safe deserialization | +| `raiseIfFailed()` | ❌ | ✅ | JS has this, .NET doesn't | + +--- + +### 9. Terminate and Purge Options + +#### TerminateInstanceOptions + +| Property | .NET | JS | Status | +|----------|------|----|----| +| `Output` | ✅ | ✅ | ✅ | +| `Recursive` | ✅ | ❌ | ❌ Not Implemented | + +#### PurgeInstanceOptions + +| Property | .NET | JS | Status | +|----------|------|----|----| +| `Recursive` | ✅ | ❌ | ❌ Not Implemented | + +--- + +### 10. Data Serialization + +**Status: ❌ Not Implemented** + +.NET provides an extensible data serialization layer that JS lacks. + +```csharp +// .NET - Abstract DataConverter +public abstract class DataConverter +{ + public abstract string? Serialize(object? value); + public abstract object? Deserialize(string? data, Type targetType); + public virtual T? Deserialize(string? data); +} + +// Custom implementation example +public class MessagePackDataConverter : DataConverter +{ + // Custom serialization logic +} +``` + +```typescript +// JS - Hardcoded JSON.stringify/JSON.parse +JSON.stringify(input) +JSON.parse(serializedOutput) +``` + +--- + +### 11. Logging and Diagnostics + +| Feature | .NET | JS | Status | +|----------|------|----|----| +| Logger Interface | ✅ | ✅ | ✅ | +| ConsoleLogger | ✅ | ✅ | ✅ | +| NoOpLogger | ✅ | ✅ | ✅ | +| Replay-Safe Logger | ✅ | ❌ | ❌ | +| ILoggerFactory Integration | ✅ | ❌ | ❌ | +| Structured Logging | ✅ | ⚠️ | Partial | + +--- + +### 12. Developer Experience + +#### Source Generators + +**Status: ❌ Not Implemented** + +.NET has Roslyn source generators that provide: +- Type-safe orchestration/activity invocation methods +- Compile-time validation +- IntelliSense support + +```csharp +// Generated extension method +await context.CallSayHelloAsync("World"); // Type-safe, no string names +``` + +#### Roslyn Analyzers + +**Status: ❌ Not Implemented** + +.NET has analyzers that detect: +- Orchestrator determinism violations +- Incorrect API usage +- Best practice violations + +#### DI Integration + +**Status: ❌ Not Implemented** + +```csharp +// .NET - Full DI support +services.AddDurableTaskClient(); +services.AddDurableTaskWorker(builder => +{ + builder.AddTasks(); +}); +``` + +--- + +### 13. Testing Infrastructure + +| Feature | .NET | JS | Status | +|----------|------|----|----| +| In-Process Test Host | ✅ | ❌ | ❌ | +| Mock Contexts | ✅ | ❌ | ❌ | +| Unit Test Utilities | ✅ | ❌ | ❌ | + +```csharp +// .NET - In-process testing without sidecar +await using var host = new DurableTaskTestHost(); +await host.StartAsync(); + +var client = host.Client; +var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(MyOrchestration)); +``` + +--- + +### 14. Extensions + +#### Azure Blob Payloads + +**Status: ❌ Not Implemented** + +Large message offloading to Azure Blob Storage. + +#### Export History + +**Status: ❌ Not Implemented** + +Export orchestration history to external systems. + +--- + +## Implementation Priority Recommendations + +### Priority 1: High Impact, Common Use Cases + +1. **Tags Support** - Simple to implement, commonly needed for filtering +2. **Parent Orchestration Info** - Important for debugging sub-orchestrations +3. **Timer Cancellation** - Essential for proper resource cleanup +4. **WaitForExternalEvent with Timeout** - Common pattern, error-prone without it +5. **Suspend/Resume with Reason** - Simple enhancement + +### Priority 2: Advanced Scenarios + +1. **Custom Retry Handler** - Enables sophisticated error handling +2. **RetryContext** - Required for custom retry handlers +3. **Activity Name Property** - Minor but useful for logging +4. **Recursive Termination** - Important for managing sub-orchestrations +5. **Version Support** - Important for production deployments + +### Priority 3: Enterprise Features + +1. **Durable Entities** - Major feature for stateful patterns +2. **Scheduled Tasks** - Important for CRON-based workflows +3. **Get Orchestration History** - Useful for debugging +4. **Custom DataConverter** - Enables alternative serialization + +### Priority 4: Developer Experience + +1. **Replay-Safe Logger** - Quality of life improvement +2. **In-Process Test Host** - Enables faster testing +3. **Helper Properties (IsRunning, IsCompleted)** - Convenience methods +4. **Type-safe Deserialization (ReadInputAs)** - TypeScript could benefit + +--- + +## Contributing + +If you'd like to help close these feature gaps, please: + +1. Check existing issues for the feature you want to implement +2. Open a new issue if one doesn't exist +3. Reference this document in your PR + +--- + +## References + +- [durabletask-dotnet Repository](https://github.com/microsoft/durabletask-dotnet) +- [durabletask-js Repository](https://github.com/microsoft/durabletask-js) +- [Durable Task Framework](https://github.com/Azure/durabletask) +- [Azure Durable Functions Documentation](https://docs.microsoft.com/azure/azure-functions/durable/) diff --git a/packages/durabletask-js/src/index.ts b/packages/durabletask-js/src/index.ts index 63337a6..09a1b7d 100644 --- a/packages/durabletask-js/src/index.ts +++ b/packages/durabletask-js/src/index.ts @@ -34,6 +34,7 @@ export { TOrchestrator } from "./types/orchestrator.type"; export { TActivity } from "./types/activity.type"; export { TInput } from "./types/input.type"; export { TOutput } from "./types/output.type"; +export { ParentOrchestrationInstance } from "./types/parent-orchestration-instance.type"; // Logger export { Logger, ConsoleLogger, NoOpLogger } from "./types/logger.type"; diff --git a/packages/durabletask-js/src/task/context/orchestration-context.ts b/packages/durabletask-js/src/task/context/orchestration-context.ts index bca1c4f..ede6ffe 100644 --- a/packages/durabletask-js/src/task/context/orchestration-context.ts +++ b/packages/durabletask-js/src/task/context/orchestration-context.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { ParentOrchestrationInstance } from "../../types/parent-orchestration-instance.type"; import { TActivity } from "../../types/activity.type"; import { TOrchestrator } from "../../types/orchestrator.type"; import { TaskOptions, SubOrchestrationOptions } from "../options"; @@ -17,6 +18,16 @@ export abstract class OrchestrationContext { */ abstract get instanceId(): string; + /** + * Gets the parent orchestration instance, or `undefined` if this is not a sub-orchestration. + * + * This property is useful for determining if the current orchestration was started by another + * orchestration (i.e., it's a sub-orchestration) and for accessing details about the parent. + * + * @returns {ParentOrchestrationInstance | undefined} The parent orchestration details, or `undefined` if this is a top-level orchestration. + */ + abstract get parent(): ParentOrchestrationInstance | undefined; + /** * Get the current date/time as UTC * diff --git a/packages/durabletask-js/src/types/parent-orchestration-instance.type.ts b/packages/durabletask-js/src/types/parent-orchestration-instance.type.ts new file mode 100644 index 0000000..1fa9b5a --- /dev/null +++ b/packages/durabletask-js/src/types/parent-orchestration-instance.type.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Represents the parent orchestration instance details for a sub-orchestration. + * + * This information is available when an orchestration is started as a sub-orchestration + * by another orchestration. It provides details about the parent that started this + * orchestration, which can be useful for debugging and tracing. + */ +export interface ParentOrchestrationInstance { + /** + * The name of the parent orchestration. + */ + name: string; + + /** + * The unique instance ID of the parent orchestration. + */ + instanceId: string; + + /** + * The task scheduled ID that corresponds to this sub-orchestration in the parent's history. + */ + taskScheduledId: number; +} diff --git a/packages/durabletask-js/src/utils/pb-helper.util.ts b/packages/durabletask-js/src/utils/pb-helper.util.ts index 38928c0..aa47980 100644 --- a/packages/durabletask-js/src/utils/pb-helper.util.ts +++ b/packages/durabletask-js/src/utils/pb-helper.util.ts @@ -22,7 +22,7 @@ export function newOrchestratorStartedEvent(timestamp?: Date | null): pb.History return event; } -export function newExecutionStartedEvent(name: string, instanceId: string, encodedInput?: string): pb.HistoryEvent { +export function newExecutionStartedEvent(name: string, instanceId: string, encodedInput?: string, parentInstance?: { name: string; instanceId: string; taskScheduledId: number }): pb.HistoryEvent { const ts = new Timestamp(); const orchestrationInstance = new pb.OrchestrationInstance(); @@ -33,6 +33,19 @@ export function newExecutionStartedEvent(name: string, instanceId: string, encod executionStartedEvent.setInput(getStringValue(encodedInput)); executionStartedEvent.setOrchestrationinstance(orchestrationInstance); + // Set parent instance info if provided (for sub-orchestrations) + if (parentInstance) { + const parentOrchestrationInstance = new pb.OrchestrationInstance(); + parentOrchestrationInstance.setInstanceid(parentInstance.instanceId); + + const parentInstanceInfo = new pb.ParentInstanceInfo(); + parentInstanceInfo.setName(getStringValue(parentInstance.name)); + parentInstanceInfo.setOrchestrationinstance(parentOrchestrationInstance); + parentInstanceInfo.setTaskscheduledid(parentInstance.taskScheduledId); + + executionStartedEvent.setParentinstance(parentInstanceInfo); + } + const event = new pb.HistoryEvent(); event.setEventid(-1); event.setTimestamp(ts); diff --git a/packages/durabletask-js/src/worker/orchestration-executor.ts b/packages/durabletask-js/src/worker/orchestration-executor.ts index f0aa183..182d879 100644 --- a/packages/durabletask-js/src/worker/orchestration-executor.ts +++ b/packages/durabletask-js/src/worker/orchestration-executor.ts @@ -129,6 +129,17 @@ export class OrchestrationExecutor { throw new OrchestratorNotRegisteredError(executionStartedEvent?.getName()); } + // Extract parent instance info if this is a sub-orchestration + const parentInstance = executionStartedEvent?.getParentinstance(); + if (parentInstance) { + const parentOrchestrationInstance = parentInstance.getOrchestrationinstance(); + ctx._parent = { + name: parentInstance.getName()?.getValue() ?? "", + instanceId: parentOrchestrationInstance?.getInstanceid() ?? "", + taskScheduledId: parentInstance.getTaskscheduledid(), + }; + } + // Deserialize the input, if any let input = undefined; diff --git a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts index 7af2435..66a5c47 100644 --- a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts +++ b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts @@ -4,6 +4,7 @@ import { createHash } from "crypto"; import { getName } from "../task"; import { OrchestrationContext } from "../task/context/orchestration-context"; +import { ParentOrchestrationInstance } from "../types/parent-orchestration-instance.type"; import * as pb from "../proto/orchestrator_service_pb"; import * as ph from "../utils/pb-helper.util"; import { CompletableTask } from "../task/completable-task"; @@ -27,6 +28,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { _newGuidCounter: number; _currentUtcDatetime: Date; _instanceId: string; + _parent?: ParentOrchestrationInstance; _completionStatus?: pb.OrchestrationStatus; _receivedEvents: Record; _pendingEvents: Record[]>; @@ -47,6 +49,7 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { this._newGuidCounter = 0; this._currentUtcDatetime = new Date(1000, 0, 1); this._instanceId = instanceId; + this._parent = undefined; this._completionStatus = undefined; this._receivedEvents = {}; this._pendingEvents = {}; @@ -59,6 +62,10 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { return this._instanceId; } + get parent(): ParentOrchestrationInstance | undefined { + return this._parent; + } + get currentUtcDateTime(): Date { return this._currentUtcDatetime; } diff --git a/packages/durabletask-js/test/parent-orchestration-instance.spec.ts b/packages/durabletask-js/test/parent-orchestration-instance.spec.ts new file mode 100644 index 0000000..7d88043 --- /dev/null +++ b/packages/durabletask-js/test/parent-orchestration-instance.spec.ts @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { OrchestrationContext } from "../src/task/context/orchestration-context"; +import { ParentOrchestrationInstance } from "../src/types/parent-orchestration-instance.type"; +import { + newExecutionStartedEvent, + newOrchestratorStartedEvent, +} from "../src/utils/pb-helper.util"; +import { OrchestrationExecutor } from "../src/worker/orchestration-executor"; +import * as pb from "../src/proto/orchestrator_service_pb"; +import { Registry } from "../src/worker/registry"; +import { TOrchestrator } from "../src/types/orchestrator.type"; +import { NoOpLogger } from "../src/types/logger.type"; + +// Use NoOpLogger to suppress log output during tests +const testLogger = new NoOpLogger(); + +const TEST_INSTANCE_ID = "child-instance-123"; +const PARENT_INSTANCE_ID = "parent-instance-456"; +const PARENT_ORCHESTRATOR_NAME = "ParentOrchestrator"; +const PARENT_TASK_SCHEDULED_ID = 5; + +describe("Parent Orchestration Instance", () => { + it("should return undefined for parent when orchestration is not a sub-orchestration", async () => { + let capturedParent: ParentOrchestrationInstance | undefined; + + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + capturedParent = ctx.parent; + return "done"; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + const startTime = new Date(); + const newEvents = [ + newOrchestratorStartedEvent(startTime), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + // Verify the orchestration completed successfully + expect(result.actions).not.toBeNull(); + expect(result.actions.length).toEqual(1); + expect(result.actions[0]?.getCompleteorchestration()?.getOrchestrationstatus()).toEqual( + pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED + ); + + // Verify parent is undefined for top-level orchestration + expect(capturedParent).toBeUndefined(); + }); + + it("should return parent instance info when orchestration is a sub-orchestration", async () => { + let capturedParent: ParentOrchestrationInstance | undefined; + + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + capturedParent = ctx.parent; + return "done"; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + const startTime = new Date(); + const newEvents = [ + newOrchestratorStartedEvent(startTime), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, { + name: PARENT_ORCHESTRATOR_NAME, + instanceId: PARENT_INSTANCE_ID, + taskScheduledId: PARENT_TASK_SCHEDULED_ID, + }), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + // Verify the orchestration completed successfully + expect(result.actions).not.toBeNull(); + expect(result.actions.length).toEqual(1); + expect(result.actions[0]?.getCompleteorchestration()?.getOrchestrationstatus()).toEqual( + pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED + ); + + // Verify parent instance info + expect(capturedParent).toBeDefined(); + expect(capturedParent!.name).toEqual(PARENT_ORCHESTRATOR_NAME); + expect(capturedParent!.instanceId).toEqual(PARENT_INSTANCE_ID); + expect(capturedParent!.taskScheduledId).toEqual(PARENT_TASK_SCHEDULED_ID); + }); + + it("should preserve parent info during replay", async () => { + let capturedParentDuringReplay: ParentOrchestrationInstance | undefined; + let capturedParentAfterReplay: ParentOrchestrationInstance | undefined; + let replayState = true; + + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + if (ctx.isReplaying) { + capturedParentDuringReplay = ctx.parent; + replayState = ctx.isReplaying; + } + + // Create a timer to force replay + yield ctx.createTimer(new Date(ctx.currentUtcDateTime.getTime() + 1000)); + + capturedParentAfterReplay = ctx.parent; + return "done"; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + const startTime = new Date(2020, 0, 1, 12, 0, 0); + const fireAt = new Date(startTime.getTime() + 1000); + + // First execution - create timer + const newEvents1 = [ + newOrchestratorStartedEvent(startTime), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, { + name: PARENT_ORCHESTRATOR_NAME, + instanceId: PARENT_INSTANCE_ID, + taskScheduledId: PARENT_TASK_SCHEDULED_ID, + }), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + await executor.execute(TEST_INSTANCE_ID, [], newEvents1); + + // Second execution - replay with timer fired + const oldEvents = [ + newOrchestratorStartedEvent(startTime), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, { + name: PARENT_ORCHESTRATOR_NAME, + instanceId: PARENT_INSTANCE_ID, + taskScheduledId: PARENT_TASK_SCHEDULED_ID, + }), + ]; + const newEvents2 = [ + newOrchestratorStartedEvent(fireAt), + { ...require("../src/utils/pb-helper.util").newTimerCreatedEvent(1, fireAt) }, + ]; + + // Import timer events + const { newTimerCreatedEvent, newTimerFiredEvent } = require("../src/utils/pb-helper.util"); + + const oldEventsWithTimer = [ + ...oldEvents, + newTimerCreatedEvent(1, fireAt), + ]; + const newEventsWithFired = [ + newTimerFiredEvent(1, fireAt), + ]; + + const executor2 = new OrchestrationExecutor(registry, testLogger); + const result = await executor2.execute(TEST_INSTANCE_ID, oldEventsWithTimer, newEventsWithFired); + + // Verify the orchestration completed successfully + expect(result.actions.length).toEqual(1); + expect(result.actions[0]?.getCompleteorchestration()?.getOrchestrationstatus()).toEqual( + pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED + ); + + // Verify parent info was preserved during replay + expect(capturedParentDuringReplay).toBeDefined(); + expect(capturedParentDuringReplay!.name).toEqual(PARENT_ORCHESTRATOR_NAME); + expect(capturedParentDuringReplay!.instanceId).toEqual(PARENT_INSTANCE_ID); + + // Verify parent info is still available after replay + expect(capturedParentAfterReplay).toBeDefined(); + expect(capturedParentAfterReplay!.name).toEqual(PARENT_ORCHESTRATOR_NAME); + expect(capturedParentAfterReplay!.instanceId).toEqual(PARENT_INSTANCE_ID); + }); + + it("should make parent info available in generator orchestrations", async () => { + let capturedParent: ParentOrchestrationInstance | undefined; + + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + capturedParent = ctx.parent; + return "done"; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + const startTime = new Date(); + const newEvents = [ + newOrchestratorStartedEvent(startTime), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, { + name: PARENT_ORCHESTRATOR_NAME, + instanceId: PARENT_INSTANCE_ID, + taskScheduledId: PARENT_TASK_SCHEDULED_ID, + }), + ]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + // Verify parent instance info is accessible in generator functions + expect(capturedParent).toBeDefined(); + expect(capturedParent!.name).toEqual(PARENT_ORCHESTRATOR_NAME); + expect(capturedParent!.instanceId).toEqual(PARENT_INSTANCE_ID); + expect(capturedParent!.taskScheduledId).toEqual(PARENT_TASK_SCHEDULED_ID); + }); + + it("should return parent info that can be used for debugging/logging", async () => { + let logMessage = ""; + + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + if (ctx.parent) { + logMessage = `Sub-orchestration of ${ctx.parent.name} (${ctx.parent.instanceId})`; + } else { + logMessage = "Top-level orchestration"; + } + return logMessage; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + + // Test with parent + const executor1 = new OrchestrationExecutor(registry, testLogger); + const result1 = await executor1.execute( + TEST_INSTANCE_ID, + [], + [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined, { + name: PARENT_ORCHESTRATOR_NAME, + instanceId: PARENT_INSTANCE_ID, + taskScheduledId: 1, + }), + ] + ); + + expect(result1.actions[0]?.getCompleteorchestration()?.getResult()?.getValue()).toEqual( + `"Sub-orchestration of ${PARENT_ORCHESTRATOR_NAME} (${PARENT_INSTANCE_ID})"` + ); + + // Test without parent + logMessage = ""; // Reset + const executor2 = new OrchestrationExecutor(registry, testLogger); + const result2 = await executor2.execute( + TEST_INSTANCE_ID, + [], + [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined), + ] + ); + + expect(result2.actions[0]?.getCompleteorchestration()?.getResult()?.getValue()).toEqual( + '"Top-level orchestration"' + ); + }); +}); diff --git a/test/e2e-azuremanaged/orchestration.spec.ts b/test/e2e-azuremanaged/orchestration.spec.ts index 9e59a73..6484e94 100644 --- a/test/e2e-azuremanaged/orchestration.spec.ts +++ b/test/e2e-azuremanaged/orchestration.spec.ts @@ -721,4 +721,77 @@ describe("Durable Task Scheduler (DTS) E2E Tests", () => { expect(receiverState?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); expect(receiverState?.serializedOutput).toEqual(JSON.stringify("received signal")); }, 45000); + + it("should expose parent orchestration info in sub-orchestrations", async () => { + // Child orchestration that captures and returns its parent info + const childOrchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + if (ctx.parent) { + return { + hasParent: true, + parentName: ctx.parent.name, + parentInstanceId: ctx.parent.instanceId, + taskScheduledId: ctx.parent.taskScheduledId, + }; + } + return { hasParent: false }; + }; + + // Parent orchestration that calls the child + const parentOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { + // First verify parent is not set for top-level orchestration + const topLevelParentInfo = ctx.parent; + + // Call sub-orchestration + const childResult = yield ctx.callSubOrchestrator(childOrchestrator); + + return { + topLevelHasParent: topLevelParentInfo !== undefined, + childResult, + }; + }; + + taskHubWorker.addOrchestrator(childOrchestrator); + taskHubWorker.addOrchestrator(parentOrchestrator); + await taskHubWorker.start(); + + const parentId = await taskHubClient.scheduleNewOrchestration(parentOrchestrator); + const state = await taskHubClient.waitForOrchestrationCompletion(parentId, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + expect(state?.failureDetails).toBeUndefined(); + + const result = JSON.parse(state?.serializedOutput || "{}"); + + // Verify top-level orchestration has no parent + expect(result.topLevelHasParent).toBe(false); + + // Verify child orchestration received parent info + expect(result.childResult.hasParent).toBe(true); + expect(result.childResult.parentName).toEqual(getName(parentOrchestrator)); + expect(result.childResult.parentInstanceId).toEqual(parentId); + expect(typeof result.childResult.taskScheduledId).toBe("number"); + }, 31000); + + it("should have undefined parent for top-level orchestration started by client", async () => { + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + return { + hasParent: ctx.parent !== undefined, + parentInfo: ctx.parent, + }; + }; + + taskHubWorker.addOrchestrator(orchestrator); + await taskHubWorker.start(); + + const id = await taskHubClient.scheduleNewOrchestration(orchestrator); + const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30); + + expect(state).toBeDefined(); + expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED); + + const result = JSON.parse(state?.serializedOutput || "{}"); + expect(result.hasParent).toBe(false); + expect(result.parentInfo).toBeUndefined(); + }, 31000); }); From cd8468fb7354a3704214b156db297e7dc878e1cb Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 3 Feb 2026 16:25:48 -0800 Subject: [PATCH 2/5] Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../durabletask-js/test/parent-orchestration-instance.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/durabletask-js/test/parent-orchestration-instance.spec.ts b/packages/durabletask-js/test/parent-orchestration-instance.spec.ts index 7d88043..5078b4e 100644 --- a/packages/durabletask-js/test/parent-orchestration-instance.spec.ts +++ b/packages/durabletask-js/test/parent-orchestration-instance.spec.ts @@ -134,10 +134,6 @@ describe("Parent Orchestration Instance", () => { taskScheduledId: PARENT_TASK_SCHEDULED_ID, }), ]; - const newEvents2 = [ - newOrchestratorStartedEvent(fireAt), - { ...require("../src/utils/pb-helper.util").newTimerCreatedEvent(1, fireAt) }, - ]; // Import timer events const { newTimerCreatedEvent, newTimerFiredEvent } = require("../src/utils/pb-helper.util"); From 6148b02dcce26f41bc5ba79b0c8258f57762b674 Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 3 Feb 2026 16:30:26 -0800 Subject: [PATCH 3/5] fix: rename variable for clarity and improve test readability --- package-lock.json | 14 ++++++++++++++ .../test/parent-orchestration-instance.spec.ts | 7 ++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1da2bc..16ba5eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -226,6 +226,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -897,6 +898,7 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -1598,6 +1600,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -1813,6 +1816,7 @@ "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -1974,6 +1978,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -2047,6 +2052,7 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -2288,6 +2294,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2612,6 +2619,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3259,6 +3267,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4174,6 +4183,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -5920,6 +5930,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6644,6 +6655,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6795,6 +6807,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -6881,6 +6894,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/packages/durabletask-js/test/parent-orchestration-instance.spec.ts b/packages/durabletask-js/test/parent-orchestration-instance.spec.ts index 5078b4e..bcb4912 100644 --- a/packages/durabletask-js/test/parent-orchestration-instance.spec.ts +++ b/packages/durabletask-js/test/parent-orchestration-instance.spec.ts @@ -92,12 +92,12 @@ describe("Parent Orchestration Instance", () => { it("should preserve parent info during replay", async () => { let capturedParentDuringReplay: ParentOrchestrationInstance | undefined; let capturedParentAfterReplay: ParentOrchestrationInstance | undefined; - let replayState = true; + let _replayState = true; const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { if (ctx.isReplaying) { capturedParentDuringReplay = ctx.parent; - replayState = ctx.isReplaying; + _replayState = ctx.isReplaying; } // Create a timer to force replay @@ -169,6 +169,7 @@ describe("Parent Orchestration Instance", () => { it("should make parent info available in generator orchestrations", async () => { let capturedParent: ParentOrchestrationInstance | undefined; + // eslint-disable-next-line require-yield const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { capturedParent = ctx.parent; return "done"; @@ -187,7 +188,7 @@ describe("Parent Orchestration Instance", () => { ]; const executor = new OrchestrationExecutor(registry, testLogger); - const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + await executor.execute(TEST_INSTANCE_ID, [], newEvents); // Verify parent instance info is accessible in generator functions expect(capturedParent).toBeDefined(); From 3882589be4ebbd2b1e946fa55d3c25fb810494c7 Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 3 Feb 2026 16:33:18 -0800 Subject: [PATCH 4/5] cleanup --- docs/FEATURE_PARITY.md | 716 ----------------------------------------- 1 file changed, 716 deletions(-) delete mode 100644 docs/FEATURE_PARITY.md diff --git a/docs/FEATURE_PARITY.md b/docs/FEATURE_PARITY.md deleted file mode 100644 index c65e65e..0000000 --- a/docs/FEATURE_PARITY.md +++ /dev/null @@ -1,716 +0,0 @@ -# Feature Parity: durabletask-js vs durabletask-dotnet - -This document provides a comprehensive comparison between the **durabletask-js** (JavaScript/TypeScript) SDK and the **durabletask-dotnet** (.NET) SDK, highlighting features that are missing or incomplete in the JavaScript implementation. - -> **Last Updated:** February 2026 -> **Comparison Basis:** `main` branches of both repositories - ---- - -## Table of Contents - -- [Summary](#summary) -- [Feature Comparison Matrix](#feature-comparison-matrix) -- [Detailed Feature Analysis](#detailed-feature-analysis) - - [1. Durable Entities](#1-durable-entities) - - [2. Scheduled Tasks](#2-scheduled-tasks) - - [3. Orchestration Context Features](#3-orchestration-context-features) - - [4. Activity Context Features](#4-activity-context-features) - - [5. Client Features](#5-client-features) - - [6. Retry Mechanisms](#6-retry-mechanisms) - - [7. StartOrchestrationOptions](#7-startorchestrationoptions) - - [8. OrchestrationMetadata / OrchestrationState](#8-orchestrationmetadata--orchestrationstate) - - [9. Terminate and Purge Options](#9-terminate-and-purge-options) - - [10. Data Serialization](#10-data-serialization) - - [11. Logging and Diagnostics](#11-logging-and-diagnostics) - - [12. Developer Experience](#12-developer-experience) - - [13. Testing Infrastructure](#13-testing-infrastructure) - - [14. Extensions](#14-extensions) -- [Implementation Priority Recommendations](#implementation-priority-recommendations) - ---- - -## Summary - -| Category | .NET Features | JS Features | Parity % | -|----------|--------------|-------------|----------| -| Core Orchestrations | 15 | 13 | ~87% | -| Durable Entities | 12 | 0 | 0% | -| Scheduled Tasks | 8 | 0 | 0% | -| Client APIs | 18 | 14 | ~78% | -| Retry Mechanisms | 4 | 1 | 25% | -| Developer Tools | 4 | 0 | 0% | -| **Overall** | **61** | **28** | **~46%** | - ---- - -## Feature Comparison Matrix - -### Legend -- ✅ Fully Implemented -- ⚠️ Partially Implemented -- ❌ Not Implemented -- N/A Not Applicable - -| Feature | .NET | JS | Notes | -|---------|------|----|----- | -| **Core Orchestration** | -| Orchestrations | ✅ | ✅ | | -| Activities | ✅ | ✅ | | -| Sub-orchestrations | ✅ | ✅ | | -| Durable Timers | ✅ | ✅ | | -| External Events | ✅ | ✅ | | -| ContinueAsNew | ✅ | ✅ | | -| Custom Status | ✅ | ✅ | | -| SendEvent (orch-to-orch) | ✅ | ✅ | | -| NewGuid (deterministic) | ✅ | ✅ | | -| IsReplaying flag | ✅ | ✅ | | -| CurrentUtcDateTime | ✅ | ✅ | | -| Timer Cancellation | ✅ | ❌ | No CancellationToken support | -| WaitForExternalEvent with Timeout | ✅ | ❌ | | -| Parent Orchestration Info | ✅ | ✅ | Implemented in v0.1.0-alpha.2 | -| Orchestration Version | ✅ | ❌ | | -| Context Properties | ✅ | ❌ | | -| Replay-Safe Logger | ✅ | ❌ | | -| **Durable Entities** | -| Entity Client | ✅ | ❌ | | -| TaskEntity / ITaskEntity | ✅ | ❌ | | -| TaskEntityContext | ✅ | ❌ | | -| SignalEntity | ✅ | ❌ | | -| CallEntity | ✅ | ❌ | | -| LockEntitiesAsync | ✅ | ❌ | | -| InCriticalSection | ✅ | ❌ | | -| EntityInstanceId | ✅ | ❌ | | -| EntityQuery | ✅ | ❌ | | -| CleanEntityStorage | ✅ | ❌ | | -| Entity from Orchestration | ✅ | ❌ | | -| Entity State Management | ✅ | ❌ | | -| **Scheduled Tasks** | -| ScheduledTaskClient | ✅ | ❌ | | -| ScheduleClient | ✅ | ❌ | | -| CreateSchedule | ✅ | ❌ | | -| UpdateSchedule | ✅ | ❌ | | -| DeleteSchedule | ✅ | ❌ | | -| PauseSchedule | ✅ | ❌ | | -| ResumeSchedule | ✅ | ❌ | | -| ListSchedules | ✅ | ❌ | | -| **Client APIs** | -| ScheduleNewOrchestration | ✅ | ✅ | | -| GetOrchestrationState | ✅ | ✅ | | -| WaitForInstanceStart | ✅ | ✅ | | -| WaitForInstanceCompletion | ✅ | ✅ | | -| RaiseEvent | ✅ | ✅ | | -| Terminate | ✅ | ✅ | | -| Suspend/Resume | ✅ | ✅ | | -| Purge Instance | ✅ | ✅ | | -| Query Instances | ✅ | ✅ | | -| List Instance IDs | ✅ | ✅ | | -| Restart Orchestration | ✅ | ✅ | | -| Rewind Instance | ✅ | ✅ | | -| Get Orchestration History | ✅ | ❌ | | -| Recursive Termination | ✅ | ❌ | | -| Recursive Purge | ✅ | ❌ | | -| CancellationToken Support | ✅ | ❌ | | -| Suspend/Resume with Reason | ✅ | ⚠️ | JS has no reason parameter | -| **Retry Mechanisms** | -| Declarative RetryPolicy | ✅ | ✅ | | -| RetryHandler (Custom) | ✅ | ❌ | | -| AsyncRetryHandler | ✅ | ❌ | | -| RetryContext | ✅ | ❌ | | -| HandleFailure Delegate | ✅ | ❌ | | -| **Data Serialization** | -| DataConverter (Abstract) | ✅ | ❌ | JS uses JSON.stringify only | -| JsonDataConverter | ✅ | ❌ | | -| Custom Serializers | ✅ | ❌ | | -| ReadInputAs | ✅ | ❌ | | -| ReadOutputAs | ✅ | ❌ | | -| ReadCustomStatusAs | ✅ | ❌ | | -| **Developer Tools** | -| Source Generators | ✅ | ❌ | | -| Roslyn Analyzers | ✅ | ❌ | | -| In-Process Test Host | ✅ | ❌ | | -| DI Integration | ✅ | ❌ | | -| **Extensions** | -| Azure Blob Payloads | ✅ | ❌ | | -| Export History | ✅ | ❌ | | -| Azure Functions Integration | ✅ | ❌ | Separate package exists | - ---- - -## Detailed Feature Analysis - -### 1. Durable Entities - -**Status: ❌ Not Implemented** - -Durable Entities provide a programming model for managing stateful objects in a distributed environment. This is a major feature gap. - -#### .NET Implementation - -```csharp -// Entity Definition -[DurableTask(nameof(Counter))] -public class Counter : TaskEntity -{ - public void Add(int amount) => this.State += amount; - public void Reset() => this.State = 0; - public int Get() => this.State; -} - -// From Orchestration -await context.Entities.CallEntityAsync( - new EntityInstanceId("Counter", "myCounter"), - "Add", - 5); - -// Entity Locking -await using (await context.Entities.LockEntitiesAsync(entity1, entity2)) -{ - // Critical section with locked entities -} -``` - -#### Missing in JS - -| Component | Description | -|-----------|-------------| -| `DurableEntityClient` | Client for signaling and querying entities | -| `TaskEntity` | Base class for entity implementations | -| `TaskEntityContext` | Context passed to entity operations | -| `EntityInstanceId` | Typed identifier for entities | -| `SignalEntityAsync` | Fire-and-forget entity operations | -| `CallEntityAsync` | Request-response entity operations | -| `LockEntitiesAsync` | Distributed locking via entities | -| `InCriticalSection` | Check if locks are held | -| `GetEntityAsync` | Query entity state | -| `GetAllEntitiesAsync` | Query multiple entities | -| `CleanEntityStorageAsync` | Cleanup orphaned entities | - ---- - -### 2. Scheduled Tasks - -**Status: ❌ Not Implemented** - -Scheduled Tasks provide CRON-based scheduling of orchestrations. - -#### .NET Implementation - -```csharp -// Create a schedule -var scheduleClient = scheduledTaskClient.GetScheduleClient("mySchedule"); -await scheduleClient.CreateAsync(new ScheduleCreationOptions -{ - OrchestrationName = "MyOrchestrator", - Schedule = "0 * * * *", // Every hour - Input = new { foo = "bar" } -}); - -// Manage schedule lifecycle -await scheduleClient.PauseAsync(); -await scheduleClient.ResumeAsync(); -await scheduleClient.DeleteAsync(); -``` - -#### Missing in JS - -| Component | Description | -|-----------|-------------| -| `ScheduledTaskClient` | Client for managing schedules | -| `ScheduleClient` | Handle to individual schedule | -| `ScheduleCreationOptions` | Options for creating schedules | -| `ScheduleDescription` | Metadata about a schedule | -| `ScheduleQuery` | Filter for listing schedules | -| CRUD Operations | Create, Read, Update, Delete schedules | -| Lifecycle Management | Pause, Resume schedules | - ---- - -### 3. Orchestration Context Features - -#### Missing Features - -##### 3.1 Timer Cancellation - -**Status: ❌ Not Implemented** - -```csharp -// .NET - Timer with cancellation -using var cts = new CancellationTokenSource(); -var timerTask = context.CreateTimer(TimeSpan.FromMinutes(5), cts.Token); -var eventTask = context.WaitForExternalEvent("approval"); - -var winner = await Task.WhenAny(timerTask, eventTask); -if (winner == eventTask) -{ - cts.Cancel(); // Cancel the timer -} -``` - -```typescript -// JS - No cancellation support -const timerTask = ctx.createTimer(Date.now() + 5 * 60 * 1000); -const eventTask = ctx.waitForExternalEvent("approval"); -// Cannot cancel timer if event arrives first -``` - -##### 3.2 WaitForExternalEvent with Timeout - -**Status: ❌ Not Implemented** - -```csharp -// .NET - Built-in timeout support -var result = await context.WaitForExternalEvent("approval", TimeSpan.FromMinutes(30)); -``` - -```typescript -// JS - Must implement manually with whenAny -const eventTask = ctx.waitForExternalEvent("approval"); -const timeoutTask = ctx.createTimer(Date.now() + 30 * 60 * 1000); -const winner = yield whenAny([eventTask, timeoutTask]); -``` - -##### 3.3 Parent Orchestration Instance - -**Status: ✅ Implemented (v0.1.0-alpha.2)** - -```csharp -// .NET -public record ParentOrchestrationInstance(TaskName Name, string InstanceId); - -// Access in orchestration -if (context.Parent != null) -{ - logger.LogInformation("Parent: {Name} ({Id})", context.Parent.Name, context.Parent.InstanceId); -} -``` - -```typescript -// JS - Now available! -import { ParentOrchestrationInstance, OrchestrationContext } from "@microsoft/durabletask-js"; - -const myOrchestrator = async (ctx: OrchestrationContext) => { - if (ctx.parent) { - console.log(`Parent: ${ctx.parent.name} (${ctx.parent.instanceId})`); - // ctx.parent.taskScheduledId is also available - } - return "done"; -}; -``` - -##### 3.4 Orchestration Version - -**Status: ❌ Not Implemented** - -```csharp -// .NET -public virtual string Version => string.Empty; - -// Usage -if (context.CompareVersionTo("2.0") < 0) -{ - // Legacy behavior for older versions -} -``` - -##### 3.5 Context Properties - -**Status: ❌ Not Implemented** - -```csharp -// .NET - Extensible properties dictionary -public virtual IReadOnlyDictionary Properties { get; } -``` - -##### 3.6 Replay-Safe Logger - -**Status: ❌ Not Implemented** - -```csharp -// .NET -var logger = context.CreateReplaySafeLogger(); -logger.LogInformation("This only logs when not replaying"); -``` - -```typescript -// JS - Manual check required -if (!ctx.isReplaying) { - console.log("This only logs when not replaying"); -} -``` - ---- - -### 4. Activity Context Features - -#### Missing Features - -| Feature | .NET | JS | Description | -|---------|------|----|----| -| `Name` | ✅ | ❌ | Name of the activity being executed | -| `InstanceId` | ✅ | ⚠️ | JS has `orchestrationId` instead | - -```csharp -// .NET TaskActivityContext -public abstract TaskName Name { get; } -public abstract string InstanceId { get; } -``` - -```typescript -// JS ActivityContext -get orchestrationId(): string; -get taskId(): number; -// Missing: name property -``` - ---- - -### 5. Client Features - -#### Missing Features - -##### 5.1 Get Orchestration History - -**Status: ❌ Not Implemented** - -```csharp -// .NET -IList history = await client.GetOrchestrationHistoryAsync(instanceId); -``` - -##### 5.2 Recursive Termination - -**Status: ❌ Not Implemented** - -```csharp -// .NET -await client.TerminateInstanceAsync(instanceId, new TerminateInstanceOptions -{ - Output = "Terminated by admin", - Recursive = true // Also terminate sub-orchestrations -}); -``` - -```typescript -// JS -await client.terminateOrchestration(instanceId, output); -// Cannot terminate sub-orchestrations recursively -``` - -##### 5.3 Suspend/Resume with Reason - -**Status: ⚠️ Partially Implemented** - -```csharp -// .NET -await client.SuspendInstanceAsync(instanceId, "Maintenance window"); -await client.ResumeInstanceAsync(instanceId, "Maintenance complete"); -``` - -```typescript -// JS - No reason parameter -await client.suspendOrchestration(instanceId); -await client.resumeOrchestration(instanceId); -``` - -##### 5.4 CancellationToken Support - -**Status: ❌ Not Implemented** - -All .NET client methods accept `CancellationToken` for request cancellation. JS has no equivalent (could use `AbortSignal`). - ---- - -### 6. Retry Mechanisms - -#### Missing Features - -##### 6.1 Custom Retry Handler - -**Status: ❌ Not Implemented** - -```csharp -// .NET - Full control over retry logic -public delegate bool RetryHandler(RetryContext retryContext); -public delegate Task AsyncRetryHandler(RetryContext retryContext); - -var options = TaskOptions.FromRetryHandler(ctx => -{ - if (ctx.LastFailure.ErrorType == "TransientError") - { - return ctx.LastAttemptNumber < 5; - } - return false; // Don't retry other errors -}); -``` - -##### 6.2 RetryContext - -**Status: ❌ Not Implemented** - -```csharp -// .NET -public record RetryContext( - TaskOrchestrationContext OrchestrationContext, - int LastAttemptNumber, - TaskFailureDetails LastFailure, - TimeSpan TotalRetryTime, - CancellationToken CancellationToken); -``` - -##### 6.3 HandleFailure Delegate - -**Status: ❌ Not Implemented** - -```csharp -// .NET - Filter retries based on failure details -var policy = new RetryPolicy(maxAttempts: 5, firstRetryInterval: TimeSpan.FromSeconds(1)) -{ - HandleFailure = failure => failure.ErrorType != "FatalError" -}; -``` - ---- - -### 7. StartOrchestrationOptions - -| Property | .NET | JS | Status | -|----------|------|----|----| -| `InstanceId` | ✅ | ✅ | ✅ Implemented | -| `StartAt` | ✅ | ✅ | ✅ Implemented | -| `Version` | ✅ | ❌ | ❌ Not Implemented | -| `Tags` | ✅ | ❌ | ❌ Not Implemented | -| `DedupeStatuses` | ✅ | ❌ | ❌ Not Implemented | - -```csharp -// .NET -public record StartOrchestrationOptions -{ - public string? InstanceId { get; init; } - public DateTimeOffset? StartAt { get; init; } - public TaskVersion? Version { get; init; } - public IReadOnlyDictionary Tags { get; init; } - public IReadOnlyList? DedupeStatuses { get; init; } -} -``` - -```typescript -// JS - Current implementation -interface StartOrchestrationOptions { - instanceId?: string; - startAt?: Date; - // Missing: version, tags, dedupeStatuses -} -``` - ---- - -### 8. OrchestrationMetadata / OrchestrationState - -| Property/Method | .NET | JS | Status | -|----------|------|----|----| -| `InstanceId` | ✅ | ✅ | ✅ | -| `Name` | ✅ | ✅ | ✅ | -| `RuntimeStatus` | ✅ | ✅ | ✅ | -| `CreatedAt` | ✅ | ✅ | ✅ | -| `LastUpdatedAt` | ✅ | ✅ | ✅ | -| `SerializedInput` | ✅ | ✅ | ✅ | -| `SerializedOutput` | ✅ | ✅ | ✅ | -| `SerializedCustomStatus` | ✅ | ✅ | ✅ | -| `FailureDetails` | ✅ | ✅ | ✅ | -| `Tags` | ✅ | ❌ | ❌ Not Implemented | -| `DataConverter` | ✅ | ❌ | ❌ | -| `IsRunning` | ✅ | ❌ | ❌ Helper property | -| `IsCompleted` | ✅ | ❌ | ❌ Helper property | -| `ReadInputAs()` | ✅ | ❌ | ❌ Type-safe deserialization | -| `ReadOutputAs()` | ✅ | ❌ | ❌ Type-safe deserialization | -| `ReadCustomStatusAs()` | ✅ | ❌ | ❌ Type-safe deserialization | -| `raiseIfFailed()` | ❌ | ✅ | JS has this, .NET doesn't | - ---- - -### 9. Terminate and Purge Options - -#### TerminateInstanceOptions - -| Property | .NET | JS | Status | -|----------|------|----|----| -| `Output` | ✅ | ✅ | ✅ | -| `Recursive` | ✅ | ❌ | ❌ Not Implemented | - -#### PurgeInstanceOptions - -| Property | .NET | JS | Status | -|----------|------|----|----| -| `Recursive` | ✅ | ❌ | ❌ Not Implemented | - ---- - -### 10. Data Serialization - -**Status: ❌ Not Implemented** - -.NET provides an extensible data serialization layer that JS lacks. - -```csharp -// .NET - Abstract DataConverter -public abstract class DataConverter -{ - public abstract string? Serialize(object? value); - public abstract object? Deserialize(string? data, Type targetType); - public virtual T? Deserialize(string? data); -} - -// Custom implementation example -public class MessagePackDataConverter : DataConverter -{ - // Custom serialization logic -} -``` - -```typescript -// JS - Hardcoded JSON.stringify/JSON.parse -JSON.stringify(input) -JSON.parse(serializedOutput) -``` - ---- - -### 11. Logging and Diagnostics - -| Feature | .NET | JS | Status | -|----------|------|----|----| -| Logger Interface | ✅ | ✅ | ✅ | -| ConsoleLogger | ✅ | ✅ | ✅ | -| NoOpLogger | ✅ | ✅ | ✅ | -| Replay-Safe Logger | ✅ | ❌ | ❌ | -| ILoggerFactory Integration | ✅ | ❌ | ❌ | -| Structured Logging | ✅ | ⚠️ | Partial | - ---- - -### 12. Developer Experience - -#### Source Generators - -**Status: ❌ Not Implemented** - -.NET has Roslyn source generators that provide: -- Type-safe orchestration/activity invocation methods -- Compile-time validation -- IntelliSense support - -```csharp -// Generated extension method -await context.CallSayHelloAsync("World"); // Type-safe, no string names -``` - -#### Roslyn Analyzers - -**Status: ❌ Not Implemented** - -.NET has analyzers that detect: -- Orchestrator determinism violations -- Incorrect API usage -- Best practice violations - -#### DI Integration - -**Status: ❌ Not Implemented** - -```csharp -// .NET - Full DI support -services.AddDurableTaskClient(); -services.AddDurableTaskWorker(builder => -{ - builder.AddTasks(); -}); -``` - ---- - -### 13. Testing Infrastructure - -| Feature | .NET | JS | Status | -|----------|------|----|----| -| In-Process Test Host | ✅ | ❌ | ❌ | -| Mock Contexts | ✅ | ❌ | ❌ | -| Unit Test Utilities | ✅ | ❌ | ❌ | - -```csharp -// .NET - In-process testing without sidecar -await using var host = new DurableTaskTestHost(); -await host.StartAsync(); - -var client = host.Client; -var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(MyOrchestration)); -``` - ---- - -### 14. Extensions - -#### Azure Blob Payloads - -**Status: ❌ Not Implemented** - -Large message offloading to Azure Blob Storage. - -#### Export History - -**Status: ❌ Not Implemented** - -Export orchestration history to external systems. - ---- - -## Implementation Priority Recommendations - -### Priority 1: High Impact, Common Use Cases - -1. **Tags Support** - Simple to implement, commonly needed for filtering -2. **Parent Orchestration Info** - Important for debugging sub-orchestrations -3. **Timer Cancellation** - Essential for proper resource cleanup -4. **WaitForExternalEvent with Timeout** - Common pattern, error-prone without it -5. **Suspend/Resume with Reason** - Simple enhancement - -### Priority 2: Advanced Scenarios - -1. **Custom Retry Handler** - Enables sophisticated error handling -2. **RetryContext** - Required for custom retry handlers -3. **Activity Name Property** - Minor but useful for logging -4. **Recursive Termination** - Important for managing sub-orchestrations -5. **Version Support** - Important for production deployments - -### Priority 3: Enterprise Features - -1. **Durable Entities** - Major feature for stateful patterns -2. **Scheduled Tasks** - Important for CRON-based workflows -3. **Get Orchestration History** - Useful for debugging -4. **Custom DataConverter** - Enables alternative serialization - -### Priority 4: Developer Experience - -1. **Replay-Safe Logger** - Quality of life improvement -2. **In-Process Test Host** - Enables faster testing -3. **Helper Properties (IsRunning, IsCompleted)** - Convenience methods -4. **Type-safe Deserialization (ReadInputAs)** - TypeScript could benefit - ---- - -## Contributing - -If you'd like to help close these feature gaps, please: - -1. Check existing issues for the feature you want to implement -2. Open a new issue if one doesn't exist -3. Reference this document in your PR - ---- - -## References - -- [durabletask-dotnet Repository](https://github.com/microsoft/durabletask-dotnet) -- [durabletask-js Repository](https://github.com/microsoft/durabletask-js) -- [Durable Task Framework](https://github.com/Azure/durabletask) -- [Azure Durable Functions Documentation](https://docs.microsoft.com/azure/azure-functions/durable/) From 1e182db889f2dd97dfeea135443c20be7b85cd47 Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 3 Feb 2026 16:35:32 -0800 Subject: [PATCH 5/5] fix: remove redundant import of timer events in parent orchestration tests --- .../test/parent-orchestration-instance.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/durabletask-js/test/parent-orchestration-instance.spec.ts b/packages/durabletask-js/test/parent-orchestration-instance.spec.ts index bcb4912..bee9048 100644 --- a/packages/durabletask-js/test/parent-orchestration-instance.spec.ts +++ b/packages/durabletask-js/test/parent-orchestration-instance.spec.ts @@ -6,6 +6,8 @@ import { ParentOrchestrationInstance } from "../src/types/parent-orchestration-i import { newExecutionStartedEvent, newOrchestratorStartedEvent, + newTimerCreatedEvent, + newTimerFiredEvent, } from "../src/utils/pb-helper.util"; import { OrchestrationExecutor } from "../src/worker/orchestration-executor"; import * as pb from "../src/proto/orchestrator_service_pb"; @@ -135,9 +137,6 @@ describe("Parent Orchestration Instance", () => { }), ]; - // Import timer events - const { newTimerCreatedEvent, newTimerFiredEvent } = require("../src/utils/pb-helper.util"); - const oldEventsWithTimer = [ ...oldEvents, newTimerCreatedEvent(1, fireAt),