Skip to content

Commit

Permalink
[BUGFIX] fixes an issue with sync dematerialization followed by a fin…
Browse files Browse the repository at this point in the history
…dRecord, adds test coverage (#5013)
  • Loading branch information
runspired authored and bmac committed Jun 18, 2017
1 parent d4b9430 commit 584d159
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 1 deletion.
15 changes: 14 additions & 1 deletion addon/-private/system/model/internal-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const {
isEmpty,
isEqual,
setOwner,
run,
RSVP,
RSVP: { Promise }
} = Ember;
Expand Down Expand Up @@ -135,6 +136,7 @@ export default class InternalModel {
// `objectAt(len - 1)` to test whether or not `firstObject` or `lastObject`
// have changed.
this._isDematerializing = false;
this._scheduledDestroy = null;

this.resetRecord();

Expand Down Expand Up @@ -489,11 +491,22 @@ export default class InternalModel {
unloadRecord() {
this.send('unloadRecord');
this.dematerializeRecord();
Ember.run.schedule('destroy', this, '_checkForOrphanedInternalModels');
if (this._scheduledDestroy === null) {
this._scheduledDestroy = run.schedule('destroy', this, '_checkForOrphanedInternalModels');
}
}

cancelDestroy() {
assert(`You cannot cancel the destruction of an InternalModel once it has already been destroyed`, !this.isDestroyed);

this._isDematerializing = false;
run.cancel(this._scheduledDestroy);
this._scheduledDestroy = null;
}

_checkForOrphanedInternalModels() {
this._isDematerializing = false;
this._scheduledDestroy = null;
if (this.isDestroyed) { return; }

this._cleanupOrphanedInternalModels();
Expand Down
4 changes: 4 additions & 0 deletions addon/-private/system/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,10 @@ Store = Service.extend({

if (!internalModel) {
internalModel = this._buildInternalModel(modelName, trueId);
} else {
// if we already have an internalModel, we need to ensure any async teardown is cancelled
// since we want it again.
internalModel.cancelDestroy();
}

return internalModel;
Expand Down
157 changes: 157 additions & 0 deletions tests/integration/records/unload-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,160 @@ test('unloading a disconnected subgraph clears the relevant internal models', fu

assert.equal(relPayloads.get('person', 1, 'cars'), null, 'person - cars relationship payload unloaded');
});


test("Unloading a record twice only schedules destroy once", function(assert) {
const store = env.store;
let record;

// populate initial record
run(function() {
record = store.push({
data: {
type: 'person',
id: '1',
attributes: {
name: 'Adam Sunderland'
}
}
});
});

const internalModel = record._internalModel;

run(function() {
store.unloadRecord(record);
store.unloadRecord(record);
internalModel.cancelDestroy();
});

assert.equal(internalModel.isDestroyed, false, 'We cancelled destroy');
});

test("Cancelling destroy leaves the record in the empty state", function(assert) {
const store = env.store;
let record;

// populate initial record
run(function() {
record = store.push({
data: {
type: 'person',
id: '1',
attributes: {
name: 'Adam Sunderland'
}
}
});
});

const internalModel = record._internalModel;
assert.equal(internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded initially');

run(function() {
store.unloadRecord(record);
assert.equal(record.isDestroying, true, 'the record is destroying');
assert.equal(internalModel.isDestroyed, false, 'the internal model is not destroyed');
assert.equal(internalModel._isDematerializing, true, 'the internal model is dematerializing');
internalModel.cancelDestroy();
assert.equal(internalModel.currentState.stateName, 'root.empty', 'We are unloaded after unloadRecord');
});

assert.equal(internalModel.isDestroyed, false, 'the internal model was not destroyed');
assert.equal(internalModel._isDematerializing, false, 'the internal model is no longer dematerializing');
assert.equal(internalModel.currentState.stateName, 'root.empty', 'We are still unloaded after unloadRecord');
});

test("after unloading a record, the record can be fetched again immediately", function(assert) {
const store = env.store;
let record;

// stub findRecord
env.adapter.findRecord = () => {
return Ember.RSVP.Promise.resolve({
data: {
type: 'person',
id: '1',
attributes: {
name: 'Adam Sunderland'
}
}
});
};

// populate initial record
run(function() {
record = store.push({
data: {
type: 'person',
id: '1',
attributes: {
name: 'Adam Sunderland'
}
}
});
});

const internalModel = record._internalModel;
assert.equal(internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded initially');

// we test that we can sync call unloadRecord followed by findRecord
run(function() {
store.unloadRecord(record);
assert.equal(record.isDestroying, true, 'the record is destroying');
assert.equal(internalModel.currentState.stateName, 'root.empty', 'We are unloaded after unloadRecord');
store.findRecord('person', '1');
});

assert.equal(internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded after findRecord');
});


test("after unloading a record, the record can be fetched again soon there after", function(assert) {
const store = env.store;
let record;

// stub findRecord
env.adapter.findRecord = () => {
return Ember.RSVP.Promise.resolve({
data: {
type: 'person',
id: '1',
attributes: {
name: 'Adam Sunderland'
}
}
});
};

// populate initial record
run(function() {
record = store.push({
data: {
type: 'person',
id: '1',
attributes: {
name: 'Adam Sunderland'
}
}
});
});

let internalModel = record._internalModel;
assert.equal(internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded initially');

run(function() {
store.unloadRecord(record);
assert.equal(record.isDestroying, true, 'the record is destroying');
assert.equal(internalModel.currentState.stateName, 'root.empty', 'We are unloaded after unloadRecord');
});

run(function() {
store.findRecord('person', '1');
});

record = store.peekRecord('person', '1');
internalModel = record._internalModel;

assert.equal(internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded after findRecord');
});

0 comments on commit 584d159

Please sign in to comment.