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
5 changes: 5 additions & 0 deletions .changeset/shaky-seals-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/core": patch
---

Pass class as `this` context to custom serializer/deserializer methods
60 changes: 60 additions & 0 deletions packages/core/src/serialization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,25 +551,25 @@ function getCommonReducers(global: Record<string, any> = 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),
Expand Down Expand Up @@ -890,7 +890,7 @@ export function getCommonRevivers(global: Record<string, any> = globalThis) {
}

// Deserialize the instance using the custom deserializer
return deserialize(data);
return deserialize.call(cls, data);
},
Set: (value) => new global.Set(value),
StepFunction: (value) => {
Expand Down
Loading