Skip to content
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] resolve issues with RecordArray sync for peekAll #5378

Merged
merged 6 commits into from
Mar 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion addon/-private/system/model/internal-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ export default class InternalModel {
// models to rematerialize their records.

return this._isDematerializing ||
this.hasScheduledDestroy() ||
this.isDestroyed ||
this.currentState.stateName === 'root.deleted.saved' ||
this.isEmpty();
Expand Down Expand Up @@ -381,9 +382,10 @@ export default class InternalModel {
this._isDematerializing = true;
this._record.destroy();
this.destroyRelationships();
this.updateRecordArrays();
this.resetRecord();
}

this.updateRecordArrays();
}

deleteRecord() {
Expand Down
82 changes: 47 additions & 35 deletions addon/-private/system/record-array-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,45 +96,49 @@ export default class RecordArrayManager {
emberRun.schedule('actions', this, this._flush);
}

_flush() {
heimdall.increment(_flush);

let pending = this._pending;
this._pending = Object.create(null);
_flushPendingInternalModelsForModelName(modelName, internalModels) {
let modelsToRemove = [];

for (let modelName in pending) {
let internalModels = pending[modelName];
for (let j = 0; j < internalModels.length; j++) {
let internalModel = internalModels[j];
// mark internalModels, so they can once again be processed by the
// recordArrayManager
internalModel._pendingRecordArrayManagerFlush = false;
// build up a set of models to ensure we have purged correctly;
if (internalModel.isHiddenFromRecordArrays()) {
modelsToRemove.push(internalModel);
}
for (let j = 0; j < internalModels.length; j++) {
let internalModel = internalModels[j];
// mark internalModels, so they can once again be processed by the
// recordArrayManager
internalModel._pendingRecordArrayManagerFlush = false;
// build up a set of models to ensure we have purged correctly;
if (internalModel.isHiddenFromRecordArrays()) {
modelsToRemove.push(internalModel);
}
}

// process filteredRecordArrays
if (this._filteredRecordArrays[modelName]) {
let recordArrays = this.filteredRecordArraysFor(modelName);
for (let i = 0; i < recordArrays.length; i++) {
this.updateFilterRecordArray(recordArrays[i], modelName, internalModels);
}
// process filteredRecordArrays
if (this._filteredRecordArrays[modelName]) {
let recordArrays = this.filteredRecordArraysFor(modelName);
for (let i = 0; i < recordArrays.length; i++) {
this.updateFilterRecordArray(recordArrays[i], modelName, internalModels);
}
}

let array = this._liveRecordArrays[modelName];
if (array) {
// TODO: skip if it only changed
// process liveRecordArrays
this.updateLiveRecordArray(array, internalModels);
}
let array = this._liveRecordArrays[modelName];
if (array) {
// TODO: skip if it only changed
// process liveRecordArrays
this.updateLiveRecordArray(array, internalModels);
}

// process adapterPopulatedRecordArrays
if (modelsToRemove.length > 0) {
removeFromAdapterPopulatedRecordArrays(modelsToRemove);
}
// process adapterPopulatedRecordArrays
if (modelsToRemove.length > 0) {
removeFromAdapterPopulatedRecordArrays(modelsToRemove);
}
}

_flush() {
heimdall.increment(_flush);

let pending = this._pending;
this._pending = Object.create(null);

for (let modelName in pending) {
this._flushPendingInternalModelsForModelName(modelName, pending[modelName]);
}
}

Expand Down Expand Up @@ -177,10 +181,11 @@ export default class RecordArrayManager {
if (shouldBeRemoved.length > 0) { array._removeInternalModels(shouldBeRemoved); }
}

// TODO: remove, utilize existing flush code but make it flush sync based on 1 modelName
_syncLiveRecordArray(array, modelName) {
assert(`recordArrayManger.syncLiveRecordArray expects modelName not modelClass as the second param`, typeof modelName === 'string');
let hasNoPotentialDeletions = Object.keys(this._pending).length === 0;
let pending = this._pending[modelName];
let hasPendingChanges = Array.isArray(pending);
let hasNoPotentialDeletions = !hasPendingChanges || pending.length === 0;
let map = this.store._internalModelsFor(modelName);
let hasNoInsertionsOrRemovals = get(map, 'length') === get(array, 'length');

Expand All @@ -194,6 +199,11 @@ export default class RecordArrayManager {
return;
}

if (hasPendingChanges) {
this._flushPendingInternalModelsForModelName(modelName, pending);
delete pending[modelName];
}

let internalModels = this._visibleInternalModelsByType(modelName);
let modelsToAdd = [];
for (let i = 0; i < internalModels.length; i++) {
Expand All @@ -205,7 +215,9 @@ export default class RecordArrayManager {
}
}

array._pushInternalModels(modelsToAdd);
if (modelsToAdd.length) {
array._pushInternalModels(modelsToAdd);
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
"ember-publisher": "0.0.7",
"ember-qunit-assert-helpers": "^0.2.1",
"ember-resolver": "^4.1.0",
"ember-source": "~2.18.0",
"ember-source": "~3.0.0",
"ember-source-channel-url": "^1.0.1",
"ember-try": "^0.2.23",
"ember-watson": "^0.7.0",
Expand Down
144 changes: 144 additions & 0 deletions tests/helpers/watch-property.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import Ember from 'ember';
import QUnit from 'qunit';

const {
addObserver,
removeObserver
} = Ember;

function makeCounter() {
let count = 0;
const counter = Object.create(null);
counter.reset = function resetCounter() { count = 0; };

Object.defineProperty(counter, 'count', {
get() { return count; },
set() {},
configurable: false,
enumerable: true
});

Object.freeze(counter);

function increment() {
count++;
}

return { counter, increment };
}

export function watchProperty(obj, propertyName) {
let { counter, increment } = makeCounter();

function observe() {
increment();
}

addObserver(obj, propertyName, observe);

function unwatch() {
removeObserver(obj, propertyName, observe);
}

return { counter, unwatch };
}

export function watchProperties(obj, propertyNames) {
let watched = {};
let counters = {};

if (!Array.isArray(propertyNames)) {
throw new Error(`Must call watchProperties with an array of propertyNames to watch, received ${propertyNames}`);
}

for (let i = 0; i < propertyNames.length; i++) {
let propertyName = propertyNames[i];

if (watched[propertyName] !== undefined) {
throw new Error(`Cannot watch the same property ${propertyName} more than once`);
}

let { counter, increment } = makeCounter();
watched[propertyName] = increment;
counters[propertyName] = counter;

addObserver(obj, propertyName, increment);
}

function unwatch() {
Object.keys(watched).forEach((propertyName) => {
removeObserver(obj, propertyName, watched[propertyName]);
});
}

return { counters, unwatch };
}

QUnit.assert.watchedPropertyCounts = function assertWatchedPropertyCount(watchedObject, expectedCounts, label = '') {
if (!watchedObject || !watchedObject.counters) {
throw new Error('Expected to receive the return value of watchProperties: an object containing counters');
}

let counters = watchedObject.counters;

Object.keys(expectedCounts).forEach((propertyName) => {
let counter = counters[propertyName];
let expectedCount = expectedCounts[propertyName];
let assertionText = label;

if (Array.isArray(expectedCount)) {
label = expectedCount[1];
expectedCount = expectedCount[0];
}

assertionText += ` | Expected ${expectedCount} change notifications for ${propertyName} but recieved ${counter.count}`;

if (counter === undefined) {
throw new Error(`Cannot assert expected count for ${propertyName} as there is no watcher for that property`);
}

this.pushResult({
result: counter.count === expectedCount,
actual: counter.count,
expected: expectedCount,
message: assertionText
});
});
};

QUnit.assert.watchedPropertyCount = function assertWatchedPropertyCount(watcher, expectedCount, label) {
let counter;
if (!watcher) {
throw new Error(`Expected to receive a watcher`);
}

// this allows us to handle watchers passed in from a watchProperties return hash
if (!watcher.counter && watcher.count !== undefined) {
counter = watcher;
} else {
counter = watcher.counter;
}

this.pushResult({
result: counter.count === expectedCount,
actual: counter.count,
expected: expectedCount,
message: label
});
};

QUnit.assert.dirties = function assertDirties(options, updateMethodCallback, label) {
let { object: obj, property, count } = options;
count = typeof count === 'number' ? count : 1;
let { counter, unwatch } = watchProperty(obj, property);
updateMethodCallback();
this.pushResult({
result: counter.count === count,
actual: counter.count,
expected: count,
message: label
});
unwatch();
};


Loading