diff --git a/packages/aws-durable-execution-sdk-js/eslint.config.js b/packages/aws-durable-execution-sdk-js/eslint.config.js deleted file mode 100644 index d98e4a18..00000000 --- a/packages/aws-durable-execution-sdk-js/eslint.config.js +++ /dev/null @@ -1,61 +0,0 @@ -const tsParser = require("@typescript-eslint/parser"); -const typescriptEslint = require("@typescript-eslint/eslint-plugin"); -const filenameConvention = require("eslint-plugin-filename-convention"); -const tsdoc = require("eslint-plugin-tsdoc"); - -module.exports = [ - { - files: ["src/**/*.ts"], - languageOptions: { - parser: tsParser, - ecmaVersion: "latest", - sourceType: "module", - parserOptions: { - tsconfigRootDir: __dirname, - }, - }, - plugins: { - "@typescript-eslint": typescriptEslint, - "filename-convention": filenameConvention, - tsdoc: tsdoc, - }, - rules: { - ...typescriptEslint.configs.recommended.rules, - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/explicit-function-return-type": "warn", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - "no-console": "warn", - "no-debugger": "warn", - "no-duplicate-imports": "error", - "filename-convention/kebab-case": "error", - "tsdoc/syntax": "warn", - }, - }, - { - files: ["src/**/*.test.ts"], - languageOptions: { - parser: tsParser, - ecmaVersion: "latest", - sourceType: "module", - parserOptions: { - tsconfigRootDir: __dirname, - }, - }, - plugins: { - "@typescript-eslint": typescriptEslint, - }, - rules: { - "@typescript-eslint/no-explicit-any": "off", - }, - }, - { - ignores: ["dist/**/*", "node_modules/**/*"], - }, -]; diff --git a/packages/aws-durable-execution-sdk-js/eslint.config.mjs b/packages/aws-durable-execution-sdk-js/eslint.config.mjs new file mode 100644 index 00000000..c0163ba1 --- /dev/null +++ b/packages/aws-durable-execution-sdk-js/eslint.config.mjs @@ -0,0 +1,115 @@ +// @ts-check + +import filenameConvention from "eslint-plugin-filename-convention"; +import eslintConfigPrettier from "eslint-config-prettier/flat"; +import tsdoc from "eslint-plugin-tsdoc"; +import { defineConfig } from "eslint/config"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import js from "@eslint/js"; +import { fileURLToPath } from "node:url"; +import { includeIgnoreFile } from "@eslint/compat"; + +const gitIgnorePath = fileURLToPath(new URL(".gitignore", import.meta.url)); + +export default defineConfig([ + includeIgnoreFile(gitIgnorePath, "Imported .gitignore patterns"), + { + files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], + plugins: { js }, + extends: ["js/recommended"], + }, + { + files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], + languageOptions: { globals: globals.node }, + }, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + parserOptions: { + projectService: { + allowDefaultProject: ["*.mjs"], + }, + tsconfigRootDir: import.meta.dirname, + }, + }, + plugins: { + "filename-convention": filenameConvention, + tsdoc: tsdoc, + }, + rules: { + // Rules that are temporarily set to warning, but should be switched to errors + "@typescript-eslint/restrict-template-expressions": [ + "warn", + { + allowNumber: true, + allowBoolean: true, + }, + ], + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/no-non-null-assertion": "warn", + "@typescript-eslint/no-floating-promises": "warn", + "no-empty": "warn", + "@typescript-eslint/no-unsafe-return": "warn", + "@typescript-eslint/prefer-nullish-coalescing": "warn", + "no-async-promise-executor": "warn", + "@typescript-eslint/no-empty-function": "warn", + "@typescript-eslint/no-misused-promises": "warn", + "@typescript-eslint/only-throw-error": "warn", + "@typescript-eslint/use-unknown-in-catch-callback-variable": "warn", + "@typescript-eslint/no-unsafe-member-access": "warn", + + // Modified rules + "@typescript-eslint/no-confusing-void-expression": [ + "error", + { + ignoreArrowShorthand: true, + }, + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + + // Additional custom rules + "no-console": "error", + "no-debugger": "error", + "no-duplicate-imports": "error", + "filename-convention/kebab-case": "error", + "tsdoc/syntax": "warn", + + // Disabled rules + + // Async functions without await allow throwing async promises still + "@typescript-eslint/require-await": "off", + // Validation of parameters will create "unnecessary" conditions + "@typescript-eslint/no-unnecessary-condition": "off", + }, + }, + { + files: ["src/**/*.test.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/unbound-method": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/restrict-plus-operands": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/only-throw-error": "off", + }, + }, + eslintConfigPrettier, +]); diff --git a/packages/aws-durable-execution-sdk-js/scripts/eslint-plugin-filename-convention/index.js b/packages/aws-durable-execution-sdk-js/scripts/eslint-plugin-filename-convention/index.js index a8b69e12..a3ccf89c 100644 --- a/packages/aws-durable-execution-sdk-js/scripts/eslint-plugin-filename-convention/index.js +++ b/packages/aws-durable-execution-sdk-js/scripts/eslint-plugin-filename-convention/index.js @@ -1,3 +1,5 @@ +// @ts-check + /** * @fileoverview ESLint plugin to enforce kebab-case for file names */ @@ -77,7 +79,8 @@ function findProjectRoot(startDir) { return null; } -module.exports = { +/** @type {import('eslint').ESLint.Plugin} */ +const plugin = { rules: { "kebab-case": { meta: { @@ -87,7 +90,7 @@ module.exports = { category: "Stylistic Issues", recommended: true, }, - fixable: null, + fixable: undefined, schema: [], }, create: function (context) { @@ -95,7 +98,7 @@ module.exports = { let ignorePatterns = defaultIgnorePatterns; // Function to check if a path should be ignored - const shouldIgnorePath = (filePath) => { + const shouldIgnorePath = (/** @type {string} */ filePath) => { // Convert to posix path for consistent pattern matching const posixPath = filePath.split(path.sep).join("/"); @@ -178,3 +181,5 @@ module.exports = { }, }, }; + +module.exports = plugin; diff --git a/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts b/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts index 4c8f80e7..f1f3ee1f 100644 --- a/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts +++ b/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts @@ -51,9 +51,9 @@ import { validateContextUsage } from "../../utils/context-tracker/context-tracke export class DurableContextImpl implements DurableContext { private _stepPrefix?: string; - private _stepCounter: number = 0; + private _stepCounter = 0; private contextLogger: Logger | null; - private modeAwareLoggingEnabled: boolean = true; + private modeAwareLoggingEnabled = true; private runningOperations = new Set(); private operationsEmitter = new EventEmitter(); private checkpoint: ReturnType; @@ -361,7 +361,7 @@ export class DurableContextImpl implements DurableContext { } waitForCallback( - nameOrSubmitter?: string | undefined | WaitForCallbackSubmitterFunc, + nameOrSubmitter?: string | WaitForCallbackSubmitterFunc, submitterOrConfig?: WaitForCallbackSubmitterFunc | WaitForCallbackConfig, maybeConfig?: WaitForCallbackConfig, ): DurablePromise { @@ -376,7 +376,7 @@ export class DurableContextImpl implements DurableContext { this.runInChildContext.bind(this), ); return waitForCallbackHandler( - nameOrSubmitter!, + nameOrSubmitter, submitterOrConfig, maybeConfig, ); @@ -413,7 +413,7 @@ export class DurableContextImpl implements DurableContext { ? waitForConditionHandler( nameOrCheckFunc, checkFuncOrConfig as WaitForConditionCheckFunc, - maybeConfig!, + maybeConfig, ) : waitForConditionHandler( nameOrCheckFunc, diff --git a/packages/aws-durable-execution-sdk-js/src/context/execution-context/execution-context.ts b/packages/aws-durable-execution-sdk-js/src/context/execution-context/execution-context.ts index b3ccf9a7..0a98f1c4 100644 --- a/packages/aws-durable-execution-sdk-js/src/context/execution-context/execution-context.ts +++ b/packages/aws-durable-execution-sdk-js/src/context/execution-context/execution-context.ts @@ -56,16 +56,15 @@ export const initializeExecutionContext = async ( log("📝", "Operations:", operationsArray); - const stepData: Record = operationsArray.reduce( - (acc, operation: Operation) => { - if (operation.Id) { - // The stepData received from backend has Id and ParentId as hash, so no need to hash it again - acc[operation.Id] = operation; - } - return acc; - }, - {} as Record, - ); + const stepData: Record = operationsArray.reduce< + Record + >((acc, operation: Operation) => { + if (operation.Id) { + // The stepData received from backend has Id and ParentId as hash, so no need to hash it again + acc[operation.Id] = operation; + } + return acc; + }, {}); log("📝", "Loaded step data:", stepData); diff --git a/packages/aws-durable-execution-sdk-js/src/errors/unrecoverable-error/unrecoverable-error.test.ts b/packages/aws-durable-execution-sdk-js/src/errors/unrecoverable-error/unrecoverable-error.test.ts index 57226112..6763ed95 100644 --- a/packages/aws-durable-execution-sdk-js/src/errors/unrecoverable-error/unrecoverable-error.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/errors/unrecoverable-error/unrecoverable-error.test.ts @@ -11,26 +11,14 @@ import { TerminationReason } from "../../termination-manager/types"; // Create concrete implementations for testing class TestUnrecoverableError extends UnrecoverableError { readonly terminationReason = TerminationReason.CUSTOM; - - constructor(message: string, originalError?: Error) { - super(message, originalError); - } } class TestExecutionError extends UnrecoverableExecutionError { readonly terminationReason = TerminationReason.CUSTOM; - - constructor(message: string, originalError?: Error) { - super(message, originalError); - } } class TestInvocationError extends UnrecoverableInvocationError { readonly terminationReason = TerminationReason.CUSTOM; - - constructor(message: string, originalError?: Error) { - super(message, originalError); - } } describe("UnrecoverableError", () => { diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/callback-handler/callback.ts b/packages/aws-durable-execution-sdk-js/src/handlers/callback-handler/callback.ts index f57bafe3..e786eb71 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/callback-handler/callback.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/callback-handler/callback.ts @@ -31,7 +31,7 @@ export const createCallback = ( parentId?: string, ) => { return ( - nameOrConfig?: string | undefined | CreateCallbackConfig, + nameOrConfig?: string | CreateCallbackConfig, maybeConfig?: CreateCallbackConfig, ): DurablePromise> => { let name: string | undefined; diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/batch-result.ts b/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/batch-result.ts index 16134af2..8309fcac 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/batch-result.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/batch-result.ts @@ -8,28 +8,28 @@ import { Serdes, SerdesContext } from "../../utils/serdes/serdes"; export class BatchResultImpl implements BatchResult { constructor( - public readonly all: Array>, + public readonly all: BatchItem[], public readonly completionReason: | "ALL_COMPLETED" | "MIN_SUCCESSFUL_REACHED" | "FAILURE_TOLERANCE_EXCEEDED", ) {} - succeeded(): Array & { result: R }> { + succeeded(): (BatchItem & { result: R })[] { return this.all.filter( (item): item is BatchItem & { result: R } => item.status === BatchItemStatus.SUCCEEDED && item.result !== undefined, ); } - failed(): Array & { error: ChildContextError }> { + failed(): (BatchItem & { error: ChildContextError })[] { return this.all.filter( (item): item is BatchItem & { error: ChildContextError } => item.status === BatchItemStatus.FAILED && item.error !== undefined, ); } - started(): Array & { status: BatchItemStatus.STARTED }> { + started(): (BatchItem & { status: BatchItemStatus.STARTED })[] { return this.all.filter( (item): item is BatchItem & { status: BatchItemStatus.STARTED } => item.status === BatchItemStatus.STARTED, @@ -53,11 +53,11 @@ export class BatchResultImpl implements BatchResult { } } - getResults(): Array { + getResults(): R[] { return this.succeeded().map((item) => item.result); } - getErrors(): Array { + getErrors(): ChildContextError[] { return this.failed().map((item) => item.error); } diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/concurrent-execution-handler.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/concurrent-execution-handler.test.ts index e0dcea72..1b17db9c 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/concurrent-execution-handler.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/concurrent-execution-handler.test.ts @@ -742,7 +742,7 @@ describe("ConcurrencyController", () => { const executor = jest.fn(); // Resolve in reverse order - let resolvers: Array<(value: any) => void> = []; + const resolvers: ((value: any) => void)[] = []; mockParentContext.runInChildContext.mockImplementation(() => { return new Promise((resolve) => { resolvers.push(resolve); diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/concurrent-execution-handler.ts b/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/concurrent-execution-handler.ts index 68a18637..a39f673a 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/concurrent-execution-handler.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/concurrent-execution-handler.ts @@ -118,7 +118,7 @@ export class ConcurrencyController { executionContext: ExecutionContext, parentEntityId: string, ): Promise> { - const resultItems: Array> = []; + const resultItems: BatchItem[] = []; log("🔄", `Replaying ${items.length} items sequentially`, { targetTotalCount, @@ -238,9 +238,7 @@ export class ConcurrencyController { config: ConcurrencyConfig, ): Promise> { const maxConcurrency = config.maxConcurrency || Infinity; - const resultItems: Array | undefined> = new Array( - items.length, - ); + const resultItems = new Array | undefined>(items.length); const startedItems = new Set(); let activeCount = 0; @@ -389,9 +387,9 @@ export class ConcurrencyController { // Include all items that were started (have a value in resultItems) // Create shallow copy to prevent mutations from affecting the returned result const finalBatchItems: BatchItem[] = []; - for (let i = 0; i < resultItems.length; i++) { - if (resultItems[i] !== undefined) { - finalBatchItems.push({ ...resultItems[i]! }); + for (const resultItem of resultItems) { + if (resultItem !== undefined) { + finalBatchItems.push({ ...resultItem }); } } @@ -528,7 +526,7 @@ export const createConcurrentExecutionHandler = ( ) { return restoreBatchResult(result); } - return result as BatchResult; + return result; }); }; }; diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/invoke-handler/invoke-handler.ts b/packages/aws-durable-execution-sdk-js/src/handlers/invoke-handler/invoke-handler.ts index 39bc5007..a2dcf535 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/invoke-handler/invoke-handler.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/invoke-handler/invoke-handler.ts @@ -199,7 +199,7 @@ export const createInvokeHandler = ( } // If stepData exists but has an unexpected status, break to avoid infinite loop - if (stepData && stepData.Status !== undefined) { + if (stepData?.Status !== undefined) { throw new InvokeError( `Unexpected operation status: ${stepData.Status}`, ); diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/map-handler/map-handler.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/map-handler/map-handler.test.ts index b4572b2a..b7f880c6 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/map-handler/map-handler.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/map-handler/map-handler.test.ts @@ -258,7 +258,7 @@ describe("Map Handler", () => { for (let i = 0; i < executionItems.length; i++) { const item = executionItems[i]; const mockChildContext = {} as DurableContext; - const result = await (executor as any)(item, mockChildContext); + const result = await executor(item, mockChildContext); results.push({ index: i, result, diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/promise-handler/promise-handler.ts b/packages/aws-durable-execution-sdk-js/src/handlers/promise-handler/promise-handler.ts index 5547a6b9..888384fa 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/promise-handler/promise-handler.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/promise-handler/promise-handler.ts @@ -56,9 +56,7 @@ function createErrorAwareSerdes(): Serdes[]> { data: string | undefined, _context: SerdesContext, ): Promise[] | undefined> => - data !== undefined - ? (restoreErrors(JSON.parse(data)) as PromiseSettledResult[]) - : undefined, + data !== undefined ? restoreErrors(JSON.parse(data)) : undefined, }; } @@ -69,7 +67,6 @@ const stepConfig = { }), }; -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export const createPromiseHandler = (step: DurableContext["step"]) => { const parseParams = ( nameOrPromises: string | undefined | Promise[], diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler.test.ts index e69c3db5..35539565 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler.test.ts @@ -441,7 +441,7 @@ describe("Run In Child Context Handler", () => { describe("runInChildContext with custom serdes", () => { class TestResult { constructor( - public value: string = "", + public value = "", public timestamp: Date = new Date(), ) {} } diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler.test.ts index bba5543d..6ef40bf3 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler.test.ts @@ -839,28 +839,16 @@ describe("Step Handler", () => { // Create a concrete test class for UnrecoverableExecutionError class TestUnrecoverableExecutionError extends UnrecoverableExecutionError { readonly terminationReason = TerminationReason.CUSTOM; - - constructor(message: string) { - super(message); - } } // Create a concrete test class for UnrecoverableInvocationError class TestUnrecoverableInvocationError extends UnrecoverableInvocationError { readonly terminationReason = TerminationReason.CUSTOM; - - constructor(message: string) { - super(message); - } } // Create a concrete test class for generic UnrecoverableError class TestGenericUnrecoverableError extends UnrecoverableError { readonly terminationReason = TerminationReason.CUSTOM; - - constructor(message: string) { - super(message); - } } test("should handle UnrecoverableExecutionError thrown by step function", async () => { diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler.ts b/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler.ts index 4d4e290f..c404f6a5 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler.ts @@ -158,19 +158,16 @@ export const createStepHandler = ( } if (stepData?.Status === OperationStatus.FAILED) { - // Return an async rejected promise to ensure it's handled asynchronously - return (async (): Promise => { - // Reconstruct the original error from stored ErrorObject - if (stepData.StepDetails?.Error) { - throw DurableOperationError.fromErrorObject( - stepData.StepDetails.Error, - ); - } else { - // Fallback for legacy data without Error field - const errorMessage = stepData?.StepDetails?.Result; - throw new StepError(errorMessage || "Unknown error"); - } - })(); + // Reconstruct the original error from stored ErrorObject + if (stepData.StepDetails?.Error) { + throw DurableOperationError.fromErrorObject( + stepData.StepDetails.Error, + ); + } else { + // Fallback for legacy data without Error field + const errorMessage = stepData?.StepDetails?.Result; + throw new StepError(errorMessage || "Unknown error"); + } } // If PENDING, wait for timer to complete @@ -362,7 +359,7 @@ export const executeStep = async ( getOperationsEmitter: () => EventEmitter, parentId: string | undefined, options?: StepConfig, - onAwaitedChange?: ((callback: () => void) => void) | undefined, + onAwaitedChange?: (callback: () => void) => void, ): Promise => { // Determine step semantics (default to AT_LEAST_ONCE_PER_RETRY if not specified) const semantics = options?.semantics || StepSemantics.AtLeastOncePerRetry; diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts index 5c9f1b0b..b62db675 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts @@ -130,7 +130,7 @@ describe("WaitForCondition Handler Two-Phase Execution", () => { undefined, ); - let executionOrder: string[] = []; + const executionOrder: string[] = []; const checkFn: WaitForConditionCheckFunc = jest.fn(async () => { executionOrder.push("check-executed"); return 42; diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts index f145d5c0..2690fb65 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts @@ -109,17 +109,13 @@ export const createWaitForConditionHandler = ( if (typeof nameOrCheck === "string" || nameOrCheck === undefined) { name = nameOrCheck; check = checkOrConfig as WaitForConditionCheckFunc; - config = maybeConfig as WaitForConditionConfig; + config = maybeConfig!; } else { check = nameOrCheck; config = checkOrConfig as WaitForConditionConfig; } - if ( - !config || - !config.waitStrategy || - config.initialState === undefined - ) { + if (!config?.waitStrategy || config.initialState === undefined) { throw new Error( "waitForCondition requires config with waitStrategy and initialState", ); @@ -134,78 +130,73 @@ export const createWaitForConditionHandler = ( }); // Main waitForCondition logic - can be re-executed if step status changes while (true) { - try { - const stepData = context.getStepData(stepId); - - // Check if already completed - if (stepData?.Status === OperationStatus.SUCCEEDED) { - return await handleCompletedWaitForCondition( - context, - stepId, - name, - config.serdes, - ); - } - - if (stepData?.Status === OperationStatus.FAILED) { - // Return an async rejected promise to ensure it's handled asynchronously - return (async (): Promise => { - // Reconstruct the original error from stored ErrorObject - if (stepData.StepDetails?.Error) { - throw DurableOperationError.fromErrorObject( - stepData.StepDetails.Error, - ); - } else { - // Fallback for legacy data without Error field - const errorMessage = stepData?.StepDetails?.Result; - throw new WaitForConditionError( - errorMessage || "waitForCondition failed", - ); - } - })(); - } - - // If PENDING, wait for timer to complete - if (stepData?.Status === OperationStatus.PENDING) { - await waitForContinuation( - context, - stepId, - name, - hasRunningOperations, - checkpoint, - getOperationsEmitter(), - isAwaited ? undefined : setWaitingCallback, - ); - continue; // Re-evaluate step status after waiting - } - - // Execute check function for READY, STARTED, or first time (undefined) - const result = await executeWaitForCondition( + const stepData = context.getStepData(stepId); + + // Check if already completed + if (stepData?.Status === OperationStatus.SUCCEEDED) { + return await handleCompletedWaitForCondition( + context, + stepId, + name, + config.serdes, + ); + } + + if (stepData?.Status === OperationStatus.FAILED) { + // Return an async rejected promise to ensure it's handled asynchronously + return (async (): Promise => { + // Reconstruct the original error from stored ErrorObject + if (stepData.StepDetails?.Error) { + throw DurableOperationError.fromErrorObject( + stepData.StepDetails.Error, + ); + } else { + // Fallback for legacy data without Error field + const errorMessage = stepData?.StepDetails?.Result; + throw new WaitForConditionError( + errorMessage || "waitForCondition failed", + ); + } + })(); + } + + // If PENDING, wait for timer to complete + if (stepData?.Status === OperationStatus.PENDING) { + await waitForContinuation( context, - checkpoint, stepId, name, - check, - config, - createContextLogger, - addRunningOperation, - removeRunningOperation, hasRunningOperations, - getOperationsEmitter, - parentId, + checkpoint, + getOperationsEmitter(), isAwaited ? undefined : setWaitingCallback, ); + continue; // Re-evaluate step status after waiting + } - // If executeWaitForCondition signals to continue the main loop, do so - if (result === CONTINUE_MAIN_LOOP) { - continue; - } + // Execute check function for READY, STARTED, or first time (undefined) + const result = await executeWaitForCondition( + context, + checkpoint, + stepId, + name, + check, + config, + createContextLogger, + addRunningOperation, + removeRunningOperation, + hasRunningOperations, + getOperationsEmitter, + parentId, + isAwaited ? undefined : setWaitingCallback, + ); - return result; - } catch (error) { - // For any error from executeWaitForCondition, re-throw it - throw error; + // If executeWaitForCondition signals to continue the main loop, do so + if (result === CONTINUE_MAIN_LOOP) { + continue; } + + return result; } })() .then((result) => { @@ -269,7 +260,7 @@ export const executeWaitForCondition = async ( hasRunningOperations: () => boolean, getOperationsEmitter: () => EventEmitter, parentId: string | undefined, - onAwaitedChange?: ((callback: () => void) => void) | undefined, + onAwaitedChange?: (callback: () => void) => void, ): Promise => { const serdes = config.serdes || defaultSerdes; diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/wait-handler/wait-handler-two-phase.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/wait-handler/wait-handler-two-phase.test.ts index 41078b3a..bb3b06c1 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/wait-handler/wait-handler-two-phase.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/wait-handler/wait-handler-two-phase.test.ts @@ -77,7 +77,7 @@ describe("Wait Handler Two-Phase Execution", () => { } // Phase 2 execution should have happened (more checkpoint calls) - expect((waitPromise as DurablePromise).isExecuted).toBe(true); + expect(waitPromise.isExecuted).toBe(true); expect(mockCheckpoint.mock.calls.length).toBeGreaterThan(phase1Calls); }); @@ -95,8 +95,8 @@ describe("Wait Handler Two-Phase Execution", () => { const wait2 = waitHandler({ seconds: 1 }); // Neither should be executed yet - expect((wait1 as DurablePromise).isExecuted).toBe(false); - expect((wait2 as DurablePromise).isExecuted).toBe(false); + expect(wait1.isExecuted).toBe(false); + expect(wait2.isExecuted).toBe(false); // Phase 2: Use Promise.race - this should trigger execution try { @@ -106,9 +106,7 @@ describe("Wait Handler Two-Phase Execution", () => { } // At least one should be executed - const executed = - (wait1 as DurablePromise).isExecuted || - (wait2 as DurablePromise).isExecuted; + const executed = wait1.isExecuted || wait2.isExecuted; expect(executed).toBe(true); }); @@ -123,13 +121,13 @@ describe("Wait Handler Two-Phase Execution", () => { // Phase 1: Create the promise const waitPromise = waitHandler({ seconds: 5 }); - expect((waitPromise as DurablePromise).isExecuted).toBe(false); + expect(waitPromise.isExecuted).toBe(false); // Phase 2: Call .then - this should trigger execution waitPromise.then(() => {}).catch(() => {}); // Should be marked as executed - expect((waitPromise as DurablePromise).isExecuted).toBe(true); + expect(waitPromise.isExecuted).toBe(true); }); it("should only checkpoint once when stepData already exists", async () => { diff --git a/packages/aws-durable-execution-sdk-js/src/run-durable.ts b/packages/aws-durable-execution-sdk-js/src/run-durable.ts index f0dc3d57..6b9157d4 100644 --- a/packages/aws-durable-execution-sdk-js/src/run-durable.ts +++ b/packages/aws-durable-execution-sdk-js/src/run-durable.ts @@ -15,6 +15,7 @@ async function runHandler( try { // Dynamically import the handler module const module = await import(handlerPath); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access const handler = module.default || module.handler; if (typeof handler !== "function") { diff --git a/packages/aws-durable-execution-sdk-js/src/testing/mock-batch-result.ts b/packages/aws-durable-execution-sdk-js/src/testing/mock-batch-result.ts index add6229a..3bb36742 100644 --- a/packages/aws-durable-execution-sdk-js/src/testing/mock-batch-result.ts +++ b/packages/aws-durable-execution-sdk-js/src/testing/mock-batch-result.ts @@ -3,28 +3,28 @@ import { ChildContextError } from "../errors/durable-error/durable-error"; export class MockBatchResult implements BatchResult { constructor( - public readonly all: Array>, + public readonly all: BatchItem[], public readonly completionReason: | "ALL_COMPLETED" | "MIN_SUCCESSFUL_REACHED" | "FAILURE_TOLERANCE_EXCEEDED" = "ALL_COMPLETED", ) {} - succeeded(): Array & { result: R }> { + succeeded(): (BatchItem & { result: R })[] { return this.all.filter( (item): item is BatchItem & { result: R } => item.status === BatchItemStatus.SUCCEEDED && item.result !== undefined, ); } - failed(): Array & { error: ChildContextError }> { + failed(): (BatchItem & { error: ChildContextError })[] { return this.all.filter( (item): item is BatchItem & { error: ChildContextError } => item.status === BatchItemStatus.FAILED && item.error !== undefined, ); } - started(): Array & { status: BatchItemStatus.STARTED }> { + started(): (BatchItem & { status: BatchItemStatus.STARTED })[] { return this.all.filter( (item): item is BatchItem & { status: BatchItemStatus.STARTED } => item.status === BatchItemStatus.STARTED, @@ -48,11 +48,11 @@ export class MockBatchResult implements BatchResult { } } - getResults(): Array { + getResults(): R[] { return this.succeeded().map((item) => item.result); } - getErrors(): Array { + getErrors(): ChildContextError[] { return this.failed().map((item) => item.error); } diff --git a/packages/aws-durable-execution-sdk-js/src/testing/mock-checkpoint.ts b/packages/aws-durable-execution-sdk-js/src/testing/mock-checkpoint.ts index 13571f87..87c104fc 100644 --- a/packages/aws-durable-execution-sdk-js/src/testing/mock-checkpoint.ts +++ b/packages/aws-durable-execution-sdk-js/src/testing/mock-checkpoint.ts @@ -1,11 +1,11 @@ import { OperationUpdate } from "@aws-sdk/client-lambda"; -export type CheckpointFunction = { +export interface CheckpointFunction { (stepId: string, data: Partial): Promise; force(): Promise; setTerminating(): void; hasPendingAncestorCompletion(stepId: string): boolean; -}; +} export const createMockCheckpoint = ( mockImplementation?: ( diff --git a/packages/aws-durable-execution-sdk-js/src/types/batch.ts b/packages/aws-durable-execution-sdk-js/src/types/batch.ts index 0bc0b8bd..54aaa4ca 100644 --- a/packages/aws-durable-execution-sdk-js/src/types/batch.ts +++ b/packages/aws-durable-execution-sdk-js/src/types/batch.ts @@ -27,13 +27,13 @@ export interface BatchItem { */ export interface BatchResult { /** All items in the batch with their results/errors */ - all: Array>; + all: BatchItem[]; /** Returns only the items that succeeded */ - succeeded(): Array & { result: R }>; + succeeded(): (BatchItem & { result: R })[]; /** Returns only the items that failed */ - failed(): Array & { error: ChildContextError }>; + failed(): (BatchItem & { error: ChildContextError })[]; /** Returns only the items that are still in progress */ - started(): Array & { status: BatchItemStatus.STARTED }>; + started(): (BatchItem & { status: BatchItemStatus.STARTED })[]; /** Overall status of the batch (SUCCEEDED if no failures, FAILED otherwise) */ status: BatchItemStatus.SUCCEEDED | BatchItemStatus.FAILED; /** Reason why the batch completed */ @@ -46,9 +46,9 @@ export interface BatchResult { /** Throws the first error if any item failed */ throwIfError(): void; /** Returns array of all successful results */ - getResults(): Array; + getResults(): R[]; /** Returns array of all errors */ - getErrors(): Array; + getErrors(): ChildContextError[]; /** Number of successful items */ successCount: number; /** Number of failed items */ diff --git a/packages/aws-durable-execution-sdk-js/src/types/core.ts b/packages/aws-durable-execution-sdk-js/src/types/core.ts index 3940d03e..f3b81d10 100644 --- a/packages/aws-durable-execution-sdk-js/src/types/core.ts +++ b/packages/aws-durable-execution-sdk-js/src/types/core.ts @@ -10,9 +10,10 @@ export enum DurableExecutionMode { ReplaySucceededContext = "ReplaySucceededContext", } -export interface LambdaHandler { - (event: T, context: Context): Promise; -} +export type LambdaHandler = ( + event: T, + context: Context, +) => Promise; // TODO - prefer to import this entire input model from the SDK, // but it's not part of the frontend model so it doesn't get generated. diff --git a/packages/aws-durable-execution-sdk-js/src/types/durable-promise.ts b/packages/aws-durable-execution-sdk-js/src/types/durable-promise.ts index bf3270c3..4fa4b9ab 100644 --- a/packages/aws-durable-execution-sdk-js/src/types/durable-promise.ts +++ b/packages/aws-durable-execution-sdk-js/src/types/durable-promise.ts @@ -72,14 +72,8 @@ export class DurablePromise implements Promise { * Triggers execution if not already started */ then( - onfulfilled?: - | ((value: T) => TResult1 | PromiseLike) - | undefined - | null, - onrejected?: - | ((reason: unknown) => TResult2 | PromiseLike) - | undefined - | null, + onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, ): Promise { return this.ensureExecution().then(onfulfilled, onrejected); } @@ -89,10 +83,7 @@ export class DurablePromise implements Promise { * Triggers execution if not already started */ catch( - onrejected?: - | ((reason: unknown) => TResult | PromiseLike) - | undefined - | null, + onrejected?: ((reason: unknown) => TResult | PromiseLike) | null, ): Promise { return this.ensureExecution().catch(onrejected); } @@ -101,14 +92,12 @@ export class DurablePromise implements Promise { * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected) * Triggers execution if not already started */ - finally(onfinally?: (() => void) | undefined | null): Promise { + finally(onfinally?: (() => void) | null): Promise { return this.ensureExecution().finally(onfinally); } /** Returns the string tag for the promise type */ - get [Symbol.toStringTag](): string { - return "DurablePromise"; - } + readonly [Symbol.toStringTag] = "DurablePromise"; /** * Check if the promise has been executed (awaited or had .then/.catch/.finally called) diff --git a/packages/aws-durable-execution-sdk-js/src/utils/checkpoint/checkpoint.ts b/packages/aws-durable-execution-sdk-js/src/utils/checkpoint/checkpoint.ts index 4dc8d9d2..29d4b203 100644 --- a/packages/aws-durable-execution-sdk-js/src/utils/checkpoint/checkpoint.ts +++ b/packages/aws-durable-execution-sdk-js/src/utils/checkpoint/checkpoint.ts @@ -28,10 +28,10 @@ class CheckpointHandler { private queue: QueuedCheckpoint[] = []; private isProcessing = false; private currentTaskToken: string; - private forceCheckpointPromises: Array<{ + private forceCheckpointPromises: { resolve: () => void; reject: (error: Error) => void; - }> = []; + }[] = []; private readonly MAX_PAYLOAD_SIZE = 750 * 1024; // 750KB in bytes private isTerminating = false; private pendingCompletions = new Set(); // Track stepIds with pending SUCCEED/FAIL @@ -493,11 +493,11 @@ export const createCheckpoint = ( stepId: string, data: Partial, ): Promise => { - return await singletonCheckpointHandler!.checkpoint(stepId, data); + await singletonCheckpointHandler!.checkpoint(stepId, data); }; checkpoint.force = async (): Promise => { - return await singletonCheckpointHandler!.forceCheckpoint(); + await singletonCheckpointHandler!.forceCheckpoint(); }; checkpoint.setTerminating = (): void => { diff --git a/packages/aws-durable-execution-sdk-js/src/utils/replay-validation/replay-validation.ts b/packages/aws-durable-execution-sdk-js/src/utils/replay-validation/replay-validation.ts index 7edc976f..ecdd7af4 100644 --- a/packages/aws-durable-execution-sdk-js/src/utils/replay-validation/replay-validation.ts +++ b/packages/aws-durable-execution-sdk-js/src/utils/replay-validation/replay-validation.ts @@ -14,7 +14,7 @@ export const validateReplayConsistency = ( context: ExecutionContext, ): void => { // Skip validation if no checkpoint data exists or if Type is undefined (first execution) - if (!checkpointData || !checkpointData.Type) { + if (!checkpointData?.Type) { return; } diff --git a/packages/aws-durable-execution-sdk-js/src/utils/safe-stringify/safe-stringify.ts b/packages/aws-durable-execution-sdk-js/src/utils/safe-stringify/safe-stringify.ts index fa8a0085..61b75b60 100644 --- a/packages/aws-durable-execution-sdk-js/src/utils/safe-stringify/safe-stringify.ts +++ b/packages/aws-durable-execution-sdk-js/src/utils/safe-stringify/safe-stringify.ts @@ -11,6 +11,7 @@ export const safeStringify = (data: unknown): string => { // Handle Error objects by extracting their properties if (value instanceof Error) { return { + // eslint-disable-next-line @typescript-eslint/no-misused-spread ...value, name: value.name, message: value.message, diff --git a/packages/aws-durable-execution-sdk-js/src/utils/serdes/serdes.test.ts b/packages/aws-durable-execution-sdk-js/src/utils/serdes/serdes.test.ts index de3870a8..c8320892 100644 --- a/packages/aws-durable-execution-sdk-js/src/utils/serdes/serdes.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/utils/serdes/serdes.test.ts @@ -73,8 +73,8 @@ describe("Serdes", () => { describe("createClassSerdes", () => { class TestClass { - name: string = ""; - value: number = 0; + name = ""; + value = 0; constructor(name?: string, value?: number) { if (name) this.name = name; @@ -128,7 +128,7 @@ describe("Serdes", () => { it("should handle class instances with Date properties", async () => { class ClassWithDate { - name: string = ""; + name = ""; createdAt: Date; constructor(name?: string) { @@ -166,7 +166,7 @@ describe("Serdes", () => { describe("createClassSerdesWithDates", () => { class ClassWithDate { - name: string = ""; + name = ""; createdAt: Date; updatedAt: Date | null = null; @@ -259,7 +259,7 @@ describe("Serdes", () => { it("should handle nested Date properties", async () => { class ClassWithNestedDate { - name: string = ""; + name = ""; metadata: { createdAt: Date; updatedAt: Date; diff --git a/packages/aws-durable-execution-sdk-js/src/utils/termination-helper/termination-helper.ts b/packages/aws-durable-execution-sdk-js/src/utils/termination-helper/termination-helper.ts index b1c9e959..e94e9dde 100644 --- a/packages/aws-durable-execution-sdk-js/src/utils/termination-helper/termination-helper.ts +++ b/packages/aws-durable-execution-sdk-js/src/utils/termination-helper/termination-helper.ts @@ -113,7 +113,7 @@ export function terminate( // Check if there are active operations before terminating const tracker = context.activeOperationsTracker; - if (tracker && tracker.hasActive()) { + if (tracker?.hasActive()) { log("⏳", "Deferring termination - active operations in progress:", { activeCount: tracker.getCount(), reason, @@ -152,7 +152,7 @@ export function terminate( // No parent context - check active operations and terminate const tracker = context.activeOperationsTracker; - if (tracker && tracker.hasActive()) { + if (tracker?.hasActive()) { log("⏳", "Deferring termination - active operations in progress:", { activeCount: tracker.getCount(), reason, diff --git a/packages/aws-durable-execution-sdk-js/src/with-durable-execution.test.ts b/packages/aws-durable-execution-sdk-js/src/with-durable-execution.test.ts index 42b4fe94..83ad146f 100644 --- a/packages/aws-durable-execution-sdk-js/src/with-durable-execution.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/with-durable-execution.test.ts @@ -402,9 +402,6 @@ describe("withDurableExecution", () => { // Setup - Create a test UnrecoverableInvocationError class TestInvocationError extends UnrecoverableInvocationError { readonly terminationReason = TerminationReason.CUSTOM; - constructor(message: string) { - super(message); - } } const testError = new TestInvocationError("Test invocation error"); @@ -428,9 +425,6 @@ describe("withDurableExecution", () => { // Setup - Create a custom UnrecoverableInvocationError class CustomInvocationError extends UnrecoverableInvocationError { readonly terminationReason = TerminationReason.CUSTOM; - constructor(message: string) { - super(message); - } } const customError = new CustomInvocationError("Custom invocation error"); @@ -454,9 +448,6 @@ describe("withDurableExecution", () => { // Setup - Create a custom UnrecoverableExecutionError class CustomExecutionError extends UnrecoverableExecutionError { readonly terminationReason = TerminationReason.CUSTOM; - constructor(message: string) { - super(message); - } } const executionError = new CustomExecutionError("Custom execution error"); diff --git a/packages/aws-durable-execution-sdk-js/src/with-durable-execution.ts b/packages/aws-durable-execution-sdk-js/src/with-durable-execution.ts index 95729723..d6465fd8 100644 --- a/packages/aws-durable-execution-sdk-js/src/with-durable-execution.ts +++ b/packages/aws-durable-execution-sdk-js/src/with-durable-execution.ts @@ -216,19 +216,14 @@ export const withDurableExecution = ( ): Promise => { const { executionContext, durableExecutionMode, checkpointToken } = await initializeExecutionContext(event); - let response: DurableExecutionInvocationOutput | null = null; - try { - response = await runHandler( - event, - context, - executionContext, - durableExecutionMode, - checkpointToken, - handler, - ); - return response; - } catch (err) { - throw err; - } + + return runHandler( + event, + context, + executionContext, + durableExecutionMode, + checkpointToken, + handler, + ); }; };