diff --git a/.changeset/shaky-seals-raise.md b/.changeset/shaky-seals-raise.md new file mode 100644 index 0000000000..163a982240 --- /dev/null +++ b/.changeset/shaky-seals-raise.md @@ -0,0 +1,5 @@ +--- +"@workflow/core": patch +--- + +Pass class as `this` context to custom serializer/deserializer methods diff --git a/packages/core/src/serialization.test.ts b/packages/core/src/serialization.test.ts index 3f9f1f557d..00dea27000 100644 --- a/packages/core/src/serialization.test.ts +++ b/packages/core/src/serialization.test.ts @@ -2217,6 +2217,66 @@ describe('custom class serialization', () => { expect(hydrated.created).toBeInstanceOf(Date); expect(hydrated.created.toISOString()).toBe('2025-01-01T00:00:00.000Z'); }); + + it('should pass class as this context to WORKFLOW_SERIALIZE and WORKFLOW_DESERIALIZE', () => { + // This test verifies that serialize.call(cls, value) and deserialize.call(cls, data) + // properly pass the class as `this` context, which is required when the serializer/deserializer + // needs to access static properties or methods on the class + + class Counter { + // Static property that the serializer uses via `this` + static serializedCount = 0; + static deserializedCount = 0; + + constructor(public value: number) {} + + static [WORKFLOW_SERIALIZE](this: typeof Counter, instance: Counter) { + // biome-ignore lint/complexity/noThisInStatic: intentionally testing `this` context binding + this.serializedCount++; + // biome-ignore lint/complexity/noThisInStatic: intentionally testing `this` context binding + return { value: instance.value, serializedAt: this.serializedCount }; + } + + static [WORKFLOW_DESERIALIZE]( + this: typeof Counter, + data: { value: number; serializedAt: number } + ) { + // biome-ignore lint/complexity/noThisInStatic: intentionally testing `this` context binding + this.deserializedCount++; + return new Counter(data.value); + } + } + + // The classId is normally generated by the SWC compiler + (Counter as any).classId = 'test/Counter'; + + // Register the class for serialization/deserialization + registerSerializationClass('test/Counter', Counter); + + // Reset counters + Counter.serializedCount = 0; + Counter.deserializedCount = 0; + + // Serialize an instance - this should increment serializedCount via `this` + const counter = new Counter(42); + const serialized = dehydrateWorkflowArguments(counter, [], mockRunId); + + // Verify serialization used `this` correctly + expect(Counter.serializedCount).toBe(1); + + // Deserialize - this should increment deserializedCount via `this` + const hydrated = hydrateWorkflowArguments(serialized, globalThis); + + // Verify deserialization used `this` correctly + expect(Counter.deserializedCount).toBe(1); + expect(hydrated).toBeInstanceOf(Counter); + expect(hydrated.value).toBe(42); + + // Serialize another instance to verify counter increments + const counter2 = new Counter(100); + dehydrateWorkflowArguments(counter2, [], mockRunId); + expect(Counter.serializedCount).toBe(2); + }); }); describe('format prefix system', () => { diff --git a/packages/core/src/serialization.ts b/packages/core/src/serialization.ts index 172275e902..fb4ce0b284 100644 --- a/packages/core/src/serialization.ts +++ b/packages/core/src/serialization.ts @@ -551,25 +551,25 @@ function getCommonReducers(global: Record = globalThis) { Instance: (value) => { // Check if this is an instance of a class with custom serialization if (value === null || typeof value !== 'object') return false; - const ctor = value.constructor; - if (!ctor || typeof ctor !== 'function') return false; + const cls = value.constructor; + if (!cls || typeof cls !== 'function') return false; // Check if the class has a static WORKFLOW_SERIALIZE method - const serialize = ctor[WORKFLOW_SERIALIZE]; + const serialize = cls[WORKFLOW_SERIALIZE]; if (typeof serialize !== 'function') { return false; } // Get the classId from the static class property (set by SWC plugin) - const classId = ctor.classId; + const classId = cls.classId; if (typeof classId !== 'string') { throw new Error( - `Class "${ctor.name}" with ${String(WORKFLOW_SERIALIZE)} must have a static "classId" property.` + `Class "${cls.name}" with ${String(WORKFLOW_SERIALIZE)} must have a static "classId" property.` ); } // Serialize the instance using the custom serializer - const data = serialize(value); + const data = serialize.call(cls, value); return { classId, data }; }, Set: (value) => value instanceof global.Set && Array.from(value), @@ -890,7 +890,7 @@ export function getCommonRevivers(global: Record = globalThis) { } // Deserialize the instance using the custom deserializer - return deserialize(data); + return deserialize.call(cls, data); }, Set: (value) => new global.Set(value), StepFunction: (value) => {