Skip to content

Commit

Permalink
Merge pull request #9290 from Automattic/gh-8884
Browse files Browse the repository at this point in the history
feat: proof of concept for using proxies to track changes on arrays
  • Loading branch information
vkarpov15 committed Feb 15, 2021
2 parents f3fee5e + 0953ff5 commit 127567d
Show file tree
Hide file tree
Showing 19 changed files with 1,332 additions and 250 deletions.
8 changes: 4 additions & 4 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -1285,7 +1285,7 @@ Document.prototype.$set = function $set(path, val, type, options) {
if (Array.isArray(val) && this.$__.populated[path]) {
for (let i = 0; i < val.length; ++i) {
if (val[i] instanceof Document) {
val[i] = val[i]._id;
val.set(i, val[i]._id, true);
}
}
}
Expand Down Expand Up @@ -2859,7 +2859,7 @@ Document.prototype.$isValid = function(path) {

Document.prototype.$__reset = function reset() {
let _this = this;
DocumentArray || (DocumentArray = require('./types/documentarray'));
DocumentArray || (DocumentArray = require('./types/DocumentArray'));

this.$__.activePaths
.map('init', 'modify', function(i) {
Expand Down Expand Up @@ -3054,7 +3054,7 @@ Document.prototype.$__setSchema = function(schema) {
*/

Document.prototype.$__getArrayPathsToValidate = function() {
DocumentArray || (DocumentArray = require('./types/documentarray'));
DocumentArray || (DocumentArray = require('./types/DocumentArray'));

// validate all document arrays.
return this.$__.activePaths
Expand Down Expand Up @@ -3082,7 +3082,7 @@ Document.prototype.$__getArrayPathsToValidate = function() {
*/

Document.prototype.$__getAllSubdocs = function() {
DocumentArray || (DocumentArray = require('./types/documentarray'));
DocumentArray || (DocumentArray = require('./types/DocumentArray'));
Embedded = Embedded || require('./types/ArraySubdocument');

function docReducer(doc, seed, path) {
Expand Down
4 changes: 4 additions & 0 deletions lib/helpers/populate/assignRawDocsToIdStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, re
let sid;
let id;

if (rawIds.isMongooseArrayProxy) {
rawIds = rawIds.__array;
}

for (let i = 0; i < rawIds.length; ++i) {
id = rawIds[i];

Expand Down
6 changes: 5 additions & 1 deletion lib/helpers/populate/assignVals.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,11 @@ function valueFilter(val, assignmentOpts, populateOptions) {
Array.prototype.pop.apply(val, []);
}
for (let i = 0; i < ret.length; ++i) {
val[i] = ret[i];
if (val.isMongooseArrayProxy) {
val.set(i, ret[i], true);
} else {
val[i] = ret[i];
}
}
return val;
}
Expand Down
6 changes: 5 additions & 1 deletion lib/helpers/populate/getModelsMapForPopulate.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,11 @@ function convertTo_id(val, schema) {
if (Array.isArray(val)) {
for (let i = 0; i < val.length; ++i) {
if (val[i] != null && val[i].$__ != null) {
val[i] = val[i]._id;
if (val.isMongooseArrayProxy) {
val.set(i, val[i]._id, true);
} else {
val[i] = val[i]._id;
}
}
}
if (val.isMongooseArray && val.$schema()) {
Expand Down
12 changes: 8 additions & 4 deletions lib/schema/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const utils = require('../utils');
const castToNumber = require('./operators/helpers').castToNumber;
const geospatial = require('./operators/geospatial');
const getDiscriminatorByValue = require('../helpers/discriminator/getDiscriminatorByValue');
const { arrayAtomicsSymbol } = require('../helpers/symbols');

let MongooseArray;
let EmbeddedDoc;
Expand Down Expand Up @@ -339,12 +340,15 @@ SchemaArray.prototype.cast = function(value, doc, init, prev, options) {
}
}

const originalValue = value;
const rawValue = Array.isArray(value) ? [...value] : [];
if (!(value && value.isMongooseArray)) {
value = MongooseArray(value, get(options, 'path', null) || this._arrayPath || this.path, doc, this);
value = MongooseArray(rawValue, get(options, 'path', null) || this._arrayPath || this.path, doc, this);
} else if (value && value.isMongooseArray) {
// We need to create a new array, otherwise change tracking will
// update the old doc (gh-4449)
value = MongooseArray(value, get(options, 'path', null) || this._arrayPath || this.path, doc, this);
value = MongooseArray(rawValue, get(options, 'path', null) || this._arrayPath || this.path, doc, this);
value[arrayAtomicsSymbol] = originalValue[arrayAtomicsSymbol];
}

const isPopulated = doc != null && doc.$__ != null && doc.populated(this.path);
Expand All @@ -355,7 +359,7 @@ SchemaArray.prototype.cast = function(value, doc, init, prev, options) {
const caster = this.caster;
if (caster && this.casterConstructor !== Mixed) {
try {
const len = value.length;
const len = rawValue.length;
for (i = 0; i < len; i++) {
const opts = {};
// Perf: creating `arrayPath` is expensive for large arrays.
Expand All @@ -368,7 +372,7 @@ SchemaArray.prototype.cast = function(value, doc, init, prev, options) {
opts.arrayPath = this.caster._arrayParentPath + '.' + i;
}
}
value[i] = caster.applySetters(value[i], doc, init, void 0, opts);
rawValue[i] = caster.applySetters(rawValue[i], doc, init, void 0, opts);
}
} catch (e) {
// rethrow
Expand Down
44 changes: 23 additions & 21 deletions lib/schema/documentarray.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ DocumentArrayPath.prototype.discriminator = function(name, schema, tiedValue) {

DocumentArrayPath.prototype.doValidate = function(array, fn, scope, options) {
// lazy load
MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentarray'));
MongooseDocumentArray || (MongooseDocumentArray = require('../types/DocumentArray'));

const _this = this;
try {
Expand Down Expand Up @@ -316,7 +316,7 @@ DocumentArrayPath.prototype.getDefault = function(scope) {
}

// lazy load
MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentarray'));
MongooseDocumentArray || (MongooseDocumentArray = require('../types/DocumentArray'));

if (!Array.isArray(ret)) {
ret = [ret];
Expand Down Expand Up @@ -352,7 +352,7 @@ DocumentArrayPath.prototype.getDefault = function(scope) {

DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) {
// lazy load
MongooseDocumentArray || (MongooseDocumentArray = require('../types/documentarray'));
MongooseDocumentArray || (MongooseDocumentArray = require('../types/DocumentArray'));

// Skip casting if `value` is the same as the previous value, no need to cast. See gh-9266
if (value != null && value[arrayPathSymbol] != null && value === prev) {
Expand Down Expand Up @@ -389,33 +389,35 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) {
value[arrayPathSymbol] = options.arrayPath;
}

const len = value.length;
const rawArray = value.isMongooseDocumentArrayProxy ? value.__array : value;

const len = rawArray.length;
const initDocumentOptions = { skipId: true, willInit: true };

for (let i = 0; i < len; ++i) {
if (!value[i]) {
if (!rawArray[i]) {
continue;
}

const Constructor = getConstructor(this.casterConstructor, value[i]);

// Check if the document has a different schema (re gh-3701)
if ((value[i].$__) &&
(!(value[i] instanceof Constructor) || value[i][documentArrayParent] !== doc)) {
value[i] = value[i].toObject({
if ((rawArray[i].$__) &&
(!(rawArray[i] instanceof Constructor) || rawArray[i][documentArrayParent] !== doc)) {
rawArray[i] = rawArray[i].toObject({
transform: false,
// Special case: if different model, but same schema, apply virtuals
// re: gh-7898
virtuals: value[i].schema === Constructor.schema
virtuals: rawArray[i].schema === Constructor.schema
});
}

if (value[i] instanceof Subdocument) {
if (rawArray[i] instanceof Subdocument) {
// Might not have the correct index yet, so ensure it does.
if (value[i].__index == null) {
value[i].$setIndex(i);
if (rawArray[i].__index == null) {
rawArray[i].$setIndex(i);
}
} else if (value[i] != null) {
} else if (rawArray[i] != null) {
if (init) {
if (doc) {
selected || (selected = scopePaths(this, doc.$__.selected, init));
Expand All @@ -424,27 +426,27 @@ DocumentArrayPath.prototype.cast = function(value, doc, init, prev, options) {
}

subdoc = new Constructor(null, value, initDocumentOptions, selected, i);
value[i] = subdoc.init(value[i]);
rawArray[i] = subdoc.init(rawArray[i]);
} else {
if (prev && typeof prev.id === 'function') {
subdoc = prev.id(value[i]._id);
subdoc = prev.id(rawArray[i]._id);
}

if (prev && subdoc && utils.deepEqual(subdoc.toObject(_opts), value[i])) {
if (prev && subdoc && utils.deepEqual(subdoc.toObject(_opts), rawArray[i])) {
// handle resetting doc with existing id and same data
subdoc.set(value[i]);
subdoc.set(rawArray[i]);
// if set() is hooked it will have no return value
// see gh-746
value[i] = subdoc;
rawArray[i] = subdoc;
} else {
try {
subdoc = new Constructor(value[i], value, undefined,
subdoc = new Constructor(rawArray[i], value, undefined,
undefined, i);
// if set() is hooked it will have no return value
// see gh-746
value[i] = subdoc;
rawArray[i] = subdoc;
} catch (error) {
const valueInErrorMessage = util.inspect(value[i]);
const valueInErrorMessage = util.inspect(rawArray[i]);
throw new CastError('embedded', valueInErrorMessage,
value[arrayPathSymbol], error, this);
}
Expand Down
130 changes: 130 additions & 0 deletions lib/types/DocumentArray/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use strict';

/*!
* Module dependencies.
*/

const ArrayMethods = require('../array/methods');
const DocumentArrayMethods = require('./methods');
const Document = require('../../document');

const arrayAtomicsSymbol = require('../../helpers/symbols').arrayAtomicsSymbol;
const arrayAtomicsBackupSymbol = require('../../helpers/symbols').arrayAtomicsBackupSymbol;
const arrayParentSymbol = require('../../helpers/symbols').arrayParentSymbol;
const arrayPathSymbol = require('../../helpers/symbols').arrayPathSymbol;
const arraySchemaSymbol = require('../../helpers/symbols').arraySchemaSymbol;

const _basePush = Array.prototype.push;

/**
* DocumentArray constructor
*
* @param {Array} values
* @param {String} path the path to this array
* @param {Document} doc parent document
* @api private
* @return {MongooseDocumentArray}
* @inherits MongooseArray
* @see http://bit.ly/f6CnZU
*/

function MongooseDocumentArray(values, path, doc) {
const arr = [];

const internals = {
[arrayAtomicsSymbol]: {},
[arrayAtomicsBackupSymbol]: void 0,
[arrayPathSymbol]: path,
[arraySchemaSymbol]: void 0,
[arrayParentSymbol]: void 0
};

if (Array.isArray(values)) {
if (values[arrayPathSymbol] === path &&
values[arrayParentSymbol] === doc) {
internals[arrayAtomicsSymbol] = Object.assign({}, values[arrayAtomicsSymbol]);
}
values.forEach(v => {
_basePush.call(arr, v);
});
}
internals[arrayPathSymbol] = path;

// Because doc comes from the context of another function, doc === global
// can happen if there was a null somewhere up the chain (see #3020 && #3034)
// RB Jun 17, 2015 updated to check for presence of expected paths instead
// to make more proof against unusual node environments
if (doc && doc instanceof Document) {
internals[arrayParentSymbol] = doc;
internals[arraySchemaSymbol] = doc.schema.path(path);

// `schema.path()` doesn't drill into nested arrays properly yet, see
// gh-6398, gh-6602. This is a workaround because nested arrays are
// always plain non-document arrays, so once you get to a document array
// nesting is done. Matryoshka code.
while (internals[arraySchemaSymbol] != null &&
internals[arraySchemaSymbol].$isMongooseArray &&
!internals[arraySchemaSymbol].$isMongooseDocumentArray) {
internals[arraySchemaSymbol] = internals[arraySchemaSymbol].casterConstructor;
}
}

const proxy = new Proxy(arr, {
get: function(target, prop) {
if (prop === 'isMongooseArray' ||
prop === 'isMongooseArrayProxy' ||
prop === 'isMongooseDocumentArray' ||
prop === 'isMongooseDocumentArrayProxy') {
return true;
}
if (prop === '__array') {
return arr;
}
if (prop === 'set') {
return set;
}
if (internals.hasOwnProperty(prop)) {
return internals[prop];
}
if (DocumentArrayMethods.hasOwnProperty(prop)) {
return DocumentArrayMethods[prop];
}
if (ArrayMethods.hasOwnProperty(prop)) {
return ArrayMethods[prop];
}

return arr[prop];
},
set: function(target, prop, value) {
if (typeof prop === 'string' && /^\d+$/.test(prop)) {
set.call(proxy, prop, value);
} else if (internals.hasOwnProperty(prop)) {
internals[prop] = value;
} else {
arr[prop] = value;
}

return true;
}
});

return proxy;
}

function set(i, val, skipModified) {
const arr = this.__array;
if (skipModified) {
arr[i] = val;
return arr;
}
const value = DocumentArrayMethods._cast.call(this, val, i);
arr[i] = value;
DocumentArrayMethods._markModified.call(this, i);
return arr;
}

/*!
* Module exports.
*/

module.exports = MongooseDocumentArray;
Loading

0 comments on commit 127567d

Please sign in to comment.