diff --git a/packages/durabletask-js/src/worker/registry.ts b/packages/durabletask-js/src/worker/registry.ts index 21e1179..9fc5dc3 100644 --- a/packages/durabletask-js/src/worker/registry.ts +++ b/packages/durabletask-js/src/worker/registry.ts @@ -175,10 +175,43 @@ export class Registry { return fn.name; } - const fnStr = fn.toString(); - const start = fnStr.indexOf("function") + "function".length; + const fnStr = fn.toString().trimStart(); + let start = 0; + const isWhitespace = (char: string | undefined) => char !== undefined && char.trim() === ""; + + if (fnStr.startsWith("async")) { + const afterAsync = "async".length; + if (!isWhitespace(fnStr[afterAsync])) { + return ""; + } + + start = afterAsync; + while (isWhitespace(fnStr[start])) { + start++; + } + } + + if (!fnStr.startsWith("function", start)) { + return ""; + } + + start += "function".length; + while (isWhitespace(fnStr[start])) { + start++; + } + + if (fnStr[start] === "*") { + start++; + while (isWhitespace(fnStr[start])) { + start++; + } + } + const end = fnStr.indexOf("(", start); + if (end === -1) { + return ""; + } - return fnStr.slice(start, end).trim() || ""; + return fnStr.slice(start, end).trim(); } } diff --git a/packages/durabletask-js/test/orchestration_executor.spec.ts b/packages/durabletask-js/test/orchestration_executor.spec.ts index 599453f..0f1581f 100644 --- a/packages/durabletask-js/test/orchestration_executor.spec.ts +++ b/packages/durabletask-js/test/orchestration_executor.spec.ts @@ -1130,7 +1130,17 @@ describe("Orchestration Executor", () => { startTime?: Date, ) { const registry = new Registry(); - const name = registry.addOrchestrator(orchestrator); + let name: string; + try { + name = registry.addOrchestrator(orchestrator); + } catch (e: unknown) { + if (!(e instanceof Error) || e.message !== "A non-empty orchestrator name is required.") { + throw e; + } + // Anonymous inline orchestrators in tests: register with a default name + name = "testOrchestrator"; + registry.addNamedOrchestrator(name, orchestrator); + } const allEvents = [ newOrchestratorStartedEvent(startTime), newExecutionStartedEvent(name, TEST_INSTANCE_ID, input !== undefined ? JSON.stringify(input) : undefined), diff --git a/packages/durabletask-js/test/registry.spec.ts b/packages/durabletask-js/test/registry.spec.ts index 748cf81..b0c4230 100644 --- a/packages/durabletask-js/test/registry.spec.ts +++ b/packages/durabletask-js/test/registry.spec.ts @@ -192,4 +192,93 @@ describe("Registry", () => { expect(registry.getEntity("myEntity")).toBe(entityFactory); }); }); + + describe("_getFunctionName", () => { + let registry: Registry; + + beforeEach(() => { + registry = new Registry(); + }); + + it("should return the name of a named function", () => { + function myFunction() {} + expect(registry._getFunctionName(myFunction)).toBe("myFunction"); + }); + + it("should return the name of a named generator function", () => { + function* myGenerator() {} + expect(registry._getFunctionName(myGenerator)).toBe("myGenerator"); + }); + + it("should return the name of a named async generator function", () => { + async function* myAsyncGenerator() { + yield 1; + } + expect(registry._getFunctionName(myAsyncGenerator)).toBe("myAsyncGenerator"); + }); + + it("should return empty string for anonymous generator function (not '*')", () => { + // This is the key bug fix: previously returned "*" for function*() {} + // which would silently register under the name "*" + const result = registry._getFunctionName(Function("return function*() {}")()); + expect(result).toBe(""); + }); + + it("should return empty string for anonymous async generator function (not '*')", () => { + // Previously returned "*" for async function*() {} + const result = registry._getFunctionName(Function("return async function*() {}")()); + expect(result).toBe(""); + }); + + it("should return empty string for anonymous arrow function (not garbage)", () => { + // Arrow functions don't contain "function" keyword, should return "" + // Previously could return garbage from string parsing + const result = registry._getFunctionName(Function("return (x) => x")()); + expect(result).toBe(""); + }); + + it("should return empty string for anonymous async arrow function (not garbage)", () => { + // Previously returned garbage like "x) =>" for async arrow functions + const result = registry._getFunctionName(Function("return async (x) => x")()); + expect(result).toBe(""); + }); + + it("should return empty string for arrow function with nested named function", () => { + const result = registry._getFunctionName(Function("return () => { function inner() {} }")()); + expect(result).toBe(""); + }); + + it("should return empty string for anonymous function expression", () => { + const result = registry._getFunctionName(Function("return function() {}")()); + expect(result).toBe(""); + }); + + it("should extract name from named function expression", () => { + const result = registry._getFunctionName(Function("return function namedExpr() {}")()); + expect(result).toBe("namedExpr"); + }); + + it("should extract name from named generator expression", () => { + const result = registry._getFunctionName(Function("return function* namedGen() {}")()); + expect(result).toBe("namedGen"); + }); + + it("should use fn.name when available (variable-assigned functions)", () => { + // When assigned to a variable, JS engines set fn.name automatically + const myOrchestrator = function* () {}; + expect(registry._getFunctionName(myOrchestrator)).toBe("myOrchestrator"); + }); + + it("addOrchestrator should throw for anonymous generator function", () => { + // With the fix, anonymous generators return "" which triggers the name validation + const anonGen = Function("return function*() {}")(); + expect(() => registry.addOrchestrator(anonGen)).toThrow("A non-empty orchestrator name is required."); + }); + + it("addActivity should throw for anonymous arrow function", () => { + // With the fix, arrow functions return "" which triggers the name validation + const anonArrow = Function("return (x) => x")(); + expect(() => registry.addActivity(anonArrow)).toThrow("A non-empty activity name is required."); + }); + }); });