Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

weak typing for yield results on callActivity #524

Open
Crisfole opened this issue Jul 13, 2023 · 1 comment
Open

weak typing for yield results on callActivity #524

Crisfole opened this issue Jul 13, 2023 · 1 comment
Labels
Enhancement New feature or request P3 Priority 3

Comments

@Crisfole
Copy link

Describe the bug

The choice to use Generators for Durable Functions in Azure was deliberate as I understand it, but it has made strongly typing the APIs for Orchestrations and Activities very challenging. With custom Promise implementations the typing is pretty straightforward, but the replay-ability is probably harder?

I don't think Typescript supports stronger typing of the types on a per-yield result basis (microsoft/TypeScript#32523). Is there a plan for this that will make this process way less painful? I'm usually pretty fantastic at hacking around stuff like this with typescript, but I can't figure out how in this scenario (specifically for activity return types).

Investigative information

  • Durable Functions extension version: ? Installed automagically by tooling. No clue. ?,
  • durable-functions npm module version: `"durable-functions": "^3.0.0-alpha.5"
  • Language (JavaScript/TypeScript) and version: Typescript ^5.1.3
  • Node.js version: node v18.12.1

If deployed to Azure App Service

Local only at this time.

If you don't want to share your Function App name or Functions names on GitHub, please be sure to provide your Invocation ID, Timestamp, and Region - we can use this to look up your Function App/Function. Provide an invocation id per Function. See the Functions Host wiki for more details.

To Reproduce

Check out the ActivityCaller type below. VS Code complains (with good reason) that the Output parameter is unused:

import type { FunctionInput, FunctionOutput, InvocationContext } from "@azure/functions";
import type { OrchestrationContext, ActivityHandler, RetryOptions } from "durable-functions";

import { app } from "durable-functions";

type ActivityExtraOptions = Partial<{
  extraInputs: FunctionInput[];
  extraOutputs: FunctionOutput[];
}>;

type CallOptions = Partial<{
  retry: RetryOptions;
}>;

type Handler<Input, Output> = (input: Input, ctx: InvocationContext) => Promise<Output>;

// LOOK HERE: I cannot type this in any meaningful way that results in a type I can manipulate into
// const out: Output = yield callThisFunction(ctx, "input");
type ActivityCaller<Input, Output> = (ctx: OrchestrationContext, input: Input, { retry }?: CallOptions) => Task;

export function createActivity<Input, Output>(name: string, handler: Handler<Input, Output>): ActivityCaller<Input, Output>;
export function createActivity<Input, Output>(name: string, options: ActivityExtraOptions, handler: Handler<Input, Output>): ActivityCaller<Input, Output>;
export function createActivity<Input, Output>(
  name: string,
  handlerOrOptions: ActivityHandler | ActivityExtraOptions,
  maybeHandler?: ActivityHandler,
): ActivityCaller<Input, Output>  {
  let options = typeof handlerOrOptions === "function" ? {} : handlerOrOptions;
  let handler = typeof handlerOrOptions === "function" ? handlerOrOptions : maybeHandler;
  if (handler == null) {
    throw new Error("Options were passed, but no handler was passed?");
  }

  app.activity(name, {
    ...options,
    handler,
  });

  return Object.defineProperty(
    function (ctx: OrchestrationContext, input: Input, { retry }: CallOptions = {}) {
      if (retry) {
        return ctx.df.callActivityWithRetry(name, retry, input) as Output;
      } else {
        return ctx.df.callActivity(name, input) as Output;
      }
    },
    "name",
    { value: name }
  );
}

Expected behavior

There is some way other than as or type assertions in the orchestrator to bind the return type of a called function to the Task result type.

Actual behavior

There is not such a technique that I know of.

Screenshots

N/A

Known workarounds

as or manually specifying this stuff.

Additional context

N/A

@ghost ghost added the Needs: Triage 🔍 label Jul 13, 2023
@Crisfole
Copy link
Author

Crisfole commented Jul 13, 2023

OK, so I have to take back the "I Can't do any better'. I've added:

type TypedTask<Output> = Task & { result: Output };
export type Yielded<T extends ActivityCallerF<any> | ActivityCallerFx<any, any>> = ReturnType<T> extends TypedTask<infer O> ? O : never;

And I've used as to return those typed tasks from the caller functions:

return ctx.df.callActivityWithRetry(name, retry, input) as TypedTask<Output>;

And in the calling orchestrator I can use them like so:

import { createActivityWithInput, type Yielded } from "../createActivity.js";
import { createOrchestrator } from "../createOrchestrator.js";

export const load = createActivityWithInput<number, string>("demo", async (input, ctx) => {
    return input.toString();
});

export const sync = createOrchestrator("sync-orchestrator", function* (ctx) {
  ctx.log("Syncing");
  const x: Yielded<typeof load> = yield load(ctx, 10);
  ctx.log("Yielded " + JSON.stringify(x) + " as " + typeof x);
});

It behaves as expected, it is still way less than ideal.

@bachuv bachuv added P3 Priority 3 and removed Needs: Triage 🔍 labels Jul 18, 2023
@lilyjma lilyjma added the Enhancement New feature or request label Jan 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement New feature or request P3 Priority 3
Projects
None yet
Development

No branches or pull requests

3 participants