Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions packages/durabletask-js/src/worker/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
12 changes: 11 additions & 1 deletion packages/durabletask-js/test/orchestration_executor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
89 changes: 89 additions & 0 deletions packages/durabletask-js/test/registry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
});
});
});
Loading