Skip to content

Commit 5d3bf3c

Browse files
committed
fix: Fix memory leak fast-copying nested instance
Ok, it’s not a real memory leak, but we don’t want extra instances in memory. This makes sure that fastCopy is never used on the first level of a model instance. (It works fine on nested model instances, because it always calls new prop.constructor on nested objects) It includes lots of tests for cloning and commiting.
1 parent e3dfb39 commit 5d3bf3c

File tree

3 files changed

+117
-3
lines changed

3 files changed

+117
-3
lines changed

src/utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,14 @@ export function mergeWithAccessors(
419419
return
420420
}
421421

422-
// If we're dealing with a Vue Observable, just assign the values.
422+
// Handle Vue observable objects
423423
if (destIsVueObservable || sourceIsVueObservable) {
424-
if (_isObject(source[key])) {
424+
const isObject = _isObject(source[key])
425+
const isFeathersVuexInstance = isObject && !!(source[key].constructor.modelName || source[key].constructor.namespace)
426+
// Do not use fastCopy directly on a feathers-vuex BaseModel instance to keep from breaking reactivity.
427+
if (isObject && !isFeathersVuexInstance) {
425428
try {
429+
const sourceObject = source[key]
426430
dest[key] = fastCopy(source[key])
427431
} catch (err) {
428432
if(!err.message.includes('getter')) {

test/service-module/model-relationships.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ function makeContext() {
152152
super(data, options)
153153
}
154154
}
155+
/**
156+
* This Model demonstrates how to use a dynamic set of instanceDefaults based on incoming data.
157+
*/
155158
class Todo extends BaseModel {
156159
public static modelName = 'Todo'
157160
public static instanceDefaults(data) {
@@ -529,4 +532,71 @@ describe('Models - Relationships', function () {
529532

530533
assert.equal(todo.task.isComplete, false, 'preserved after clone')
531534
})
535+
536+
it('returns the same object when an instance is cloned twice', function () {
537+
const { Todo } = makeContext()
538+
539+
const todo = new Todo({
540+
task: {
541+
id: 1,
542+
description: 'test',
543+
isComplete: true
544+
}
545+
})
546+
547+
const clone1 = todo.clone()
548+
const clone2 = todo.clone()
549+
550+
assert(clone1 === clone2, 'there should only ever be one clone in memory for an instance with the same id')
551+
})
552+
553+
it('on clone, nested instances do not get cloned', function () {
554+
const { Todo } = makeContext()
555+
556+
const todo = new Todo({
557+
task: {
558+
id: 1,
559+
description: 'test',
560+
isComplete: true
561+
}
562+
})
563+
564+
const todoClone = todo.clone()
565+
566+
assert(todoClone.task.__isClone === undefined, 'todo.task should still be the original item and not the clone')
567+
})
568+
569+
it('on nested commit in instance, original nested instances get updated', function () {
570+
const { Todo } = makeContext()
571+
572+
const todo = new Todo({
573+
task: {
574+
id: 1,
575+
description: 'test',
576+
isComplete: true
577+
}
578+
})
579+
580+
const taskClone = todo.task.clone()
581+
582+
taskClone.description = 'changed'
583+
taskClone.commit()
584+
585+
assert(todo.task.description === 'changed', 'the nested task should have been updated')
586+
})
587+
588+
it('nested instances get updated in clones and original records', function () {
589+
const { Todo } = makeContext()
590+
591+
const todo = new Todo({
592+
task: {
593+
id: 1,
594+
description: 'test',
595+
isComplete: true
596+
}
597+
})
598+
const todoClone = todo.clone()
599+
600+
assert(todo.task === todoClone.task, 'the same task instance should be in both the original and clone')
601+
})
532602
})

test/service-module/service-module.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ function makeContext() {
4949
public constructor(data, options?) {
5050
super(data, options)
5151
}
52+
public static instanceDefaults(data) {
53+
return {
54+
description: ''
55+
}
56+
}
5257
}
5358
class HotspotMedia extends BaseModel {
5459
public static modelName = 'HotspotMedia'
@@ -304,7 +309,42 @@ describe('Service Module', function () {
304309
)
305310
})
306311

307-
it(`no longer changes original if you don't use the return value of commit()`, function () {
312+
it(`the object returned from clone is not the same as the original`, function () {
313+
const { serviceTodo, owners } = this
314+
const serviceTodoClone = serviceTodo.clone()
315+
316+
assert(serviceTodo !== serviceTodoClone, 'the objects are distinct')
317+
})
318+
319+
it(`the object returned from commit is not the same as the clone`, function () {
320+
const { serviceTodo, owners } = this
321+
const serviceTodoClone = serviceTodo.clone()
322+
const committedTodo = serviceTodoClone.commit()
323+
324+
assert(committedTodo !== serviceTodoClone, 'the objects are distinct')
325+
})
326+
327+
it(`the object returned from commit is the same as the original`, function () {
328+
const { serviceTodo, owners } = this
329+
const serviceTodoClone = serviceTodo.clone()
330+
const committedTodo = serviceTodoClone.commit()
331+
332+
assert(serviceTodo === committedTodo, 'the objects are the same')
333+
})
334+
335+
it(`nested arrays are distinct after clone`, function () {
336+
const { ServiceTodo } = this
337+
338+
const todo = new ServiceTodo({
339+
description: 'test',
340+
owners: ['Marshall', 'Mariah']
341+
})
342+
const clone = todo.clone()
343+
344+
assert(todo.owners !== clone.owners, 'the arrays are not the same in memory')
345+
})
346+
347+
it.skip(`modifying a clone after calling commit() does not change the original `, function () {
308348
const { serviceTodo, owners } = this
309349
const serviceTodoClone = serviceTodo.clone()
310350

0 commit comments

Comments
 (0)