-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BUGFIX] ensure destroy-sync cleanup is correct #5438
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -518,6 +518,92 @@ test('unloadAll(type) does not leave stranded internalModels in relationships (r | |
assert.ok(reloadedBoatInternalModel === initialBoatInternalModel, 'after an unloadAll, subsequent fetch results in the same InternalModel'); | ||
}); | ||
|
||
test('(regression) unloadRecord followed by push in the same run-loop', function(assert) { | ||
let { store } = env; | ||
|
||
let person = run(() => store.push({ | ||
data: { | ||
type: 'person', | ||
id: '1', | ||
attributes: { | ||
name: 'Could be Anybody' | ||
}, | ||
relationships: { | ||
boats: { | ||
data: [ | ||
{ type: 'boat', id: '1' } | ||
] | ||
} | ||
} | ||
}, | ||
included: [ | ||
makeBoatOneForPersonOne() | ||
] | ||
})); | ||
|
||
let boat = store.peekRecord('boat', '1'); | ||
let initialBoatInternalModel = boat._internalModel; | ||
let relationshipState = person.hasMany('boats').hasManyRelationship; | ||
let knownPeople = env.store._internalModelsFor('person'); | ||
let knownBoats = store._internalModelsFor('boat'); | ||
|
||
// ensure we loaded the people and boats | ||
assert.equal(knownPeople.models.length, 1, 'one person record is loaded'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are fine, but better assertions would match against eg an array of ids rather than just the length as that would catch more regressions. eg assert.deepEqual(knownPeople.models.map(m => m.id), ['1'], 'one person record is loaded'); i don't think it matters enough to not merge an important bug fix, but is an improvement we can make to the tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (the above comment applies to the rest of the tests that assert against There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
assert.equal(knownBoats.models.length, 1, 'one boat record is loaded'); | ||
assert.equal(env.store.hasRecordForId('person', '1'), true); | ||
assert.equal(env.store.hasRecordForId('boat', '1'), true); | ||
|
||
// ensure the relationship was established (we reach through the async proxy here) | ||
let peopleBoats = run(() => person.get('boats.content')); | ||
let boatPerson = run(() => boat.get('person.content')); | ||
|
||
assert.equal(relationshipState.canonicalMembers.size, 1, 'canonical member size should be 1'); | ||
assert.equal(relationshipState.members.size, 1, 'members size should be 1'); | ||
assert.ok(get(peopleBoats, 'length') === 1, 'Our person has a boat'); | ||
assert.ok(peopleBoats.objectAt(0) === boat, 'Our person has the right boat'); | ||
assert.ok(boatPerson === person, 'Our boat has the right person'); | ||
|
||
run(() => boat.unloadRecord()); | ||
|
||
// ensure that our new state is correct | ||
assert.equal(knownPeople.models.length, 1, 'one person record is loaded'); | ||
assert.equal(knownBoats.models.length, 1, 'one boat record is known'); | ||
assert.ok(knownBoats.models[0] === initialBoatInternalModel, 'We still have our boat'); | ||
assert.equal(initialBoatInternalModel.isEmpty(), true, 'Model is in the empty state'); | ||
assert.equal(relationshipState.canonicalMembers.size, 1, 'canonical member size should still be 1'); | ||
assert.equal(relationshipState.members.size, 1, 'members size should still be 1'); | ||
assert.ok(get(peopleBoats, 'length') === 0, 'Our person thinks they have no boats'); | ||
|
||
run(() => store.push({ | ||
data: makeBoatOneForPersonOne() | ||
})); | ||
|
||
let reloadedBoat = store.peekRecord('boat', '1'); | ||
let reloadedBoatInternalModel = reloadedBoat._internalModel; | ||
|
||
assert.equal(relationshipState.canonicalMembers.size, 1, 'canonical member size should be 1'); | ||
assert.equal(relationshipState.members.size, 1, 'members size should be 1'); | ||
assert.ok(reloadedBoatInternalModel === initialBoatInternalModel, 'after an unloadRecord, subsequent fetch results in the same InternalModel'); | ||
|
||
// and now the kicker, run-loop fun! | ||
// here, we will dematerialize the record, but push it back into the store | ||
// all in the same run-loop! | ||
// effectively this tests that our destroySync is not stupid | ||
run(() => { | ||
reloadedBoat.unloadRecord(); | ||
store.push({ | ||
data: makeBoatOneForPersonOne() | ||
}); | ||
}); | ||
|
||
let yaBoat = store.peekRecord('boat', '1'); | ||
let yaBoatInternalModel = yaBoat._internalModel; | ||
|
||
assert.equal(relationshipState.canonicalMembers.size, 1, 'canonical member size should be 1'); | ||
assert.equal(relationshipState.members.size, 1, 'members size should be 1'); | ||
assert.ok(yaBoatInternalModel === initialBoatInternalModel, 'after an unloadRecord, subsequent same-loop push results in the same InternalModel'); | ||
}); | ||
|
||
test('unloading a disconnected subgraph clears the relevant internal models', function(assert) { | ||
env.adapter.shouldBackgroundReloadRecord = () => false; | ||
|
||
|
@@ -632,7 +718,6 @@ test('unloading a disconnected subgraph clears the relevant internal models', fu | |
}); | ||
}); | ||
|
||
|
||
test('Unloading a record twice only schedules destroy once', function(assert) { | ||
const store = env.store; | ||
let record; | ||
|
@@ -753,7 +838,7 @@ test('after unloading a record, the record can be fetched again immediately', fu | |
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(newRecord => { | ||
assert.equal(internalModel.currentState.stateName, 'root.empty', 'the old internalModel is discarded'); | ||
assert.ok(internalModel === newRecord._internalModel, 'the old internalModel is reused'); | ||
assert.equal(newRecord._internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded after findRecord'); | ||
}); | ||
}); | ||
|
@@ -772,7 +857,9 @@ test('after unloading a record, the record can be fetched again immediately (pur | |
name: 'Adam Sunderland' | ||
}, | ||
relationships: { | ||
cars: { data: null } | ||
cars: { | ||
data: [] | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this payload change? these are both equivalent relationship payloads no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "cosmetic".
The spec is specific on this although we have been lax historically: http://jsonapi.org/format/#document-resource-object-linkage There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (the tests passed either way, just thought it was good to continue working us little by little closer to full spec) |
||
} | ||
} | ||
}; | ||
|
@@ -791,7 +878,7 @@ test('after unloading a record, the record can be fetched again immediately (pur | |
cars: { | ||
data: [ | ||
{ | ||
id: 1, | ||
id: '1', | ||
type: 'car' | ||
} | ||
] | ||
|
@@ -801,7 +888,7 @@ test('after unloading a record, the record can be fetched again immediately (pur | |
included: [ | ||
{ | ||
type: 'car', | ||
id: 1, | ||
id: '1', | ||
attributes: { | ||
make: 'jeep', | ||
model: 'wrangler' | ||
|
@@ -822,8 +909,8 @@ test('after unloading a record, the record can be fetched again immediately (pur | |
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(internalModel.currentState.stateName, 'root.empty', 'Expected the previous internal model to STILL be unloaded'); | ||
assert.equal(record.get('cars.length'), 0, 'Expected relationship to be cleared by the new push'); | ||
assert.ok(internalModel === record._internalModel, 'the old internalModel is reused'); | ||
assert.equal(record._internalModel.currentState.stateName, 'root.loaded.saved', 'Expected the NEW internal model to be loaded'); | ||
}); | ||
}); | ||
|
@@ -870,6 +957,7 @@ test('after unloading a record, the record can be fetched again immediately (wit | |
}); | ||
|
||
const internalModel = record._internalModel; | ||
const bike = store.peekRecord('bike', '1'); | ||
assert.equal(internalModel.currentState.stateName, 'root.loaded.saved', 'We are loaded initially'); | ||
|
||
assert.equal(record.get('bike.name'), 'mr bike'); | ||
|
@@ -880,10 +968,12 @@ test('after unloading a record, the record can be fetched again immediately (wit | |
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.ok(newRecord.get('bike') === bike, 'the newRecord should retain knowledge of the bike'); | ||
}); | ||
|
||
assert.equal(record.isDestroyed, false, 'the record is NOT YET destroyed'); | ||
return wait; | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These comments are good but this logic relies on
_internalModelForId
and_existingInternalModelForId
being called from separate code paths which is nonobvious and brittle.I'm 👍 on merging this to get the fix in, but we'll want to clean this up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My hope is that once we have
remove
operations we will simply stop supportingunloadRecord + createRecord
which is the only reason why this divergence exists. Instead you would be required toremove
before theadd
to ensure the store fully forgets about the previous record by that ID.