Skip to content

Commit edf2a0b

Browse files
Windvisrunspired
andauthored
fix: clear relationships properly when unloading new records (#8791)
* Add a failing test case Unloading a new record that is related to another record does not remove it from the relationship. * update test * fix: dont orphan new records in implicit relationships * fixes --------- Co-authored-by: Chris Thoburn <runspired@gmail.com>
1 parent ba1ab08 commit edf2a0b

6 files changed

Lines changed: 95 additions & 20 deletions

File tree

packages/graph/src/-private/graph.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export class Graph {
234234
continue;
235235
}
236236
assert(`Expected a relationship`, relationship);
237-
if (relationship.definition.inverseIsAsync) {
237+
if (relationship.definition.inverseIsAsync && !isNew(identifier)) {
238238
if (LOG_GRAPH) {
239239
// eslint-disable-next-line no-console
240240
console.log(`graph: <<NOT>> RELEASABLE ${String(identifier)}`);

packages/json-api/src/-private/cache.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,16 @@ export default class JSONAPICache implements Cache {
868868
const removeFromRecordArray = !this.isDeletionCommitted(identifier);
869869
let removed = false;
870870
const cached = this.__peek(identifier, false);
871-
peekGraph(storeWrapper)?.unload(identifier);
871+
872+
if (cached.isNew) {
873+
peekGraph(storeWrapper)?.push({
874+
op: 'deleteRecord',
875+
record: identifier,
876+
isNew: true,
877+
});
878+
} else {
879+
peekGraph(storeWrapper)?.unload(identifier);
880+
}
872881

873882
// effectively clearing these is ensuring that
874883
// we report as `isEmpty` during teardown.
@@ -1025,11 +1034,7 @@ export default class JSONAPICache implements Cache {
10251034
}
10261035

10271036
if (cached.isNew) {
1028-
this.__graph.push({
1029-
op: 'deleteRecord',
1030-
record: identifier,
1031-
isNew: true,
1032-
});
1037+
// > Note: Graph removal handled by unloadRecord
10331038
cached.isDeleted = true;
10341039
cached.isNew = false;
10351040
}
@@ -1083,14 +1088,7 @@ export default class JSONAPICache implements Cache {
10831088
setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void {
10841089
const cached = this.__peek(identifier, false);
10851090
cached.isDeleted = isDeleted;
1086-
if (cached.isNew) {
1087-
// TODO can we delete this since we will do this in unload?
1088-
this.__graph.push({
1089-
op: 'deleteRecord',
1090-
record: identifier,
1091-
isNew: true,
1092-
});
1093-
}
1091+
// > Note: Graph removal for isNew handled by unloadRecord
10941092
this.__storeWrapper.notifyChange(identifier, 'state');
10951093
}
10961094

tests/graph/ember-cli-build.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ module.exports = function (defaults) {
88
let app = new EmberApp(defaults, {
99
emberData: {
1010
compatWith,
11+
debug: {
12+
// LOG_GRAPH: true,
13+
},
1114
},
1215
babel: {
1316
// this ensures that the same build-time code stripping that is done

tests/graph/tests/integration/graph/edge-removal/abstract-edge-removal-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ module('Integration | Graph | Edge Removal', function (hooks) {
175175

176176
// we remove if the record was new or if the relationship was sync (client side delete semantics)
177177
let removed = config.useCreate || !config.async;
178-
// we clear sync non-implicit relationships (client side delete semantics)
179-
let cleared = !config.async && !config.inverseNull;
178+
// we clear new records, or sync non-implicit relationships (client side delete semantics)
179+
let cleared = config.useCreate || (!config.async && !config.inverseNull);
180180

181181
await testFinalState(this, testState, config, { removed, cleared, implicitCleared: true }, assert);
182182
});

tests/main/tests/index.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@
3636
// the reporter is configured to ignore them. This results
3737
// in extremely high memory and cpu utilization and eventually
3838
// will crash the console.
39-
const ignoredLogTypes = new Set(['log', 'warn', 'info', 'group']);
39+
const ignoredLogTypes = new Set(['log', 'warn', 'info', 'group', 'debug', 'groupEnd', 'error', 'trace', 'dir']);
4040
// restore original functionality
4141
ignoredLogTypes.forEach((method) => {
42-
console[method] = Testem.console[method];
42+
if (Testem.console[method]) {
43+
console[method] = Testem.console[method];
44+
}
4345
});
4446

4547
// enable debugging memory over time

tests/main/tests/integration/records/new-record-unload-test.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { module, test } from 'qunit';
44

55
import { setupTest } from 'ember-qunit';
66

7-
import Model, { attr, hasMany } from '@ember-data/model';
7+
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
88
import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug';
99

1010
class Person extends Model {
1111
@attr name;
1212
@hasMany('person', { async: true, inverse: 'friends' }) friends;
13+
@belongsTo('person', { async: true, inverse: null }) bestFriend;
1314
}
1415

1516
module('Integration | Records | New Record Unload', function (hooks) {
@@ -60,6 +61,77 @@ module('Integration | Records | New Record Unload', function (hooks) {
6061
assert.strictEqual(people.length, 1, 'precond - one person left in the store');
6162
});
6263

64+
test('Rolling Back Attributes on multiple New (related via async self-reflexive HasMany) Records unloads them safely', async function (assert) {
65+
const store = this.owner.lookup('service:store');
66+
const Pat = store.createRecord('person', { name: 'Patrick Wachter' });
67+
const Matt = store.createRecord('person', { name: 'Matthew Seidel', friends: [Pat] });
68+
const friends = Matt.hasMany('friends').value();
69+
const people = store.peekAll('person');
70+
71+
assert.strictEqual(friends.length, 1, 'Matt has friends');
72+
assert.strictEqual(people.length, 2, 'precond - two people records in the store');
73+
assert.true(Matt.hasDirtyAttributes, 'precond - record has dirty attributes');
74+
assert.true(Matt.isNew, 'precond - record is new');
75+
assert.true(Pat.hasDirtyAttributes, 'precond - record has dirty attributes');
76+
assert.true(Pat.isNew, 'precond - record is new');
77+
78+
Pat.rollbackAttributes();
79+
80+
assert.false(Pat.isDestroyed, 'Pat record is not yet destroyed');
81+
assert.true(Pat.isDestroying, 'Pat record is destroying');
82+
assert.strictEqual(friends.length, 0, 'Matt has no friends');
83+
assert.strictEqual(people.length, 1, 'precond - one person left in the store');
84+
85+
Matt.rollbackAttributes();
86+
assert.false(Matt.isDestroyed, 'Matt record is not yet destroyed');
87+
assert.true(Matt.isDestroying, 'Matt record is destroying');
88+
assert.strictEqual(people.length, 0, 'precond - no people left in the store');
89+
90+
await settled();
91+
92+
assert.true(Pat.isDestroyed, 'Pat record is destroyed');
93+
assert.true(Pat.isDestroying, 'Pat record is destroying');
94+
assert.true(Matt.isDestroyed, 'Matt record is destroyed');
95+
assert.true(Matt.isDestroying, 'Matt record is destroying');
96+
assert.strictEqual(people.length, 0, 'precond - no people left in the store');
97+
});
98+
99+
test('Rolling Back Attributes on multiple New (related via async belongsTo with no inverse) Records unloads them safely', async function (assert) {
100+
const store = this.owner.lookup('service:store');
101+
const Pat = store.createRecord('person', { name: 'Patrick Wachter' });
102+
const Matt = store.createRecord('person', { name: 'Matthew Seidel', bestFriend: Pat });
103+
let bestFriend = Matt.belongsTo('bestFriend').value();
104+
const people = store.peekAll('person');
105+
106+
assert.strictEqual(people.length, 2, 'precond - two people records in the store');
107+
assert.strictEqual(bestFriend, Pat, 'Matt has a best friend');
108+
assert.true(Matt.hasDirtyAttributes, 'precond - record has dirty attributes');
109+
assert.true(Matt.isNew, 'precond - record is new');
110+
assert.true(Pat.hasDirtyAttributes, 'precond - record has dirty attributes');
111+
assert.true(Pat.isNew, 'precond - record is new');
112+
113+
Pat.rollbackAttributes();
114+
115+
bestFriend = Matt.belongsTo('bestFriend').value();
116+
assert.strictEqual(bestFriend, null, 'Matt has no best friend');
117+
assert.false(Pat.isDestroyed, 'Pat record is not yet destroyed');
118+
assert.true(Pat.isDestroying, 'Pat record is destroying');
119+
assert.strictEqual(people.length, 1, 'precond - one person left in the store');
120+
121+
Matt.rollbackAttributes();
122+
assert.false(Matt.isDestroyed, 'Matt record is not yet destroyed');
123+
assert.true(Matt.isDestroying, 'Matt record is destroying');
124+
assert.strictEqual(people.length, 0, 'precond - no people left in the store');
125+
126+
await settled();
127+
128+
assert.true(Pat.isDestroyed, 'Pat record is destroyed');
129+
assert.true(Pat.isDestroying, 'Pat record is destroying');
130+
assert.true(Matt.isDestroyed, 'Matt record is destroyed');
131+
assert.true(Matt.isDestroying, 'Matt record is destroying');
132+
assert.strictEqual(people.length, 0, 'precond - no people left in the store');
133+
});
134+
63135
test('Unload on a New Record unloads that record safely', async function (assert) {
64136
const store = this.owner.lookup('service:store');
65137
const Matt = store.push({

0 commit comments

Comments
 (0)