Skip to content

Commit

Permalink
Merge pull request #5106 from emberjs/master-unload-with-orphaned-rel…
Browse files Browse the repository at this point in the history
…ationships-and-sync-find

{forwardport master}
  • Loading branch information
stefanpenner authored Jul 29, 2017
2 parents 1d0e67a + c7fca21 commit d35feb1
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 32 deletions.
5 changes: 5 additions & 0 deletions addon/-private/system/model/internal-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,11 @@ export default class InternalModel {
this.cancelDestroy();
}
this._checkForOrphanedInternalModels();
if (this.isDestroyed || this.isDestroying) { return; }

// just in-case we are not one of the orphaned, we should still
// still destroy ourselves
this.destroy();
}

_checkForOrphanedInternalModels() {
Expand Down
15 changes: 8 additions & 7 deletions addon/-private/system/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -1127,15 +1127,16 @@ Store = Service.extend({
let trueId = coerceId(id);
let internalModel = this._internalModelsFor(modelName).get(trueId);

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

return internalModel;
},

_internalModelDidReceiveRelationshipData(modelName, id, relationshipData) {
Expand Down
115 changes: 90 additions & 25 deletions tests/integration/records/unload-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ let env;
let Person = DS.Model.extend({
name: attr('string'),
cars: hasMany('car', { async: false }),
boats: hasMany('boat', { async: true })
boats: hasMany('boat', { async: true }),
bike: belongsTo('boat', { async: false, inverse: null })
});
Person.reopenClass({ toString() { return 'Person'; } });

Expand All @@ -38,14 +39,20 @@ let Boat = DS.Model.extend({
});
Boat.toString = function() { return 'Boat'; };

let Bike = DS.Model.extend({
name: DS.attr()
});
Bike.toString = function() { return 'Bike'; };

module("integration/unload - Unloading Records", {
beforeEach() {
env = setupStore({
adapter: DS.JSONAPIAdapter,
person: Person,
car: Car,
group: Group,
boat: Boat
boat: Boat,
bike: Bike
});
},

Expand Down Expand Up @@ -485,24 +492,23 @@ test("Cancelling destroy leaves the record in the empty state", function(assert)

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({
return {
data: {
type: 'person',
id: '1',
attributes: {
name: 'Adam Sunderland'
}
}
});
};
};

// populate initial record
run(function() {
record = store.push({
let record = run(() => {
return store.push({
data: {
type: 'person',
id: '1',
Expand Down Expand Up @@ -537,27 +543,23 @@ test("after unloading a record, the record can be fetched again immediately", fu
assert.equal(internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded initially');

// we test that we can sync call unloadRecord followed by findRecord
return run(function() {
assert.equal(record.get('cars.firstObject.make'), 'jeep');
return run(() => {
store.unloadRecord(record);
assert.equal(record.isDestroying, true, 'the record is destroying');
assert.equal(internalModel.currentState.stateName, 'root.empty', 'We are unloaded after unloadRecord');
return store.findRecord('person', '1').then(record => {
assert.equal(record.get('cars.length'), 1);
assert.equal(record._internalModel, internalModel, 'new record, has old internal model');
assert.equal(internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded after findRecord');
})
return store.findRecord('person', '1').then(newRecord => {
assert.equal(internalModel.currentState.stateName, 'root.empty', 'the old internalModel is discarded');
assert.equal(newRecord._internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded after findRecord');
});
});

});

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

// stub findRecord
env.adapter.findRecord = () => {
return Ember.RSVP.Promise.resolve({
return {
data: {
type: 'person',
id: '1',
Expand All @@ -568,12 +570,12 @@ test("after unloading a record, the record can be fetched again immediately (pur
cars: { data: null }
}
}
});
};
};

// populate initial record
run(function() {
record = store.push({
let record = run(() => {
return store.push({
data: {
type: 'person',
id: '1',
Expand Down Expand Up @@ -608,18 +610,81 @@ test("after unloading a record, the record can be fetched again immediately (pur
assert.equal(internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded initially');

// we test that we can sync call unloadRecord followed by findRecord
return run(function() {
return run(() => {
assert.equal(record.get('cars.firstObject.make'), 'jeep');
store.unloadRecord(record);
assert.equal(record.isDestroying, true, 'the record is destroying');
assert.equal(internalModel.currentState.stateName, 'root.empty', 'We are unloaded after unloadRecord');
assert.equal(internalModel.currentState.stateName, 'root.empty', 'Expected the previous internal model tobe unloaded');

return store.findRecord('person', '1').then(record => {
assert.equal(record.get('cars.length'), 0);
assert.equal(record._internalModel, internalModel, 'new record, has old internal model');
assert.equal(internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded after findRecord');
})
assert.equal(internalModel.currentState.stateName, 'root.empty', 'Expected the previous internal model to STILL be unloaded');
assert.equal(record._internalModel.currentState.stateName, 'root.loaded.saved', 'Expected the NEW internal model to be loaded');
});
});
});

test("after unloading a record, the record can be fetched again immediately (with relationships)", function(assert) {
const store = env.store;
// stub findRecord
env.adapter.findRecord = () => {
return {
data: {
type: 'person',
id: '1',
attributes: {
name: 'Adam Sunderland'
}
}
};
};

// populate initial record
let record = run(() => {
return store.push({
data: {
type: 'person',
id: '1',
relationships: {
bike: {
data: { type: 'bike', id: '1' }
}
}
},

included: [
{
id: '1',
type: 'bike',
attributes: {
name: 'mr bike'
}
}
]
});
});

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

assert.equal(record.get('bike.name'), 'mr bike');

// we test that we can sync call unloadRecord followed by findRecord
let wait = run(() => {
store.unloadRecord(record);
assert.equal(record.isDestroying, true, 'the record is destroying');
assert.equal(record.isDestroyed, false, 'the record is NOT YET destroyed');
assert.equal(internalModel.currentState.stateName, 'root.empty', 'We are unloaded after unloadRecord');
let wait = store.findRecord('person', '1').then(newRecord => {
assert.equal(record.isDestroyed, false, 'the record is NOT YET destroyed');
assert.ok(newRecord.get('bike') === null, 'the newRecord should NOT have had a bike');
});
assert.equal(record.isDestroyed, false, 'the record is NOT YET destroyed');
return wait;
});

assert.equal(record.isDestroyed, true, 'the record IS destroyed');
return wait;
});

test("after unloading a record, the record can be fetched again soon there after", function(assert) {
Expand Down

0 comments on commit d35feb1

Please sign in to comment.