I have a fairly deep interface
declared that looks something like this:
export interface Job {
JobId: JobId; // type JobId = string
UserId: UserId; // type UserId = string
JobName: string;
AudioFile: JobAudioFile; // this is an interface
Status: JobStatus; // this is an enum
Tracks: JobTracks[]; // 'JobTracks' is an enum
Results: JobResults; // this is an interface
Timestamps: JobTimestamps // interface
}
Most of the members of this interface are themselves interfaces, with the general architecture following this pattern of using enums, strings, arrays and more interfaces. All code is written as TypeScript, transpiled down to JS and uploaded to AWS as JS. (Node 8.10 is running on AWS)
At one point in the code, I need to make a deep copy of a Job
instantiation which was passed in as a function parameter:
export const StartPipeline: Handler = async (
event: PipelineEvent
): Promise<PipelineEvent> => {
console.log('StartPipeline Event: %o', event);
const newBucket = await copyToJobsBucket$(event.Job);
await deleteFromOriginalBucket$(event.Job);
console.log(`Job [${event.Job.JobId}] moved to Jobs bucket: ${newBucket}`);
event.Job.AudioFile.Bucket = newBucket;
event.Job.Status = Types.JobStatus.Processing;
// update the job status
// VVV PROBLEM OCCURS HERE VVV
const msg: Types.JobUpdatedMessage = new Types.JobUpdatedMessage({ Job: Object.assign({}, event.Job) });
await Send.to$(event.Job.UserId, msg);
return { ...event };
};
The definition of the JobUpdatedMessage
:
export class JobUpdatedMessage extends BaseMessage {
constructor(payload: { Job: Types.Job }) {
console.log('Incoming: %o', payload);
const copy: object = { ...payload.Job };
// VVV PROBLEM ON NEXT LINE VVV
const filtered = JobUtils.FilterJobProperties(copy as Types.Job);
super(MessageTypes.JobUpdated, filtered);
}
}
The problem is after the call to JobUtils.FilterJobProperties
, payload.Job
has also been mutated in an undesirable and unexpected way.
Here's the implementation of JobUtils.FilterJobProperties
:
export const FilterJobProperties = (from: Types.Job): Types.Job => {
const fieldsToRemove: string[] = [
'Transcripts.GSTT',
'Transcripts.WSTT',
'Transcripts.ASTT',
'TranscriptTracks',
'Transcripts.Stream.File',
'Transcripts.Stream.State',
'AudioFile.Bucket',
'AudioFile.S3Key',
];
let job: Types.Job = { ...from }; // LINE ONE
fieldsToRemove.forEach(field => _.unset(job, field)); // LINE TWO
return job;
};
(I'm using the lodash library here)
The line market 'LINE TWO' is also mutating the from
function parameter, even though on 'LINE ONE' I'm doing what I think is a deep clone of from
.
I know that this is the case because if I change 'LINE ONE' to:
// super hard core deep cloning
let job: Types.Job = JSON.parse(JSON.stringify(from));
... everything works as expected. from
is not mutated, the resulting JobUpdatedMessage
is as expected, and StartPipeline
's event
parameter doesn't have a bunch of properties removed from event.Job
.
I struggled with hours on this, including relearning everything I believed I knew about cloning objects in Es6 using the spread operator.
Why was 'LINE ONE' mutating the input as well?