From 3829f109afa6bd8a30a5b87680509af476a6874a Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Tue, 13 May 2014 10:49:13 +0100 Subject: [PATCH 001/479] Update instructions for running the tests. --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 57921997d..8eb0ba231 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,7 @@ $ coffee server.coffee Run the tests with ``` -$ npm install -g grunt-cli -$ grunt test +$ npm test ``` ## Usage From 94cd7f5d1839e91055c5aa3a6b33f60f0e23405c Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Wed, 14 May 2014 10:07:31 +0100 Subject: [PATCH 002/479] Test that recreates bug in remove event where two items are deleted. --- test/Model/MockConnectionModel.coffee | 3 ++ test/Model/refList.mocha.coffee | 59 ++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/test/Model/MockConnectionModel.coffee b/test/Model/MockConnectionModel.coffee index cdc177314..882753a7f 100644 --- a/test/Model/MockConnectionModel.coffee +++ b/test/Model/MockConnectionModel.coffee @@ -15,3 +15,6 @@ MockConnectionModel::createConnection = -> onopen: -> onconnecting: -> @root.shareConnection = new share.client.Connection(socketMock) +# Invoke the callback immediately, for bundling +MockConnectionModel::whenNothingPending = (cb) -> + cb() diff --git a/test/Model/refList.mocha.coffee b/test/Model/refList.mocha.coffee index ee1afaf0b..77fdbb865 100644 --- a/test/Model/refList.mocha.coffee +++ b/test/Model/refList.mocha.coffee @@ -1,5 +1,5 @@ {expect} = require '../util' -Model = require '../../lib/Model' +Model = require './MockConnectionModel' # '../../lib/Model' describe 'refList', -> @@ -520,3 +520,60 @@ describe 'refList', -> id: 'green' rgb: [0, 255, 0] hex: '#0f0' + + it 'correctly dereferences nested lists when items are removed', (done) -> + serverModel = new Model() + serverModel.createConnection() + serverModel.getOrCreateDoc 'test', 'one' + + model = serverModel.ref '_page.test', 'test.one' + model.set 'colors', + green: + id: 'green' + rgb: [0, 255, 0] + hex: '#0f0' + red: + id: 'red' + rgb: [255, 0, 0] + hex: '#f00' + blue: + id: 'blue' + rgb: [0, 0, 255] + hex: '#00f' + white: + id: 'white' + rgb: [255, 255, 255] + hex: '#fff' + model.set 'palettes', + nature: + id: 'nature' + colors: ['green', 'blue', 'white'] + flag: + id: 'flag' + colors: ['red', 'white', 'blue'] + model.set 'schemes', ['nature', 'flag'] + + choices = serverModel.refList '_page.choices', model.path('palettes'), model.path('schemes'), {deleteRemoved: true} + choice = serverModel.ref '_page.choice', choices.path(0), {updateIndices: true} + paint = serverModel.refList '_page.paint', model.path('colors'), choice.path('colors'), {deleteRemoved: true} + + model.bundle (err, bundle) -> + # Fake a version number for the non-_page collection + bundle.collections.test.one.v = 1 + + clientModel = new Model() + clientModel.createConnection() + clientModel.unbundle(bundle) + + #events = 0 + list = clientModel.scope '_page.paint' + list.on 'remove', '', (index, removed) -> + expect(index).to.equal 1 + #console.log removed[0].id + expect(removed).to.eql [ + {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} + ] + done() #if ++events == 2 + + list.remove 1 + From 381a289355b156a5cfae8b8e5b4102438a70dc3d Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Wed, 14 May 2014 10:25:16 +0100 Subject: [PATCH 003/479] Simplify test to only use the _page. --- test/Model/refList.mocha.coffee | 43 ++++++++++----------------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/test/Model/refList.mocha.coffee b/test/Model/refList.mocha.coffee index 77fdbb865..607494f72 100644 --- a/test/Model/refList.mocha.coffee +++ b/test/Model/refList.mocha.coffee @@ -521,29 +521,16 @@ describe 'refList', -> rgb: [0, 255, 0] hex: '#0f0' - it 'correctly dereferences nested lists when items are removed', (done) -> - serverModel = new Model() - serverModel.createConnection() - serverModel.getOrCreateDoc 'test', 'one' - - model = serverModel.ref '_page.test', 'test.one' - model.set 'colors', - green: - id: 'green' - rgb: [0, 255, 0] - hex: '#0f0' - red: - id: 'red' - rgb: [255, 0, 0] - hex: '#f00' - blue: - id: 'blue' - rgb: [0, 0, 255] - hex: '#00f' - white: - id: 'white' - rgb: [255, 255, 255] - hex: '#fff' + it 'correctly dereferences chained lists/refs when items are removed', (done) -> + model = setup() + model.add 'colors', + id: 'blue' + rgb: [0, 0, 255] + hex: '#00f' + model.add 'colors', + id: 'white' + rgb: [255, 255, 255] + hex: '#fff' model.set 'palettes', nature: id: 'nature' @@ -553,16 +540,12 @@ describe 'refList', -> colors: ['red', 'white', 'blue'] model.set 'schemes', ['nature', 'flag'] - choices = serverModel.refList '_page.choices', model.path('palettes'), model.path('schemes'), {deleteRemoved: true} - choice = serverModel.ref '_page.choice', choices.path(0), {updateIndices: true} - paint = serverModel.refList '_page.paint', model.path('colors'), choice.path('colors'), {deleteRemoved: true} + choices = model.refList 'choices', 'palettes', 'schemes', {deleteRemoved: true} + choice = model.ref 'choice', 'choices.0', {updateIndices: true} + paint = model.refList 'paint', 'colors', 'choice.colors', {deleteRemoved: true} model.bundle (err, bundle) -> - # Fake a version number for the non-_page collection - bundle.collections.test.one.v = 1 - clientModel = new Model() - clientModel.createConnection() clientModel.unbundle(bundle) #events = 0 From 00ee88e35720a354256426c6232087b51acd0942 Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Wed, 14 May 2014 10:35:52 +0100 Subject: [PATCH 004/479] Simplify test further, it's not related to bundling or deleteRemoved/updateIndices. --- test/Model/MockConnectionModel.coffee | 3 -- test/Model/refList.mocha.coffee | 71 +++++++++++++-------------- 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/test/Model/MockConnectionModel.coffee b/test/Model/MockConnectionModel.coffee index 882753a7f..cdc177314 100644 --- a/test/Model/MockConnectionModel.coffee +++ b/test/Model/MockConnectionModel.coffee @@ -15,6 +15,3 @@ MockConnectionModel::createConnection = -> onopen: -> onconnecting: -> @root.shareConnection = new share.client.Connection(socketMock) -# Invoke the callback immediately, for bundling -MockConnectionModel::whenNothingPending = (cb) -> - cb() diff --git a/test/Model/refList.mocha.coffee b/test/Model/refList.mocha.coffee index 607494f72..eff6aa9b2 100644 --- a/test/Model/refList.mocha.coffee +++ b/test/Model/refList.mocha.coffee @@ -502,25 +502,6 @@ describe 'refList', -> expect(model.get 'arrayIds').to.have.length(1) - describe 'deleteRemoved', -> - it 'deletes the underlying object when an item is removed', -> - model = setup {deleteRemoved: true} - expect(model.get 'colors').to.eql - green: - id: 'green' - rgb: [0, 255, 0] - hex: '#0f0' - red: - id: 'red' - rgb: [255, 0, 0] - hex: '#f00' - model.remove 'list', 0 - expect(model.get 'colors').to.eql - green: - id: 'green' - rgb: [0, 255, 0] - hex: '#0f0' - it 'correctly dereferences chained lists/refs when items are removed', (done) -> model = setup() model.add 'colors', @@ -540,23 +521,39 @@ describe 'refList', -> colors: ['red', 'white', 'blue'] model.set 'schemes', ['nature', 'flag'] - choices = model.refList 'choices', 'palettes', 'schemes', {deleteRemoved: true} - choice = model.ref 'choice', 'choices.0', {updateIndices: true} - paint = model.refList 'paint', 'colors', 'choice.colors', {deleteRemoved: true} - - model.bundle (err, bundle) -> - clientModel = new Model() - clientModel.unbundle(bundle) - - #events = 0 - list = clientModel.scope '_page.paint' - list.on 'remove', '', (index, removed) -> - expect(index).to.equal 1 - #console.log removed[0].id - expect(removed).to.eql [ - {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} - ] - done() #if ++events == 2 + choices = model.refList 'choices', 'palettes', 'schemes' + choice = model.ref 'choice', 'choices.0' + paint = model.refList 'paint', 'colors', 'choice.colors' + + #events = 0 + list = model.scope '_page.paint' + list.on 'remove', '', (index, removed) -> + expect(index).to.equal 1 + #console.log removed[0].id + expect(removed).to.eql [ + {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} + ] + done() #if ++events == 2 + + list.remove 1 - list.remove 1 + describe 'deleteRemoved', -> + + it 'deletes the underlying object when an item is removed', -> + model = setup {deleteRemoved: true} + expect(model.get 'colors').to.eql + green: + id: 'green' + rgb: [0, 255, 0] + hex: '#0f0' + red: + id: 'red' + rgb: [255, 0, 0] + hex: '#f00' + model.remove 'list', 0 + expect(model.get 'colors').to.eql + green: + id: 'green' + rgb: [0, 255, 0] + hex: '#0f0' From 47a1f63c849282f4dceeb90becda80682dad3cd7 Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Wed, 14 May 2014 10:42:23 +0100 Subject: [PATCH 005/479] Revert to using Model. --- test/Model/refList.mocha.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Model/refList.mocha.coffee b/test/Model/refList.mocha.coffee index eff6aa9b2..ac3ddd059 100644 --- a/test/Model/refList.mocha.coffee +++ b/test/Model/refList.mocha.coffee @@ -1,5 +1,5 @@ {expect} = require '../util' -Model = require './MockConnectionModel' # '../../lib/Model' +Model = require '../../lib/Model' describe 'refList', -> From 19c5890758e03a2d7ea3267d48ed3b5d36f962b8 Mon Sep 17 00:00:00 2001 From: Artur Zayats Date: Thu, 25 Sep 2014 16:44:49 +0400 Subject: [PATCH 006/479] fix pathQuery issue clone ids-array, add some checks --- lib/Model/Query.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 189893aab..9b993bcd2 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -65,8 +65,8 @@ Model.prototype._initQueries = function(items) { // a more correct thing to do would be to get the actual value for the // path before creating the query subscription. This feature should // probably be rethought. - if (query.isPathQuery) { - this._setNull(expression, ids); + if (query.isPathQuery && expression.length > 0 && this._isLocal(expression[0])) { + this._setNull(expression, [].concat(ids)); } if (extra !== void 0) { From bdb97afceb113f95c80cd875d93a93be0009691a Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 5 Oct 2014 02:34:58 -0700 Subject: [PATCH 007/479] update dependencies and move to uuid instead of node-uuid --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7ec0bf9ce..bc36a5fed 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "test": "./node_modules/.bin/mocha test/**/*.mocha.coffee && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" }, "dependencies": { - "arraydiff": "~0.1.1", - "deep-is": "~0.1.1", - "node-uuid": "~1.4.1", - "share": "~0.7.1" + "arraydiff": "^0.1.1", + "deep-is": "^0.1.3", + "uuid": "^2.0.1", + "share": "^0.7.3" }, "devDependencies": { "coffee-script": "~1.7.1", From 14aed84b3c164dc19707348d8075d5de351feb11 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 5 Oct 2014 02:35:58 -0700 Subject: [PATCH 008/479] fix uuid require --- lib/Model/Model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/Model.js b/lib/Model/Model.js index 26a79ada7..575e58a1e 100644 --- a/lib/Model/Model.js +++ b/lib/Model/Model.js @@ -1,4 +1,4 @@ -var uuid = require('node-uuid'); +var uuid = require('uuid'); Model.INITS = []; From 69fbb8131f64139362c9147ce7883fbbe4af0765 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 8 Oct 2014 22:05:16 -0700 Subject: [PATCH 009/479] 0.6.0-alpha22 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc36a5fed..d6351b5b9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha21", + "version": "0.6.0-alpha22", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.coffee && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 4c5ef990f505654ac85c17871fafdecaf07483f1 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 30 Oct 2014 15:09:38 -0700 Subject: [PATCH 010/479] cleanup test --- test/Model/refList.mocha.coffee | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/test/Model/refList.mocha.coffee b/test/Model/refList.mocha.coffee index ac3ddd059..adf310529 100644 --- a/test/Model/refList.mocha.coffee +++ b/test/Model/refList.mocha.coffee @@ -136,7 +136,7 @@ describe 'refList', -> ] it 'emits on `from` when `ids` are inserted', (done) -> - model = setup() + model = setup() model.on 'all', 'list**', (capture, method, index, inserted) -> expect(capture).to.equal '' expect(method).to.equal 'insert' @@ -207,14 +207,14 @@ describe 'refList', -> id = model.add 'profiles', { tagIds: tagIds } model.push 'profileIds', id model.refList 'profilesList', 'profiles', 'profileIds' - + #ref a single item from collection model.ref 'profile', 'profilesList.0' - + #remove from nested refList model.refList 'tagsList', 'tags', 'profile.tagIds' model.remove('tagsList', 0) - + describe 'updates on `to` mutations', -> it 'updates the value when `to` is set', -> @@ -429,7 +429,7 @@ describe 'refList', -> red: {id: 'red', rgb: [255, 0, 0], hex: '#f00'} blue: {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} yellow: {id: 'yellow', rgb: [255, 255, 0], hex: '#ff0'} - + model.at('list.0').remove() expect(model.get 'ids').to.eql ['yellow', 'red'] expect(model.get 'colors').to.eql @@ -521,21 +521,17 @@ describe 'refList', -> colors: ['red', 'white', 'blue'] model.set 'schemes', ['nature', 'flag'] - choices = model.refList 'choices', 'palettes', 'schemes' - choice = model.ref 'choice', 'choices.0' + model.refList 'choices', 'palettes', 'schemes' + model.ref 'choice', 'choices.0' paint = model.refList 'paint', 'colors', 'choice.colors' - #events = 0 - list = model.scope '_page.paint' - list.on 'remove', '', (index, removed) -> + paint.on 'remove', (index, removed) -> expect(index).to.equal 1 - #console.log removed[0].id expect(removed).to.eql [ {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} ] - done() #if ++events == 2 - - list.remove 1 + done() + paint.remove 1 describe 'deleteRemoved', -> From 136cddf98536da6a826b0d17438cf2c8f91d7743 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 30 Oct 2014 16:22:18 -0700 Subject: [PATCH 011/479] convert tests from coffee to js --- package.json | 3 +- test/Model/LocalDoc.mocha.coffee | 19 - test/Model/LocalDoc.mocha.js | 22 + test/Model/MockConnectionModel.coffee | 17 - test/Model/MockConnectionModel.js | 22 + test/Model/RemoteDoc.mocha.coffee | 20 - test/Model/RemoteDoc.mocha.js | 21 + test/Model/docs.coffee | 187 ------ test/Model/docs.js | 231 +++++++ test/Model/events.mocha.coffee | 134 ---- test/Model/events.mocha.js | 139 ++++ test/Model/filter.mocha.coffee | 18 +- test/Model/filter.mocha.js | 205 ++++++ test/Model/fn.mocha.coffee | 202 ------ test/Model/fn.mocha.js | 290 +++++++++ test/Model/ref.mocha.coffee | 204 ------ test/Model/ref.mocha.js | 214 +++++++ test/Model/refList.mocha.coffee | 555 ---------------- test/Model/refList.mocha.js | 882 ++++++++++++++++++++++++++ test/mocha.opts | 1 - test/util/util.mocha.js | 65 ++ 21 files changed, 2104 insertions(+), 1347 deletions(-) delete mode 100644 test/Model/LocalDoc.mocha.coffee create mode 100644 test/Model/LocalDoc.mocha.js delete mode 100644 test/Model/MockConnectionModel.coffee create mode 100644 test/Model/MockConnectionModel.js delete mode 100644 test/Model/RemoteDoc.mocha.coffee create mode 100644 test/Model/RemoteDoc.mocha.js delete mode 100644 test/Model/docs.coffee create mode 100644 test/Model/docs.js delete mode 100644 test/Model/events.mocha.coffee create mode 100644 test/Model/events.mocha.js create mode 100644 test/Model/filter.mocha.js delete mode 100644 test/Model/fn.mocha.coffee create mode 100644 test/Model/fn.mocha.js delete mode 100644 test/Model/ref.mocha.coffee create mode 100644 test/Model/ref.mocha.js delete mode 100644 test/Model/refList.mocha.coffee create mode 100644 test/Model/refList.mocha.js create mode 100644 test/util/util.mocha.js diff --git a/package.json b/package.json index d6351b5b9..dfe1cbe1e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "version": "0.6.0-alpha22", "main": "./lib/index.js", "scripts": { - "test": "./node_modules/.bin/mocha test/**/*.mocha.coffee && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" + "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" }, "dependencies": { "arraydiff": "^0.1.1", @@ -18,7 +18,6 @@ "share": "^0.7.3" }, "devDependencies": { - "coffee-script": "~1.7.1", "expect.js": "~0.3.1", "jshint": "~2.4.4", "mocha": "~1.17.1" diff --git a/test/Model/LocalDoc.mocha.coffee b/test/Model/LocalDoc.mocha.coffee deleted file mode 100644 index ca44e7d1f..000000000 --- a/test/Model/LocalDoc.mocha.coffee +++ /dev/null @@ -1,19 +0,0 @@ -{expect} = require '../util' -LocalDoc = require '../../lib/Model/LocalDoc' -docs = require './docs' - -describe 'LocalDoc', -> - - createDoc = -> - modelMock = - data: - _colors: {} - new LocalDoc modelMock, '_colors', 'green' - - describe 'create', -> - it 'should set the collectionName and id properties', -> - doc = createDoc() - expect(doc.collectionName).to.equal '_colors' - expect(doc.id).to.equal 'green' - - docs createDoc diff --git a/test/Model/LocalDoc.mocha.js b/test/Model/LocalDoc.mocha.js new file mode 100644 index 000000000..704289bb7 --- /dev/null +++ b/test/Model/LocalDoc.mocha.js @@ -0,0 +1,22 @@ +var expect = require('../util').expect; +var LocalDoc = require('../../lib/Model/LocalDoc'); +var docs = require('./docs'); + +describe('LocalDoc', function() { + function createDoc() { + var modelMock = { + data: { + _colors: {} + } + }; + return new LocalDoc(modelMock, '_colors', 'green'); + } + describe('create', function() { + it('should set the collectionName and id properties', function() { + var doc = createDoc(); + expect(doc.collectionName).to.equal('_colors'); + expect(doc.id).to.equal('green'); + }); + }); + docs(createDoc); +}); diff --git a/test/Model/MockConnectionModel.coffee b/test/Model/MockConnectionModel.coffee deleted file mode 100644 index cdc177314..000000000 --- a/test/Model/MockConnectionModel.coffee +++ /dev/null @@ -1,17 +0,0 @@ -share = require 'share' -Model = require '../../lib/Model' - -# Mock up a connection with a fake socket -module.exports = MockConnectionModel = -> - Model.apply this, arguments -MockConnectionModel:: = Object.create Model:: -MockConnectionModel::createConnection = -> - socketMock = - send: (message) -> - close: -> - onmessage: -> - onclose: -> - onerror: -> - onopen: -> - onconnecting: -> - @root.shareConnection = new share.client.Connection(socketMock) diff --git a/test/Model/MockConnectionModel.js b/test/Model/MockConnectionModel.js new file mode 100644 index 000000000..f029ff28f --- /dev/null +++ b/test/Model/MockConnectionModel.js @@ -0,0 +1,22 @@ +var share = require('share'); +var Model = require('../../lib/Model'); + +module.exports = MockConnectionModel; +function MockConnectionModel() { + Model.apply(this, arguments); +} +MockConnectionModel.prototype = Object.create(Model.prototype); + +MockConnectionModel.prototype.createConnection = function() { + var socketMock; + socketMock = { + send: function(message) {}, + close: function() {}, + onmessage: function() {}, + onclose: function() {}, + onerror: function() {}, + onopen: function() {}, + onconnecting: function() {} + }; + this.root.shareConnection = new share.client.Connection(socketMock); +}; diff --git a/test/Model/RemoteDoc.mocha.coffee b/test/Model/RemoteDoc.mocha.coffee deleted file mode 100644 index 6d40748fa..000000000 --- a/test/Model/RemoteDoc.mocha.coffee +++ /dev/null @@ -1,20 +0,0 @@ -{expect} = require '../util' -Model = require './MockConnectionModel' -RemoteDoc = require '../../lib/Model/RemoteDoc' -docs = require './docs' - -describe 'RemoteDoc', -> - - createDoc = -> - model = new Model - model.createConnection() - model.data.colors = {} - return new RemoteDoc model, 'colors', 'green' - - describe 'create', -> - it 'should set the collectionName and id properties', -> - doc = createDoc() - expect(doc.collectionName).to.equal 'colors' - expect(doc.id).to.equal 'green' - - docs createDoc diff --git a/test/Model/RemoteDoc.mocha.js b/test/Model/RemoteDoc.mocha.js new file mode 100644 index 000000000..0ea1eb78d --- /dev/null +++ b/test/Model/RemoteDoc.mocha.js @@ -0,0 +1,21 @@ +var expect = require('../util').expect; +var Model = require('./MockConnectionModel'); +var RemoteDoc = require('../../lib/Model/RemoteDoc'); +var docs = require('./docs'); + +describe('RemoteDoc', function() { + function createDoc() { + var model = new Model; + model.createConnection(); + model.data.colors = {}; + return new RemoteDoc(model, 'colors', 'green'); + }; + describe('create', function() { + it('should set the collectionName and id properties', function() { + var doc = createDoc(); + expect(doc.collectionName).to.equal('colors'); + expect(doc.id).to.equal('green'); + }); + }); + docs(createDoc); +}); diff --git a/test/Model/docs.coffee b/test/Model/docs.coffee deleted file mode 100644 index 9c882404c..000000000 --- a/test/Model/docs.coffee +++ /dev/null @@ -1,187 +0,0 @@ -{expect} = require '../util' - -module.exports = (createDoc) -> - - describe 'get', -> - - it 'creates an undefined doc', -> - doc = createDoc() - expect(doc.get()).eql undefined - - it 'gets a defined doc', -> - doc = createDoc() - doc.set [], {id: 'green'}, -> - expect(doc.get()).eql {id: 'green'} - - it 'gets a property on an undefined document', -> - doc = createDoc() - expect(doc.get ['x']).eql undefined - - it 'gets an undefined property', -> - doc = createDoc() - doc.set [], {}, -> - expect(doc.get ['x']).eql undefined - - it 'gets a defined property', -> - doc = createDoc() - doc.set [], {'id': 'green'}, -> - expect(doc.get ['id']).eql 'green' - - it 'gets a false property', -> - doc = createDoc() - doc.set [], {shown: false}, -> - expect(doc.get ['shown']).eql false - - it 'gets a null property', -> - doc = createDoc() - doc.set [], {shown: null}, -> - expect(doc.get ['shown']).eql null - - it 'gets a method property', -> - doc = createDoc() - doc.set [], {empty: ''}, -> - expect(doc.get ['empty', 'charAt']).eql ''.charAt - - it 'gets an array member', -> - doc = createDoc() - doc.set [], {rgb: [0, 255, 0]}, -> - expect(doc.get ['rgb', '1']).eql 255 - - it 'gets an array length', -> - doc = createDoc() - doc.set [], {rgb: [0, 255, 0]}, -> - expect(doc.get ['rgb', 'length']).eql 3 - - describe 'set', -> - - it 'sets a property', -> - doc = createDoc() - previous = doc.set ['shown'], false, -> - expect(previous).equal undefined - expect(doc.get(['shown'])).eql false - - it 'sets a multi-nested property', -> - doc = createDoc() - previous = doc.set ['rgb', 'green', 'float'], 1, -> - expect(previous).equal undefined - expect(doc.get(['rgb'])).eql {green: {float: 1}} - - it 'sets on an existing document', -> - doc = createDoc() - previous = doc.set [], {id: 'green'}, -> - expect(previous).equal undefined - expect(doc.get()).eql {id: 'green'} - previous = doc.set ['shown'], false, -> - expect(previous).equal undefined - expect(doc.get()).eql {id: 'green', shown: false} - - it 'returns the previous value on set', -> - doc = createDoc() - previous = doc.set ['shown'], false, -> - expect(previous).equal undefined - expect(doc.get(['shown'])).eql false - previous = doc.set ['shown'], true, -> - expect(previous).equal false - expect(doc.get(['shown'])).eql true - - it 'creates an implied array on set', -> - doc = createDoc() - doc.set ['rgb', '2'], 0, -> - doc.set ['rgb', '1'], 255, -> - doc.set ['rgb', '0'], 127, -> - expect(doc.get(['rgb'])).eql [127, 255, 0] - - it 'creates an implied object on an array', -> - doc = createDoc() - doc.set ['colors'], [], -> - doc.set ['colors', '0', 'value'], 'green', -> - expect(doc.get(['colors'])).eql [{value: 'green'}] - - describe 'del', -> - - it 'can del on an undefined path without effect', -> - doc = createDoc() - previous = doc.del ['rgb', '2'], -> - expect(previous).equal undefined - expect(doc.get()).eql undefined - - it 'can del on a document', -> - doc = createDoc() - doc.set [], {id: 'green'}, -> - previous = doc.del [], -> - expect(previous).eql {id: 'green'} - expect(doc.get()).eql undefined - - it 'can del on a nested property', -> - doc = createDoc() - doc.set ['rgb'], [ - {float: 0, int: 0} - {float: 1, int: 255} - {float: 0, int: 0} - ], -> - previous = doc.del ['rgb', '0', 'float'], -> - expect(previous).eql 0 - expect(doc.get ['rgb']).eql [ - {int: 0} - {float: 1, int: 255} - {float: 0, int: 0} - ] - - describe 'push', -> - - it 'can push on an undefined property', -> - doc = createDoc() - len = doc.push ['friends'], 'jim', -> - expect(len).equal 1 - expect(doc.get(['friends'])).eql ['jim'] - - it 'can push on a defined array', -> - doc = createDoc() - len = doc.push ['friends'], 'jim', -> - expect(len).equal 1 - len = doc.push ['friends'], 'sue', -> - expect(len).equal 2 - expect(doc.get(['friends'])).eql ['jim', 'sue'] - - it 'throws a TypeError when pushing on a non-array', (done) -> - doc = createDoc() - doc.set ['friends'], {}, -> - doc.push ['friends'], ['x'], (err) -> - expect(err).a TypeError - done() - - describe 'move', -> - - it 'can move an item from the end to the beginning of the array', -> - doc = createDoc() - doc.set ['array'], [0, 1, 2, 3, 4], -> - - moved = doc.move ['array'], 4, 0, 1, -> - expect(moved).eql [4] - expect(doc.get(['array'])).eql [4, 0, 1, 2, 3] - - it 'can swap the first two items in the array', -> - doc = createDoc() - doc.set ['array'], [0, 1, 2, 3, 4], -> - - moved = doc.move ['array'], 1, 0, 1, -> - expect(moved).eql [1] - expect(doc.get(['array'])).eql [1, 0, 2, 3, 4] - - it 'can move an item from the begnning to the end of the array', -> - doc = createDoc() - doc.set ['array'], [0, 1, 2, 3, 4], -> - - # note that destination is index after removal of item - moved = doc.move ['array'], 0, 4, 1, -> - expect(moved).eql [0] - expect(doc.get(['array'])).eql [1, 2, 3, 4, 0] - - it 'can move several items mid-array, with an event for each', -> - doc = createDoc() - doc.set ['array'], [0, 1, 2, 3, 4], -> - - # note that destination is index after removal of items - moved = doc.move ['array'], 1, 3, 2, -> - expect(moved).eql [1, 2] - expect(doc.get(['array'])).eql [0, 3, 4, 1, 2] diff --git a/test/Model/docs.js b/test/Model/docs.js new file mode 100644 index 000000000..6786b5470 --- /dev/null +++ b/test/Model/docs.js @@ -0,0 +1,231 @@ +var expect = require('../util').expect; + +module.exports = function(createDoc) { + describe('get', function() { + it('creates an undefined doc', function() { + var doc = createDoc(); + expect(doc.get()).eql(void 0); + }); + it('gets a defined doc', function() { + var doc = createDoc(); + doc.set([], { + id: 'green' + }, function() {}); + expect(doc.get()).eql({ + id: 'green' + }); + }); + it('gets a property on an undefined document', function() { + var doc = createDoc(); + expect(doc.get(['x'])).eql(void 0); + }); + it('gets an undefined property', function() { + var doc = createDoc(); + doc.set([], {}, function() {}); + expect(doc.get(['x'])).eql(void 0); + }); + it('gets a defined property', function() { + var doc = createDoc(); + doc.set([], { + 'id': 'green' + }, function() {}); + expect(doc.get(['id'])).eql('green'); + }); + it('gets a false property', function() { + var doc = createDoc(); + doc.set([], { + shown: false + }, function() {}); + expect(doc.get(['shown'])).eql(false); + }); + it('gets a null property', function() { + var doc = createDoc(); + doc.set([], { + shown: null + }, function() {}); + expect(doc.get(['shown'])).eql(null); + }); + it('gets a method property', function() { + var doc = createDoc(); + doc.set([], { + empty: '' + }, function() {}); + expect(doc.get(['empty', 'charAt'])).eql(''.charAt); + }); + it('gets an array member', function() { + var doc = createDoc(); + doc.set([], { + rgb: [0, 255, 0] + }, function() {}); + expect(doc.get(['rgb', '1'])).eql(255); + }); + it('gets an array length', function() { + var doc = createDoc(); + doc.set([], { + rgb: [0, 255, 0] + }, function() {}); + expect(doc.get(['rgb', 'length'])).eql(3); + }); + }); + describe('set', function() { + it('sets a property', function() { + var doc = createDoc(); + var previous = doc.set(['shown'], false, function() {}); + expect(previous).equal(void 0); + expect(doc.get(['shown'])).eql(false); + }); + it('sets a multi-nested property', function() { + var doc = createDoc(); + var previous = doc.set(['rgb', 'green', 'float'], 1, function() {}); + expect(previous).equal(void 0); + expect(doc.get(['rgb'])).eql({ + green: { + float: 1 + } + }); + }); + it('sets on an existing document', function() { + var doc = createDoc(); + var previous = doc.set([], { + id: 'green' + }, function() {}); + expect(previous).equal(void 0); + expect(doc.get()).eql({ + id: 'green' + }); + previous = doc.set(['shown'], false, function() {}); + expect(previous).equal(void 0); + expect(doc.get()).eql({ + id: 'green', + shown: false + }); + }); + it('returns the previous value on set', function() { + var doc = createDoc(); + var previous = doc.set(['shown'], false, function() {}); + expect(previous).equal(void 0); + expect(doc.get(['shown'])).eql(false); + previous = doc.set(['shown'], true, function() {}); + expect(previous).equal(false); + expect(doc.get(['shown'])).eql(true); + }); + it('creates an implied array on set', function() { + var doc = createDoc(); + doc.set(['rgb', '2'], 0, function() {}); + doc.set(['rgb', '1'], 255, function() {}); + doc.set(['rgb', '0'], 127, function() {}); + expect(doc.get(['rgb'])).eql([127, 255, 0]); + }); + it('creates an implied object on an array', function() { + var doc = createDoc(); + doc.set(['colors'], [], function() {}); + doc.set(['colors', '0', 'value'], 'green', function() {}); + expect(doc.get(['colors'])).eql([ + { + value: 'green' + } + ]); + }); + }); + describe('del', function() { + it('can del on an undefined path without effect', function() { + var doc = createDoc(); + var previous = doc.del(['rgb', '2'], function() {}); + expect(previous).equal(void 0); + expect(doc.get()).eql(void 0); + }); + it('can del on a document', function() { + var doc = createDoc(); + doc.set([], { + id: 'green' + }, function() {}); + var previous = doc.del([], function() {}); + expect(previous).eql({ + id: 'green' + }); + expect(doc.get()).eql(void 0); + }); + it('can del on a nested property', function() { + var doc = createDoc(); + doc.set(['rgb'], [ + { + float: 0, + int: 0 + }, { + float: 1, + int: 255 + }, { + float: 0, + int: 0 + } + ], function() {}); + var previous = doc.del(['rgb', '0', 'float'], function() {}); + expect(previous).eql(0); + expect(doc.get(['rgb'])).eql([ + { + int: 0 + }, { + float: 1, + int: 255 + }, { + float: 0, + int: 0 + } + ]); + }); + }); + describe('push', function() { + it('can push on an undefined property', function() { + var doc = createDoc(); + var len = doc.push(['friends'], 'jim', function() {}); + expect(len).equal(1); + expect(doc.get(['friends'])).eql(['jim']); + }); + it('can push on a defined array', function() { + var doc = createDoc(); + var len = doc.push(['friends'], 'jim', function() {}); + expect(len).equal(1); + len = doc.push(['friends'], 'sue', function() {}); + expect(len).equal(2); + expect(doc.get(['friends'])).eql(['jim', 'sue']); + }); + it('throws a TypeError when pushing on a non-array', function(done) { + var doc = createDoc(); + doc.set(['friends'], {}, function() {}); + doc.push(['friends'], ['x'], function(err) { + expect(err).a(TypeError); + done(); + }); + }); + }); + describe('move', function() { + it('can move an item from the end to the beginning of the array', function() { + var doc = createDoc(); + doc.set(['array'], [0, 1, 2, 3, 4], function() {}); + var moved = doc.move(['array'], 4, 0, 1, function() {}); + expect(moved).eql([4]); + expect(doc.get(['array'])).eql([4, 0, 1, 2, 3]); + }); + it('can swap the first two items in the array', function() { + var doc = createDoc(); + doc.set(['array'], [0, 1, 2, 3, 4], function() {}); + var moved = doc.move(['array'], 1, 0, 1, function() {}); + expect(moved).eql([1]); + expect(doc.get(['array'])).eql([1, 0, 2, 3, 4]); + }); + it('can move an item from the begnning to the end of the array', function() { + var doc = createDoc(); + doc.set(['array'], [0, 1, 2, 3, 4], function() {}); + var moved = doc.move(['array'], 0, 4, 1, function() {}); + expect(moved).eql([0]); + expect(doc.get(['array'])).eql([1, 2, 3, 4, 0]); + }); + it('can move several items mid-array, with an event for each', function() { + var doc = createDoc(); + doc.set(['array'], [0, 1, 2, 3, 4], function() {}); + var moved = doc.move(['array'], 1, 3, 2, function() {}); + expect(moved).eql([1, 2]); + expect(doc.get(['array'])).eql([0, 3, 4, 1, 2]); + }); + }); +}; diff --git a/test/Model/events.mocha.coffee b/test/Model/events.mocha.coffee deleted file mode 100644 index b6ec9e190..000000000 --- a/test/Model/events.mocha.coffee +++ /dev/null @@ -1,134 +0,0 @@ -{expect} = require '../util' -Model = require './MockConnectionModel' - -mutationEvents = (createModels) -> - - describe 'set', -> - it 'can raise events registered on array indices', (done) -> - [local, remote] = createModels() - local.set 'array', [0, 1, 2, 3, 4], -> - - remote.on 'change', 'array.0', (value, previous) -> - expect(value).to.equal 1 - expect(previous).to.equal 0 - done() - - local.set 'array.0', 1 - - describe 'move', -> - - it 'can move an item from the end to the beginning of the array', (done) -> - [local, remote] = createModels() - local.set 'array', [0, 1, 2, 3, 4] - - remote.on 'move', '**', (captures..., from, to, howMany, passed) -> - expect(from).to.equal 4 - expect(to).to.equal 0 - done() - - local.move 'array', 4, 0, 1 - - it 'can swap the first two items in the array', (done) -> - [local, remote] = createModels() - local.set 'array', [0, 1, 2, 3, 4], -> - - remote.on 'move', '**', (captures..., from, to, howMany, passed) -> - expect(from).to.equal 1 - expect(to).to.equal 0 - done() - - local.move 'array', 1, 0, 1, -> - - it 'can move an item from the begnning to the end of the array', (done) -> - [local, remote] = createModels() - local.set 'array', [0, 1, 2, 3, 4], -> - - remote.on 'move', '**', (captures..., from, to, howMany, passed) -> - expect(from).to.equal 0 - expect(to).to.equal 4 - done() - - # note that destination is index after removal of item - local.move 'array', 0, 4, 1, -> - - it 'supports a negative destination index of -1 (for last)', (done) -> - [local, remote] = createModels() - local.set 'array', [0, 1, 2, 3, 4], -> - - remote.on 'move', '**', (captures, from, to, howMany, passed) -> - expect(from).to.equal 0 - expect(to).to.equal 4 - done() - - local.move 'array', 0, -1, 1, -> - - it 'supports a negative source index of -1 (for last)', (done) -> - [local, remote] = createModels() - local.set 'array', [0, 1, 2, 3, 4], -> - - remote.on 'move', '**', (captures..., from, to, howMany, passed) -> - expect(from).to.equal 4 - expect(to).to.equal 2 - done() - - local.move 'array', -1, 2, 1, -> - - it 'can move several items mid-array, with an event for each', (done) -> - [local, remote] = createModels() - local.set 'array', [0, 1, 2, 3, 4], -> - - events = 0 - # When going through ShareJS, the single howMany > 1 move is split into - # lots of howMany==1 moves - remote.on 'move', '**', (captures..., from, to, howMany, passed) -> - expect(from).to.equal 1 - expect(to).to.equal 4 - done() if ++events == 2 - - # note that destination is index after removal of items - local.move 'array', 1, 3, 2, -> - -describe 'Model events', -> - - describe 'mutator events', -> - - it 'calls earlier listeners in the order of mutations', (done) -> - model = (new Model).at '_page' - expectedPaths = ['a', 'b', 'c'] - model.on 'change', '**', (path) -> - expect(path).to.equal expectedPaths.shift() - done() unless expectedPaths.length - model.on 'change', 'a', -> - model.set 'b', 2 - model.on 'change', 'b', -> - model.set 'c', 3 - model.set 'a', 1 - - it 'calls later listeners in the order of mutations', (done) -> - model = (new Model).at '_page' - model.on 'change', 'a', -> - model.set 'b', 2 - model.on 'change', 'b', -> - model.set 'c', 3 - expectedPaths = ['a', 'b', 'c'] - model.on 'change', '**', (path) -> - expect(path).to.equal expectedPaths.shift() - done() unless expectedPaths.length - model.set 'a', 1 - - describe 'remote events', -> - createModels = -> - localModel = new Model() - localModel.createConnection() - remoteModel = new Model() - remoteModel.createConnection() - - # Link the two models explicitly, so we can test event content - localDoc = localModel.getOrCreateDoc 'colors', 'green' - remoteDoc = remoteModel.getOrCreateDoc 'colors', 'green' - localDoc.shareDoc.on 'op', (op, isLocal) -> - remoteDoc._onOp op - - return [localModel.scope('colors.green'), remoteModel.scope('colors.green')] - - mutationEvents createModels diff --git a/test/Model/events.mocha.js b/test/Model/events.mocha.js new file mode 100644 index 000000000..d0ecad47e --- /dev/null +++ b/test/Model/events.mocha.js @@ -0,0 +1,139 @@ +var expect = require('../util').expect; +var Model = require('./MockConnectionModel'); + +function mutationEvents(createModels) { + describe('set', function() { + it('can raise events registered on array indices', function(done) { + var models = createModels(); + models.local.set('array', [0, 1, 2, 3, 4], function() {}); + models.remote.on('change', 'array.0', function(value, previous) { + expect(value).to.equal(1); + expect(previous).to.equal(0); + done(); + }); + models.local.set('array.0', 1); + }); + }); + describe('move', function() { + it('can move an item from the end to the beginning of the array', function(done) { + var models = createModels(); + models.local.set('array', [0, 1, 2, 3, 4]); + models.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(4); + expect(to).to.equal(0); + done(); + }); + models.local.move('array', 4, 0, 1); + }); + it('can swap the first two items in the array', function(done) { + var models = createModels(); + models.local.set('array', [0, 1, 2, 3, 4], function() {}); + models.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(1); + expect(to).to.equal(0); + done(); + }); + models.local.move('array', 1, 0, 1, function() {}); + }); + it('can move an item from the begnning to the end of the array', function(done) { + var models = createModels(); + models.local.set('array', [0, 1, 2, 3, 4], function() {}); + models.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(0); + expect(to).to.equal(4); + done(); + }); + models.local.move('array', 0, 4, 1, function() {}); + }); + it('supports a negative destination index of -1 (for last)', function(done) { + var models = createModels(); + models.local.set('array', [0, 1, 2, 3, 4], function() {}); + models.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(0); + expect(to).to.equal(4); + done(); + }); + models.local.move('array', 0, -1, 1, function() {}); + }); + it('supports a negative source index of -1 (for last)', function(done) { + var models = createModels(); + models.local.set('array', [0, 1, 2, 3, 4], function() {}); + models.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(4); + expect(to).to.equal(2); + done(); + }); + models.local.move('array', -1, 2, 1, function() {}); + }); + it('can move several items mid-array, with an event for each', function(done) { + var models = createModels(); + models.local.set('array', [0, 1, 2, 3, 4], function() {}); + var events = 0; + models.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(1); + expect(to).to.equal(4); + if (++events === 2) { + done(); + } + }); + models.local.move('array', 1, 3, 2, function() {}); + }); + }); +} + +describe('Model events', function() { + describe('mutator events', function() { + it('calls earlier listeners in the order of mutations', function(done) { + var model = (new Model).at('_page'); + var expectedPaths = ['a', 'b', 'c']; + model.on('change', '**', function(path) { + expect(path).to.equal(expectedPaths.shift()); + if (!expectedPaths.length) { + done(); + } + }); + model.on('change', 'a', function() { + model.set('b', 2); + }); + model.on('change', 'b', function() { + model.set('c', 3); + }); + model.set('a', 1); + }); + it('calls later listeners in the order of mutations', function(done) { + var model = (new Model).at('_page'); + model.on('change', 'a', function() { + model.set('b', 2); + }); + model.on('change', 'b', function() { + model.set('c', 3); + }); + var expectedPaths = ['a', 'b', 'c']; + model.on('change', '**', function(path) { + expect(path).to.equal(expectedPaths.shift()); + if (!expectedPaths.length) { + done(); + } + }); + model.set('a', 1); + }); + }); + describe('remote events', function() { + function createModels() { + var localModel = new Model(); + localModel.createConnection(); + var remoteModel = new Model(); + remoteModel.createConnection(); + var localDoc = localModel.getOrCreateDoc('colors', 'green'); + var remoteDoc = remoteModel.getOrCreateDoc('colors', 'green'); + localDoc.shareDoc.on('op', function(op, isLocal) { + remoteDoc._onOp(op); + }); + return { + local: localModel.scope('colors.green'), + remote: remoteModel.scope('colors.green') + }; + } + mutationEvents(createModels); + }); +}); diff --git a/test/Model/filter.mocha.coffee b/test/Model/filter.mocha.coffee index a0d4f47de..3f746b572 100644 --- a/test/Model/filter.mocha.coffee +++ b/test/Model/filter.mocha.coffee @@ -30,7 +30,8 @@ describe 'filter', -> it 'supports filter of object', -> model = (new Model).at '_page' - for number in [0, 3, 4, 1, 2, 3, 0] + numbers = [0, 3, 4, 1, 2, 3, 0] + for number in numbers model.set 'numbers.' + model.id(), number filter = model.filter 'numbers', (number, id, numbers) -> return (number % 2) == 0 @@ -38,7 +39,8 @@ describe 'filter', -> it 'supports sort of object', -> model = (new Model).at '_page' - for number in [0, 3, 4, 1, 2, 3, 0] + numbers = [0, 3, 4, 1, 2, 3, 0] + for number in numbers model.set 'numbers.' + model.id(), number filter = model.sort 'numbers', 'asc' expect(filter.get()).to.eql [0, 0, 1, 2, 3, 3, 4] @@ -47,7 +49,8 @@ describe 'filter', -> it 'supports filter and sort of object', -> model = (new Model).at '_page' - for number in [0, 3, 4, 1, 2, 3, 0] + numbers = [0, 3, 4, 1, 2, 3, 0] + for number in numbers model.set 'numbers.' + model.id(), number model.fn 'even', (number) -> return (number % 2) == 0 @@ -84,7 +87,8 @@ describe 'filter', -> it 'supports filter of object', -> model = (new Model).at '_page' - for number in [0, 3, 4, 1, 2, 3, 0] + numbers = [0, 3, 4, 1, 2, 3, 0] + for number in numbers model.set 'numbers.' + model.id(), number filter = model.filter 'numbers', (number) -> return (number % 2) == 0 @@ -93,7 +97,8 @@ describe 'filter', -> it 'supports sort of object', -> model = (new Model).at '_page' - for number in [0, 3, 4, 1, 2, 3, 0] + numbers = [0, 3, 4, 1, 2, 3, 0] + for number in numbers model.set 'numbers.' + model.id(), number filter = model.sort 'numbers', 'asc' expect(filter.get()).to.eql [0, 0, 1, 2, 3, 3, 4] @@ -103,7 +108,8 @@ describe 'filter', -> it 'supports filter and sort of object', -> model = (new Model).at '_page' - for number in [0, 3, 4, 1, 2, 3, 0] + numbers = [0, 3, 4, 1, 2, 3, 0] + for number in numbers model.set 'numbers.' + model.id(), number model.fn 'even', (number) -> return (number % 2) == 0 diff --git a/test/Model/filter.mocha.js b/test/Model/filter.mocha.js new file mode 100644 index 000000000..63fb783ff --- /dev/null +++ b/test/Model/filter.mocha.js @@ -0,0 +1,205 @@ +var expect = require('../util').expect; +var Model = require('../../lib/Model'); + +describe('filter', function() { + describe('getting', function() { + it('supports filter of array', function() { + var model = (new Model).at('_page'); + model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); + var filter = model.filter('numbers', function(number, i, numbers) { + return (number % 2) === 0; + }); + expect(filter.get()).to.eql([0, 4, 2, 0]); + }); + it('supports sort of array', function() { + var model = (new Model).at('_page'); + model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); + filter = model.sort('numbers', 'asc'); + expect(filter.get()).to.eql([0, 0, 1, 2, 3, 3, 4]); + filter = model.sort('numbers', 'desc'); + expect(filter.get()).to.eql([4, 3, 3, 2, 1, 0, 0]); + }); + it('supports filter and sort of array', function() { + var model = (new Model).at('_page'); + model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); + model.fn('even', function(number) { + return (number % 2) === 0; + }); + var filter = model.filter('numbers', 'even').sort(); + expect(filter.get()).to.eql([0, 0, 2, 4]); + }); + it('supports filter of object', function() { + var model = (new Model).at('_page'); + var numbers = [0, 3, 4, 1, 2, 3, 0]; + for (var i = 0; i < numbers.length; i++) { + model.set('numbers.' + model.id(), numbers[i]); + } + var filter = model.filter('numbers', function(number, id, numbers) { + return (number % 2) === 0; + }); + expect(filter.get()).to.eql([0, 4, 2, 0]); + }); + it('supports sort of object', function() { + var model = (new Model).at('_page'); + var numbers = [0, 3, 4, 1, 2, 3, 0]; + for (var i = 0; i < numbers.length; i++) { + model.set('numbers.' + model.id(), numbers[i]); + } + filter = model.sort('numbers', 'asc'); + expect(filter.get()).to.eql([0, 0, 1, 2, 3, 3, 4]); + filter = model.sort('numbers', 'desc'); + expect(filter.get()).to.eql([4, 3, 3, 2, 1, 0, 0]); + }); + it('supports filter and sort of object', function() { + var model = (new Model).at('_page'); + var numbers = [0, 3, 4, 1, 2, 3, 0]; + for (var i = 0; i < numbers.length; i++) { + model.set('numbers.' + model.id(), numbers[i]); + } + model.fn('even', function(number) { + return (number % 2) === 0; + }); + var filter = model.filter('numbers', 'even').sort(); + expect(filter.get()).to.eql([0, 0, 2, 4]); + }); + }); + describe('initial value set by ref', function() { + it('supports filter of array', function() { + var model = (new Model).at('_page'); + model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); + var filter = model.filter('numbers', function(number) { + return (number % 2) === 0; + }); + filter.ref('_page.out'); + expect(model.get('out')).to.eql([0, 4, 2, 0]); + }); + it('supports sort of array', function() { + var model = (new Model).at('_page'); + model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); + filter = model.sort('numbers', 'asc'); + expect(filter.get()).to.eql([0, 0, 1, 2, 3, 3, 4]); + filter = model.sort('numbers', 'desc'); + filter.ref('_page.out'); + expect(model.get('out')).to.eql([4, 3, 3, 2, 1, 0, 0]); + }); + it('supports filter and sort of array', function() { + var model = (new Model).at('_page'); + model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); + model.fn('even', function(number) { + return (number % 2) === 0; + }); + var filter = model.filter('numbers', 'even').sort(); + filter.ref('_page.out'); + expect(model.get('out')).to.eql([0, 0, 2, 4]); + }); + it('supports filter of object', function() { + var model = (new Model).at('_page'); + var numbers = [0, 3, 4, 1, 2, 3, 0]; + for (var i = 0; i < numbers.length; i++) { + model.set('numbers.' + model.id(), numbers[i]); + } + var filter = model.filter('numbers', function(number) { + return (number % 2) === 0; + }); + filter.ref('_page.out'); + expect(model.get('out')).to.eql([0, 4, 2, 0]); + }); + it('supports sort of object', function() { + var model = (new Model).at('_page'); + var numbers = [0, 3, 4, 1, 2, 3, 0]; + for (var i = 0; i < numbers.length; i++) { + model.set('numbers.' + model.id(), numbers[i]); + } + filter = model.sort('numbers', 'asc'); + expect(filter.get()).to.eql([0, 0, 1, 2, 3, 3, 4]); + filter = model.sort('numbers', 'desc'); + filter.ref('_page.out'); + expect(model.get('out')).to.eql([4, 3, 3, 2, 1, 0, 0]); + }); + it('supports filter and sort of object', function() { + var model = (new Model).at('_page'); + var numbers = [0, 3, 4, 1, 2, 3, 0]; + for (var i = 0; i < numbers.length; i++) { + model.set('numbers.' + model.id(), numbers[i]); + } + model.fn('even', function(number) { + return (number % 2) === 0; + }); + var filter = model.filter('numbers', 'even').sort(); + filter.ref('_page.out'); + expect(model.get('out')).to.eql([0, 0, 2, 4]); + }); + }); + describe('ref updates as items are modified', function() { + it('supports filter of array', function() { + var model = (new Model).at('_page'); + model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); + var filter = model.filter('numbers', function(number) { + return (number % 2) === 0; + }); + filter.ref('_page.out'); + expect(model.get('out')).to.eql([0, 4, 2, 0]); + model.push('numbers', 6); + expect(model.get('out')).to.eql([0, 4, 2, 0, 6]); + model.set('numbers.2', 1); + expect(model.get('out')).to.eql([0, 2, 0, 6]); + model.del('numbers'); + expect(model.get('out')).to.eql([]); + model.set('numbers', [1, 2, 0]); + expect(model.get('out')).to.eql([2, 0]); + }); + it('supports filter of object', function() { + var model = (new Model).at('_page'); + var greenId = model.add('colors', { + name: 'green', + primary: true + }); + var orangeId = model.add('colors', { + name: 'orange', + primary: false + }); + var redId = model.add('colors', { + name: 'red', + primary: true + }); + var filter = model.filter('colors', function(color) { + return color.primary; + }); + filter.ref('_page.out'); + expect(model.get('out')).to.eql([ + { + name: 'green', + primary: true, + id: greenId + }, { + name: 'red', + primary: true, + id: redId + } + ]); + model.set('colors.' + greenId + '.primary', false); + expect(model.get('out')).to.eql([ + { + name: 'red', + primary: true, + id: redId + } + ]); + var yellowId = model.add('colors', { + name: 'yellow', + primary: true + }); + expect(model.get('out')).to.eql([ + { + name: 'red', + primary: true, + id: redId + }, { + name: 'yellow', + primary: true, + id: yellowId + } + ]); + }); + }); +}); diff --git a/test/Model/fn.mocha.coffee b/test/Model/fn.mocha.coffee deleted file mode 100644 index 984f53c4e..000000000 --- a/test/Model/fn.mocha.coffee +++ /dev/null @@ -1,202 +0,0 @@ -{expect} = require '../util' -Model = require '../../lib/Model' - -describe 'fn', -> - - describe 'evaluate', -> - - it 'supports fn with a getter function', -> - model = new Model - model.fn 'sum', (a, b) -> a + b - model.set '_nums.a', 2 - model.set '_nums.b', 4 - result = model.evaluate '_nums.a', '_nums.b', 'sum' - expect(result).to.equal 6 - - it 'supports fn with an object', -> - model = new Model - model.fn 'sum', - get: (a, b) -> a + b - model.set '_nums.a', 2 - model.set '_nums.b', 4 - result = model.evaluate '_nums.a', '_nums.b', 'sum' - expect(result).to.equal 6 - - it 'supports fn with variable arguments', -> - model = new Model - model.fn 'sum', (args...) -> - sum = 0 - sum += arg for arg in args - return sum - model.set '_nums.a', 2 - model.set '_nums.b', 4 - model.set '_nums.c', 7 - result = model.evaluate '_nums.a', '_nums.b', '_nums.c', 'sum' - expect(result).to.equal 13 - - it 'supports scoped model paths', -> - model = new Model - model.fn 'sum', (a, b) -> a + b - $nums = model.at '_nums' - $nums.set 'a', 2 - $nums.set 'b', 4 - result = model.evaluate '_nums.a', '_nums.b', 'sum' - expect(result).to.equal 6 - result = $nums.evaluate 'a', 'b', 'sum' - expect(result).to.equal 6 - - describe 'start and stop with getter', -> - - it 'sets the output immediately on start', -> - model = new Model - model.fn 'sum', (a, b) -> a + b - model.set '_nums.a', 2 - model.set '_nums.b', 4 - value = model.start '_nums.sum', '_nums.a', '_nums.b', 'sum' - expect(value).to.equal 6 - expect(model.get '_nums.sum').to.equal 6 - - it 'sets the output when an input changes', -> - model = new Model - model.fn 'sum', (a, b) -> a + b - model.set '_nums.a', 2 - model.set '_nums.b', 4 - model.start '_nums.sum', '_nums.a', '_nums.b', 'sum' - expect(model.get '_nums.sum').to.equal 6 - model.set '_nums.a', 5 - expect(model.get '_nums.sum').to.equal 9 - - it 'sets the output when a parent of the input changes', -> - model = new Model - model.fn 'sum', (a, b) -> a + b - model.set '_nums.in', {a: 2, b: 4} - model.start '_nums.sum', '_nums.in.a', '_nums.in.b', 'sum' - expect(model.get '_nums.sum').to.equal 6 - model.set '_nums.in', {a: 5, b: 7} - expect(model.get '_nums.sum').to.equal 12 - - it 'does not set the output when a sibling of the input changes', -> - model = new Model - count = 0 - model.fn 'sum', (a, b) -> count++; a + b - model.set '_nums.in', {a: 2, b: 4} - model.start '_nums.sum', '_nums.in.a', '_nums.in.b', 'sum' - expect(model.get '_nums.sum').to.equal 6 - expect(count).to.equal 1 - model.set '_nums.in.a', 3 - expect(model.get '_nums.sum').to.equal 7 - expect(count).to.equal 2 - model.set '_nums.in.c', -1 - expect(model.get '_nums.sum').to.equal 7 - expect(count).to.equal 2 - - it 'can call stop without start', -> - model = new Model - model.stop '_nums.sum' - - it 'stops updating after calling stop', -> - model = new Model - model.fn 'sum', (a, b) -> a + b - model.set '_nums.a', 2 - model.set '_nums.b', 4 - model.start '_nums.sum', '_nums.a', '_nums.b', 'sum' - model.set '_nums.a', 1 - expect(model.get '_nums.sum').to.equal 5 - model.stop '_nums.sum' - model.set '_nums.a', 3 - expect(model.get '_nums.sum').to.equal 5 - - describe 'setter', -> - - it 'sets the input when the output changes', -> - model = new Model - model.fn 'fullName', - get: (first, last) -> first + ' ' + last - set: (fullName) -> fullName.split ' ' - model.set '_user.name', - first: 'John' - last: 'Smith' - model.at('_user.name').start 'full', 'first', 'last', 'fullName' - expect(model.get '_user.name').to.eql - first: 'John' - last: 'Smith' - full: 'John Smith' - model.set '_user.name.full', 'Jane Doe' - expect(model.get '_user.name').to.eql - first: 'Jane' - last: 'Doe' - full: 'Jane Doe' - - describe 'event mirroring', -> - - it 'emits move event on output when input changes', (done) -> - model = new Model - model.fn 'unity', - get: (value) -> value - set: (value) -> [value] - model.set '_test.in', - a: [ - {x: 1, y: 2} - {x: 2, y: 0} - ] - model.start '_test.out', '_test.in', 'unity' - model.on 'all', '_test.out**', (path, event) -> - expect(event).to.equal 'move' - expect(path).to.equal 'a' - done() - model.move '_test.in.a', 0, 1 - expect(model.get '_test.out').to.eql model.get('_test.in') - - it 'emits move event on input when output changes', (done) -> - model = new Model - model.fn 'unity', - get: (value) -> value - set: (value) -> [value] - model.set '_test.in', - a: [ - {x: 1, y: 2} - {x: 2, y: 0} - ] - model.start '_test.out', '_test.in', 'unity' - model.on 'all', '_test.in**', (path, event) -> - expect(event).to.equal 'move' - expect(path).to.equal 'a' - done() - model.move '_test.out.a', 0, 1 - expect(model.get '_test.out').to.eql model.get('_test.in') - - it 'emits granular change event under an array when input changes', (done) -> - model = new Model - model.fn 'unity', - get: (value) -> value - set: (value) -> [value] - model.set '_test.in', - a: [ - {x: 1, y: 2} - {x: 2, y: 0} - ] - model.start '_test.out', '_test.in', 'unity' - model.on 'all', '_test.out**', (path, event) -> - expect(event).to.equal 'change' - expect(path).to.equal 'a.0.x' - done() - model.set '_test.in.a.0.x', 3 - expect(model.get '_test.out').to.eql model.get('_test.in') - - it 'emits granular change event under an array when output changes', (done) -> - model = new Model - model.fn 'unity', - get: (value) -> value - set: (value) -> [value] - model.set '_test.in', - a: [ - {x: 1, y: 2} - {x: 2, y: 0} - ] - model.start '_test.out', '_test.in', 'unity' - model.on 'all', '_test.in**', (path, event) -> - expect(event).to.equal 'change' - expect(path).to.equal 'a.0.x' - done() - model.set '_test.out.a.0.x', 3 - expect(model.get '_test.out').to.eql model.get('_test.in') diff --git a/test/Model/fn.mocha.js b/test/Model/fn.mocha.js new file mode 100644 index 000000000..a708738bf --- /dev/null +++ b/test/Model/fn.mocha.js @@ -0,0 +1,290 @@ +var expect = require('../util').expect; +var Model = require('../../lib/Model'); + +describe('fn', function() { + describe('evaluate', function() { + it('supports fn with a getter function', function() { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + var result = model.evaluate('_nums.a', '_nums.b', 'sum'); + expect(result).to.equal(6); + }); + it('supports fn with an object', function() { + var model = new Model(); + model.fn('sum', { + get: function(a, b) { + return a + b; + } + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + var result = model.evaluate('_nums.a', '_nums.b', 'sum'); + expect(result).to.equal(6); + }); + it('supports fn with variable arguments', function() { + var model = new Model(); + model.fn('sum', function() { + var sum = 0; + var i = arguments.length; + while (i--) { + sum += arguments[i]; + } + return sum; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + model.set('_nums.c', 7); + var result = model.evaluate('_nums.a', '_nums.b', '_nums.c', 'sum'); + expect(result).to.equal(13); + }); + it('supports scoped model paths', function() { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + var $nums = model.at('_nums'); + $nums.set('a', 2); + $nums.set('b', 4); + var result = model.evaluate('_nums.a', '_nums.b', 'sum'); + expect(result).to.equal(6); + result = $nums.evaluate('a', 'b', 'sum'); + expect(result).to.equal(6); + }); + }); + describe('start and stop with getter', function() { + it('sets the output immediately on start', function() { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + var value = model.start('_nums.sum', '_nums.a', '_nums.b', 'sum'); + expect(value).to.equal(6); + expect(model.get('_nums.sum')).to.equal(6); + }); + it('sets the output when an input changes', function() { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + model.start('_nums.sum', '_nums.a', '_nums.b', 'sum'); + expect(model.get('_nums.sum')).to.equal(6); + model.set('_nums.a', 5); + expect(model.get('_nums.sum')).to.equal(9); + }); + it('sets the output when a parent of the input changes', function() { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.in', { + a: 2, + b: 4 + }); + model.start('_nums.sum', '_nums.in.a', '_nums.in.b', 'sum'); + expect(model.get('_nums.sum')).to.equal(6); + model.set('_nums.in', { + a: 5, + b: 7 + }); + expect(model.get('_nums.sum')).to.equal(12); + }); + it('does not set the output when a sibling of the input changes', function() { + var model = new Model(); + var count = 0; + model.fn('sum', function(a, b) { + count++; + return a + b; + }); + model.set('_nums.in', { + a: 2, + b: 4 + }); + model.start('_nums.sum', '_nums.in.a', '_nums.in.b', 'sum'); + expect(model.get('_nums.sum')).to.equal(6); + expect(count).to.equal(1); + model.set('_nums.in.a', 3); + expect(model.get('_nums.sum')).to.equal(7); + expect(count).to.equal(2); + model.set('_nums.in.c', -1); + expect(model.get('_nums.sum')).to.equal(7); + expect(count).to.equal(2); + }); + it('can call stop without start', function() { + var model = new Model(); + model.stop('_nums.sum'); + }); + it('stops updating after calling stop', function() { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + model.start('_nums.sum', '_nums.a', '_nums.b', 'sum'); + model.set('_nums.a', 1); + expect(model.get('_nums.sum')).to.equal(5); + model.stop('_nums.sum'); + model.set('_nums.a', 3); + expect(model.get('_nums.sum')).to.equal(5); + }); + }); + describe('setter', function() { + it('sets the input when the output changes', function() { + var model = new Model(); + model.fn('fullName', { + get: function(first, last) { + return first + ' ' + last; + }, + set: function(fullName) { + return fullName.split(' '); + } + }); + model.set('_user.name', { + first: 'John', + last: 'Smith' + }); + model.at('_user.name').start('full', 'first', 'last', 'fullName'); + expect(model.get('_user.name')).to.eql({ + first: 'John', + last: 'Smith', + full: 'John Smith' + }); + model.set('_user.name.full', 'Jane Doe'); + expect(model.get('_user.name')).to.eql({ + first: 'Jane', + last: 'Doe', + full: 'Jane Doe' + }); + }); + }); + describe('event mirroring', function() { + it('emits move event on output when input changes', function(done) { + var model = new Model(); + model.fn('unity', { + get: function(value) { + return value; + }, + set: function(value) { + return [value]; + } + }); + model.set('_test.in', { + a: [ + { + x: 1, + y: 2 + }, { + x: 2, + y: 0 + } + ] + }); + model.start('_test.out', '_test.in', 'unity'); + model.on('all', '_test.out**', function(path, event) { + expect(event).to.equal('move'); + expect(path).to.equal('a'); + done(); + }); + model.move('_test.in.a', 0, 1); + expect(model.get('_test.out')).to.eql(model.get('_test.in')); + }); + it('emits move event on input when output changes', function(done) { + var model = new Model(); + model.fn('unity', { + get: function(value) { + return value; + }, + set: function(value) { + return [value]; + } + }); + model.set('_test.in', { + a: [ + { + x: 1, + y: 2 + }, { + x: 2, + y: 0 + } + ] + }); + model.start('_test.out', '_test.in', 'unity'); + model.on('all', '_test.in**', function(path, event) { + expect(event).to.equal('move'); + expect(path).to.equal('a'); + done(); + }); + model.move('_test.out.a', 0, 1); + expect(model.get('_test.out')).to.eql(model.get('_test.in')); + }); + it('emits granular change event under an array when input changes', function(done) { + var model = new Model(); + model.fn('unity', { + get: function(value) { + return value; + }, + set: function(value) { + return [value]; + } + }); + model.set('_test.in', { + a: [ + { + x: 1, + y: 2 + }, { + x: 2, + y: 0 + } + ] + }); + model.start('_test.out', '_test.in', 'unity'); + model.on('all', '_test.out**', function(path, event) { + expect(event).to.equal('change'); + expect(path).to.equal('a.0.x'); + done(); + }); + model.set('_test.in.a.0.x', 3); + expect(model.get('_test.out')).to.eql(model.get('_test.in')); + }); + it('emits granular change event under an array when output changes', function(done) { + var model = new Model(); + model.fn('unity', { + get: function(value) { + return value; + }, + set: function(value) { + return [value]; + } + }); + model.set('_test.in', { + a: [ + { + x: 1, + y: 2 + }, { + x: 2, + y: 0 + } + ] + }); + model.start('_test.out', '_test.in', 'unity'); + model.on('all', '_test.in**', function(path, event) { + expect(event).to.equal('change'); + expect(path).to.equal('a.0.x'); + done(); + }); + model.set('_test.out.a.0.x', 3); + expect(model.get('_test.out')).to.eql(model.get('_test.in')); + }); + }); +}); diff --git a/test/Model/ref.mocha.coffee b/test/Model/ref.mocha.coffee deleted file mode 100644 index 43439bf31..000000000 --- a/test/Model/ref.mocha.coffee +++ /dev/null @@ -1,204 +0,0 @@ -{expect} = require '../util' -Model = require '../../lib/Model' - -describe 'ref', -> - - expectEvents = (pattern, model, done, events) -> - model.on 'all', pattern, -> - events.shift() arguments... - done() unless events.length - done() unless events?.length - - describe 'event emission', -> - - it 're-emits on a reffed path', (done) -> - model = new Model - model.ref '_page.color', '_page.colors.green' - model.on 'change', '_page.color', (value) -> - expect(value).to.equal '#0f0' - done() - model.set '_page.colors.green', '#0f0' - - it 'also emits on the original path', (done) -> - model = new Model - model.ref '_page.color', '_page.colors.green' - model.on 'change', '_page.colors.green', (value) -> - expect(value).to.equal '#0f0' - done() - model.set '_page.colors.green', '#0f0' - - it 're-emits on a child of a reffed path', (done) -> - model = new Model - model.ref '_page.color', '_page.colors.green' - model.on 'change', '_page.color.*', (capture, value) -> - expect(capture).to.equal 'hex' - expect(value).to.equal '#0f0' - done() - model.set '_page.colors.green.hex', '#0f0' - - it 're-emits when a parent is changed', (done) -> - model = new Model - model.ref '_page.color', '_page.colors.green' - model.on 'change', '_page.color', (value) -> - expect(value).to.equal '#0e0' - done() - model.set '_page.colors', - green: '#0e0' - - it 're-emits on a ref to a ref', (done) -> - model = new Model - model.ref '_page.myFavorite', '_page.color' - model.ref '_page.color', '_page.colors.green' - model.on 'change', '_page.myFavorite', (value) -> - expect(value).to.equal '#0f0' - done() - model.set '_page.colors.green', '#0f0' - - it 're-emits on multiple reffed paths', (done) -> - model = new Model - model.set '_page.colors.green', '#0f0' - model.ref '_page.favorites.my', '_page.colors.green' - model.ref '_page.favorites.your', '_page.colors.green' - - expectEvents '_page.favorites**', model, done, [ - (capture, method, value, previous) -> - expect(method).to.equal 'change' - expect(capture).to.equal 'my' - expect(value).to.equal '#0f1' - , (capture, method, value, previous) -> - expect(method).to.equal 'change' - expect(capture).to.equal 'your' - expect(value).to.equal '#0f1' - ] - model.set '_page.colors.green', '#0f1' - - describe 'get', -> - - it 'gets from a reffed path', -> - model = new Model - model.set '_page.colors.green', '#0f0' - expect(model.get '_page.color').to.equal undefined - model.ref '_page.color', '_page.colors.green' - expect(model.get '_page.color').to.equal '#0f0' - - it 'gets from a child of a reffed path', -> - model = new Model - model.set '_page.colors.green.hex', '#0f0' - model.ref '_page.color', '_page.colors.green' - expect(model.get '_page.color').to.eql {hex: '#0f0'} - expect(model.get '_page.color.hex').to.equal '#0f0' - - it 'gets from a ref to a ref', -> - model = new Model - model.ref '_page.myFavorite', '_page.color' - model.ref '_page.color', '_page.colors.green' - model.set '_page.colors.green', '#0f0' - expect(model.get '_page.myFavorite').to.equal '#0f0' - - describe 'updateIndices option', -> - - it 'updates a ref when an array insert happens at the `to` path', -> - model = new Model - model.set '_page.colors', ['red', 'green', 'blue'] - model.ref '_page.color', '_page.colors.1', {updateIndices: true} - expect(model.get '_page.color').to.equal 'green' - model.unshift '_page.colors', 'yellow' - expect(model.get '_page.color').to.equal 'green' - model.push '_page.colors', 'orange' - expect(model.get '_page.color').to.equal 'green' - model.insert '_page.colors', 2, ['purple', 'cyan'] - expect(model.get '_page.color').to.equal 'green' - - it 'updates a ref when an array remove happens at the `to` path', -> - model = new Model - model.set '_page.colors', ['red', 'blue', 'purple', 'cyan', 'green', 'yellow'] - model.ref '_page.color', '_page.colors.4', {updateIndices: true} - expect(model.get '_page.color').to.equal 'green' - model.shift '_page.colors' - expect(model.get '_page.color').to.equal 'green' - model.pop '_page.colors' - expect(model.get '_page.color').to.equal 'green' - model.remove '_page.colors', 1, 2 - expect(model.get '_page.color').to.equal 'green' - - it 'updates a ref when an array move happens at the `to` path', -> - model = new Model - model.set '_page.colors', ['red', 'blue', 'purple', 'green', 'cyan', 'yellow'] - model.ref '_page.color', '_page.colors.3', {updateIndices: true} - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 0, 1 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 4, 5 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 0, 5 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 1, 3 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 0, 3, 2 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 2, 3, 2 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 3, 2, 2 - expect(model.get '_page.color').to.equal 'green' - - it 'updates a ref when an array insert happens within the `to` path', -> - model = new Model - model.set '_page.colors', [ - {name: 'red'} - {name: 'green'} - {name: 'blue'} - ] - model.ref '_page.color', '_page.colors.1.name', {updateIndices: true} - expect(model.get '_page.color').to.equal 'green' - model.unshift '_page.colors', 'yellow' - expect(model.get '_page.color').to.equal 'green' - model.push '_page.colors', 'orange' - expect(model.get '_page.color').to.equal 'green' - model.insert '_page.colors', 2, ['purple', 'cyan'] - expect(model.get '_page.color').to.equal 'green' - - it 'updates a ref when an array remove happens within the `to` path', -> - model = new Model - model.set '_page.colors', [ - {name: 'red'} - {name: 'blue'} - {name: 'purple'} - {name: 'cyan'} - {name: 'green'} - {name: 'yellow'} - ] - model.ref '_page.color', '_page.colors.4.name', {updateIndices: true} - expect(model.get '_page.color').to.equal 'green' - model.shift '_page.colors' - expect(model.get '_page.color').to.equal 'green' - model.pop '_page.colors' - expect(model.get '_page.color').to.equal 'green' - model.remove '_page.colors', 1, 2 - expect(model.get '_page.color').to.equal 'green' - - it 'updates a ref when an array move happens within the `to` path', -> - model = new Model - model.set '_page.colors', [ - {name: 'red'} - {name: 'blue'} - {name: 'purple'} - {name: 'green'} - {name: 'cyan'} - {name: 'yellow'} - ] - model.ref '_page.color', '_page.colors.3.name', {updateIndices: true} - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 0, 1 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 4, 5 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 0, 5 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 1, 3 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 0, 3, 2 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 2, 3, 2 - expect(model.get '_page.color').to.equal 'green' - model.move '_page.colors', 3, 2, 2 - expect(model.get '_page.color').to.equal 'green' diff --git a/test/Model/ref.mocha.js b/test/Model/ref.mocha.js new file mode 100644 index 000000000..bc616f1bc --- /dev/null +++ b/test/Model/ref.mocha.js @@ -0,0 +1,214 @@ +var expect = require('../util').expect; +var Model = require('../../lib/Model'); + +describe('ref', function() { + function expectEvents(pattern, model, done, events) { + model.on('all', pattern, function() { + events.shift().apply(null, arguments); + if (!events.length) done(); + }); + if (!events || !events.length) done(); + } + describe('event emission', function() { + it('re-emits on a reffed path', function(done) { + var model = new Model; + model.ref('_page.color', '_page.colors.green'); + model.on('change', '_page.color', function(value) { + expect(value).to.equal('#0f0'); + done(); + }); + model.set('_page.colors.green', '#0f0'); + }); + it('also emits on the original path', function(done) { + var model = new Model; + model.ref('_page.color', '_page.colors.green'); + model.on('change', '_page.colors.green', function(value) { + expect(value).to.equal('#0f0'); + done(); + }); + model.set('_page.colors.green', '#0f0'); + }); + it('re-emits on a child of a reffed path', function(done) { + var model = new Model; + model.ref('_page.color', '_page.colors.green'); + model.on('change', '_page.color.*', function(capture, value) { + expect(capture).to.equal('hex'); + expect(value).to.equal('#0f0'); + done(); + }); + model.set('_page.colors.green.hex', '#0f0'); + }); + it('re-emits when a parent is changed', function(done) { + var model = new Model; + model.ref('_page.color', '_page.colors.green'); + model.on('change', '_page.color', function(value) { + expect(value).to.equal('#0e0'); + done(); + }); + model.set('_page.colors', { + green: '#0e0' + }); + }); + it('re-emits on a ref to a ref', function(done) { + var model = new Model; + model.ref('_page.myFavorite', '_page.color'); + model.ref('_page.color', '_page.colors.green'); + model.on('change', '_page.myFavorite', function(value) { + expect(value).to.equal('#0f0'); + done(); + }); + model.set('_page.colors.green', '#0f0'); + }); + it('re-emits on multiple reffed paths', function(done) { + var model = new Model; + model.set('_page.colors.green', '#0f0'); + model.ref('_page.favorites.my', '_page.colors.green'); + model.ref('_page.favorites.your', '_page.colors.green'); + expectEvents('_page.favorites**', model, done, [ + function(capture, method, value, previous) { + expect(method).to.equal('change'); + expect(capture).to.equal('my'); + expect(value).to.equal('#0f1'); + }, function(capture, method, value, previous) { + expect(method).to.equal('change'); + expect(capture).to.equal('your'); + expect(value).to.equal('#0f1'); + } + ]); + model.set('_page.colors.green', '#0f1'); + }); + }); + describe('get', function() { + it('gets from a reffed path', function() { + var model = new Model; + model.set('_page.colors.green', '#0f0'); + expect(model.get('_page.color')).to.equal(void 0); + model.ref('_page.color', '_page.colors.green'); + expect(model.get('_page.color')).to.equal('#0f0'); + }); + it('gets from a child of a reffed path', function() { + var model = new Model; + model.set('_page.colors.green.hex', '#0f0'); + model.ref('_page.color', '_page.colors.green'); + expect(model.get('_page.color')).to.eql({ + hex: '#0f0' + }); + expect(model.get('_page.color.hex')).to.equal('#0f0'); + }); + it('gets from a ref to a ref', function() { + var model = new Model; + model.ref('_page.myFavorite', '_page.color'); + model.ref('_page.color', '_page.colors.green'); + model.set('_page.colors.green', '#0f0'); + expect(model.get('_page.myFavorite')).to.equal('#0f0'); + }); + }); + describe('updateIndices option', function() { + it('updates a ref when an array insert happens at the `to` path', function() { + var model = new Model; + model.set('_page.colors', ['red', 'green', 'blue']); + model.ref('_page.color', '_page.colors.1', {updateIndices: true}); + expect(model.get('_page.color')).to.equal('green'); + model.unshift('_page.colors', 'yellow'); + expect(model.get('_page.color')).to.equal('green'); + model.push('_page.colors', 'orange'); + expect(model.get('_page.color')).to.equal('green'); + model.insert('_page.colors', 2, ['purple', 'cyan']); + expect(model.get('_page.color')).to.equal('green'); + }); + it('updates a ref when an array remove happens at the `to` path', function() { + var model = new Model; + model.set('_page.colors', ['red', 'blue', 'purple', 'cyan', 'green', 'yellow']); + model.ref('_page.color', '_page.colors.4', {updateIndices: true}); + expect(model.get('_page.color')).to.equal('green'); + model.shift('_page.colors'); + expect(model.get('_page.color')).to.equal('green'); + model.pop('_page.colors'); + expect(model.get('_page.color')).to.equal('green'); + model.remove('_page.colors', 1, 2); + expect(model.get('_page.color')).to.equal('green'); + }); + it('updates a ref when an array move happens at the `to` path', function() { + var model = new Model; + model.set('_page.colors', ['red', 'blue', 'purple', 'green', 'cyan', 'yellow']); + model.ref('_page.color', '_page.colors.3', {updateIndices: true}); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 0, 1); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 4, 5); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 0, 5); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 1, 3); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 0, 3, 2); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 2, 3, 2); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 3, 2, 2); + expect(model.get('_page.color')).to.equal('green'); + }); + it('updates a ref when an array insert happens within the `to` path', function() { + var model = new Model; + model.set('_page.colors', [ + {name: 'red'}, + {name: 'green'}, + {name: 'blue'} + ]); + model.ref('_page.color', '_page.colors.1.name', {updateIndices: true}); + expect(model.get('_page.color')).to.equal('green'); + model.unshift('_page.colors', 'yellow'); + expect(model.get('_page.color')).to.equal('green'); + model.push('_page.colors', 'orange'); + expect(model.get('_page.color')).to.equal('green'); + model.insert('_page.colors', 2, ['purple', 'cyan']); + expect(model.get('_page.color')).to.equal('green'); + }); + it('updates a ref when an array remove happens within the `to` path', function() { + var model = new Model; + model.set('_page.colors', [ + {name: 'red'}, + {name: 'blue'}, + {name: 'purple'}, + {name: 'cyan'}, + {name: 'green'}, + {name: 'yellow'} + ]); + model.ref('_page.color', '_page.colors.4.name', {updateIndices: true}); + expect(model.get('_page.color')).to.equal('green'); + model.shift('_page.colors'); + expect(model.get('_page.color')).to.equal('green'); + model.pop('_page.colors'); + expect(model.get('_page.color')).to.equal('green'); + model.remove('_page.colors', 1, 2); + expect(model.get('_page.color')).to.equal('green'); + }); + it('updates a ref when an array move happens within the `to` path', function() { + var model = new Model; + model.set('_page.colors', [ + {name: 'red'}, + {name: 'blue'}, + {name: 'purple'}, + {name: 'green'}, + {name: 'cyan'}, + {name: 'yellow'} + ]); + model.ref('_page.color', '_page.colors.3.name', {updateIndices: true}); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 0, 1); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 4, 5); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 0, 5); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 1, 3); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 0, 3, 2); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 2, 3, 2); + expect(model.get('_page.color')).to.equal('green'); + model.move('_page.colors', 3, 2, 2); + expect(model.get('_page.color')).to.equal('green'); + }); + }); +}); diff --git a/test/Model/refList.mocha.coffee b/test/Model/refList.mocha.coffee deleted file mode 100644 index adf310529..000000000 --- a/test/Model/refList.mocha.coffee +++ /dev/null @@ -1,555 +0,0 @@ -{expect} = require '../util' -Model = require '../../lib/Model' - -describe 'refList', -> - - setup = (options) -> - model = (new Model).at '_page' - model.set 'colors', - green: - id: 'green' - rgb: [0, 255, 0] - hex: '#0f0' - red: - id: 'red' - rgb: [255, 0, 0] - hex: '#f00' - model.set 'ids', ['red', 'green', 'red'] - model.refList 'list', 'colors', 'ids', options - return model - - expectEvents = (pattern, model, done, events) -> - model.on 'all', pattern, -> - events.shift() arguments... - done() unless events.length - done() unless events?.length - expectFromEvents = (model, done, events) -> - expectEvents 'list**', model, done, events - expectToEvents = (model, done, events) -> - expectEvents 'colors**', model, done, events - expectIdsEvents = (model, done, events) -> - expectEvents 'ids**', model, done, events - - describe 'sets output on initial call', -> - - it 'sets the initial value to empty array if no inputs', -> - model = (new Model).at '_page' - model.refList 'empty', 'colors', 'noIds' - expect(model.get 'empty').to.eql [] - - it 'sets the initial value for already populated data', -> - model = setup() - expect(model.get 'list').to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - - describe 'updates on `ids` mutations', -> - - it 'updates the value when `ids` is set', -> - model = (new Model).at '_page' - model.set 'colors', - green: - id: 'green' - rgb: [0, 255, 0] - hex: '#0f0' - red: - id: 'red' - rgb: [255, 0, 0] - hex: '#f00' - model.refList 'list', 'colors', 'ids' - expect(model.get 'list').to.eql [] - model.set 'ids', ['red', 'green', 'red'] - expect(model.get 'list').to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - - it 'emits on `from` when `ids` is set', (done) -> - model = (new Model).at '_page' - model.set 'colors', - green: - id: 'green' - rgb: [0, 255, 0] - hex: '#0f0' - red: - id: 'red' - rgb: [255, 0, 0] - hex: '#f00' - model.refList 'list', 'colors', 'ids' - model.on 'all', 'list**', (capture, method, index, values) -> - expect(capture).to.equal '' - expect(method).to.equal 'insert' - expect(index).to.equal 0 - expect(values).to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - done() - model.set 'ids', ['red', 'green', 'red'] - - it 'updates the value when `ids` children are set', -> - model = setup() - model.set 'ids.0', 'green' - expect(model.get 'list').to.eql [ - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - model.set 'ids.2', 'blue' - expect(model.get 'list').to.eql [ - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - undefined - ] - - it 'emits on `from` when `ids` children are set', (done) -> - model = setup() - model.on 'all', 'list**', (capture, method, value, previous) -> - expect(capture).to.equal '2' - expect(method).to.equal 'change' - expect(value).to.eql {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - expect(previous).to.eql {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - done() - model.set 'ids.2', 'green' - - it 'updates the value when `ids` are inserted', -> - model = setup() - model.push 'ids', 'green' - expect(model.get 'list').to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - ] - model.insert 'ids', 1, ['blue', 'red'] - expect(model.get 'list').to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - undefined - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - ] - - it 'emits on `from` when `ids` are inserted', (done) -> - model = setup() - model.on 'all', 'list**', (capture, method, index, inserted) -> - expect(capture).to.equal '' - expect(method).to.equal 'insert' - expect(index).to.equal 1 - expect(inserted).to.eql [ - undefined - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - done() - model.insert 'ids', 1, ['blue', 'red'] - - it 'updates the value when `ids` are removed', -> - model = setup() - model.pop 'ids' - expect(model.get 'list').to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - ] - model.remove 'ids', 0, 2 - expect(model.get 'list').to.eql [] - - it 'emits on `from` when `ids` are removed', (done) -> - model = setup() - model.on 'all', 'list**', (capture, method, index, removed) -> - expect(capture).to.equal '' - expect(method).to.equal 'remove' - expect(index).to.equal 0 - expect(removed).to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - ] - done() - model.remove 'ids', 0, 2 - - it 'updates the value when `ids` are moved', -> - model = setup() - model.move 'ids', 0, 2, 2 - expect(model.get 'list').to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - ] - model.move 'ids', 2, 0 - expect(model.get 'list').to.eql [ - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - - it 'emits on `from` when `ids` are moved', (done) -> - model = setup() - model.on 'all', 'list**', (capture, method, from, to, howMany) -> - expect(capture).to.equal '' - expect(method).to.equal 'move' - expect(from).to.equal 0 - expect(to).to.equal 2 - expect(howMany).to.eql 2 - done() - model.move 'ids', 0, 2, 2 - - describe 'emits events involving multiple refLists', -> - it 'removes data from a refList pointing to data in another refList', -> - model = (new Model).at '_page' - tagId = model.add 'tags', { text: 'hi' } - tagIds = [tagId] - - #profiles collection - id = model.add 'profiles', { tagIds: tagIds } - model.push 'profileIds', id - model.refList 'profilesList', 'profiles', 'profileIds' - - #ref a single item from collection - model.ref 'profile', 'profilesList.0' - - #remove from nested refList - model.refList 'tagsList', 'tags', 'profile.tagIds' - model.remove('tagsList', 0) - - describe 'updates on `to` mutations', -> - - it 'updates the value when `to` is set', -> - model = (new Model).at '_page' - model.set 'ids', ['red', 'green', 'red'] - model.refList 'list', 'colors', 'ids' - expect(model.get 'list').to.eql [undefined, undefined, undefined] - model.set 'colors', - green: - id: 'green' - rgb: [0, 255, 0] - hex: '#0f0' - red: - id: 'red' - rgb: [255, 0, 0] - hex: '#f00' - expect(model.get 'list').to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - - it 'emits on `from` when `to` is set', (done) -> - model = (new Model).at '_page' - model.set 'ids', ['red', 'green', 'red'] - model.refList 'list', 'colors', 'ids' - expectFromEvents model, done, [ - (capture, method, index, removed) -> - expect(capture).to.equal '' - expect(method).to.equal 'remove' - expect(index).to.equal 0 - expect(removed).to.eql [undefined, undefined, undefined] - , (capture, method, index, inserted) -> - expect(capture).to.equal '' - expect(method).to.equal 'insert' - expect(index).to.equal 0 - expect(inserted).to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - ] - model.set 'colors', - green: - id: 'green' - rgb: [0, 255, 0] - hex: '#0f0' - red: - id: 'red' - rgb: [255, 0, 0] - hex: '#f00' - - it 'updates the value when `to` children are set', -> - model = (new Model).at '_page' - model.set 'ids', ['red', 'green', 'red'] - model.refList 'list', 'colors', 'ids' - model.set 'colors.green', - id: 'green' - rgb: [0, 255, 0] - hex: '#0f0' - expect(model.get 'list').to.eql [ - undefined - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - undefined - ] - model.set 'colors.red', - id: 'red' - rgb: [255, 0, 0] - hex: '#f00' - expect(model.get 'list').to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - model.del 'colors.green' - expect(model.get 'list').to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - undefined - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - - it 'emits on `from` when `to` children are set', (done) -> - model = (new Model).at '_page' - model.set 'ids', ['red', 'green', 'red'] - model.refList 'list', 'colors', 'ids' - expectFromEvents model, done, [ - (capture, method, value, previous) -> - expect(capture).to.equal '0' - expect(method).to.equal 'change' - expect(value).to.eql {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - expect(previous).to.equal undefined - , (capture, method, value, previous) -> - expect(capture).to.equal '2' - expect(method).to.equal 'change' - expect(value).to.eql {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - expect(previous).to.equal undefined - ] - model.set 'colors.red', - id: 'red' - rgb: [255, 0, 0] - hex: '#f00' - - it 'updates the value when `to` descendants are set', -> - model = setup() - model.set 'colors.red.hex', '#e00' - expect(model.get 'list').to.eql [ - {id: 'red', rgb: [255, 0, 0], hex: '#e00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#e00'} - ] - model.set 'colors.red.rgb.0', 238 - expect(model.get 'list').to.eql [ - {id: 'red', rgb: [238, 0, 0], hex: '#e00'} - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [238, 0, 0], hex: '#e00'} - ] - - it 'emits on `from` when `to` descendants are set', (done) -> - model = setup() - expectFromEvents model, done, [ - (capture, method, value, previous) -> - expect(capture).to.equal '0.hex' - expect(method).to.equal 'change' - expect(value).to.eql '#e00' - expect(previous).to.equal '#f00' - , (capture, method, value, previous) -> - expect(capture).to.equal '2.hex' - expect(method).to.equal 'change' - expect(value).to.eql '#e00' - expect(previous).to.equal '#f00' - , (capture, method, value, previous) -> - expect(capture).to.equal '0.rgb.0' - expect(method).to.equal 'change' - expect(value).to.eql 238 - expect(previous).to.equal 255 - , (capture, method, value, previous) -> - expect(capture).to.equal '2.rgb.0' - expect(method).to.equal 'change' - expect(value).to.eql 238 - expect(previous).to.equal 255 - ] - model.set 'colors.red.hex', '#e00' - model.set 'colors.red.rgb.0', 238 - - it 'updates the value when inserting on `to` children', -> - model = (new Model).at '_page' - model.set 'nums', - even: [2, 4, 6] - odd: [1, 3] - model.set 'ids', ['even', 'odd', 'even'] - model.refList 'list', 'nums', 'ids' - expect(model.get 'list').to.eql [ - [2, 4, 6] - [1, 3] - [2, 4, 6] - ] - model.push 'nums.even', 8 - expect(model.get 'list').to.eql [ - [2, 4, 6, 8] - [1, 3] - [2, 4, 6, 8] - ] - - it 'emits on `from` when inserting on `to` children', (done) -> - model = (new Model).at '_page' - model.set 'nums', - even: [2, 4, 6] - odd: [1, 3] - model.set 'ids', ['even', 'odd', 'even'] - model.refList 'list', 'nums', 'ids' - expectFromEvents model, done, [ - (capture, method, index, inserted) -> - expect(capture).to.equal '0' - expect(method).to.equal 'insert' - expect(index).to.equal 3 - expect(inserted).to.eql [8] - , (capture, method, index, inserted) -> - expect(capture).to.equal '2' - expect(method).to.equal 'insert' - expect(index).to.equal 3 - expect(inserted).to.eql [8] - ] - model.push 'nums.even', 8 - - describe 'updates on `from` mutations', -> - - it 'updates `to` and `ids` when `from` is set', -> - model = setup() - model.set 'list', [ - {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - expect(model.get 'ids').to.eql ['green', 'red'] - expect(model.get 'colors').to.eql - green: {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - red: {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - # Changing items in the `from` list can only create new objects - # under `to`, and it does not remove them - model.del 'list' - expect(model.get 'ids').to.eql [] - expect(model.get 'colors').to.eql - green: {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - red: {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - model.set 'list', [ - {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} - {id: 'yellow', rgb: [255, 255, 0], hex: '#ff0'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - ] - expect(model.get 'ids').to.eql ['blue', 'yellow', 'red'] - expect(model.get 'colors').to.eql - green: {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - red: {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - blue: {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} - yellow: {id: 'yellow', rgb: [255, 255, 0], hex: '#ff0'} - - model.at('list.0').remove() - expect(model.get 'ids').to.eql ['yellow', 'red'] - expect(model.get 'colors').to.eql - green: {id: 'green', rgb: [0, 255, 0], hex: '#0f0'} - red: {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - blue: {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} - yellow: {id: 'yellow', rgb: [255, 255, 0], hex: '#ff0'} - - it 'emits on `to` when `from` is set', (done) -> - model = setup() - expectToEvents model, done, [ - (capture, method, value, previous) -> - expect(capture).to.equal 'blue' - expect(method).to.equal 'change' - expect(value).to.eql {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} - expect(previous).to.eql undefined - (capture, method, value, previous) -> - expect(capture).to.equal 'yellow' - expect(method).to.equal 'change' - expect(value).to.eql {id: 'yellow', rgb: [255, 255, 0], hex: '#ff0'} - expect(previous).to.eql undefined - ] - model.set 'list', [ - {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} - model.get('colors.red') - {id: 'yellow', rgb: [255, 255, 0], hex: '#ff0'} - ] - - it 'emits on `ids` when `from is set', (done) -> - model = setup() - expectIdsEvents model, done, [ - (capture, method, value, previous) -> - expect(capture).to.equal '' - expect(method).to.equal 'change' - expect(value).to.eql ['blue', 'red', 'yellow'] - expect(previous).to.eql ['red', 'green', 'red'] - ] - model.set 'list', [ - {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} - {id: 'red', rgb: [255, 0, 0], hex: '#f00'} - {id: 'yellow', rgb: [255, 255, 0], hex: '#ff0'} - ] - - it 'emits nothing on `to` when `from` is set, removing items', (done) -> - model = setup() - expectToEvents model, done, [] - model.set 'list', [] - - it 'creates a document in `to` on an insert', -> - model = setup() - model.insert 'list', 0, {id: 'yellow'} - expect(model.get('colors.yellow')).to.eql {id: 'yellow'} - - it 'creates a document in `to` on an insert of a doc with no id', -> - model = setup() - model.insert 'list', 0, {rgb: [1, 1, 1]} - newId = model.get('list.0').id - expect(model.get("colors.#{newId}")).to.eql {id: newId, rgb: [1, 1, 1]} - - describe 'event ordering', -> - - it 'should be able to resolve a non-existent nested property as undefined, inside an event listener on refA (where refA -> refList)', (done) -> - model = setup() - model.refList 'array', 'colors', 'arrayIds' - model.ref 'arrayAlias', 'array' - model.on 'insert', 'arrayAlias', -> - expect(model.get 'array.0.names.0').to.eql undefined - done() - model.insert 'arrayAlias', 0, {rgb: [1, 1, 1]} - - expect(model.get 'arrayIds').to.have.length(1) - - it 'correctly dereferences chained lists/refs when items are removed', (done) -> - model = setup() - model.add 'colors', - id: 'blue' - rgb: [0, 0, 255] - hex: '#00f' - model.add 'colors', - id: 'white' - rgb: [255, 255, 255] - hex: '#fff' - model.set 'palettes', - nature: - id: 'nature' - colors: ['green', 'blue', 'white'] - flag: - id: 'flag' - colors: ['red', 'white', 'blue'] - model.set 'schemes', ['nature', 'flag'] - - model.refList 'choices', 'palettes', 'schemes' - model.ref 'choice', 'choices.0' - paint = model.refList 'paint', 'colors', 'choice.colors' - - paint.on 'remove', (index, removed) -> - expect(index).to.equal 1 - expect(removed).to.eql [ - {id: 'blue', rgb: [0, 0, 255], hex: '#00f'} - ] - done() - paint.remove 1 - - describe 'deleteRemoved', -> - - it 'deletes the underlying object when an item is removed', -> - model = setup {deleteRemoved: true} - expect(model.get 'colors').to.eql - green: - id: 'green' - rgb: [0, 255, 0] - hex: '#0f0' - red: - id: 'red' - rgb: [255, 0, 0] - hex: '#f00' - model.remove 'list', 0 - expect(model.get 'colors').to.eql - green: - id: 'green' - rgb: [0, 255, 0] - hex: '#0f0' - diff --git a/test/Model/refList.mocha.js b/test/Model/refList.mocha.js new file mode 100644 index 000000000..960771927 --- /dev/null +++ b/test/Model/refList.mocha.js @@ -0,0 +1,882 @@ +var expect = require('../util').expect; +var Model = require('../../lib/Model'); + +describe('refList', function() { + function setup(options) { + var model = (new Model).at('_page'); + model.set('colors', { + green: { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, + red: { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + }); + model.set('ids', ['red', 'green', 'red']); + model.refList('list', 'colors', 'ids', options); + return model; + } + function expectEvents(pattern, model, done, events) { + model.on('all', pattern, function() { + events.shift().apply(null, arguments); + if (!events.length) done(); + }); + if (!events || !events.length) done(); + } + function expectFromEvents(model, done, events) { + expectEvents('list**', model, done, events); + } + function expectToEvents(model, done, events) { + expectEvents('colors**', model, done, events); + } + function expectIdsEvents(model, done, events) { + expectEvents('ids**', model, done, events); + } + describe('sets output on initial call', function() { + it('sets the initial value to empty array if no inputs', function() { + var model = (new Model).at('_page'); + model.refList('empty', 'colors', 'noIds'); + expect(model.get('empty')).to.eql([]); + }); + it('sets the initial value for already populated data', function() { + var model = setup(); + expect(model.get('list')).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + }); + }); + describe('updates on `ids` mutations', function() { + it('updates the value when `ids` is set', function() { + var model = (new Model).at('_page'); + model.set('colors', { + green: { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, + red: { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + }); + model.refList('list', 'colors', 'ids'); + expect(model.get('list')).to.eql([]); + model.set('ids', ['red', 'green', 'red']); + expect(model.get('list')).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + }); + it('emits on `from` when `ids` is set', function(done) { + var model = (new Model).at('_page'); + model.set('colors', { + green: { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, + red: { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + }); + model.refList('list', 'colors', 'ids'); + model.on('all', 'list**', function(capture, method, index, values) { + expect(capture).to.equal(''); + expect(method).to.equal('insert'); + expect(index).to.equal(0); + expect(values).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + done(); + }); + model.set('ids', ['red', 'green', 'red']); + }); + it('updates the value when `ids` children are set', function() { + var model = setup(); + model.set('ids.0', 'green'); + expect(model.get('list')).to.eql([ + { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + model.set('ids.2', 'blue'); + expect(model.get('list')).to.eql([ + { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, void 0 + ]); + }); + it('emits on `from` when `ids` children are set', function(done) { + var model = setup(); + model.on('all', 'list**', function(capture, method, value, previous) { + expect(capture).to.equal('2'); + expect(method).to.equal('change'); + expect(value).to.eql({ + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }); + expect(previous).to.eql({ + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }); + done(); + }); + model.set('ids.2', 'green'); + }); + it('updates the value when `ids` are inserted', function() { + var model = setup(); + model.push('ids', 'green'); + expect(model.get('list')).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + } + ]); + model.insert('ids', 1, ['blue', 'red']); + expect(model.get('list')).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, void 0, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + } + ]); + }); + it('emits on `from` when `ids` are inserted', function(done) { + var model = setup(); + model.on('all', 'list**', function(capture, method, index, inserted) { + expect(capture).to.equal(''); + expect(method).to.equal('insert'); + expect(index).to.equal(1); + expect(inserted).to.eql([ + void 0, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + done(); + }); + model.insert('ids', 1, ['blue', 'red']); + }); + it('updates the value when `ids` are removed', function() { + var model = setup(); + model.pop('ids'); + expect(model.get('list')).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + } + ]); + model.remove('ids', 0, 2); + expect(model.get('list')).to.eql([]); + }); + it('emits on `from` when `ids` are removed', function(done) { + var model = setup(); + model.on('all', 'list**', function(capture, method, index, removed) { + expect(capture).to.equal(''); + expect(method).to.equal('remove'); + expect(index).to.equal(0); + expect(removed).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + } + ]); + done(); + }); + model.remove('ids', 0, 2); + }); + it('updates the value when `ids` are moved', function() { + var model = setup(); + model.move('ids', 0, 2, 2); + expect(model.get('list')).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + } + ]); + model.move('ids', 2, 0); + expect(model.get('list')).to.eql([ + { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + }); + it('emits on `from` when `ids` are moved', function(done) { + var model = setup(); + model.on('all', 'list**', function(capture, method, from, to, howMany) { + expect(capture).to.equal(''); + expect(method).to.equal('move'); + expect(from).to.equal(0); + expect(to).to.equal(2); + expect(howMany).to.eql(2); + done(); + }); + model.move('ids', 0, 2, 2); + }); + }); + describe('emits events involving multiple refLists', function() { + it('removes data from a refList pointing to data in another refList', function() { + var model = (new Model).at('_page'); + var tagId = model.add('tags', { + text: 'hi' + }); + var tagIds = [tagId]; + var id = model.add('profiles', { + tagIds: tagIds + }); + model.push('profileIds', id); + model.refList('profilesList', 'profiles', 'profileIds'); + model.ref('profile', 'profilesList.0'); + model.refList('tagsList', 'tags', 'profile.tagIds'); + model.remove('tagsList', 0); + }); + }); + describe('updates on `to` mutations', function() { + it('updates the value when `to` is set', function() { + var model = (new Model).at('_page'); + model.set('ids', ['red', 'green', 'red']); + model.refList('list', 'colors', 'ids'); + expect(model.get('list')).to.eql([void 0, void 0, void 0]); + model.set('colors', { + green: { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, + red: { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + }); + expect(model.get('list')).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + }); + it('emits on `from` when `to` is set', function(done) { + var model = (new Model).at('_page'); + model.set('ids', ['red', 'green', 'red']); + model.refList('list', 'colors', 'ids'); + expectFromEvents(model, done, [ + function(capture, method, index, removed) { + expect(capture).to.equal(''); + expect(method).to.equal('remove'); + expect(index).to.equal(0); + expect(removed).to.eql([void 0, void 0, void 0]); + }, function(capture, method, index, inserted) { + expect(capture).to.equal(''); + expect(method).to.equal('insert'); + expect(index).to.equal(0); + expect(inserted).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + } + ]); + model.set('colors', { + green: { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, + red: { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + }); + }); + it('updates the value when `to` children are set', function() { + var model = (new Model).at('_page'); + model.set('ids', ['red', 'green', 'red']); + model.refList('list', 'colors', 'ids'); + model.set('colors.green', { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }); + expect(model.get('list')).to.eql([ + void 0, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, void 0 + ]); + model.set('colors.red', { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }); + expect(model.get('list')).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + model.del('colors.green'); + expect(model.get('list')).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, void 0, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + }); + it('emits on `from` when `to` children are set', function(done) { + var model = (new Model).at('_page'); + model.set('ids', ['red', 'green', 'red']); + model.refList('list', 'colors', 'ids'); + expectFromEvents(model, done, [ + function(capture, method, value, previous) { + expect(capture).to.equal('0'); + expect(method).to.equal('change'); + expect(value).to.eql({ + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }); + expect(previous).to.equal(void 0); + }, function(capture, method, value, previous) { + expect(capture).to.equal('2'); + expect(method).to.equal('change'); + expect(value).to.eql({ + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }); + expect(previous).to.equal(void 0); + } + ]); + model.set('colors.red', { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }); + }); + it('updates the value when `to` descendants are set', function() { + var model = setup(); + model.set('colors.red.hex', '#e00'); + expect(model.get('list')).to.eql([ + { + id: 'red', + rgb: [255, 0, 0], + hex: '#e00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#e00' + } + ]); + model.set('colors.red.rgb.0', 238); + expect(model.get('list')).to.eql([ + { + id: 'red', + rgb: [238, 0, 0], + hex: '#e00' + }, { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [238, 0, 0], + hex: '#e00' + } + ]); + }); + it('emits on `from` when `to` descendants are set', function(done) { + var model = setup(); + expectFromEvents(model, done, [ + function(capture, method, value, previous) { + expect(capture).to.equal('0.hex'); + expect(method).to.equal('change'); + expect(value).to.eql('#e00'); + expect(previous).to.equal('#f00'); + }, function(capture, method, value, previous) { + expect(capture).to.equal('2.hex'); + expect(method).to.equal('change'); + expect(value).to.eql('#e00'); + expect(previous).to.equal('#f00'); + }, function(capture, method, value, previous) { + expect(capture).to.equal('0.rgb.0'); + expect(method).to.equal('change'); + expect(value).to.eql(238); + expect(previous).to.equal(255); + }, function(capture, method, value, previous) { + expect(capture).to.equal('2.rgb.0'); + expect(method).to.equal('change'); + expect(value).to.eql(238); + expect(previous).to.equal(255); + } + ]); + model.set('colors.red.hex', '#e00'); + model.set('colors.red.rgb.0', 238); + }); + it('updates the value when inserting on `to` children', function() { + var model = (new Model).at('_page'); + model.set('nums', { + even: [2, 4, 6], + odd: [1, 3] + }); + model.set('ids', ['even', 'odd', 'even']); + model.refList('list', 'nums', 'ids'); + expect(model.get('list')).to.eql([[2, 4, 6], [1, 3], [2, 4, 6]]); + model.push('nums.even', 8); + expect(model.get('list')).to.eql([[2, 4, 6, 8], [1, 3], [2, 4, 6, 8]]); + }); + it('emits on `from` when inserting on `to` children', function(done) { + var model = (new Model).at('_page'); + model.set('nums', { + even: [2, 4, 6], + odd: [1, 3] + }); + model.set('ids', ['even', 'odd', 'even']); + model.refList('list', 'nums', 'ids'); + expectFromEvents(model, done, [ + function(capture, method, index, inserted) { + expect(capture).to.equal('0'); + expect(method).to.equal('insert'); + expect(index).to.equal(3); + expect(inserted).to.eql([8]); + }, function(capture, method, index, inserted) { + expect(capture).to.equal('2'); + expect(method).to.equal('insert'); + expect(index).to.equal(3); + expect(inserted).to.eql([8]); + } + ]); + model.push('nums.even', 8); + }); + }); + describe('updates on `from` mutations', function() { + it('updates `to` and `ids` when `from` is set', function() { + var model = setup(); + model.set('list', [ + { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + expect(model.get('ids')).to.eql(['green', 'red']); + expect(model.get('colors')).to.eql({ + green: { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, + red: { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + }); + model.del('list'); + expect(model.get('ids')).to.eql([]); + expect(model.get('colors')).to.eql({ + green: { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, + red: { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + }); + model.set('list', [ + { + id: 'blue', + rgb: [0, 0, 255], + hex: '#00f' + }, { + id: 'yellow', + rgb: [255, 255, 0], + hex: '#ff0' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + ]); + expect(model.get('ids')).to.eql(['blue', 'yellow', 'red']); + expect(model.get('colors')).to.eql({ + green: { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, + red: { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, + blue: { + id: 'blue', + rgb: [0, 0, 255], + hex: '#00f' + }, + yellow: { + id: 'yellow', + rgb: [255, 255, 0], + hex: '#ff0' + } + }); + model.at('list.0').remove(); + expect(model.get('ids')).to.eql(['yellow', 'red']); + expect(model.get('colors')).to.eql({ + green: { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, + red: { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, + blue: { + id: 'blue', + rgb: [0, 0, 255], + hex: '#00f' + }, + yellow: { + id: 'yellow', + rgb: [255, 255, 0], + hex: '#ff0' + } + }); + }); + it('emits on `to` when `from` is set', function(done) { + var model = setup(); + expectToEvents(model, done, [ + function(capture, method, value, previous) { + expect(capture).to.equal('blue'); + expect(method).to.equal('change'); + expect(value).to.eql({ + id: 'blue', + rgb: [0, 0, 255], + hex: '#00f' + }); + expect(previous).to.eql(void 0); + }, function(capture, method, value, previous) { + expect(capture).to.equal('yellow'); + expect(method).to.equal('change'); + expect(value).to.eql({ + id: 'yellow', + rgb: [255, 255, 0], + hex: '#ff0' + }); + expect(previous).to.eql(void 0); + } + ]); + model.set('list', [ + { + id: 'blue', + rgb: [0, 0, 255], + hex: '#00f' + }, model.get('colors.red'), { + id: 'yellow', + rgb: [255, 255, 0], + hex: '#ff0' + } + ]); + }); + it('emits on `ids` when `from is set', function(done) { + var model = setup(); + expectIdsEvents(model, done, [ + function(capture, method, value, previous) { + expect(capture).to.equal(''); + expect(method).to.equal('change'); + expect(value).to.eql(['blue', 'red', 'yellow']); + expect(previous).to.eql(['red', 'green', 'red']); + } + ]); + model.set('list', [ + { + id: 'blue', + rgb: [0, 0, 255], + hex: '#00f' + }, { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + }, { + id: 'yellow', + rgb: [255, 255, 0], + hex: '#ff0' + } + ]); + }); + it('emits nothing on `to` when `from` is set, removing items', function(done) { + var model = setup(); + expectToEvents(model, done, []); + model.set('list', []); + }); + it('creates a document in `to` on an insert', function() { + var model = setup(); + model.insert('list', 0, { + id: 'yellow' + }); + expect(model.get('colors.yellow')).to.eql({ + id: 'yellow' + }); + }); + it('creates a document in `to` on an insert of a doc with no id', function() { + var model = setup(); + model.insert('list', 0, { + rgb: [1, 1, 1] + }); + var newId = model.get('list.0').id; + expect(model.get("colors." + newId)).to.eql({ + id: newId, + rgb: [1, 1, 1] + }); + }); + }); + describe('event ordering', function() { + it('should be able to resolve a non-existent nested property as undefined, inside an event listener on refA (where refA -> refList)', function(done) { + var model = setup(); + model.refList('array', 'colors', 'arrayIds'); + model.ref('arrayAlias', 'array'); + model.on('insert', 'arrayAlias', function() { + expect(model.get('array.0.names.0')).to.eql(void 0); + done(); + }); + model.insert('arrayAlias', 0, { + rgb: [1, 1, 1] + }); + expect(model.get('arrayIds')).to.have.length(1); + }); + // This is a bug. Currently skipping it, since it is a complex edge case and + // really may require thinking how events + mutations are propogated in refs + it.skip('correctly dereferences chained lists/refs when items are removed', function(done) { + var model = setup(); + model.add('colors', { + id: 'blue', + rgb: [0, 0, 255], + hex: '#00f' + }); + model.add('colors', { + id: 'white', + rgb: [255, 255, 255], + hex: '#fff' + }); + model.set('palettes', { + nature: { + id: 'nature', + colors: ['green', 'blue', 'white'] + }, + flag: { + id: 'flag', + colors: ['red', 'white', 'blue'] + } + }); + model.set('schemes', ['nature', 'flag']); + model.refList('choices', 'palettes', 'schemes'); + model.ref('choice', 'choices.0'); + var paint = model.refList('paint', 'colors', 'choice.colors'); + paint.on('remove', function(index, removed) { + expect(index).to.equal(1); + expect(removed).to.eql([ + { + id: 'blue', + rgb: [0, 0, 255], + hex: '#00f' + } + ]); + done(); + }); + paint.remove(1); + }); + }); + describe('deleteRemoved', function() { + it('deletes the underlying object when an item is removed', function() { + var model = setup({deleteRemoved: true}); + expect(model.get('colors')).to.eql({ + green: { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + }, + red: { + id: 'red', + rgb: [255, 0, 0], + hex: '#f00' + } + }); + model.remove('list', 0); + expect(model.get('colors')).to.eql({ + green: { + id: 'green', + rgb: [0, 255, 0], + hex: '#0f0' + } + }); + }); + }); +}); diff --git a/test/mocha.opts b/test/mocha.opts index a4a98d415..df349bb0a 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,4 +1,3 @@ ---compilers coffee:coffee-script/register --reporter spec --timeout 1200 --bail diff --git a/test/util/util.mocha.js b/test/util/util.mocha.js new file mode 100644 index 000000000..fb9e935f3 --- /dev/null +++ b/test/util/util.mocha.js @@ -0,0 +1,65 @@ +// Generated by CoffeeScript 1.8.0 +var expect, util; + +expect = require('../util').expect; + +util = require('../../lib/util'); + +describe('util', function() { + return describe('util.mergeInto', function() { + it('merges empty objects', function() { + var a, b; + a = {}; + b = {}; + return expect(util.mergeInto(a, b)).to.eql({}); + }); + it('merges an empty object with a populated object', function() { + var a, b, fn; + fn = function(x) { + return x++; + }; + a = {}; + b = { + x: 's', + y: [1, 3], + fn: fn + }; + return expect(util.mergeInto(a, b)).to.eql({ + x: 's', + y: [1, 3], + fn: fn + }); + }); + return it('merges a populated object with a populated object', function() { + var a, b, fn; + fn = function(x) { + return x++; + }; + a = { + x: 's', + y: [1, 3], + fn: fn + }; + b = { + x: 7, + z: {} + }; + expect(util.mergeInto(a, b)).to.eql({ + x: 7, + y: [1, 3], + fn: fn, + z: {} + }); + expect(a).to.eql({ + x: 7, + y: [1, 3], + fn: fn, + z: {} + }); + return expect(b).to.eql({ + x: 7, + z: {} + }); + }); + }); +}); From 1639e64a10c5d58f66950337d0d14ec889295574 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 30 Oct 2014 16:26:03 -0700 Subject: [PATCH 012/479] remove straggler coffee test file --- test/Model/filter.mocha.coffee | 165 --------------------------------- 1 file changed, 165 deletions(-) delete mode 100644 test/Model/filter.mocha.coffee diff --git a/test/Model/filter.mocha.coffee b/test/Model/filter.mocha.coffee deleted file mode 100644 index 3f746b572..000000000 --- a/test/Model/filter.mocha.coffee +++ /dev/null @@ -1,165 +0,0 @@ -{expect} = require '../util' -Model = require '../../lib/Model' - -describe 'filter', -> - - describe 'getting', -> - - it 'supports filter of array', -> - model = (new Model).at '_page' - model.set 'numbers', [0, 3, 4, 1, 2, 3, 0] - filter = model.filter 'numbers', (number, i, numbers) -> - return (number % 2) == 0 - expect(filter.get()).to.eql [0, 4, 2, 0] - - it 'supports sort of array', -> - model = (new Model).at '_page' - model.set 'numbers', [0, 3, 4, 1, 2, 3, 0] - filter = model.sort 'numbers', 'asc' - expect(filter.get()).to.eql [0, 0, 1, 2, 3, 3, 4] - filter = model.sort 'numbers', 'desc' - expect(filter.get()).to.eql [4, 3, 3, 2, 1, 0, 0] - - it 'supports filter and sort of array', -> - model = (new Model).at '_page' - model.set 'numbers', [0, 3, 4, 1, 2, 3, 0] - model.fn 'even', (number) -> - return (number % 2) == 0 - filter = model.filter('numbers', 'even').sort() - expect(filter.get()).to.eql [0, 0, 2, 4] - - it 'supports filter of object', -> - model = (new Model).at '_page' - numbers = [0, 3, 4, 1, 2, 3, 0] - for number in numbers - model.set 'numbers.' + model.id(), number - filter = model.filter 'numbers', (number, id, numbers) -> - return (number % 2) == 0 - expect(filter.get()).to.eql [0, 4, 2, 0] - - it 'supports sort of object', -> - model = (new Model).at '_page' - numbers = [0, 3, 4, 1, 2, 3, 0] - for number in numbers - model.set 'numbers.' + model.id(), number - filter = model.sort 'numbers', 'asc' - expect(filter.get()).to.eql [0, 0, 1, 2, 3, 3, 4] - filter = model.sort 'numbers', 'desc' - expect(filter.get()).to.eql [4, 3, 3, 2, 1, 0, 0] - - it 'supports filter and sort of object', -> - model = (new Model).at '_page' - numbers = [0, 3, 4, 1, 2, 3, 0] - for number in numbers - model.set 'numbers.' + model.id(), number - model.fn 'even', (number) -> - return (number % 2) == 0 - filter = model.filter('numbers', 'even').sort() - expect(filter.get()).to.eql [0, 0, 2, 4] - - describe 'initial value set by ref', -> - - it 'supports filter of array', -> - model = (new Model).at '_page' - model.set 'numbers', [0, 3, 4, 1, 2, 3, 0] - filter = model.filter 'numbers', (number) -> - return (number % 2) == 0 - filter.ref '_page.out' - expect(model.get 'out').to.eql [0, 4, 2, 0] - - it 'supports sort of array', -> - model = (new Model).at '_page' - model.set 'numbers', [0, 3, 4, 1, 2, 3, 0] - filter = model.sort 'numbers', 'asc' - expect(filter.get()).to.eql [0, 0, 1, 2, 3, 3, 4] - filter = model.sort 'numbers', 'desc' - filter.ref '_page.out' - expect(model.get 'out').to.eql [4, 3, 3, 2, 1, 0, 0] - - it 'supports filter and sort of array', -> - model = (new Model).at '_page' - model.set 'numbers', [0, 3, 4, 1, 2, 3, 0] - model.fn 'even', (number) -> - return (number % 2) == 0 - filter = model.filter('numbers', 'even').sort() - filter.ref '_page.out' - expect(model.get 'out').to.eql [0, 0, 2, 4] - - it 'supports filter of object', -> - model = (new Model).at '_page' - numbers = [0, 3, 4, 1, 2, 3, 0] - for number in numbers - model.set 'numbers.' + model.id(), number - filter = model.filter 'numbers', (number) -> - return (number % 2) == 0 - filter.ref '_page.out' - expect(model.get 'out').to.eql [0, 4, 2, 0] - - it 'supports sort of object', -> - model = (new Model).at '_page' - numbers = [0, 3, 4, 1, 2, 3, 0] - for number in numbers - model.set 'numbers.' + model.id(), number - filter = model.sort 'numbers', 'asc' - expect(filter.get()).to.eql [0, 0, 1, 2, 3, 3, 4] - filter = model.sort 'numbers', 'desc' - filter.ref '_page.out' - expect(model.get 'out').to.eql [4, 3, 3, 2, 1, 0, 0] - - it 'supports filter and sort of object', -> - model = (new Model).at '_page' - numbers = [0, 3, 4, 1, 2, 3, 0] - for number in numbers - model.set 'numbers.' + model.id(), number - model.fn 'even', (number) -> - return (number % 2) == 0 - filter = model.filter('numbers', 'even').sort() - filter.ref '_page.out' - expect(model.get 'out').to.eql [0, 0, 2, 4] - - describe 'ref updates as items are modified', -> - - it 'supports filter of array', -> - model = (new Model).at '_page' - model.set 'numbers', [0, 3, 4, 1, 2, 3, 0] - filter = model.filter 'numbers', (number) -> - return (number % 2) == 0 - filter.ref '_page.out' - expect(model.get 'out').to.eql [0, 4, 2, 0] - model.push 'numbers', 6 - expect(model.get 'out').to.eql [0, 4, 2, 0, 6] - model.set 'numbers.2', 1 - expect(model.get 'out').to.eql [0, 2, 0, 6] - model.del 'numbers' - expect(model.get 'out').to.eql [] - model.set 'numbers', [1, 2, 0] - expect(model.get 'out').to.eql [2, 0] - - it 'supports filter of object', -> - model = (new Model).at '_page' - greenId = model.add 'colors', - name: 'green' - primary: true - orangeId = model.add 'colors', - name: 'orange' - primary: false - redId = model.add 'colors', - name: 'red' - primary: true - filter = model.filter 'colors', (color) -> color.primary - filter.ref '_page.out' - expect(model.get 'out').to.eql [ - {name: 'green', primary: true, id: greenId} - {name: 'red', primary: true, id: redId} - ] - model.set 'colors.' + greenId + '.primary', false - expect(model.get 'out').to.eql [ - {name: 'red', primary: true, id: redId} - ] - yellowId = model.add 'colors', - name: 'yellow' - primary: true - expect(model.get 'out').to.eql [ - {name: 'red', primary: true, id: redId} - {name: 'yellow', primary: true, id: yellowId} - ] From 08f548bbd6ec7bf60f2ff7deb2e3d75e387c4751 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 30 Oct 2014 16:34:38 -0700 Subject: [PATCH 013/479] remove model.filter on an array (only support objects) This feature was really buggy anyway. Better just to remove it for now, since it works well on objects only --- lib/Model/filter.js | 58 +++++++++++--------------------- test/Model/filter.mocha.js | 68 +++----------------------------------- 2 files changed, 24 insertions(+), 102 deletions(-) diff --git a/lib/Model/filter.js b/lib/Model/filter.js index f0b8689b0..736ec7846 100644 --- a/lib/Model/filter.js +++ b/lib/Model/filter.js @@ -160,27 +160,18 @@ Filter.prototype.ids = function() { var ids = []; if (!items) return ids; if (Array.isArray(items)) { - if (this.filterFn) { - for (var i = 0; i < items.length; i++) { - if (this.filterFn.call(this.model, items[i], i, items)) { - ids.push(i); - } + throw new Error('model.filter is not currently supported on arrays'); + } + if (this.filterFn) { + for (var key in items) { + if (items.hasOwnProperty(key) && + this.filterFn.call(this.model, items[key], key, items) + ) { + ids.push(key); } - } else { - for (var i = 0; i < items.length; i++) ids.push(i); } } else { - if (this.filterFn) { - for (var key in items) { - if (items.hasOwnProperty(key) && - this.filterFn.call(this.model, items[key], key, items) - ) { - ids.push(key); - } - } - } else { - ids = Object.keys(items); - } + ids = Object.keys(items); } var sortFn = this.sortFn; if (sortFn) { @@ -195,29 +186,20 @@ Filter.prototype.get = function() { var items = this.model._get(this.inputSegments); var results = []; if (Array.isArray(items)) { - if (this.filterFn) { - for (var i = 0; i < items.length; i++) { - if (this.filterFn.call(this.model, items[i], i, items)) { - results.push(items[i]); - } + throw new Error('model.filter is not currently supported on arrays'); + } + if (this.filterFn) { + for (var key in items) { + if (items.hasOwnProperty(key) && + this.filterFn.call(this.model, items[key], key, items) + ) { + results.push(items[key]); } - } else { - results = items.slice(); } } else { - if (this.filterFn) { - for (var key in items) { - if (items.hasOwnProperty(key) && - this.filterFn.call(this.model, items[key], key, items) - ) { - results.push(items[key]); - } - } - } else { - for (var key in items) { - if (items.hasOwnProperty(key)) { - results.push(items[key]); - } + for (var key in items) { + if (items.hasOwnProperty(key)) { + results.push(items[key]); } } } diff --git a/test/Model/filter.mocha.js b/test/Model/filter.mocha.js index 63fb783ff..ae3e23c82 100644 --- a/test/Model/filter.mocha.js +++ b/test/Model/filter.mocha.js @@ -3,30 +3,15 @@ var Model = require('../../lib/Model'); describe('filter', function() { describe('getting', function() { - it('supports filter of array', function() { + it('does not support array', function() { var model = (new Model).at('_page'); model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); var filter = model.filter('numbers', function(number, i, numbers) { return (number % 2) === 0; }); - expect(filter.get()).to.eql([0, 4, 2, 0]); - }); - it('supports sort of array', function() { - var model = (new Model).at('_page'); - model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); - filter = model.sort('numbers', 'asc'); - expect(filter.get()).to.eql([0, 0, 1, 2, 3, 3, 4]); - filter = model.sort('numbers', 'desc'); - expect(filter.get()).to.eql([4, 3, 3, 2, 1, 0, 0]); - }); - it('supports filter and sort of array', function() { - var model = (new Model).at('_page'); - model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); - model.fn('even', function(number) { - return (number % 2) === 0; - }); - var filter = model.filter('numbers', 'even').sort(); - expect(filter.get()).to.eql([0, 0, 2, 4]); + expect(function() { + filter.get(); + }).to.throwException(); }); it('supports filter of object', function() { var model = (new Model).at('_page'); @@ -64,34 +49,6 @@ describe('filter', function() { }); }); describe('initial value set by ref', function() { - it('supports filter of array', function() { - var model = (new Model).at('_page'); - model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); - var filter = model.filter('numbers', function(number) { - return (number % 2) === 0; - }); - filter.ref('_page.out'); - expect(model.get('out')).to.eql([0, 4, 2, 0]); - }); - it('supports sort of array', function() { - var model = (new Model).at('_page'); - model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); - filter = model.sort('numbers', 'asc'); - expect(filter.get()).to.eql([0, 0, 1, 2, 3, 3, 4]); - filter = model.sort('numbers', 'desc'); - filter.ref('_page.out'); - expect(model.get('out')).to.eql([4, 3, 3, 2, 1, 0, 0]); - }); - it('supports filter and sort of array', function() { - var model = (new Model).at('_page'); - model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); - model.fn('even', function(number) { - return (number % 2) === 0; - }); - var filter = model.filter('numbers', 'even').sort(); - filter.ref('_page.out'); - expect(model.get('out')).to.eql([0, 0, 2, 4]); - }); it('supports filter of object', function() { var model = (new Model).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; @@ -131,23 +88,6 @@ describe('filter', function() { }); }); describe('ref updates as items are modified', function() { - it('supports filter of array', function() { - var model = (new Model).at('_page'); - model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); - var filter = model.filter('numbers', function(number) { - return (number % 2) === 0; - }); - filter.ref('_page.out'); - expect(model.get('out')).to.eql([0, 4, 2, 0]); - model.push('numbers', 6); - expect(model.get('out')).to.eql([0, 4, 2, 0, 6]); - model.set('numbers.2', 1); - expect(model.get('out')).to.eql([0, 2, 0, 6]); - model.del('numbers'); - expect(model.get('out')).to.eql([]); - model.set('numbers', [1, 2, 0]); - expect(model.get('out')).to.eql([2, 0]); - }); it('supports filter of object', function() { var model = (new Model).at('_page'); var greenId = model.add('colors', { From a87f347fc1c90c0de1f3317342ad419b08f1a5fd Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 30 Oct 2014 17:48:07 -0700 Subject: [PATCH 014/479] add support for additional reactive inputs to model.filter --- lib/Model/filter.js | 131 ++++++++++++++++++++++--------------- lib/Model/fn.js | 6 +- lib/Model/unbundle.js | 2 +- test/Model/filter.mocha.js | 17 +++++ 4 files changed, 100 insertions(+), 56 deletions(-) diff --git a/lib/Model/filter.js b/lib/Model/filter.js index 736ec7846..b0bb125a6 100644 --- a/lib/Model/filter.js +++ b/lib/Model/filter.js @@ -11,52 +11,57 @@ Model.INITS.push(function(model) { for (var path in map) { var filter = map[path]; if (pass.$filter === filter) continue; - if (util.mayImpact(filter.inputSegments, segments)) { + if ( + util.mayImpact(filter.segments, segments) || + (filter.inputsSegments && util.mayImpactAny(filter.inputsSegments, segments)) + ) { filter.update(pass); } } } }); -Model.prototype.filter = function() { - var input, options, fn; - if (arguments.length === 1) { - fn = arguments[0]; - } else if (arguments.length === 2) { - if (this.isPath(arguments[0])) { - input = arguments[0]; - } else { - options = arguments[0]; - } - fn = arguments[1]; - } else { - input = arguments[0]; - options = arguments[1]; - fn = arguments[2]; +function parseFilterArguments(model, args) { + var fn = args.pop(); + var path = model.path(args.shift()); + var options; + if (!model.isPath(args[args.length - 1])) { + options = args.pop(); + } + var i = args.length; + while (i--) { + args[i] = model.path(args[i]); } - var inputPath = this.path(input); - return this.root._filters.add(inputPath, fn, null, options); + return { + path: path, + inputPaths: (args.length) ? args : null, + options: options, + fn: fn + }; +} + +Model.prototype.filter = function() { + var args = Array.prototype.slice.call(arguments); + var parsed = parseFilterArguments(this, args); + return this.root._filters.add( + parsed.path, + parsed.fn, + null, + parsed.inputPaths, + parsed.options + ); }; Model.prototype.sort = function() { - var input, options, fn; - if (arguments.length === 1) { - fn = arguments[0]; - } else if (arguments.length === 2) { - if (this.isPath(arguments[0])) { - input = arguments[0]; - } else { - options = arguments[0]; - } - fn = arguments[1]; - } else { - input = arguments[0]; - options = arguments[1]; - fn = arguments[2]; - } - if (!fn) throw new TypeError('Sort function is required'); - var inputPath = this.path(input); - return this.root._filters.add(inputPath, null, fn, options); + var args = Array.prototype.slice.call(arguments); + var parsed = parseFilterArguments(this, args); + return this.root._filters.add( + parsed.path, + null, + parsed.fn || 'asc', + parsed.inputPaths, + parsed.options + ); }; Model.prototype.removeAllFilters = function(subpath) { @@ -78,8 +83,8 @@ function Filters(model) { this.fromMap = new FromMap(); } -Filters.prototype.add = function(inputPath, filterFn, sortFn, options) { - return new Filter(this, inputPath, filterFn, sortFn, options); +Filters.prototype.add = function(path, filterFn, sortFn, inputPaths, options) { + return new Filter(this, path, filterFn, sortFn, inputPaths, options); }; Filters.prototype.toJSON = function() { @@ -88,23 +93,32 @@ Filters.prototype.toJSON = function() { var filter = this.fromMap[from]; // Don't try to bundle if functions were passed directly instead of by name if (!filter.bundle) continue; - var args = [from, filter.inputPath, filter.filterName, filter.sortName]; + var args = [from, filter.path, filter.filterName, filter.sortName, filter.inputPaths]; if (filter.options) args.push(filter.options); out.push(args); } return out; }; -function Filter(filters, inputPath, filterFn, sortFn, options) { +function Filter(filters, path, filterFn, sortFn, inputPaths, options) { this.filters = filters; this.model = filters.model.pass({$filter: this}); - this.inputPath = inputPath; - this.inputSegments = inputPath.split('.'); + this.path = path; + this.segments = path.split('.'); this.filterName = null; this.sortName = null; this.bundle = true; this.filterFn = null; this.sortFn = null; + this.inputPaths = inputPaths; + this.inputsSegments = null; + if (inputPaths) { + this.inputsSegments = []; + for (var i = 0; i < this.inputPaths.length; i++) { + var segments = this.inputPaths[i].split('.'); + this.inputsSegments.push(segments); + } + } this.options = options; this.skip = options && options.skip; this.limit = options && options.limit; @@ -155,18 +169,34 @@ Filter.prototype._slice = function(results) { return results.slice(begin, end); }; +Filter.prototype.getInputs = function() { + if (!this.inputsSegments) return; + var inputs = []; + for (var i = 0, len = this.inputsSegments.length; i < len; i++) { + var input = this.model._get(this.inputsSegments[i]); + inputs.push(input); + } + return inputs; +}; + +Filter.prototype.callFilter = function(items, key, inputs) { + var item = items[key]; + return (inputs) ? + this.filterFn.apply(this.model, [item, key, items].concat(inputs)) : + this.filterFn.call(this.model, item, key, items); +}; + Filter.prototype.ids = function() { - var items = this.model._get(this.inputSegments); + var items = this.model._get(this.segments); var ids = []; if (!items) return ids; if (Array.isArray(items)) { throw new Error('model.filter is not currently supported on arrays'); } if (this.filterFn) { + var inputs = this.getInputs(); for (var key in items) { - if (items.hasOwnProperty(key) && - this.filterFn.call(this.model, items[key], key, items) - ) { + if (items.hasOwnProperty(key) && this.callFilter(items, key, inputs)) { ids.push(key); } } @@ -183,16 +213,15 @@ Filter.prototype.ids = function() { }; Filter.prototype.get = function() { - var items = this.model._get(this.inputSegments); + var items = this.model._get(this.segments); var results = []; if (Array.isArray(items)) { throw new Error('model.filter is not currently supported on arrays'); } if (this.filterFn) { + var inputs = this.getInputs(); for (var key in items) { - if (items.hasOwnProperty(key) && - this.filterFn.call(this.model, items[key], key, items) - ) { + if (items.hasOwnProperty(key) && this.callFilter(items, key, inputs)) { results.push(items[key]); } } @@ -219,7 +248,7 @@ Filter.prototype.ref = function(from) { this.filters.fromMap[from] = this; this.idsSegments = ['$filters', from.replace(/\./g, '|')]; this.update(); - return this.model.refList(from, this.inputPath, this.idsSegments.join('.')); + return this.model.refList(from, this.path, this.idsSegments.join('.')); }; Filter.prototype.destroy = function() { diff --git a/lib/Model/fn.js b/lib/Model/fn.js index 4034785e6..a49057440 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -41,13 +41,11 @@ function parseStartArguments(model, args, hasPath) { if (hasPath) { path = model.path(args.shift()); } - var i = args.length - 1; var options; - if (model.isPath(args[i])) { - args[i] = model.path(args[i]); - } else { + if (!model.isPath(args[args.length - 1])) { options = args.pop(); } + var i = args.length; while (i--) { args[i] = model.path(args[i]); } diff --git a/lib/Model/unbundle.js b/lib/Model/unbundle.js index ce1f43c45..4582ea1f9 100644 --- a/lib/Model/unbundle.js +++ b/lib/Model/unbundle.js @@ -51,7 +51,7 @@ Model.prototype.unbundle = function(data) { // Re-create filters for (var i = 0; i < data.filters.length; i++) { var item = data.filters[i]; - var filter = this._filters.add(item[1], item[2], item[3], item[4]); + var filter = this._filters.add(item[1], item[2], item[3], item[4], item[5]); filter.ref(item[0]); } }; diff --git a/test/Model/filter.mocha.js b/test/Model/filter.mocha.js index ae3e23c82..851584a8b 100644 --- a/test/Model/filter.mocha.js +++ b/test/Model/filter.mocha.js @@ -141,5 +141,22 @@ describe('filter', function() { } ]); }); + it('supports additional dynamic inputs', function() { + var model = (new Model).at('_page'); + var numbers = [0, 3, 4, 1, 2, 3, 0]; + for (var i = 0; i < numbers.length; i++) { + model.set('numbers.' + model.id(), numbers[i]); + } + model.set('mod', 3); + model.set('offset', 0); + var filter = model.filter('numbers', 'mod', 'offset', function(number, id, numbers, mod, offset) { + return (number % mod) === offset; + }); + expect(filter.get()).to.eql([0, 3, 3, 0]); + model.set('offset', 1); + expect(filter.get()).to.eql([4, 1]); + model.set('mod', 2); + expect(filter.get()).to.eql([3, 1, 3]); + }); }); }); From 1ea554d8663c276eaae9ee5fd8544ef50e7bce74 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 30 Oct 2014 17:54:33 -0700 Subject: [PATCH 015/479] use slice for cloning an array instead of concat --- lib/Model/Query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 9b993bcd2..508980284 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -66,7 +66,7 @@ Model.prototype._initQueries = function(items) { // path before creating the query subscription. This feature should // probably be rethought. if (query.isPathQuery && expression.length > 0 && this._isLocal(expression[0])) { - this._setNull(expression, [].concat(ids)); + this._setNull(expression, ids.slice()); } if (extra !== void 0) { From 254f96780fd68d6813f3ead43ad82f6f7165e532 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 30 Oct 2014 18:23:01 -0700 Subject: [PATCH 016/479] fix argument parsing in filter --- lib/Model/filter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/filter.js b/lib/Model/filter.js index b0bb125a6..a1412565c 100644 --- a/lib/Model/filter.js +++ b/lib/Model/filter.js @@ -23,11 +23,11 @@ Model.INITS.push(function(model) { function parseFilterArguments(model, args) { var fn = args.pop(); - var path = model.path(args.shift()); var options; if (!model.isPath(args[args.length - 1])) { options = args.pop(); } + var path = model.path(args.shift()); var i = args.length; while (i--) { args[i] = model.path(args[i]); From 485f55b71dcf66c924bf93b26558d424efe46adf Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 30 Oct 2014 18:24:19 -0700 Subject: [PATCH 017/479] 0.6.0-alpha23 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dfe1cbe1e..e9758a3b5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha22", + "version": "0.6.0-alpha23", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From c04c9cba54db32e01d72080d01146cc28295d040 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 5 Nov 2014 20:27:43 -0800 Subject: [PATCH 018/479] remove dead code from old stringInsert and stringRemove events --- lib/Model/events.js | 2 -- lib/Model/ref.js | 12 ------------ lib/Model/refList.js | 38 ++------------------------------------ 3 files changed, 2 insertions(+), 50 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index c911e331e..6f40bd302 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -8,8 +8,6 @@ Model.MUTATOR_EVENTS = { , insert: true , remove: true , move: true -, stringInsert: true -, stringRemove: true , load: true , unload: true }; diff --git a/lib/Model/ref.js b/lib/Model/ref.js index ed25fa769..864703661 100644 --- a/lib/Model/ref.js +++ b/lib/Model/ref.js @@ -11,8 +11,6 @@ Model.INITS.push(function(model) { addListener(root, 'insert', refInsert); addListener(root, 'remove', refRemove); addListener(root, 'move', refMove); - addListener(root, 'stringInsert', refStringInsert); - addListener(root, 'stringRemove', refStringRemove); }); function addIndexListeners(model) { @@ -104,16 +102,6 @@ function refMove(model, dereferenced, eventArgs) { var howMany = eventArgs[2]; model._move(dereferenced, from, to, howMany); } -function refStringInsert(model, dereferenced, eventArgs) { - var index = eventArgs[0]; - var text = eventArgs[1]; - model._stringInsert(dereferenced, index, text); -} -function refStringRemove(model, dereferenced, eventArgs) { - var index = eventArgs[0]; - var howMany = eventArgs[1]; - model._stringRemove(dereferenced, index, howMany); -} function addListener(model, type, fn) { model.on(type, refListener); diff --git a/lib/Model/refList.js b/lib/Model/refList.js index 72ff6e194..302d9daaf 100644 --- a/lib/Model/refList.js +++ b/lib/Model/refList.js @@ -109,21 +109,6 @@ function patchFromEvent(type, segments, eventArgs, refList) { updateIdForValue(model, refList, index, value); return; } - // The same goes for string mutations, since strings are immutable - if (type === 'stringInsert') { - var stringIndex = eventArgs[0]; - var stringValue = eventArgs[1]; - model._stringInsert(toSegments, stringIndex, stringValue); - updateIdForValue(model, refList, index, value); - return; - } - if (type === 'stringRemove') { - var stringIndex = eventArgs[0]; - var howMany = eventArgs[1]; - model._stringRemove(toSegments, stringIndex, howMany); - updateIdForValue(model, refList, index, value); - return; - } if (type === 'insert' || type === 'remove' || type === 'move') { throw new Error('Array mutation on child of refList `from`' + 'should have been dereferenced: ' + segments.join('.')); @@ -270,25 +255,6 @@ function patchToEvent(type, segments, eventArgs, refList) { var indices = refList.indicesByItem(value); if (!indices) return; - // The same goes for string mutations, since strings are immutable - if (type === 'stringInsert') { - var stringIndex = eventArgs[0]; - var value = eventArgs[1]; - for (var i = 0; i < indices.length; i++) { - var outSegments = refList.fromSegments(indices[i]); - model._stringInsert(outSegments, stringIndex, value); - } - return; - } - if (type === 'stringRemove') { - var stringIndex = eventArgs[0]; - var howMany = eventArgs[1]; - for (var i = 0; i < indices.length; i++) { - var outSegments = refList.fromSegments(indices[i]); - model._stringRemove(outSegments, stringIndex, howMany); - } - return; - } if (type === 'insert' || type === 'remove' || type === 'move') { // Array mutations will have already been updated via an object // reference, so only re-emit @@ -420,9 +386,9 @@ function RefList(model, from, to, ids, options) { // keys on the to object. Ideally, this mapping could be customized via // inheriting from RefList and overriding these methods without having to // modify the above event handling code. -// +// // In the default refList implementation, `key` and `id` are equal. -// +// // Terms in the below methods: // `item` - Object on the `to` path, which gets mirrored on the `from` path // `key` - The property under `to` at which an item is located From 14da1b33d329033344a7f69f64508852a7d42ba3 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 5 Nov 2014 20:29:09 -0800 Subject: [PATCH 019/479] more isolated api for emitting stringInsert and stringRemove params --- lib/Model/RemoteDoc.js | 4 ++-- lib/Model/mutators.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 4cf9e626a..2ac8d9e84 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -371,7 +371,7 @@ RemoteDoc.prototype._onOp = function(op) { segments = segments.slice(0, -1); var value = model._get(segments); var previous = value.slice(0, index) + value.slice(index + text.length); - var pass = model.pass({$type: 'stringInsert', index: index, text: text})._pass; + var pass = model.pass({$stringInsert: {index: index, text: text}})._pass; model.emit('change', segments, [value, previous, pass]); // StringRemoveOp @@ -382,7 +382,7 @@ RemoteDoc.prototype._onOp = function(op) { segments = segments.slice(0, -1); var value = model._get(segments); var previous = value.slice(0, index) + text + value.slice(index); - var pass = model.pass({$type: 'stringRemove', index: index, howMany: howMany})._pass; + var pass = model.pass({$stringRemove: {index: index, howMany: howMany}})._pass; model.emit('change', segments, [value, previous, pass]); // IncrementOp diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index 1a1d904a5..036afca6c 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -522,7 +522,7 @@ Model.prototype._stringInsert = function(segments, index, text, cb) { function stringInsert(doc, docSegments, fnCb) { var previous = doc.stringInsert(docSegments, index, text, fnCb); var value = doc.get(docSegments); - var pass = model.pass({$type: 'stringInsert', index: index, text: text})._pass; + var pass = model.pass({$stringInsert: {index: index, text: text}})._pass; model.emit('change', segments, [value, previous, pass]); return; } @@ -561,7 +561,7 @@ Model.prototype._stringRemove = function(segments, index, howMany, cb) { function stringRemove(doc, docSegments, fnCb) { var previous = doc.stringRemove(docSegments, index, howMany, fnCb); var value = doc.get(docSegments); - var pass = model.pass({$type: 'stringRemove', index: index, howMany: howMany})._pass; + var pass = model.pass({$stringRemove: {index: index, howMany: howMany}})._pass; model.emit('change', segments, [value, previous, pass]); return; } From 11a93d33095fec18683d9c942b28d229aa006f91 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 5 Nov 2014 20:32:15 -0800 Subject: [PATCH 020/479] 0.6.0-alpha24 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e9758a3b5..965907a57 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha23", + "version": "0.6.0-alpha24", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 5c4c48c7c8ab00ddd2ed7948feb3bf3c1739b040 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 6 Nov 2014 17:02:39 -0800 Subject: [PATCH 021/479] simplify setDiff - remove options object with custom equal function - make setDiff like setNull where it is just a cheap equality check and won't ever traverse --- lib/Model/setDiff.js | 116 +++++++++++++++---------------------------- 1 file changed, 39 insertions(+), 77 deletions(-) diff --git a/lib/Model/setDiff.js b/lib/Model/setDiff.js index 06381b214..7448ba036 100644 --- a/lib/Model/setDiff.js +++ b/lib/Model/setDiff.js @@ -1,76 +1,62 @@ var util = require('../util'); var Model = require('./Model'); var arrayDiff = require('arraydiff'); -var deepEqual = util.deepEqual; Model.prototype.setDiff = function() { - var subpath, value, options, cb; + var subpath, value, cb; if (arguments.length === 1) { value = arguments[0]; } else if (arguments.length === 2) { subpath = arguments[0]; value = arguments[1]; - } else if (arguments.length === 3) { - subpath = arguments[0]; - value = arguments[1]; - if (typeof arguments[2] === 'function') { - cb = arguments[2]; - } else { - options = arguments[2]; - } } else { subpath = arguments[0]; value = arguments[1]; - options = arguments[2]; - cb = arguments[3]; + cb = arguments[2]; } var segments = this._splitPath(subpath); - return this._setDiff(segments, value, options, cb); + return this._setDiff(segments, value, cb); +}; +Model.prototype._setDiff = function(segments, value, cb) { + segments = this._dereference(segments); + var model = this; + function setDiff(doc, docSegments, fnCb) { + var previous = doc.get(docSegments); + if (util.equal(previous, value)) { + fnCb(); + return previous; + } + doc.set(docSegments, value, fnCb); + model.emit('change', segments, [value, previous, model._pass]); + return previous; + } + return this._mutate(segments, setDiff, cb); }; + Model.prototype.setDiffDeep = function() { - var subpath, value, options, cb; + var subpath, value, cb; if (arguments.length === 1) { value = arguments[0]; } else if (arguments.length === 2) { subpath = arguments[0]; value = arguments[1]; - } else if (arguments.length === 3) { - subpath = arguments[0]; - value = arguments[1]; - if (typeof arguments[2] === 'function') { - cb = arguments[2]; - } else { - options = arguments[2]; - } } else { subpath = arguments[0]; value = arguments[1]; - options = arguments[2]; - cb = arguments[3]; + cb = arguments[2]; } var segments = this._splitPath(subpath); - return this._setDiffDeep(segments, value, options, cb); + return this._setDiffDeep(segments, value, cb); }; -Model.prototype._setDiffDeep = function(segments, value, options, cb) { - if (options) { - options.equal = deepEqual; - } else { - options = {equal: deepEqual}; - } - return this._setDiff(segments, value, options, cb); -}; -Model.prototype._setDiff = function(segments, value, options, cb) { - var equalFn = (options && options.equal) || util.equal; +Model.prototype._setDiffDeep = function(segments, value, cb) { var before = this._get(segments); cb = this.wrapCallback(cb); - if (equalFn(before, value)) return cb(); - var group = util.asyncGroup(cb); var finished = group(); - doDiff(this, segments, before, value, equalFn, group); + diffDeep(this, segments, before, value, group); finished(); }; -function doDiff(model, segments, before, after, equalFn, group) { +function diffDeep(model, segments, before, after, group) { if (typeof before !== 'object' || !before || typeof after !== 'object' || !after) { // Set the entire value if not diffable @@ -78,7 +64,7 @@ function doDiff(model, segments, before, after, equalFn, group) { return; } if (Array.isArray(before) && Array.isArray(after)) { - var diff = arrayDiff(before, after, equalFn); + var diff = arrayDiff(before, after, util.deepEqual); if (!diff.length) return; // If the only change is a single item replacement, diff the item instead if ( @@ -91,7 +77,7 @@ function doDiff(model, segments, before, after, equalFn, group) { ) { var index = diff[0].index; var itemSegments = segments.concat(index); - doDiff(model, itemSegments, before[index], after[index], equalFn, group); + diffDeep(model, itemSegments, before[index], after[index], group); return; } model._applyArrayDiff(segments, diff, group()); @@ -107,77 +93,53 @@ function doDiff(model, segments, before, after, equalFn, group) { // Diff each property in after for (var key in after) { - if (equalFn(before[key], after[key])) continue; + if (util.deepEqual(before[key], after[key])) continue; var itemSegments = segments.concat(key); - doDiff(model, itemSegments, before[key], after[key], equalFn, group); + diffDeep(model, itemSegments, before[key], after[key], group); } } Model.prototype.setArrayDiff = function() { - var subpath, value, options, cb; + var subpath, value, cb; if (arguments.length === 1) { value = arguments[0]; } else if (arguments.length === 2) { subpath = arguments[0]; value = arguments[1]; - } else if (arguments.length === 3) { - subpath = arguments[0]; - value = arguments[1]; - if (typeof arguments[2] === 'function') { - cb = arguments[2]; - } else { - options = arguments[2]; - } } else { subpath = arguments[0]; value = arguments[1]; - options = arguments[2]; - cb = arguments[3]; + cb = arguments[2]; } var segments = this._splitPath(subpath); - return this._setArrayDiff(segments, value, options, cb); + return this._setArrayDiff(segments, value, cb); }; Model.prototype.setArrayDiffDeep = function() { - var subpath, value, options, cb; + var subpath, value, cb; if (arguments.length === 1) { value = arguments[0]; } else if (arguments.length === 2) { subpath = arguments[0]; value = arguments[1]; - } else if (arguments.length === 3) { - subpath = arguments[0]; - value = arguments[1]; - if (typeof arguments[2] === 'function') { - cb = arguments[2]; - } else { - options = arguments[2]; - } } else { subpath = arguments[0]; value = arguments[1]; - options = arguments[2]; - cb = arguments[3]; + cb = arguments[2]; } var segments = this._splitPath(subpath); - return this._setArrayDiffDeep(segments, value, options, cb); + return this._setArrayDiffDeep(segments, value, cb); }; -Model.prototype._setArrayDiffDeep = function(segments, value, options, cb) { - if (options) { - options.equal = deepEqual; - } else { - options = {equal: deepEqual}; - } - return this._setArrayDiff(segments, value, options, cb); +Model.prototype._setArrayDiffDeep = function(segments, value, cb) { + return this._setArrayDiff(segments, value, cb, util.deepEqual); }; -Model.prototype._setArrayDiff = function(segments, value, options, cb) { +Model.prototype._setArrayDiff = function(segments, value, cb, _equalFn) { var before = this._get(segments); if (before === value) return this.wrapCallback(cb)(); if (!Array.isArray(before) || !Array.isArray(value)) { this._set(segments, value, cb); return; } - var equalFn = options && options.equal; - var diff = arrayDiff(before, value, equalFn); + var diff = arrayDiff(before, value, _equalFn); this._applyArrayDiff(segments, diff, cb); }; Model.prototype._applyArrayDiff = function(segments, diff, cb) { From ae346dd0667b332914d0f8f575e4fe91d4b11756 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 6 Nov 2014 17:12:50 -0800 Subject: [PATCH 022/479] with updated setDiff, model.fn needed have set mode --- lib/Model/fn.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/Model/fn.js b/lib/Model/fn.js index a49057440..5bd18d1cf 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -155,7 +155,7 @@ function Fn(model, name, from, inputPaths, fns, options) { this.copyInput = (copy === 'input' || copy === 'both'); this.copyOutput = (copy === 'output' || copy === 'both'); - // Mode can be 'diffDeep', 'diff', 'arrayDeep', 'array', or 'set' + // Mode can be 'diffDeep', 'diff', 'arrayDeep', or 'array' this.mode = (options && options.mode) || 'diffDeep'; } @@ -197,13 +197,11 @@ Fn.prototype.onOutput = function(pass) { Fn.prototype._setValue = function(model, segments, value) { if (this.mode === 'diffDeep') { model._setDiffDeep(segments, value); - } else if (this.mode === 'diff') { - model._setDiff(segments, value); } else if (this.mode === 'arrayDeep') { model._setArrayDiffDeep(segments, value); } else if (this.mode === 'array') { model._setArrayDiff(segments, value); } else { - model._set(segments, value); + model._setDiff(segments, value); } }; From a4a3524d057680061212dd405b14771f235c08ff Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 6 Nov 2014 17:13:33 -0800 Subject: [PATCH 023/479] use arrayDiff for query ids now that setDiff is simpler --- lib/Model/Query.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 508980284..f58c7bbe4 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -218,7 +218,7 @@ Query.prototype.fetch = function(cb) { // Keep track of the ids at fetch time for use in unfetch query.fetchIds.push(ids.slice()); // Update the results ids and extra - model._setDiff(query.idsSegments, ids); + model._setArrayDiff(query.idsSegments, ids); if (extra !== void 0) { model._setDiffDeep(query.extraSegments, extra); } @@ -439,7 +439,7 @@ Query.prototype._onChange = function(ids, previousIds, cb) { // The results are updated via a different diff, since they might already // have a value from a fetch or previous shareQuery instance - this.model._setDiff(this.idsSegments, ids); + this.model._setArrayDiff(this.idsSegments, ids); var group, finished; if (cb) { From a50d4fc5230cfd3ff5d64abf11ed059e1d6baa30 Mon Sep 17 00:00:00 2001 From: kurtsergey Date: Thu, 20 Nov 2014 20:41:52 +0300 Subject: [PATCH 024/479] Update fn.js arguments order --- lib/Model/fn.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Model/fn.js b/lib/Model/fn.js index 5bd18d1cf..7e6da059d 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -125,8 +125,9 @@ Fns.prototype.toJSON = function() { // Don't try to bundle non-named functions that were started via // model.start directly instead of by name if (!fn.name) continue; - var args = [fn.from].concat(fn.inputPaths, fn.name); + var args = [fn.from].concat(fn.inputPaths); if (fn.options) args.push(fn.options); + args.push(fn.name); out.push(args); } return out; From a9c5ad699db76d3d66b72713c6b375958e3dcaaa Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 28 Jan 2015 16:09:53 -0800 Subject: [PATCH 025/479] fix Model::hasPending / Model::hasWritePending when deleting --- lib/Model/connection.js | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/lib/Model/connection.js b/lib/Model/connection.js index 6fc1f5438..4730fa51d 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -69,22 +69,22 @@ Model.prototype._getDocConstructor = function(name) { }; Model.prototype.hasPending = function() { - return !!this._firstPendingDoc(); + return !!this._firstShareDoc(hasPending); }; Model.prototype.hasWritePending = function() { - return !!this._firstWritePendingDoc(); + return !!this._firstShareDoc(hasWritePending); }; Model.prototype.whenNothingPending = function(cb) { - var doc = this._firstPendingDoc(); - if (doc) { + var shareDoc = this._firstShareDoc(hasPending); + if (shareDoc) { // If a document is found with a pending operation, wait for it to emit // that nothing is pending anymore, and then recheck all documents again. // We have to recheck all documents, just in case another mutation has // been made in the meantime as a result of an event callback var model = this; - doc.shareDoc.once('nothing pending', function retryNothingPending() { + shareDoc.once('nothing pending', function retryNothingPending() { process.nextTick(function(){ model.whenNothingPending(cb); }); @@ -95,13 +95,6 @@ Model.prototype.whenNothingPending = function(cb) { process.nextTick(cb); }; -Model.prototype._firstPendingDoc = function() { - return this._firstShareDoc(hasPending); -}; -Model.prototype._firstWritePendingDoc = function() { - return this._firstShareDoc(hasWritePending); -}; - function hasPending(shareDoc) { return shareDoc.hasPending(); } @@ -110,15 +103,15 @@ function hasWritePending(shareDoc) { } Model.prototype._firstShareDoc = function(fn) { - // Loop through all of this model's documents, and return the first document - // encountered with that matches the provided test function - var collections = this.root.collections; + // Loop through all of the documents on the share connection, and return the + // first document encountered with that matches the provided test function + var collections = this.root.shareConnection.collections; for (var collectionName in collections) { var collection = collections[collectionName]; - for (var id in collection.docs) { - var doc = collection.docs[id]; - if (doc.shareDoc && fn(doc.shareDoc)) { - return doc; + for (var id in collection) { + var shareDoc = collection[id]; + if (shareDoc && fn(shareDoc)) { + return shareDoc; } } } From a4804c28508415e60ce1eb81c4f88b51015f6590 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 28 Jan 2015 16:12:09 -0800 Subject: [PATCH 026/479] 0.6.0-alpha25 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 965907a57..8c67b4299 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha24", + "version": "0.6.0-alpha25", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From e9923f0ab8b99dc7e771b1e30697fa88f50b9d66 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 28 Jan 2015 16:22:40 -0800 Subject: [PATCH 027/479] Add back in line to clean up sharejs docs, hoping the race condition was fixed --- lib/Model/subscriptions.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index ab3beb4f5..1ea801f80 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -240,9 +240,8 @@ Model.prototype._maybeUnloadDoc = function(collectionName, id, path) { var previous = doc.get(); this.root.collections[collectionName].remove(id); - // TODO: There is a bug in ShareJS where a race condition between subscribe - // and destroying the document data. For now, not cleaning up ShareJS docs - // if (doc.shareDoc) doc.shareDoc.destroy(); + // Remove doc from memory in Share as well + if (doc.shareDoc) doc.shareDoc.destroy(); delete this.root._loadVersions[path]; this.emit('unload', [collectionName, id], [previous, this._pass]); From 6139c487d2821c0898318fb85490f1aa1524b453 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 28 Jan 2015 16:23:25 -0800 Subject: [PATCH 028/479] 0.6.0-alpha26 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8c67b4299..2bd9e1a0e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha25", + "version": "0.6.0-alpha26", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From fa317e0f8f74cb4edf8c307f84c0048a8791d6f6 Mon Sep 17 00:00:00 2001 From: Raine Date: Thu, 29 Jan 2015 08:02:06 -0700 Subject: [PATCH 029/479] README: Note that examples are only in <= v0.5.14 The instructions as given for viewing the examples didn't work for the current alpha version. This should be noted so that newcomers (like me) aren't confused. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8eb0ba231..ec2f13a41 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,10 @@ Racer requires [Node v0.10](http://nodejs.org/). You will also need to have a [M $ npm install racer ``` -The examples can then be run by: +The examples (<= v0.5.14) can then be run by: ``` +$ npm install racer@0.5.14 $ cd node_modules/racer/examples/pad $ npm install $ node server.js From 98caeaebbbf9350e61e258073508fe2b30f4137b Mon Sep 17 00:00:00 2001 From: Dag Date: Thu, 29 Jan 2015 18:16:04 +0100 Subject: [PATCH 030/479] Fix links to Travis CI --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 8eb0ba231..78c591e31 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ Racer is a realtime model synchronization engine for Node.js. By leveraging [ShareJS](http://sharejs.org/), multiple users can interact with the same data in realtime via Operational Transformation, a sophisticated conflict resolution algorithm that works in realtime and with offline clients. ShareJS also supports PubSub across multiple servers for horizontal scaling. Clients can express data subscriptions and fetches in terms of queries and specific documents, so different clients can be subscribed to different overlapping sets of data. On top of this sophisticated backend, Racer provides a simple model and event interface for writing application logic. -[![Build -Status](https://secure.travis-ci.org/codeparty/racer.png?branch=0.5)](https://travis-ci.org/codeparty/racer/branches) +[![Build Status](https://travis-ci.org/derbyjs/racer.svg)](https://travis-ci.org/derbyjs/racer) ## Disclaimer From 5e252abe394429dfa70cb774294879722b501af5 Mon Sep 17 00:00:00 2001 From: Raine Date: Fri, 30 Jan 2015 07:05:09 -0700 Subject: [PATCH 031/479] Direct users to racer-examples repo Clarify use of racer-examples --- README.md | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index ec2f13a41..3847a7f1e 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,10 @@ If you are interested in contributing, please reach out to [Nate](https://github ## Demos -There are currently two demos, which are included under the examples directory. See [Installation](#installation) below. - -### Pad - - - -A very simple collaborative [text editor](https://github.com/codeparty/racer-examples/tree/master/pad). - -### Todos - - - -Classic [todo list](https://github.com/codeparty/racer-examples/tree/master/todos) demonstrating the use of Racer's model methods. +There are currently two demos, which are included in the [racer-examples](https://github.com/derbyjs/racer-examples) repo. + * [Pad](https://github.com/derbyjs/racer-examples/tree/master/pad) – A very simple collaborative text editor. + * [Todos](https://github.com/derbyjs/racer-examples/tree/master/todos) – Classic todo list demonstrating the use of Racer's model methods. ## Features @@ -64,24 +54,6 @@ Racer requires [Node v0.10](http://nodejs.org/). You will also need to have a [M $ npm install racer ``` -The examples (<= v0.5.14) can then be run by: - -``` -$ npm install racer@0.5.14 -$ cd node_modules/racer/examples/pad -$ npm install -$ node server.js -``` - -and - -``` -$ cd node_modules/racer/examples/todos -$ npm install -$ npm install -g coffee-script -$ coffee server.coffee -``` - ## Tests Run the tests with From 209b91e5846c60af45d8539e84ce537c5ee952c3 Mon Sep 17 00:00:00 2001 From: rachael Date: Mon, 16 Feb 2015 23:16:23 -0800 Subject: [PATCH 032/479] add disableSubmit and remoteMutations debugging options --- lib/Model/Model.js | 1 + lib/Model/RemoteDoc.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/Model/Model.js b/lib/Model/Model.js index 575e58a1e..79f1f7385 100644 --- a/lib/Model/Model.js +++ b/lib/Model/Model.js @@ -9,6 +9,7 @@ function Model(options) { var inits = Model.INITS; options || (options = {}); + this.debug = options.debug || {}; for (var i = 0; i < inits.length; i++) { inits[i](this, options); } diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 2ac8d9e84..452526725 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -14,6 +14,10 @@ module.exports = RemoteDoc; function RemoteDoc(model, collectionName, id, data) { Doc.call(this, model, collectionName, id); var shareDoc = this.shareDoc = model._getOrCreateShareDoc(collectionName, id, data); + if (model.root.debug.disableSubmit) { + shareDoc.submitOp = function() {}; + } + this.debugMutations = model.root.debug.remoteMutations; this.createdLocally = false; this.model = model = model.pass({$remote: true}); this._updateCollectionData(); @@ -61,6 +65,9 @@ RemoteDoc.prototype._updateCollectionData = function() { }; RemoteDoc.prototype.set = function(segments, value, cb) { + if (this.debugMutations) { + console.log('RemoteDoc set', this.path(segments), value); + } if (segments.length === 0 && !this.shareDoc.type) { // We copy the snapshot at time of create to prevent the id added outside // of ShareJS from getting stored in the data @@ -89,6 +96,9 @@ RemoteDoc.prototype.set = function(segments, value, cb) { }; RemoteDoc.prototype.del = function(segments, cb) { + if (this.debugMutations) { + console.log('RemoteDoc del', this.path(segments)); + } if (segments.length === 0) { var previous = this.get(); this.shareDoc.del(cb); @@ -109,6 +119,9 @@ RemoteDoc.prototype.del = function(segments, cb) { }; RemoteDoc.prototype.increment = function(segments, byNumber, cb) { + if (this.debugMutations) { + console.log('RemoteDoc increment', this.path(segments), byNumber); + } var previous = this._createImplied(segments); if (previous instanceof ImpliedOp) { var lastSegment = segments[segments.length - 1]; @@ -133,6 +146,9 @@ RemoteDoc.prototype.increment = function(segments, byNumber, cb) { }; RemoteDoc.prototype.push = function(segments, value, cb) { + if (this.debugMutations) { + console.log('RemoteDoc push', this.path(segments), value); + } var shareDoc = this.shareDoc; function push(arr, fnCb) { var op = [new ListInsertOp(segments, arr.length, value)]; @@ -143,6 +159,9 @@ RemoteDoc.prototype.push = function(segments, value, cb) { }; RemoteDoc.prototype.unshift = function(segments, value, cb) { + if (this.debugMutations) { + console.log('RemoteDoc unshift', this.path(segments), value); + } var shareDoc = this.shareDoc; function unshift(arr, fnCb) { var op = [new ListInsertOp(segments, 0, value)]; @@ -153,6 +172,9 @@ RemoteDoc.prototype.unshift = function(segments, value, cb) { }; RemoteDoc.prototype.insert = function(segments, index, values, cb) { + if (this.debugMutations) { + console.log('RemoteDoc insert', this.path(segments), index, values); + } var shareDoc = this.shareDoc; function insert(arr, fnCb) { var op = createInsertOp(segments, index, values); @@ -174,6 +196,9 @@ function createInsertOp(segments, index, values) { } RemoteDoc.prototype.pop = function(segments, cb) { + if (this.debugMutations) { + console.log('RemoteDoc pop', this.path(segments)); + } var shareDoc = this.shareDoc; function pop(arr, fnCb) { var index = arr.length - 1; @@ -186,6 +211,9 @@ RemoteDoc.prototype.pop = function(segments, cb) { }; RemoteDoc.prototype.shift = function(segments, cb) { + if (this.debugMutations) { + console.log('RemoteDoc shift', this.path(segments)); + } var shareDoc = this.shareDoc; function shift(arr, fnCb) { var value = arr[0]; @@ -197,6 +225,9 @@ RemoteDoc.prototype.shift = function(segments, cb) { }; RemoteDoc.prototype.remove = function(segments, index, howMany, cb) { + if (this.debugMutations) { + console.log('RemoteDoc remove', this.path(segments), index, howMany); + } var shareDoc = this.shareDoc; function remove(arr, fnCb) { var values = arr.slice(index, index + howMany); @@ -211,6 +242,9 @@ RemoteDoc.prototype.remove = function(segments, index, howMany, cb) { }; RemoteDoc.prototype.move = function(segments, from, to, howMany, cb) { + if (this.debugMutations) { + console.log('RemoteDoc move', this.path(segments), from, to, howMany); + } var shareDoc = this.shareDoc; function move(arr, fnCb) { // Get the return value @@ -229,6 +263,9 @@ RemoteDoc.prototype.move = function(segments, from, to, howMany, cb) { }; RemoteDoc.prototype.stringInsert = function(segments, index, value, cb) { + if (this.debugMutations) { + console.log('RemoteDoc stringInsert', this.path(segments), index, value); + } var previous = this._createImplied(segments); if (previous instanceof ImpliedOp) { var lastSegment = segments[segments.length - 1]; @@ -253,6 +290,9 @@ RemoteDoc.prototype.stringInsert = function(segments, index, value, cb) { }; RemoteDoc.prototype.stringRemove = function(segments, index, howMany, cb) { + if (this.debugMutations) { + console.log('RemoteDoc stringRemove', this.path(segments), index, howMany); + } var previous = this._createImplied(segments); if (previous instanceof ImpliedOp) return; if (previous == null) return previous; From 0a5a511a986c544e4afc853d1b10b88176561a17 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 16 Feb 2015 23:23:44 -0800 Subject: [PATCH 033/479] 0.6.0-alpha27 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2bd9e1a0e..d3766ca34 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha26", + "version": "0.6.0-alpha27", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 35d926f2e4091c1054b539f45a92556a6e2c01e5 Mon Sep 17 00:00:00 2001 From: Ian Johnson Date: Fri, 20 Feb 2015 18:12:41 -0800 Subject: [PATCH 034/479] update README with link to new derby docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4498f7b5c..1da1aeb8f 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ $ npm test Racer can be used independently as shown in the examples, but Racer and Derby are designed to work especially well together. Racer can also be used along with other MVC frameworks, such as Angular. -For now, Racer is mostly documented along with Derby. See the Derby [model docs](http://derbyjs.com/#models). +For now, Racer is mostly documented along with Derby. See the Derby [model docs](http://derbyjs.com/docs/derby-0.6/models). ### MIT License Copyright (c) 2011 by Brian Noguchi and Nate Smith From 52ea7f8c5df79b41f835dfcb78f6383446bb4618 Mon Sep 17 00:00:00 2001 From: Randal Truong Date: Tue, 24 Feb 2015 14:29:25 -0800 Subject: [PATCH 035/479] Tie to latest stable version of share --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d3766ca34..0662e72b1 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "arraydiff": "^0.1.1", "deep-is": "^0.1.3", "uuid": "^2.0.1", - "share": "^0.7.3" + "share": "0.7.5" }, "devDependencies": { "expect.js": "~0.3.1", From 629ffa8b35a9f81caa880ec64f2ff484cf3363d2 Mon Sep 17 00:00:00 2001 From: Randal Truong Date: Tue, 24 Feb 2015 14:31:38 -0800 Subject: [PATCH 036/479] Upgrade to 0.6.0-alpha28 to tie to stable version of share --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0662e72b1..062ee5d71 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha27", + "version": "0.6.0-alpha28", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 37498a4b705df6b7382896f4c6b83c134505e4f2 Mon Sep 17 00:00:00 2001 From: Randal Truong Date: Tue, 24 Feb 2015 15:29:31 -0800 Subject: [PATCH 037/479] Revert "Tie to latest stable version of share" This reverts commit 52ea7f8c5df79b41f835dfcb78f6383446bb4618. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 062ee5d71..451e77b03 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "arraydiff": "^0.1.1", "deep-is": "^0.1.3", "uuid": "^2.0.1", - "share": "0.7.5" + "share": "^0.7.3" }, "devDependencies": { "expect.js": "~0.3.1", From 422ab8117eb1d3061fc76b808510bcd621270911 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 4 Mar 2015 01:00:52 -0800 Subject: [PATCH 038/479] null out model / req.getModel if request is closed prematurely this is important to avoid memory leak potential, but means that getModel will return undefined if a req was already closed --- lib/Store.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Store.js b/lib/Store.js index 49eff940d..c65d61144 100644 --- a/lib/Store.js +++ b/lib/Store.js @@ -57,9 +57,11 @@ Store.prototype.modelMiddleware = function() { req.getModel = getModel; function closeModel() { + req.getModel = getModelUndefined; res.removeListener('finish', closeModel); res.removeListener('close', closeModel); model && model.close(); + model = null; } res.on('finish', closeModel); res.on('close', closeModel); @@ -69,6 +71,8 @@ Store.prototype.modelMiddleware = function() { return modelMiddleware; }; +function getModelUndefined() {} + function ClientSocket(client) { this.client = client; var socket = this; From 29c362430bdfdec3ed5477af7a23b3ace8af904a Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 4 Mar 2015 01:01:02 -0800 Subject: [PATCH 039/479] 0.6.0-alpha29 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 451e77b03..fc7e2977b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha28", + "version": "0.6.0-alpha29", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 000a4798592195f8c3ee2a03c24e07cab499b24d Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 4 Mar 2015 04:28:14 -0800 Subject: [PATCH 040/479] emit share agents when created by store --- lib/Store.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Store.js b/lib/Store.js index c65d61144..66255e666 100644 --- a/lib/Store.js +++ b/lib/Store.js @@ -39,7 +39,8 @@ Store.prototype.createModel = function(options, req) { this.emit('modelStream', stream); model.createConnection(stream, this.logger); - this.shareClient.listen(stream, req); + var agent = this.shareClient.listen(stream, req); + this.emit('shareAgent', agent); return model; }; From ce245bd764c04f7208b59656d9524b91a8d7d55c Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 4 Mar 2015 04:29:05 -0800 Subject: [PATCH 041/479] 0.6.0-alpha30 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc7e2977b..494618cea 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha29", + "version": "0.6.0-alpha30", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 15fb92f2bc3d20cda453d34567f65e9b1307aabd Mon Sep 17 00:00:00 2001 From: Alexander Wenzowski Date: Thu, 5 Mar 2015 16:31:14 -0500 Subject: [PATCH 042/479] there are global leaks; check for them --- test/mocha.opts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/mocha.opts b/test/mocha.opts index df349bb0a..ec2f95b66 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,4 @@ --reporter spec --timeout 1200 --bail +--check-leaks From 30994c84fc5345bf0df591ffe12c7fff82c4909b Mon Sep 17 00:00:00 2001 From: Alexander Wenzowski Date: Thu, 5 Mar 2015 16:37:24 -0500 Subject: [PATCH 043/479] add missing var statements --- test/Model/filter.mocha.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Model/filter.mocha.js b/test/Model/filter.mocha.js index 851584a8b..6cd9f46a4 100644 --- a/test/Model/filter.mocha.js +++ b/test/Model/filter.mocha.js @@ -30,7 +30,7 @@ describe('filter', function() { for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); } - filter = model.sort('numbers', 'asc'); + var filter = model.sort('numbers', 'asc'); expect(filter.get()).to.eql([0, 0, 1, 2, 3, 3, 4]); filter = model.sort('numbers', 'desc'); expect(filter.get()).to.eql([4, 3, 3, 2, 1, 0, 0]); @@ -67,7 +67,7 @@ describe('filter', function() { for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); } - filter = model.sort('numbers', 'asc'); + var filter = model.sort('numbers', 'asc'); expect(filter.get()).to.eql([0, 0, 1, 2, 3, 3, 4]); filter = model.sort('numbers', 'desc'); filter.ref('_page.out'); From 82fa8327e35900101a58beb8f3ed925515ea85e2 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 20 Mar 2015 19:43:05 -0700 Subject: [PATCH 044/479] When calling model.subscribe with identical queries prior to data being returned, make sure all the callbacks are called. --- lib/Model/Query.js | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index f58c7bbe4..211fc2edf 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -249,8 +249,11 @@ Query.prototype.subscribe = function(cb) { if (this.subscribeCount++) { process.nextTick(function() { var data = query.model._get(query.segments); - if (data) cb(); - else query._pendingSubscribeCallbacks.push(cb); + if (data) { + cb(); + } else { + query._pendingSubscribeCallbacks.push(cb); + } }); return this; } @@ -273,15 +276,19 @@ Query.prototype.subscribe = function(cb) { options.docMode = 'fetch'; model.root.shareConnection.createFetchQuery( this.collectionName, this.sourceQuery(), options, function(err, results, extra) { - if (err) return cb(err); + if (err) { + cb(err); + query._flushPendingCallbacks(err); + return; + } var ids = resultsIds(results); if (extra !== void 0) { model._setDiffDeep(query.extraSegments, extra); } - query._onChange(ids, null, cb); - while (cb = query._pendingSubscribeCallbacks.shift()) { - query._onChange(ids, null, cb); - } + query._onChange(ids, null, function(err) { + cb(err); + query._flushPendingCallbacks(err); + }); } ); return this; @@ -300,13 +307,18 @@ Query.prototype._shareSubscribe = function(options, cb) { var model = this.model; this.shareQuery = this.model.root.shareConnection.createSubscribeQuery( this.collectionName, this.sourceQuery(), options, function(err, results, extra) { - if (err) return cb(err); + if (err) { + cb(err); + query._flushPendingCallbacks(); + return; + } if (extra !== void 0) { model._setDiffDeep(query.extraSegments, extra); } // Results are not set in the callback, because the shareQuery should // emit a 'change' event before calling back cb(); + query._flushPendingCallbacks(); } ); var query = this; @@ -330,6 +342,19 @@ Query.prototype._shareSubscribe = function(options, cb) { }); }; +/** + * Flushes `_pendingSubscribeCallbacks`, calling each callback in the array, + * with an optional error to pass into each. `_pendingSubscribeCallbacks` will + * be empty after this runs. + * @private + */ +Query.prototype._flushPendingCallbacks = function(err) { + var pendingCallback; + while (pendingCallback = this._pendingSubscribeCallbacks.shift()) { + pendingCallback(err); + } +}; + /** * @public * @param {Function} cb(err, newFetchCount) From e60d4ca46740fa8bc115f219f81e5adbd4f3d10a Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 20 Mar 2015 20:02:54 -0700 Subject: [PATCH 045/479] 0.6.0-alpha31 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 494618cea..15d86d60c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha30", + "version": "0.6.0-alpha31", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 8deb13084667c8a00b72b84b38ba92ff852aa34f Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 27 Mar 2015 15:52:08 -0700 Subject: [PATCH 046/479] support more useful logging of server stream data --- lib/Model/connection.server.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/Model/connection.server.js b/lib/Model/connection.server.js index b6034a3f7..880835dae 100644 --- a/lib/Model/connection.server.js +++ b/lib/Model/connection.server.js @@ -2,7 +2,7 @@ var share = require('share'); var Model = require('./Model'); Model.prototype.createConnection = function(stream, logger) { - var socket = new StreamSocket(stream, logger); + var socket = new StreamSocket(this, stream, logger); this.root.socket = socket; this.root.shareConnection = new share.client.Connection(socket); socket.onopen(); @@ -14,7 +14,8 @@ Model.prototype.createConnection = function(stream, logger) { * Wrapper to make a stream look like a BrowserChannel socket * @param {Stream} stream */ -function StreamSocket(stream, logger) { +function StreamSocket(model, stream, logger) { + this.model = model; this.stream = stream; this.logger = logger; var socket = this; @@ -24,14 +25,20 @@ function StreamSocket(stream, logger) { type: 'message', data: chunk }); - if (logger) logger.write(chunk); + if (logger) { + var src = model.shareConnection && model.shareConnection.id; + logger.write({type: 'S->C', chunk: chunk, src: src}); + } callback(); }; } StreamSocket.prototype.send = function(data) { var copy = JSON.parse(JSON.stringify(data)); this.stream.push(copy); - if (this.logger) this.logger.write(copy); + var src = this.model.shareConnection && this.model.shareConnection.id; + if (this.logger) { + this.logger.write({type: 'C->S', chunk: copy, src: src}); + } }; StreamSocket.prototype.close = function() { this.stream.end(); From a97e7d1a288e4ad902020aa35a139c3722d12103 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 27 Mar 2015 15:52:20 -0700 Subject: [PATCH 047/479] 0.6.0-alpha32 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 15d86d60c..90a4b9173 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha31", + "version": "0.6.0-alpha32", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From b0261be625b409d54dc4b176bcd5727cc5f44afe Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 7 May 2015 19:52:06 -0700 Subject: [PATCH 048/479] separate query & doc subscriptions & fetches --- lib/Model/CollectionCounter.js | 27 +++ lib/Model/Query.js | 323 ++++++++++++--------------------- lib/Model/contexts.js | 47 +++-- lib/Model/subscriptions.js | 172 ++++++++---------- lib/Model/unbundle.js | 9 +- 5 files changed, 247 insertions(+), 331 deletions(-) create mode 100644 lib/Model/CollectionCounter.js diff --git a/lib/Model/CollectionCounter.js b/lib/Model/CollectionCounter.js new file mode 100644 index 000000000..f1ff357e9 --- /dev/null +++ b/lib/Model/CollectionCounter.js @@ -0,0 +1,27 @@ +module.exports = CollectionCounter; + +function CollectionCounter() { + this.collections = {}; +} +CollectionCounter.prototype.get = function(collectionName, id) { + var collection = this.collections[collectionName]; + return collection && collection[id]; +}; +CollectionCounter.prototype.increment = function(collectionName, id) { + var collection = this.collections[collectionName] || + (this.collections[collectionName] = {}); + return collection[id] = (collection[id] || 0) + 1; +}; +CollectionCounter.prototype.decrement = function(collectionName, id) { + var collection = this.collections[collectionName]; + var count = collection && collection[id]; + if (count == null) return; + if (count > 1) { + return collection[id] = count - 1; + } + delete collection[id]; + // Check if the collection still has any keys + for (var key in collection) return 0; + delete this.collections[collection]; + return 0; +}; diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 211fc2edf..3785cda51 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -8,24 +8,19 @@ Model.INITS.push(function(model) { model.root._queries = new Queries(); if (model.root.fetchOnly) return; model.on('all', function(segments) { - var map = model.root._queries.map; + var collectionName = segments[0]; + var map = model.root._queries.collections[collectionName]; + if (!map) return; for (var hash in map) { var query = map[hash]; if (query.isPathQuery && query.shareQuery && util.mayImpact(query.expression, segments)) { var ids = pathIds(model, query.expression); - var previousIds = model._get(query.idsSegments); - query._onChange(ids, previousIds); + query._setResultIds(ids); } } }); }); -/** - * @param {String} collectionName - * @param {Object} expression - * @param {String} source - * @return {Query} - */ Model.prototype.query = function(collectionName, expression, source) { if (typeof expression.path === 'function' || typeof expression !== 'object') { expression = this._splitPath(expression); @@ -37,9 +32,7 @@ Model.prototype.query = function(collectionName, expression, source) { return query; }; -/** - * Called during initialization of the bundle on page load. - */ +// Called during initialization of the bundle on page load. Model.prototype._initQueries = function(items) { var queries = this.root._queries; for (var i = 0; i < items.length; i++) { @@ -69,9 +62,7 @@ Model.prototype._initQueries = function(items) { this._setNull(expression, ids.slice()); } - if (extra !== void 0) { - this._set(query.extraSegments, extra); - } + query._setExtra(extra); for (var j = 0; j < snapshots.length; j++) { var snapshot = snapshots[j]; @@ -80,7 +71,6 @@ Model.prototype._initQueries = function(items) { var version = versions[j]; var data = {data: snapshot, v: version, type: 'json0'}; this.getOrCreateDoc(collectionName, id, data); - this._loadVersions[collectionName + '.' + id] = version; } for (var j = 0; j < counts.length; j++) { @@ -94,27 +84,32 @@ Model.prototype._initQueries = function(items) { } query.fetchCount += fetched; while (fetched--) { - query.fetchIds.push(ids); query.model._context.fetchQuery(query); - var alreadyLoaded = true; - for (var k = 0; k < ids.length; k++) { - query.model.fetchDoc(collectionName, ids[k], null, alreadyLoaded); - } } } } }; -function QueriesMap() {} - function Queries() { - this.map = new QueriesMap(); + // Map is a flattened map of queries by hash. Currently used in contexts + this.map = {}; + // Collections is a nested map of queries by collection then hash + this.collections = {}; } Queries.prototype.add = function(query) { this.map[query.hash] = query; + var collection = this.collections[query.collectionName] || + (this.collections[query.collectionName] = {}); + collection[query.hash] = query; }; Queries.prototype.remove = function(query) { delete this.map[query.hash]; + var collection = this.collections[query.collectionName]; + if (!collection) return; + delete collection[query.hash]; + // Check if the collection still has any keys + for (var key in collection) return; + delete this.collections[collection]; }; Queries.prototype.get = function(collectionName, expression, source) { var hash = queryHash(collectionName, expression, source); @@ -131,17 +126,6 @@ Queries.prototype.toJSON = function() { return out; }; -/** - * @private - * @constructor - * @param {Model} model - * @param {Object} collectionName - * @param {Object} expression - * @param {String} source (e.g., 'solr') - * @param {Number} subscribeCount - * @param {Number} fetchCount - * @param {Array>} fetchIds - */ function Query(model, collectionName, expression, source) { this.model = model.pass({$query: this}); this.collectionName = collectionName; @@ -160,9 +144,6 @@ function Query(model, collectionName, expression, source) { // are called the same number of times that fetch and subscribe were called. this.subscribeCount = 0; this.fetchCount = 0; - // The list of ids at the time of each fetch is pushed onto fetchIds, so - // that unfetchDoc can be called the same number of times as fetchDoc - this.fetchIds = []; this.created = false; this.shareQuery = null; @@ -181,6 +162,7 @@ Query.prototype.destroy = function() { } this.model.root._queries.remove(this); this.model._del(this.segments); + this._maybeUnloadDocs(); }; Query.prototype.sourceQuery = function() { @@ -191,9 +173,6 @@ Query.prototype.sourceQuery = function() { return this.expression; }; -/** - * @param {Function} [cb] cb(err) - */ Query.prototype.fetch = function(cb) { cb = this.model.wrapCallback(cb); this.model._context.fetchQuery(this); @@ -201,52 +180,33 @@ Query.prototype.fetch = function(cb) { this.fetchCount++; if (!this.created) this.create(); - var query = this; - var model = this.model; var shareDocs = collectionShareDocs(this.model, this.collectionName); var options = {docMode: 'fetch', knownDocs: shareDocs}; if (this.source) options.source = this.source; - model.root.shareConnection.createFetchQuery( - this.collectionName, this.sourceQuery(), options, fetchQueryCallback - ); - function fetchQueryCallback(err, results, extra) { + var query = this; + function fetchCb(err, results, extra) { if (err) return cb(err); - var ids = resultsIds(results); - - // Keep track of the ids at fetch time for use in unfetch - query.fetchIds.push(ids.slice()); - // Update the results ids and extra - model._setArrayDiff(query.idsSegments, ids); - if (extra !== void 0) { - model._setDiffDeep(query.extraSegments, extra); - } - - // Call fetchDoc for each document returned so that the proper load events - // and internal counts are maintained. However, specify that we already - // loaded the documents as part of the query, since we don't want to - // actually fetch the documents again - var alreadyLoaded = true; - for (var i = 0; i < ids.length; i++) { - model.fetchDoc(query.collectionName, ids[i], null, alreadyLoaded); - } + query._setExtra(extra); + query._setResults(results); cb(); } + this.model.root.shareConnection.createFetchQuery( + this.collectionName, + this.sourceQuery(), + options, + fetchCb + ); return this; }; -/** - * Sets up a subscription to `this` query. - * @param {Function} cb(err) - */ Query.prototype.subscribe = function(cb) { cb = this.model.wrapCallback(cb); this.model._context.subscribeQuery(this); - var query = this; - if (this.subscribeCount++) { + var query = this; process.nextTick(function() { var data = query.model._get(query.segments); if (data) { @@ -267,98 +227,117 @@ Query.prototype.subscribe = function(cb) { var options = {docMode: 'sub', knownDocs: shareDocs}; if (this.source) options.source = this.source; - if (!this.model.root.fetchOnly) { + if (this.model.root.fetchOnly) { + this._shareFetchedSubscribe(options, cb); + } else { this._shareSubscribe(options, cb); - return this; } - var model = this.model; + return this; +}; + +Query.prototype._shareFetchedSubscribe = function(options, cb) { + var query = this; + function fetchedSubscribeCb(err, results, extra) { + if (err) return query._flushSubscribeCallbacks(cb, err); + query._setExtra(extra); + query._setResults(results); + query._flushSubscribeCallbacks(cb); + } options.docMode = 'fetch'; - model.root.shareConnection.createFetchQuery( - this.collectionName, this.sourceQuery(), options, function(err, results, extra) { - if (err) { - cb(err); - query._flushPendingCallbacks(err); - return; - } - var ids = resultsIds(results); - if (extra !== void 0) { - model._setDiffDeep(query.extraSegments, extra); - } - query._onChange(ids, null, function(err) { - cb(err); - query._flushPendingCallbacks(err); - }); - } + this.model.root.shareConnection.createFetchQuery( + this.collectionName, + this.sourceQuery(), + options, + fetchedSubscribeCb ); - return this; }; -/** - * @private - * @param {Object} options - * @param {String} [options.source] - * @param {Boolean} [options.poll] - * @param {Boolean} [options.docMode = fetch or subscribe] - * @param {Function} cb(err, results) - */ Query.prototype._shareSubscribe = function(options, cb) { var query = this; - var model = this.model; + function subscribeCb(err, results, extra) { + if (err) return query._flushSubscribeCallbacks(cb, err); + query._setExtra(extra); + // Results are not set, since a change event will already + // have been emitted with the same results + query._flushSubscribeCallbacks(cb); + } + if (this.shareQuery) { + console.log('OLD SHARE QUERY', this.shareQuery); + this.shareQuery.destroy(); + } this.shareQuery = this.model.root.shareConnection.createSubscribeQuery( - this.collectionName, this.sourceQuery(), options, function(err, results, extra) { - if (err) { - cb(err); - query._flushPendingCallbacks(); - return; - } - if (extra !== void 0) { - model._setDiffDeep(query.extraSegments, extra); - } - // Results are not set in the callback, because the shareQuery should - // emit a 'change' event before calling back - cb(); - query._flushPendingCallbacks(); - } + this.collectionName, + this.sourceQuery(), + options, + subscribeCb ); - var query = this; this.shareQuery.on('insert', function(shareDocs, index) { - query._onInsert(shareDocs, index); + var ids = resultsIds(shareDocs); + query._registerDocs(ids); + query.model._insert(query.idsSegments, index, ids); }); this.shareQuery.on('remove', function(shareDocs, index) { - query._onRemove(shareDocs, index); + query.model._remove(query.idsSegments, index, shareDocs.length); + query._maybeUnloadDocs(); }); this.shareQuery.on('move', function(shareDocs, from, to) { - query._onMove(shareDocs, from, to); + query.model._move(query.idsSegments, from, to, shareDocs.length); }); - this.shareQuery.on('change', function(results, previous) { - // Get the new and previous list of ids when the entire results set changes - var ids = resultsIds(results); - var previousIds = previous && resultsIds(previous); - query._onChange(ids, previousIds); + this.shareQuery.on('change', function(shareDocs) { + query._setResults(shareDocs); }); this.shareQuery.on('extra', function(extra) { - model._setDiffDeep(query.extraSegments, extra); + query.model._setDiffDeep(query.extraSegments, extra); }); }; -/** - * Flushes `_pendingSubscribeCallbacks`, calling each callback in the array, - * with an optional error to pass into each. `_pendingSubscribeCallbacks` will - * be empty after this runs. - * @private - */ -Query.prototype._flushPendingCallbacks = function(err) { +Query.prototype._setExtra = function(extra) { + if (extra === undefined) return; + this.model._setDiffDeep(this.extraSegments, extra); +}; +Query.prototype._setResults = function(results) { + var ids = resultsIds(results); + this._setResultIds(ids); +}; +Query.prototype._setResultIds = function(ids) { + this._registerDocs(ids); + this.model._setArrayDiff(this.idsSegments, ids); + this._maybeUnloadDocs(); +}; +Query.prototype._registerDocs = function(ids) { + // Register documents with Racer + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var doc = this.model.getDoc(this.collectionName, id); + if (!doc) { + var doc = this.model.getOrCreateDoc(this.collectionName, id); + doc._updateCollectionData(); + var segments = [doc.collectionName, doc.id]; + var eventArgs = [doc.get(), this.model._pass]; + this.model.emit('load', segments, eventArgs); + } + } +}; +Query.prototype._maybeUnloadDocs = function(ids) { + var collection = this.model.getCollection(this.collectionName); + if (!collection) return; + for (var id in collection.docs) { + this.model._maybeUnloadDoc(this.collectionName, id); + } +}; + +// Flushes `_pendingSubscribeCallbacks`, calling each callback in the array, +// with an optional error to pass into each. `_pendingSubscribeCallbacks` will +// be empty after this runs. +Query.prototype._flushSubscribeCallbacks = function(cb, err) { + cb(err); var pendingCallback; while (pendingCallback = this._pendingSubscribeCallbacks.shift()) { pendingCallback(err); } }; -/** - * @public - * @param {Function} cb(err, newFetchCount) - */ Query.prototype.unfetch = function(cb) { cb = this.model.wrapCallback(cb); this.model._context.unfetchQuery(this); @@ -369,11 +348,6 @@ Query.prototype.unfetch = function(cb) { return this; } - var ids = this.fetchIds.shift() || []; - for (var i = 0; i < ids.length; i++) { - this.model.unfetchDoc(this.collectionName, ids[i]); - } - var query = this; if (this.model.root.unloadDelay) { setTimeout(finishUnfetchQuery, this.model.root.unloadDelay); @@ -410,20 +384,11 @@ Query.prototype.unsubscribe = function(cb) { var count = --query.subscribeCount; if (count) return cb(null, count); - var ids; if (query.shareQuery) { - ids = resultsIds(query.shareQuery.results); query.shareQuery.destroy(); query.shareQuery = null; } - if (!query.model.root.fetchOnly && ids && ids.length) { - // Unsubscribe all documents that this query currently has in results - var group = util.asyncGroup(unsubscribeQueryCallback); - for (var i = 0; i < ids.length; i++) { - query.model.unsubscribeDoc(query.collectionName, ids[i], group()); - } - } unsubscribeQueryCallback(); } function unsubscribeQueryCallback(err) { @@ -435,64 +400,6 @@ Query.prototype.unsubscribe = function(cb) { return this; }; -Query.prototype._onInsert = function(shareDocs, index) { - var ids = []; - for (var i = 0; i < shareDocs.length; i++) { - var id = shareDocs[i].name; - ids.push(id); - this.model.subscribeDoc(this.collectionName, id); - } - this.model._insert(this.idsSegments, index, ids); -}; -Query.prototype._onRemove = function(shareDocs, index) { - this.model._remove(this.idsSegments, index, shareDocs.length); - for (var i = 0; i < shareDocs.length; i++) { - this.model.unsubscribeDoc(this.collectionName, shareDocs[i].name); - } -}; -Query.prototype._onMove = function(shareDocs, from, to) { - this.model._move(this.idsSegments, from, to, shareDocs.length); -}; - -Query.prototype._onChange = function(ids, previousIds, cb) { - // Diff the new and previous list of ids, subscribing to documents for - // inserted ids and unsubscribing from documents for removed ids - var diff = (previousIds) ? - arrayDiff(previousIds, ids) : - [new arrayDiff.InsertDiff(0, ids)]; - var previousCopy = previousIds && previousIds.slice(); - - // The results are updated via a different diff, since they might already - // have a value from a fetch or previous shareQuery instance - this.model._setArrayDiff(this.idsSegments, ids); - - var group, finished; - if (cb) { - group = util.asyncGroup(cb); - finished = group(); - } - for (var i = 0; i < diff.length; i++) { - var item = diff[i]; - if (item instanceof arrayDiff.InsertDiff) { - // Subscribe to the document for each inserted id - var values = item.values; - for (var j = 0; j < values.length; j++) { - this.model.subscribeDoc(this.collectionName, values[j], cb && group()); - } - } else if (item instanceof arrayDiff.RemoveDiff) { - var values = previousCopy.splice(item.index, item.howMany); - // Unsubscribe from the document for each removed id - for (var j = 0; j < values.length; j++) { - this.model.unsubscribeDoc(this.collectionName, values[j], cb && group()); - } - } - // Moving doesn't change document subscriptions, so that is ignored. - } - // Make sure that the callback gets called if the diff is empty or it - // contains no inserts or removes - finished && finished(); -}; - Query.prototype.get = function() { var results = []; var data = this.model._get(this.segments); @@ -513,7 +420,7 @@ Query.prototype.get = function() { }; Query.prototype.getIds = function() { - return this.model._get(this.idsSegments); + return this.model._get(this.idsSegments) || []; }; Query.prototype.getExtra = function() { diff --git a/lib/Model/contexts.js b/lib/Model/contexts.js index 008ff2003..fe5149e40 100644 --- a/lib/Model/contexts.js +++ b/lib/Model/contexts.js @@ -4,6 +4,7 @@ var Model = require('./Model'); var Query = require('./Query'); +var CollectionCounter = require('./CollectionCounter'); Model.INITS.push(function(model) { model.root._contexts = new Contexts(); @@ -39,16 +40,14 @@ Model.prototype.unloadAll = function() { function Contexts() {} -function FetchedDocs() {} -function SubscribedDocs() {} function FetchedQueries() {} function SubscribedQueries() {} function Context(model, id) { this.model = model; this.id = id; - this.fetchedDocs = new FetchedDocs(); - this.subscribedDocs = new SubscribedDocs(); + this.fetchedDocs = new CollectionCounter(); + this.subscribedDocs = new CollectionCounter(); this.fetchedQueries = new FetchedQueries(); this.subscribedQueries = new SubscribedQueries(); } @@ -60,21 +59,17 @@ Context.prototype.toJSON = function() { }; }; -Context.prototype.fetchDoc = function(path, pass) { - if (pass.$query) return; - mapIncrement(this.fetchedDocs, path); +Context.prototype.fetchDoc = function(collectionName, id) { + this.fetchedDocs.increment(collectionName, id); }; -Context.prototype.subscribeDoc = function(path, pass) { - if (pass.$query) return; - mapIncrement(this.subscribedDocs, path); +Context.prototype.subscribeDoc = function(collectionName, id) { + this.subscribedDocs.increment(collectionName, id); }; -Context.prototype.unfetchDoc = function(path, pass) { - if (pass.$query) return; - mapDecrement(this.fetchedDocs, path); +Context.prototype.unfetchDoc = function(collectionName, id) { + this.fetchedDocs.decrement(collectionName, id); }; -Context.prototype.unsubscribeDoc = function(path, pass) { - if (pass.$query) return; - mapDecrement(this.subscribedDocs, path); +Context.prototype.unsubscribeDoc = function(collectionName, id) { + this.subscribedDocs.decrement(collectionName, id); }; Context.prototype.fetchQuery = function(query) { mapIncrement(this.fetchedQueries, query.hash); @@ -110,14 +105,18 @@ Context.prototype.unload = function() { var count = this.subscribedQueries[hash]; while (count--) query.unsubscribe(); } - for (var path in this.fetchedDocs) { - var segments = path.split('.'); - var count = this.fetchedDocs[path]; - while (count--) model.unfetchDoc(segments[0], segments[1]); + for (var collectionName in this.fetchedDocs.collections) { + var collection = this.fetchedDocs.collections[collectionName]; + for (var id in collection) { + var count = collection[id]; + while (count--) model.unfetchDoc(collectionName, id); + } } - for (var path in this.subscribedDocs) { - var segments = path.split('.'); - var count = this.subscribedDocs[path]; - while (count--) model.unsubscribeDoc(segments[0], segments[1]); + for (var collectionName in this.subscribedDocs.collections) { + var collection = this.subscribedDocs.collections[collectionName]; + for (var id in collection) { + var count = collection[id]; + while (count--) model.unsubscribeDoc(collectionName, id); + } } }; diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 1ea801f80..ee100d005 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -1,27 +1,18 @@ var util = require('../util'); var Model = require('./Model'); var Query = require('./Query'); +var CollectionCounter = require('./CollectionCounter'); Model.INITS.push(function(model, options) { model.root.fetchOnly = options.fetchOnly; model.root.unloadDelay = options.unloadDelay || (util.isServer) ? 0 : 1000; - // Keeps track of the count of fetches (that haven't been undone by an - // unfetch) per doc. Maps doc id to the fetch count. - model.root._fetchedDocs = new FetchedDocs(); - - // Keeps track of the count of subscribes (that haven't been undone by an - // unsubscribe) per doc. Maps doc id to the subscribe count. - model.root._subscribedDocs = new SubscribedDocs(); - - // Maps doc path to doc version - model.root._loadVersions = new LoadVersions(); + // Track the total number of active fetches per doc + model.root._fetchedDocs = new CollectionCounter(); + // Track the total number of active susbscribes per doc + model.root._subscribedDocs = new CollectionCounter(); }); -function FetchedDocs() {} -function SubscribedDocs() {} -function LoadVersions() {} - Model.prototype.fetch = function() { this._forSubscribable(arguments, 'fetch'); return this; @@ -87,85 +78,66 @@ Model.prototype._forSubscribable = function(argumentsObject, method) { process.nextTick(finished); }; -/** - * @param {String} - * @param {String} id - * @param {Function} cb(err) - * @param {Boolean} alreadyLoaded - */ -Model.prototype.fetchDoc = function(collectionName, id, cb, alreadyLoaded) { +Model.prototype.fetchDoc = function(collectionName, id, cb) { cb = this.wrapCallback(cb); // Maintain a count of fetches so that we can unload the document when // there are no remaining fetches or subscribes for that document - var path = collectionName + '.' + id; - this._context.fetchDoc(path, this._pass); - this.root._fetchedDocs[path] = (this.root._fetchedDocs[path] || 0) + 1; + this._context.fetchDoc(collectionName, id); + this.root._fetchedDocs.increment(collectionName, id); - var model = this; + // Fetch var doc = this.getOrCreateDoc(collectionName, id); - if (alreadyLoaded) { - fetchDocCallback(); - } else { - doc.shareDoc.fetch(fetchDocCallback); - } - function fetchDocCallback(err) { - if (err) return cb(err); - if (doc.shareDoc.version !== model.root._loadVersions[path]) { - model.root._loadVersions[path] = doc.shareDoc.version; - doc._updateCollectionData(); - model.emit('load', [collectionName, id], [doc.get(), model._pass]); - } - cb(); - } + var loadCb = this._getLoadCb(doc, cb); + doc.shareDoc.fetch(loadCb); }; -/** - * @param {String} collectionName - * @param {String} id of the document we want to subscribe to - * @param {Function} cb(err) - */ Model.prototype.subscribeDoc = function(collectionName, id, cb) { cb = this.wrapCallback(cb); - var path = collectionName + '.' + id; - this._context.subscribeDoc(path, this._pass); - var count = this.root._subscribedDocs[path] = (this.root._subscribedDocs[path] || 0) + 1; + this._context.subscribeDoc(collectionName, id); + var count = this.root._subscribedDocs.increment(collectionName, id); // Already requested a subscribe, so just return if (count > 1) return cb(); - // Subscribe if currently unsubscribed - var model = this; + // Subscribe var doc = this.getOrCreateDoc(collectionName, id); + var loadCb = this._getLoadCb(doc, cb); if (this.root.fetchOnly) { - // Only fetch if the document isn't already loaded - if (doc.get() === void 0) { - doc.shareDoc.fetch(subscribeDocCallback); - } else { - subscribeDocCallback(); - } + doc.shareDoc.fetch(loadCb); } else { - doc.shareDoc.subscribe(subscribeDocCallback); + doc.shareDoc.subscribe(loadCb); } - function subscribeDocCallback(err) { +}; + +Model.prototype._getLoadCb = function(doc, cb) { + // Version will be null before ever fetching or subscribing to doc data from + // the server. Once the doc already has a verison, ShareJS will emit change + // events as ops happen, so there is no need to emit load events + if (doc.shareDoc.version != null) { + return cb; + } + var model = this; + return function loadCb(err) { if (err) return cb(err); - if (!doc.createdLocally && doc.shareDoc.version !== model.root._loadVersions[path]) { - model.root._loadVersions[path] = doc.shareDoc.version; - doc._updateCollectionData(); - model.emit('load', [collectionName, id], [doc.get(), model._pass]); + doc._updateCollectionData(); + // If we created a doc locally, we will have been emitting events all along + // and no load event should be emitted. Also, if the shareDoc version is 0, + // that means it hasn't ever been created. Thus, there is no need to emit + // an event, since the doc snapshot will still be undefined + if (!doc.createdLocally && doc.shareDoc.version > 0) { + model.emit('load', [doc.collectionName, doc.id], [doc.get(), model._pass]); } cb(); - } + }; }; Model.prototype.unfetchDoc = function(collectionName, id, cb) { cb = this.wrapCallback(cb); - var path = collectionName + '.' + id; - this._context.unfetchDoc(path, this._pass); - var fetchedDocs = this.root._fetchedDocs; + this._context.unfetchDoc(collectionName, id); - // No effect if the document has no fetch count - if (!fetchedDocs[path]) return cb(); + // No effect if the document is not currently fetched + if (!this.root._fetchedDocs.get(collectionName, id)) return cb(); var model = this; if (this.root.unloadDelay && !this._pass.$query) { @@ -174,22 +146,19 @@ Model.prototype.unfetchDoc = function(collectionName, id, cb) { finishUnfetchDoc(); } function finishUnfetchDoc() { - var count = --fetchedDocs[path]; + var count = model.root._fetchedDocs.decrement(collectionName, id); if (count) return cb(null, count); - delete fetchedDocs[path]; - model._maybeUnloadDoc(collectionName, id, path); + model._maybeUnloadDoc(collectionName, id); cb(null, 0); } }; Model.prototype.unsubscribeDoc = function(collectionName, id, cb) { cb = this.wrapCallback(cb); - var path = collectionName + '.' + id; - this._context.unsubscribeDoc(path, this._pass); - var subscribedDocs = this.root._subscribedDocs; + this._context.unsubscribeDoc(collectionName, id); // No effect if the document is not currently subscribed - if (!subscribedDocs[path]) return cb(); + if (!this.root._subscribedDocs.get(collectionName, id)) return cb(); var model = this; if (this.root.unloadDelay && !this._pass.$query) { @@ -198,51 +167,64 @@ Model.prototype.unsubscribeDoc = function(collectionName, id, cb) { finishUnsubscribeDoc(); } function finishUnsubscribeDoc() { - var count = --subscribedDocs[path]; + var count = model.root._subscribedDocs.decrement(collectionName, id); // If there are more remaining subscriptions, only decrement the count // and callback with how many subscriptions are remaining if (count) return cb(null, count); // If there is only one remaining subscription, actually unsubscribe - delete subscribedDocs[path]; if (model.root.fetchOnly) { unsubscribeDocCallback(); } else { - var shareDoc = model.root.shareConnection.get(collectionName, id); + var shareDoc = model.getDoc(collectionName, id).shareDoc; if (!shareDoc) { - return cb(new Error('Share document not found for: ' + path)); + return cb(new Error('Share document not found for: ' + collectionName + '.' + id)); } shareDoc.unsubscribe(unsubscribeDocCallback); } } function unsubscribeDocCallback(err) { - model._maybeUnloadDoc(collectionName, id, path); + model._maybeUnloadDoc(collectionName, id); if (err) return cb(err); cb(null, 0); } }; -/** - * Removes the document from the local model if the model no longer has any - * remaining fetches or subscribes on path. - * Called from Model.prototype.unfetchDoc and Model.prototype.unsubscribeDoc as - * part of attempted cleanup. - * @param {String} collectionName - * @param {String} id - * @param {String} path - */ -Model.prototype._maybeUnloadDoc = function(collectionName, id, path) { +// Removes the document from the local model if the model no longer has any +// remaining fetches or subscribes via a query or direct loading +Model.prototype._maybeUnloadDoc = function(collectionName, id) { + if (this._hasDocReferences(collectionName, id)) return; + var doc = this.getDoc(collectionName, id); if (!doc) return; - // Remove the document from the local model if it no longer has any - // remaining fetches or subscribes - if (this.root._fetchedDocs[path] || this.root._subscribedDocs[path]) return; var previous = doc.get(); - this.root.collections[collectionName].remove(id); - // Remove doc from memory in Share as well + // Remove doc from Racer + this.root.collections[collectionName].remove(id); + // Remove doc from Share if (doc.shareDoc) doc.shareDoc.destroy(); - delete this.root._loadVersions[path]; this.emit('unload', [collectionName, id], [previous, this._pass]); }; + +Model.prototype._hasDocReferences = function(collectionName, id) { + // Check if any fetched or subscribed queries currently have the + // id in their results + var queries = this.root._queries.collections[collectionName]; + if (queries) { + for (var hash in queries) { + var query = queries[hash]; + if (!query.subscribeCount && !query.fetchCount) continue; + var ids = query.getIds(); + if (ids.indexOf(id) !== -1) return true; + } + } + + // Check if document currently has direct fetch or subscribe + if ( + this.root._fetchedDocs.get(collectionName, id) || + this.root._subscribedDocs.get(collectionName, id) + ) return true; + + return false; +}; diff --git a/lib/Model/unbundle.js b/lib/Model/unbundle.js index 4582ea1f9..f484412fd 100644 --- a/lib/Model/unbundle.js +++ b/lib/Model/unbundle.js @@ -1,6 +1,8 @@ var Model = require('./Model'); Model.prototype.unbundle = function(data) { + if (this.shareConnection) this.shareConnection.bsStart(); + // Re-create and subscribe queries; re-create documents associated with queries this._initQueries(data.queries); @@ -8,10 +10,7 @@ Model.prototype.unbundle = function(data) { for (var collectionName in data.collections) { var collection = data.collections[collectionName]; for (var id in collection) { - var doc = this.getOrCreateDoc(collectionName, id, collection[id]); - if (doc.shareDoc) { - this._loadVersions[collectionName + '.' + id] = doc.shareDoc.version; - } + this.getOrCreateDoc(collectionName, id, collection[id]); } } @@ -33,6 +32,8 @@ Model.prototype.unbundle = function(data) { } } + if (this.shareConnection) this.shareConnection.bsEnd(); + // Re-create refs for (var i = 0; i < data.refs.length; i++) { var item = data.refs[i]; From 5774399cec12ec4fbea0becd365706844947eb9e Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 7 May 2015 19:55:29 -0700 Subject: [PATCH 049/479] remove log --- lib/Model/Query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 3785cda51..92c0323fd 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -262,8 +262,8 @@ Query.prototype._shareSubscribe = function(options, cb) { // have been emitted with the same results query._flushSubscribeCallbacks(cb); } + // Sanity check, though this shouldn't happen if (this.shareQuery) { - console.log('OLD SHARE QUERY', this.shareQuery); this.shareQuery.destroy(); } this.shareQuery = this.model.root.shareConnection.createSubscribeQuery( From 448688a733cb042eedd6589a75cd5060b174a36e Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 7 May 2015 20:13:06 -0700 Subject: [PATCH 050/479] cleanup server subscriptions --- lib/Model/subscriptions.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index ee100d005..5ca481973 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -202,7 +202,22 @@ Model.prototype._maybeUnloadDoc = function(collectionName, id) { // Remove doc from Racer this.root.collections[collectionName].remove(id); // Remove doc from Share - if (doc.shareDoc) doc.shareDoc.destroy(); + if (doc.shareDoc) { + // Share queries automatically subscribe to documents in results, but they + // don't automatically unsubscribe. If we don't unsubscribe here properly, + // the server will continue to keep the document subscription open even if + // the query subscription is destroyed. + // + // We need to send an unsubscribe message directly instead of through the + // document, since the document was likely subscribed via a query and not + // the doc itself. Calling shareDoc.unsubscribe() won't send a message + // unless the doc was directly subscribed earlier. + // + // TODO: Add support to Share to send unsubscribe messages in bulk + // TODO: Avoid sending if we have only ever gotten a document via fetch + this.root.shareConnection.sendUnsubscribe(collectionName, id); + doc.shareDoc.destroy(); + } this.emit('unload', [collectionName, id], [previous, this._pass]); }; From 3bf95ca89860dd35aab0865aae3167943c9087d2 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 7 May 2015 20:32:17 -0700 Subject: [PATCH 051/479] update share version requirement --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90a4b9173..14f2e66d2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "arraydiff": "^0.1.1", "deep-is": "^0.1.3", "uuid": "^2.0.1", - "share": "^0.7.3" + "share": "^0.7.29" }, "devDependencies": { "expect.js": "~0.3.1", From 18a86359e5c1e3779b81c0ad7f3ded6cd5eebbca Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 7 May 2015 20:32:29 -0700 Subject: [PATCH 052/479] 0.6.0-alpha33 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 14f2e66d2..84e96b146 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha32", + "version": "0.6.0-alpha33", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From f08cb1568cd484ed73fbb396e8f5c0f2018a0edc Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 7 May 2015 23:13:14 -0700 Subject: [PATCH 053/479] null check doc existence on unsubscribe and fail softly --- lib/Model/subscriptions.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 5ca481973..855dcac95 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -176,10 +176,9 @@ Model.prototype.unsubscribeDoc = function(collectionName, id, cb) { if (model.root.fetchOnly) { unsubscribeDocCallback(); } else { - var shareDoc = model.getDoc(collectionName, id).shareDoc; - if (!shareDoc) { - return cb(new Error('Share document not found for: ' + collectionName + '.' + id)); - } + var doc = model.getDoc(collectionName, id); + var shareDoc = doc && doc.shareDoc; + if (!shareDoc) return unsubscribeDocCallback(); shareDoc.unsubscribe(unsubscribeDocCallback); } } From ce87abf096a5939a8084e60bf4f1ca6de0401b68 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 7 May 2015 23:45:57 -0700 Subject: [PATCH 054/479] fix bundling and unbundling of contexts --- lib/Model/CollectionCounter.js | 7 +++++++ lib/Model/contexts.js | 7 +++++-- lib/Model/unbundle.js | 24 ++++++++++++++++-------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/Model/CollectionCounter.js b/lib/Model/CollectionCounter.js index f1ff357e9..e39287973 100644 --- a/lib/Model/CollectionCounter.js +++ b/lib/Model/CollectionCounter.js @@ -25,3 +25,10 @@ CollectionCounter.prototype.decrement = function(collectionName, id) { delete this.collections[collection]; return 0; }; +CollectionCounter.prototype.toJSON = function() { + // Check to see if we have any keys + for (var key in this.collections) { + return this.collections; + } + return; +}; diff --git a/lib/Model/contexts.js b/lib/Model/contexts.js index fe5149e40..946e506d0 100644 --- a/lib/Model/contexts.js +++ b/lib/Model/contexts.js @@ -53,9 +53,12 @@ function Context(model, id) { } Context.prototype.toJSON = function() { + var fetchedDocs = this.fetchedDocs.toJSON(); + var subscribedDocs = this.subscribedDocs.toJSON(); + if (!fetchedDocs && !subscribedDocs) return; return { - fetchedDocs: this.fetchedDocs - , subscribedDocs: this.subscribedDocs + fetchedDocs: fetchedDocs, + subscribedDocs: subscribedDocs }; }; diff --git a/lib/Model/unbundle.js b/lib/Model/unbundle.js index f484412fd..9508c181b 100644 --- a/lib/Model/unbundle.js +++ b/lib/Model/unbundle.js @@ -18,16 +18,24 @@ Model.prototype.unbundle = function(data) { var contextData = data.contexts[contextId]; var contextModel = this.context(contextId); // Re-init fetchedDocs counts - for (var path in contextData.fetchedDocs) { - contextModel._context.fetchDoc(path, contextModel._pass); - this._fetchedDocs[path] = (this._fetchedDocs[path] || 0) + - contextData.fetchedDocs[path]; + for (var collectionName in contextData.fetchedDocs) { + var collection = contextData.fetchedDocs[collectionName]; + for (var id in collection) { + var count = collection[id]; + while (count--) { + contextModel._context.fetchDoc(collectionName, id); + this._fetchedDocs.increment(collectionName, id); + } + } } // Subscribe to document subscriptions - for (var path in contextData.subscribedDocs) { - var subscribed = contextData.subscribedDocs[path]; - while (subscribed--) { - contextModel.subscribe(path); + for (var collectionName in contextData.subscribedDocs) { + var collection = contextData.subscribedDocs[collectionName]; + for (var id in collection) { + var count = collection[id]; + while (count--) { + contextModel.subscribeDoc(collectionName, id); + } } } } From 85b538cf969810e51f90cdf33bb3b44b7dc9713a Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 7 May 2015 23:47:12 -0700 Subject: [PATCH 055/479] 0.6.0-alpha34 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 84e96b146..24d2b4845 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha33", + "version": "0.6.0-alpha34", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 392ed21f03298f1dbb161fa9d246fb1eaade5a44 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 8 May 2015 15:45:20 -0700 Subject: [PATCH 056/479] emit share error events on model with extra context data; set disconnect reason on $connection.reason --- lib/Model/Query.js | 3 +++ lib/Model/RemoteDoc.js | 3 +++ lib/Model/connection.js | 13 ++++++++----- lib/Model/events.js | 18 ++++++++++++++++++ 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 92c0323fd..24c36b276 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -290,6 +290,9 @@ Query.prototype._shareSubscribe = function(options, cb) { this.shareQuery.on('extra', function(extra) { query.model._setDiffDeep(query.extraSegments, extra); }); + this.shareQuery.on('error', function(err) { + query.model._emitError(err, query.hash); + }); }; Query.prototype._setExtra = function(extra) { diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 452526725..2b3fb1b96 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -52,6 +52,9 @@ function RemoteDoc(model, collectionName, id, data) { var value = shareDoc.snapshot; model.emit('change', [collectionName, id], [value, void 0, model._pass]); }); + shareDoc.on('error', function(err) { + model._emitError(err, collectionName + '.' + id); + }); } RemoteDoc.prototype = new Doc(); diff --git a/lib/Model/connection.js b/lib/Model/connection.js index 4730fa51d..86b4f52e3 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -10,16 +10,19 @@ Model.prototype.createConnection = function(bundle) { // The Share connection will bind to the socket by defining the onopen, // onmessage, etc. methods + var model = this; var shareConnection = this.root.shareConnection = new share.Connection(this.root.socket); - var segments = ['$connection', 'state']; + shareConnection.on('error', function(err, data) { + model._emitError(err, data); + }); var states = ['connecting', 'connected', 'disconnected', 'stopped']; - var model = this; states.forEach(function(state) { - shareConnection.on(state, function() { - model._setDiff(segments, state); + shareConnection.on(state, function(reason) { + model._setDiff(['$connection', 'state'], state); + model._setDiff(['$connection', 'reason'], reason); }); }); - this._set(segments, 'connected'); + this._set(['$connection', 'state'], 'connected'); // Wrap the socket methods on top of Share's methods this._createChannel(); diff --git a/lib/Model/events.js b/lib/Model/events.js index 6f40bd302..e000f2a7b 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -46,6 +46,24 @@ Model.prototype.wrapCallback = function(cb) { }; }; +Model.prototype._emitError = function(err, additionalMessage) { + if (typeof additionalMessage !== 'string') { + try { + additionalMessage = JSON.stringify(additionalMessage); + } catch (stringifyErr) {} + } + if (typeof err === 'string') { + err = new Error(err + ' ' + additionalMessage); + } else if (err instanceof Error) { + err.message = err.message + ' ' + additionalMessage; + } else { + try { + err = new Error(JSON.stringify(err) + ' ' + additionalMessage); + } catch (stringifyErr) {} + } + this.emit('error', err); +}; + // EventEmitter.prototype.on, EventEmitter.prototype.addListener, and // EventEmitter.prototype.once return `this`. The Model equivalents return // the listener instead, since it is made internally for method subscriptions From 6e50767e063427ed2bf7cadd1c614a5ff820e673 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 8 May 2015 15:59:57 -0700 Subject: [PATCH 057/479] 0.6.0-alpha35 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 24d2b4845..7e6ffce3d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha34", + "version": "0.6.0-alpha35", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From f6c27d0f455dfa069d78eb87cfc272de87fccd7f Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 8 May 2015 16:44:21 -0700 Subject: [PATCH 058/479] add query error handling for fetches --- lib/Model/Query.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 24c36b276..a0fe7b9c3 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -192,12 +192,15 @@ Query.prototype.fetch = function(cb) { query._setResults(results); cb(); } - this.model.root.shareConnection.createFetchQuery( + var shareQuery = this.model.root.shareConnection.createFetchQuery( this.collectionName, this.sourceQuery(), options, fetchCb ); + shareQuery.on('error', function(err) { + query.model._emitError(err, query.hash); + }); return this; }; @@ -245,12 +248,15 @@ Query.prototype._shareFetchedSubscribe = function(options, cb) { query._flushSubscribeCallbacks(cb); } options.docMode = 'fetch'; - this.model.root.shareConnection.createFetchQuery( + var shareQuery = this.model.root.shareConnection.createFetchQuery( this.collectionName, this.sourceQuery(), options, fetchedSubscribeCb ); + shareQuery.on('error', function(err) { + query.model._emitError(err, query.hash); + }); }; Query.prototype._shareSubscribe = function(options, cb) { From f4249b5f3f32d435765846f8b0423f2b95bede36 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 8 May 2015 16:44:54 -0700 Subject: [PATCH 059/479] 0.6.0-alpha36 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7e6ffce3d..800c52af4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha35", + "version": "0.6.0-alpha36", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 4d447c47a68c1ce6e2a618bdfcfdb5e5015248eb Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 11 May 2015 02:56:14 -0700 Subject: [PATCH 060/479] remove unsubscribe hack now that share queries don't cause doc subscriptions server side --- lib/Model/subscriptions.js | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 855dcac95..a5063275f 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -201,22 +201,7 @@ Model.prototype._maybeUnloadDoc = function(collectionName, id) { // Remove doc from Racer this.root.collections[collectionName].remove(id); // Remove doc from Share - if (doc.shareDoc) { - // Share queries automatically subscribe to documents in results, but they - // don't automatically unsubscribe. If we don't unsubscribe here properly, - // the server will continue to keep the document subscription open even if - // the query subscription is destroyed. - // - // We need to send an unsubscribe message directly instead of through the - // document, since the document was likely subscribed via a query and not - // the doc itself. Calling shareDoc.unsubscribe() won't send a message - // unless the doc was directly subscribed earlier. - // - // TODO: Add support to Share to send unsubscribe messages in bulk - // TODO: Avoid sending if we have only ever gotten a document via fetch - this.root.shareConnection.sendUnsubscribe(collectionName, id); - doc.shareDoc.destroy(); - } + if (doc.shareDoc) doc.shareDoc.destroy(); this.emit('unload', [collectionName, id], [previous, this._pass]); }; From c1ef5bd87d5d1d24aad6ab2bc5b2434df0d7841f Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 11 May 2015 14:19:50 -0700 Subject: [PATCH 061/479] maybeUnload a more efficient by maintaining a map of the current results per query and only checking items that might have been removed instead of every doc in the collection --- lib/Model/Query.js | 42 ++++++++++++++++++++++++++++++++------ lib/Model/subscriptions.js | 8 ++++---- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index a0fe7b9c3..bb5619e05 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -48,6 +48,7 @@ Model.prototype._initQueries = function(items) { var query = new Query(this, collectionName, expression, source); queries.add(query); + query._addMapIds(ids); this._set(query.idsSegments, ids); // This is a bit of a hack, but it should be correct. Given that queries @@ -147,6 +148,15 @@ function Query(model, collectionName, expression, source) { this.created = false; this.shareQuery = null; + + // idMap is checked in maybeUnload to see if the query is currently holding + // a reference to an id in its results set. This map is duplicative of the + // actual results id list stored in the model, but we are maintaining it, + // because otherwise maybeUnload would be looping through the entire results + // set of each query on the same collection for every doc checked + // + // Map of id -> true + this.idMap = {}; } Query.prototype.create = function() { @@ -155,14 +165,16 @@ Query.prototype.create = function() { }; Query.prototype.destroy = function() { + var ids = this.getIds(); this.created = false; if (this.shareQuery) { this.shareQuery.destroy(); this.shareQuery = null; } this.model.root._queries.remove(this); + this.idMap = {}; this.model._del(this.segments); - this._maybeUnloadDocs(); + this._maybeUnloadDocs(ids); }; Query.prototype.sourceQuery = function() { @@ -281,11 +293,14 @@ Query.prototype._shareSubscribe = function(options, cb) { this.shareQuery.on('insert', function(shareDocs, index) { var ids = resultsIds(shareDocs); query._registerDocs(ids); + query._addMapIds(ids); query.model._insert(query.idsSegments, index, ids); }); this.shareQuery.on('remove', function(shareDocs, index) { + var ids = resultsIds(shareDocs); + query._removeMapIds(ids); query.model._remove(query.idsSegments, index, shareDocs.length); - query._maybeUnloadDocs(); + query._maybeUnloadDocs(ids); }); this.shareQuery.on('move', function(shareDocs, from, to) { query.model._move(query.idsSegments, from, to, shareDocs.length); @@ -301,6 +316,18 @@ Query.prototype._shareSubscribe = function(options, cb) { }); }; +Query.prototype._removeMapIds = function(ids) { + for (var i = ids.length; i--;) { + var id = ids[i]; + delete this.idMap[id]; + } +}; +Query.prototype._addMapIds = function(ids) { + for (var i = ids.length; i--;) { + var id = ids[i]; + this.idMap[id] = true; + } +}; Query.prototype._setExtra = function(extra) { if (extra === undefined) return; this.model._setDiffDeep(this.extraSegments, extra); @@ -311,8 +338,12 @@ Query.prototype._setResults = function(results) { }; Query.prototype._setResultIds = function(ids) { this._registerDocs(ids); + var previousIds = this.getIds(); + // Reset the map of ids, which is checked in maybeUnload + this.idMap = {}; + this._addMapIds(ids); this.model._setArrayDiff(this.idsSegments, ids); - this._maybeUnloadDocs(); + this._maybeUnloadDocs(previousIds); }; Query.prototype._registerDocs = function(ids) { // Register documents with Racer @@ -329,9 +360,8 @@ Query.prototype._registerDocs = function(ids) { } }; Query.prototype._maybeUnloadDocs = function(ids) { - var collection = this.model.getCollection(this.collectionName); - if (!collection) return; - for (var id in collection.docs) { + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; this.model._maybeUnloadDoc(this.collectionName, id); } }; diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index a5063275f..37d6d4ecb 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -192,10 +192,11 @@ Model.prototype.unsubscribeDoc = function(collectionName, id, cb) { // Removes the document from the local model if the model no longer has any // remaining fetches or subscribes via a query or direct loading Model.prototype._maybeUnloadDoc = function(collectionName, id) { - if (this._hasDocReferences(collectionName, id)) return; - var doc = this.getDoc(collectionName, id); if (!doc) return; + + if (this._hasDocReferences(collectionName, id)) return; + var previous = doc.get(); // Remove doc from Racer @@ -214,8 +215,7 @@ Model.prototype._hasDocReferences = function(collectionName, id) { for (var hash in queries) { var query = queries[hash]; if (!query.subscribeCount && !query.fetchCount) continue; - var ids = query.getIds(); - if (ids.indexOf(id) !== -1) return true; + if (query.idMap[id]) return true; } } From e958ae2d9c5a51067535bb57c07273b95b445630 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 11 May 2015 15:24:00 -0700 Subject: [PATCH 062/479] min share version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 800c52af4..2fc78f7a2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "arraydiff": "^0.1.1", "deep-is": "^0.1.3", "uuid": "^2.0.1", - "share": "^0.7.29" + "share": "^0.7.31" }, "devDependencies": { "expect.js": "~0.3.1", From 1a1d0a012b767798c83418006d1aa48c277f1394 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 11 May 2015 15:27:04 -0700 Subject: [PATCH 063/479] 0.6.0-alpha37 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2fc78f7a2..979d26ed9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha36", + "version": "0.6.0-alpha37", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From fa75939c7f34a985c1f89024211bb1431e136144 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 11 May 2015 21:39:26 -0700 Subject: [PATCH 064/479] update data on share doc 'ready' event, which happens when a new snapshot is loaded --- lib/Model/Query.js | 5 ++--- lib/Model/RemoteDoc.js | 3 +++ lib/Model/subscriptions.js | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index bb5619e05..581bc8403 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -351,9 +351,8 @@ Query.prototype._registerDocs = function(ids) { var id = ids[i]; var doc = this.model.getDoc(this.collectionName, id); if (!doc) { - var doc = this.model.getOrCreateDoc(this.collectionName, id); - doc._updateCollectionData(); - var segments = [doc.collectionName, doc.id]; + doc = this.model.getOrCreateDoc(this.collectionName, id); + var segments = [this.collectionName, id]; var eventArgs = [doc.get(), this.model._pass]; this.model.emit('load', segments, eventArgs); } diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 2b3fb1b96..64e848e36 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -55,6 +55,9 @@ function RemoteDoc(model, collectionName, id, data) { shareDoc.on('error', function(err) { model._emitError(err, collectionName + '.' + id); }); + shareDoc.on('ready', function() { + doc._updateCollectionData(); + }); } RemoteDoc.prototype = new Doc(); diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 37d6d4ecb..3a33ae820 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -120,7 +120,6 @@ Model.prototype._getLoadCb = function(doc, cb) { var model = this; return function loadCb(err) { if (err) return cb(err); - doc._updateCollectionData(); // If we created a doc locally, we will have been emitting events all along // and no load event should be emitted. Also, if the shareDoc version is 0, // that means it hasn't ever been created. Thus, there is no need to emit From 5d37da40230052c9df45b2451d0ac49a105e4e81 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 11 May 2015 21:54:47 -0700 Subject: [PATCH 065/479] 0.6.0-alpha38 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 979d26ed9..daa78545f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha37", + "version": "0.6.0-alpha38", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 334861db41b0764f912cca0ceb72d9e85107be38 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 12 May 2015 01:36:24 -0700 Subject: [PATCH 066/479] use shareConnection.on('doc') event to register any created docs & 'ready' event to emit 'load' more accurately --- lib/Model/RemoteDoc.js | 52 +++++++++++++++++++++++----------- lib/Model/collections.js | 2 +- lib/Model/connection.js | 30 ++++++++++---------- lib/Model/connection.server.js | 2 +- lib/Model/subscriptions.js | 39 ++++++------------------- 5 files changed, 61 insertions(+), 64 deletions(-) diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 64e848e36..781bacfe6 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -11,18 +11,38 @@ var util = require('../util'); module.exports = RemoteDoc; -function RemoteDoc(model, collectionName, id, data) { +function RemoteDoc(model, collectionName, id, data, collection) { + // This is a bit messy, but we have to immediately register this doc + // on the collection that added it, so that when we create the shareDoc + // and the shareConnection emits the 'doc' event, we'll find this doc + // instead of creating a new one + if (collection) collection.docs[id] = this; + Doc.call(this, model, collectionName, id); - var shareDoc = this.shareDoc = model._getOrCreateShareDoc(collectionName, id, data); - if (model.root.debug.disableSubmit) { - shareDoc.submitOp = function() {}; - } + this.model = model.pass({$remote: true}); this.debugMutations = model.root.debug.remoteMutations; - this.createdLocally = false; - this.model = model = model.pass({$remote: true}); - this._updateCollectionData(); + // Get or create the Share document. Note that we must have already added + // this doc to the collection to avoid creating a duplicate doc + this.shareDoc = model.root.shareConnection.get(collectionName, id, data); + this._initShareDoc(); +} + +RemoteDoc.prototype = new Doc(); + +RemoteDoc.prototype._initShareDoc = function() { var doc = this; + var model = this.model; + var collectionName = this.collectionName; + var id = this.id; + var shareDoc = this.shareDoc; + // Needed to follow along events properly + shareDoc.incremental = true; + // Override submitOp to disable all writes and perform a dry-run + if (model.root.debug.disableSubmit) { + shareDoc.submitOp = function() {}; + } + // Subscribe to doc events shareDoc.on('op', function(op, isLocal) { // Don't emit on local operations, since they are emitted in the mutator if (isLocal) return; @@ -42,12 +62,7 @@ function RemoteDoc(model, collectionName, id, data) { // emit the appropriate event. Remote creates can set the snapshot data // without emitting an operation event, so an event needs to be emitted // for them. - if (isLocal) { - // Track when a document was created by this client, so that we don't - // emit a load event when subsequently subscribed - doc.createdLocally = true; - return; - } + if (isLocal) return; doc._updateCollectionData(); var value = shareDoc.snapshot; model.emit('change', [collectionName, id], [value, void 0, model._pass]); @@ -57,10 +72,13 @@ function RemoteDoc(model, collectionName, id, data) { }); shareDoc.on('ready', function() { doc._updateCollectionData(); + var value = shareDoc.snapshot; + // If we subscribe to an uncreated document, no need to emit 'load' event + if (value === undefined) return; + model.emit('load', [collectionName, id], [value, model._pass]); }); -} - -RemoteDoc.prototype = new Doc(); + this._updateCollectionData(); +}; RemoteDoc.prototype._updateCollectionData = function() { var snapshot = this.shareDoc.snapshot; diff --git a/lib/Model/collections.js b/lib/Model/collections.js index 85583c1a5..a7f06b184 100644 --- a/lib/Model/collections.js +++ b/lib/Model/collections.js @@ -112,7 +112,7 @@ function Collection(model, name, Doc) { * @return {LocalDoc|RemoteDoc} doc */ Collection.prototype.add = function(id, data) { - var doc = new this.Doc(this.model, this.name, id, data); + var doc = new this.Doc(this.model, this.name, id, data, this); this.docs[id] = doc; return doc; }; diff --git a/lib/Model/connection.js b/lib/Model/connection.js index 86b4f52e3..34395212e 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -12,9 +12,6 @@ Model.prototype.createConnection = function(bundle) { // onmessage, etc. methods var model = this; var shareConnection = this.root.shareConnection = new share.Connection(this.root.socket); - shareConnection.on('error', function(err, data) { - model._emitError(err, data); - }); var states = ['connecting', 'connected', 'disconnected', 'stopped']; states.forEach(function(state) { shareConnection.on(state, function(reason) { @@ -24,8 +21,21 @@ Model.prototype.createConnection = function(bundle) { }); this._set(['$connection', 'state'], 'connected'); - // Wrap the socket methods on top of Share's methods - this._createChannel(); + this._finishCreateConnection(); +}; + +Model.prototype._finishCreateConnection = function() { + var model = this; + this.shareConnection.on('error', function(err, data) { + model._emitError(err, data); + }); + // Share docs can be created by queries, so we need to register them + // with Racer as soon as they are created to capture their events + this.shareConnection.on('doc', function(shareDoc) { + model.getOrCreateDoc(shareDoc.collection, shareDoc.name); + }); + + this.root.channel = new Channel(this.root.socket); }; Model.prototype.connect = function() { @@ -48,16 +58,6 @@ Model.prototype.close = function(cb) { }); }; -Model.prototype._createChannel = function() { - this.root.channel = new Channel(this.root.socket); -}; - -Model.prototype._getOrCreateShareDoc = function(collectionName, id, data) { - var shareDoc = this.root.shareConnection.get(collectionName, id, data); - shareDoc.incremental = true; - return shareDoc; -}; - Model.prototype._isLocal = function(name) { // Whether the collection is local or remote is determined by its name. // Collections starting with an underscore ('_') are for user-defined local diff --git a/lib/Model/connection.server.js b/lib/Model/connection.server.js index 880835dae..097fa08b3 100644 --- a/lib/Model/connection.server.js +++ b/lib/Model/connection.server.js @@ -7,7 +7,7 @@ Model.prototype.createConnection = function(stream, logger) { this.root.shareConnection = new share.client.Connection(socket); socket.onopen(); this._set(['$connection', 'state'], 'connected'); - this._createChannel(); + this._finishCreateConnection(); }; /** diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 3a33ae820..9a0e3f302 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -81,20 +81,21 @@ Model.prototype._forSubscribable = function(argumentsObject, method) { Model.prototype.fetchDoc = function(collectionName, id, cb) { cb = this.wrapCallback(cb); - // Maintain a count of fetches so that we can unload the document when - // there are no remaining fetches or subscribes for that document + // Maintain a count of fetches so that we can unload the document + // when there are no remaining fetches or subscribes for that document this._context.fetchDoc(collectionName, id); this.root._fetchedDocs.increment(collectionName, id); // Fetch var doc = this.getOrCreateDoc(collectionName, id); - var loadCb = this._getLoadCb(doc, cb); - doc.shareDoc.fetch(loadCb); + doc.shareDoc.fetch(cb); }; Model.prototype.subscribeDoc = function(collectionName, id, cb) { cb = this.wrapCallback(cb); + // Maintain a count of subscribes so that we can unload the document + // when there are no remaining fetches or subscribes for that document this._context.subscribeDoc(collectionName, id); var count = this.root._subscribedDocs.increment(collectionName, id); // Already requested a subscribe, so just return @@ -102,35 +103,13 @@ Model.prototype.subscribeDoc = function(collectionName, id, cb) { // Subscribe var doc = this.getOrCreateDoc(collectionName, id); - var loadCb = this._getLoadCb(doc, cb); if (this.root.fetchOnly) { - doc.shareDoc.fetch(loadCb); + doc.shareDoc.fetch(cb); } else { - doc.shareDoc.subscribe(loadCb); + doc.shareDoc.subscribe(cb); } }; -Model.prototype._getLoadCb = function(doc, cb) { - // Version will be null before ever fetching or subscribing to doc data from - // the server. Once the doc already has a verison, ShareJS will emit change - // events as ops happen, so there is no need to emit load events - if (doc.shareDoc.version != null) { - return cb; - } - var model = this; - return function loadCb(err) { - if (err) return cb(err); - // If we created a doc locally, we will have been emitting events all along - // and no load event should be emitted. Also, if the shareDoc version is 0, - // that means it hasn't ever been created. Thus, there is no need to emit - // an event, since the doc snapshot will still be undefined - if (!doc.createdLocally && doc.shareDoc.version > 0) { - model.emit('load', [doc.collectionName, doc.id], [doc.get(), model._pass]); - } - cb(); - }; -}; - Model.prototype.unfetchDoc = function(collectionName, id, cb) { cb = this.wrapCallback(cb); this._context.unfetchDoc(collectionName, id); @@ -139,7 +118,7 @@ Model.prototype.unfetchDoc = function(collectionName, id, cb) { if (!this.root._fetchedDocs.get(collectionName, id)) return cb(); var model = this; - if (this.root.unloadDelay && !this._pass.$query) { + if (this.root.unloadDelay) { setTimeout(finishUnfetchDoc, this.root.unloadDelay); } else { finishUnfetchDoc(); @@ -160,7 +139,7 @@ Model.prototype.unsubscribeDoc = function(collectionName, id, cb) { if (!this.root._subscribedDocs.get(collectionName, id)) return cb(); var model = this; - if (this.root.unloadDelay && !this._pass.$query) { + if (this.root.unloadDelay) { setTimeout(finishUnsubscribeDoc, this.root.unloadDelay); } else { finishUnsubscribeDoc(); From f977dc5d4fecd7c84c5d5278ec29ac013d9e804b Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 12 May 2015 01:37:40 -0700 Subject: [PATCH 067/479] delay checking if query results need to be unloaded in case they are removed from one query and added to another right away --- lib/Model/Query.js | 52 +++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 581bc8403..c2a019de5 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -292,7 +292,6 @@ Query.prototype._shareSubscribe = function(options, cb) { ); this.shareQuery.on('insert', function(shareDocs, index) { var ids = resultsIds(shareDocs); - query._registerDocs(ids); query._addMapIds(ids); query.model._insert(query.idsSegments, index, ids); }); @@ -300,7 +299,6 @@ Query.prototype._shareSubscribe = function(options, cb) { var ids = resultsIds(shareDocs); query._removeMapIds(ids); query.model._remove(query.idsSegments, index, shareDocs.length); - query._maybeUnloadDocs(ids); }); this.shareQuery.on('move', function(shareDocs, from, to) { query.model._move(query.idsSegments, from, to, shareDocs.length); @@ -321,6 +319,19 @@ Query.prototype._removeMapIds = function(ids) { var id = ids[i]; delete this.idMap[id]; } + // Technically this isn't quite right and we might not wait the full unload + // delay if someone else calls maybeUnload for the same doc id. However, + // it is a lot easier to implement than delaying the removal until later and + // dealing with adds that might happen in the meantime. This will probably + // work to avoid thrashing subscribe/unsubscribe in expected cases + if (this.model.root.unloadDelay) { + var query = this; + setTimeout(function() { + query._maybeUnloadDocs(ids); + }, this.model.root.unloadDelay); + return; + } + this._maybeUnloadDocs(ids); }; Query.prototype._addMapIds = function(ids) { for (var i = ids.length; i--;) { @@ -328,6 +339,23 @@ Query.prototype._addMapIds = function(ids) { this.idMap[id] = true; } }; +Query.prototype._diffMapIds = function(ids) { + var addedIds = []; + var removedIds = []; + var newMap = {}; + for (var i = ids.length; i--;) { + var id = ids[i]; + newMap[id] = true; + if (this.idMap[id]) continue; + addedIds.push(id); + } + for (var id in this.idMap) { + if (newMap[id]) continue; + removedIds.push(id); + } + if (addedIds.length) this._addMapIds(addedIds); + if (removedIds.length) this._removeMapIds(removedIds); +}; Query.prototype._setExtra = function(extra) { if (extra === undefined) return; this.model._setDiffDeep(this.extraSegments, extra); @@ -337,26 +365,8 @@ Query.prototype._setResults = function(results) { this._setResultIds(ids); }; Query.prototype._setResultIds = function(ids) { - this._registerDocs(ids); - var previousIds = this.getIds(); - // Reset the map of ids, which is checked in maybeUnload - this.idMap = {}; - this._addMapIds(ids); + this._diffMapIds(ids); this.model._setArrayDiff(this.idsSegments, ids); - this._maybeUnloadDocs(previousIds); -}; -Query.prototype._registerDocs = function(ids) { - // Register documents with Racer - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var doc = this.model.getDoc(this.collectionName, id); - if (!doc) { - doc = this.model.getOrCreateDoc(this.collectionName, id); - var segments = [this.collectionName, id]; - var eventArgs = [doc.get(), this.model._pass]; - this.model.emit('load', segments, eventArgs); - } - } }; Query.prototype._maybeUnloadDocs = function(ids) { for (var i = 0; i < ids.length; i++) { From 6a6511ac480f7339cdab458e35adc067259c02de Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 12 May 2015 02:59:25 -0700 Subject: [PATCH 068/479] minimum share version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index daa78545f..57f1e6906 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "arraydiff": "^0.1.1", "deep-is": "^0.1.3", "uuid": "^2.0.1", - "share": "^0.7.31" + "share": "^0.7.32" }, "devDependencies": { "expect.js": "~0.3.1", From a32f1acdacca9f88bbdf182f5b0fd60acf1c4779 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 12 May 2015 02:59:46 -0700 Subject: [PATCH 069/479] 0.6.0-alpha39 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 57f1e6906..ecf3bfdf6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha38", + "version": "0.6.0-alpha39", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 8071ff71bda111ba2e3af0ba1b55852ed95768e9 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 20 May 2015 16:00:31 -0700 Subject: [PATCH 070/479] expose livedb as store.backend explicitly --- lib/Store.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Store.js b/lib/Store.js index 66255e666..f3e0f7cce 100644 --- a/lib/Store.js +++ b/lib/Store.js @@ -12,6 +12,9 @@ function Store(racer, options) { this.racer = racer; this.modelOptions = options && options.modelOptions; this.shareClient = options && share.server.createClient(options); + // Expose livedb directly, since we want to encourage use of + // store.backend.fetch and store.backend.queryFetch directly + this.backend = this.shareClient.backend; this.logger = options && options.logger; this.on('client', function(client) { var socket = new ClientSocket(client); From 32be0723e366df5ef6166aaaa4341ca0a5fab9b6 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 20 May 2015 16:53:42 -0700 Subject: [PATCH 071/479] 0.6.0-alpha40 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ecf3bfdf6..9afd68dcc 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha39", + "version": "0.6.0-alpha40", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From fa349b3281a14c9d3f68c066aa728d4264258da5 Mon Sep 17 00:00:00 2001 From: rachael Date: Mon, 25 May 2015 12:18:54 -0700 Subject: [PATCH 072/479] do not allow creates or deletes when disableSubmit --- lib/Model/RemoteDoc.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 781bacfe6..bf8a612cf 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -41,6 +41,8 @@ RemoteDoc.prototype._initShareDoc = function() { // Override submitOp to disable all writes and perform a dry-run if (model.root.debug.disableSubmit) { shareDoc.submitOp = function() {}; + shareDoc.create = function() {}; + shareDoc.del = function() {}; } // Subscribe to doc events shareDoc.on('op', function(op, isLocal) { From 4479e8bd83f6a811ebea2ceb256c3af1b89671fb Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 1 Jun 2015 15:59:19 -0700 Subject: [PATCH 073/479] support tracking of created docs for unloading remote docs --- lib/Model/CollectionCounter.js | 5 ++++- lib/Model/contexts.js | 17 +++++++++++++++-- lib/Model/unbundle.js | 8 ++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/Model/CollectionCounter.js b/lib/Model/CollectionCounter.js index e39287973..278c86fda 100644 --- a/lib/Model/CollectionCounter.js +++ b/lib/Model/CollectionCounter.js @@ -1,8 +1,11 @@ module.exports = CollectionCounter; function CollectionCounter() { - this.collections = {}; + this.reset(); } +CollectionCounter.prototype.reset = function() { + this.collections = {}; +}; CollectionCounter.prototype.get = function(collectionName, id) { var collection = this.collections[collectionName]; return collection && collection[id]; diff --git a/lib/Model/contexts.js b/lib/Model/contexts.js index 946e506d0..30b0a4a6c 100644 --- a/lib/Model/contexts.js +++ b/lib/Model/contexts.js @@ -48,6 +48,7 @@ function Context(model, id) { this.id = id; this.fetchedDocs = new CollectionCounter(); this.subscribedDocs = new CollectionCounter(); + this.createdDocs = new CollectionCounter(); this.fetchedQueries = new FetchedQueries(); this.subscribedQueries = new SubscribedQueries(); } @@ -55,10 +56,12 @@ function Context(model, id) { Context.prototype.toJSON = function() { var fetchedDocs = this.fetchedDocs.toJSON(); var subscribedDocs = this.subscribedDocs.toJSON(); - if (!fetchedDocs && !subscribedDocs) return; + var createdDocs = this.createdDocs.toJSON(); + if (!fetchedDocs && !subscribedDocs && !createdDocs) return; return { fetchedDocs: fetchedDocs, - subscribedDocs: subscribedDocs + subscribedDocs: subscribedDocs, + createdDocs: createdDocs }; }; @@ -74,6 +77,9 @@ Context.prototype.unfetchDoc = function(collectionName, id) { Context.prototype.unsubscribeDoc = function(collectionName, id) { this.subscribedDocs.decrement(collectionName, id); }; +Context.prototype.createDoc = function(collectionName, id) { + this.createdDocs.increment(collectionName, id); +}; Context.prototype.fetchQuery = function(query) { mapIncrement(this.fetchedQueries, query.hash); }; @@ -122,4 +128,11 @@ Context.prototype.unload = function() { while (count--) model.unsubscribeDoc(collectionName, id); } } + for (var collectionName in this.createdDocs.collections) { + var collection = this.createdDocs.collections[collectionName]; + for (var id in collection) { + model._maybeUnloadDoc(collectionName, id); + } + } + this.createdDocs.reset(); }; diff --git a/lib/Model/unbundle.js b/lib/Model/unbundle.js index 9508c181b..5a095b61f 100644 --- a/lib/Model/unbundle.js +++ b/lib/Model/unbundle.js @@ -38,6 +38,14 @@ Model.prototype.unbundle = function(data) { } } } + // Re-init createdDocs counts + for (var collectionName in contextData.createdDocs) { + var collection = contextData.createdDocs[collectionName]; + for (var id in collection) { + // Count value doesn't matter for tracking creates + contextModel._context.createDoc(collectionName, id); + } + } } if (this.shareConnection) this.shareConnection.bsEnd(); From d15a135a52a830615d52e3309084e4733b881ded Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 1 Jun 2015 16:00:18 -0700 Subject: [PATCH 074/479] only support creation of remote docs via model.add and remove implicit creation of remote docs --- lib/Model/Doc.js | 4 +++- lib/Model/LocalDoc.js | 11 +++++++++++ lib/Model/RemoteDoc.js | 29 +++++++++++++++++------------ lib/Model/mutators.js | 16 +++++++++++++++- test/Model/RemoteDoc.mocha.js | 4 +++- test/Model/events.mocha.js | 1 + 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/lib/Model/Doc.js b/lib/Model/Doc.js index 6eddfb6aa..ea892894c 100644 --- a/lib/Model/Doc.js +++ b/lib/Model/Doc.js @@ -7,7 +7,9 @@ function Doc(model, collectionName, id) { } Doc.prototype.path = function(segments) { - return this.collectionName + '.' + this.id + '.' + segments.join('.'); + var path = this.collectionName + '.' + this.id; + if (segments && segments.lenth) path += '.' + segments.join('.'); + return path; }; Doc.prototype._errorMessage = function(description, segments, value) { diff --git a/lib/Model/LocalDoc.js b/lib/Model/LocalDoc.js index a3b185f51..702e9accb 100644 --- a/lib/Model/LocalDoc.js +++ b/lib/Model/LocalDoc.js @@ -15,6 +15,17 @@ LocalDoc.prototype._updateCollectionData = function() { this.collectionData[this.id] = this.snapshot; }; +LocalDoc.prototype.create = function(value, cb) { + if (this.snapshot !== undefined) { + var message = this._errorMessage('create on local document with data', null, this.snapshot); + var err = new Error(message); + return cb(err); + } + this.snapshot = value; + this._updateCollectionData(); + cb(); +}; + LocalDoc.prototype.set = function(segments, value, cb) { function set(node, key) { var previous = node[key]; diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index bf8a612cf..7f7d22771 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -90,21 +90,26 @@ RemoteDoc.prototype._updateCollectionData = function() { this.collectionData[this.id] = snapshot; }; +RemoteDoc.prototype.create = function(value, cb) { + if (this.debugMutations) { + console.log('RemoteDoc create', this.path(), value); + } + // We copy the snapshot at time of create to prevent the id added outside + // of ShareJS from getting stored in the data + var snapshot = util.copy(value); + if (snapshot) delete snapshot.id; + this.shareDoc.create('json0', snapshot, cb); + // The id value will get added to the snapshot that was passed in + this.shareDoc.snapshot = value; + this._updateCollectionData(); + this.model._context.createDoc(this.collectionName, this.id); + return; +}; + RemoteDoc.prototype.set = function(segments, value, cb) { if (this.debugMutations) { console.log('RemoteDoc set', this.path(segments), value); } - if (segments.length === 0 && !this.shareDoc.type) { - // We copy the snapshot at time of create to prevent the id added outside - // of ShareJS from getting stored in the data - var snapshot = util.copy(value); - if (snapshot) delete snapshot.id; - this.shareDoc.create('json0', snapshot, cb); - // The id value will get added to the snapshot that was passed in - this.shareDoc.snapshot = value; - this._updateCollectionData(); - return; - } var previous = this._createImplied(segments); var lastSegment = segments[segments.length - 1]; if (previous instanceof ImpliedOp) { @@ -335,7 +340,7 @@ RemoteDoc.prototype.get = function(segments) { RemoteDoc.prototype._createImplied = function(segments) { if (!this.shareDoc.type) { - this.shareDoc.create('json0'); + throw new Error('mutation on uncreated remote document'); } var parent = this.shareDoc; var key = 'snapshot'; diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index 036afca6c..99251160d 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -96,7 +96,21 @@ Model.prototype._add = function(segments, value, cb) { } var id = value.id || this.id(); value.id = id; - this._set(segments.concat(id), value, cb); + segments = this._dereference(segments.concat(id)); + var model = this; + function add(doc, docSegments, fnCb) { + var previous; + if (docSegments.length) { + previous = doc.set(docSegments, value, fnCb); + } else { + doc.create(value, fnCb); + // On creating the doc, remote docs do a copy to add the id without + // it being stored in the database by ShareJS + value = doc.get(docSegments); + } + model.emit('change', segments, [value, previous, model._pass]); + } + this._mutate(segments, add, cb); return id; }; diff --git a/test/Model/RemoteDoc.mocha.js b/test/Model/RemoteDoc.mocha.js index 0ea1eb78d..058433926 100644 --- a/test/Model/RemoteDoc.mocha.js +++ b/test/Model/RemoteDoc.mocha.js @@ -8,7 +8,9 @@ describe('RemoteDoc', function() { var model = new Model; model.createConnection(); model.data.colors = {}; - return new RemoteDoc(model, 'colors', 'green'); + var doc = new RemoteDoc(model, 'colors', 'green'); + doc.create(); + return doc; }; describe('create', function() { it('should set the collectionName and id properties', function() { diff --git a/test/Model/events.mocha.js b/test/Model/events.mocha.js index d0ecad47e..2d0ba8ddb 100644 --- a/test/Model/events.mocha.js +++ b/test/Model/events.mocha.js @@ -125,6 +125,7 @@ describe('Model events', function() { var remoteModel = new Model(); remoteModel.createConnection(); var localDoc = localModel.getOrCreateDoc('colors', 'green'); + localDoc.create(); var remoteDoc = remoteModel.getOrCreateDoc('colors', 'green'); localDoc.shareDoc.on('op', function(op, isLocal) { remoteDoc._onOp(op); From ad0c50925d4cb03665568f662b549fe21ba0d481 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 1 Jun 2015 16:51:05 -0700 Subject: [PATCH 075/479] 0.6.0-alpha41 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9afd68dcc..e3c5955fd 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha40", + "version": "0.6.0-alpha41", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 4e6f3e40ec1d97f5766c3dbb1313bad4351b4993 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 4 Jun 2015 00:52:20 -0700 Subject: [PATCH 076/479] fix issue adding refs prematurely when a ref is set in a mutation event callback --- lib/Model/events.js | 17 +++++++++++++---- lib/Model/ref.js | 36 +++++++++++++++++++++--------------- lib/Model/refList.js | 29 ++++++++++++++++++++--------- 3 files changed, 54 insertions(+), 28 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index e000f2a7b..3caf4c7c4 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -2,7 +2,9 @@ var EventEmitter = require('events').EventEmitter; var util = require('../util'); var Model = require('./Model'); -// This map determines which events get re-emitted as an 'all' event +// These events are re-emitted as 'all' events, and they are queued up and +// emitted in sequence, so that events generated by other events are not +// seen in a different order by later listeners Model.MUTATOR_EVENTS = { change: true , insert: true @@ -11,6 +13,9 @@ Model.MUTATOR_EVENTS = { , load: true , unload: true }; +// These events queue changes in the same queue as mutator events but don't +// emit an 'all' event +Model.INTERNAL_EVENTS = {}; Model.INITS.push(function(model) { EventEmitter.call(this); @@ -74,7 +79,7 @@ Model.prototype.emit = function(type) { if (type === 'error') { return this._emit.apply(this, arguments); } - if (Model.MUTATOR_EVENTS[type]) { + if (Model.MUTATOR_EVENTS[type] || Model.INTERNAL_EVENTS[type]) { if (this._silent) return this; var segments = arguments[1]; var eventArgs = arguments[2]; @@ -84,14 +89,18 @@ Model.prototype.emit = function(type) { } this.root._mutatorEventQueue = []; this._emit(type, segments, eventArgs); - this._emit('all', segments, [type].concat(eventArgs)); + if (Model.MUTATOR_EVENTS[type]) { + this._emit('all', segments, [type].concat(eventArgs)); + } while (this.root._mutatorEventQueue.length) { var queued = this.root._mutatorEventQueue.shift(); type = queued[0]; segments = queued[1]; eventArgs = queued[2]; this._emit(type, segments, eventArgs); - this._emit('all', segments, [type].concat(eventArgs)); + if (Model.MUTATOR_EVENTS[type]) { + this._emit('all', segments, [type].concat(eventArgs)); + } } this.root._mutatorEventQueue = null; return this; diff --git a/lib/Model/ref.js b/lib/Model/ref.js index 864703661..424c182bc 100644 --- a/lib/Model/ref.js +++ b/lib/Model/ref.js @@ -3,7 +3,7 @@ var Model = require('./Model'); Model.INITS.push(function(model) { var root = model.root; - root._refs = new Refs(root); + root._refs = new Refs(); addIndexListeners(root); addListener(root, 'change', refChange); addListener(root, 'load', refLoad); @@ -11,8 +11,16 @@ Model.INITS.push(function(model) { addListener(root, 'insert', refInsert); addListener(root, 'remove', refRemove); addListener(root, 'move', refMove); + addInternalListener(root); }); +Model.INTERNAL_EVENTS.$addRef = true; +function addInternalListener(model) { + model.on('$addRef', function addRef(ref) { + model._refs.add(ref); + }); +} + function addIndexListeners(model) { model.on('insert', function refInsertIndex(segments, eventArgs) { var index = eventArgs[0]; @@ -60,13 +68,14 @@ function addIndexListeners(model) { model._refs.remove(from); ref.toSegments[segments.length] = '' + patched; ref.to = ref.toSegments.join('.'); - model._refs._add(ref); + model._refs.add(ref); } } } function refChange(model, dereferenced, eventArgs, segments) { var value = eventArgs[0]; + var pass = eventArgs[2]; // Detect if we are deleting vs. setting to undefined if (value === void 0) { var parentSegments = segments.slice(); @@ -178,15 +187,19 @@ Model.prototype.ref = function() { var toPath = this.path(to); // Make ref to reffable object, such as query or filter if (!toPath) return to.ref(fromPath); - var fromSegments = fromPath.split('.'); - if (fromSegments.length < 2) { + var ref = new Ref(this.root, fromPath, toPath, options); + if (ref.fromSegments.length < 2) { throw new Error('ref must be performed under a collection ' + 'and document id. Invalid path: ' + fromPath); } this.root._refs.remove(fromPath); + this.root._refLists.remove(fromPath); var value = this.get(to); - this._set(fromSegments, value); - this.root._refs.add(fromPath, toPath, options); + this._set(ref.fromSegments, value); + // We don't want the ref to get added until after the change event where its + // set gets emitted. Thus, emit a special internal event, since events get + // queued up and emitted serially + this.emit('$addRef', ref); return this.scope(fromPath); }; @@ -279,25 +292,18 @@ function Ref(model, from, to, options) { function FromMap() {} function ToMap() {} -function Refs(model) { - this.model = model; +function Refs() { this.fromMap = new FromMap(); this.toMap = new ToMap(); this.parentToMap = new ToMap(); } -Refs.prototype.add = function(from, to, options) { - var ref = new Ref(this.model, from, to, options); - return this._add(ref); -}; - -Refs.prototype._add = function(ref) { +Refs.prototype.add = function(ref) { this.fromMap[ref.from] = ref; listMapAdd(this.toMap, ref.to, ref); for (var i = 0, len = ref.parentTos.length; i < len; i++) { listMapAdd(this.parentToMap, ref.parentTos[i], ref); } - return ref; }; Refs.prototype.remove = function(from) { diff --git a/lib/Model/refList.js b/lib/Model/refList.js index 302d9daaf..681c27ddb 100644 --- a/lib/Model/refList.js +++ b/lib/Model/refList.js @@ -3,12 +3,20 @@ var Model = require('./Model'); Model.INITS.push(function(model) { var root = model.root; - root._refLists = new RefLists(root); + root._refLists = new RefLists(); for (var type in Model.MUTATOR_EVENTS) { addListener(root, type); } + addInternalListener(root); }); +Model.INTERNAL_EVENTS.$addRefList = true; +function addInternalListener(model) { + model.on('$addRefList', function addRef(refList) { + model._refLists.add(refList); + }); +} + function addListener(model, type) { model.on(type, refListListener); function refListListener(segments, eventArgs) { @@ -365,8 +373,14 @@ Model.prototype.refList = function() { toPath = this.path(to); } var idsPath = this.path(ids); - var refList = this.root._refLists.add(fromPath, toPath, idsPath, options); - this.pass({$refList: refList})._setArrayDiff(refList.fromSegments, refList.get()); + var refList = new RefList(this.root, fromPath, toPath, idsPath, options); + this.root._refs.remove(fromPath); + this.root._refLists.remove(fromPath); + this._setArrayDiff(refList.fromSegments, refList.get()); + // We don't want the ref to get added until after the change event where its + // set gets emitted. Thus, emit a special internal event, since events get + // queued up and emitted serially + this.emit('$addRefList', refList); return this.scope(fromPath); }; @@ -459,15 +473,12 @@ RefList.prototype.onMutation = function(type, segments, eventArgs) { function FromMap() {} -function RefLists(model) { - this.model = model; +function RefLists() { this.fromMap = new FromMap(); } -RefLists.prototype.add = function(from, to, ids, options) { - var refList = new RefList(this.model, from, to, ids, options); - this.fromMap[from] = refList; - return refList; +RefLists.prototype.add = function(refList) { + this.fromMap[refList.from] = refList; }; RefLists.prototype.remove = function(from) { From 8920a0bd74a84804cc9fa3f53452f04aec55b24f Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 4 Jun 2015 00:52:46 -0700 Subject: [PATCH 077/479] 0.6.0-alpha42 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e3c5955fd..503ea3b04 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha41", + "version": "0.6.0-alpha42", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 8a056a3e8f7b06c347c82cb08ff584406b4fe052 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 5 Jun 2015 17:38:25 -0700 Subject: [PATCH 078/479] don't remove refs on fromPath when creating refLists, since a ref to a refList is a valid combination; emit changeImmediate etc events and use those for ref & refList instead of delaying addition, which has problems in dereferencing --- lib/Model/events.js | 14 ++++---------- lib/Model/ref.js | 23 ++++++----------------- lib/Model/refList.js | 18 +++--------------- test/Model/ref.mocha.js | 27 +++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 42 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index 3caf4c7c4..b3b21540f 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -13,9 +13,6 @@ Model.MUTATOR_EVENTS = { , load: true , unload: true }; -// These events queue changes in the same queue as mutator events but don't -// emit an 'all' event -Model.INTERNAL_EVENTS = {}; Model.INITS.push(function(model) { EventEmitter.call(this); @@ -79,28 +76,25 @@ Model.prototype.emit = function(type) { if (type === 'error') { return this._emit.apply(this, arguments); } - if (Model.MUTATOR_EVENTS[type] || Model.INTERNAL_EVENTS[type]) { + if (Model.MUTATOR_EVENTS[type]) { if (this._silent) return this; var segments = arguments[1]; var eventArgs = arguments[2]; + this._emit(type + 'Immediate', segments, eventArgs); if (this.root._mutatorEventQueue) { this.root._mutatorEventQueue.push([type, segments, eventArgs]); return this; } this.root._mutatorEventQueue = []; this._emit(type, segments, eventArgs); - if (Model.MUTATOR_EVENTS[type]) { - this._emit('all', segments, [type].concat(eventArgs)); - } + this._emit('all', segments, [type].concat(eventArgs)); while (this.root._mutatorEventQueue.length) { var queued = this.root._mutatorEventQueue.shift(); type = queued[0]; segments = queued[1]; eventArgs = queued[2]; this._emit(type, segments, eventArgs); - if (Model.MUTATOR_EVENTS[type]) { - this._emit('all', segments, [type].concat(eventArgs)); - } + this._emit('all', segments, [type].concat(eventArgs)); } this.root._mutatorEventQueue = null; return this; diff --git a/lib/Model/ref.js b/lib/Model/ref.js index 424c182bc..d0d5e7787 100644 --- a/lib/Model/ref.js +++ b/lib/Model/ref.js @@ -11,18 +11,10 @@ Model.INITS.push(function(model) { addListener(root, 'insert', refInsert); addListener(root, 'remove', refRemove); addListener(root, 'move', refMove); - addInternalListener(root); }); -Model.INTERNAL_EVENTS.$addRef = true; -function addInternalListener(model) { - model.on('$addRef', function addRef(ref) { - model._refs.add(ref); - }); -} - function addIndexListeners(model) { - model.on('insert', function refInsertIndex(segments, eventArgs) { + model.on('insertImmediate', function refInsertIndex(segments, eventArgs) { var index = eventArgs[0]; var howMany = eventArgs[1].length; function patchInsert(refIndex) { @@ -30,7 +22,7 @@ function addIndexListeners(model) { } onIndexChange(segments, patchInsert); }); - model.on('remove', function refRemoveIndex(segments, eventArgs) { + model.on('removeImmediate', function refRemoveIndex(segments, eventArgs) { var index = eventArgs[0]; var howMany = eventArgs[1].length; function patchRemove(refIndex) { @@ -38,7 +30,7 @@ function addIndexListeners(model) { } onIndexChange(segments, patchRemove); }); - model.on('move', function refMoveIndex(segments, eventArgs) { + model.on('moveImmediate', function refMoveIndex(segments, eventArgs) { var from = eventArgs[0]; var to = eventArgs[1]; var howMany = eventArgs[2]; @@ -113,7 +105,7 @@ function refMove(model, dereferenced, eventArgs) { } function addListener(model, type, fn) { - model.on(type, refListener); + model.on(type + 'Immediate', refListener); function refListener(segments, eventArgs) { var pass = eventArgs[eventArgs.length - 1]; // Find cases where an event is emitted on a path where a reference @@ -195,11 +187,8 @@ Model.prototype.ref = function() { this.root._refs.remove(fromPath); this.root._refLists.remove(fromPath); var value = this.get(to); - this._set(ref.fromSegments, value); - // We don't want the ref to get added until after the change event where its - // set gets emitted. Thus, emit a special internal event, since events get - // queued up and emitted serially - this.emit('$addRef', ref); + ref.model._set(ref.fromSegments, value); + this.root._refs.add(ref); return this.scope(fromPath); }; diff --git a/lib/Model/refList.js b/lib/Model/refList.js index 681c27ddb..b6ffd5c84 100644 --- a/lib/Model/refList.js +++ b/lib/Model/refList.js @@ -7,18 +7,10 @@ Model.INITS.push(function(model) { for (var type in Model.MUTATOR_EVENTS) { addListener(root, type); } - addInternalListener(root); }); -Model.INTERNAL_EVENTS.$addRefList = true; -function addInternalListener(model) { - model.on('$addRefList', function addRef(refList) { - model._refLists.add(refList); - }); -} - function addListener(model, type) { - model.on(type, refListListener); + model.on(type + 'Immediate', refListListener); function refListListener(segments, eventArgs) { var pass = eventArgs[eventArgs.length - 1]; // Check for updates on or underneath paths @@ -374,13 +366,9 @@ Model.prototype.refList = function() { } var idsPath = this.path(ids); var refList = new RefList(this.root, fromPath, toPath, idsPath, options); - this.root._refs.remove(fromPath); this.root._refLists.remove(fromPath); - this._setArrayDiff(refList.fromSegments, refList.get()); - // We don't want the ref to get added until after the change event where its - // set gets emitted. Thus, emit a special internal event, since events get - // queued up and emitted serially - this.emit('$addRefList', refList); + refList.model._setArrayDiff(refList.fromSegments, refList.get()); + this.root._refLists.add(refList); return this.scope(fromPath); }; diff --git a/test/Model/ref.mocha.js b/test/Model/ref.mocha.js index bc616f1bc..cc1cab902 100644 --- a/test/Model/ref.mocha.js +++ b/test/Model/ref.mocha.js @@ -103,6 +103,33 @@ describe('ref', function() { expect(model.get('_page.myFavorite')).to.equal('#0f0'); }); }); + describe('event/add ordering', function() { + it('ref results are propogated when set in reponse to an event', function() { + var model = new Model; + model.on('change', '_page.start', function() { + model.ref('_page.myColor', '_page.color'); + model.ref('_page.yourColor', '_page.color'); + model.set('_page.yourColor', 'green'); + }); + model.set('_page.start', true); + expect(model.get('_page.color')).to.equal('green'); + expect(model.get('_page.myColor')).to.equal('green'); + }); + it('can create refList in event callback', function() { + var model = new Model; + model.on('change', '_page.start', function() { + model.set('_page.colors', { + red: '#f00', + green: '#0f0', + blue: '#00f' + }); + model.set('_page.ids', ['blue', 'green']); + model.refList('_page.list', '_page.colors', '_page.ids'); + }); + model.set('_page.start', true); + expect(model.get('_page.list')).to.eql(['#00f', '#0f0']); + }); + }); describe('updateIndices option', function() { it('updates a ref when an array insert happens at the `to` path', function() { var model = new Model; From cb44c8c962963fdab5a95ef8575331d5505fc44d Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 5 Jun 2015 17:38:37 -0700 Subject: [PATCH 079/479] 0.6.0-alpha43 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 503ea3b04..413ff2caf 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha42", + "version": "0.6.0-alpha43", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 15529072ea599c5d60ef0d05ebd5eb76e21bdf28 Mon Sep 17 00:00:00 2001 From: rachael Date: Fri, 17 Jul 2015 19:54:29 -0700 Subject: [PATCH 080/479] Add deep copy to avoid duplicate ops when composing a just created document --- lib/Model/RemoteDoc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 7f7d22771..660dd85c5 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -96,7 +96,7 @@ RemoteDoc.prototype.create = function(value, cb) { } // We copy the snapshot at time of create to prevent the id added outside // of ShareJS from getting stored in the data - var snapshot = util.copy(value); + var snapshot = util.deepCopy(value); if (snapshot) delete snapshot.id; this.shareDoc.create('json0', snapshot, cb); // The id value will get added to the snapshot that was passed in From 9c54c1b961cb56afb7c7187cf0b8388fd8ff6dc3 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 17 Jul 2015 19:58:03 -0700 Subject: [PATCH 081/479] 0.6.0-alpha44 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 413ff2caf..e2e3e70ac 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha43", + "version": "0.6.0-alpha44", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 5f4aa8c01a8d9cbc991c58fc4fbc867ef00f65cb Mon Sep 17 00:00:00 2001 From: Randal Truong Date: Tue, 18 Aug 2015 15:16:55 -0700 Subject: [PATCH 082/479] When creating implied operation detect case where we need to replace null values in an array --- lib/Model/RemoteDoc.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 660dd85c5..78bade44f 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -354,9 +354,15 @@ RemoteDoc.prototype._createImplied = function(segments) { value = value[key] = util.isArrayIndex(nextKey) ? [] : {}; } else { value = util.isArrayIndex(nextKey) ? [] : {}; - op = (Array.isArray(parent)) ? - new ListInsertOp(segments.slice(0, i - 2), key, value) : - new ObjectInsertOp(segments.slice(0, i - 1), value); + if (Array.isArray(parent)) { + if (key >= parent.length) { + op = new ListInsertOp(segments.slice(0, i - 2), key, value); + } else { + op = new ListReplaceOp(segments.slice(0, i - 2), key, node, value); + } + } else { + op = new ObjectInsertOp(segments.slice(0, i - 1), value); + } } node = value; } From a6cf4b41628188a6a82a7e1ee5ddcb454c24cb07 Mon Sep 17 00:00:00 2001 From: Randal Truong Date: Tue, 18 Aug 2015 16:03:31 -0700 Subject: [PATCH 083/479] 0.6.0-alpha45 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2e3e70ac..ca419c573 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha44", + "version": "0.6.0-alpha45", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From c457777909c80356f0951c98e2c930c05f22d4a4 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 5 Oct 2015 21:16:21 -0700 Subject: [PATCH 084/479] expose the ChildModel constructor on Model in case someone wants to call it in a super style --- lib/Model/Model.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Model/Model.js b/lib/Model/Model.js index 79f1f7385..272b9effb 100644 --- a/lib/Model/Model.js +++ b/lib/Model/Model.js @@ -23,6 +23,8 @@ Model.prototype._child = function() { return new ChildModel(this); }; +Model.ChildModel = ChildModel; + function ChildModel(model) { // Shared properties should be accessed via the root. This makes inheritance // cheap and easily extensible From 6454851bb8b3a202456f8aecdd93724e472dd112 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 5 Oct 2015 21:17:29 -0700 Subject: [PATCH 085/479] update dependency to sharedb 0.8 from sharejs 0.7 --- README.md | 6 +++--- lib/Model/connection.js | 4 ++-- lib/Model/connection.server.js | 4 ++-- lib/Store.js | 7 ++----- package.json | 8 ++++---- test/Model/MockConnectionModel.js | 4 ++-- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 1da1aeb8f..45b6c8379 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Racer -Racer is a realtime model synchronization engine for Node.js. By leveraging [ShareJS](http://sharejs.org/), multiple users can interact with the same data in realtime via Operational Transformation, a sophisticated conflict resolution algorithm that works in realtime and with offline clients. ShareJS also supports PubSub across multiple servers for horizontal scaling. Clients can express data subscriptions and fetches in terms of queries and specific documents, so different clients can be subscribed to different overlapping sets of data. On top of this sophisticated backend, Racer provides a simple model and event interface for writing application logic. +Racer is a realtime model synchronization engine for Node.js. By leveraging [ShareDB](https://github.com/share/sharedb), multiple users can interact with the same data in realtime via Operational Transformation, a sophisticated conflict resolution algorithm that works in realtime and with offline clients. ShareDB also supports PubSub across multiple servers for horizontal scaling. Clients can express data subscriptions and fetches in terms of queries and specific documents, so different clients can be subscribed to different overlapping sets of data. On top of this sophisticated backend, Racer provides a simple model and event interface for writing application logic. [![Build Status](https://travis-ci.org/derbyjs/racer.svg)](https://travis-ci.org/derbyjs/racer) @@ -23,7 +23,7 @@ There are currently two demos, which are included in the [racer-examples](https: * **Realtime query subscriptions** – Clients may subscribe to a limited set of information relevant to the current session. Both document and realtime query subscriptions are supported. Currently, arbitrary Mongo queries are supported. - * **Conflict resolution** – Leveraging ShareJS's JSON Operational Transformation algorithm, Racer will emit events that bring conflicting client states into eventual consistency. In addition to their synchronous API, model methods have callbacks for handling the resolved state after a server response. + * **Conflict resolution** – Leveraging ShareDB's JSON Operational Transformation algorithm, Racer will emit events that bring conflicting client states into eventual consistency. In addition to their synchronous API, model methods have callbacks for handling the resolved state after a server response. * **Immediate interaction** – Model methods appear to take effect immediately. Meanwhile, Racer sends updates to the server and checks for conflicts. If the updates are successful, they are stored and broadcast to other clients. @@ -31,7 +31,7 @@ There are currently two demos, which are included in the [racer-examples](https: * **Unified server and client interface** – The same model interface can be used on the server for initial page rendering and on the client for user interaction. Racer supports bundling models created on the server and reinitializing them in the same state in the browser. - * **Persistent storage** – Racer/ShareJS use [LiveDB](https://github.com/josephg/livedb) to keep a journal of all data operations, publish operations to multiple frontend servers, and automatically persist documents. It currently supports MongoDB, and it can be easily adapted to support other document stores. + * **Persistent storage** – Racer uses [ShareDB](https://github.com/share/sharedb) to keep a journal of all data operations, publish operations to multiple frontend servers, and automatically persist documents. It currently supports MongoDB, and it can be easily adapted to support other document stores. * **Access control** – (Under development) Racer will have hooks for access control to protect documents from malicious reads and writes. diff --git a/lib/Model/connection.js b/lib/Model/connection.js index 34395212e..2ef9c1382 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -1,4 +1,4 @@ -var share = require('share/lib/client'); +var ShareConnection = require('sharedb/lib/client').Connection; var Channel = require('../Channel'); var Model = require('./Model'); var LocalDoc = require('./LocalDoc'); @@ -11,7 +11,7 @@ Model.prototype.createConnection = function(bundle) { // The Share connection will bind to the socket by defining the onopen, // onmessage, etc. methods var model = this; - var shareConnection = this.root.shareConnection = new share.Connection(this.root.socket); + var shareConnection = this.root.shareConnection = new ShareConnection(this.root.socket); var states = ['connecting', 'connected', 'disconnected', 'stopped']; states.forEach(function(state) { shareConnection.on(state, function(reason) { diff --git a/lib/Model/connection.server.js b/lib/Model/connection.server.js index 097fa08b3..a06c2e8a9 100644 --- a/lib/Model/connection.server.js +++ b/lib/Model/connection.server.js @@ -1,10 +1,10 @@ -var share = require('share'); +var ShareConnection = require('sharedb/lib/client').Connection; var Model = require('./Model'); Model.prototype.createConnection = function(stream, logger) { var socket = new StreamSocket(this, stream, logger); this.root.socket = socket; - this.root.shareConnection = new share.client.Connection(socket); + this.root.shareConnection = new ShareConnection(socket); socket.onopen(); this._set(['$connection', 'state'], 'connected'); this._finishCreateConnection(); diff --git a/lib/Store.js b/lib/Store.js index f3e0f7cce..4db232fac 100644 --- a/lib/Store.js +++ b/lib/Store.js @@ -1,6 +1,6 @@ var Duplex = require('stream').Duplex; var EventEmitter = require('events').EventEmitter; -var share = require('share'); +var ShareDB = require('sharedb'); var util = require('./util'); var Channel = require('./Channel'); var Model = require('./Model'); @@ -11,10 +11,7 @@ function Store(racer, options) { EventEmitter.call(this); this.racer = racer; this.modelOptions = options && options.modelOptions; - this.shareClient = options && share.server.createClient(options); - // Expose livedb directly, since we want to encourage use of - // store.backend.fetch and store.backend.queryFetch directly - this.backend = this.shareClient.backend; + this.backend = (options && options.backend) || new ShareDB(options); this.logger = options && options.logger; this.on('client', function(client) { var socket = new ClientSocket(client); diff --git a/package.json b/package.json index ca419c573..6552d51e8 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,12 @@ "arraydiff": "^0.1.1", "deep-is": "^0.1.3", "uuid": "^2.0.1", - "share": "^0.7.32" + "sharedb": "^0.8.0" }, "devDependencies": { - "expect.js": "~0.3.1", - "jshint": "~2.4.4", - "mocha": "~1.17.1" + "expect.js": "^0.3.1", + "jshint": "^2.8.0", + "mocha": "^2.3.3" }, "optionalDependencies": {}, "engines": { diff --git a/test/Model/MockConnectionModel.js b/test/Model/MockConnectionModel.js index f029ff28f..a15b013ad 100644 --- a/test/Model/MockConnectionModel.js +++ b/test/Model/MockConnectionModel.js @@ -1,4 +1,4 @@ -var share = require('share'); +var ShareConnection = require('sharedb/lib/client').Connection; var Model = require('../../lib/Model'); module.exports = MockConnectionModel; @@ -18,5 +18,5 @@ MockConnectionModel.prototype.createConnection = function() { onopen: function() {}, onconnecting: function() {} }; - this.root.shareConnection = new share.client.Connection(socketMock); + this.root.shareConnection = new ShareConnection(socketMock); }; From 6cd9d0d3ffe10e44feadc0840c0b30e9c356819a Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 12 Oct 2015 14:08:03 -0700 Subject: [PATCH 086/479] sharedb 0.8 api updates --- lib/Model/Query.js | 78 ++++++++++++++++++++++-------------------- lib/Model/RemoteDoc.js | 2 +- lib/Store.js | 4 +-- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index c2a019de5..b76a608b3 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -21,13 +21,13 @@ Model.INITS.push(function(model) { }); }); -Model.prototype.query = function(collectionName, expression, source) { +Model.prototype.query = function(collectionName, expression, db) { if (typeof expression.path === 'function' || typeof expression !== 'object') { expression = this._splitPath(expression); } - var query = this.root._queries.get(collectionName, expression, source); + var query = this.root._queries.get(collectionName, expression, db); if (query) return query; - query = new Query(this, collectionName, expression, source); + query = new Query(this, collectionName, expression, db); this.root._queries.add(query); return query; }; @@ -43,9 +43,9 @@ Model.prototype._initQueries = function(items) { var ids = item[3] || []; var snapshots = item[4] || []; var versions = item[5] || []; - var source = item[6]; + var db = item[6]; var extra = item[7]; - var query = new Query(this, collectionName, expression, source); + var query = new Query(this, collectionName, expression, db); queries.add(query); query._addMapIds(ids); @@ -112,8 +112,8 @@ Queries.prototype.remove = function(query) { for (var key in collection) return; delete this.collections[collection]; }; -Queries.prototype.get = function(collectionName, expression, source) { - var hash = queryHash(collectionName, expression, source); +Queries.prototype.get = function(collectionName, expression, db) { + var hash = queryHash(collectionName, expression, db); return this.map[hash]; }; Queries.prototype.toJSON = function() { @@ -127,12 +127,12 @@ Queries.prototype.toJSON = function() { return out; }; -function Query(model, collectionName, expression, source) { +function Query(model, collectionName, expression, db) { this.model = model.pass({$query: this}); this.collectionName = collectionName; this.expression = expression; - this.source = source; - this.hash = queryHash(collectionName, expression, source); + this.db = db; + this.hash = queryHash(collectionName, expression, db); this.segments = ['$queries', this.hash]; this.idsSegments = ['$queries', this.hash, 'ids']; this.extraSegments = ['$queries', this.hash, 'extra']; @@ -177,7 +177,7 @@ Query.prototype.destroy = function() { this._maybeUnloadDocs(ids); }; -Query.prototype.sourceQuery = function() { +Query.prototype.dbQuery = function() { if (this.isPathQuery) { var ids = pathIds(this.model, this.expression); return {_id: {$in: ids}}; @@ -193,9 +193,9 @@ Query.prototype.fetch = function(cb) { if (!this.created) this.create(); - var shareDocs = collectionShareDocs(this.model, this.collectionName); - var options = {docMode: 'fetch', knownDocs: shareDocs}; - if (this.source) options.source = this.source; + var options = { + db: this.db + }; var query = this; function fetchCb(err, results, extra) { @@ -206,7 +206,7 @@ Query.prototype.fetch = function(cb) { } var shareQuery = this.model.root.shareConnection.createFetchQuery( this.collectionName, - this.sourceQuery(), + this.dbQuery(), options, fetchCb ); @@ -235,13 +235,14 @@ Query.prototype.subscribe = function(cb) { if (!this.created) this.create(); + var options = { + db: this.db, + results: this._getShareResults() + }; + // When doing server-side rendering, we actually do a fetch the first time // that subscribe is called, but keep track of the state as if subscribe // were called for proper initialization in the client - var shareDocs = collectionShareDocs(this.model, this.collectionName); - var options = {docMode: 'sub', knownDocs: shareDocs}; - if (this.source) options.source = this.source; - if (this.model.root.fetchOnly) { this._shareFetchedSubscribe(options, cb); } else { @@ -259,10 +260,9 @@ Query.prototype._shareFetchedSubscribe = function(options, cb) { query._setResults(results); query._flushSubscribeCallbacks(cb); } - options.docMode = 'fetch'; var shareQuery = this.model.root.shareConnection.createFetchQuery( this.collectionName, - this.sourceQuery(), + this.dbQuery(), options, fetchedSubscribeCb ); @@ -286,7 +286,7 @@ Query.prototype._shareSubscribe = function(options, cb) { } this.shareQuery = this.model.root.shareConnection.createSubscribeQuery( this.collectionName, - this.sourceQuery(), + this.dbQuery(), options, subscribeCb ); @@ -448,6 +448,22 @@ Query.prototype.unsubscribe = function(cb) { return this; }; +Query.prototype._getShareResults = function() { + var ids = this.model._get(this.idsSegments); + if (!ids) return; + + var collection = this.model.getCollection(this.collectionName); + if (!collection) return; + + var results = []; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var doc = collection.docs[id]; + results.push(doc && doc.shareDoc); + } + return results; +}; + Query.prototype.get = function() { var results = []; var data = this.model._get(this.segments); @@ -534,7 +550,7 @@ Query.prototype.serialize = function() { , ids , snapshots , versions - , this.source + , this.db , this.getExtra() ]; while (serialized[serialized.length - 1] == null) { @@ -543,8 +559,8 @@ Query.prototype.serialize = function() { return serialized; }; -function queryHash(collectionName, expression, source) { - var args = [collectionName, expression, source]; +function queryHash(collectionName, expression, db) { + var args = [collectionName, expression, db]; return JSON.stringify(args).replace(/\./g, '|'); } @@ -562,15 +578,3 @@ function pathIds(model, segments) { return (typeof value === 'string') ? [value] : (Array.isArray(value)) ? value.slice() : []; } - -function collectionShareDocs(model, collectionName) { - var collection = model.getCollection(collectionName); - if (!collection) return; - - var results = []; - for (var name in collection.docs) { - results.push(collection.docs[name].shareDoc); - } - - return results; -} diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 78bade44f..2ba6d5ee8 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -51,7 +51,7 @@ RemoteDoc.prototype._initShareDoc = function() { doc._updateCollectionData(); doc._onOp(op); }); - shareDoc.on('del', function(isLocal, previous) { + shareDoc.on('del', function(previous, isLocal) { // Calling the shareDoc.del method does not emit an operation event, // so we create the appropriate event here. if (isLocal) return; diff --git a/lib/Store.js b/lib/Store.js index 4db232fac..64c0d6bf8 100644 --- a/lib/Store.js +++ b/lib/Store.js @@ -12,6 +12,7 @@ function Store(racer, options) { this.racer = racer; this.modelOptions = options && options.modelOptions; this.backend = (options && options.backend) || new ShareDB(options); + this.shareClient = this.backend; // DEPRECATED this.logger = options && options.logger; this.on('client', function(client) { var socket = new ClientSocket(client); @@ -39,8 +40,7 @@ Store.prototype.createModel = function(options, req) { this.emit('modelStream', stream); model.createConnection(stream, this.logger); - var agent = this.shareClient.listen(stream, req); - this.emit('shareAgent', agent); + this.backend.listen(stream, req); return model; }; From dc6d525e22eecb20c068472cabe1d2c257fc476f Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 12 Oct 2015 16:13:07 -0700 Subject: [PATCH 087/479] 0.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6552d51e8..de206f964 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0-alpha45", + "version": "0.6.0", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 024a8143ac6a40627d5d451ffff6ca30610a78c1 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 4 Nov 2015 16:18:25 -0800 Subject: [PATCH 088/479] 0.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de206f964..378654d31 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.6.0", + "version": "0.7.0", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 386dde110653eae7d35fe3929b09b5fcec326068 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sat, 5 Dec 2015 22:22:39 -0800 Subject: [PATCH 089/479] sharedb api changes --- lib/Model/Query.js | 41 ++++++++++++----------------------- lib/Model/connection.js | 47 ++++------------------------------------- lib/Model/unbundle.js | 4 ++-- 3 files changed, 20 insertions(+), 72 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index b76a608b3..3832ecf95 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -204,15 +204,12 @@ Query.prototype.fetch = function(cb) { query._setResults(results); cb(); } - var shareQuery = this.model.root.shareConnection.createFetchQuery( + this.model.root.shareConnection.createFetchQuery( this.collectionName, this.dbQuery(), options, fetchCb ); - shareQuery.on('error', function(err) { - query.model._emitError(err, query.hash); - }); return this; }; @@ -252,34 +249,27 @@ Query.prototype.subscribe = function(cb) { return this; }; -Query.prototype._shareFetchedSubscribe = function(options, cb) { +Query.prototype._subscribeCb = function(cb) { var query = this; - function fetchedSubscribeCb(err, results, extra) { - if (err) return query._flushSubscribeCallbacks(cb, err); + return function subscribeCb(err, results, extra) { + if (err) return query._flushSubscribeCallbacks(err, cb); query._setExtra(extra); query._setResults(results); - query._flushSubscribeCallbacks(cb); - } - var shareQuery = this.model.root.shareConnection.createFetchQuery( + query._flushSubscribeCallbacks(null, cb); + }; +}; + +Query.prototype._shareFetchedSubscribe = function(options, cb) { + this.model.root.shareConnection.createFetchQuery( this.collectionName, this.dbQuery(), options, - fetchedSubscribeCb + this._subscribeCb(cb) ); - shareQuery.on('error', function(err) { - query.model._emitError(err, query.hash); - }); }; Query.prototype._shareSubscribe = function(options, cb) { var query = this; - function subscribeCb(err, results, extra) { - if (err) return query._flushSubscribeCallbacks(cb, err); - query._setExtra(extra); - // Results are not set, since a change event will already - // have been emitted with the same results - query._flushSubscribeCallbacks(cb); - } // Sanity check, though this shouldn't happen if (this.shareQuery) { this.shareQuery.destroy(); @@ -288,7 +278,7 @@ Query.prototype._shareSubscribe = function(options, cb) { this.collectionName, this.dbQuery(), options, - subscribeCb + this._subscribeCb(cb) ); this.shareQuery.on('insert', function(shareDocs, index) { var ids = resultsIds(shareDocs); @@ -303,9 +293,6 @@ Query.prototype._shareSubscribe = function(options, cb) { this.shareQuery.on('move', function(shareDocs, from, to) { query.model._move(query.idsSegments, from, to, shareDocs.length); }); - this.shareQuery.on('change', function(shareDocs) { - query._setResults(shareDocs); - }); this.shareQuery.on('extra', function(extra) { query.model._setDiffDeep(query.extraSegments, extra); }); @@ -378,7 +365,7 @@ Query.prototype._maybeUnloadDocs = function(ids) { // Flushes `_pendingSubscribeCallbacks`, calling each callback in the array, // with an optional error to pass into each. `_pendingSubscribeCallbacks` will // be empty after this runs. -Query.prototype._flushSubscribeCallbacks = function(cb, err) { +Query.prototype._flushSubscribeCallbacks = function(err, cb) { cb(err); var pendingCallback; while (pendingCallback = this._pendingSubscribeCallbacks.shift()) { @@ -568,7 +555,7 @@ function resultsIds(results) { var ids = []; for (var i = 0; i < results.length; i++) { var shareDoc = results[i]; - ids.push(shareDoc.name); + ids.push(shareDoc.id); } return ids; } diff --git a/lib/Model/connection.js b/lib/Model/connection.js index 2ef9c1382..773490b94 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -32,7 +32,7 @@ Model.prototype._finishCreateConnection = function() { // Share docs can be created by queries, so we need to register them // with Racer as soon as they are created to capture their events this.shareConnection.on('doc', function(shareDoc) { - model.getOrCreateDoc(shareDoc.collection, shareDoc.name); + model.getOrCreateDoc(shareDoc.collection, shareDoc.id); }); this.root.channel = new Channel(this.root.socket); @@ -72,50 +72,11 @@ Model.prototype._getDocConstructor = function(name) { }; Model.prototype.hasPending = function() { - return !!this._firstShareDoc(hasPending); + return this.shareConnection.hasPending(); }; - Model.prototype.hasWritePending = function() { - return !!this._firstShareDoc(hasWritePending); + return this.shareConnection.hasWritePending(); }; - Model.prototype.whenNothingPending = function(cb) { - var shareDoc = this._firstShareDoc(hasPending); - if (shareDoc) { - // If a document is found with a pending operation, wait for it to emit - // that nothing is pending anymore, and then recheck all documents again. - // We have to recheck all documents, just in case another mutation has - // been made in the meantime as a result of an event callback - var model = this; - shareDoc.once('nothing pending', function retryNothingPending() { - process.nextTick(function(){ - model.whenNothingPending(cb); - }); - }); - return; - } - // Call back when no Share documents have pending operations - process.nextTick(cb); -}; - -function hasPending(shareDoc) { - return shareDoc.hasPending(); -} -function hasWritePending(shareDoc) { - return shareDoc.inflightData != null || !!shareDoc.pendingData.length; -} - -Model.prototype._firstShareDoc = function(fn) { - // Loop through all of the documents on the share connection, and return the - // first document encountered with that matches the provided test function - var collections = this.root.shareConnection.collections; - for (var collectionName in collections) { - var collection = collections[collectionName]; - for (var id in collection) { - var shareDoc = collection[id]; - if (shareDoc && fn(shareDoc)) { - return shareDoc; - } - } - } + return this.shareConnection.whenNothingPending(cb); }; diff --git a/lib/Model/unbundle.js b/lib/Model/unbundle.js index 5a095b61f..afb9917ee 100644 --- a/lib/Model/unbundle.js +++ b/lib/Model/unbundle.js @@ -1,7 +1,7 @@ var Model = require('./Model'); Model.prototype.unbundle = function(data) { - if (this.shareConnection) this.shareConnection.bsStart(); + if (this.shareConnection) this.shareConnection.startBulk(); // Re-create and subscribe queries; re-create documents associated with queries this._initQueries(data.queries); @@ -48,7 +48,7 @@ Model.prototype.unbundle = function(data) { } } - if (this.shareConnection) this.shareConnection.bsEnd(); + if (this.shareConnection) this.shareConnection.endBulk(); // Re-create refs for (var i = 0; i < data.refs.length; i++) { From 5ceb13583adb494e7f43c3ace9c1185de7b185db Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sat, 5 Dec 2015 22:24:56 -0800 Subject: [PATCH 090/479] remove support for path queries, which were buggy; bulk document fetching and subscribing support is a better solution since optimizations have been made in share db --- lib/Model/Query.js | 50 +++------------------------------------------- 1 file changed, 3 insertions(+), 47 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 3832ecf95..171796ca7 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -6,25 +6,9 @@ module.exports = Query; Model.INITS.push(function(model) { model.root._queries = new Queries(); - if (model.root.fetchOnly) return; - model.on('all', function(segments) { - var collectionName = segments[0]; - var map = model.root._queries.collections[collectionName]; - if (!map) return; - for (var hash in map) { - var query = map[hash]; - if (query.isPathQuery && query.shareQuery && util.mayImpact(query.expression, segments)) { - var ids = pathIds(model, query.expression); - query._setResultIds(ids); - } - } - }); }); Model.prototype.query = function(collectionName, expression, db) { - if (typeof expression.path === 'function' || typeof expression !== 'object') { - expression = this._splitPath(expression); - } var query = this.root._queries.get(collectionName, expression, db); if (query) return query; query = new Query(this, collectionName, expression, db); @@ -50,19 +34,6 @@ Model.prototype._initQueries = function(items) { query._addMapIds(ids); this._set(query.idsSegments, ids); - - // This is a bit of a hack, but it should be correct. Given that queries - // are initialized first, the ids path is probably not set yet, but it will - // be used to generate the query. Therefore, we assume that the value of - // path will be the ids that the query results were on the server. There - // are probably some really odd edge cases where this doesn't work, and - // a more correct thing to do would be to get the actual value for the - // path before creating the query subscription. This feature should - // probably be rethought. - if (query.isPathQuery && expression.length > 0 && this._isLocal(expression[0])) { - this._setNull(expression, ids.slice()); - } - query._setExtra(extra); for (var j = 0; j < snapshots.length; j++) { @@ -136,7 +107,6 @@ function Query(model, collectionName, expression, db) { this.segments = ['$queries', this.hash]; this.idsSegments = ['$queries', this.hash, 'ids']; this.extraSegments = ['$queries', this.hash, 'extra']; - this.isPathQuery = Array.isArray(expression); this._pendingSubscribeCallbacks = []; @@ -177,14 +147,6 @@ Query.prototype.destroy = function() { this._maybeUnloadDocs(ids); }; -Query.prototype.dbQuery = function() { - if (this.isPathQuery) { - var ids = pathIds(this.model, this.expression); - return {_id: {$in: ids}}; - } - return this.expression; -}; - Query.prototype.fetch = function(cb) { cb = this.model.wrapCallback(cb); this.model._context.fetchQuery(this); @@ -206,7 +168,7 @@ Query.prototype.fetch = function(cb) { } this.model.root.shareConnection.createFetchQuery( this.collectionName, - this.dbQuery(), + this.expression, options, fetchCb ); @@ -262,7 +224,7 @@ Query.prototype._subscribeCb = function(cb) { Query.prototype._shareFetchedSubscribe = function(options, cb) { this.model.root.shareConnection.createFetchQuery( this.collectionName, - this.dbQuery(), + this.expression, options, this._subscribeCb(cb) ); @@ -276,7 +238,7 @@ Query.prototype._shareSubscribe = function(options, cb) { } this.shareQuery = this.model.root.shareConnection.createSubscribeQuery( this.collectionName, - this.dbQuery(), + this.expression, options, this._subscribeCb(cb) ); @@ -559,9 +521,3 @@ function resultsIds(results) { } return ids; } - -function pathIds(model, segments) { - var value = model._get(segments); - return (typeof value === 'string') ? [value] : - (Array.isArray(value)) ? value.slice() : []; -} From 4c25753c7be92cc768ed806b8f3443b48ce0c295 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sat, 5 Dec 2015 22:27:14 -0800 Subject: [PATCH 091/479] make it so that calls to model.subscribe, fetch, and unsubscribe, with multiple arguments produce bulk messages --- lib/Model/subscriptions.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 9a0e3f302..d65ee9052 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -54,6 +54,7 @@ Model.prototype._forSubscribable = function(argumentsObject, method) { var finished = group(); var docMethod = method + 'Doc'; + this.root.shareConnection.startBulk(); for (var i = 0; i < args.length; i++) { var item = args[i]; if (item instanceof Query) { @@ -75,6 +76,7 @@ Model.prototype._forSubscribable = function(argumentsObject, method) { } } } + this.root.shareConnection.endBulk(); process.nextTick(finished); }; From 69b10808eb339229340403cb2bf0194dc3a2cba3 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sat, 5 Dec 2015 22:27:58 -0800 Subject: [PATCH 092/479] remove hacky support for subscribing to entire collection with a path; must explicitly make a query --- lib/Model/subscriptions.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index d65ee9052..732b65100 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -64,15 +64,9 @@ Model.prototype._forSubscribable = function(argumentsObject, method) { if (segments.length === 2) { // Do the appropriate method for a single document. this[docMethod](segments[0], segments[1], group()); - } else if (segments.length === 1) { - // Make a query to an entire collection. - var query = this.query(segments[0], {}); - query[method](group()); - } else if (segments.length === 0) { - group()(new Error('No path specified for ' + method)); } else { - group()(new Error('Cannot ' + method + ' to a path within a document: ' + - segments.join('.'))); + var message = 'Cannot ' + method + ' to path: ' + segments.join('.'); + group()(new Error(message)); } } } From 66c81cd7e418ffd1916c07dd7df16c35ee3e9dae Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 6 Dec 2015 16:31:22 -0800 Subject: [PATCH 093/479] update sharedb dep --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 378654d31..1ab500f3f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "arraydiff": "^0.1.1", "deep-is": "^0.1.3", "uuid": "^2.0.1", - "sharedb": "^0.8.0" + "sharedb": "^0.9.0" }, "devDependencies": { "expect.js": "^0.3.1", From 1835c15f11862a0a6944a92a49d371ab7be40c71 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 6 Dec 2015 16:32:04 -0800 Subject: [PATCH 094/479] 0.7.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ab500f3f..ffdb83f76 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.7.0", + "version": "0.7.1", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 38f1312c6cdf80b963513e59f42ae389dcbe81fc Mon Sep 17 00:00:00 2001 From: Will Riley Date: Tue, 8 Dec 2015 14:06:56 -0800 Subject: [PATCH 095/479] Fix bug where shareConnection isn't set on scoped models --- lib/Model/connection.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Model/connection.js b/lib/Model/connection.js index 773490b94..9c6661dea 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -26,12 +26,12 @@ Model.prototype.createConnection = function(bundle) { Model.prototype._finishCreateConnection = function() { var model = this; - this.shareConnection.on('error', function(err, data) { + this.root.shareConnection.on('error', function(err, data) { model._emitError(err, data); }); // Share docs can be created by queries, so we need to register them // with Racer as soon as they are created to capture their events - this.shareConnection.on('doc', function(shareDoc) { + this.root.shareConnection.on('doc', function(shareDoc) { model.getOrCreateDoc(shareDoc.collection, shareDoc.id); }); @@ -72,11 +72,11 @@ Model.prototype._getDocConstructor = function(name) { }; Model.prototype.hasPending = function() { - return this.shareConnection.hasPending(); + return this.root.shareConnection.hasPending(); }; Model.prototype.hasWritePending = function() { - return this.shareConnection.hasWritePending(); + return this.root.shareConnection.hasWritePending(); }; Model.prototype.whenNothingPending = function(cb) { - return this.shareConnection.whenNothingPending(cb); + return this.root.shareConnection.whenNothingPending(cb); }; From f4ab92e1940a2769f8dbbdfb9f0f19a7283d5c6e Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 8 Dec 2015 14:51:57 -0800 Subject: [PATCH 096/479] 0.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ffdb83f76..e8603a4ee 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.7.1", + "version": "0.7.2", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From d8e15d6000fc1656b615f7bc4121cfff49e21ecb Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 16 Dec 2015 15:22:56 -0800 Subject: [PATCH 097/479] breaking sharedb api changes; don't serialize doc type when bundling remote docs of the default type --- lib/Model/LocalDoc.js | 16 +++++++------- lib/Model/Query.js | 41 ++++++++++++++++++----------------- lib/Model/RemoteDoc.js | 47 ++++++++++++++++++++--------------------- lib/Model/bundle.js | 21 ++++++++++++------ lib/Model/connection.js | 9 +++----- package.json | 2 +- 6 files changed, 71 insertions(+), 65 deletions(-) diff --git a/lib/Model/LocalDoc.js b/lib/Model/LocalDoc.js index 702e9accb..93eb28ad2 100644 --- a/lib/Model/LocalDoc.js +++ b/lib/Model/LocalDoc.js @@ -3,25 +3,25 @@ var util = require('../util'); module.exports = LocalDoc; -function LocalDoc(model, collectionName, id, snapshot) { +function LocalDoc(model, collectionName, id, data) { Doc.call(this, model, collectionName, id); - this.snapshot = snapshot; + this.data = data; this._updateCollectionData(); } LocalDoc.prototype = new Doc(); LocalDoc.prototype._updateCollectionData = function() { - this.collectionData[this.id] = this.snapshot; + this.collectionData[this.id] = this.data; }; LocalDoc.prototype.create = function(value, cb) { - if (this.snapshot !== undefined) { - var message = this._errorMessage('create on local document with data', null, this.snapshot); + if (this.data !== undefined) { + var message = this._errorMessage('create on local document with data', null, this.data); var err = new Error(message); return cb(err); } - this.snapshot = value; + this.data = value; this._updateCollectionData(); cb(); }; @@ -160,7 +160,7 @@ LocalDoc.prototype.stringRemove = function(segments, index, howMany, cb) { }; LocalDoc.prototype.get = function(segments) { - return util.lookup(segments, this.snapshot); + return util.lookup(segments, this.data); }; /** @@ -170,7 +170,7 @@ LocalDoc.prototype.get = function(segments) { */ LocalDoc.prototype._createImplied = function(segments, fn) { var node = this; - var key = 'snapshot'; + var key = 'data'; var i = 0; var nextKey = segments[i++]; while (nextKey != null) { diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 171796ca7..94d6acc6d 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -1,6 +1,7 @@ var util = require('../util'); var Model = require('./Model'); var arrayDiff = require('arraydiff'); +var defaultType = require('sharedb').types.defaultType; module.exports = Query; @@ -25,10 +26,9 @@ Model.prototype._initQueries = function(items) { var collectionName = item[1]; var expression = item[2]; var ids = item[3] || []; - var snapshots = item[4] || []; - var versions = item[5] || []; - var db = item[6]; - var extra = item[7]; + var results = item[4] || []; + var db = item[5]; + var extra = item[6]; var query = new Query(this, collectionName, expression, db); queries.add(query); @@ -36,13 +36,16 @@ Model.prototype._initQueries = function(items) { this._set(query.idsSegments, ids); query._setExtra(extra); - for (var j = 0; j < snapshots.length; j++) { - var snapshot = snapshots[j]; - if (!snapshot) continue; + for (var j = 0; j < results.length; j++) { + var result = results[j]; + if (!result) continue; var id = ids[j]; - var version = versions[j]; - var data = {data: snapshot, v: version, type: 'json0'}; - this.getOrCreateDoc(collectionName, id, data); + var snapshot = { + data: result[0], + v: result[1], + type: result[2] + }; + this.getOrCreateDoc(collectionName, id, snapshot); } for (var j = 0; j < counts.length; j++) { @@ -459,20 +462,21 @@ Query.prototype.refExtra = function(from, relPath) { Query.prototype.serialize = function() { var ids = this.getIds(); var collection = this.model.getCollection(this.collectionName); - var snapshots, versions; + var results; if (collection) { - snapshots = []; - versions = []; + results = []; for (var i = 0; i < ids.length; i++) { var id = ids[i]; var doc = collection.docs[id]; if (doc) { - snapshots.push(doc.shareDoc.snapshot); - versions.push(doc.shareDoc.version); collection.remove(id); + var result = [doc.shareDoc.data, doc.shareDoc.version]; + if (doc.shareDoc.type !== defaultType) { + result.push(doc.shareDoc.type && doc.shareDoc.type.name); + } + results.push(result); } else { - snapshots.push(0); - versions.push(0); + results.push(0); } } } @@ -497,8 +501,7 @@ Query.prototype.serialize = function() { , this.collectionName , this.expression , ids - , snapshots - , versions + , results , this.db , this.getExtra() ]; diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 2ba6d5ee8..a5bc77773 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -11,11 +11,11 @@ var util = require('../util'); module.exports = RemoteDoc; -function RemoteDoc(model, collectionName, id, data, collection) { - // This is a bit messy, but we have to immediately register this doc - // on the collection that added it, so that when we create the shareDoc - // and the shareConnection emits the 'doc' event, we'll find this doc - // instead of creating a new one +function RemoteDoc(model, collectionName, id, snapshot, collection) { + // This is a bit messy, but we have to immediately register this doc on the + // collection that added it, so that when we create the shareDoc and the + // shareConnection emits the 'doc' event, we'll find this doc instead of + // creating a new one if (collection) collection.docs[id] = this; Doc.call(this, model, collectionName, id); @@ -24,7 +24,8 @@ function RemoteDoc(model, collectionName, id, data, collection) { // Get or create the Share document. Note that we must have already added // this doc to the collection to avoid creating a duplicate doc - this.shareDoc = model.root.shareConnection.get(collectionName, id, data); + this.shareDoc = model.root.shareConnection.get(collectionName, id); + this.shareDoc.ingestSnapshot(snapshot); this._initShareDoc(); } @@ -36,8 +37,6 @@ RemoteDoc.prototype._initShareDoc = function() { var collectionName = this.collectionName; var id = this.id; var shareDoc = this.shareDoc; - // Needed to follow along events properly - shareDoc.incremental = true; // Override submitOp to disable all writes and perform a dry-run if (model.root.debug.disableSubmit) { shareDoc.submitOp = function() {}; @@ -66,15 +65,15 @@ RemoteDoc.prototype._initShareDoc = function() { // for them. if (isLocal) return; doc._updateCollectionData(); - var value = shareDoc.snapshot; + var value = shareDoc.data; model.emit('change', [collectionName, id], [value, void 0, model._pass]); }); shareDoc.on('error', function(err) { model._emitError(err, collectionName + '.' + id); }); - shareDoc.on('ready', function() { + shareDoc.on('load', function() { doc._updateCollectionData(); - var value = shareDoc.snapshot; + var value = shareDoc.data; // If we subscribe to an uncreated document, no need to emit 'load' event if (value === undefined) return; model.emit('load', [collectionName, id], [value, model._pass]); @@ -83,24 +82,24 @@ RemoteDoc.prototype._initShareDoc = function() { }; RemoteDoc.prototype._updateCollectionData = function() { - var snapshot = this.shareDoc.snapshot; - if (typeof snapshot === 'object' && !Array.isArray(snapshot) && snapshot !== null) { - snapshot.id = this.id; + var data = this.shareDoc.data; + if (typeof data === 'object' && !Array.isArray(data) && data !== null) { + data.id = this.id; } - this.collectionData[this.id] = snapshot; + this.collectionData[this.id] = data; }; RemoteDoc.prototype.create = function(value, cb) { if (this.debugMutations) { console.log('RemoteDoc create', this.path(), value); } - // We copy the snapshot at time of create to prevent the id added outside - // of ShareJS from getting stored in the data - var snapshot = util.deepCopy(value); - if (snapshot) delete snapshot.id; - this.shareDoc.create('json0', snapshot, cb); - // The id value will get added to the snapshot that was passed in - this.shareDoc.snapshot = value; + // We copy the snapshot data at time of create to prevent the id added + // outside of ShareJS from getting stored in the data + var data = util.deepCopy(value); + if (data) delete data.id; + this.shareDoc.create(data, cb); + // The id value will get added to the data that was passed in + this.shareDoc.data = value; this._updateCollectionData(); this.model._context.createDoc(this.collectionName, this.id); return; @@ -335,7 +334,7 @@ RemoteDoc.prototype.stringRemove = function(segments, index, howMany, cb) { }; RemoteDoc.prototype.get = function(segments) { - return util.lookup(segments, this.shareDoc.snapshot); + return util.lookup(segments, this.shareDoc.data); }; RemoteDoc.prototype._createImplied = function(segments) { @@ -343,7 +342,7 @@ RemoteDoc.prototype._createImplied = function(segments) { throw new Error('mutation on uncreated remote document'); } var parent = this.shareDoc; - var key = 'snapshot'; + var key = 'data'; var node = parent[key]; var i = 0; var nextKey = segments[i++]; diff --git a/lib/Model/bundle.js b/lib/Model/bundle.js index f7aad0462..abcc9329e 100644 --- a/lib/Model/bundle.js +++ b/lib/Model/bundle.js @@ -1,4 +1,5 @@ var Model = require('./Model'); +var defaultType = require('sharedb').types.defaultType; Model.BUNDLE_TIMEOUT = 10 * 1000; @@ -72,13 +73,19 @@ function serializeCollections(root) { for (var id in collection.docs) { var doc = collection.docs[id]; var shareDoc = doc.shareDoc; - out[collectionName][id] = (shareDoc) ? - { - v: shareDoc.version - , data: shareDoc.snapshot - , type: shareDoc.type && shareDoc.type.name - } : - doc.snapshot; + var snapshot; + if (shareDoc) { + snapshot = { + v: shareDoc.version, + data: shareDoc.data + }; + if (shareDoc.type !== defaultType) { + snapshot.type = doc.shareDoc.type && doc.shareDoc.type.name; + } + } else { + snapshot = doc.data; + } + out[collectionName][id] = snapshot; } } return out; diff --git a/lib/Model/connection.js b/lib/Model/connection.js index 9c6661dea..0ad383b77 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -12,12 +12,9 @@ Model.prototype.createConnection = function(bundle) { // onmessage, etc. methods var model = this; var shareConnection = this.root.shareConnection = new ShareConnection(this.root.socket); - var states = ['connecting', 'connected', 'disconnected', 'stopped']; - states.forEach(function(state) { - shareConnection.on(state, function(reason) { - model._setDiff(['$connection', 'state'], state); - model._setDiff(['$connection', 'reason'], reason); - }); + shareConnection.on('state', function(state, reason) { + model._setDiff(['$connection', 'state'], state); + model._setDiff(['$connection', 'reason'], reason); }); this._set(['$connection', 'state'], 'connected'); diff --git a/package.json b/package.json index e8603a4ee..7d64c1e57 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "arraydiff": "^0.1.1", "deep-is": "^0.1.3", "uuid": "^2.0.1", - "sharedb": "^0.9.0" + "sharedb": "^0.10.0" }, "devDependencies": { "expect.js": "^0.3.1", From 936a1d1855045fd7d0cb8cfd3e4c6a47ee1f5d44 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 16 Dec 2015 15:25:36 -0800 Subject: [PATCH 098/479] add support for subtypeSubmit on remote docs; not implemented on local docs --- lib/Model/RemoteDoc.js | 34 ++++++++++++++++++++++++++ lib/Model/mutators.js | 55 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index a5bc77773..62232c6a8 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -333,6 +333,21 @@ RemoteDoc.prototype.stringRemove = function(segments, index, howMany, cb) { return previous; }; +RemoteDoc.prototype.subtypeSubmit = function(segments, subtype, subtypeOp, cb) { + if (this.debugMutations) { + console.log('RemoteDoc subtypeSubmit', this.path(segments), subtype, subtypeOp); + } + var previous = this._createImplied(segments); + if (previous instanceof ImpliedOp) { + this.shareDoc.submitOp(previous.op); + previous = void 0; + } + var op = new SubtypeOp(segments, subtype, subtypeOp); + this.shareDoc.submitOp(op, cb); + this._updateCollectionData(); + return previous; +}; + RemoteDoc.prototype.get = function(segments) { return util.lookup(segments, this.shareDoc.data); }; @@ -466,6 +481,20 @@ RemoteDoc.prototype._onOp = function(op) { var value = this.get(item.p); var previous = value - item.na; model.emit('change', segments, [value, previous, model._pass]); + + // SubtypeOp + } else if (defined(item.t)) { + var value = this.get(item.p); + // Since this is generic to all subtypes, we don't know how to get a copy + // of the previous value efficiently. We could make a copy eagerly, but + // given that embedded types are likely to be used for custom editors, + // we'll assume they primarily use the returned op and are unlikely to + // need the previous snapshot data + var previous = void 0; + var type = item.t; + var op = item.o; + var pass = model.pass({$subtype: {type: type, op: op}})._pass; + model.emit('change', segments, [value, previous, pass]); } }; @@ -511,6 +540,11 @@ function IncrementOp(segments, byNumber) { this.p = util.castSegments(segments); this.na = byNumber; } +function SubtypeOp(segments, subtype, subtypeOp) { + this.p = util.castSegments(segments); + this.t = subtype; + this.o = subtypeOp; +} function defined(value) { return value !== void 0; diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index 99251160d..c9cc44e8e 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -277,7 +277,7 @@ Model.prototype._unshift = function(segments, value, cb) { Model.prototype.insert = function() { var subpath, index, values, cb; - if (arguments.length === 1) { + if (arguments.length < 2) { throw new Error('Not enough arguments for insert'); } else if (arguments.length === 2) { index = arguments[0]; @@ -376,7 +376,7 @@ Model.prototype._shift = function(segments, cb) { Model.prototype.remove = function() { var subpath, index, howMany, cb; - if (arguments.length === 1) { + if (arguments.length < 2) { index = arguments[0]; } else if (arguments.length === 2) { if (typeof arguments[1] === 'function') { @@ -435,7 +435,7 @@ Model.prototype._remove = function(segments, index, howMany, cb) { Model.prototype.move = function() { var subpath, from, to, howMany, cb; - if (arguments.length === 1) { + if (arguments.length < 2) { throw new Error('Not enough arguments for move'); } else if (arguments.length === 2) { from = arguments[0]; @@ -506,7 +506,7 @@ Model.prototype._move = function(segments, from, to, howMany, cb) { Model.prototype.stringInsert = function() { var subpath, index, text, cb; - if (arguments.length === 1) { + if (arguments.length < 2) { throw new Error('Not enough arguments for stringInsert'); } else if (arguments.length === 2) { index = arguments[0]; @@ -545,7 +545,7 @@ Model.prototype._stringInsert = function(segments, index, text, cb) { Model.prototype.stringRemove = function() { var subpath, index, howMany, cb; - if (arguments.length === 1) { + if (arguments.length < 2) { throw new Error('Not enough arguments for stringRemove'); } else if (arguments.length === 2) { index = arguments[0]; @@ -581,3 +581,48 @@ Model.prototype._stringRemove = function(segments, index, howMany, cb) { } return this._mutate(segments, stringRemove, cb); }; + +Model.prototype.subtypeSubmit = function() { + var subpath, subtype, subtypeOp, cb; + if (arguments.length < 2) { + throw new Error('Not enough arguments for subtypeSubmit'); + } else if (arguments.length === 2) { + subtype = arguments[0]; + subtypeOp = arguments[1]; + } else if (arguments.length === 3) { + if (typeof arguments[2] === 'function') { + subtype = arguments[0]; + subtypeOp = arguments[1]; + cb = arguments[2]; + } else { + subpath = arguments[0]; + subtype = arguments[1]; + subtypeOp = arguments[2]; + } + } else { + subpath = arguments[0]; + subtype = arguments[1]; + subtypeOp = arguments[2]; + cb = arguments[3]; + } + var segments = this._splitPath(subpath); + return this._subtypeSubmit(segments, subtype, subtypeOp, cb); +}; + +Model.prototype._subtypeSubmit = function(segments, subtype, subtypeOp, cb) { + segments = this._dereference(segments); + var model = this; + function subtypeSubmit(doc, docSegments, fnCb) { + var previous = doc.subtypeSubmit(docSegments, subtype, subtypeOp, fnCb); + var value = doc.get(docSegments); + var pass = model.pass({$subtype: {type: subtype, op: subtypeOp}})._pass; + // Emit undefined for the previous value, since we don't really know + // whether or not the previous value returned by the subtypeSubmit is the + // same object returned by reference or not. This may cause change + // listeners to over-trigger, but that is usually going to be better than + // under-triggering + model.emit('change', segments, [value, void 0, pass]); + return previous; + } + return this._mutate(segments, subtypeSubmit, cb); +}; From ae27f5ac1938e2535e2a70cd54865a02b9535192 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 16 Dec 2015 20:18:05 -0800 Subject: [PATCH 099/479] 0.7.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d64c1e57..4e01399ec 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.7.2", + "version": "0.7.3", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From a069ba449cfa661c1cc2ad03af47b2d97b548e17 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 21 Dec 2015 13:09:07 -0800 Subject: [PATCH 100/479] update sharedb dep --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e01399ec..549e315e7 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "arraydiff": "^0.1.1", "deep-is": "^0.1.3", "uuid": "^2.0.1", - "sharedb": "^0.10.0" + "sharedb": "^0.11.0" }, "devDependencies": { "expect.js": "^0.3.1", From 503b555f71090f0516c8efbbcbfa12afc6311e8d Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Dec 2015 13:47:38 -0800 Subject: [PATCH 101/479] remove model.channel feature; sharedb client better supports piggybacking on its connection directly now --- lib/Channel.js | 92 ----------------------------------------- lib/Model/connection.js | 2 - 2 files changed, 94 deletions(-) delete mode 100644 lib/Channel.js diff --git a/lib/Channel.js b/lib/Channel.js deleted file mode 100644 index fab842b6d..000000000 --- a/lib/Channel.js +++ /dev/null @@ -1,92 +0,0 @@ -var EventEmitter = require('events').EventEmitter; -var util = require('./util'); - -module.exports = Channel; - -function Channel(socket) { - EventEmitter.call(this); - - this.socket = socket; - this.messages = new Messages(); - - var channel = this; - var onmessage = socket.onmessage; - socket.onmessage = function(message) { - var data = message.data; - if (typeof data === 'string') data = JSON.parse(data); - - if (data && data.racer) return channel._onMessage(data); - onmessage && onmessage.call(socket, message); - }; -} - -util.mergeInto(Channel.prototype, EventEmitter.prototype); - -Channel.prototype.send = function(name, data, cb) { - var message = this.messages.add(name, data, cb); - // Proactively call the toJSON function, since the Google Closure JSON - // serializer doesn't check for it - this.socket.send(message.toJSON()); -}; - -Channel.prototype._reply = function(id, name, data) { - var message = new Message(id, true, name, data); - this.socket.send(message.toJSON()); -}; - -Channel.prototype._onMessage = function(data) { - if (data.ack) { - var message = this.messages.remove(data.id); - if (message && message.cb) message.cb.apply(null, data.data); - return; - } - var name = data.racer; - if (data.cb) { - var channel = this; - var hasListeners = this.emit(name, data.data, function() { - var args = Array.prototype.slice.call(arguments); - channel._reply(data.id, name, args); - }); - if (!hasListeners) this._reply(data.id, name); - } else { - this.emit(name, data.data); - this._reply(data.id, name); - } -}; - -function MessagesMap() {} - -function Messages() { - this.map = new MessagesMap(); - this.idCount = 0; -} -Messages.prototype.id = function() { - return (++this.idCount).toString(36); -}; -Messages.prototype.add = function(name, data, cb) { - var message = new Message(this.id(), false, name, data, cb); - this.map[message.id] = message; - return message; -}; -Messages.prototype.remove = function(id) { - var message = this.map[id]; - delete this.map[id]; - return message; -}; - -function Message(id, ack, name, data, cb) { - this.id = id; - this.ack = ack; - this.name = name; - this.data = data; - this.cb = cb; -} -Message.prototype.toJSON = function() { - return { - racer: this.name - , id: this.id - , data: this.data - , ack: +this.ack - , cb: (this.cb) ? 1 : 0 - }; -}; diff --git a/lib/Model/connection.js b/lib/Model/connection.js index 0ad383b77..0bcd0bf5c 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -31,8 +31,6 @@ Model.prototype._finishCreateConnection = function() { this.root.shareConnection.on('doc', function(shareDoc) { model.getOrCreateDoc(shareDoc.collection, shareDoc.id); }); - - this.root.channel = new Channel(this.root.socket); }; Model.prototype.connect = function() { From c44dd96f28572636d0ee571151221fc19f404b0f Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Dec 2015 13:50:37 -0800 Subject: [PATCH 102/479] use sharedb's backend.connect method instead of mocking the StreamSocket in racer --- lib/Model/connection.server.js | 55 ++++++---------------------------- 1 file changed, 9 insertions(+), 46 deletions(-) diff --git a/lib/Model/connection.server.js b/lib/Model/connection.server.js index a06c2e8a9..bb8ff6938 100644 --- a/lib/Model/connection.server.js +++ b/lib/Model/connection.server.js @@ -1,53 +1,16 @@ -var ShareConnection = require('sharedb/lib/client').Connection; var Model = require('./Model'); -Model.prototype.createConnection = function(stream, logger) { - var socket = new StreamSocket(this, stream, logger); - this.root.socket = socket; - this.root.shareConnection = new ShareConnection(socket); - socket.onopen(); +Model.prototype.createConnection = function(backend, req) { + this.root.backend = backend; + this.root.req = req; + this.root.connection = backend.connect(null, req); + this.root.socket = this.root.connection.socket; + // Pretend like we are always connected on the server for rendering purposes this._set(['$connection', 'state'], 'connected'); this._finishCreateConnection(); }; -/** - * Wrapper to make a stream look like a BrowserChannel socket - * @param {Stream} stream - */ -function StreamSocket(model, stream, logger) { - this.model = model; - this.stream = stream; - this.logger = logger; - var socket = this; - stream._read = function _read() {}; - stream._write = function _write(chunk, encoding, callback) { - socket.onmessage({ - type: 'message', - data: chunk - }); - if (logger) { - var src = model.shareConnection && model.shareConnection.id; - logger.write({type: 'S->C', chunk: chunk, src: src}); - } - callback(); - }; -} -StreamSocket.prototype.send = function(data) { - var copy = JSON.parse(JSON.stringify(data)); - this.stream.push(copy); - var src = this.model.shareConnection && this.model.shareConnection.id; - if (this.logger) { - this.logger.write({type: 'C->S', chunk: copy, src: src}); - } +Model.prototype.connect = function() { + this.root.backend.connect(this.root.connection, this.root.req); + this.root.socket = this.root.connection.socket; }; -StreamSocket.prototype.close = function() { - this.stream.end(); - this.stream.emit('close'); - this.stream.emit('end'); - this.onclose(); -}; -StreamSocket.prototype.onmessage = function() {}; -StreamSocket.prototype.onclose = function() {}; -StreamSocket.prototype.onerror = function() {}; -StreamSocket.prototype.onopen = function() {}; -StreamSocket.prototype.onconnecting = function() {}; From 081ad2e2fad4239e45f7496191c57ae268f7bc76 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Dec 2015 13:51:20 -0800 Subject: [PATCH 103/479] rename model.shareConnection -> model.connection --- lib/Model/Query.js | 6 +++--- lib/Model/RemoteDoc.js | 4 ++-- lib/Model/connection.js | 17 ++++++++--------- lib/Model/subscriptions.js | 4 ++-- lib/Model/unbundle.js | 4 ++-- test/Model/MockConnectionModel.js | 2 +- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 94d6acc6d..8bed45a7a 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -169,7 +169,7 @@ Query.prototype.fetch = function(cb) { query._setResults(results); cb(); } - this.model.root.shareConnection.createFetchQuery( + this.model.root.connection.createFetchQuery( this.collectionName, this.expression, options, @@ -225,7 +225,7 @@ Query.prototype._subscribeCb = function(cb) { }; Query.prototype._shareFetchedSubscribe = function(options, cb) { - this.model.root.shareConnection.createFetchQuery( + this.model.root.connection.createFetchQuery( this.collectionName, this.expression, options, @@ -239,7 +239,7 @@ Query.prototype._shareSubscribe = function(options, cb) { if (this.shareQuery) { this.shareQuery.destroy(); } - this.shareQuery = this.model.root.shareConnection.createSubscribeQuery( + this.shareQuery = this.model.root.connection.createSubscribeQuery( this.collectionName, this.expression, options, diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 62232c6a8..ea9c0a7e4 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -14,7 +14,7 @@ module.exports = RemoteDoc; function RemoteDoc(model, collectionName, id, snapshot, collection) { // This is a bit messy, but we have to immediately register this doc on the // collection that added it, so that when we create the shareDoc and the - // shareConnection emits the 'doc' event, we'll find this doc instead of + // connection emits the 'doc' event, we'll find this doc instead of // creating a new one if (collection) collection.docs[id] = this; @@ -24,7 +24,7 @@ function RemoteDoc(model, collectionName, id, snapshot, collection) { // Get or create the Share document. Note that we must have already added // this doc to the collection to avoid creating a duplicate doc - this.shareDoc = model.root.shareConnection.get(collectionName, id); + this.shareDoc = model.root.connection.get(collectionName, id); this.shareDoc.ingestSnapshot(snapshot); this._initShareDoc(); } diff --git a/lib/Model/connection.js b/lib/Model/connection.js index 0bcd0bf5c..e0be4da44 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -1,5 +1,4 @@ -var ShareConnection = require('sharedb/lib/client').Connection; -var Channel = require('../Channel'); +var Connection = require('sharedb/lib/client').Connection; var Model = require('./Model'); var LocalDoc = require('./LocalDoc'); var RemoteDoc = require('./RemoteDoc'); @@ -11,8 +10,8 @@ Model.prototype.createConnection = function(bundle) { // The Share connection will bind to the socket by defining the onopen, // onmessage, etc. methods var model = this; - var shareConnection = this.root.shareConnection = new ShareConnection(this.root.socket); - shareConnection.on('state', function(state, reason) { + this.root.connection = new Connection(this.root.socket); + this.root.connection.on('state', function(state, reason) { model._setDiff(['$connection', 'state'], state); model._setDiff(['$connection', 'reason'], reason); }); @@ -23,12 +22,12 @@ Model.prototype.createConnection = function(bundle) { Model.prototype._finishCreateConnection = function() { var model = this; - this.root.shareConnection.on('error', function(err, data) { + this.root.connection.on('error', function(err, data) { model._emitError(err, data); }); // Share docs can be created by queries, so we need to register them // with Racer as soon as they are created to capture their events - this.root.shareConnection.on('doc', function(shareDoc) { + this.root.connection.on('doc', function(shareDoc) { model.getOrCreateDoc(shareDoc.collection, shareDoc.id); }); }; @@ -67,11 +66,11 @@ Model.prototype._getDocConstructor = function(name) { }; Model.prototype.hasPending = function() { - return this.root.shareConnection.hasPending(); + return this.root.connection.hasPending(); }; Model.prototype.hasWritePending = function() { - return this.root.shareConnection.hasWritePending(); + return this.root.connection.hasWritePending(); }; Model.prototype.whenNothingPending = function(cb) { - return this.root.shareConnection.whenNothingPending(cb); + return this.root.connection.whenNothingPending(cb); }; diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 732b65100..e20bea2a5 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -54,7 +54,7 @@ Model.prototype._forSubscribable = function(argumentsObject, method) { var finished = group(); var docMethod = method + 'Doc'; - this.root.shareConnection.startBulk(); + this.root.connection.startBulk(); for (var i = 0; i < args.length; i++) { var item = args[i]; if (item instanceof Query) { @@ -70,7 +70,7 @@ Model.prototype._forSubscribable = function(argumentsObject, method) { } } } - this.root.shareConnection.endBulk(); + this.root.connection.endBulk(); process.nextTick(finished); }; diff --git a/lib/Model/unbundle.js b/lib/Model/unbundle.js index afb9917ee..f7806279c 100644 --- a/lib/Model/unbundle.js +++ b/lib/Model/unbundle.js @@ -1,7 +1,7 @@ var Model = require('./Model'); Model.prototype.unbundle = function(data) { - if (this.shareConnection) this.shareConnection.startBulk(); + if (this.connection) this.connection.startBulk(); // Re-create and subscribe queries; re-create documents associated with queries this._initQueries(data.queries); @@ -48,7 +48,7 @@ Model.prototype.unbundle = function(data) { } } - if (this.shareConnection) this.shareConnection.endBulk(); + if (this.connection) this.connection.endBulk(); // Re-create refs for (var i = 0; i < data.refs.length; i++) { diff --git a/test/Model/MockConnectionModel.js b/test/Model/MockConnectionModel.js index a15b013ad..548b2a259 100644 --- a/test/Model/MockConnectionModel.js +++ b/test/Model/MockConnectionModel.js @@ -18,5 +18,5 @@ MockConnectionModel.prototype.createConnection = function() { onopen: function() {}, onconnecting: function() {} }; - this.root.shareConnection = new ShareConnection(socketMock); + this.root.connection = new ShareConnection(socketMock); }; From 726b29a7fea2a405eaa51d17b6ffdf5be59b2e79 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Dec 2015 13:53:46 -0800 Subject: [PATCH 104/479] racer.createStore -> racer.createBackend, which inherits from sharedb Backend instead of holding a reference to a nested object --- lib/Backend.js | 54 ++++++++++++++++++++++++++++ lib/Racer.server.js | 10 +++--- lib/Store.js | 87 --------------------------------------------- 3 files changed, 58 insertions(+), 93 deletions(-) create mode 100644 lib/Backend.js delete mode 100644 lib/Store.js diff --git a/lib/Backend.js b/lib/Backend.js new file mode 100644 index 000000000..d006fd9d9 --- /dev/null +++ b/lib/Backend.js @@ -0,0 +1,54 @@ +var Backend = require('sharedb').Backend; +var util = require('./util'); +var Model = require('./Model'); + +module.exports = RacerBackend; + +function RacerBackend(racer, options) { + Backend.call(this, options); + this.racer = racer; + this.modelOptions = options && options.modelOptions; + this.on('bundle', function(browserify) { + browserify.require(__dirname + '/index.js', {expose: 'racer'}); + }); +} +RacerBackend.prototype = Object.create(Backend.prototype); + +RacerBackend.prototype.createModel = function(options, req) { + if (this.modelOptions) { + options = (options) ? + util.mergeInto(options, this.modelOptions) : + this.modelOptions; + } + var model = new Model(options); + this.emit('model', model); + model.createConnection(this, req); + return model; +}; + +RacerBackend.prototype.modelMiddleware = function() { + var backend = this; + function modelMiddleware(req, res, next) { + req.model = backend.createModel({fetchOnly: true}, req); + // DEPRECATED: + req.getModel = function() { + return req.model; + }; + + function closeModel() { + res.removeListener('finish', closeModel); + res.removeListener('close', closeModel); + if (req.model) req.model.close(); + req.model = null; + // DEPRECATED: + req.getModel = getModelUndefined; + } + res.on('finish', closeModel); + res.on('close', closeModel); + + next(); + } + return modelMiddleware; +}; + +function getModelUndefined() {} diff --git a/lib/Racer.server.js b/lib/Racer.server.js index c82953357..559fea0d2 100644 --- a/lib/Racer.server.js +++ b/lib/Racer.server.js @@ -1,11 +1,9 @@ -var Store = require('./Store'); +var Backend = require('./Backend'); var Racer = require('./Racer'); -Racer.prototype.Store = Store; +Racer.prototype.Backend = Backend; Racer.prototype.version = require('../package').version; -Racer.prototype.createStore = function(options) { - var store = new Store(this, options); - this.emit('store', store); - return store; +Racer.prototype.createBackend = function(options) { + return new Backend(this, options); }; diff --git a/lib/Store.js b/lib/Store.js deleted file mode 100644 index 64c0d6bf8..000000000 --- a/lib/Store.js +++ /dev/null @@ -1,87 +0,0 @@ -var Duplex = require('stream').Duplex; -var EventEmitter = require('events').EventEmitter; -var ShareDB = require('sharedb'); -var util = require('./util'); -var Channel = require('./Channel'); -var Model = require('./Model'); - -module.exports = Store; - -function Store(racer, options) { - EventEmitter.call(this); - this.racer = racer; - this.modelOptions = options && options.modelOptions; - this.backend = (options && options.backend) || new ShareDB(options); - this.shareClient = this.backend; // DEPRECATED - this.logger = options && options.logger; - this.on('client', function(client) { - var socket = new ClientSocket(client); - client.channel = new Channel(socket); - }); - this.on('bundle', function(browserify) { - browserify.require(__dirname + '/index.js', {expose: 'racer'}); - }); -} - -util.mergeInto(Store.prototype, EventEmitter.prototype); - -Store.prototype.use = util.use; - -Store.prototype.createModel = function(options, req) { - if (this.modelOptions) { - options = (options) ? - util.mergeInto(options, this.modelOptions) : - this.modelOptions; - } - var model = new Model(options); - this.emit('model', model); - var stream = new Duplex({objectMode: true}); - stream.isServer = true; - this.emit('modelStream', stream); - - model.createConnection(stream, this.logger); - this.backend.listen(stream, req); - - return model; -}; - -Store.prototype.modelMiddleware = function() { - var store = this; - function modelMiddleware(req, res, next) { - var model; - - function getModel() { - if (model) return model; - model = store.createModel({fetchOnly: true}, req); - return model; - } - req.getModel = getModel; - - function closeModel() { - req.getModel = getModelUndefined; - res.removeListener('finish', closeModel); - res.removeListener('close', closeModel); - model && model.close(); - model = null; - } - res.on('finish', closeModel); - res.on('close', closeModel); - - next(); - } - return modelMiddleware; -}; - -function getModelUndefined() {} - -function ClientSocket(client) { - this.client = client; - var socket = this; - client.on('message', function(message) { - socket.onmessage({type:'message', data:message}); - }); -} -ClientSocket.prototype.send = function(data) { - if (typeof data !== 'string') data = JSON.stringify(data); - this.client.send(data); -}; From 5c290513924ddb10f14e91f47d56566d3b861e33 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Dec 2015 15:07:56 -0800 Subject: [PATCH 105/479] support defaulting to empty object in model.add --- lib/Model/mutators.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index c9cc44e8e..83e16b337 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -70,8 +70,15 @@ Model.prototype._setEach = function(segments, object, cb) { Model.prototype.add = function() { var subpath, value, cb; - if (arguments.length === 1) { - value = arguments[0]; + if (arguments.length === 0) { + value = {}; + } else if (arguments.length === 1) { + if (typeof arguments[0] === 'function') { + value = {}; + cb = arguments[0]; + } else { + value = arguments[0]; + } } else if (arguments.length === 2) { if (typeof arguments[1] === 'function') { value = arguments[0]; From 4d89467c3501cf836b54906e0a6792b446187eef Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Dec 2015 15:10:28 -0800 Subject: [PATCH 106/479] add model.create() for document creation --- lib/Model/mutators.js | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index 83e16b337..844e1072a 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -68,6 +68,50 @@ Model.prototype._setEach = function(segments, object, cb) { } }; +Model.prototype.create = function() { + var subpath, value, cb; + if (arguments.length === 0) { + value = {}; + } else if (arguments.length === 1) { + if (typeof arguments[0] === 'function') { + value = {}; + cb = arguments[0]; + } else { + value = arguments[0]; + } + } else if (arguments.length === 2) { + if (typeof arguments[1] === 'function') { + value = arguments[0]; + cb = arguments[1]; + } else { + subpath = arguments[0]; + value = arguments[1]; + } + } else { + subpath = arguments[0]; + value = arguments[1]; + cb = arguments[2]; + } + var segments = this._splitPath(subpath); + return this._create(segments, value, cb); +}; + +Model.prototype._create = function(segments, value, cb) { + cb = this.wrapCallback(cb); + segments = this._dereference(segments); + if (segments.length !== 2) { + var message = 'create may only be used on a document path. ' + + 'Invalid path: ' + segments.join('.'); + return cb(new Error(message)); + } + var doc = this.getOrCreateDoc(segments[0], segments[1]); + doc.create(value, cb); + // On creating the doc, remote docs do a copy to add the id without + // it being stored in the database by ShareJS + value = doc.get(); + this.emit('change', segments, [value, void 0, this._pass]); +}; + Model.prototype.add = function() { var subpath, value, cb; if (arguments.length === 0) { From 96a9ecd7fed6cd466641318a019b326cec26a107 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Dec 2015 15:14:59 -0800 Subject: [PATCH 107/479] remove MockConnectionModel and update tests to use actual backend --- test/Model/MockConnectionModel.js | 22 ---- test/Model/RemoteDoc.mocha.js | 9 +- test/Model/events.mocha.js | 184 ++++++++++++++---------------- 3 files changed, 89 insertions(+), 126 deletions(-) delete mode 100644 test/Model/MockConnectionModel.js diff --git a/test/Model/MockConnectionModel.js b/test/Model/MockConnectionModel.js deleted file mode 100644 index 548b2a259..000000000 --- a/test/Model/MockConnectionModel.js +++ /dev/null @@ -1,22 +0,0 @@ -var ShareConnection = require('sharedb/lib/client').Connection; -var Model = require('../../lib/Model'); - -module.exports = MockConnectionModel; -function MockConnectionModel() { - Model.apply(this, arguments); -} -MockConnectionModel.prototype = Object.create(Model.prototype); - -MockConnectionModel.prototype.createConnection = function() { - var socketMock; - socketMock = { - send: function(message) {}, - close: function() {}, - onmessage: function() {}, - onclose: function() {}, - onerror: function() {}, - onopen: function() {}, - onconnecting: function() {} - }; - this.root.connection = new ShareConnection(socketMock); -}; diff --git a/test/Model/RemoteDoc.mocha.js b/test/Model/RemoteDoc.mocha.js index 058433926..8ca32ba64 100644 --- a/test/Model/RemoteDoc.mocha.js +++ b/test/Model/RemoteDoc.mocha.js @@ -1,14 +1,13 @@ var expect = require('../util').expect; -var Model = require('./MockConnectionModel'); +var racer = require('../../lib/index'); var RemoteDoc = require('../../lib/Model/RemoteDoc'); var docs = require('./docs'); describe('RemoteDoc', function() { function createDoc() { - var model = new Model; - model.createConnection(); - model.data.colors = {}; - var doc = new RemoteDoc(model, 'colors', 'green'); + var backend = racer.createBackend(); + var model = backend.createModel(); + var doc = model.getOrCreateDoc('colors', 'green'); doc.create(); return doc; }; diff --git a/test/Model/events.mocha.js b/test/Model/events.mocha.js index 2d0ba8ddb..7a3c383e9 100644 --- a/test/Model/events.mocha.js +++ b/test/Model/events.mocha.js @@ -1,90 +1,11 @@ var expect = require('../util').expect; -var Model = require('./MockConnectionModel'); - -function mutationEvents(createModels) { - describe('set', function() { - it('can raise events registered on array indices', function(done) { - var models = createModels(); - models.local.set('array', [0, 1, 2, 3, 4], function() {}); - models.remote.on('change', 'array.0', function(value, previous) { - expect(value).to.equal(1); - expect(previous).to.equal(0); - done(); - }); - models.local.set('array.0', 1); - }); - }); - describe('move', function() { - it('can move an item from the end to the beginning of the array', function(done) { - var models = createModels(); - models.local.set('array', [0, 1, 2, 3, 4]); - models.remote.on('move', '**', function(captures, from, to, howMany, passed) { - expect(from).to.equal(4); - expect(to).to.equal(0); - done(); - }); - models.local.move('array', 4, 0, 1); - }); - it('can swap the first two items in the array', function(done) { - var models = createModels(); - models.local.set('array', [0, 1, 2, 3, 4], function() {}); - models.remote.on('move', '**', function(captures, from, to, howMany, passed) { - expect(from).to.equal(1); - expect(to).to.equal(0); - done(); - }); - models.local.move('array', 1, 0, 1, function() {}); - }); - it('can move an item from the begnning to the end of the array', function(done) { - var models = createModels(); - models.local.set('array', [0, 1, 2, 3, 4], function() {}); - models.remote.on('move', '**', function(captures, from, to, howMany, passed) { - expect(from).to.equal(0); - expect(to).to.equal(4); - done(); - }); - models.local.move('array', 0, 4, 1, function() {}); - }); - it('supports a negative destination index of -1 (for last)', function(done) { - var models = createModels(); - models.local.set('array', [0, 1, 2, 3, 4], function() {}); - models.remote.on('move', '**', function(captures, from, to, howMany, passed) { - expect(from).to.equal(0); - expect(to).to.equal(4); - done(); - }); - models.local.move('array', 0, -1, 1, function() {}); - }); - it('supports a negative source index of -1 (for last)', function(done) { - var models = createModels(); - models.local.set('array', [0, 1, 2, 3, 4], function() {}); - models.remote.on('move', '**', function(captures, from, to, howMany, passed) { - expect(from).to.equal(4); - expect(to).to.equal(2); - done(); - }); - models.local.move('array', -1, 2, 1, function() {}); - }); - it('can move several items mid-array, with an event for each', function(done) { - var models = createModels(); - models.local.set('array', [0, 1, 2, 3, 4], function() {}); - var events = 0; - models.remote.on('move', '**', function(captures, from, to, howMany, passed) { - expect(from).to.equal(1); - expect(to).to.equal(4); - if (++events === 2) { - done(); - } - }); - models.local.move('array', 1, 3, 2, function() {}); - }); - }); -} +var racer = require('../../lib/index'); describe('Model events', function() { + describe('mutator events', function() { it('calls earlier listeners in the order of mutations', function(done) { - var model = (new Model).at('_page'); + var model = (new racer.Model).at('_page'); var expectedPaths = ['a', 'b', 'c']; model.on('change', '**', function(path) { expect(path).to.equal(expectedPaths.shift()); @@ -101,7 +22,7 @@ describe('Model events', function() { model.set('a', 1); }); it('calls later listeners in the order of mutations', function(done) { - var model = (new Model).at('_page'); + var model = (new racer.Model).at('_page'); model.on('change', 'a', function() { model.set('b', 2); }); @@ -118,23 +39,88 @@ describe('Model events', function() { model.set('a', 1); }); }); + describe('remote events', function() { - function createModels() { - var localModel = new Model(); - localModel.createConnection(); - var remoteModel = new Model(); - remoteModel.createConnection(); - var localDoc = localModel.getOrCreateDoc('colors', 'green'); - localDoc.create(); - var remoteDoc = remoteModel.getOrCreateDoc('colors', 'green'); - localDoc.shareDoc.on('op', function(op, isLocal) { - remoteDoc._onOp(op); + beforeEach(function(done) { + var backend = racer.createBackend(); + var local = this.local = backend.createModel().scope('colors.green'); + var remote = this.remote = backend.createModel().scope('colors.green'); + local.create(function(err) { + if (err) return done(err); + remote.subscribe(done); + }); + }); + + describe('set', function() { + it('can raise events registered on array indices', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + this.remote.on('change', 'array.0', function(value, previous) { + expect(value).to.equal(1); + expect(previous).to.equal(0); + done(); + }); + this.local.set('array.0', 1); }); - return { - local: localModel.scope('colors.green'), - remote: remoteModel.scope('colors.green') - }; - } - mutationEvents(createModels); + }); + + describe('move', function() { + it('can move an item from the end to the beginning of the array', function(done) { + this.local.set('array', [0, 1, 2, 3, 4]); + this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(4); + expect(to).to.equal(0); + done(); + }); + this.local.move('array', 4, 0, 1); + }); + it('can swap the first two items in the array', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(1); + expect(to).to.equal(0); + done(); + }); + this.local.move('array', 1, 0, 1, function() {}); + }); + it('can move an item from the begnning to the end of the array', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(0); + expect(to).to.equal(4); + done(); + }); + this.local.move('array', 0, 4, 1, function() {}); + }); + it('supports a negative destination index of -1 (for last)', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(0); + expect(to).to.equal(4); + done(); + }); + this.local.move('array', 0, -1, 1, function() {}); + }); + it('supports a negative source index of -1 (for last)', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(4); + expect(to).to.equal(2); + done(); + }); + this.local.move('array', -1, 2, 1, function() {}); + }); + it('can move several items mid-array, with an event for each', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + var events = 0; + this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + expect(from).to.equal(1); + expect(to).to.equal(4); + if (++events === 2) { + done(); + } + }); + this.local.move('array', 1, 3, 2, function() {}); + }); + }); }); }); From 78b774beb134a4b60cfc123eb9cf6d16eb815759 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Dec 2015 15:16:15 -0800 Subject: [PATCH 108/479] 0.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 549e315e7..7961a1c6b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.7.3", + "version": "0.8.0", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From a9379557ef0761160b7410d56c34d56bbfd2858b Mon Sep 17 00:00:00 2001 From: Zach Millman Date: Sun, 27 Dec 2015 21:11:23 -0800 Subject: [PATCH 109/479] "codeparty" => "derbyjs" --- README.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 45b6c8379..c340e6720 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ There are currently two demos, which are included in the [racer-examples](https: ## Features - * **Realtime updates** – Model methods automatically propagate changes among browser clients and Node servers in realtime. The [racer-browserchannel](https://github.com/codeparty/racer-browserchannel) adapter is recommended for connecting browsers in realtime. + * **Realtime updates** – Model methods automatically propagate changes among browser clients and Node servers in realtime. The [racer-browserchannel](https://github.com/derbyjs/racer-browserchannel) adapter is recommended for connecting browsers in realtime. * **Realtime query subscriptions** – Clients may subscribe to a limited set of information relevant to the current session. Both document and realtime query subscriptions are supported. Currently, arbitrary Mongo queries are supported. diff --git a/package.json b/package.json index 7961a1c6b..610bc22bf 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://racerjs.com/", "repository": { "type": "git", - "url": "git://github.com/codeparty/racer.git" + "url": "git://github.com/derbyjs/racer.git" }, "version": "0.8.0", "main": "./lib/index.js", From 9aa35f8e690e51b4e150dd16d1bdf1506f9cf2f8 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 1 Jan 2016 22:50:55 -0800 Subject: [PATCH 110/479] better error message handling for sharedb --- lib/Model/connection.js | 4 ++-- lib/Model/events.js | 16 +++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/Model/connection.js b/lib/Model/connection.js index e0be4da44..237e5b079 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -22,8 +22,8 @@ Model.prototype.createConnection = function(bundle) { Model.prototype._finishCreateConnection = function() { var model = this; - this.root.connection.on('error', function(err, data) { - model._emitError(err, data); + this.root.connection.on('error', function(err) { + model._emitError(err); }); // Share docs can be created by queries, so we need to register them // with Racer as soon as they are created to capture their events diff --git a/lib/Model/events.js b/lib/Model/events.js index b3b21540f..1071d884b 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -48,19 +48,13 @@ Model.prototype.wrapCallback = function(cb) { }; }; -Model.prototype._emitError = function(err, additionalMessage) { - if (typeof additionalMessage !== 'string') { - try { - additionalMessage = JSON.stringify(additionalMessage); - } catch (stringifyErr) {} +Model.prototype._emitError = function(err, context) { + if (err.message && context) { + err.message += ' ' + context; } - if (typeof err === 'string') { - err = new Error(err + ' ' + additionalMessage); - } else if (err instanceof Error) { - err.message = err.message + ' ' + additionalMessage; - } else { + if (err.message && err.data) { try { - err = new Error(JSON.stringify(err) + ' ' + additionalMessage); + err.message += ' ' + JSON.stringify(err.data); } catch (stringifyErr) {} } this.emit('error', err); From c8e18464a07009ea75d9becc9ae910cd6e700214 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 1 Jan 2016 22:54:02 -0800 Subject: [PATCH 111/479] 0.8.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7961a1c6b..5cb33f9a5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.8.0", + "version": "0.8.1", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From afc9f7ece7f4d3512cfaf3d3ec2cb42b57754bc3 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sat, 2 Jan 2016 01:14:54 -0800 Subject: [PATCH 112/479] cast to Error before emitting --- lib/Model/events.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index 1071d884b..d7ecf5795 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -49,14 +49,22 @@ Model.prototype.wrapCallback = function(cb) { }; Model.prototype._emitError = function(err, context) { - if (err.message && context) { - err.message += ' ' + context; + var message = (err.message) ? err.message : + (typeof err === 'string') ? err : + 'Unknown model error'; + if (context) { + message += ' ' + context; } - if (err.message && err.data) { + if (err.data) { try { - err.message += ' ' + JSON.stringify(err.data); + message += ' ' + JSON.stringify(err.data); } catch (stringifyErr) {} } + if (err instanceof Error) { + err.message = message; + } else { + err = new Error(message); + } this.emit('error', err); }; From 331d288b84e2240bb85ecdfdf9f3b7559949d0ff Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sat, 2 Jan 2016 01:15:14 -0800 Subject: [PATCH 113/479] 0.8.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5cb33f9a5..effb28c1c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.8.1", + "version": "0.8.2", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 570c7b8d4429b87d54374170e3e679c5267b7b99 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 3 Jan 2016 22:50:22 -0800 Subject: [PATCH 114/479] add Model::sanitizeQuery and replace undefined with null in query objects by default this helps to avoid accidentally querying a bunch of stuff by passing in an undefined --- lib/Model/Query.js | 23 +++++++++++++++++++++++ test/Model/query.mocha.js | 17 +++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 test/Model/query.mocha.js diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 8bed45a7a..2d963bf06 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -10,6 +10,7 @@ Model.INITS.push(function(model) { }); Model.prototype.query = function(collectionName, expression, db) { + expression = this.sanitizeQuery(expression); var query = this.root._queries.get(collectionName, expression, db); if (query) return query; query = new Query(this, collectionName, expression, db); @@ -17,6 +18,28 @@ Model.prototype.query = function(collectionName, expression, db) { return query; }; +// This method replaces undefined in query objects with null, because +// undefined properties are removed in JSON stringify. This can be dangerous +// in queries, where presenece of a property may indicate that it should be a +// filter and absence means that all values are accepted. We aren't checking +// for cycles, which aren't allowed in JSON, so this could throw a max call +// stack error +Model.prototype.sanitizeQuery = function(expression) { + if (expression && typeof expression === 'object') { + for (var key in expression) { + if (expression.hasOwnProperty(key)) { + var value = expression[key]; + if (value === undefined) { + expression[key] = null; + } else { + this.sanitizeQuery(value); + } + } + } + } + return expression; +}; + // Called during initialization of the bundle on page load. Model.prototype._initQueries = function(items) { var queries = this.root._queries; diff --git a/test/Model/query.mocha.js b/test/Model/query.mocha.js new file mode 100644 index 000000000..af482d9b7 --- /dev/null +++ b/test/Model/query.mocha.js @@ -0,0 +1,17 @@ +var expect = require('../util').expect; +var Model = require('../../lib/Model'); + +describe.only('query', function() { + describe('sanitizeQuery', function() { + it('replaces undefined with null in object query expressions', function() { + var model = new Model(); + var query = model.query('foo', {x: undefined, y: 'foo'}); + expect(query.expression).eql({x: null, y: 'foo'}); + }); + it('replaces undefined with null in nested object query expressions', function() { + var model = new Model(); + var query = model.query('foo', [{x: undefined}, {x: {y: undefined, z: 0}}]); + expect(query.expression).eql([{x: null}, {x: {y: null, z: 0}}]); + }); + }); +}); From 6bae888b7a6f5c5a959c3591a006d847823bb8ce Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 3 Jan 2016 22:50:32 -0800 Subject: [PATCH 115/479] 0.8.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index effb28c1c..7cba24d2d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.8.2", + "version": "0.8.3", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From b40865f2e0ec7e359291b8c5b5c17ff4502216cf Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 3 Jan 2016 22:50:57 -0800 Subject: [PATCH 116/479] remove only from test --- test/Model/query.mocha.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Model/query.mocha.js b/test/Model/query.mocha.js index af482d9b7..e8d69c03c 100644 --- a/test/Model/query.mocha.js +++ b/test/Model/query.mocha.js @@ -1,7 +1,7 @@ var expect = require('../util').expect; var Model = require('../../lib/Model'); -describe.only('query', function() { +describe('query', function() { describe('sanitizeQuery', function() { it('replaces undefined with null in object query expressions', function() { var model = new Model(); From 9c3eabac89910494643bcd71ebb9ac5974a31cf5 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 3 Jan 2016 22:51:06 -0800 Subject: [PATCH 117/479] 0.8.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7cba24d2d..07cc0c7df 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.8.3", + "version": "0.8.4", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 239d0f2dc77c2d8ad91b1fba376144ab87e99a5b Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 6 Jan 2016 23:52:54 -0800 Subject: [PATCH 118/479] change third argument in model.query to an options object instead of just the db name --- lib/Model/Query.js | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 2d963bf06..269bed7d6 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -9,11 +9,16 @@ Model.INITS.push(function(model) { model.root._queries = new Queries(); }); -Model.prototype.query = function(collectionName, expression, db) { +Model.prototype.query = function(collectionName, expression, options) { expression = this.sanitizeQuery(expression); - var query = this.root._queries.get(collectionName, expression, db); + // DEPRECATED: Passing in a string as the third argument specifies the db + // option for backward compatibility + if (typeof options === 'string') { + options = {db: options}; + } + var query = this.root._queries.get(collectionName, expression, options); if (query) return query; - query = new Query(this, collectionName, expression, db); + query = new Query(this, collectionName, expression, options); this.root._queries.add(query); return query; }; @@ -50,9 +55,9 @@ Model.prototype._initQueries = function(items) { var expression = item[2]; var ids = item[3] || []; var results = item[4] || []; - var db = item[5]; + var options = item[5]; var extra = item[6]; - var query = new Query(this, collectionName, expression, db); + var query = new Query(this, collectionName, expression, options); queries.add(query); query._addMapIds(ids); @@ -109,8 +114,8 @@ Queries.prototype.remove = function(query) { for (var key in collection) return; delete this.collections[collection]; }; -Queries.prototype.get = function(collectionName, expression, db) { - var hash = queryHash(collectionName, expression, db); +Queries.prototype.get = function(collectionName, expression, options) { + var hash = queryHash(collectionName, expression, options); return this.map[hash]; }; Queries.prototype.toJSON = function() { @@ -124,12 +129,12 @@ Queries.prototype.toJSON = function() { return out; }; -function Query(model, collectionName, expression, db) { +function Query(model, collectionName, expression, options) { this.model = model.pass({$query: this}); this.collectionName = collectionName; this.expression = expression; - this.db = db; - this.hash = queryHash(collectionName, expression, db); + this.options = options; + this.hash = queryHash(collectionName, expression, options); this.segments = ['$queries', this.hash]; this.idsSegments = ['$queries', this.hash, 'ids']; this.extraSegments = ['$queries', this.hash, 'extra']; @@ -181,10 +186,6 @@ Query.prototype.fetch = function(cb) { if (!this.created) this.create(); - var options = { - db: this.db - }; - var query = this; function fetchCb(err, results, extra) { if (err) return cb(err); @@ -195,7 +196,7 @@ Query.prototype.fetch = function(cb) { this.model.root.connection.createFetchQuery( this.collectionName, this.expression, - options, + this.options, fetchCb ); return this; @@ -220,10 +221,8 @@ Query.prototype.subscribe = function(cb) { if (!this.created) this.create(); - var options = { - db: this.db, - results: this._getShareResults() - }; + var options = (this.options) ? util.copy(this.options) : {}; + options.results = this._getShareResults(); // When doing server-side rendering, we actually do a fetch the first time // that subscribe is called, but keep track of the state as if subscribe @@ -525,7 +524,7 @@ Query.prototype.serialize = function() { , this.expression , ids , results - , this.db + , this.options , this.getExtra() ]; while (serialized[serialized.length - 1] == null) { @@ -534,8 +533,8 @@ Query.prototype.serialize = function() { return serialized; }; -function queryHash(collectionName, expression, db) { - var args = [collectionName, expression, db]; +function queryHash(collectionName, expression, options) { + var args = [collectionName, expression, options]; return JSON.stringify(args).replace(/\./g, '|'); } From 602e6aace078e98b2c8a8d990a529b3d163539e0 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 6 Jan 2016 23:53:01 -0800 Subject: [PATCH 119/479] 0.8.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07cc0c7df..5e175ac71 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.8.4", + "version": "0.8.5", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From a6f94455b5abd0df83c78c5a779462e2cae6a441 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 7 Jan 2016 14:51:31 -0800 Subject: [PATCH 120/479] update model.create to use _mutate like all other mutators so we can put more common logic in there --- lib/Model/mutators.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index 844e1072a..ec6f11f9e 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -97,19 +97,23 @@ Model.prototype.create = function() { }; Model.prototype._create = function(segments, value, cb) { - cb = this.wrapCallback(cb); segments = this._dereference(segments); if (segments.length !== 2) { var message = 'create may only be used on a document path. ' + 'Invalid path: ' + segments.join('.'); + cb = this.wrapCallback(cb); return cb(new Error(message)); } - var doc = this.getOrCreateDoc(segments[0], segments[1]); - doc.create(value, cb); - // On creating the doc, remote docs do a copy to add the id without - // it being stored in the database by ShareJS - value = doc.get(); - this.emit('change', segments, [value, void 0, this._pass]); + var model = this; + function create(doc, docSegments, fnCb) { + var previous; + doc.create(value, fnCb); + // On creating the doc, remote docs do a copy to add the id without + // it being stored in the database by ShareJS + value = doc.get(); + model.emit('change', segments, [value, previous, model._pass]); + } + this._mutate(segments, create, cb); }; Model.prototype.add = function() { @@ -157,7 +161,7 @@ Model.prototype._add = function(segments, value, cb) { doc.create(value, fnCb); // On creating the doc, remote docs do a copy to add the id without // it being stored in the database by ShareJS - value = doc.get(docSegments); + value = doc.get(); } model.emit('change', segments, [value, previous, model._pass]); } From 0562f0d25fd1cc413e8016570bb19a841f9609c5 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 7 Jan 2016 15:05:57 -0800 Subject: [PATCH 121/479] implement model.preventCompose() and model.allowCompose() for setting sharedb doc.preventCompose around mutations --- lib/Model/Model.js | 1 + lib/Model/connection.js | 16 +++++++++ lib/Model/mutators.js | 7 ++++ test/Model/RemoteDoc.mocha.js | 61 +++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+) diff --git a/lib/Model/Model.js b/lib/Model/Model.js index 272b9effb..2bb7d20bb 100644 --- a/lib/Model/Model.js +++ b/lib/Model/Model.js @@ -41,5 +41,6 @@ function ChildModel(model) { this._pass = model._pass; this._silent = model._silent; this._eventContext = model._eventContext; + this._preventCompose = model._preventCompose; } ChildModel.prototype = new Model(); diff --git a/lib/Model/connection.js b/lib/Model/connection.js index 237e5b079..b83861f66 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -3,6 +3,22 @@ var Model = require('./Model'); var LocalDoc = require('./LocalDoc'); var RemoteDoc = require('./RemoteDoc'); +Model.INITS.push(function(model) { + model.root._preventCompose = false; +}); + +Model.prototype.preventCompose = function() { + var model = this._child(); + model._preventCompose = true; + return model; +}; + +Model.prototype.allowCompose = function() { + var model = this._child(); + model._preventCompose = false; + return model; +}; + Model.prototype.createConnection = function(bundle) { // Model::_createSocket should be defined by the socket plugin this.root.socket = this._createSocket(bundle); diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index ec6f11f9e..2e5e591d9 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -12,6 +12,13 @@ Model.prototype._mutate = function(segments, fn, cb) { } var doc = this.getOrCreateDoc(collectionName, id); var docSegments = segments.slice(2); + if (this._preventCompose && doc.shareDoc) { + var oldPreventCompose = doc.shareDoc.preventCompose; + doc.shareDoc.preventCompose = true; + var out = fn(doc, docSegments, cb); + doc.shareDoc.preventCompose = oldPreventCompose; + return out; + } return fn(doc, docSegments, cb); }; diff --git a/test/Model/RemoteDoc.mocha.js b/test/Model/RemoteDoc.mocha.js index 8ca32ba64..59d1ee955 100644 --- a/test/Model/RemoteDoc.mocha.js +++ b/test/Model/RemoteDoc.mocha.js @@ -18,5 +18,66 @@ describe('RemoteDoc', function() { expect(doc.id).to.equal('green'); }); }); + describe('preventCompose', function() { + beforeEach(function() { + this.backend = racer.createBackend(); + this.model = this.backend.createModel(); + }); + it('composes ops by default', function(done) { + var fido = this.model.at('dogs.fido'); + var doc = this.model.connection.get('dogs', 'fido'); + fido.create({age: 3}); + fido.increment('age', 2); + fido.increment('age', 2, function(err) { + if (err) return done(err); + expect(doc.version).equal(1); + expect(fido.get()).eql({id: 'fido', age: 7}); + fido.increment('age', 2); + fido.increment('age', 2, function(err) { + if (err) return done(err); + expect(doc.version).equal(2); + expect(fido.get()).eql({id: 'fido', age: 11}); + done(); + }); + }); + }); + it('does not compose ops on a model.preventCompose() child model', function(done) { + var fido = this.model.at('dogs.fido').preventCompose(); + var doc = this.model.connection.get('dogs', 'fido'); + fido.create({age: 3}); + fido.increment('age', 2); + fido.increment('age', 2, function(err) { + if (err) return done(err); + expect(doc.version).equal(3); + expect(fido.get()).eql({id: 'fido', age: 7}); + fido.increment('age', 2); + fido.increment('age', 2, function(err) { + if (err) return done(err); + expect(doc.version).equal(5); + expect(fido.get()).eql({id: 'fido', age: 11}); + done(); + }); + }); + }); + it('composes ops on a model.allowCompose() child model', function(done) { + var fido = this.model.at('dogs.fido').preventCompose(); + var doc = this.model.connection.get('dogs', 'fido'); + fido.create({age: 3}); + fido.increment('age', 2); + fido.increment('age', 2, function(err) { + if (err) return done(err); + expect(doc.version).equal(3); + expect(fido.get()).eql({id: 'fido', age: 7}); + fido = fido.allowCompose(); + fido.increment('age', 2); + fido.increment('age', 2, function(err) { + if (err) return done(err); + expect(doc.version).equal(4); + expect(fido.get()).eql({id: 'fido', age: 11}); + done(); + }); + }); + }); + }); docs(createDoc); }); From e350e49c1c282a692386e99905adeea472f77dd9 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Thu, 7 Jan 2016 15:06:15 -0800 Subject: [PATCH 122/479] 0.8.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e175ac71..c02ca702b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.8.5", + "version": "0.8.6", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 3e2b47ca6e88ed52f6a3e34d8db09c32cad73c80 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sat, 9 Jan 2016 22:06:10 -0800 Subject: [PATCH 123/479] ignore no-ops emitted from sharedb; ensure other ops have exactly one component --- lib/Model/RemoteDoc.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index ea9c0a7e4..d2ace0b9f 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -420,7 +420,19 @@ RemoteDoc.prototype._arrayApply = function(segments, fn, cb) { }; RemoteDoc.prototype._onOp = function(op) { - var item = op[0]; + var item; + if (op.length === 1) { + // ShareDB docs shatter json0 ops into single components during apply + item = op[0]; + } else if (op.length === 0) { + // Ignore no-ops + return; + } else { + try { + op = JSON.stringify(op); + } catch (err) {} + throw new Error('Received op with multiple components from ShareDB ' + op); + } var segments = [this.collectionName, this.id].concat(item.p); var model = this.model; From 08ec0cb6b39c0ce6de01519da53fecb9dcedff18 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sat, 9 Jan 2016 22:06:24 -0800 Subject: [PATCH 124/479] consistent capitalization in error message --- lib/Model/RemoteDoc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index d2ace0b9f..5e6180f37 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -354,7 +354,7 @@ RemoteDoc.prototype.get = function(segments) { RemoteDoc.prototype._createImplied = function(segments) { if (!this.shareDoc.type) { - throw new Error('mutation on uncreated remote document'); + throw new Error('Mutation on uncreated remote document'); } var parent = this.shareDoc; var key = 'data'; From cf5668354d314a417c0fe77df4836bd69822e879 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sat, 9 Jan 2016 22:06:37 -0800 Subject: [PATCH 125/479] 0.8.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c02ca702b..80d0b9a99 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/codeparty/racer.git" }, - "version": "0.8.6", + "version": "0.8.7", "main": "./lib/index.js", "scripts": { "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From d115c0aa824fbc72d3e1bc368f59c93efd8536ba Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 11 Jan 2016 22:47:36 -0800 Subject: [PATCH 126/479] codeparty => derbyjs --- package.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9de97df3e..23f8aaa52 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "version": "0.8.7", "main": "./lib/index.js", "scripts": { - "test": "./node_modules/.bin/mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" + "test": "mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" }, "dependencies": { "arraydiff": "^0.1.1", @@ -25,5 +25,13 @@ "optionalDependencies": {}, "engines": { "node": ">=0.10.0" - } + }, + "bugs": { + "url": "https://github.com/derbyjs/racer/issues" + }, + "directories": { + "test": "test" + }, + "author": "Nate Smith", + "license": "MIT" } From 36b75f3b27a99dd3f5c1d7b9c0f9c3e7cec93245 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 11 Jan 2016 22:47:55 -0800 Subject: [PATCH 127/479] 0.8.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 23f8aaa52..b2caafc6b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.8.7", + "version": "0.8.8", "main": "./lib/index.js", "scripts": { "test": "mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From e49efce8e2bb7896e3917ff2dbec996d0d611a58 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 13 Jan 2016 16:46:11 -0800 Subject: [PATCH 128/479] use the same error cleanup function for emitting all model errors --- lib/Model/events.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index d7ecf5795..0c50eaffd 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -24,8 +24,7 @@ Model.INITS.push(function(model) { // This will throw if there is no handler for model.on('error') model.root._defaultCallback = defaultCallback; function defaultCallback(err) { - if (typeof err === 'string') err = new Error(err); - if (err) model.emit('error', err); + if (err) model._emitError(err); } model.root._mutatorEventQueue = null; @@ -43,7 +42,7 @@ Model.prototype.wrapCallback = function(cb) { try { return cb.apply(this, arguments); } catch (err) { - model.emit('error', err); + model._emitError(err); } }; }; From 07c1e6d7ff56a3f48c3f259e6a07efaa1da2a551 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 13 Jan 2016 16:46:36 -0800 Subject: [PATCH 129/479] 0.8.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b2caafc6b..1c44d6fc2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.8.8", + "version": "0.8.9", "main": "./lib/index.js", "scripts": { "test": "mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 09512e6be96a8f52fa50eaa34be7b272f95f948f Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Mar 2016 15:35:45 -0700 Subject: [PATCH 130/479] leave req.model after request ends instead of nulling the reference this was originally added to protect against memory leaks, but it should not be required and it creates other issues where the model is unexpectedly removed during async waiting periods within a middleware if the request is ended early as the server is processing it --- lib/Backend.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Backend.js b/lib/Backend.js index d006fd9d9..c7b34ab80 100644 --- a/lib/Backend.js +++ b/lib/Backend.js @@ -39,7 +39,6 @@ RacerBackend.prototype.modelMiddleware = function() { res.removeListener('finish', closeModel); res.removeListener('close', closeModel); if (req.model) req.model.close(); - req.model = null; // DEPRECATED: req.getModel = getModelUndefined; } From fe0d9d8e69122849b3bccd47716fba219209a3a9 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Mar 2016 15:36:07 -0700 Subject: [PATCH 131/479] add deprecation warning for req.getModel() --- lib/Backend.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Backend.js b/lib/Backend.js index c7b34ab80..fe314c2ea 100644 --- a/lib/Backend.js +++ b/lib/Backend.js @@ -32,6 +32,7 @@ RacerBackend.prototype.modelMiddleware = function() { req.model = backend.createModel({fetchOnly: true}, req); // DEPRECATED: req.getModel = function() { + console.warn('Warning: req.getModel() is deprecated. Please use req.model instead.'); return req.model; }; From 74059d13a1f79d6a9767157513fb93f006ff6033 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Mar 2016 15:36:23 -0700 Subject: [PATCH 132/479] avoid overwriting req.model if defined already --- lib/Backend.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/Backend.js b/lib/Backend.js index fe314c2ea..18a3dc19c 100644 --- a/lib/Backend.js +++ b/lib/Backend.js @@ -29,6 +29,10 @@ RacerBackend.prototype.createModel = function(options, req) { RacerBackend.prototype.modelMiddleware = function() { var backend = this; function modelMiddleware(req, res, next) { + // Do not add a model to the request if one has been added already + if (req.model) return next(); + + // Create a new model for this request req.model = backend.createModel({fetchOnly: true}, req); // DEPRECATED: req.getModel = function() { @@ -36,6 +40,7 @@ RacerBackend.prototype.modelMiddleware = function() { return req.model; }; + // Close the model when this request ends function closeModel() { res.removeListener('finish', closeModel); res.removeListener('close', closeModel); From 706f9dd9b3fd69d354621349a352fb196d42d528 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 22 Mar 2016 15:36:34 -0700 Subject: [PATCH 133/479] 0.8.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c44d6fc2..b9b2503d9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.8.9", + "version": "0.8.10", "main": "./lib/index.js", "scripts": { "test": "mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From 904aaa751c0cf41d7c78ff580b04cfac706c2980 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 23 Mar 2016 14:14:42 -0700 Subject: [PATCH 134/479] add Model::getAgent() --- lib/Model/connection.js | 8 ++++++++ test/Model/connection.mocha.js | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 test/Model/connection.mocha.js diff --git a/lib/Model/connection.js b/lib/Model/connection.js index b83861f66..015c2037f 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -68,6 +68,14 @@ Model.prototype.close = function(cb) { }); }; +// Returns a reference to the ShareDB agent if it is connected directly on the +// server. Will return null if the ShareDB connection has been disconnected or +// if we are not in the same process and we do not have a reference to the +// server-side agent object +Model.prototype.getAgent = function() { + return this.root.connection.agent; +}; + Model.prototype._isLocal = function(name) { // Whether the collection is local or remote is determined by its name. // Collections starting with an underscore ('_') are for user-defined local diff --git a/test/Model/connection.mocha.js b/test/Model/connection.mocha.js new file mode 100644 index 000000000..1621254d4 --- /dev/null +++ b/test/Model/connection.mocha.js @@ -0,0 +1,26 @@ +var expect = require('../util').expect; +var racer = require('../../lib/index'); + +describe('connection', function() { + + describe('getAgent', function() { + + it('returns a reference to the ShareDB agent on the server', function() { + var backend = racer.createBackend(); + var model = backend.createModel(); + var agent = model.getAgent(); + expect(agent).ok(); + }); + + it('returns null once the model is disconnected', function(done) { + var backend = racer.createBackend(); + var model = backend.createModel(); + model.close(function() { + var agent = model.getAgent(); + expect(agent).equal(null); + done(); + }); + }); + + }); +}); From a5c8aac60b6a2bdc097db4e914b377412f1b7ad5 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 23 Mar 2016 14:15:00 -0700 Subject: [PATCH 135/479] 0.8.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9b2503d9..2e03dc078 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.8.10", + "version": "0.8.11", "main": "./lib/index.js", "scripts": { "test": "mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" From fbda8d4ce861beac34da288c73e27b1ba9dde70d Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 1 May 2016 23:23:55 -0700 Subject: [PATCH 136/479] jshint => eslint; add test coverage --- .eslintrc.js | 29 +++++++++++++++++++++++++++++ .gitignore | 1 + .jshintrc | 14 -------------- .travis.yml | 7 ++++++- package.json | 17 +++++++++-------- test/.eslintrc.js | 12 ++++++++++++ test/.jshintrc | 22 ---------------------- test/mocha.opts | 2 +- 8 files changed, 58 insertions(+), 46 deletions(-) create mode 100644 .eslintrc.js delete mode 100644 .jshintrc create mode 100644 test/.eslintrc.js delete mode 100644 test/.jshintrc diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..dea65e61e --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + extends: 'xo', + rules: { + 'block-scoped-var': 'off', + 'curly': ['error', 'multi-line', 'consistent'], + 'eqeqeq': ['error', 'allow-null'], + 'guard-for-in': 'off', + 'indent': ['error', 2, {SwitchCase: 1}], + 'max-len': ['off', 80, 4, { + ignoreComments: true, + ignoreUrls: true + }], + 'no-eq-null': 'off', + 'no-implicit-coercion': 'off', + 'no-nested-ternary': 'off', + 'no-redeclare': 'off', + 'no-undef-init': 'off', + 'no-unused-expressions': ['error', {allowShortCircuit: true}], + 'one-var': ['error', {initialized: 'never'}], + 'require-jsdoc': 'off', + 'space-before-function-paren': ['error', 'never'], + 'valid-jsdoc': ['off', { + requireReturn: false, + prefer: { + returns: 'return' + } + }], + } +}; diff --git a/.gitignore b/.gitignore index 8e9f2d6de..33ec369f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store *.swp node_modules +coverage diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index de9cb6572..000000000 --- a/.jshintrc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "node": true, - "laxcomma": true, - "eqnull": true, - "eqeqeq": true, - "indent": 2, - "newcap": true, - "quotmark": "single", - "undef": true, - "trailing": true, - "shadow": true, - "expr": true, - "boss": true -} diff --git a/.travis.yml b/.travis.yml index 6e5919de3..330e7a247 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,8 @@ language: node_js node_js: - - "0.10" + - 6 + - 4 + - 0.10 +script: "npm run test-cover" +# Send coverage data to Coveralls +after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" diff --git a/package.json b/package.json index 2e03dc078..091ca2430 100644 --- a/package.json +++ b/package.json @@ -9,23 +9,24 @@ "version": "0.8.11", "main": "./lib/index.js", "scripts": { - "test": "mocha test/**/*.mocha.js && ./node_modules/.bin/jshint lib/*.js lib/**/*.js" + "lint": "eslint --ignore-path .gitignore .", + "test": "node_modules/.bin/mocha && npm run lint", + "test-cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha && npm run lint" }, "dependencies": { "arraydiff": "^0.1.1", "deep-is": "^0.1.3", - "uuid": "^2.0.1", - "sharedb": "^0.11.0" + "sharedb": "^0.11.0", + "uuid": "^2.0.1" }, "devDependencies": { + "coveralls": "^2.11.8", + "eslint": "^2.9.0", + "eslint-config-xo": "^0.14.1", "expect.js": "^0.3.1", - "jshint": "^2.8.0", + "istanbul": "^0.4.2", "mocha": "^2.3.3" }, - "optionalDependencies": {}, - "engines": { - "node": ">=0.10.0" - }, "bugs": { "url": "https://github.com/derbyjs/racer/issues" }, diff --git a/test/.eslintrc.js b/test/.eslintrc.js new file mode 100644 index 000000000..0c95edf69 --- /dev/null +++ b/test/.eslintrc.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = { + env: { + 'mocha': true + }, + rules: { + 'max-len': 'off', + 'max-nested-callbacks': 'off', + 'require-jsdoc': 'off' + } +}; diff --git a/test/.jshintrc b/test/.jshintrc deleted file mode 100644 index 1cb88cb41..000000000 --- a/test/.jshintrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "node": true, - "laxcomma": true, - "eqnull": true, - "eqeqeq": true, - "indent": 2, - "newcap": true, - "quotmark": "single", - "undef": true, - "trailing": true, - "shadow": true, - "expr": true, - "boss": true, - "globals": { - "describe": false, - "it": false, - "before": false, - "after": false, - "beforeEach": false, - "afterEach": false - } -} diff --git a/test/mocha.opts b/test/mocha.opts index ec2f95b66..f3d32c552 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,4 +1,4 @@ --reporter spec --timeout 1200 ---bail --check-leaks +--recursive From 14de14a98e92fcb86e8cf81db9e2547db4f6ad40 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 1 May 2016 23:24:41 -0700 Subject: [PATCH 137/479] remove coffee file --- test/util/util.js | 34 +++++++++++++++++++ test/util/util.mocha.coffee | 28 ---------------- test/util/util.mocha.js | 65 ------------------------------------- 3 files changed, 34 insertions(+), 93 deletions(-) create mode 100644 test/util/util.js delete mode 100644 test/util/util.mocha.coffee delete mode 100644 test/util/util.mocha.js diff --git a/test/util/util.js b/test/util/util.js new file mode 100644 index 000000000..7246fcaed --- /dev/null +++ b/test/util/util.js @@ -0,0 +1,34 @@ +var expect = require('../util').expect; +var util = require('../../lib/util'); + +describe('util', function() { + describe('util.mergeInto', function() { + it('merges empty objects', function() { + var a = {}; + var b = {}; + expect(util.mergeInto(a, b)).to.eql({}); + }); + + it('merges an empty object with a populated object', function() { + var fn = function(x) { + return x++; + }; + var a = {}; + var b = {x: 's', y: [1, 3], fn: fn}; + expect(util.mergeInto(a, b)).to.eql({x: 's', y: [1, 3], fn: fn}); + }); + + it('merges a populated object with a populated object', function() { + var fn = function(x) { + return x++; + }; + var a = {x: 's', y: [1, 3], fn: fn}; + var b = {x: 7, z: {}}; + expect(util.mergeInto(a, b)).to.eql({x: 7, y: [1, 3], fn: fn, z: {}}); + // Merge should modify the first argument + expect(a).to.eql({x: 7, y: [1, 3], fn: fn, z: {}}); + // But not the second + expect(b).to.eql({x: 7, z: {}}); + }); + }); +}); diff --git a/test/util/util.mocha.coffee b/test/util/util.mocha.coffee deleted file mode 100644 index f0f9baf56..000000000 --- a/test/util/util.mocha.coffee +++ /dev/null @@ -1,28 +0,0 @@ -{expect} = require '../util' -util = require '../../lib/util' - -describe 'util', -> - - describe 'util.mergeInto', -> - - it 'merges empty objects', -> - a = {} - b = {} - expect(util.mergeInto a, b).to.eql {} - - it 'merges an empty object with a populated object', -> - fn = (x) -> x++ - a = {} - b = x: 's', y: [1, 3], fn: fn - expect(util.mergeInto a, b).to.eql x: 's', y: [1, 3], fn: fn - - it 'merges a populated object with a populated object', -> - fn = (x) -> x++ - a = x: 's', y: [1, 3], fn: fn - b = x: 7, z: {} - expect(util.mergeInto a, b).to.eql x: 7, y: [1, 3], fn: fn, z: {} - - # Merge should modify the first argument - expect(a).to.eql x: 7, y: [1, 3], fn: fn, z: {} - # But not the second - expect(b).to.eql x: 7, z: {} diff --git a/test/util/util.mocha.js b/test/util/util.mocha.js deleted file mode 100644 index fb9e935f3..000000000 --- a/test/util/util.mocha.js +++ /dev/null @@ -1,65 +0,0 @@ -// Generated by CoffeeScript 1.8.0 -var expect, util; - -expect = require('../util').expect; - -util = require('../../lib/util'); - -describe('util', function() { - return describe('util.mergeInto', function() { - it('merges empty objects', function() { - var a, b; - a = {}; - b = {}; - return expect(util.mergeInto(a, b)).to.eql({}); - }); - it('merges an empty object with a populated object', function() { - var a, b, fn; - fn = function(x) { - return x++; - }; - a = {}; - b = { - x: 's', - y: [1, 3], - fn: fn - }; - return expect(util.mergeInto(a, b)).to.eql({ - x: 's', - y: [1, 3], - fn: fn - }); - }); - return it('merges a populated object with a populated object', function() { - var a, b, fn; - fn = function(x) { - return x++; - }; - a = { - x: 's', - y: [1, 3], - fn: fn - }; - b = { - x: 7, - z: {} - }; - expect(util.mergeInto(a, b)).to.eql({ - x: 7, - y: [1, 3], - fn: fn, - z: {} - }); - expect(a).to.eql({ - x: 7, - y: [1, 3], - fn: fn, - z: {} - }); - return expect(b).to.eql({ - x: 7, - z: {} - }); - }); - }); -}); From 7bdffb3389fb0d3e022cd5abed41cf05d213cc26 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 1 May 2016 23:25:18 -0700 Subject: [PATCH 138/479] remove mocha from test filenames --- test/Model/{LocalDoc.mocha.js => LocalDoc.js} | 0 .../{RemoteDoc.mocha.js => RemoteDoc.js} | 8 ++- .../{connection.mocha.js => connection.js} | 3 -- test/Model/{events.mocha.js => events.js} | 17 +++---- test/Model/{filter.mocha.js => filter.js} | 24 ++++----- test/Model/{fn.mocha.js => fn.js} | 0 test/Model/{query.mocha.js => query.js} | 0 test/Model/{ref.mocha.js => ref.js} | 46 ++++++++--------- test/Model/{refList.mocha.js => refList.js} | 50 +++++++++---------- 9 files changed, 74 insertions(+), 74 deletions(-) rename test/Model/{LocalDoc.mocha.js => LocalDoc.js} (100%) rename test/Model/{RemoteDoc.mocha.js => RemoteDoc.js} (97%) rename test/Model/{connection.mocha.js => connection.js} (99%) rename test/Model/{events.mocha.js => events.js} (95%) rename test/Model/{filter.mocha.js => filter.js} (89%) rename test/Model/{fn.mocha.js => fn.js} (100%) rename test/Model/{query.mocha.js => query.js} (100%) rename test/Model/{ref.mocha.js => ref.js} (92%) rename test/Model/{refList.mocha.js => refList.js} (95%) diff --git a/test/Model/LocalDoc.mocha.js b/test/Model/LocalDoc.js similarity index 100% rename from test/Model/LocalDoc.mocha.js rename to test/Model/LocalDoc.js diff --git a/test/Model/RemoteDoc.mocha.js b/test/Model/RemoteDoc.js similarity index 97% rename from test/Model/RemoteDoc.mocha.js rename to test/Model/RemoteDoc.js index 59d1ee955..a09714b27 100644 --- a/test/Model/RemoteDoc.mocha.js +++ b/test/Model/RemoteDoc.js @@ -1,6 +1,5 @@ var expect = require('../util').expect; var racer = require('../../lib/index'); -var RemoteDoc = require('../../lib/Model/RemoteDoc'); var docs = require('./docs'); describe('RemoteDoc', function() { @@ -10,7 +9,8 @@ describe('RemoteDoc', function() { var doc = model.getOrCreateDoc('colors', 'green'); doc.create(); return doc; - }; + } + describe('create', function() { it('should set the collectionName and id properties', function() { var doc = createDoc(); @@ -18,11 +18,13 @@ describe('RemoteDoc', function() { expect(doc.id).to.equal('green'); }); }); + describe('preventCompose', function() { beforeEach(function() { this.backend = racer.createBackend(); this.model = this.backend.createModel(); }); + it('composes ops by default', function(done) { var fido = this.model.at('dogs.fido'); var doc = this.model.connection.get('dogs', 'fido'); @@ -41,6 +43,7 @@ describe('RemoteDoc', function() { }); }); }); + it('does not compose ops on a model.preventCompose() child model', function(done) { var fido = this.model.at('dogs.fido').preventCompose(); var doc = this.model.connection.get('dogs', 'fido'); @@ -59,6 +62,7 @@ describe('RemoteDoc', function() { }); }); }); + it('composes ops on a model.allowCompose() child model', function(done) { var fido = this.model.at('dogs.fido').preventCompose(); var doc = this.model.connection.get('dogs', 'fido'); diff --git a/test/Model/connection.mocha.js b/test/Model/connection.js similarity index 99% rename from test/Model/connection.mocha.js rename to test/Model/connection.js index 1621254d4..45709798b 100644 --- a/test/Model/connection.mocha.js +++ b/test/Model/connection.js @@ -2,9 +2,7 @@ var expect = require('../util').expect; var racer = require('../../lib/index'); describe('connection', function() { - describe('getAgent', function() { - it('returns a reference to the ShareDB agent on the server', function() { var backend = racer.createBackend(); var model = backend.createModel(); @@ -21,6 +19,5 @@ describe('connection', function() { done(); }); }); - }); }); diff --git a/test/Model/events.mocha.js b/test/Model/events.js similarity index 95% rename from test/Model/events.mocha.js rename to test/Model/events.js index 7a3c383e9..aa1d1113c 100644 --- a/test/Model/events.mocha.js +++ b/test/Model/events.js @@ -2,10 +2,9 @@ var expect = require('../util').expect; var racer = require('../../lib/index'); describe('Model events', function() { - describe('mutator events', function() { it('calls earlier listeners in the order of mutations', function(done) { - var model = (new racer.Model).at('_page'); + var model = (new racer.Model()).at('_page'); var expectedPaths = ['a', 'b', 'c']; model.on('change', '**', function(path) { expect(path).to.equal(expectedPaths.shift()); @@ -22,7 +21,7 @@ describe('Model events', function() { model.set('a', 1); }); it('calls later listeners in the order of mutations', function(done) { - var model = (new racer.Model).at('_page'); + var model = (new racer.Model()).at('_page'); model.on('change', 'a', function() { model.set('b', 2); }); @@ -66,7 +65,7 @@ describe('Model events', function() { describe('move', function() { it('can move an item from the end to the beginning of the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4]); - this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + this.remote.on('move', '**', function(captures, from, to) { expect(from).to.equal(4); expect(to).to.equal(0); done(); @@ -75,7 +74,7 @@ describe('Model events', function() { }); it('can swap the first two items in the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + this.remote.on('move', '**', function(captures, from, to) { expect(from).to.equal(1); expect(to).to.equal(0); done(); @@ -84,7 +83,7 @@ describe('Model events', function() { }); it('can move an item from the begnning to the end of the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + this.remote.on('move', '**', function(captures, from, to) { expect(from).to.equal(0); expect(to).to.equal(4); done(); @@ -93,7 +92,7 @@ describe('Model events', function() { }); it('supports a negative destination index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + this.remote.on('move', '**', function(captures, from, to) { expect(from).to.equal(0); expect(to).to.equal(4); done(); @@ -102,7 +101,7 @@ describe('Model events', function() { }); it('supports a negative source index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + this.remote.on('move', '**', function(captures, from, to) { expect(from).to.equal(4); expect(to).to.equal(2); done(); @@ -112,7 +111,7 @@ describe('Model events', function() { it('can move several items mid-array, with an event for each', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); var events = 0; - this.remote.on('move', '**', function(captures, from, to, howMany, passed) { + this.remote.on('move', '**', function(captures, from, to) { expect(from).to.equal(1); expect(to).to.equal(4); if (++events === 2) { diff --git a/test/Model/filter.mocha.js b/test/Model/filter.js similarity index 89% rename from test/Model/filter.mocha.js rename to test/Model/filter.js index 6cd9f46a4..b75033722 100644 --- a/test/Model/filter.mocha.js +++ b/test/Model/filter.js @@ -4,9 +4,9 @@ var Model = require('../../lib/Model'); describe('filter', function() { describe('getting', function() { it('does not support array', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); - var filter = model.filter('numbers', function(number, i, numbers) { + var filter = model.filter('numbers', function(number) { return (number % 2) === 0; }); expect(function() { @@ -14,18 +14,18 @@ describe('filter', function() { }).to.throwException(); }); it('supports filter of object', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); } - var filter = model.filter('numbers', function(number, id, numbers) { + var filter = model.filter('numbers', function(number) { return (number % 2) === 0; }); expect(filter.get()).to.eql([0, 4, 2, 0]); }); it('supports sort of object', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -36,7 +36,7 @@ describe('filter', function() { expect(filter.get()).to.eql([4, 3, 3, 2, 1, 0, 0]); }); it('supports filter and sort of object', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -50,7 +50,7 @@ describe('filter', function() { }); describe('initial value set by ref', function() { it('supports filter of object', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -62,7 +62,7 @@ describe('filter', function() { expect(model.get('out')).to.eql([0, 4, 2, 0]); }); it('supports sort of object', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -74,7 +74,7 @@ describe('filter', function() { expect(model.get('out')).to.eql([4, 3, 3, 2, 1, 0, 0]); }); it('supports filter and sort of object', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -89,12 +89,12 @@ describe('filter', function() { }); describe('ref updates as items are modified', function() { it('supports filter of object', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); var greenId = model.add('colors', { name: 'green', primary: true }); - var orangeId = model.add('colors', { + model.add('colors', { name: 'orange', primary: false }); @@ -142,7 +142,7 @@ describe('filter', function() { ]); }); it('supports additional dynamic inputs', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); diff --git a/test/Model/fn.mocha.js b/test/Model/fn.js similarity index 100% rename from test/Model/fn.mocha.js rename to test/Model/fn.js diff --git a/test/Model/query.mocha.js b/test/Model/query.js similarity index 100% rename from test/Model/query.mocha.js rename to test/Model/query.js diff --git a/test/Model/ref.mocha.js b/test/Model/ref.js similarity index 92% rename from test/Model/ref.mocha.js rename to test/Model/ref.js index cc1cab902..d31854dea 100644 --- a/test/Model/ref.mocha.js +++ b/test/Model/ref.js @@ -11,7 +11,7 @@ describe('ref', function() { } describe('event emission', function() { it('re-emits on a reffed path', function(done) { - var model = new Model; + var model = new Model(); model.ref('_page.color', '_page.colors.green'); model.on('change', '_page.color', function(value) { expect(value).to.equal('#0f0'); @@ -20,7 +20,7 @@ describe('ref', function() { model.set('_page.colors.green', '#0f0'); }); it('also emits on the original path', function(done) { - var model = new Model; + var model = new Model(); model.ref('_page.color', '_page.colors.green'); model.on('change', '_page.colors.green', function(value) { expect(value).to.equal('#0f0'); @@ -29,7 +29,7 @@ describe('ref', function() { model.set('_page.colors.green', '#0f0'); }); it('re-emits on a child of a reffed path', function(done) { - var model = new Model; + var model = new Model(); model.ref('_page.color', '_page.colors.green'); model.on('change', '_page.color.*', function(capture, value) { expect(capture).to.equal('hex'); @@ -39,7 +39,7 @@ describe('ref', function() { model.set('_page.colors.green.hex', '#0f0'); }); it('re-emits when a parent is changed', function(done) { - var model = new Model; + var model = new Model(); model.ref('_page.color', '_page.colors.green'); model.on('change', '_page.color', function(value) { expect(value).to.equal('#0e0'); @@ -50,7 +50,7 @@ describe('ref', function() { }); }); it('re-emits on a ref to a ref', function(done) { - var model = new Model; + var model = new Model(); model.ref('_page.myFavorite', '_page.color'); model.ref('_page.color', '_page.colors.green'); model.on('change', '_page.myFavorite', function(value) { @@ -60,16 +60,16 @@ describe('ref', function() { model.set('_page.colors.green', '#0f0'); }); it('re-emits on multiple reffed paths', function(done) { - var model = new Model; + var model = new Model(); model.set('_page.colors.green', '#0f0'); model.ref('_page.favorites.my', '_page.colors.green'); model.ref('_page.favorites.your', '_page.colors.green'); expectEvents('_page.favorites**', model, done, [ - function(capture, method, value, previous) { + function(capture, method, value) { expect(method).to.equal('change'); expect(capture).to.equal('my'); expect(value).to.equal('#0f1'); - }, function(capture, method, value, previous) { + }, function(capture, method, value) { expect(method).to.equal('change'); expect(capture).to.equal('your'); expect(value).to.equal('#0f1'); @@ -80,14 +80,14 @@ describe('ref', function() { }); describe('get', function() { it('gets from a reffed path', function() { - var model = new Model; + var model = new Model(); model.set('_page.colors.green', '#0f0'); - expect(model.get('_page.color')).to.equal(void 0); + expect(model.get('_page.color')).to.equal(undefined); model.ref('_page.color', '_page.colors.green'); expect(model.get('_page.color')).to.equal('#0f0'); }); it('gets from a child of a reffed path', function() { - var model = new Model; + var model = new Model(); model.set('_page.colors.green.hex', '#0f0'); model.ref('_page.color', '_page.colors.green'); expect(model.get('_page.color')).to.eql({ @@ -96,7 +96,7 @@ describe('ref', function() { expect(model.get('_page.color.hex')).to.equal('#0f0'); }); it('gets from a ref to a ref', function() { - var model = new Model; + var model = new Model(); model.ref('_page.myFavorite', '_page.color'); model.ref('_page.color', '_page.colors.green'); model.set('_page.colors.green', '#0f0'); @@ -105,7 +105,7 @@ describe('ref', function() { }); describe('event/add ordering', function() { it('ref results are propogated when set in reponse to an event', function() { - var model = new Model; + var model = new Model(); model.on('change', '_page.start', function() { model.ref('_page.myColor', '_page.color'); model.ref('_page.yourColor', '_page.color'); @@ -116,7 +116,7 @@ describe('ref', function() { expect(model.get('_page.myColor')).to.equal('green'); }); it('can create refList in event callback', function() { - var model = new Model; + var model = new Model(); model.on('change', '_page.start', function() { model.set('_page.colors', { red: '#f00', @@ -132,7 +132,7 @@ describe('ref', function() { }); describe('updateIndices option', function() { it('updates a ref when an array insert happens at the `to` path', function() { - var model = new Model; + var model = new Model(); model.set('_page.colors', ['red', 'green', 'blue']); model.ref('_page.color', '_page.colors.1', {updateIndices: true}); expect(model.get('_page.color')).to.equal('green'); @@ -144,7 +144,7 @@ describe('ref', function() { expect(model.get('_page.color')).to.equal('green'); }); it('updates a ref when an array remove happens at the `to` path', function() { - var model = new Model; + var model = new Model(); model.set('_page.colors', ['red', 'blue', 'purple', 'cyan', 'green', 'yellow']); model.ref('_page.color', '_page.colors.4', {updateIndices: true}); expect(model.get('_page.color')).to.equal('green'); @@ -156,7 +156,7 @@ describe('ref', function() { expect(model.get('_page.color')).to.equal('green'); }); it('updates a ref when an array move happens at the `to` path', function() { - var model = new Model; + var model = new Model(); model.set('_page.colors', ['red', 'blue', 'purple', 'green', 'cyan', 'yellow']); model.ref('_page.color', '_page.colors.3', {updateIndices: true}); expect(model.get('_page.color')).to.equal('green'); @@ -176,12 +176,12 @@ describe('ref', function() { expect(model.get('_page.color')).to.equal('green'); }); it('updates a ref when an array insert happens within the `to` path', function() { - var model = new Model; + var model = new Model(); model.set('_page.colors', [ {name: 'red'}, {name: 'green'}, {name: 'blue'} - ]); + ]); model.ref('_page.color', '_page.colors.1.name', {updateIndices: true}); expect(model.get('_page.color')).to.equal('green'); model.unshift('_page.colors', 'yellow'); @@ -192,7 +192,7 @@ describe('ref', function() { expect(model.get('_page.color')).to.equal('green'); }); it('updates a ref when an array remove happens within the `to` path', function() { - var model = new Model; + var model = new Model(); model.set('_page.colors', [ {name: 'red'}, {name: 'blue'}, @@ -200,7 +200,7 @@ describe('ref', function() { {name: 'cyan'}, {name: 'green'}, {name: 'yellow'} - ]); + ]); model.ref('_page.color', '_page.colors.4.name', {updateIndices: true}); expect(model.get('_page.color')).to.equal('green'); model.shift('_page.colors'); @@ -211,7 +211,7 @@ describe('ref', function() { expect(model.get('_page.color')).to.equal('green'); }); it('updates a ref when an array move happens within the `to` path', function() { - var model = new Model; + var model = new Model(); model.set('_page.colors', [ {name: 'red'}, {name: 'blue'}, @@ -219,7 +219,7 @@ describe('ref', function() { {name: 'green'}, {name: 'cyan'}, {name: 'yellow'} - ]); + ]); model.ref('_page.color', '_page.colors.3.name', {updateIndices: true}); expect(model.get('_page.color')).to.equal('green'); model.move('_page.colors', 0, 1); diff --git a/test/Model/refList.mocha.js b/test/Model/refList.js similarity index 95% rename from test/Model/refList.mocha.js rename to test/Model/refList.js index 960771927..857d7f873 100644 --- a/test/Model/refList.mocha.js +++ b/test/Model/refList.js @@ -3,7 +3,7 @@ var Model = require('../../lib/Model'); describe('refList', function() { function setup(options) { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); model.set('colors', { green: { id: 'green', @@ -38,7 +38,7 @@ describe('refList', function() { } describe('sets output on initial call', function() { it('sets the initial value to empty array if no inputs', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); model.refList('empty', 'colors', 'noIds'); expect(model.get('empty')).to.eql([]); }); @@ -63,7 +63,7 @@ describe('refList', function() { }); describe('updates on `ids` mutations', function() { it('updates the value when `ids` is set', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); model.set('colors', { green: { id: 'green', @@ -96,7 +96,7 @@ describe('refList', function() { ]); }); it('emits on `from` when `ids` is set', function(done) { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); model.set('colors', { green: { id: 'green', @@ -161,7 +161,7 @@ describe('refList', function() { id: 'green', rgb: [0, 255, 0], hex: '#0f0' - }, void 0 + }, undefined ]); }); it('emits on `from` when `ids` children are set', function(done) { @@ -211,7 +211,7 @@ describe('refList', function() { id: 'red', rgb: [255, 0, 0], hex: '#f00' - }, void 0, { + }, undefined, { id: 'red', rgb: [255, 0, 0], hex: '#f00' @@ -237,7 +237,7 @@ describe('refList', function() { expect(method).to.equal('insert'); expect(index).to.equal(1); expect(inserted).to.eql([ - void 0, { + undefined, { id: 'red', rgb: [255, 0, 0], hex: '#f00' @@ -335,7 +335,7 @@ describe('refList', function() { }); describe('emits events involving multiple refLists', function() { it('removes data from a refList pointing to data in another refList', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); var tagId = model.add('tags', { text: 'hi' }); @@ -352,10 +352,10 @@ describe('refList', function() { }); describe('updates on `to` mutations', function() { it('updates the value when `to` is set', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); model.set('ids', ['red', 'green', 'red']); model.refList('list', 'colors', 'ids'); - expect(model.get('list')).to.eql([void 0, void 0, void 0]); + expect(model.get('list')).to.eql([undefined, undefined, undefined]); model.set('colors', { green: { id: 'green', @@ -385,7 +385,7 @@ describe('refList', function() { ]); }); it('emits on `from` when `to` is set', function(done) { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); model.set('ids', ['red', 'green', 'red']); model.refList('list', 'colors', 'ids'); expectFromEvents(model, done, [ @@ -393,7 +393,7 @@ describe('refList', function() { expect(capture).to.equal(''); expect(method).to.equal('remove'); expect(index).to.equal(0); - expect(removed).to.eql([void 0, void 0, void 0]); + expect(removed).to.eql([undefined, undefined, undefined]); }, function(capture, method, index, inserted) { expect(capture).to.equal(''); expect(method).to.equal('insert'); @@ -429,7 +429,7 @@ describe('refList', function() { }); }); it('updates the value when `to` children are set', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); model.set('ids', ['red', 'green', 'red']); model.refList('list', 'colors', 'ids'); model.set('colors.green', { @@ -438,11 +438,11 @@ describe('refList', function() { hex: '#0f0' }); expect(model.get('list')).to.eql([ - void 0, { + undefined, { id: 'green', rgb: [0, 255, 0], hex: '#0f0' - }, void 0 + }, undefined ]); model.set('colors.red', { id: 'red', @@ -470,7 +470,7 @@ describe('refList', function() { id: 'red', rgb: [255, 0, 0], hex: '#f00' - }, void 0, { + }, undefined, { id: 'red', rgb: [255, 0, 0], hex: '#f00' @@ -478,7 +478,7 @@ describe('refList', function() { ]); }); it('emits on `from` when `to` children are set', function(done) { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); model.set('ids', ['red', 'green', 'red']); model.refList('list', 'colors', 'ids'); expectFromEvents(model, done, [ @@ -490,7 +490,7 @@ describe('refList', function() { rgb: [255, 0, 0], hex: '#f00' }); - expect(previous).to.equal(void 0); + expect(previous).to.equal(undefined); }, function(capture, method, value, previous) { expect(capture).to.equal('2'); expect(method).to.equal('change'); @@ -499,7 +499,7 @@ describe('refList', function() { rgb: [255, 0, 0], hex: '#f00' }); - expect(previous).to.equal(void 0); + expect(previous).to.equal(undefined); } ]); model.set('colors.red', { @@ -572,7 +572,7 @@ describe('refList', function() { model.set('colors.red.rgb.0', 238); }); it('updates the value when inserting on `to` children', function() { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); model.set('nums', { even: [2, 4, 6], odd: [1, 3] @@ -584,7 +584,7 @@ describe('refList', function() { expect(model.get('list')).to.eql([[2, 4, 6, 8], [1, 3], [2, 4, 6, 8]]); }); it('emits on `from` when inserting on `to` children', function(done) { - var model = (new Model).at('_page'); + var model = (new Model()).at('_page'); model.set('nums', { even: [2, 4, 6], odd: [1, 3] @@ -722,7 +722,7 @@ describe('refList', function() { rgb: [0, 0, 255], hex: '#00f' }); - expect(previous).to.eql(void 0); + expect(previous).to.eql(undefined); }, function(capture, method, value, previous) { expect(capture).to.equal('yellow'); expect(method).to.equal('change'); @@ -731,7 +731,7 @@ describe('refList', function() { rgb: [255, 255, 0], hex: '#ff0' }); - expect(previous).to.eql(void 0); + expect(previous).to.eql(undefined); } ]); model.set('list', [ @@ -792,7 +792,7 @@ describe('refList', function() { rgb: [1, 1, 1] }); var newId = model.get('list.0').id; - expect(model.get("colors." + newId)).to.eql({ + expect(model.get('colors.' + newId)).to.eql({ id: newId, rgb: [1, 1, 1] }); @@ -804,7 +804,7 @@ describe('refList', function() { model.refList('array', 'colors', 'arrayIds'); model.ref('arrayAlias', 'array'); model.on('insert', 'arrayAlias', function() { - expect(model.get('array.0.names.0')).to.eql(void 0); + expect(model.get('array.0.names.0')).to.eql(undefined); done(); }); model.insert('arrayAlias', 0, { From 908581185d804be6695067e0fa2633292d7b7f6a Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sun, 1 May 2016 23:26:48 -0700 Subject: [PATCH 139/479] eslint changes --- lib/Backend.js | 6 ++++-- lib/Model/CollectionCounter.js | 10 ++++++++-- lib/Model/LocalDoc.js | 2 +- lib/Model/Model.js | 2 +- lib/Model/Query.js | 28 ++++++++++++++-------------- lib/Model/RemoteDoc.js | 24 ++++++++++++------------ lib/Model/bundle.js | 14 +++++++------- lib/Model/collections.js | 1 + lib/Model/contexts.js | 1 - lib/Model/events.js | 15 +++++++-------- lib/Model/fn.js | 10 +++++----- lib/Model/mutators.js | 5 +++-- lib/Model/ref.js | 5 ++--- lib/Model/refList.js | 19 +++++++++---------- lib/util.js | 1 + test/Model/docs.js | 24 ++++++++++++------------ 16 files changed, 87 insertions(+), 80 deletions(-) diff --git a/lib/Backend.js b/lib/Backend.js index 18a3dc19c..2f2801d13 100644 --- a/lib/Backend.js +++ b/lib/Backend.js @@ -1,6 +1,7 @@ +var path = require('path'); var Backend = require('sharedb').Backend; -var util = require('./util'); var Model = require('./Model'); +var util = require('./util'); module.exports = RacerBackend; @@ -9,7 +10,8 @@ function RacerBackend(racer, options) { this.racer = racer; this.modelOptions = options && options.modelOptions; this.on('bundle', function(browserify) { - browserify.require(__dirname + '/index.js', {expose: 'racer'}); + var racerPath = path.join(__dirname, 'index.js'); + browserify.require(racerPath, {expose: 'racer'}); }); } RacerBackend.prototype = Object.create(Backend.prototype); diff --git a/lib/Model/CollectionCounter.js b/lib/Model/CollectionCounter.js index 278c86fda..74751cb9e 100644 --- a/lib/Model/CollectionCounter.js +++ b/lib/Model/CollectionCounter.js @@ -13,23 +13,29 @@ CollectionCounter.prototype.get = function(collectionName, id) { CollectionCounter.prototype.increment = function(collectionName, id) { var collection = this.collections[collectionName] || (this.collections[collectionName] = {}); - return collection[id] = (collection[id] || 0) + 1; + var count = (collection[id] || 0) + 1; + collection[id] = count; + return count; }; CollectionCounter.prototype.decrement = function(collectionName, id) { var collection = this.collections[collectionName]; var count = collection && collection[id]; if (count == null) return; if (count > 1) { - return collection[id] = count - 1; + count--; + collection[id] = count; + return count; } delete collection[id]; // Check if the collection still has any keys + // eslint-disable-next-line no-unused-vars for (var key in collection) return 0; delete this.collections[collection]; return 0; }; CollectionCounter.prototype.toJSON = function() { // Check to see if we have any keys + // eslint-disable-next-line no-unused-vars for (var key in this.collections) { return this.collections; } diff --git a/lib/Model/LocalDoc.js b/lib/Model/LocalDoc.js index 93eb28ad2..d94af104d 100644 --- a/lib/Model/LocalDoc.js +++ b/lib/Model/LocalDoc.js @@ -40,7 +40,7 @@ LocalDoc.prototype.del = function(segments, cb) { // apply creates objects as it traverses, and the del method // should not create anything var previous = this.get(segments); - if (previous === void 0) { + if (previous === undefined) { cb(); return; } diff --git a/lib/Model/Model.js b/lib/Model/Model.js index 2bb7d20bb..d0eaf2577 100644 --- a/lib/Model/Model.js +++ b/lib/Model/Model.js @@ -8,7 +8,7 @@ function Model(options) { this.root = this; var inits = Model.INITS; - options || (options = {}); + if (!options) options = {}; this.debug = options.debug || {}; for (var i = 0; i < inits.length; i++) { inits[i](this, options); diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 269bed7d6..abc6c23f5 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -1,6 +1,5 @@ var util = require('../util'); var Model = require('./Model'); -var arrayDiff = require('arraydiff'); var defaultType = require('sharedb').types.defaultType; module.exports = Query; @@ -64,10 +63,10 @@ Model.prototype._initQueries = function(items) { this._set(query.idsSegments, ids); query._setExtra(extra); - for (var j = 0; j < results.length; j++) { - var result = results[j]; + for (var resultIndex = 0; resultIndex < results.length; resultIndex++) { + var result = results[resultIndex]; if (!result) continue; - var id = ids[j]; + var id = ids[resultIndex]; var snapshot = { data: result[0], v: result[1], @@ -76,8 +75,8 @@ Model.prototype._initQueries = function(items) { this.getOrCreateDoc(collectionName, id, snapshot); } - for (var j = 0; j < counts.length; j++) { - var count = counts[j]; + for (var countIndex = 0; countIndex < counts.length; countIndex++) { + var count = counts[countIndex]; var subscribed = count[0] || 0; var fetched = count[1] || 0; var contextId = count[2]; @@ -111,6 +110,7 @@ Queries.prototype.remove = function(query) { if (!collection) return; delete collection[query.hash]; // Check if the collection still has any keys + // eslint-disable-next-line no-unused-vars for (var key in collection) return; delete this.collections[collection]; }; @@ -355,7 +355,7 @@ Query.prototype._maybeUnloadDocs = function(ids) { Query.prototype._flushSubscribeCallbacks = function(err, cb) { cb(err); var pendingCallback; - while (pendingCallback = this._pendingSubscribeCallbacks.shift()) { + while ((pendingCallback = this._pendingSubscribeCallbacks.shift())) { pendingCallback(err); } }; @@ -519,13 +519,13 @@ Query.prototype.serialize = function() { } } var serialized = [ - counts - , this.collectionName - , this.expression - , ids - , results - , this.options - , this.getExtra() + counts, + this.collectionName, + this.expression, + ids, + results, + this.options, + this.getExtra() ]; while (serialized[serialized.length - 1] == null) { serialized.pop(); diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index 5e6180f37..b4c856106 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -55,7 +55,7 @@ RemoteDoc.prototype._initShareDoc = function() { // so we create the appropriate event here. if (isLocal) return; delete doc.collectionData[id]; - model.emit('change', [collectionName, id], [void 0, previous, model._pass]); + model.emit('change', [collectionName, id], [undefined, previous, model._pass]); }); shareDoc.on('create', function(isLocal) { // Local creates should not emit an event, since they only happen @@ -66,7 +66,7 @@ RemoteDoc.prototype._initShareDoc = function() { if (isLocal) return; doc._updateCollectionData(); var value = shareDoc.data; - model.emit('change', [collectionName, id], [value, void 0, model._pass]); + model.emit('change', [collectionName, id], [value, undefined, model._pass]); }); shareDoc.on('error', function(err) { model._emitError(err, collectionName + '.' + id); @@ -138,7 +138,7 @@ RemoteDoc.prototype.del = function(segments, cb) { // Don't do anything if the value is already undefined, since // the del method should not create anything var previous = this.get(segments); - if (previous === void 0) { + if (previous === undefined) { cb(); return; } @@ -340,7 +340,7 @@ RemoteDoc.prototype.subtypeSubmit = function(segments, subtype, subtypeOp, cb) { var previous = this._createImplied(segments); if (previous instanceof ImpliedOp) { this.shareDoc.submitOp(previous.op); - previous = void 0; + previous = undefined; } var op = new SubtypeOp(segments, subtype, subtypeOp); this.shareDoc.submitOp(op, cb); @@ -502,7 +502,7 @@ RemoteDoc.prototype._onOp = function(op) { // given that embedded types are likely to be used for custom editors, // we'll assume they primarily use the returned op and are unlikely to // need the previous snapshot data - var previous = void 0; + var previous = undefined; var type = item.t; var op = item.o; var pass = model.pass({$subtype: {type: type, op: op}})._pass; @@ -513,28 +513,28 @@ RemoteDoc.prototype._onOp = function(op) { function ObjectReplaceOp(segments, before, after) { this.p = util.castSegments(segments); this.od = before; - this.oi = (after === void 0) ? null : after; + this.oi = (after === undefined) ? null : after; } function ObjectInsertOp(segments, value) { this.p = util.castSegments(segments); - this.oi = (value === void 0) ? null : value; + this.oi = (value === undefined) ? null : value; } function ObjectDeleteOp(segments, value) { this.p = util.castSegments(segments); - this.od = (value === void 0) ? null : value; + this.od = (value === undefined) ? null : value; } function ListReplaceOp(segments, index, before, after) { this.p = util.castSegments(segments.concat(index)); this.ld = before; - this.li = (after === void 0) ? null : after; + this.li = (after === undefined) ? null : after; } function ListInsertOp(segments, index, value) { this.p = util.castSegments(segments.concat(index)); - this.li = (value === void 0) ? null : value; + this.li = (value === undefined) ? null : value; } function ListRemoveOp(segments, index, value) { this.p = util.castSegments(segments.concat(index)); - this.ld = (value === void 0) ? null : value; + this.ld = (value === undefined) ? null : value; } function ListMoveOp(segments, from, to) { this.p = util.castSegments(segments.concat(from)); @@ -559,5 +559,5 @@ function SubtypeOp(segments, subtype, subtypeOp) { } function defined(value) { - return value !== void 0; + return value !== undefined; } diff --git a/lib/Model/bundle.js b/lib/Model/bundle.js index abcc9329e..88175940f 100644 --- a/lib/Model/bundle.js +++ b/lib/Model/bundle.js @@ -21,13 +21,13 @@ Model.prototype.bundle = function(cb) { clearTimeout(timeout); stripIds(root); var bundle = { - queries: root._queries.toJSON() - , contexts: root._contexts - , refs: root._refs.toJSON() - , refLists: root._refLists.toJSON() - , fns: root._fns.toJSON() - , filters: root._filters.toJSON() - , nodeEnv: process.env.NODE_ENV + queries: root._queries.toJSON(), + contexts: root._contexts, + refs: root._refs.toJSON(), + refLists: root._refLists.toJSON(), + fns: root._fns.toJSON(), + filters: root._filters.toJSON(), + nodeEnv: process.env.NODE_ENV }; stripComputed(root); bundle.collections = serializeCollections(root); diff --git a/lib/Model/collections.js b/lib/Model/collections.js index a7f06b184..4990fbdfa 100644 --- a/lib/Model/collections.js +++ b/lib/Model/collections.js @@ -142,6 +142,7 @@ Collection.prototype.get = function() { }; function noKeys(object) { + // eslint-disable-next-line no-unused-vars for (var key in object) { return false; } diff --git a/lib/Model/contexts.js b/lib/Model/contexts.js index 30b0a4a6c..26e66c806 100644 --- a/lib/Model/contexts.js +++ b/lib/Model/contexts.js @@ -3,7 +3,6 @@ */ var Model = require('./Model'); -var Query = require('./Query'); var CollectionCounter = require('./CollectionCounter'); Model.INITS.push(function(model) { diff --git a/lib/Model/events.js b/lib/Model/events.js index 0c50eaffd..5700d0d32 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -6,12 +6,12 @@ var Model = require('./Model'); // emitted in sequence, so that events generated by other events are not // seen in a different order by later listeners Model.MUTATOR_EVENTS = { - change: true -, insert: true -, remove: true -, move: true -, load: true -, unload: true + change: true, + insert: true, + remove: true, + move: true, + load: true, + unload: true }; Model.INITS.push(function(model) { @@ -137,9 +137,8 @@ Model.prototype.removeAllListeners = function(type, subpattern) { if (!pattern) { if (arguments.length === 0) { return this._removeAllListeners(); - } else { - return this._removeAllListeners(type); } + return this._removeAllListeners(type); } // Remove all listeners for an event under a pattern diff --git a/lib/Model/fn.js b/lib/Model/fn.js index 7e6da059d..f9513fcec 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -50,11 +50,11 @@ function parseStartArguments(model, args, hasPath) { args[i] = model.path(args[i]); } return { - name: name - , path: path - , inputPaths: args - , fns: fns - , options: options + name: name, + path: path, + inputPaths: args, + fns: fns, + options: options }; } diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index 2e5e591d9..1eb2ad2fd 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -234,7 +234,7 @@ Model.prototype._del = function(segments, cb) { var id = segments[1]; model.root.collections[collectionName].remove(id); } - model.emit('change', segments, [void 0, previous, model._pass]); + model.emit('change', segments, [undefined, previous, model._pass]); return previous; } return this._mutate(segments, del, cb); @@ -449,6 +449,7 @@ Model.prototype.remove = function() { subpath = arguments[0]; } } else { + // eslint-disable-next-line no-lonely-if if (typeof arguments[0] === 'number') { index = arguments[0]; howMany = arguments[1]; @@ -683,7 +684,7 @@ Model.prototype._subtypeSubmit = function(segments, subtype, subtypeOp, cb) { // same object returned by reference or not. This may cause change // listeners to over-trigger, but that is usually going to be better than // under-triggering - model.emit('change', segments, [value, void 0, pass]); + model.emit('change', segments, [value, undefined, pass]); return previous; } return this._mutate(segments, subtypeSubmit, cb); diff --git a/lib/Model/ref.js b/lib/Model/ref.js index d0d5e7787..05e85b53b 100644 --- a/lib/Model/ref.js +++ b/lib/Model/ref.js @@ -67,9 +67,8 @@ function addIndexListeners(model) { function refChange(model, dereferenced, eventArgs, segments) { var value = eventArgs[0]; - var pass = eventArgs[2]; // Detect if we are deleting vs. setting to undefined - if (value === void 0) { + if (value === undefined) { var parentSegments = segments.slice(); var last = parentSegments.pop(); var parent = model._get(parentSegments); @@ -84,7 +83,7 @@ function refLoad(model, dereferenced, eventArgs) { var value = eventArgs[0]; model._set(dereferenced, value); } -function refUnload(model, dereferenced, eventArgs) { +function refUnload(model, dereferenced) { model._del(dereferenced); } function refInsert(model, dereferenced, eventArgs) { diff --git a/lib/Model/refList.js b/lib/Model/refList.js index b6ffd5c84..852388418 100644 --- a/lib/Model/refList.js +++ b/lib/Model/refList.js @@ -121,16 +121,16 @@ function patchFromEvent(type, segments, eventArgs, refList) { * @param {RefList} refList * @param {Array} values */ -function setNewToValues(model, refList, values, fn) { +function setNewToValues(model, refList, values) { var ids = []; for (var i = 0; i < values.length; i++) { var value = values[i]; var id = refList.idByItem(value); - if (id === void 0 && typeof value === 'object') { + if (id === undefined && typeof value === 'object') { id = value.id = model.id(); } var toSegments = refList.toSegmentsByItem(value); - if (id === void 0 || toSegments === void 0) { + if (id === undefined || toSegments === undefined) { throw new Error('Unable to add item to refList: ' + value); } if (model._get(toSegments) !== value) { @@ -155,7 +155,6 @@ function patchToEvent(type, segments, eventArgs, refList) { // Mutation on the `to` object itself if (segmentsLength === toLength) { if (type === 'insert') { - var insertIndex = eventArgs[0]; var values = eventArgs[1]; for (var i = 0; i < values.length; i++) { var value = values[i]; @@ -178,7 +177,7 @@ function patchToEvent(type, segments, eventArgs, refList) { if (!indices) continue; for (var j = 0, indicesLen = indices.length; j < indicesLen; j++) { var outSegments = refList.fromSegments.concat(indices[j]); - model._set(outSegments, void 0); + model._set(outSegments, undefined); } } return; @@ -227,9 +226,9 @@ function patchToEvent(type, segments, eventArgs, refList) { previous = eventArgs[1]; } else if (type === 'load') { value = eventArgs[0]; - previous = void 0; + previous = undefined; } else if (type === 'unload') { - value = void 0; + value = undefined; previous = eventArgs[0]; } var newIndices = refList.indicesByItem(value); @@ -239,7 +238,7 @@ function patchToEvent(type, segments, eventArgs, refList) { // The changed item used to refer to some indices, but no longer does for (var i = 0; i < oldIndices.length; i++) { var outSegments = refList.fromSegments.concat(oldIndices[i]); - model._set(outSegments, void 0); + model._set(outSegments, undefined); } } if (newIndices) { @@ -416,7 +415,7 @@ RefList.prototype.dereference = function(segments, i) { }; RefList.prototype.toSegmentsByItem = function(item) { var key = this.idByItem(item); - if (key === void 0) return; + if (key === undefined) return; return this.toSegments.concat(key); }; RefList.prototype.idByItem = function(item) { @@ -432,7 +431,7 @@ RefList.prototype.indicesByItem = function(item) { if (!ids) return; var indices; var index = -1; - while (true) { + for (;;) { index = ids.indexOf(id, index + 1); if (index === -1) break; if (indices) { diff --git a/lib/util.js b/lib/util.js index f43f50de0..bcb2c46bf 100644 --- a/lib/util.js +++ b/lib/util.js @@ -119,6 +119,7 @@ function equal(a, b) { } function equalsNaN(x) { + // eslint-disable-next-line no-self-compare return x !== x; } diff --git a/test/Model/docs.js b/test/Model/docs.js index 6786b5470..588096a09 100644 --- a/test/Model/docs.js +++ b/test/Model/docs.js @@ -4,7 +4,7 @@ module.exports = function(createDoc) { describe('get', function() { it('creates an undefined doc', function() { var doc = createDoc(); - expect(doc.get()).eql(void 0); + expect(doc.get()).eql(undefined); }); it('gets a defined doc', function() { var doc = createDoc(); @@ -17,17 +17,17 @@ module.exports = function(createDoc) { }); it('gets a property on an undefined document', function() { var doc = createDoc(); - expect(doc.get(['x'])).eql(void 0); + expect(doc.get(['x'])).eql(undefined); }); it('gets an undefined property', function() { var doc = createDoc(); doc.set([], {}, function() {}); - expect(doc.get(['x'])).eql(void 0); + expect(doc.get(['x'])).eql(undefined); }); it('gets a defined property', function() { var doc = createDoc(); doc.set([], { - 'id': 'green' + id: 'green' }, function() {}); expect(doc.get(['id'])).eql('green'); }); @@ -71,13 +71,13 @@ module.exports = function(createDoc) { it('sets a property', function() { var doc = createDoc(); var previous = doc.set(['shown'], false, function() {}); - expect(previous).equal(void 0); + expect(previous).equal(undefined); expect(doc.get(['shown'])).eql(false); }); it('sets a multi-nested property', function() { var doc = createDoc(); var previous = doc.set(['rgb', 'green', 'float'], 1, function() {}); - expect(previous).equal(void 0); + expect(previous).equal(undefined); expect(doc.get(['rgb'])).eql({ green: { float: 1 @@ -89,12 +89,12 @@ module.exports = function(createDoc) { var previous = doc.set([], { id: 'green' }, function() {}); - expect(previous).equal(void 0); + expect(previous).equal(undefined); expect(doc.get()).eql({ id: 'green' }); previous = doc.set(['shown'], false, function() {}); - expect(previous).equal(void 0); + expect(previous).equal(undefined); expect(doc.get()).eql({ id: 'green', shown: false @@ -103,7 +103,7 @@ module.exports = function(createDoc) { it('returns the previous value on set', function() { var doc = createDoc(); var previous = doc.set(['shown'], false, function() {}); - expect(previous).equal(void 0); + expect(previous).equal(undefined); expect(doc.get(['shown'])).eql(false); previous = doc.set(['shown'], true, function() {}); expect(previous).equal(false); @@ -131,8 +131,8 @@ module.exports = function(createDoc) { it('can del on an undefined path without effect', function() { var doc = createDoc(); var previous = doc.del(['rgb', '2'], function() {}); - expect(previous).equal(void 0); - expect(doc.get()).eql(void 0); + expect(previous).equal(undefined); + expect(doc.get()).eql(undefined); }); it('can del on a document', function() { var doc = createDoc(); @@ -143,7 +143,7 @@ module.exports = function(createDoc) { expect(previous).eql({ id: 'green' }); - expect(doc.get()).eql(void 0); + expect(doc.get()).eql(undefined); }); it('can del on a nested property', function() { var doc = createDoc(); From 53892b9a56b5f86ee66f4e2acb4d52d54b96f8ec Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 3 May 2016 00:28:21 -0700 Subject: [PATCH 140/479] sharedb: ^0.11.36 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 091ca2430..cf1e5bea0 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "arraydiff": "^0.1.1", "deep-is": "^0.1.3", - "sharedb": "^0.11.0", + "sharedb": "^0.11.36", "uuid": "^2.0.1" }, "devDependencies": { From 4c7a2167b6eb3ab63eb6f28bb5b3103ddd44bea6 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 3 May 2016 01:00:04 -0700 Subject: [PATCH 141/479] add tests of issues with simultaneous subscribe --- test/Model/loading.js | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 test/Model/loading.js diff --git a/test/Model/loading.js b/test/Model/loading.js new file mode 100644 index 000000000..af142a2dd --- /dev/null +++ b/test/Model/loading.js @@ -0,0 +1,48 @@ +var expect = require('../util').expect; +var racer = require('../../lib/index'); + +describe('loading', function() { + describe('subscribe', function() { + beforeEach(function(done) { + this.backend = racer.createBackend(); + // Add a delay on all messages to help catch race issues + var delay = 5; + this.backend.use('receive', function(request, next) { + delay++; + setTimeout(next, delay); + }); + this.model = this.backend.createModel(); + this.model.connection.on('connected', done); + }); + + it('calls back simultaneous subscribes to the same document', function(done) { + var doc = this.model.connection.get('colors', 'green'); + expect(doc.version).equal(null); + + var calls = 0; + var cb = function(err) { + if (err) return done(err); + expect(doc.version).equal(0); + calls++; + }; + for (var i = 3; i--;) { + this.model.subscribe('colors.green', cb); + } + + this.model.whenNothingPending(function() { + expect(calls).equal(3); + done(); + }); + }); + + it('calls back when doc is already subscribed', function(done) { + var model = this.model; + var doc = model.connection.get('colors', 'green'); + model.subscribe('colors.green', function(err) { + if (err) return done(err); + expect(doc.subscribed).equal(true); + model.subscribe('colors.green', done); + }); + }); + }); +}); From 30beb673654faabf21cbda519cc5fdf385a44577 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 3 May 2016 01:05:09 -0700 Subject: [PATCH 142/479] fix bug with simultaneous subscribes to the same doc uses sharedb's state tracking to call back if already subscribed. This is what was intended before. However, no longer will avoid multiple subscribes being sent for the same document at the same time before the first subscribe returns --- lib/Model/subscriptions.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index e20bea2a5..b7a94346c 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -93,12 +93,14 @@ Model.prototype.subscribeDoc = function(collectionName, id, cb) { // Maintain a count of subscribes so that we can unload the document // when there are no remaining fetches or subscribes for that document this._context.subscribeDoc(collectionName, id); - var count = this.root._subscribedDocs.increment(collectionName, id); - // Already requested a subscribe, so just return - if (count > 1) return cb(); + this.root._subscribedDocs.increment(collectionName, id); - // Subscribe var doc = this.getOrCreateDoc(collectionName, id); + // Early return if we know we are already subscribed + if (doc.shareDoc.subscribed) { + return cb(); + } + // Subscribe if (this.root.fetchOnly) { doc.shareDoc.fetch(cb); } else { From 4bcb1156923c70516f5bc1ee28913ae5d6b2af10 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 3 May 2016 01:05:19 -0700 Subject: [PATCH 143/479] 0.8.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cf1e5bea0..3922058cb 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.8.11", + "version": "0.8.12", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From dbec0725f041db52917f322d9b4eef5131b1e055 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 11 May 2016 13:40:08 -0700 Subject: [PATCH 144/479] implement Model::createNull --- lib/Model/mutators.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index 1eb2ad2fd..286b1068c 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -123,6 +123,40 @@ Model.prototype._create = function(segments, value, cb) { this._mutate(segments, create, cb); }; +Model.prototype.createNull = function() { + var subpath, value, cb; + if (arguments.length === 0) { + value = {}; + } else if (arguments.length === 1) { + if (typeof arguments[0] === 'function') { + value = {}; + cb = arguments[0]; + } else { + value = arguments[0]; + } + } else if (arguments.length === 2) { + if (typeof arguments[1] === 'function') { + value = arguments[0]; + cb = arguments[1]; + } else { + subpath = arguments[0]; + value = arguments[1]; + } + } else { + subpath = arguments[0]; + value = arguments[1]; + cb = arguments[2]; + } + var segments = this._splitPath(subpath); + return this._createNull(segments, value, cb); +}; +Model.prototype._createNull = function(segments, value, cb) { + segments = this._dereference(segments); + var doc = this.getDoc(segments[0], segments[1]); + if (doc && doc.get() != null) return; + this._create(segments, value, cb); +}; + Model.prototype.add = function() { var subpath, value, cb; if (arguments.length === 0) { From fe7c68a35d03f27c54e166ea270fe0908226f1f2 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 11 May 2016 13:40:25 -0700 Subject: [PATCH 145/479] reorder mutators file a bit --- lib/Model/mutators.js | 63 +++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index 286b1068c..c0cbac757 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -51,6 +51,37 @@ Model.prototype._set = function(segments, value, cb) { return this._mutate(segments, set, cb); }; +Model.prototype.setNull = function() { + var subpath, value, cb; + if (arguments.length === 1) { + value = arguments[0]; + } else if (arguments.length === 2) { + subpath = arguments[0]; + value = arguments[1]; + } else { + subpath = arguments[0]; + value = arguments[1]; + cb = arguments[2]; + } + var segments = this._splitPath(subpath); + return this._setNull(segments, value, cb); +}; +Model.prototype._setNull = function(segments, value, cb) { + segments = this._dereference(segments); + var model = this; + function setNull(doc, docSegments, fnCb) { + var previous = doc.get(docSegments); + if (previous != null) { + fnCb(); + return previous; + } + doc.set(docSegments, value, fnCb); + model.emit('change', segments, [value, previous, model._pass]); + return value; + } + return this._mutate(segments, setNull, cb); +}; + Model.prototype.setEach = function() { var subpath, object, cb; if (arguments.length === 1) { @@ -102,7 +133,6 @@ Model.prototype.create = function() { var segments = this._splitPath(subpath); return this._create(segments, value, cb); }; - Model.prototype._create = function(segments, value, cb) { segments = this._dereference(segments); if (segments.length !== 2) { @@ -210,37 +240,6 @@ Model.prototype._add = function(segments, value, cb) { return id; }; -Model.prototype.setNull = function() { - var subpath, value, cb; - if (arguments.length === 1) { - value = arguments[0]; - } else if (arguments.length === 2) { - subpath = arguments[0]; - value = arguments[1]; - } else { - subpath = arguments[0]; - value = arguments[1]; - cb = arguments[2]; - } - var segments = this._splitPath(subpath); - return this._setNull(segments, value, cb); -}; -Model.prototype._setNull = function(segments, value, cb) { - segments = this._dereference(segments); - var model = this; - function setNull(doc, docSegments, fnCb) { - var previous = doc.get(docSegments); - if (previous != null) { - fnCb(); - return previous; - } - doc.set(docSegments, value, fnCb); - model.emit('change', segments, [value, previous, model._pass]); - return value; - } - return this._mutate(segments, setNull, cb); -}; - Model.prototype.del = function() { var subpath, cb; if (arguments.length === 1) { From 950ca79537e99751c8e41b87e289a4252f2cb67f Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 11 May 2016 13:40:39 -0700 Subject: [PATCH 146/479] 0.8.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3922058cb..9e2325129 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.8.12", + "version": "0.8.13", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 3a9d1b9df44e2e9932a0c06ebaf30a6139152e7c Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 27 Jul 2016 20:24:01 -0700 Subject: [PATCH 147/479] fix lint rule --- lib/Model/LocalDoc.js | 3 ++- lib/Model/contexts.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Model/LocalDoc.js b/lib/Model/LocalDoc.js index d94af104d..eff4f4bf1 100644 --- a/lib/Model/LocalDoc.js +++ b/lib/Model/LocalDoc.js @@ -217,5 +217,6 @@ LocalDoc.prototype._arrayApply = function(segments, fn, cb) { }; function nodeCreateArray(node, key) { - return node[key] || (node[key] = []); + var node = node[key] || (node[key] = []); + return node; } diff --git a/lib/Model/contexts.js b/lib/Model/contexts.js index 26e66c806..a63f1c859 100644 --- a/lib/Model/contexts.js +++ b/lib/Model/contexts.js @@ -21,8 +21,9 @@ Model.prototype.setContext = function(id) { }; Model.prototype.getOrCreateContext = function(id) { - return this.root._contexts[id] || + var context = this.root._contexts[id] || (this.root._contexts[id] = new Context(this, id)); + return context; }; Model.prototype.unload = function(id) { From 4d51d63c25f0f529475a907dbfdc9c3792129bbd Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 27 Jul 2016 22:15:18 -0700 Subject: [PATCH 148/479] update sharedb to ^1.0.0-beta --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e2325129..783bca6b5 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "arraydiff": "^0.1.1", "deep-is": "^0.1.3", - "sharedb": "^0.11.36", + "sharedb": "^1.0.0-beta", "uuid": "^2.0.1" }, "devDependencies": { From e5061f9bab0603da3b6eefa0d7581d3cb99d8cb6 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 27 Jul 2016 22:15:42 -0700 Subject: [PATCH 149/479] 0.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 783bca6b5..08453ec51 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.8.13", + "version": "0.9.0", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From a2460894fea7df2dc25ddf2ea60c79a89e07eb0c Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 17 Aug 2016 22:28:39 -0700 Subject: [PATCH 150/479] avoid requiring sharedb server code from model code for smaller bundle --- lib/Model/Query.js | 2 +- lib/Model/bundle.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index abc6c23f5..c0795145a 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -1,6 +1,6 @@ var util = require('../util'); var Model = require('./Model'); -var defaultType = require('sharedb').types.defaultType; +var defaultType = require('sharedb/lib/client').types.defaultType; module.exports = Query; diff --git a/lib/Model/bundle.js b/lib/Model/bundle.js index 88175940f..d5b0fdd44 100644 --- a/lib/Model/bundle.js +++ b/lib/Model/bundle.js @@ -1,5 +1,5 @@ var Model = require('./Model'); -var defaultType = require('sharedb').types.defaultType; +var defaultType = require('sharedb/lib/client').types.defaultType; Model.BUNDLE_TIMEOUT = 10 * 1000; From 3d9cb6ea6d5a8326e7ad51d7bc34542ec20f0e1d Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 17 Aug 2016 22:29:13 -0700 Subject: [PATCH 151/479] 0.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 08453ec51..5c46113d1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.0", + "version": "0.9.1", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From a63b9e3defc962972e5660ae202e3d77089e7c4e Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 19 Sep 2016 09:58:47 -0700 Subject: [PATCH 152/479] fix significant performance issue serializing large queries by not iterating keys of collection --- lib/Model/Query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index c0795145a..7a9189bc3 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -491,8 +491,8 @@ Query.prototype.serialize = function() { var id = ids[i]; var doc = collection.docs[id]; if (doc) { - collection.remove(id); var result = [doc.shareDoc.data, doc.shareDoc.version]; + delete collection.docs[id]; if (doc.shareDoc.type !== defaultType) { result.push(doc.shareDoc.type && doc.shareDoc.type.name); } From fdaa8b0337ef5ce9deccb8fceed6e895f7f822d0 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 19 Sep 2016 10:00:15 -0700 Subject: [PATCH 153/479] refactor query serialization instead of stripping ids from doc data for faster serialization --- lib/Model/Query.js | 41 +++++++++++++++++++++++------------------ lib/Model/bundle.js | 15 --------------- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 7a9189bc3..f7c8b4824 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -52,28 +52,30 @@ Model.prototype._initQueries = function(items) { var counts = item[0]; var collectionName = item[1]; var expression = item[2]; - var ids = item[3] || []; - var results = item[4] || []; - var options = item[5]; - var extra = item[6]; + var results = item[3] || []; + var options = item[4]; + var extra = item[5]; var query = new Query(this, collectionName, expression, options); queries.add(query); - - query._addMapIds(ids); - this._set(query.idsSegments, ids); query._setExtra(extra); + var ids = []; for (var resultIndex = 0; resultIndex < results.length; resultIndex++) { var result = results[resultIndex]; - if (!result) continue; - var id = ids[resultIndex]; - var snapshot = { - data: result[0], - v: result[1], - type: result[2] - }; + if (typeof result === 'string') { + ids.push(result); + continue; + } + var data = result[0]; + var v = result[1]; + var id = result[2] || data.id; + var type = result[3]; + ids.push(id); + var snapshot = {data: data, v: v, type: type}; this.getOrCreateDoc(collectionName, id, snapshot); } + query._addMapIds(ids); + this._set(query.idsSegments, ids); for (var countIndex = 0; countIndex < counts.length; countIndex++) { var count = counts[countIndex]; @@ -491,14 +493,18 @@ Query.prototype.serialize = function() { var id = ids[i]; var doc = collection.docs[id]; if (doc) { - var result = [doc.shareDoc.data, doc.shareDoc.version]; delete collection.docs[id]; + var data = doc.shareDoc.data; + var result = [data, doc.shareDoc.version]; + if (!data || data.id !== id) { + result[2] = id; + } if (doc.shareDoc.type !== defaultType) { - result.push(doc.shareDoc.type && doc.shareDoc.type.name); + result[3] = doc.shareDoc.type && doc.shareDoc.type.name; } results.push(result); } else { - results.push(0); + results.push(id); } } } @@ -522,7 +528,6 @@ Query.prototype.serialize = function() { counts, this.collectionName, this.expression, - ids, results, this.options, this.getExtra() diff --git a/lib/Model/bundle.js b/lib/Model/bundle.js index d5b0fdd44..3381bf000 100644 --- a/lib/Model/bundle.js +++ b/lib/Model/bundle.js @@ -19,7 +19,6 @@ Model.prototype.bundle = function(cb) { root.whenNothingPending(function finishBundle() { clearTimeout(timeout); - stripIds(root); var bundle = { queries: root._queries.toJSON(), contexts: root._contexts, @@ -37,20 +36,6 @@ Model.prototype.bundle = function(cb) { }); }; -function stripIds(root) { - // Strip ids from remote documents, which get added automatically. Don't do - // this for local documents, since they are often not traditional object - // documents with ids and it doesn't make sense to add ids to them always - for (var collectionName in root.data) { - if (root._isLocal(collectionName)) continue; - var collectionData = root.data[collectionName]; - for (var id in collectionData) { - var docData = collectionData[id]; - if (docData) delete docData.id; - } - } -} - function stripComputed(root) { var silentModel = root.silent(); var refListsMap = root._refLists.fromMap; From e87440fb865991713f96161fe795659764c7bd28 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 19 Sep 2016 10:00:27 -0700 Subject: [PATCH 154/479] 0.9.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c46113d1..1da7b3518 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.1", + "version": "0.9.2", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From c3afc9b4bd1af831da87f170abd1f39b957a2b7e Mon Sep 17 00:00:00 2001 From: Michael Brade Date: Thu, 26 Jan 2017 12:29:44 +0100 Subject: [PATCH 155/479] fixed memory leak in CollectionCounter, added unit tests --- lib/Model/CollectionCounter.js | 2 +- test/Model/CollectionCounter.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 test/Model/CollectionCounter.js diff --git a/lib/Model/CollectionCounter.js b/lib/Model/CollectionCounter.js index 74751cb9e..6b413c7e8 100644 --- a/lib/Model/CollectionCounter.js +++ b/lib/Model/CollectionCounter.js @@ -30,7 +30,7 @@ CollectionCounter.prototype.decrement = function(collectionName, id) { // Check if the collection still has any keys // eslint-disable-next-line no-unused-vars for (var key in collection) return 0; - delete this.collections[collection]; + delete this.collections[collectionName]; return 0; }; CollectionCounter.prototype.toJSON = function() { diff --git a/test/Model/CollectionCounter.js b/test/Model/CollectionCounter.js new file mode 100644 index 000000000..28c358e17 --- /dev/null +++ b/test/Model/CollectionCounter.js @@ -0,0 +1,29 @@ +var expect = require('../util').expect; +var CollectionCounter = require('../../lib/Model/CollectionCounter'); + +describe('CollectionCounter', function() { + var cc = new CollectionCounter(); + + it('increment', function() { + expect(cc.toJSON()).to.be(undefined); + expect(cc.get('numbers', 'one')).to.be(undefined); + expect(cc.increment('numbers', 'one')).to.equal(1); + expect(cc.increment('numbers', 'one')).to.equal(2); + }); + it('toJSON', function() { + expect(cc.toJSON()).to.eql({ + numbers: { + one: 2 + } + }); + }); + it('decrement', function() { + expect(cc.get('numbers', 'one')).to.equal(2); + + expect(cc.decrement('numbers', 'one')).to.equal(1); + expect(cc.decrement('numbers', 'one')).to.equal(0); + + expect(cc.get('numbers', 'one')).to.be(undefined); + expect(cc.toJSON()).to.be(undefined); + }); +}); From f54a684b3025456b7fafe5070fad8c92f4db4042 Mon Sep 17 00:00:00 2001 From: Zeus Date: Mon, 29 May 2017 16:45:15 -0700 Subject: [PATCH 156/479] Fix typo in Doc.js: `lenth` -> `length` --- lib/Model/Doc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/Doc.js b/lib/Model/Doc.js index ea892894c..90ffdbbe6 100644 --- a/lib/Model/Doc.js +++ b/lib/Model/Doc.js @@ -8,7 +8,7 @@ function Doc(model, collectionName, id) { Doc.prototype.path = function(segments) { var path = this.collectionName + '.' + this.id; - if (segments && segments.lenth) path += '.' + segments.join('.'); + if (segments && segments.length) path += '.' + segments.join('.'); return path; }; From 9d6b56628ed21aebf7fff94cb7c0def0469c5991 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 28 Jul 2017 13:16:58 -0700 Subject: [PATCH 157/479] Change Query.idMap to count ids, to be defensive against duplicate ids in query results (see https://github.com/share/sharedb-mongo/issues/55) --- lib/Model/Query.js | 12 +++++++++--- test/Model/query.js | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index f7c8b4824..585fc2f36 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -158,7 +158,7 @@ function Query(model, collectionName, expression, options) { // because otherwise maybeUnload would be looping through the entire results // set of each query on the same collection for every doc checked // - // Map of id -> true + // Map of id -> count of ids this.idMap = {}; } @@ -293,7 +293,12 @@ Query.prototype._shareSubscribe = function(options, cb) { Query.prototype._removeMapIds = function(ids) { for (var i = ids.length; i--;) { var id = ids[i]; - delete this.idMap[id]; + if (this.idMap[id] > 0) { + this.idMap[id]--; + } + if (this.idMap[id] === 0) { + delete this.idMap[id]; + } } // Technically this isn't quite right and we might not wait the full unload // delay if someone else calls maybeUnload for the same doc id. However, @@ -312,7 +317,8 @@ Query.prototype._removeMapIds = function(ids) { Query.prototype._addMapIds = function(ids) { for (var i = ids.length; i--;) { var id = ids[i]; - this.idMap[id] = true; + this.idMap[id] = this.idMap[id] || 0; + this.idMap[id]++; } }; Query.prototype._diffMapIds = function(ids) { diff --git a/test/Model/query.js b/test/Model/query.js index e8d69c03c..29cdc54c0 100644 --- a/test/Model/query.js +++ b/test/Model/query.js @@ -1,4 +1,5 @@ var expect = require('../util').expect; +var racer = require('../../lib'); var Model = require('../../lib/Model'); describe('query', function() { @@ -14,4 +15,29 @@ describe('query', function() { expect(query.expression).eql([{x: null}, {x: {y: null, z: 0}}]); }); }); + + describe('idMap', function() { + beforeEach('create in-memory backend and model', function() { + this.backend = racer.createBackend(); + this.model = this.backend.createModel(); + }); + it('handles insert and remove of a duplicate id', function() { + var query = this.model.query('myCollection', {key: 'myVal'}); + query.subscribe(); + query.shareQuery.emit('insert', [ + {id: 'a'}, + {id: 'b'}, + {id: 'c'}, + ], 0); + // Add and immediately remove a duplicate id. + query.shareQuery.emit('insert', [ + {id: 'a'}, + ], 3); + query.shareQuery.emit('remove', [ + {id: 'a'}, + ], 3); + // 'a' is still present once in the results, should still be in the map. + expect(query.idMap).to.only.have.keys(['a', 'b', 'c']); + }); + }); }); From e4cc334e406caa2612e232f632a39a964815347f Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 28 Jul 2017 16:13:23 -0700 Subject: [PATCH 158/479] Switch query.idMap check to use "> 0" --- lib/Model/subscriptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index b7a94346c..e9d9147d7 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -191,7 +191,7 @@ Model.prototype._hasDocReferences = function(collectionName, id) { for (var hash in queries) { var query = queries[hash]; if (!query.subscribeCount && !query.fetchCount) continue; - if (query.idMap[id]) return true; + if (query.idMap[id] > 0) return true; } } From 34d63dc10714bcdc77991c73c11e1e4aba50d635 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 28 Jul 2017 16:14:27 -0700 Subject: [PATCH 159/479] Query#_addMapIds: Consolidate idMap update to one line --- lib/Model/Query.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 585fc2f36..f47039c7b 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -317,8 +317,7 @@ Query.prototype._removeMapIds = function(ids) { Query.prototype._addMapIds = function(ids) { for (var i = ids.length; i--;) { var id = ids[i]; - this.idMap[id] = this.idMap[id] || 0; - this.idMap[id]++; + this.idMap[id] = (this.idMap[id] || 0) + 1; } }; Query.prototype._diffMapIds = function(ids) { From 51673d7063d9d34516fe49dcfed3b263a9aa79e5 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 28 Jul 2017 16:20:26 -0700 Subject: [PATCH 160/479] Query#_removeMapIds: Refactoring to remove a conditional check --- lib/Model/Query.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index f47039c7b..679f4b280 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -293,10 +293,9 @@ Query.prototype._shareSubscribe = function(options, cb) { Query.prototype._removeMapIds = function(ids) { for (var i = ids.length; i--;) { var id = ids[i]; - if (this.idMap[id] > 0) { + if (this.idMap[id] > 1) { this.idMap[id]--; - } - if (this.idMap[id] === 0) { + } else { delete this.idMap[id]; } } From 12f15dff9a0a9fadac8efa5072d761522c2064a7 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 28 Jul 2017 16:25:28 -0700 Subject: [PATCH 161/479] fix lint errors --- test/Model/query.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Model/query.js b/test/Model/query.js index 29cdc54c0..33ae2938d 100644 --- a/test/Model/query.js +++ b/test/Model/query.js @@ -27,14 +27,14 @@ describe('query', function() { query.shareQuery.emit('insert', [ {id: 'a'}, {id: 'b'}, - {id: 'c'}, + {id: 'c'} ], 0); // Add and immediately remove a duplicate id. query.shareQuery.emit('insert', [ - {id: 'a'}, + {id: 'a'} ], 3); query.shareQuery.emit('remove', [ - {id: 'a'}, + {id: 'a'} ], 3); // 'a' is still present once in the results, should still be in the map. expect(query.idMap).to.only.have.keys(['a', 'b', 'c']); From 4e6b7d0372f056a6e2a395f03ebea9d0eaf992db Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 28 Jul 2017 16:27:24 -0700 Subject: [PATCH 162/479] 0.9.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1da7b3518..897042422 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.2", + "version": "0.9.3", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 01eee36a2c0fd473e335e9a0330bbcf6d7cbc2d3 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 29 Apr 2019 15:50:35 -0700 Subject: [PATCH 163/479] Dedupe queries when unbundling (_initQueries) --- lib/Model/Query.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 679f4b280..615dee8d2 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -55,8 +55,12 @@ Model.prototype._initQueries = function(items) { var results = item[3] || []; var options = item[4]; var extra = item[5]; - var query = new Query(this, collectionName, expression, options); - queries.add(query); + var query = this.root._queries.get(collectionName, expression, options); + if (!query) { + query = new Query(this, collectionName, expression, options); + queries.add(query); + } + query._setExtra(extra); var ids = []; From 8dc009da58ab1c4a2a5f6d01b0df9c128329c84e Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 29 Apr 2019 17:07:45 -0700 Subject: [PATCH 164/479] Add test for unbundle deduping --- test/Model/unbundle.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 test/Model/unbundle.js diff --git a/test/Model/unbundle.js b/test/Model/unbundle.js new file mode 100644 index 000000000..7020740b3 --- /dev/null +++ b/test/Model/unbundle.js @@ -0,0 +1,39 @@ +var expect = require('../util').expect; +var racer = require('../../lib/index'); + +describe('unbundle', function() { + it('dedupes against existing queries', function(done) { + var backend = racer.createBackend(); + var setupModel = backend.createModel(); + setupModel.add('dogs', {id: 'fido', name: 'Fido'}); + setupModel.whenNothingPending(function() { + var model1 = backend.createModel({fetchOnly: true}); + var model1Query = model1.query('dogs', {}); + model1.subscribe(model1Query, function() { + model1.bundle(function(err, bundleData) { + if (err) { + return done(err); + } + // Simulate serialization of bundle data between server and client. + bundleData = JSON.parse(JSON.stringify(bundleData)); + + var model2 = backend.createModel(); + // Unbundle should load data into model. + model2.unbundle(bundleData); + expect(model2.get('dogs.fido.name')).to.eql('Fido'); + // Unloaded data available until after `unloadDelay` ms has elapsed. + model2.unloadDelay = 4; + model2.unload(); + expect(model2.get('dogs.fido.name')).to.eql('Fido'); + // Another unbundle should re-increment subscribe count, so the data + // should still be present even after `unloadDelay` has passed. + model2.unbundle(bundleData); + setTimeout(function() { + expect(model2.get('dogs.fido.name')).to.eql('Fido'); + done(); + }, 8); + }); + }); + }); + }); +}); From a1e7347085627a37d3614eb1f40a66308c780b9a Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 3 Jun 2019 15:39:59 -0700 Subject: [PATCH 165/479] Add Model#_getOrCreateQuery method This behaves exactly like the existing Model#query, except that it accepts a Query constructor function to use, allowing subclasses of Query to be utilized. --- lib/Model/Query.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 615dee8d2..e6f51e8bc 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -9,15 +9,30 @@ Model.INITS.push(function(model) { }); Model.prototype.query = function(collectionName, expression, options) { - expression = this.sanitizeQuery(expression); // DEPRECATED: Passing in a string as the third argument specifies the db // option for backward compatibility if (typeof options === 'string') { options = {db: options}; } + return this._getOrCreateQuery(collectionName, expression, options, Query); +}; + +/** + * If an existing query is present with the same `collectionName`, `expression`, + * and `options`, then returns the existing query; otherwise, constructs and + * returns a new query using `QueryConstructor`. + * + * @param {string} collectionName + * @param {*} expression + * @param {*} options + * @param {new (model: Model, collectionName: string, expression: any, options: any) => Query} QueryConstructor - + * constructor function for a Query, to create one if not already present on this model + */ +Model.prototype._getOrCreateQuery = function(collectionName, expression, options, QueryConstructor) { + expression = this.sanitizeQuery(expression); var query = this.root._queries.get(collectionName, expression, options); if (query) return query; - query = new Query(this, collectionName, expression, options); + query = new QueryConstructor(this, collectionName, expression, options); this.root._queries.add(query); return query; }; From 4f7a647de1487f0f556046775155dc05d8371299 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 5 Jun 2019 15:35:43 -0700 Subject: [PATCH 166/479] 0.9.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 897042422..be35f0bb4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.3", + "version": "0.9.4", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 38034f5503a6d767e244d8b677326c7f14dff309 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 6 Jun 2019 09:48:13 -0700 Subject: [PATCH 167/479] Fix homepage like in package.json racerjs.com no longer points to anything. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be35f0bb4..1d9d29f80 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "racer", "description": "Realtime model synchronization engine for Node.js", - "homepage": "http://racerjs.com/", + "homepage": "https://github.com/derbyjs/racer", "repository": { "type": "git", "url": "git://github.com/derbyjs/racer.git" From 9bba9abf0e7f74ea2bb9144e28db99e4ef1fe389 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 3 Jul 2019 15:02:50 -0700 Subject: [PATCH 168/479] initial implementation of boolean option for model.start --- lib/Model/fn.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/Model/fn.js b/lib/Model/fn.js index f9513fcec..4f45d263e 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -158,6 +158,9 @@ function Fn(model, name, from, inputPaths, fns, options) { // Mode can be 'diffDeep', 'diff', 'arrayDeep', or 'array' this.mode = (options && options.mode) || 'diffDeep'; + + this.async = !!(options && options.async); + this.eventDelayed = false; } Fn.prototype.apply = function(fn, inputs) { @@ -185,6 +188,20 @@ Fn.prototype.set = function(value, pass) { }; Fn.prototype.onInput = function(pass) { + if (this.async) { + if (this.eventDelayed) return; + this.eventDelayed = true; + var fn = this; + process.nextTick(function() { + fn._onInput(pass); + fn.eventDelayed = false; + }); + return; + } + return this._onInput(pass); +}; + +Fn.prototype._onInput = function(pass) { var value = (this.copyOutput) ? util.deepCopy(this.get()) : this.get(); this._setValue(this.model.pass(pass, true), this.fromSegments, value); return value; From f738055a5f63418c77ed8c98e757a458e46a4c21 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 3 Jul 2019 16:09:48 -0700 Subject: [PATCH 169/479] Initially evaluate the function on call to model.start() when async --- lib/Model/fn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/fn.js b/lib/Model/fn.js index 4f45d263e..f83d22878 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -109,7 +109,7 @@ Fns.prototype.start = function(name, path, inputPaths, fns, options) { fns || (fns = this.nameMap[name] || defaultFns[name]); var fn = new Fn(this.model, name, path, inputPaths, fns, options); this.fromMap[path] = fn; - return fn.onInput(); + return fn._onInput(); }; Fns.prototype.stop = function(path) { From c75966078b6c1738b4161ebe24f5155d01a4a49b Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 3 Jul 2019 16:28:14 -0700 Subject: [PATCH 170/479] add tests for model.start with {async: true} option --- test/Model/fn.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/test/Model/fn.js b/test/Model/fn.js index a708738bf..e9f1d85bf 100644 --- a/test/Model/fn.js +++ b/test/Model/fn.js @@ -136,6 +136,86 @@ describe('fn', function() { expect(model.get('_nums.sum')).to.equal(5); }); }); + describe('start with async option', function() { + it('sets the output immediately on start', function() { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + var value = model.start('_nums.sum', '_nums.a', '_nums.b', {async: true}, 'sum'); + expect(value).to.equal(6); + expect(model.get('_nums.sum')).to.equal(6); + }); + it('async sets the output when an input changes', function(done) { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + model.start('_nums.sum', '_nums.a', '_nums.b', {async: true}, 'sum'); + expect(model.get('_nums.sum')).to.equal(6); + model.set('_nums.a', 5); + // Synchronously, there should be no change + expect(model.get('_nums.sum')).to.equal(6); + // Async, the value should be updated + process.nextTick(function() { + expect(model.get('_nums.sum')).to.equal(9); + done(); + }); + }); + it('no async sets the output multiple times when an input changes multiple times', function() { + var model = new Model(); + var calls = 0; + model.fn('sum', function(a, b) { + calls++; + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + model.start('_nums.sum', '_nums.a', '_nums.b', 'sum'); + expect(model.get('_nums.sum')).to.equal(6); + // Synchronously, the value should change + model.set('_nums.a', 1); + expect(model.get('_nums.sum')).to.equal(5); + model.set('_nums.a', 5); + expect(model.get('_nums.sum')).to.equal(9); + model.set('_nums.b', 10); + expect(model.get('_nums.sum')).to.equal(15); + model.set('_nums.b', 4); + expect(model.get('_nums.sum')).to.equal(9); + expect(calls).to.equal(5); + }); + it('async sets the output when an input changes multiple times', function(done) { + var model = new Model(); + var calls = 0; + model.fn('sum', function(a, b) { + calls++; + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + model.start('_nums.sum', '_nums.a', '_nums.b', {async: true}, 'sum'); + expect(model.get('_nums.sum')).to.equal(6); + // Synchronously, there should be no change + model.set('_nums.a', 1); + expect(model.get('_nums.sum')).to.equal(6); + model.set('_nums.a', 5); + expect(model.get('_nums.sum')).to.equal(6); + model.set('_nums.b', 10); + expect(model.get('_nums.sum')).to.equal(6); + model.set('_nums.b', 4); + expect(model.get('_nums.sum')).to.equal(6); + // Async, the value should be updated once + process.nextTick(function() { + expect(model.get('_nums.sum')).to.equal(9); + expect(calls).to.equal(2); + done(); + }); + }); + }); describe('setter', function() { it('sets the input when the output changes', function() { var model = new Model(); From 9e8bf2570e75ba4758abba19304a1742e568b2b4 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 3 Jul 2019 16:34:55 -0700 Subject: [PATCH 171/479] add test that debouncing is properly reset in async start --- test/Model/fn.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/Model/fn.js b/test/Model/fn.js index e9f1d85bf..fd0b68086 100644 --- a/test/Model/fn.js +++ b/test/Model/fn.js @@ -166,6 +166,29 @@ describe('fn', function() { done(); }); }); + it('debouncing gets reset', function(done) { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + model.start('_nums.sum', '_nums.a', '_nums.b', {async: true}, 'sum'); + expect(model.get('_nums.sum')).to.equal(6); + model.set('_nums.a', 5); + // Synchronously, there should be no change + expect(model.get('_nums.sum')).to.equal(6); + // Async, the value should be updated + process.nextTick(function() { + expect(model.get('_nums.sum')).to.equal(9); + model.set('_nums.b', 0); + expect(model.get('_nums.sum')).to.equal(9); + process.nextTick(function() { + expect(model.get('_nums.sum')).to.equal(5); + done(); + }); + }); + }); it('no async sets the output multiple times when an input changes multiple times', function() { var model = new Model(); var calls = 0; From 37528bae98089d9ca7f85f622b6939ebfe22d6ad Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 3 Jul 2019 17:08:06 -0700 Subject: [PATCH 172/479] fn.eventDelayed -> fn.eventPending --- lib/Model/fn.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Model/fn.js b/lib/Model/fn.js index f83d22878..45706d062 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -160,7 +160,7 @@ function Fn(model, name, from, inputPaths, fns, options) { this.mode = (options && options.mode) || 'diffDeep'; this.async = !!(options && options.async); - this.eventDelayed = false; + this.eventPending = false; } Fn.prototype.apply = function(fn, inputs) { @@ -189,12 +189,12 @@ Fn.prototype.set = function(value, pass) { Fn.prototype.onInput = function(pass) { if (this.async) { - if (this.eventDelayed) return; - this.eventDelayed = true; + if (this.eventPending) return; + this.eventPending = true; var fn = this; process.nextTick(function() { fn._onInput(pass); - fn.eventDelayed = false; + fn.eventPending = false; }); return; } From 5e6ac76528097b55b40d6edbe442ef917f0f48fd Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 3 Jul 2019 17:41:04 -0700 Subject: [PATCH 173/479] 0.9.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be35f0bb4..cc7378ce5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.4", + "version": "0.9.5", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From c922964b65d4ab50b04990b57ba9fe2e9b9c231b Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 8 Jul 2019 20:57:16 -0700 Subject: [PATCH 174/479] Add alternative signatures for Model#start and Model#on that don't use var-args The var-args in the middle of parameter lists for Model#start and Model#on make it difficult to write TypeScript type definitions for them. These backwards-compatible changes add alternative signatures that don't use var-args. 1) Model#start now supports a new array format for input paths: model.start(outPath, inPath1, inPath2, ..., fn); // Old model.start(outPath, [inPath1, inPath2, ...], fn); // New 2) Model#on now checks if the path pattern is a pre-split array, and if so, it calls the listener with a ____Event instance and the wildcard-captured segments as an array: model.on('change', 'foo.**', (captures..., value, previous, passed) => {}); // Old model.on('change', ['foo', '**'], (changeEvent, captures) => {}); // New A ChangeEvent would look like {value, previous, passed}. --- lib/Model/events.js | 205 ++++++++++++++++++++++++++++++++++++++++---- lib/Model/fn.js | 26 +++++- lib/util.js | 4 + 3 files changed, 212 insertions(+), 23 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index 5700d0d32..e5fe44805 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -1,5 +1,8 @@ +// @ts-check + var EventEmitter = require('events').EventEmitter; var util = require('../util'); +/** @type any */ var Model = require('./Model'); // These events are re-emitted as 'all' events, and they are queued up and @@ -79,6 +82,11 @@ Model.prototype.emit = function(type) { } if (Model.MUTATOR_EVENTS[type]) { if (this._silent) return this; + // `segments` is almost definitely an array of strings. + // + // A search for `.emit(` shows that `segments` is generated from either + // `Model#_splitPath` or `Model#_dereference`, both of which return an array + // of strings. var segments = arguments[1]; var eventArgs = arguments[2]; this._emit(type + 'Immediate', segments, eventArgs); @@ -106,13 +114,13 @@ Model.prototype.emit = function(type) { Model.prototype._on = EventEmitter.prototype.on; Model.prototype.addListener = Model.prototype.on = function(type, pattern, cb) { - var listener = eventListener(this, pattern, cb); + var listener = eventListener(this, type, pattern, cb); this._on(type, listener); return listener; }; Model.prototype.once = function(type, pattern, cb) { - var listener = eventListener(this, pattern, cb); + var listener = eventListener(this, type, pattern, cb); function g() { var matches = listener.apply(null, arguments); if (matches) this.removeListener(type, g); @@ -221,35 +229,83 @@ Model.prototype.removeContextListeners = function(value) { return this; }; -function eventListener(model, subpattern, cb) { +/** + * @param {Model} model + * @param {string} eventType + * @param {string | Model} subpattern + * @param {Function} cb + */ +function eventListener(model, eventType, subpattern, cb) { if (cb) { // For signatures: // model.on('change', 'example.subpath', callback) // model.at('example').on('change', 'subpath', callback) var pattern = model.path(subpattern); - return modelEventListener(pattern, cb, model._eventContext); + return modelEventListener(eventType, pattern, cb, model._eventContext); } var path = model.path(); - cb = arguments[1]; + cb = arguments[2]; // For signature: // model.at('example').on('change', callback) - if (path) return modelEventListener(path, cb, model._eventContext); + if (path) return modelEventListener(eventType, path, cb, model._eventContext); // For signature: // model.on('normalEvent', callback) return cb; } -function modelEventListener(pattern, cb, eventContext) { - var patternSegments = util.castSegments(pattern.split('.')); - var testFn = testPatternFn(pattern, patternSegments); - - function modelListener(segments, eventArgs) { - var captures = testFn(segments); - if (!captures) return; +/** + * Returns a function that can be passed to `EventEmitter#on`, with some + * additional properties used for `Model#removeAllListeners`. + * + * When the function is called, it checks if the event matches `patternArg`, and + * if there's a match, it calls `cb`. + * + * @param {string} eventType + * @param {string | string[]} patternArg + * @param {Function} cb + * @param {*} eventContext + * @return {ModelListenerFn & ModelListenerProps} + */ +function modelEventListener(eventType, patternArg, cb, eventContext) { + var pattern, patternSegments, useEventClasses; + if (Array.isArray(patternArg)) { + // If `patternArg` was provided as an array, then the `cb` listener will be + // invoked with an `____Event` object. + useEventClasses = true; + patternSegments = util.castSegments(patternArg); + if (patternSegments.length === 1 && patternSegments[0] === '**') { + pattern = '**'; + } + } else { + // Old-style: If `patternArg` was provided as a string, then the `cb` + // listener will invoked with a variable number of arguments. + useEventClasses = false; + pattern = patternArg; + patternSegments = util.castSegments(patternArg.split('.')); + } + var testFn = testPatternFn(patternSegments, pattern); + + /** @type {any} */ + var modelListener; + if (useEventClasses) { + var eventFactory = getEventFactory(eventType); + modelListener = function(segments, eventArgs) { + var captures = testFn(segments); + if (!captures) return; + + var event = eventFactory(eventArgs); + cb(event, captures); + return true; + }; + } else { + modelListener = function(segments, eventArgs) { + var captures = testFn(segments); + if (!captures) return; - var args = (captures.length) ? captures.concat(eventArgs) : eventArgs; - cb.apply(null, args); - return true; + var args = (captures.length) ? captures.concat(eventArgs) : eventArgs; + cb.apply(null, args); + return true; + }; } // Used in Model#removeAllListeners @@ -260,7 +316,112 @@ function modelEventListener(pattern, cb, eventContext) { return modelListener; } -function testPatternFn(pattern, patternSegments) { +/** @typedef { (segments: string[], eventArgs: any[]) => (boolean | undefined) } ModelListenerFn */ +/** @typedef { {pattern: string, patternSegments: Array, eventContext: any} } ModelListenerProps */ + +/** + * Returns a factory function that creates an `___Event` object based on an + * old-style `eventArgs` array. + * + * @param {string} eventType + * @return {(eventArgs: any[]) => ChangeEvent | InsertEvent | RemoveEvent | MoveEvent | LoadEvent | UnloadEvent} + */ +function getEventFactory(eventType) { + switch (eventType) { + case 'change': + return function(eventArgs) { + return new ChangeEvent(eventArgs); + }; + case 'insert': + return function(eventArgs) { + return new InsertEvent(eventArgs); + }; + case 'remove': + return function(eventArgs) { + return new RemoveEvent(eventArgs); + }; + case 'move': + return function(eventArgs) { + return new MoveEvent(eventArgs); + }; + case 'load': + return function(eventArgs) { + return new LoadEvent(eventArgs); + }; + case 'unload': + return function(eventArgs) { + return new UnloadEvent(eventArgs); + }; + case 'all': + return function(eventArgs) { + var concreteEventType = eventArgs[0]; // 'change', 'insert', etc. + var concreteEventFactory = getEventFactory(concreteEventType); + return concreteEventFactory(eventArgs.slice(1)); + }; + default: throw new Error('Unknown event: ' + eventType); + } +} + +// These constructors accept the `eventArgs` array format that Racer uses +// internally when calling `Model#emit`. +// +// Eventually, Racer should switch to passing these events around directly, +// but that will require updating all the places that parse the `eventArgs` +// array format, to extract things like `passed`. + +function ChangeEvent(eventArgs) { + this.value = eventArgs[0]; + this.previous = eventArgs[1]; + this.passed = eventArgs[2]; +} +ChangeEvent.prototype.type = 'change'; + +function InsertEvent(eventArgs) { + this.index = eventArgs[0]; + this.values = eventArgs[1]; + this.passed = eventArgs[2]; +} +InsertEvent.prototype.type = 'insert'; + +function RemoveEvent(eventArgs) { + this.index = eventArgs[0]; + this.removed = eventArgs[1]; + this.passed = eventArgs[2]; +} +RemoveEvent.prototype.type = 'remove'; + +function MoveEvent(eventArgs) { + this.from = eventArgs[0]; + this.to = eventArgs[1]; + this.howMany = eventArgs[2]; + this.passed = eventArgs[3]; +} +MoveEvent.prototype.type = 'move'; + +function LoadEvent(eventArgs) { + this.document = eventArgs[0]; + this.passed = eventArgs[1]; +} +LoadEvent.prototype.type = 'load'; + +function UnloadEvent(eventArgs) { + this.previousDocument = eventArgs[0]; + this.passed = eventArgs[1]; +} +UnloadEvent.prototype.type = 'unload'; + +/** + * Returns a function that tests an array of event segments against the + * `patternSegments`. (`pattern` only matters if it's exactly `'**'`.) + * + * @param {Array} patternSegments + * @param {string?} pattern + * @return {(segments: string[]) => (string[] | undefined)} A function to test + * an array of event segments. If the event segments match, an array of 0 or + * more segments captured by `*` / `**` is returned. If the event segments + * don't match, `undefined` is returned. + */ +function testPatternFn(patternSegments, pattern) { if (pattern === '**') { return function testPattern(segments) { return [segments.join('.')]; @@ -279,6 +440,7 @@ function testPatternFn(pattern, patternSegments) { // if it ends in a rest wildcard and each of the corresponding // segments are wildcards or equal. if (patternLen === segments.length || endingRest) { + /** @type string[] */ var captures = []; for (var i = 0; i < patternLen; i++) { var patternSegment = patternSegments[i]; @@ -298,15 +460,20 @@ function testPatternFn(pattern, patternSegments) { }; } +/** + * @param {Array} segments + */ function stripRestWildcard(segments) { // ['example', '**'] -> ['example']; return true var lastIndex = segments.length - 1; - if (segments[lastIndex] === '**') { + var lastSegment = segments[lastIndex]; + if (lastSegment === '**') { segments.pop(); return true; } // ['example', 'subpath**'] -> ['example', 'subpath']; return true - var match = /^([^\*]+)\*\*$/.exec(segments[lastIndex]); + if (typeof lastSegment !== 'string') return false; + var match = /^([^\*]+)\*\*$/.exec(lastSegment); if (!match) return false; segments[lastIndex] = match[1]; return true; diff --git a/lib/Model/fn.js b/lib/Model/fn.js index 45706d062..930fd6392 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -37,22 +37,40 @@ function parseStartArguments(model, args, hasPath) { } else { fns = last; } + // For `Model#start`, the first parameter is the output path. var path; if (hasPath) { path = model.path(args.shift()); } + // The second-to-last original argument could be an options object. + // If it's not an array and not path-like, then it's an options object. + last = args[args.length - 1]; var options; - if (!model.isPath(args[args.length - 1])) { + if (!Array.isArray(last) && !model.isPath(last)) { options = args.pop(); } - var i = args.length; + + // `args` is just the input paths at this point. + var inputs; + if (args.length === 1 && Array.isArray(args[0])) { + // Inputs provided as one array: + // model.start(outPath, [inPath1, inPath2], fn); + inputs = args[0]; + } else { + // Inputs provided as var-args: + // model.start(outPath, inPath1, inPath2, fn); + inputs = args; + } + + // Normalize each input into a string path. + var i = inputs.length; while (i--) { - args[i] = model.path(args[i]); + inputs[i] = model.path(inputs[i]); } return { name: name, path: path, - inputPaths: args, + inputPaths: inputs, fns: fns, options: options }; diff --git a/lib/util.js b/lib/util.js index bcb2c46bf..14d467efb 100644 --- a/lib/util.js +++ b/lib/util.js @@ -54,6 +54,10 @@ AsyncGroup.prototype.add = function() { }; }; +/** + * @param {Array} segments + * @return {Array} + */ function castSegments(segments) { // Cast number path segments from strings to numbers for (var i = segments.length; i--;) { From 552dcdfb84d02829342c358e694a58e3d676b601 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 10 Jul 2019 14:36:37 -0700 Subject: [PATCH 175/479] Add tests for non-vararg #start an #on, fix a bug with non-vararg #on --- lib/Model/events.js | 7 +- test/Model/events.js | 159 ++++++++++++++++++++++++++++++++++++++----- test/Model/fn.js | 81 ++++++++++++++++++++++ 3 files changed, 228 insertions(+), 19 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index e5fe44805..7411fdd2b 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -240,7 +240,12 @@ function eventListener(model, eventType, subpattern, cb) { // For signatures: // model.on('change', 'example.subpath', callback) // model.at('example').on('change', 'subpath', callback) - var pattern = model.path(subpattern); + var pattern; + if (Array.isArray(subpattern)) { + pattern = model._splitPath().concat(subpattern); + } else { + pattern = model.path(subpattern); + } return modelEventListener(eventType, pattern, cb, model._eventContext); } var path = model.path(); diff --git a/test/Model/events.js b/test/Model/events.js index aa1d1113c..35f4ec55a 100644 --- a/test/Model/events.js +++ b/test/Model/events.js @@ -65,45 +65,45 @@ describe('Model events', function() { describe('move', function() { it('can move an item from the end to the beginning of the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4]); - this.remote.on('move', '**', function(captures, from, to) { - expect(from).to.equal(4); - expect(to).to.equal(0); + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(4); + expect(event.to).to.equal(0); done(); }); this.local.move('array', 4, 0, 1); }); it('can swap the first two items in the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(captures, from, to) { - expect(from).to.equal(1); - expect(to).to.equal(0); + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(1); + expect(event.to).to.equal(0); done(); }); this.local.move('array', 1, 0, 1, function() {}); }); it('can move an item from the begnning to the end of the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(captures, from, to) { - expect(from).to.equal(0); - expect(to).to.equal(4); + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(0); + expect(event.to).to.equal(4); done(); }); this.local.move('array', 0, 4, 1, function() {}); }); it('supports a negative destination index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(captures, from, to) { - expect(from).to.equal(0); - expect(to).to.equal(4); + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(0); + expect(event.to).to.equal(4); done(); }); this.local.move('array', 0, -1, 1, function() {}); }); it('supports a negative source index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(captures, from, to) { - expect(from).to.equal(4); - expect(to).to.equal(2); + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(4); + expect(event.to).to.equal(2); done(); }); this.local.move('array', -1, 2, 1, function() {}); @@ -111,9 +111,132 @@ describe('Model events', function() { it('can move several items mid-array, with an event for each', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); var events = 0; - this.remote.on('move', '**', function(captures, from, to) { - expect(from).to.equal(1); - expect(to).to.equal(4); + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(1); + expect(event.to).to.equal(4); + if (++events === 2) { + done(); + } + }); + this.local.move('array', 1, 3, 2, function() {}); + }); + }); + }); +}); + +describe('Model events (structured)', function() { + describe('mutator events', function() { + it('calls earlier listeners in the order of mutations', function(done) { + var model = (new racer.Model()).at('_page'); + var expectedPaths = ['a', 'b', 'c']; + model.on('change', ['**'], function(_event, captures) { + expect(captures).to.eql([expectedPaths.shift()]); + if (!expectedPaths.length) { + done(); + } + }); + model.on('change', ['a'], function() { + model.set('b', 2); + }); + model.on('change', ['b'], function() { + model.set('c', 3); + }); + model.set('a', 1); + }); + it('calls later listeners in the order of mutations', function(done) { + var model = (new racer.Model()).at('_page'); + model.on('change', ['a'], function() { + model.set('b', 2); + }); + model.on('change', ['b'], function() { + model.set('c', 3); + }); + var expectedPaths = ['a', 'b', 'c']; + model.on('change', ['**'], function(_event, captures) { + expect(captures).to.eql([expectedPaths.shift()]); + if (!expectedPaths.length) { + done(); + } + }); + model.set('a', 1); + }); + }); + + describe('remote events', function() { + beforeEach(function(done) { + var backend = racer.createBackend(); + var local = this.local = backend.createModel().scope('colors.green'); + var remote = this.remote = backend.createModel().scope('colors.green'); + local.create(function(err) { + if (err) return done(err); + remote.subscribe(done); + }); + }); + + describe('set', function() { + it('can raise events registered on array indices', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + this.remote.on('change', ['array', '0'], function(event) { + expect(event.value).to.equal(1); + expect(event.previous).to.equal(0); + done(); + }); + this.local.set('array.0', 1); + }); + }); + + describe('move', function() { + it('can move an item from the end to the beginning of the array', function(done) { + this.local.set('array', [0, 1, 2, 3, 4]); + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(4); + expect(event.to).to.equal(0); + done(); + }); + this.local.move('array', 4, 0, 1); + }); + it('can swap the first two items in the array', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(1); + expect(event.to).to.equal(0); + done(); + }); + this.local.move('array', 1, 0, 1, function() {}); + }); + it('can move an item from the begnning to the end of the array', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(0); + expect(event.to).to.equal(4); + done(); + }); + this.local.move('array', 0, 4, 1, function() {}); + }); + it('supports a negative destination index of -1 (for last)', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(0); + expect(event.to).to.equal(4); + done(); + }); + this.local.move('array', 0, -1, 1, function() {}); + }); + it('supports a negative source index of -1 (for last)', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(4); + expect(event.to).to.equal(2); + done(); + }); + this.local.move('array', -1, 2, 1, function() {}); + }); + it('can move several items mid-array, with an event for each', function(done) { + this.local.set('array', [0, 1, 2, 3, 4], function() {}); + var events = 0; + this.remote.on('move', ['**'], function(event) { + expect(event.from).to.equal(1); + expect(event.to).to.equal(4); if (++events === 2) { done(); } diff --git a/test/Model/fn.js b/test/Model/fn.js index fd0b68086..9d1706f64 100644 --- a/test/Model/fn.js +++ b/test/Model/fn.js @@ -136,6 +136,87 @@ describe('fn', function() { expect(model.get('_nums.sum')).to.equal(5); }); }); + describe('start (array inputs) and stop with getter', function() { + it('sets the output immediately on start', function() { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + var value = model.start('_nums.sum', ['_nums.a', '_nums.b'], 'sum'); + expect(value).to.equal(6); + expect(model.get('_nums.sum')).to.equal(6); + }); + it('sets the output when an input changes', function() { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + model.start('_nums.sum', ['_nums.a', '_nums.b'], 'sum'); + expect(model.get('_nums.sum')).to.equal(6); + model.set('_nums.a', 5); + expect(model.get('_nums.sum')).to.equal(9); + }); + it('sets the output when a parent of the input changes', function() { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.in', { + a: 2, + b: 4 + }); + model.start('_nums.sum', ['_nums.in.a', '_nums.in.b'], 'sum'); + expect(model.get('_nums.sum')).to.equal(6); + model.set('_nums.in', { + a: 5, + b: 7 + }); + expect(model.get('_nums.sum')).to.equal(12); + }); + it('does not set the output when a sibling of the input changes', function() { + var model = new Model(); + var count = 0; + model.fn('sum', function(a, b) { + count++; + return a + b; + }); + model.set('_nums.in', { + a: 2, + b: 4 + }); + model.start('_nums.sum', ['_nums.in.a', '_nums.in.b'], 'sum'); + expect(model.get('_nums.sum')).to.equal(6); + expect(count).to.equal(1); + model.set('_nums.in.a', 3); + expect(model.get('_nums.sum')).to.equal(7); + expect(count).to.equal(2); + model.set('_nums.in.c', -1); + expect(model.get('_nums.sum')).to.equal(7); + expect(count).to.equal(2); + }); + it('can call stop without start', function() { + var model = new Model(); + model.stop('_nums.sum'); + }); + it('stops updating after calling stop', function() { + var model = new Model(); + model.fn('sum', function(a, b) { + return a + b; + }); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + model.start('_nums.sum', ['_nums.a', '_nums.b'], 'sum'); + model.set('_nums.a', 1); + expect(model.get('_nums.sum')).to.equal(5); + model.stop('_nums.sum'); + model.set('_nums.a', 3); + expect(model.get('_nums.sum')).to.equal(5); + }); + }); describe('start with async option', function() { it('sets the output immediately on start', function() { var model = new Model(); From 8cf47b5cbc58db9e071e0a05322086309fbe304d Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 10 Jul 2019 17:37:19 -0700 Subject: [PATCH 176/479] Model#on: Add {useEventObjects: true} option, revert pathSegments changes --- lib/Model/events.js | 153 ++++++++++++++++++++++++++----------------- test/Model/events.js | 82 +++++++++++------------ 2 files changed, 134 insertions(+), 101 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index 7411fdd2b..7dcab061f 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -112,15 +112,27 @@ Model.prototype.emit = function(type) { }; Model.prototype._on = EventEmitter.prototype.on; +/** + * @param {string} type + * @param {string} pattern + * @param {Function} cb + * @param {ModelOnOptions} [options] + */ Model.prototype.addListener = -Model.prototype.on = function(type, pattern, cb) { - var listener = eventListener(this, type, pattern, cb); +Model.prototype.on = function(type, pattern, cb, options) { + var listener = eventListener(this, type, pattern, cb, options); this._on(type, listener); return listener; }; -Model.prototype.once = function(type, pattern, cb) { - var listener = eventListener(this, type, pattern, cb); +/** + * @param {string} type + * @param {string} pattern + * @param {Function} cb + * @param {ModelOnOptions} [options] + */ +Model.prototype.once = function(type, pattern, cb, options) { + var listener = eventListener(this, type, pattern, cb, options); function g() { var matches = listener.apply(null, arguments); if (matches) this.removeListener(type, g); @@ -129,6 +141,13 @@ Model.prototype.once = function(type, pattern, cb) { return g; }; +/** + * @typedef {Object} ModelOnOptions + * @property {boolean} [useEventObjects] - If true, the listener is called with + * `cb(event: ___Event, captures: string[])`, instead of the legacy var-args + * style `cb(captures..., [eventType], eventArgs..., passed)`. + */ + Model.prototype._removeAllListeners = EventEmitter.prototype.removeAllListeners; Model.prototype.removeAllListeners = function(type, subpattern) { // If a pattern is specified without an event type, remove all model event @@ -234,30 +253,71 @@ Model.prototype.removeContextListeners = function(value) { * @param {string} eventType * @param {string | Model} subpattern * @param {Function} cb + * @param {ModelOnOptions} [options] */ -function eventListener(model, eventType, subpattern, cb) { +function eventListener(model, eventType, subpattern, cb, options) { + options = options || {}; if (cb) { - // For signatures: - // model.on('change', 'example.subpath', callback) + // For signatures with pattern: + // model.on('change', 'example.subpath.**', callback) // model.at('example').on('change', 'subpath', callback) - var pattern; - if (Array.isArray(subpattern)) { - pattern = model._splitPath().concat(subpattern); - } else { - pattern = model.path(subpattern); + if (options.useEventObjects) { + var pattern = model.path(subpattern); + return modelEventListener(eventType, pattern, cb, model._eventContext); + } else { // eslint-disable-line no-else-return + var pattern = model.path(subpattern); + return modelEventListenerLegacy(pattern, cb, model._eventContext); } - return modelEventListener(eventType, pattern, cb, model._eventContext); } + /** @type string */ var path = model.path(); cb = arguments[2]; - // For signature: + // For signature without explicit pattern: // model.at('example').on('change', callback) - if (path) return modelEventListener(eventType, path, cb, model._eventContext); + if (path) { + if (options.useEventObjects) { + return modelEventListener(eventType, path, cb, model._eventContext); + } else { // eslint-disable-line no-else-return + return modelEventListenerLegacy(path, cb, model._eventContext); + } + } // For signature: // model.on('normalEvent', callback) return cb; } +/** + * Legacy version of `modelEventListener` that calls `cb` with var-args + * `(captures..., [eventType], args..., passed)` instead of new-style + * `___Event` objects. + * + * @param {string} pattern + * @param {Function} cb + * @param {*} eventContext + * @return {ModelListenerFn & ModelListenerProps} + */ +function modelEventListenerLegacy(pattern, cb, eventContext) { + var patternSegments = util.castSegments(pattern.split('.')); + var testFn = testPatternFn(pattern, patternSegments); + + /** @type ModelListenerFn */ + function modelListener(segments, eventArgs) { + var captures = testFn(segments); + if (!captures) return; + + var args = (captures.length) ? captures.concat(eventArgs) : eventArgs; + cb.apply(null, args); + return true; + } + + // Used in Model#removeAllListeners + modelListener.pattern = pattern; + modelListener.patternSegments = patternSegments; + modelListener.eventContext = eventContext; + + return modelListener; +} + /** * Returns a function that can be passed to `EventEmitter#on`, with some * additional properties used for `Model#removeAllListeners`. @@ -266,51 +326,24 @@ function eventListener(model, eventType, subpattern, cb) { * if there's a match, it calls `cb`. * * @param {string} eventType - * @param {string | string[]} patternArg + * @param {string} pattern * @param {Function} cb * @param {*} eventContext * @return {ModelListenerFn & ModelListenerProps} */ -function modelEventListener(eventType, patternArg, cb, eventContext) { - var pattern, patternSegments, useEventClasses; - if (Array.isArray(patternArg)) { - // If `patternArg` was provided as an array, then the `cb` listener will be - // invoked with an `____Event` object. - useEventClasses = true; - patternSegments = util.castSegments(patternArg); - if (patternSegments.length === 1 && patternSegments[0] === '**') { - pattern = '**'; - } - } else { - // Old-style: If `patternArg` was provided as a string, then the `cb` - // listener will invoked with a variable number of arguments. - useEventClasses = false; - pattern = patternArg; - patternSegments = util.castSegments(patternArg.split('.')); - } - var testFn = testPatternFn(patternSegments, pattern); - - /** @type {any} */ - var modelListener; - if (useEventClasses) { - var eventFactory = getEventFactory(eventType); - modelListener = function(segments, eventArgs) { - var captures = testFn(segments); - if (!captures) return; - - var event = eventFactory(eventArgs); - cb(event, captures); - return true; - }; - } else { - modelListener = function(segments, eventArgs) { - var captures = testFn(segments); - if (!captures) return; - - var args = (captures.length) ? captures.concat(eventArgs) : eventArgs; - cb.apply(null, args); - return true; - }; +function modelEventListener(eventType, pattern, cb, eventContext) { + var patternSegments = util.castSegments(pattern.split('.')); + var testFn = testPatternFn(pattern, patternSegments); + + var eventFactory = getEventFactory(eventType); + /** @type ModelListenerFn */ + function modelListener(segments, eventArgs) { + var captures = testFn(segments); + if (!captures) return; + + var event = eventFactory(eventArgs); + cb(event, captures); + return true; } // Used in Model#removeAllListeners @@ -419,14 +452,14 @@ UnloadEvent.prototype.type = 'unload'; * Returns a function that tests an array of event segments against the * `patternSegments`. (`pattern` only matters if it's exactly `'**'`.) * - * @param {Array} patternSegments * @param {string?} pattern + * @param {Array} patternSegments * @return {(segments: string[]) => (string[] | undefined)} A function to test * an array of event segments. If the event segments match, an array of 0 or - * more segments captured by `*` / `**` is returned. If the event segments - * don't match, `undefined` is returned. + * more segments captured by `'*'` / `'**'` is returned, one per wildcard. If + * the event segments don't match, `undefined` is returned. */ -function testPatternFn(patternSegments, pattern) { +function testPatternFn(pattern, patternSegments) { if (pattern === '**') { return function testPattern(segments) { return [segments.join('.')]; diff --git a/test/Model/events.js b/test/Model/events.js index 35f4ec55a..8a2c46a38 100644 --- a/test/Model/events.js +++ b/test/Model/events.js @@ -65,45 +65,45 @@ describe('Model events', function() { describe('move', function() { it('can move an item from the end to the beginning of the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4]); - this.remote.on('move', ['**'], function(event) { - expect(event.from).to.equal(4); - expect(event.to).to.equal(0); + this.remote.on('move', '**', function(captures, from, to) { + expect(from).to.equal(4); + expect(to).to.equal(0); done(); }); this.local.move('array', 4, 0, 1); }); it('can swap the first two items in the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', ['**'], function(event) { - expect(event.from).to.equal(1); - expect(event.to).to.equal(0); + this.remote.on('move', '**', function(captures, from, to) { + expect(from).to.equal(1); + expect(to).to.equal(0); done(); }); this.local.move('array', 1, 0, 1, function() {}); }); it('can move an item from the begnning to the end of the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', ['**'], function(event) { - expect(event.from).to.equal(0); - expect(event.to).to.equal(4); + this.remote.on('move', '**', function(captures, from, to) { + expect(from).to.equal(0); + expect(to).to.equal(4); done(); }); this.local.move('array', 0, 4, 1, function() {}); }); it('supports a negative destination index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', ['**'], function(event) { - expect(event.from).to.equal(0); - expect(event.to).to.equal(4); + this.remote.on('move', '**', function(captures, from, to) { + expect(from).to.equal(0); + expect(to).to.equal(4); done(); }); this.local.move('array', 0, -1, 1, function() {}); }); it('supports a negative source index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', ['**'], function(event) { - expect(event.from).to.equal(4); - expect(event.to).to.equal(2); + this.remote.on('move', '**', function(captures, from, to) { + expect(from).to.equal(4); + expect(to).to.equal(2); done(); }); this.local.move('array', -1, 2, 1, function() {}); @@ -111,9 +111,9 @@ describe('Model events', function() { it('can move several items mid-array, with an event for each', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); var events = 0; - this.remote.on('move', ['**'], function(event) { - expect(event.from).to.equal(1); - expect(event.to).to.equal(4); + this.remote.on('move', '**', function(captures, from, to) { + expect(from).to.equal(1); + expect(to).to.equal(4); if (++events === 2) { done(); } @@ -124,40 +124,40 @@ describe('Model events', function() { }); }); -describe('Model events (structured)', function() { +describe('Model events with {useEventObjects: true}', function() { describe('mutator events', function() { it('calls earlier listeners in the order of mutations', function(done) { var model = (new racer.Model()).at('_page'); var expectedPaths = ['a', 'b', 'c']; - model.on('change', ['**'], function(_event, captures) { + model.on('change', '**', function(_event, captures) { expect(captures).to.eql([expectedPaths.shift()]); if (!expectedPaths.length) { done(); } - }); - model.on('change', ['a'], function() { + }, {useEventObjects: true}); + model.on('change', 'a', function() { model.set('b', 2); }); - model.on('change', ['b'], function() { + model.on('change', 'b', function() { model.set('c', 3); }); model.set('a', 1); }); it('calls later listeners in the order of mutations', function(done) { var model = (new racer.Model()).at('_page'); - model.on('change', ['a'], function() { + model.on('change', 'a', function() { model.set('b', 2); }); - model.on('change', ['b'], function() { + model.on('change', 'b', function() { model.set('c', 3); }); var expectedPaths = ['a', 'b', 'c']; - model.on('change', ['**'], function(_event, captures) { + model.on('change', '**', function(_event, captures) { expect(captures).to.eql([expectedPaths.shift()]); if (!expectedPaths.length) { done(); } - }); + }, {useEventObjects: true}); model.set('a', 1); }); }); @@ -176,11 +176,11 @@ describe('Model events (structured)', function() { describe('set', function() { it('can raise events registered on array indices', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('change', ['array', '0'], function(event) { + this.remote.on('change', 'array.0', function(event) { expect(event.value).to.equal(1); expect(event.previous).to.equal(0); done(); - }); + }, {useEventObjects: true}); this.local.set('array.0', 1); }); }); @@ -188,59 +188,59 @@ describe('Model events (structured)', function() { describe('move', function() { it('can move an item from the end to the beginning of the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4]); - this.remote.on('move', ['**'], function(event) { + this.remote.on('move', '**', function(event) { expect(event.from).to.equal(4); expect(event.to).to.equal(0); done(); - }); + }, {useEventObjects: true}); this.local.move('array', 4, 0, 1); }); it('can swap the first two items in the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', ['**'], function(event) { + this.remote.on('move', '**', function(event) { expect(event.from).to.equal(1); expect(event.to).to.equal(0); done(); - }); + }, {useEventObjects: true}); this.local.move('array', 1, 0, 1, function() {}); }); it('can move an item from the begnning to the end of the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', ['**'], function(event) { + this.remote.on('move', '**', function(event) { expect(event.from).to.equal(0); expect(event.to).to.equal(4); done(); - }); + }, {useEventObjects: true}); this.local.move('array', 0, 4, 1, function() {}); }); it('supports a negative destination index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', ['**'], function(event) { + this.remote.on('move', '**', function(event) { expect(event.from).to.equal(0); expect(event.to).to.equal(4); done(); - }); + }, {useEventObjects: true}); this.local.move('array', 0, -1, 1, function() {}); }); it('supports a negative source index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', ['**'], function(event) { + this.remote.on('move', '**', function(event) { expect(event.from).to.equal(4); expect(event.to).to.equal(2); done(); - }); + }, {useEventObjects: true}); this.local.move('array', -1, 2, 1, function() {}); }); it('can move several items mid-array, with an event for each', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); var events = 0; - this.remote.on('move', ['**'], function(event) { + this.remote.on('move', '**', function(event) { expect(event.from).to.equal(1); expect(event.to).to.equal(4); if (++events === 2) { done(); } - }); + }, {useEventObjects: true}); this.local.move('array', 1, 3, 2, function() {}); }); }); From d1dcca4390bad89a6a65faa19c80223301f660f9 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 11 Jul 2019 11:36:08 -0700 Subject: [PATCH 177/479] Move Model#on options to before callback --- lib/Model/events.js | 62 +++++++++++++++++++++++++------------------- test/Model/events.js | 36 ++++++++++++------------- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index 7dcab061f..7db263ebc 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -112,27 +112,15 @@ Model.prototype.emit = function(type) { }; Model.prototype._on = EventEmitter.prototype.on; -/** - * @param {string} type - * @param {string} pattern - * @param {Function} cb - * @param {ModelOnOptions} [options] - */ Model.prototype.addListener = -Model.prototype.on = function(type, pattern, cb, options) { - var listener = eventListener(this, type, pattern, cb, options); +Model.prototype.on = function(type, pattern, options, cb) { + var listener = eventListener(this, type, pattern, options, cb); this._on(type, listener); return listener; }; -/** - * @param {string} type - * @param {string} pattern - * @param {Function} cb - * @param {ModelOnOptions} [options] - */ -Model.prototype.once = function(type, pattern, cb, options) { - var listener = eventListener(this, type, pattern, cb, options); +Model.prototype.once = function(type, pattern, options, cb) { + var listener = eventListener(this, type, pattern, options, cb); function g() { var matches = listener.apply(null, arguments); if (matches) this.removeListener(type, g); @@ -251,17 +239,38 @@ Model.prototype.removeContextListeners = function(value) { /** * @param {Model} model * @param {string} eventType - * @param {string | Model} subpattern - * @param {Function} cb - * @param {ModelOnOptions} [options] */ -function eventListener(model, eventType, subpattern, cb, options) { - options = options || {}; - if (cb) { +function eventListener(model, eventType, arg2, arg3, arg4) { + var subpattern, options, cb; + if (arg4) { + // on(eventType, path, options, cb) + subpattern = arg2; + options = arg3; + cb = arg4; + } else if (arg3) { + // on(eventType, path, cb) + // on(eventType, options, cb) + cb = arg3; + if (model.isPath(arg2)) { + subpattern = arg2; + } else { + options = arg2; + } + } else { // if (arg2) + // on(eventType, cb) + cb = arg2; + } + if (options) { + if (options.useEventObjects) { + var useEventObjects = true; + } + } + + if (subpattern) { // For signatures with pattern: // model.on('change', 'example.subpath.**', callback) // model.at('example').on('change', 'subpath', callback) - if (options.useEventObjects) { + if (useEventObjects) { var pattern = model.path(subpattern); return modelEventListener(eventType, pattern, cb, model._eventContext); } else { // eslint-disable-line no-else-return @@ -269,13 +278,12 @@ function eventListener(model, eventType, subpattern, cb, options) { return modelEventListenerLegacy(pattern, cb, model._eventContext); } } - /** @type string */ - var path = model.path(); - cb = arguments[2]; // For signature without explicit pattern: // model.at('example').on('change', callback) + /** @type string */ + var path = model.path(); if (path) { - if (options.useEventObjects) { + if (useEventObjects) { return modelEventListener(eventType, path, cb, model._eventContext); } else { // eslint-disable-line no-else-return return modelEventListenerLegacy(path, cb, model._eventContext); diff --git a/test/Model/events.js b/test/Model/events.js index 8a2c46a38..8d835a842 100644 --- a/test/Model/events.js +++ b/test/Model/events.js @@ -129,12 +129,12 @@ describe('Model events with {useEventObjects: true}', function() { it('calls earlier listeners in the order of mutations', function(done) { var model = (new racer.Model()).at('_page'); var expectedPaths = ['a', 'b', 'c']; - model.on('change', '**', function(_event, captures) { + model.on('change', '**', {useEventObjects: true}, function(_event, captures) { expect(captures).to.eql([expectedPaths.shift()]); if (!expectedPaths.length) { done(); } - }, {useEventObjects: true}); + }); model.on('change', 'a', function() { model.set('b', 2); }); @@ -152,12 +152,12 @@ describe('Model events with {useEventObjects: true}', function() { model.set('c', 3); }); var expectedPaths = ['a', 'b', 'c']; - model.on('change', '**', function(_event, captures) { + model.on('change', '**', {useEventObjects: true}, function(_event, captures) { expect(captures).to.eql([expectedPaths.shift()]); if (!expectedPaths.length) { done(); } - }, {useEventObjects: true}); + }); model.set('a', 1); }); }); @@ -176,11 +176,11 @@ describe('Model events with {useEventObjects: true}', function() { describe('set', function() { it('can raise events registered on array indices', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('change', 'array.0', function(event) { + this.remote.on('change', 'array.0', {useEventObjects: true}, function(event) { expect(event.value).to.equal(1); expect(event.previous).to.equal(0); done(); - }, {useEventObjects: true}); + }); this.local.set('array.0', 1); }); }); @@ -188,59 +188,59 @@ describe('Model events with {useEventObjects: true}', function() { describe('move', function() { it('can move an item from the end to the beginning of the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4]); - this.remote.on('move', '**', function(event) { + this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(4); expect(event.to).to.equal(0); done(); - }, {useEventObjects: true}); + }); this.local.move('array', 4, 0, 1); }); it('can swap the first two items in the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(event) { + this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(1); expect(event.to).to.equal(0); done(); - }, {useEventObjects: true}); + }); this.local.move('array', 1, 0, 1, function() {}); }); it('can move an item from the begnning to the end of the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(event) { + this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(0); expect(event.to).to.equal(4); done(); - }, {useEventObjects: true}); + }); this.local.move('array', 0, 4, 1, function() {}); }); it('supports a negative destination index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(event) { + this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(0); expect(event.to).to.equal(4); done(); - }, {useEventObjects: true}); + }); this.local.move('array', 0, -1, 1, function() {}); }); it('supports a negative source index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); - this.remote.on('move', '**', function(event) { + this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(4); expect(event.to).to.equal(2); done(); - }, {useEventObjects: true}); + }); this.local.move('array', -1, 2, 1, function() {}); }); it('can move several items mid-array, with an event for each', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); var events = 0; - this.remote.on('move', '**', function(event) { + this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(1); expect(event.to).to.equal(4); if (++events === 2) { done(); } - }, {useEventObjects: true}); + }); this.local.move('array', 1, 3, 2, function() {}); }); }); From 055b8f3951ef35b27b3e412a2ec49185d4779cb2 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 11 Jul 2019 11:40:30 -0700 Subject: [PATCH 178/479] Style tweaks --- lib/Model/events.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index 7db263ebc..d700264ab 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -270,24 +270,19 @@ function eventListener(model, eventType, arg2, arg3, arg4) { // For signatures with pattern: // model.on('change', 'example.subpath.**', callback) // model.at('example').on('change', 'subpath', callback) - if (useEventObjects) { - var pattern = model.path(subpattern); - return modelEventListener(eventType, pattern, cb, model._eventContext); - } else { // eslint-disable-line no-else-return - var pattern = model.path(subpattern); - return modelEventListenerLegacy(pattern, cb, model._eventContext); - } + var pattern = model.path(subpattern); + return (useEventObjects) ? + modelEventListener(eventType, pattern, cb, model._eventContext) : + modelEventListenerLegacy(pattern, cb, model._eventContext); } // For signature without explicit pattern: // model.at('example').on('change', callback) /** @type string */ var path = model.path(); if (path) { - if (useEventObjects) { - return modelEventListener(eventType, path, cb, model._eventContext); - } else { // eslint-disable-line no-else-return - return modelEventListenerLegacy(path, cb, model._eventContext); - } + return (useEventObjects) ? + modelEventListener(eventType, path, cb, model._eventContext) : + modelEventListenerLegacy(path, cb, model._eventContext); } // For signature: // model.on('normalEvent', callback) From a93a9f1d64b6d5468a9b33469f60182ba3741918 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 11 Jul 2019 14:31:10 -0700 Subject: [PATCH 179/479] 0.9.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc7378ce5..94e443eb6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.5", + "version": "0.9.6", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From a6374765621745d3e76977b8483e4263ba4a61c5 Mon Sep 17 00:00:00 2001 From: jeremymcintyre Date: Thu, 11 Jul 2019 17:45:02 -0700 Subject: [PATCH 180/479] Do not garbage collect a doc if it has pending ops in Share --- lib/Model/subscriptions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index e9d9147d7..1afd712fe 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -172,6 +172,7 @@ Model.prototype._maybeUnloadDoc = function(collectionName, id) { if (!doc) return; if (this._hasDocReferences(collectionName, id)) return; + if (doc.shareDoc && doc.shareDoc.hasPending()) return; var previous = doc.get(); From 0f64bd6cee638588f2419fecc062d7975145bcf6 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 17 Jul 2019 10:47:43 -0700 Subject: [PATCH 181/479] update dev dependencies because of security warnings upgrades coveralls and mocha and migrates from istanbul to nyc --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 94e443eb6..5e1548a2d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "lint": "eslint --ignore-path .gitignore .", "test": "node_modules/.bin/mocha && npm run lint", - "test-cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha && npm run lint" + "test-cover": "node_modules/nyc/bin/nyc.js --temp-dir=coverage -r text -r lcov node_modules/mocha/bin/_mocha && npm run lint" }, "dependencies": { "arraydiff": "^0.1.1", @@ -20,12 +20,12 @@ "uuid": "^2.0.1" }, "devDependencies": { - "coveralls": "^2.11.8", + "coveralls": "^3.0.5", "eslint": "^2.9.0", "eslint-config-xo": "^0.14.1", "expect.js": "^0.3.1", - "istanbul": "^0.4.2", - "mocha": "^2.3.3" + "mocha": "^6.1.4", + "nyc": "^14.1.1" }, "bugs": { "url": "https://github.com/derbyjs/racer/issues" From eb4921e321cdbac841c5ca015e03414d7260848d Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 17 Jul 2019 10:56:07 -0700 Subject: [PATCH 182/479] add explanatory comments --- lib/Model/subscriptions.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 1afd712fe..f51440df3 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -171,7 +171,14 @@ Model.prototype._maybeUnloadDoc = function(collectionName, id) { var doc = this.getDoc(collectionName, id); if (!doc) return; + // If there is a query or direct fetch or subscribe that is holding reference + // to this doc, leave it loaded if (this._hasDocReferences(collectionName, id)) return; + // Calling sharedDoc.destroy() will remove it from the connection only when + // there aren't any operations, fetches, or subscribes pending on the doc. + // Thus, if we remove the doc from Racer's model but don't remove it from + // ShareDB, we can end up with an inconsistent state, with the data existing + // in ShareDB not reflected in the racer model data if (doc.shareDoc && doc.shareDoc.hasPending()) return; var previous = doc.get(); From d180f07bdee40b1a1b3ad88f1e8a7a8e0ccbd673 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 17 Jul 2019 11:02:55 -0700 Subject: [PATCH 183/479] update node versions for travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 330e7a247..d47ad2a7f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: node_js node_js: + - 10 + - 8 - 6 - - 4 - - 0.10 script: "npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" From 2062fa8ca9e49fe6348cff719ddc995dfc352988 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 17 Jul 2019 11:09:47 -0700 Subject: [PATCH 184/479] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c340e6720..8e8aa8adf 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,11 @@ Racer is a realtime model synchronization engine for Node.js. By leveraging [ShareDB](https://github.com/share/sharedb), multiple users can interact with the same data in realtime via Operational Transformation, a sophisticated conflict resolution algorithm that works in realtime and with offline clients. ShareDB also supports PubSub across multiple servers for horizontal scaling. Clients can express data subscriptions and fetches in terms of queries and specific documents, so different clients can be subscribed to different overlapping sets of data. On top of this sophisticated backend, Racer provides a simple model and event interface for writing application logic. -[![Build Status](https://travis-ci.org/derbyjs/racer.svg)](https://travis-ci.org/derbyjs/racer) +[![Build Status](https://travis-ci.org/derbyjs/racer.svg?branch=master)](https://travis-ci.org/derbyjs/racer.svg?branch=master) ## Disclaimer -Racer is alpha software. We are now on the road to a production ready version, but we are currently cleaning up the code, finishing loose ends, and testing. - -If you are interested in contributing, please reach out to [Nate](https://github.com/nateps), [Joseph](https://github.com/josephg), and [Brian](https://github.com/bnoguchi). +Racer is alpha software. If you are interested in contributing, please reach out to [Nate](https://github.com/nateps). ## Demos From 9552bff6a29e07dd332ef28d3476547bc7c7aa3d Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 17 Jul 2019 11:11:47 -0700 Subject: [PATCH 185/479] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e8aa8adf..c4cc49c11 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Racer is a realtime model synchronization engine for Node.js. By leveraging [ShareDB](https://github.com/share/sharedb), multiple users can interact with the same data in realtime via Operational Transformation, a sophisticated conflict resolution algorithm that works in realtime and with offline clients. ShareDB also supports PubSub across multiple servers for horizontal scaling. Clients can express data subscriptions and fetches in terms of queries and specific documents, so different clients can be subscribed to different overlapping sets of data. On top of this sophisticated backend, Racer provides a simple model and event interface for writing application logic. -[![Build Status](https://travis-ci.org/derbyjs/racer.svg?branch=master)](https://travis-ci.org/derbyjs/racer.svg?branch=master) +[![Build Status](https://travis-ci.org/derbyjs/racer.svg?branch=master)](https://travis-ci.org/derbyjs/racer) ## Disclaimer From 93edd7fd856e03ba92b95b18255f535793192595 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 17 Jul 2019 11:13:10 -0700 Subject: [PATCH 186/479] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c4cc49c11..ddd8e5f62 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ Racer is a realtime model synchronization engine for Node.js. By leveraging [ShareDB](https://github.com/share/sharedb), multiple users can interact with the same data in realtime via Operational Transformation, a sophisticated conflict resolution algorithm that works in realtime and with offline clients. ShareDB also supports PubSub across multiple servers for horizontal scaling. Clients can express data subscriptions and fetches in terms of queries and specific documents, so different clients can be subscribed to different overlapping sets of data. On top of this sophisticated backend, Racer provides a simple model and event interface for writing application logic. -[![Build Status](https://travis-ci.org/derbyjs/racer.svg?branch=master)](https://travis-ci.org/derbyjs/racer) + [![Build Status](https://travis-ci.org/derbyjs/racer.svg?branch=master)](https://travis-ci.org/derbyjs/racer) + [![Coverage Status](https://coveralls.io/repos/github/derbyjs/racer/badge.svg?branch=master)](https://coveralls.io/github/derbyjs/racer?branch=master) ## Disclaimer From f4939ab347d857376a0a0c6b3678105c17f4083e Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 17 Jul 2019 15:46:36 -0700 Subject: [PATCH 187/479] make CollectionCounter tests self contained --- test/Model/CollectionCounter.js | 35 ++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/test/Model/CollectionCounter.js b/test/Model/CollectionCounter.js index 28c358e17..21b2ebacf 100644 --- a/test/Model/CollectionCounter.js +++ b/test/Model/CollectionCounter.js @@ -2,28 +2,35 @@ var expect = require('../util').expect; var CollectionCounter = require('../../lib/Model/CollectionCounter'); describe('CollectionCounter', function() { - var cc = new CollectionCounter(); - it('increment', function() { - expect(cc.toJSON()).to.be(undefined); - expect(cc.get('numbers', 'one')).to.be(undefined); - expect(cc.increment('numbers', 'one')).to.equal(1); - expect(cc.increment('numbers', 'one')).to.equal(2); + var counter = new CollectionCounter(); + expect(counter.get('colors', 'green')).to.be(undefined); + expect(counter.increment('colors', 'green')).to.equal(1); + expect(counter.increment('colors', 'green')).to.equal(2); + expect(counter.get('colors', 'green')).to.equal(2); }); + it('toJSON', function() { - expect(cc.toJSON()).to.eql({ - numbers: { - one: 2 + var counter = new CollectionCounter(); + expect(counter.toJSON()).to.be(undefined); + expect(counter.increment('colors', 'green')).to.equal(1); + expect(counter.increment('colors', 'green')).to.equal(2); + expect(counter.toJSON()).to.eql({ + colors: { + green: 2 } }); }); + it('decrement', function() { - expect(cc.get('numbers', 'one')).to.equal(2); + var counter = new CollectionCounter(); + expect(counter.increment('colors', 'green')); + expect(counter.increment('colors', 'green')); - expect(cc.decrement('numbers', 'one')).to.equal(1); - expect(cc.decrement('numbers', 'one')).to.equal(0); + expect(counter.decrement('colors', 'green')).to.equal(1); + expect(counter.decrement('colors', 'green')).to.equal(0); - expect(cc.get('numbers', 'one')).to.be(undefined); - expect(cc.toJSON()).to.be(undefined); + expect(counter.get('colors', 'green')).to.be(undefined); + expect(counter.toJSON()).to.be(undefined); }); }); From 5dbd754198f1839bb3873cf99f29c5d8a089ae58 Mon Sep 17 00:00:00 2001 From: Michael Brade Date: Fri, 27 Jan 2017 20:11:23 +0100 Subject: [PATCH 188/479] fixed memory leak in Queries no direct unit tests possible without exporting Queries --- lib/Model/Query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index e6f51e8bc..600f5bce8 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -133,7 +133,7 @@ Queries.prototype.remove = function(query) { // Check if the collection still has any keys // eslint-disable-next-line no-unused-vars for (var key in collection) return; - delete this.collections[collection]; + delete this.collections[query.collectionName]; }; Queries.prototype.get = function(collectionName, expression, options) { var hash = queryHash(collectionName, expression, options); From bd43932f519b0004e3df9361c2ae12f38dae29a4 Mon Sep 17 00:00:00 2001 From: Zach Millman Date: Wed, 13 Jun 2018 16:29:33 -0700 Subject: [PATCH 189/479] Don't create redundant ops in `_setDiffDeep` Fixes https://github.com/derbyjs/racer/issues/255 --- lib/Model/setDiff.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Model/setDiff.js b/lib/Model/setDiff.js index 7448ba036..02ab1759f 100644 --- a/lib/Model/setDiff.js +++ b/lib/Model/setDiff.js @@ -59,8 +59,8 @@ Model.prototype._setDiffDeep = function(segments, value, cb) { function diffDeep(model, segments, before, after, group) { if (typeof before !== 'object' || !before || typeof after !== 'object' || !after) { - // Set the entire value if not diffable - model._set(segments, after, group()); + // Diff the entire value if not diffable objects + model._setDiff(segments, after, group()); return; } if (Array.isArray(before) && Array.isArray(after)) { From a6199c0a08ff32d4469607fc151637eedc59e11e Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 17 Jul 2019 17:16:16 -0700 Subject: [PATCH 190/479] add setDiff tests --- test/Model/setDiff.js | 263 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 test/Model/setDiff.js diff --git a/test/Model/setDiff.js b/test/Model/setDiff.js new file mode 100644 index 000000000..a25509924 --- /dev/null +++ b/test/Model/setDiff.js @@ -0,0 +1,263 @@ +var expect = require('../util').expect; +var Model = require('../../lib/Model'); + +['setDiff', 'setDiffDeep', 'setArrayDiff', 'setArrayDiffDeep'].forEach(function(method) { + describe(method + ' common diff functionality', function() { + it('sets the value when undefined', function() { + var model = new Model(); + model[method]('_page.color', 'green'); + expect(model.get('_page.color')).to.equal('green'); + }); + + it('changes the value', function() { + var model = new Model(); + model.set('_page.color', 'green'); + model[method]('_page.color', 'red'); + expect(model.get('_page.color')).to.equal('red'); + }); + + it('changes an object', function() { + var model = new Model(); + model.set('_page.color', {hex: '#0f0', name: 'green'}); + model[method]('_page.color', {hex: '#f00', name: 'red'}); + expect(model.get('_page.color')).to.eql({hex: '#f00', name: 'red'}); + }); + + it('deletes keys from an object', function() { + var model = new Model(); + model.set('_page.color', {hex: '#0f0', name: 'green'}); + model[method]('_page.color', {name: 'green'}); + expect(model.get('_page.color')).to.eql({name: 'green'}); + }); + + it('adds items to an array', function() { + var model = new Model(); + model.set('_page.items', [4]); + model[method]('_page.items', [2, 3, 4]); + expect(model.get('_page.items')).to.eql([2, 3, 4]); + }); + + it('removes items in an array', function() { + var model = new Model(); + model.set('_page.items', [2, 3, 4]); + model[method]('_page.items', [3, 4]); + expect(model.get('_page.items')).to.eql([3, 4]); + }); + + it('moves items in an array', function() { + var model = new Model(); + model.set('_page.items', [2, 3, 4]); + model[method]('_page.items', [3, 4, 2]); + expect(model.get('_page.items')).to.eql([3, 4, 2]); + }); + + it('adds items to an array in an object', function() { + var model = new Model(); + model.set('_page.lists', {a: [4]}); + model[method]('_page.lists', {a: [2, 3, 4]}); + expect(model.get('_page.lists')).to.eql({a: [2, 3, 4]}); + }); + + it('emits an event when changing value', function(done) { + var model = new Model(); + model.on('all', function(segments, eventArgs) { + var type = eventArgs[0]; + var value = eventArgs[1]; + var previous = eventArgs[2]; + expect(segments).eql(['_page', 'color']); + expect(type).equal('change'); + expect(value).equal('green'); + expect(previous).equal(undefined); + done(); + }); + model[method]('_page.color', 'green'); + }); + + it('does not emit an event when value is not changed', function(done) { + var model = new Model(); + model.set('_page.color', 'green'); + model.on('all', function() { + done(new Error('unexpected event emission')); + }); + model[method]('_page.color', 'green'); + done(); + }); + }); +}); + +describe('setDiff', function() { + it('emits an event when an object is set to an equivalent object', function(done) { + var model = new Model(); + model.set('_page.color', {name: 'green'}); + model.on('all', function(segments, eventArgs) { + var type = eventArgs[0]; + var value = eventArgs[1]; + var previous = eventArgs[2]; + expect(segments).eql(['_page', 'color']); + expect(type).equal('change'); + expect(value).eql({name: 'green'}); + expect(previous).eql({name: 'green'}); + done(); + }); + model.setDiff('_page.color', {name: 'green'}); + }); + + it('emits an event when an array is set to an equivalent array', function(done) { + var model = new Model(); + model.set('_page.list', [2, 3, 4]); + model.on('all', function(segments, eventArgs) { + var type = eventArgs[0]; + var value = eventArgs[1]; + var previous = eventArgs[2]; + expect(segments).eql(['_page', 'list']); + expect(type).equal('change'); + expect(value).eql([2, 3, 4]); + expect(previous).eql([2, 3, 4]); + done(); + }); + model.setDiff('_page.list', [2, 3, 4]); + }); +}); + +describe('setDiffDeep', function() { + it('does not emit an event when an object is set to an equivalent object', function(done) { + var model = new Model(); + model.set('_page.color', {name: 'green'}); + model.on('all', function() { + done(new Error('unexpected event emission')); + }); + model.setDiffDeep('_page.color', {name: 'green'}); + done(); + }); + + it('does not emit an event when an array is set to an equivalent array', function(done) { + var model = new Model(); + model.set('_page.list', [2, 3, 4]); + model.on('all', function() { + done(new Error('unexpected event emission')); + }); + model.setDiffDeep('_page.list', [2, 3, 4]); + done(); + }); + + it('does not emit an event when a deep object / array is set to an equivalent value', function(done) { + var model = new Model(); + model.set('_page.lists', {a: [2, 3], b: [1], _meta: {foo: 'bar'}}); + model.on('all', function() { + done(new Error('unexpected event emission')); + }); + model.setDiffDeep('_page.lists', {a: [2, 3], b: [1], _meta: {foo: 'bar'}}); + done(); + }); + + it('equivalent objects ignore key order', function(done) { + var model = new Model(); + model.set('_page.lists', {a: [2, 3], b: [1]}); + model.on('all', function() { + done(new Error('unexpected event emission')); + }); + model.setDiffDeep('_page.lists', {b: [1], a: [2, 3]}); + done(); + }); + + it('adds items to an array', function(done) { + var model = new Model(); + model.set('_page.items', [4]); + model.on('all', function(segments, eventArgs) { + var type = eventArgs[0]; + var index = eventArgs[1]; + var values = eventArgs[2]; + expect(segments).eql(['_page', 'items']); + expect(type).equal('insert'); + expect(values).eql([2, 3]); + expect(index).eql(0); + done(); + }); + model.setDiffDeep('_page.items', [2, 3, 4]); + }); + + it('adds items to an array in an object', function(done) { + var model = new Model(); + model.set('_page.lists', {a: [4]}); + model.on('all', function(segments, eventArgs) { + var type = eventArgs[0]; + var index = eventArgs[1]; + var values = eventArgs[2]; + expect(segments).eql(['_page', 'lists', 'a']); + expect(type).equal('insert'); + expect(values).eql([2, 3]); + expect(index).eql(0); + done(); + }); + model.setDiffDeep('_page.lists', {a: [2, 3, 4]}); + }); + + it('emits a delete event when a key is removed from an object', function(done) { + var model = new Model(); + model.set('_page.color', {hex: '#0f0', name: 'green'}); + model.on('all', function(segments, eventArgs) { + var type = eventArgs[0]; + var value = eventArgs[1]; + var previous = eventArgs[2]; + expect(segments).eql(['_page', 'color', 'hex']); + expect(type).equal('change'); + expect(value).equal(undefined); + expect(previous).equal('#0f0'); + done(); + }); + model.setDiffDeep('_page.color', {name: 'green'}); + expect(model.get('_page.color')).to.eql({name: 'green'}); + }); +}); + +describe('setArrayDiff', function() { + it('does not emit an event when an array is set to an equivalent array', function(done) { + var model = new Model(); + model.set('_page.list', [2, 3, 4]); + model.on('all', function() { + done(new Error('unexpected event emission')); + }); + model.setArrayDiff('_page.list', [2, 3, 4]); + done(); + }); + + it('emits an event when objects in an array are set to an equivalent array', function(done) { + var model = new Model(); + model.set('_page.list', [{a: 2}, {c: 3}, {b: 4}]); + var expectedEvents = ['remove', 'insert']; + model.on('all', function(segments, eventArgs) { + expect(segments).eql(['_page', 'list']); + var type = eventArgs[0]; + var index = eventArgs[1]; + var values = eventArgs[2]; + var expected = expectedEvents.shift(); + expect(type).equal(expected); + expect(values).eql([{a: 2}, {c: 3}, {b: 4}]); + expect(index).eql(0); + if (expectedEvents.length === 0) done(); + }); + model.setArrayDiff('_page.list', [{a: 2}, {c: 3}, {b: 4}]); + }); +}); + +describe('setArrayDiffDeep', function() { + it('does not emit an event when an array is set to an equivalent array', function(done) { + var model = new Model(); + model.set('_page.list', [2, 3, 4]); + model.on('all', function() { + done(new Error('unexpected event emission')); + }); + model.setArrayDiffDeep('_page.list', [2, 3, 4]); + done(); + }); + + it('does not emit an event when objects in an array are set to an equivalent array', function(done) { + var model = new Model(); + model.set('_page.list', [{a: 2}, {c: 3}, {b: 4}]); + model.on('all', function() { + done(new Error('unexpected event emission')); + }); + model.setArrayDiffDeep('_page.list', [{a: 2}, {c: 3}, {b: 4}]); + done(); + }); +}); From 7c0708ca438699180a72831a7fb2156e665bdf96 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 19 Jul 2019 15:53:29 -0700 Subject: [PATCH 191/479] update eslint --- .eslintrc.js | 62 +++++++++++++++++++++++++-------------------- lib/Model/events.js | 6 ++--- package.json | 4 +-- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index dea65e61e..f78ae0876 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,29 +1,37 @@ +// The ESLint ecmaVersion argument is inconsistently used. Some rules will ignore it entirely, so if the rule has +// been set, it will still error even if it's not applicable to that version number. Since Google sets these +// rules, we have to turn them off ourselves. +const DISABLED_ES6_OPTIONS = { + 'no-var': 'off', + 'prefer-rest-params': 'off', + 'prefer-spread': 'off', + // Not supported in ES3 + 'comma-dangle': ['error', 'never'] +}; + +const CUSTOM_RULES = { + 'one-var': 'off', + // We control our own objects and prototypes, so no need for this check + 'guard-for-in': 'off', + // Google prescribes different indents for different cases. Let's just use 2 spaces everywhere. Note that we have + // to override ESLint's default of 0 indents for this. + 'indent': ['error', 2, {'SwitchCase': 1}], + // Less aggressive line length than Google, which is especially useful when we have a lot of callbacks in our code + 'max-len': ['error', {code: 120, tabWidth: 2, ignoreUrls: true}], + // Google overrides the default ESLint behaviour here, which is slightly better for catching erroneously unused variables + 'no-unused-vars': ['error', {vars: 'all', args: 'after-used'}], + 'require-jsdoc': 'off', + 'valid-jsdoc': 'off' +}; + module.exports = { - extends: 'xo', - rules: { - 'block-scoped-var': 'off', - 'curly': ['error', 'multi-line', 'consistent'], - 'eqeqeq': ['error', 'allow-null'], - 'guard-for-in': 'off', - 'indent': ['error', 2, {SwitchCase: 1}], - 'max-len': ['off', 80, 4, { - ignoreComments: true, - ignoreUrls: true - }], - 'no-eq-null': 'off', - 'no-implicit-coercion': 'off', - 'no-nested-ternary': 'off', - 'no-redeclare': 'off', - 'no-undef-init': 'off', - 'no-unused-expressions': ['error', {allowShortCircuit: true}], - 'one-var': ['error', {initialized: 'never'}], - 'require-jsdoc': 'off', - 'space-before-function-paren': ['error', 'never'], - 'valid-jsdoc': ['off', { - requireReturn: false, - prefer: { - returns: 'return' - } - }], - } + extends: 'google', + parserOptions: { + ecmaVersion: 3 + }, + rules: Object.assign( + {}, + DISABLED_ES6_OPTIONS, + CUSTOM_RULES + ), }; diff --git a/lib/Model/events.js b/lib/Model/events.js index d700264ab..e5aaf3f36 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -53,7 +53,7 @@ Model.prototype.wrapCallback = function(cb) { Model.prototype._emitError = function(err, context) { var message = (err.message) ? err.message : (typeof err === 'string') ? err : - 'Unknown model error'; + 'Unknown model error'; if (context) { message += ' ' + context; } @@ -256,7 +256,7 @@ function eventListener(model, eventType, arg2, arg3, arg4) { } else { options = arg2; } - } else { // if (arg2) + } else { // if (arg2) // on(eventType, cb) cb = arg2; } @@ -395,7 +395,7 @@ function getEventFactory(eventType) { }; case 'all': return function(eventArgs) { - var concreteEventType = eventArgs[0]; // 'change', 'insert', etc. + var concreteEventType = eventArgs[0]; // 'change', 'insert', etc. var concreteEventFactory = getEventFactory(concreteEventType); return concreteEventFactory(eventArgs.slice(1)); }; diff --git a/package.json b/package.json index ff0908695..090665cf4 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ }, "devDependencies": { "coveralls": "^3.0.5", - "eslint": "^2.9.0", - "eslint-config-xo": "^0.14.1", + "eslint": "^6.0.1", + "eslint-config-google": "^0.13.0", "expect.js": "^0.3.1", "mocha": "^6.1.4", "nyc": "^14.1.1" From 5bd2628458e683335975b0828d29c472adfcdf0e Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 19 Jul 2019 16:09:15 -0700 Subject: [PATCH 192/479] downgrade eslint, since it is broken in node 6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 090665cf4..795042185 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "coveralls": "^3.0.5", - "eslint": "^6.0.1", + "eslint": "^5.16.0", "eslint-config-google": "^0.13.0", "expect.js": "^0.3.1", "mocha": "^6.1.4", From f152e49de88c84e5dfeba7433a5b80498d769fb2 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 24 Jul 2019 13:54:29 -0700 Subject: [PATCH 193/479] 0.9.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 795042185..e8cbe2fc2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.6", + "version": "0.9.7", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 5be55c6e4c24d035255400d3623ac57db7ec9e96 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 9 Aug 2019 13:08:06 -0700 Subject: [PATCH 194/479] deep-is => fast-deep-equal Switch dependencies to fast-deep-equal from deep-is. The new module is under active development, and it appears to be much higher quality in terms of testing, correctness, and speed. --- lib/util.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/util.js b/lib/util.js index 14d467efb..398014b81 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,4 +1,4 @@ -var deepIs = require('deep-is'); +var deepEqual = require('fast-deep-equal'); var isServer = process.title !== 'browser'; exports.isServer = isServer; @@ -9,7 +9,7 @@ exports.contains = contains; exports.copy = copy; exports.copyObject = copyObject; exports.deepCopy = deepCopy; -exports.deepEqual = deepIs; +exports.deepEqual = deepEqual; exports.equal = equal; exports.equalsNaN = equalsNaN; exports.isArrayIndex = isArrayIndex; diff --git a/package.json b/package.json index e8cbe2fc2..b6532e2dc 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "arraydiff": "^0.1.1", - "deep-is": "^0.1.3", + "fast-deep-equal": "^2.0.1", "sharedb": "^1.0.0-beta", "uuid": "^2.0.1" }, From 9c71a0f0a9d2a94f29e3bce3faab5ae8676b351e Mon Sep 17 00:00:00 2001 From: Leslie Kim Date: Tue, 13 Aug 2019 13:02:23 -0700 Subject: [PATCH 195/479] Shallow clone refs value to avoid trying to access removed ref --- lib/Model/ref.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Model/ref.js b/lib/Model/ref.js index 05e85b53b..ffc4e0df0 100644 --- a/lib/Model/ref.js +++ b/lib/Model/ref.js @@ -119,6 +119,10 @@ function addListener(model, type, fn) { // occured at that path var refs = toMap[subpath]; if (!refs) continue; + + // Shallow clone refs in case a ref is removed while going through + // the loop + refs = refs.slice(); var remaining = segments.slice(i + 1); for (var refIndex = 0, numRefs = refs.length; refIndex < numRefs; refIndex++) { var ref = refs[refIndex]; From 2f2251ebb338aff9fc71d92b52ca4c09f6abb296 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Tue, 13 Aug 2019 13:30:50 -0700 Subject: [PATCH 196/479] Add test for removing ref in event callback --- test/Model/ref.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/Model/ref.js b/test/Model/ref.js index d31854dea..73bf49589 100644 --- a/test/Model/ref.js +++ b/test/Model/ref.js @@ -129,6 +129,30 @@ describe('ref', function() { model.set('_page.start', true); expect(model.get('_page.list')).to.eql(['#00f', '#0f0']); }); + it('removing ref on same toPath in event callback is ok', function() { + // The effects of listeners are synchronous, so while fanning out the refs + // for a given toPath - "_page.color" in this case - it's possible for one + // of the refs to be removed. Modifying during iteration can cause issues + // if not handled correctly. + var model = new Model(); + model.ref('_page.ref1', '_page.color'); + model.ref('_page.ref2', '_page.color'); + model.set('_page.color', 'red'); + model.once('change', '_page.ref1', function() { + model.removeRef('_page.ref2'); + }); + + model.set('_page.color', 'green'); + expect(model.get('_page.ref1')).to.eql('green'); + // ref2 was removed while processing ref, but it should still be updated + // for this current change. + expect(model.get('_page.ref2')).to.eql('green'); + + // ref2 is now removed, so it should no longer be updated. + model.set('_page.color', 'blue'); + expect(model.get('_page.ref1')).to.eql('blue'); + expect(model.get('_page.ref2')).to.eql('green'); + }); }); describe('updateIndices option', function() { it('updates a ref when an array insert happens at the `to` path', function() { From 292129c9086551a6415452bbc3ce5d368283ce8a Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Tue, 13 Aug 2019 14:12:00 -0700 Subject: [PATCH 197/479] 0.9.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b6532e2dc..493a3a442 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.7", + "version": "0.9.8", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From c9bc540c862f68a7311a238458fa37c041e6814c Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 4 Oct 2019 17:05:00 -0700 Subject: [PATCH 198/479] add support for multiple arguments to model.scope() & model.at() The arguments are effectively joined by dots. This will be helpful for adding typescript types to models while maintaining backwards compatitibility --- lib/Model/paths.js | 22 +++++++++++++--- test/Model/path.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 test/Model/path.js diff --git a/lib/Model/paths.js b/lib/Model/paths.js index 65f7d4f99..97b348c10 100644 --- a/lib/Model/paths.js +++ b/lib/Model/paths.js @@ -28,9 +28,12 @@ Model.prototype.isPath = function(subpath) { }; Model.prototype.scope = function(path) { - var model = this._child(); - model._at = path; - return model; + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + path = path + '.' + arguments[i]; + } + } + return createScoped(this, path); }; /** @@ -47,10 +50,21 @@ Model.prototype.scope = function(path) { * @api public */ Model.prototype.at = function(subpath) { + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + subpath = subpath + '.' + arguments[i]; + } + } var path = this.path(subpath); - return this.scope(path); + return createScoped(this, path); }; +function createScoped(model, path) { + var scoped = model._child(); + scoped._at = path; + return scoped; +} + /** * Returns a model scope that is a number of levels above the current scoped * path. Number of levels defaults to 1, so this method called without diff --git a/test/Model/path.js b/test/Model/path.js new file mode 100644 index 000000000..fb94371f1 --- /dev/null +++ b/test/Model/path.js @@ -0,0 +1,63 @@ +var expect = require('../util').expect; +var Model = require('../../lib/Model'); + +describe('path methods', function() { + describe('path', function() { + it('returns empty string for model without scope', function() { + var model = new Model(); + expect(model.path()).equal(''); + }); + }); + describe('scope', function() { + it('returns a child model with the absolute scope', function() { + var model = new Model(); + var scoped = model.scope('foo.bar.baz'); + expect(model.path()).equal(''); + expect(scoped.path()).equal('foo.bar.baz'); + }); + it('supports segments as separate arguments', function() { + var model = new Model(); + var scoped = model.scope('foo', 'bar', 'baz'); + expect(model.path()).equal(''); + expect(scoped.path()).equal('foo.bar.baz'); + }); + it('overrides a previous scope', function() { + var model = new Model(); + var scoped = model.scope('foo', 'bar', 'baz'); + var scoped2 = scoped.scope('colors', 4); + expect(scoped2.path()).equal('colors.4'); + }); + it('supports no arguments', function() { + var model = new Model(); + var scoped = model.scope('foo', 'bar', 'baz'); + var scoped2 = scoped.scope(); + expect(scoped2.path()).equal(''); + }); + }); + describe('at', function() { + it('returns a child model with the relative scope', function() { + var model = new Model(); + var scoped = model.at('foo.bar.baz'); + expect(model.path()).equal(''); + expect(scoped.path()).equal('foo.bar.baz'); + }); + it('supports segments as separate arguments', function() { + var model = new Model(); + var scoped = model.at('foo', 'bar', 'baz'); + expect(model.path()).equal(''); + expect(scoped.path()).equal('foo.bar.baz'); + }); + it('overrides a previous scope', function() { + var model = new Model(); + var scoped = model.at('colors'); + var scoped2 = scoped.at(4); + expect(scoped2.path()).equal('colors.4'); + }); + it('supports no arguments', function() { + var model = new Model(); + var scoped = model.at('foo', 'bar', 'baz'); + var scoped2 = scoped.at(); + expect(scoped2.path()).equal('foo.bar.baz'); + }); + }); +}); From 5606aae781740ee7da85b383661664b47c6cf49f Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 4 Oct 2019 17:11:28 -0700 Subject: [PATCH 199/479] 0.9.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 493a3a442..f4a1395f3 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.8", + "version": "0.9.9", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 44562986a211fd8f9893cf3e574f59c4e6d3f4c5 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 8 Jul 2019 11:59:19 -0700 Subject: [PATCH 200/479] initial implementation of EventTree A tree style of event emitter designed for internal use in making model.start more efficient. Could also be used to re-implement model events more efficiently and avoid regular expressions --- lib/Model/EventTree.js | 114 +++++++++++++++++++++++++ lib/Model/fn.js | 37 ++++++--- test/Model/EventTree.js | 179 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 317 insertions(+), 13 deletions(-) create mode 100644 lib/Model/EventTree.js create mode 100644 test/Model/EventTree.js diff --git a/lib/Model/EventTree.js b/lib/Model/EventTree.js new file mode 100644 index 000000000..57a1ff03b --- /dev/null +++ b/lib/Model/EventTree.js @@ -0,0 +1,114 @@ +module.exports = EventTree; + +function EventTree(parent, segment) { + this.parent = parent; + this.segment = segment; + this.children = null; + this.listeners = null; +} + +EventTree.prototype.destroy = function() { + // Ignore calls to destroy a root node + if (!this.parent) return; + + // Remove reference this node from its parent + if (hasOtherKeys(this.parent.children, this.segment)) { + delete this.parent.children[this.segment]; + return; + } + this.parent.children = null; + + // Destroy parent if it no longer has any dependents + if (!this.parent.listeners) { + this.parent.destroy(); + } +}; + +EventTree.prototype.getChild = function(segments) { + var node = this; + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + node = node.children && node.children[segment]; + if (!node) return; + } + return node; +}; + +EventTree.prototype.getOrCreateChild = function(segments) { + var node = this; + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + if (!node.children) { + node.children = {}; + } + var node = node.children[segment] || + (node.children[segment] = new EventTree(node, segment)); + } + return node; +}; + +EventTree.prototype.addListener = function(segments, listener) { + var node = this.getOrCreateChild(segments); + if (!node.listeners) { + node.listeners = [listener]; + return; + } + var i = node.listeners.indexOf(listener); + if (i === -1) { + node.listeners.push(listener); + } +}; + +EventTree.prototype.removeListener = function(segments, listener) { + var node = this.getChild(segments); + if (!node || !node.listeners) return; + if (node.listeners.length === 1) { + if (node.listeners[0] === listener) { + node.listeners = null; + if (!node.children) { + node.destroy(); + } + } + return; + } + var i = node.listeners.indexOf(listener); + if (i > -1) { + node.listeners.splice(i, 1); + } +}; + +EventTree.prototype.forListeners = function(callback) { + if (!this.listeners) return; + for (var i = 0; i < this.listeners.length; i++) { + var listener = this.listeners[i]; + callback(listener); + } +}; + +EventTree.prototype.forEach = function(segments, callback) { + var node = this; + node.forListeners(callback); + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + node = node.children && node.children[segment]; + if (!node) return; + node.forListeners(callback); + } + forDescendents(node, callback); +}; + +function forDescendents(node, callback) { + if (!node.children) return; + for (var key in node.children) { + var child = node.children[key]; + child.forListeners(callback); + forDescendents(child, callback); + } +} + +function hasOtherKeys(object, ignore) { + for (var key in object) { + if (key !== ignore) return true; + } + return false; +} diff --git a/lib/Model/fn.js b/lib/Model/fn.js index 930fd6392..43d33f634 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -1,6 +1,7 @@ var util = require('../util'); var Model = require('./Model'); var defaultFns = require('./defaultFns'); +var EventTree = require('./EventTree'); function NamedFns() {} @@ -10,18 +11,14 @@ Model.INITS.push(function(model) { model.on('all', fnListener); function fnListener(segments, eventArgs) { var pass = eventArgs[eventArgs.length - 1]; - var map = model.root._fns.fromMap; - for (var path in map) { - var fn = map[path]; - if (pass.$fn === fn) continue; - if (util.mayImpactAny(fn.inputsSegments, segments)) { - // Mutation affecting input path - fn.onInput(pass); - } else if (util.mayImpact(fn.fromSegments, segments)) { - // Mutation affecting output path - fn.onOutput(pass); - } - } + // Mutation affecting input path + model.root._fns.inputListeners.forEach(segments, function(fn) { + if (fn !== pass.$fn) fn.onInput(pass); + }); + // Mutation affecting output path + model.root._fns.outputListeners.forEach(segments, function(fn) { + if (fn !== pass.$fn) fn.onOutput(pass); + }); } }); @@ -115,6 +112,8 @@ function Fns(model) { this.model = model; this.nameMap = model.root._namedFns; this.fromMap = new FromMap(); + this.inputListeners = new EventTree(); + this.outputListeners = new EventTree(); } Fns.prototype.get = function(name, inputPaths, fns, options) { @@ -127,12 +126,24 @@ Fns.prototype.start = function(name, path, inputPaths, fns, options) { fns || (fns = this.nameMap[name] || defaultFns[name]); var fn = new Fn(this.model, name, path, inputPaths, fns, options); this.fromMap[path] = fn; + for (var i = 0; i < fn.inputsSegments.length; i++) { + var inputSegements = fn.inputsSegments[i]; + this.inputListeners.addListener(inputSegements, fn); + } + this.outputListeners.addListener(fn.fromSegments, fn); return fn._onInput(); }; Fns.prototype.stop = function(path) { var fn = this.fromMap[path]; - delete this.fromMap[path]; + if (fn) { + delete this.fromMap[path]; + for (var i = 0; i < fn.inputsSegments.length; i++) { + var inputSegements = fn.inputsSegments[i]; + this.inputListeners.removeListener(inputSegements, fn); + } + this.outputListeners.removeListener(fn.fromSegments, fn); + } return fn; }; diff --git a/test/Model/EventTree.js b/test/Model/EventTree.js new file mode 100644 index 000000000..33f62c293 --- /dev/null +++ b/test/Model/EventTree.js @@ -0,0 +1,179 @@ +var expect = require('../util').expect; +var EventTree = require('../../lib/Model/EventTree'); + +describe('EventTree', function() { + describe('implementation', function() { + describe('constructor', function() { + it('creates an empty tree', function() { + var tree = new EventTree(); + expect(tree.listeners).equal(null); + expect(tree.children).equal(null); + }); + }); + describe('addListener', function() { + it('adds a listener object at the root', function() { + var tree = new EventTree(); + var listener = {}; + tree.addListener([], listener); + expect(tree.listeners).eql([listener]); + expect(tree.children).eql(null); + }); + it('only a listener object once', function() { + var tree = new EventTree(); + var listener = {}; + tree.addListener([], listener); + tree.addListener([], listener); + expect(tree.listeners).eql([listener]); + expect(tree.children).eql(null); + }); + it('adds a listener object at a path', function() { + var tree = new EventTree(); + var listener = {}; + tree.addListener(['colors'], listener); + expect(tree.listeners).eql(null); + expect(tree.children.colors.listeners).eql([listener]); + }); + it('adds a listener object at a subpath', function() { + var tree = new EventTree(); + var listener = {}; + tree.addListener(['colors', 'green'], listener); + expect(tree.listeners).eql(null); + expect(tree.children.colors.listeners).eql(null); + expect(tree.children.colors.children.green.listeners).eql([listener]); + }); + }); + describe('removeListener', function() { + it('can be called before addListener', function() { + var tree = new EventTree(); + var listener = {}; + tree.removeListener([], listener); + expect(tree.listeners).eql(null); + expect(tree.children).eql(null); + }); + it('removes listener at root', function() { + var tree = new EventTree(); + var listener = {}; + tree.addListener([], listener); + expect(tree.listeners).eql([listener]); + tree.removeListener([], listener); + expect(tree.listeners).eql(null); + }); + it('removes listener at subpath', function() { + var tree = new EventTree(); + var listener = {}; + tree.addListener(['colors', 'green'], listener); + expect(tree.children.colors.children.green.listeners).eql([listener]); + tree.removeListener(['colors', 'green'], listener); + expect(tree.children).eql(null); + }); + it('removes listener with remaining peers', function() { + var tree = new EventTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + tree.addListener([], listener1); + tree.addListener([], listener2); + tree.addListener([], listener3); + expect(tree.listeners).eql([listener1, listener2, listener3]); + tree.removeListener([], listener2); + expect(tree.listeners).eql([listener1, listener3]); + tree.removeListener([], listener3); + expect(tree.listeners).eql([listener1]); + tree.removeListener([], listener1); + expect(tree.listeners).eql(null); + }); + it('removes listener with remaining peer children', function() { + var tree = new EventTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + tree.addListener(['colors'], listener1); + tree.addListener(['colors', 'green'], listener2); + expect(tree.children.colors.listeners).eql([listener1]); + expect(tree.children.colors.children.green.listeners).eql([listener2]); + tree.removeListener(['colors'], listener1); + expect(tree.children.colors.listeners).eql(null); + expect(tree.children.colors.children.green.listeners).eql([listener2]); + }); + }); + }); + describe('forEach', function() { + function expectResults(expected, done) { + var pending = expected.slice(); + return function(result) { + var value = pending.shift(); + expect(value).eql(result); + if (pending.length > 0) return; + done(); + }; + } + it('can be called without listeners', function(done) { + var tree = new EventTree(); + tree.forEach([], done); + done(); + }); + it('calls a callback with all direct listeners', function(done) { + var tree = new EventTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + tree.addListener([], listener1); + tree.addListener([], listener2); + var callback = expectResults([listener1, listener2], done); + tree.forEach([], callback); + }); + it('removeListener stops listener from being returned', function(done) { + var tree = new EventTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + tree.addListener([], listener1); + tree.addListener([], listener2); + tree.removeListener([], listener1); + var callback = expectResults([listener2], done); + tree.forEach([], callback); + }); + it('calls a callback with all descendant listeners in depth order', function(done) { + var tree = new EventTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + var listener4 = 'listener4'; + var listener5 = 'listener5'; + tree.addListener(['colors', 'green'], listener1); + tree.addListener(['colors', 'red'], listener2); + tree.addListener(['colors', 'red'], listener3); + tree.addListener([], listener4); + tree.addListener(['colors'], listener5); + var callback = expectResults([listener4, listener5, listener1, listener2, listener3], done); + tree.forEach([], callback); + }); + it('calls a callback with all parent listeners in depth order', function(done) { + var tree = new EventTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + var listener4 = 'listener4'; + var listener5 = 'listener5'; + tree.addListener(['colors', 'green'], listener1); + tree.addListener(['colors', 'red'], listener2); + tree.addListener(['colors', 'red'], listener3); + tree.addListener([], listener4); + tree.addListener(['colors'], listener5); + var callback = expectResults([listener4, listener5, listener1], done); + tree.forEach(['colors', 'green'], callback); + }); + it('does not call for peers or peer children', function(done) { + var tree = new EventTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + var listener4 = 'listener4'; + var listener5 = 'listener5'; + tree.addListener([], listener1); + tree.addListener(['colors'], listener2); + tree.addListener(['colors', 'green'], listener3); + tree.addListener(['textures'], listener4); + tree.addListener(['textures', 'smooth'], listener5); + var callback = expectResults([listener1, listener4, listener5], done); + tree.forEach(['textures'], callback); + }); + }); +}); From cc3f60f016c611beb850bc25465a6e780cf95aac Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 22 Jul 2019 15:14:54 -0700 Subject: [PATCH 201/479] improve readability of EventListenerTree --- lib/Model/EventListenerTree.js | 229 ++++++++++++++++++ lib/Model/EventTree.js | 114 --------- lib/Model/fn.js | 10 +- .../{EventTree.js => EventListenerTree.js} | 84 +++++-- 4 files changed, 293 insertions(+), 144 deletions(-) create mode 100644 lib/Model/EventListenerTree.js delete mode 100644 lib/Model/EventTree.js rename test/Model/{EventTree.js => EventListenerTree.js} (71%) diff --git a/lib/Model/EventListenerTree.js b/lib/Model/EventListenerTree.js new file mode 100644 index 000000000..aff218d63 --- /dev/null +++ b/lib/Model/EventListenerTree.js @@ -0,0 +1,229 @@ +module.exports = EventListenerTree; + +/** + * Construct a tree root when invoked without any arguments. Children nodes are + * constructred internally as needed on calls to addListener() + * + * @param {EventListenerTree} [parent] + * @param {string} [segment] + */ +function EventListenerTree(parent, segment) { + this.parent = parent; + this.segment = segment; + this.children = null; + this.listeners = null; +} + +/** + * Remove the reference to this node from its parent so that it can be garbage + * collected. This is called internally when all listeners to a node + * are removed + */ +EventListenerTree.prototype._destroy = function() { + // For all non-root nodes, remove the reference to the node + if (this.parent) { + removeChild(this.parent, this.segment); + // For the root node, reset any references to listeners or children + } else { + this.children = null; + this.listeners = null; + } +}; + +/** + * Get a node for a path if it exists + * + * @param {string[]} segments + * @return {EventListenerTree|undefined} + */ +EventListenerTree.prototype._getChild = function(segments) { + var node = this; + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + node = node.children && node.children[segment]; + if (!node) return; + } + return node; +}; + +/** + * If a path already has a node, return it. Otherwise, create the node and + * parents in a lazy manner and return the node for the path + * + * @param {string[]} segments + * @return {EventListenerTree} + */ +EventListenerTree.prototype._getOrCreateChild = function(segments) { + var node = this; + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + if (!node.children) { + node.children = {}; + } + var node = node.children[segment] || + (node.children[segment] = new EventListenerTree(node, segment)); + } + return node; +}; + +/** + * Add a listener to a path location. Listener should be unique per path + * location, and calling twice witht the same segments and listener value has no + * effect. Unlike EventEmitter, listener may be any type of value. The value is + * returned to a callback upon calling `forEachAffected`. + * + * @param {string[]} segments + * @param {*} listener + */ +EventListenerTree.prototype.addListener = function(segments, listener) { + var node = this._getOrCreateChild(segments); + if (!node.listeners) { + node.listeners = [listener]; + return; + } + var i = node.listeners.indexOf(listener); + if (i === -1) { + node.listeners.push(listener); + } +}; + +/** + * Remove a listener from a path location. + * + * @param {string[]} segments + * @param {*} listener + */ +EventListenerTree.prototype.removeListener = function(segments, listener) { + var node = this._getChild(segments); + if (!node || !node.listeners) return; + if (node.listeners.length === 1) { + if (node.listeners[0] === listener) { + node.listeners = null; + if (!node.children) { + node._destroy(); + } + } + return; + } + var i = node.listeners.indexOf(listener); + if (i > -1) { + node.listeners.splice(i, 1); + } +}; + +/** + * Remove all listeners and descendent listeners for a path location. + * + * @param {string[]} segments + */ +EventListenerTree.prototype.removeAllListeners = function(segments) { + var node = this._getChild(segments); + if (node) { + node._destroy(); + } +}; + +/** + * Dispatch an event to each of the listeners that may be affected by a change + * to a model path. These are: + * 1. Listeners to each node from the root to the node for `segments` + * 2. Listeners to all descendent nodes under `segments` + * + * Calls the callback with each listener value, conceptually similar to + * Array#forEach() + * + * @param {string[]} segments + * @param {Function} callback + */ +EventListenerTree.prototype.forEachAffected = function(segments, callback) { + var node = forAncestorListeners(this, segments, callback); + if (node) { + forDescendantListeners(node, callback); + } +}; + +/** + * Call the callback with each listener value in the current node. + * + * @param {EventListenerTree} node + * @param {Function} callback + */ +function forListeners(node, callback) { + if (!node.listeners) return; + for (var i = 0; i < node.listeners.length; i++) { + var listener = node.listeners[i]; + callback(listener); + } +} + +/** + * Call the callback with each listener value from the root node passed in to + * the node for `segments`. Return the node at `segments` if it exists + * + * @param {EventListenerTree} node + * @param {string[]} segments + * @param {Function} callback + * @return {EventListenerTree|undefined} + */ +function forAncestorListeners(node, segments, callback) { + forListeners(node, callback); + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + node = node.children && node.children[segment]; + if (!node) return; + forListeners(node, callback); + } + return node; +} + +/** + * Call the callback with each listener value for each of the node's children + * and their recursive children + * + * @param {EventListenerTree} node + * @param {Function} callback + */ +function forDescendantListeners(node, callback) { + if (!node.children) return; + for (var key in node.children) { + var child = node.children[key]; + forListeners(child, callback); + forDescendantListeners(child, callback); + } +} + +/** + * Remove the child at the specified segment from a node. Also recursively + * remove parent nodes if there are no remaining dependencies + * + * @param {EventListenerTree} node + * @param {string} segment + */ +function removeChild(node, segment) { + // Remove reference this node from its parent + if (hasOtherKeys(node.children, segment)) { + delete node.children[segment]; + return; + } + node.children = null; + + // Destroy parent if it no longer has any dependents + if (!node.listeners) { + node._destroy(); + } +} + +/** + * Return whether the object has any other property key other than the + * provided value. + * + * @param {Object} object + * @param {string} ignore + * @return {Boolean} + */ +function hasOtherKeys(object, ignore) { + for (var key in object) { + if (key !== ignore) return true; + } + return false; +} diff --git a/lib/Model/EventTree.js b/lib/Model/EventTree.js deleted file mode 100644 index 57a1ff03b..000000000 --- a/lib/Model/EventTree.js +++ /dev/null @@ -1,114 +0,0 @@ -module.exports = EventTree; - -function EventTree(parent, segment) { - this.parent = parent; - this.segment = segment; - this.children = null; - this.listeners = null; -} - -EventTree.prototype.destroy = function() { - // Ignore calls to destroy a root node - if (!this.parent) return; - - // Remove reference this node from its parent - if (hasOtherKeys(this.parent.children, this.segment)) { - delete this.parent.children[this.segment]; - return; - } - this.parent.children = null; - - // Destroy parent if it no longer has any dependents - if (!this.parent.listeners) { - this.parent.destroy(); - } -}; - -EventTree.prototype.getChild = function(segments) { - var node = this; - for (var i = 0; i < segments.length; i++) { - var segment = segments[i]; - node = node.children && node.children[segment]; - if (!node) return; - } - return node; -}; - -EventTree.prototype.getOrCreateChild = function(segments) { - var node = this; - for (var i = 0; i < segments.length; i++) { - var segment = segments[i]; - if (!node.children) { - node.children = {}; - } - var node = node.children[segment] || - (node.children[segment] = new EventTree(node, segment)); - } - return node; -}; - -EventTree.prototype.addListener = function(segments, listener) { - var node = this.getOrCreateChild(segments); - if (!node.listeners) { - node.listeners = [listener]; - return; - } - var i = node.listeners.indexOf(listener); - if (i === -1) { - node.listeners.push(listener); - } -}; - -EventTree.prototype.removeListener = function(segments, listener) { - var node = this.getChild(segments); - if (!node || !node.listeners) return; - if (node.listeners.length === 1) { - if (node.listeners[0] === listener) { - node.listeners = null; - if (!node.children) { - node.destroy(); - } - } - return; - } - var i = node.listeners.indexOf(listener); - if (i > -1) { - node.listeners.splice(i, 1); - } -}; - -EventTree.prototype.forListeners = function(callback) { - if (!this.listeners) return; - for (var i = 0; i < this.listeners.length; i++) { - var listener = this.listeners[i]; - callback(listener); - } -}; - -EventTree.prototype.forEach = function(segments, callback) { - var node = this; - node.forListeners(callback); - for (var i = 0; i < segments.length; i++) { - var segment = segments[i]; - node = node.children && node.children[segment]; - if (!node) return; - node.forListeners(callback); - } - forDescendents(node, callback); -}; - -function forDescendents(node, callback) { - if (!node.children) return; - for (var key in node.children) { - var child = node.children[key]; - child.forListeners(callback); - forDescendents(child, callback); - } -} - -function hasOtherKeys(object, ignore) { - for (var key in object) { - if (key !== ignore) return true; - } - return false; -} diff --git a/lib/Model/fn.js b/lib/Model/fn.js index 43d33f634..8027344d0 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -1,7 +1,7 @@ var util = require('../util'); var Model = require('./Model'); var defaultFns = require('./defaultFns'); -var EventTree = require('./EventTree'); +var EventListenerTree = require('./EventListenerTree'); function NamedFns() {} @@ -12,11 +12,11 @@ Model.INITS.push(function(model) { function fnListener(segments, eventArgs) { var pass = eventArgs[eventArgs.length - 1]; // Mutation affecting input path - model.root._fns.inputListeners.forEach(segments, function(fn) { + model.root._fns.inputListeners.forEachAffected(segments, function(fn) { if (fn !== pass.$fn) fn.onInput(pass); }); // Mutation affecting output path - model.root._fns.outputListeners.forEach(segments, function(fn) { + model.root._fns.outputListeners.forEachAffected(segments, function(fn) { if (fn !== pass.$fn) fn.onOutput(pass); }); } @@ -112,8 +112,8 @@ function Fns(model) { this.model = model; this.nameMap = model.root._namedFns; this.fromMap = new FromMap(); - this.inputListeners = new EventTree(); - this.outputListeners = new EventTree(); + this.inputListeners = new EventListenerTree(); + this.outputListeners = new EventListenerTree(); } Fns.prototype.get = function(name, inputPaths, fns, options) { diff --git a/test/Model/EventTree.js b/test/Model/EventListenerTree.js similarity index 71% rename from test/Model/EventTree.js rename to test/Model/EventListenerTree.js index 33f62c293..2e4b127d8 100644 --- a/test/Model/EventTree.js +++ b/test/Model/EventListenerTree.js @@ -1,25 +1,25 @@ var expect = require('../util').expect; -var EventTree = require('../../lib/Model/EventTree'); +var EventListenerTree = require('../../lib/Model/EventListenerTree'); -describe('EventTree', function() { +describe('EventListenerTree', function() { describe('implementation', function() { describe('constructor', function() { it('creates an empty tree', function() { - var tree = new EventTree(); + var tree = new EventListenerTree(); expect(tree.listeners).equal(null); expect(tree.children).equal(null); }); }); describe('addListener', function() { it('adds a listener object at the root', function() { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener = {}; tree.addListener([], listener); expect(tree.listeners).eql([listener]); expect(tree.children).eql(null); }); it('only a listener object once', function() { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener = {}; tree.addListener([], listener); tree.addListener([], listener); @@ -27,14 +27,14 @@ describe('EventTree', function() { expect(tree.children).eql(null); }); it('adds a listener object at a path', function() { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener = {}; tree.addListener(['colors'], listener); expect(tree.listeners).eql(null); expect(tree.children.colors.listeners).eql([listener]); }); it('adds a listener object at a subpath', function() { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener = {}; tree.addListener(['colors', 'green'], listener); expect(tree.listeners).eql(null); @@ -44,14 +44,14 @@ describe('EventTree', function() { }); describe('removeListener', function() { it('can be called before addListener', function() { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener = {}; tree.removeListener([], listener); expect(tree.listeners).eql(null); expect(tree.children).eql(null); }); it('removes listener at root', function() { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener = {}; tree.addListener([], listener); expect(tree.listeners).eql([listener]); @@ -59,7 +59,7 @@ describe('EventTree', function() { expect(tree.listeners).eql(null); }); it('removes listener at subpath', function() { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener = {}; tree.addListener(['colors', 'green'], listener); expect(tree.children.colors.children.green.listeners).eql([listener]); @@ -67,7 +67,7 @@ describe('EventTree', function() { expect(tree.children).eql(null); }); it('removes listener with remaining peers', function() { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; var listener3 = 'listener3'; @@ -83,7 +83,7 @@ describe('EventTree', function() { expect(tree.listeners).eql(null); }); it('removes listener with remaining peer children', function() { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; tree.addListener(['colors'], listener1); @@ -95,8 +95,42 @@ describe('EventTree', function() { expect(tree.children.colors.children.green.listeners).eql([listener2]); }); }); + describe('removeAllListeners', function() { + it('can be called on empty root', function() { + var tree = new EventListenerTree(); + tree.removeAllListeners([]); + }); + it('can be called on missing node', function() { + var tree = new EventListenerTree(); + tree.removeAllListeners(['colors', 'green']); + }); + it('removes all listeners and children when called on root', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + tree.addListener([], listener1); + tree.addListener(['colors'], listener2); + tree.addListener(['colors', 'green'], listener3); + tree.removeAllListeners([]); + expect(tree.listeners).eql(null); + expect(tree.children).eql(null); + }); + it('removes listeners and descendent children on path', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + tree.addListener([], listener1); + tree.addListener(['colors'], listener2); + tree.addListener(['colors', 'green'], listener3); + tree.removeAllListeners(['colors']); + expect(tree.listeners).eql([listener1]); + expect(tree.children).eql(null); + }); + }); }); - describe('forEach', function() { + describe('forEachAffected', function() { function expectResults(expected, done) { var pending = expected.slice(); return function(result) { @@ -107,31 +141,31 @@ describe('EventTree', function() { }; } it('can be called without listeners', function(done) { - var tree = new EventTree(); - tree.forEach([], done); + var tree = new EventListenerTree(); + tree.forEachAffected([], done); done(); }); it('calls a callback with all direct listeners', function(done) { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; tree.addListener([], listener1); tree.addListener([], listener2); var callback = expectResults([listener1, listener2], done); - tree.forEach([], callback); + tree.forEachAffected([], callback); }); it('removeListener stops listener from being returned', function(done) { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; tree.addListener([], listener1); tree.addListener([], listener2); tree.removeListener([], listener1); var callback = expectResults([listener2], done); - tree.forEach([], callback); + tree.forEachAffected([], callback); }); it('calls a callback with all descendant listeners in depth order', function(done) { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; var listener3 = 'listener3'; @@ -143,10 +177,10 @@ describe('EventTree', function() { tree.addListener([], listener4); tree.addListener(['colors'], listener5); var callback = expectResults([listener4, listener5, listener1, listener2, listener3], done); - tree.forEach([], callback); + tree.forEachAffected([], callback); }); it('calls a callback with all parent listeners in depth order', function(done) { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; var listener3 = 'listener3'; @@ -158,10 +192,10 @@ describe('EventTree', function() { tree.addListener([], listener4); tree.addListener(['colors'], listener5); var callback = expectResults([listener4, listener5, listener1], done); - tree.forEach(['colors', 'green'], callback); + tree.forEachAffected(['colors', 'green'], callback); }); it('does not call for peers or peer children', function(done) { - var tree = new EventTree(); + var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; var listener3 = 'listener3'; @@ -173,7 +207,7 @@ describe('EventTree', function() { tree.addListener(['textures'], listener4); tree.addListener(['textures', 'smooth'], listener5); var callback = expectResults([listener1, listener4, listener5], done); - tree.forEach(['textures'], callback); + tree.forEachAffected(['textures'], callback); }); }); }); From 3f82543223ec73247c650786be1f0a7492846d54 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 22 Jul 2019 15:21:14 -0700 Subject: [PATCH 202/479] EventListenerTree: add tests for uncovered cases --- test/Model/EventListenerTree.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/Model/EventListenerTree.js b/test/Model/EventListenerTree.js index 2e4b127d8..56be7b3e8 100644 --- a/test/Model/EventListenerTree.js +++ b/test/Model/EventListenerTree.js @@ -66,6 +66,26 @@ describe('EventListenerTree', function() { tree.removeListener(['colors', 'green'], listener); expect(tree.children).eql(null); }); + it('does not remove listener if not found with one listener', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + tree.addListener(['colors', 'green'], listener1); + expect(tree.children.colors.children.green.listeners).eql([listener1]); + tree.removeListener(['colors', 'green'], listener2); + expect(tree.children.colors.children.green.listeners).eql([listener1]); + }); + it('does not remove listener if not found with multiple listeners', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + tree.addListener(['colors', 'green'], listener1); + tree.addListener(['colors', 'green'], listener2); + expect(tree.children.colors.children.green.listeners).eql([listener1, listener2]); + tree.removeListener(['colors', 'green'], listener3); + expect(tree.children.colors.children.green.listeners).eql([listener1, listener2]); + }); it('removes listener with remaining peers', function() { var tree = new EventListenerTree(); var listener1 = 'listener1'; From c82ac53f78ae106444b5959b17c4662adcd73210 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 31 Jul 2019 13:42:11 -0700 Subject: [PATCH 203/479] implement EventMapTree and use in fn, ref, and refList instead of object maps --- lib/Model/EventListenerTree.js | 104 +++++---- lib/Model/EventMapTree.js | 279 ++++++++++++++++++++++ lib/Model/bundle.js | 14 +- lib/Model/fn.js | 95 ++++---- lib/Model/ref.js | 134 +++++------ lib/Model/refList.js | 63 ++--- test/Model/EventListenerTree.js | 400 ++++++++++++++++++-------------- test/Model/EventMapTree.js | 271 ++++++++++++++++++++++ test/Model/fn.js | 159 ++++++------- 9 files changed, 1066 insertions(+), 453 deletions(-) create mode 100644 lib/Model/EventMapTree.js create mode 100644 test/Model/EventMapTree.js diff --git a/lib/Model/EventListenerTree.js b/lib/Model/EventListenerTree.js index aff218d63..262a0c718 100644 --- a/lib/Model/EventListenerTree.js +++ b/lib/Model/EventListenerTree.js @@ -48,7 +48,7 @@ EventListenerTree.prototype._getChild = function(segments) { /** * If a path already has a node, return it. Otherwise, create the node and - * parents in a lazy manner and return the node for the path + * ancestors in a lazy manner. Return the node for the path * * @param {string[]} segments * @return {EventListenerTree} @@ -68,27 +68,26 @@ EventListenerTree.prototype._getOrCreateChild = function(segments) { /** * Add a listener to a path location. Listener should be unique per path - * location, and calling twice witht the same segments and listener value has no - * effect. Unlike EventEmitter, listener may be any type of value. The value is - * returned to a callback upon calling `forEachAffected`. + * location, and calling twice with the same segments and listener value has no + * effect. Unlike EventEmitter, listener may be any type of value * * @param {string[]} segments * @param {*} listener */ EventListenerTree.prototype.addListener = function(segments, listener) { var node = this._getOrCreateChild(segments); - if (!node.listeners) { + if (node.listeners) { + var i = node.listeners.indexOf(listener); + if (i === -1) { + node.listeners.push(listener); + } + } else { node.listeners = [listener]; - return; - } - var i = node.listeners.indexOf(listener); - if (i === -1) { - node.listeners.push(listener); } }; /** - * Remove a listener from a path location. + * Remove a listener from a path location * * @param {string[]} segments * @param {*} listener @@ -112,7 +111,7 @@ EventListenerTree.prototype.removeListener = function(segments, listener) { }; /** - * Remove all listeners and descendent listeners for a path location. + * Remove all listeners and descendent listeners for a path location * * @param {string[]} segments */ @@ -124,71 +123,96 @@ EventListenerTree.prototype.removeAllListeners = function(segments) { }; /** - * Dispatch an event to each of the listeners that may be affected by a change - * to a model path. These are: + * Return direct listeners to `segments` + * + * @param {string[]} segments + * @return {Array} listeners + */ +EventListenerTree.prototype.getListeners = function(segments) { + var node = this._getChild(segments); + return (node && node.listeners) ? node.listeners.slice() : []; +}; + +/** + * Return an array with each of the listeners that may be affected by a change + * to `segments`. These are: * 1. Listeners to each node from the root to the node for `segments` * 2. Listeners to all descendent nodes under `segments` * - * Calls the callback with each listener value, conceptually similar to - * Array#forEach() + * @param {string[]} segments + * @return {Array} listeners + */ +EventListenerTree.prototype.getAffectedListeners = function(segments) { + var listeners = []; + var node = pushAncestorListeners(listeners, segments, this); + if (node) { + pushDescendantListeners(listeners, node); + } + return listeners; +}; + +/** + * Return an array with each of the listeners to descendent nodes, not + * including listeners to `segments` itself * - * @param {string[]} segments - * @param {Function} callback + * @param {string[]} segments + * @return {Array} listeners */ -EventListenerTree.prototype.forEachAffected = function(segments, callback) { - var node = forAncestorListeners(this, segments, callback); +EventListenerTree.prototype.getDescendantListeners = function(segments) { + var listeners = []; + var node = this._getChild(segments); if (node) { - forDescendantListeners(node, callback); + pushDescendantListeners(listeners, node); } + return listeners; }; /** - * Call the callback with each listener value in the current node. + * Push direct listeners onto the passed in array * + * @param {Array} listeners * @param {EventListenerTree} node - * @param {Function} callback */ -function forListeners(node, callback) { +function pushListeners(listeners, node) { if (!node.listeners) return; - for (var i = 0; i < node.listeners.length; i++) { - var listener = node.listeners[i]; - callback(listener); + for (var i = 0, len = node.listeners.length; i < len; i++) { + listeners.push(node.listeners[i]); } } /** - * Call the callback with each listener value from the root node passed in to - * the node for `segments`. Return the node at `segments` if it exists + * Push listeners for each ancestor node and the node at `segments` onto the + * passed in array. Return the node at `segments` if it exists * - * @param {EventListenerTree} node + * @param {Array} listeners * @param {string[]} segments - * @param {Function} callback + * @param {EventListenerTree} node * @return {EventListenerTree|undefined} */ -function forAncestorListeners(node, segments, callback) { - forListeners(node, callback); +function pushAncestorListeners(listeners, segments, node) { + pushListeners(listeners, node); for (var i = 0; i < segments.length; i++) { var segment = segments[i]; node = node.children && node.children[segment]; if (!node) return; - forListeners(node, callback); + pushListeners(listeners, node); } return node; } /** - * Call the callback with each listener value for each of the node's children - * and their recursive children + * Push listeners for each of the node's children and their recursive children + * onto the passed in array * + * @param {Array} listeners * @param {EventListenerTree} node - * @param {Function} callback */ -function forDescendantListeners(node, callback) { +function pushDescendantListeners(listeners, node) { if (!node.children) return; for (var key in node.children) { var child = node.children[key]; - forListeners(child, callback); - forDescendantListeners(child, callback); + pushListeners(listeners, child); + pushDescendantListeners(listeners, child); } } diff --git a/lib/Model/EventMapTree.js b/lib/Model/EventMapTree.js new file mode 100644 index 000000000..930ef810b --- /dev/null +++ b/lib/Model/EventMapTree.js @@ -0,0 +1,279 @@ +module.exports = EventMapTree; + +/** + * Construct a tree root when invoked without any arguments. Children nodes are + * constructred internally as needed on calls to addListener() + * + * @param {EventMapTree} [parent] + * @param {string} [segment] + */ +function EventMapTree(parent, segment) { + this.parent = parent; + this.segment = segment; + this.children = null; + this.listener = null; +} + +/** + * Remove the reference to this node from its parent so that it can be garbage + * collected. This is called internally when all listener to a node + * are removed + */ +EventMapTree.prototype._destroy = function() { + // For all non-root nodes, remove the reference to the node + if (this.parent) { + removeChild(this.parent, this.segment); + // For the root node, reset any references to listener or children + } else { + this.children = null; + this.listener = null; + } +}; + +/** + * Get a node for a path if it exists + * + * @param {string[]} segments + * @return {EventMapTree|undefined} + */ +EventMapTree.prototype._getChild = function(segments) { + var node = this; + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + node = node.children && node.children[segment]; + if (!node) return; + } + return node; +}; + +/** + * If a path already has a node, return it. Otherwise, create the node and + * parents in a lazy manner and return the node for the path + * + * @param {string[]} segments + * @return {EventMapTree} + */ +EventMapTree.prototype._getOrCreateChild = function(segments) { + var node = this; + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + if (!node.children) { + node.children = {}; + } + var node = node.children[segment] || + (node.children[segment] = new EventMapTree(node, segment)); + } + return node; +}; + +/** + * Assign a listener to a path location. Listener may be any type of value. + * Return the previous listener value if any + * + * @param {string[]} segments + * @param {*} listener + */ +EventMapTree.prototype.setListener = function(segments, listener) { + var node = this._getOrCreateChild(segments); + var previous = node.listener; + node.listener = listener; + return previous; +}; + +/** + * Remove the listener at a path location and return it + * + * @param {string[]} segments + * @return {*} listener + */ +EventMapTree.prototype.deleteListener = function(segments) { + var node = this._getChild(segments); + if (!node) return; + var previous = node.listener; + node.listener = null; + if (!node.children) { + node._destroy(); + } + return previous; +}; + +/** + * Remove all listeners and descendent listeners for a path location + * + * @param {string[]} segments + */ +EventMapTree.prototype.deleteAllListeners = function(segments) { + var node = this._getChild(segments); + if (node) { + node._destroy(); + } +}; + +/** + * Return the direct listener to `segments` if any + * + * @param {string[]} segments + * @return {*} listeners + */ +EventMapTree.prototype.getListener = function(segments) { + var node = this._getChild(segments); + return (node) ? node.listener : null; +}; + +/** + * Return an array with each of the listeners that may be affected by a change + * to `segments`. These are: + * 1. Listeners to each node from the root to the node for `segments` + * 2. Listeners to all descendent nodes under `segments` + * + * @param {string[]} segments + * @return {Array} listeners + */ +EventMapTree.prototype.getAffectedListeners = function(segments) { + var listeners = []; + var node = pushAncestorListeners(listeners, segments, this); + if (node) { + pushDescendantListeners(listeners, node); + } + return listeners; +}; + +/** + * Return an array with each of the listeners to `segments` and descendent nodes + * + * @param {string[]} segments + * @return {Array} listeners + */ +EventMapTree.prototype.getAllListeners = function(segments) { + var listeners = []; + var node = this._getChild(segments); + if (node) { + pushListener(listeners, node); + pushDescendantListeners(listeners, node); + } + return listeners; +}; + +/** + * Push node's direct listener onto the passed in array if not null + * + * @param {Array} listeners + * @param {EventMapTree} node + */ +function pushListener(listeners, node) { + if (node.listener != null) { + listeners.push(node.listener); + } +} + +/** + * Push listeners for each ancestor node and the node at `segments` onto the + * passed in array. Return the node at `segments` if it exists + * + * @param {Array} listeners + * @param {string[]} segments + * @param {EventMapTree} node + * @return {EventMapTree|undefined} + */ +function pushAncestorListeners(listeners, segments, node) { + pushListener(listeners, node); + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + node = node.children && node.children[segment]; + if (!node) return; + pushListener(listeners, node); + } + return node; +} + +/** + * Push listeners for each of the node's children and their recursive children + * onto the passed in array + * + * @param {Array} listeners + * @param {EventMapTree} node + */ +function pushDescendantListeners(listeners, node) { + if (!node.children) return; + for (var key in node.children) { + var child = node.children[key]; + pushListener(listeners, child); + pushDescendantListeners(listeners, child); + } +} + +/** + * Call the callback with each listener to the node and its decendants + * + * @param {EventMapTree} node + * @param {Function} callback + */ +EventMapTree.prototype.forEach = function(callback) { + forListener(this, callback); + forDescendantListeners(this, callback); +}; + +/** + * Call the callback with the node's direct listener if not null + * + * @param {EventMapTree} node + * @param {Function} callback + */ +function forListener(node, callback) { + if (node.listener != null) { + callback(node.listener); + } +} + +/** + * Call the callback with each listener value for each of the node's children + * and their recursive children + * + * @param {EventMapTree} node + * @param {Function} callback + */ +function forDescendantListeners(node, callback) { + if (!node.children) return; + for (var key in node.children) { + var child = node.children[key]; + forListener(child, callback); + forDescendantListeners(child, callback); + } +} + +/** + * Remove the child at the specified segment from a node. Also recursively + * remove parent nodes if there are no remaining dependencies + * + * @param {EventMapTree} node + * @param {string} segment + */ +function removeChild(node, segment) { + // Remove reference this node from its parent + if (hasOtherKeys(node.children, segment)) { + delete node.children[segment]; + return; + } + node.children = null; + + // Destroy parent if it no longer has any dependents + if (node.listener == null) { + node._destroy(); + } +} + +/** + * Return whether the object has any other property key other than the + * provided value. + * + * @param {Object} object + * @param {string} ignore + * @return {Boolean} + */ +function hasOtherKeys(object, ignore) { + for (var key in object) { + if (key !== ignore) return true; + } + return false; +} + diff --git a/lib/Model/bundle.js b/lib/Model/bundle.js index 3381bf000..d4376ad7e 100644 --- a/lib/Model/bundle.js +++ b/lib/Model/bundle.js @@ -38,14 +38,12 @@ Model.prototype.bundle = function(cb) { function stripComputed(root) { var silentModel = root.silent(); - var refListsMap = root._refLists.fromMap; - var fnsMap = root._fns.fromMap; - for (var from in refListsMap) { - silentModel._del(refListsMap[from].fromSegments); - } - for (var from in fnsMap) { - silentModel._del(fnsMap[from].fromSegments); - } + root._refLists.fromMap.forEach(function(refList) { + silentModel._del(refList.fromSegments); + }); + root._fns.fromMap.forEach(function(fn) { + silentModel._del(fn.fromSegments); + }); silentModel.removeAllFilters(); silentModel.destroy('$queries'); } diff --git a/lib/Model/fn.js b/lib/Model/fn.js index 8027344d0..cadf44836 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -2,25 +2,36 @@ var util = require('../util'); var Model = require('./Model'); var defaultFns = require('./defaultFns'); var EventListenerTree = require('./EventListenerTree'); +var EventMapTree = require('./EventMapTree'); function NamedFns() {} Model.INITS.push(function(model) { - model.root._namedFns = new NamedFns(); - model.root._fns = new Fns(model); - model.on('all', fnListener); - function fnListener(segments, eventArgs) { + var root = model.root; + root._namedFns = new NamedFns(); + root._fns = new Fns(root); + addFnListener(root); +}); + +function addFnListener(model) { + var inputListeners = model._fns.inputListeners; + var fromMap = model._fns.fromMap; + model.on('all', function fnListener(segments, eventArgs) { var pass = eventArgs[eventArgs.length - 1]; // Mutation affecting input path - model.root._fns.inputListeners.forEachAffected(segments, function(fn) { + var fns = inputListeners.getAffectedListeners(segments); + for (var i = 0; i < fns.length; i++) { + var fn = fns[i]; if (fn !== pass.$fn) fn.onInput(pass); - }); + } // Mutation affecting output path - model.root._fns.outputListeners.forEachAffected(segments, function(fn) { + var fns = fromMap.getAffectedListeners(segments); + for (var i = 0; i < fns.length; i++) { + var fn = fns[i]; if (fn !== pass.$fn) fn.onOutput(pass); - }); - } -}); + } + }); +} Model.prototype.fn = function(name, fns) { this.root._namedFns[name] = fns; @@ -86,11 +97,11 @@ Model.prototype.start = function() { }; Model.prototype.stop = function(subpath) { - var path = this.path(subpath); - this._stop(path); + var segments = this._splitPath(subpath); + this._stop(segments); }; -Model.prototype._stop = function(fromPath) { - this.root._fns.stop(fromPath); +Model.prototype._stop = function(segments) { + this.root._fns.stop(segments); }; Model.prototype.stopAll = function(subpath) { @@ -98,22 +109,14 @@ Model.prototype.stopAll = function(subpath) { this._stopAll(segments); }; Model.prototype._stopAll = function(segments) { - var fns = this.root._fns.fromMap; - for (var from in fns) { - var fromSegments = fns[from].fromSegments; - if (util.contains(segments, fromSegments)) { - this._stop(from); - } - } + this.root._fns.stopAll(segments); }; -function FromMap() {} function Fns(model) { this.model = model; - this.nameMap = model.root._namedFns; - this.fromMap = new FromMap(); + this.nameMap = model._namedFns; this.inputListeners = new EventListenerTree(); - this.outputListeners = new EventListenerTree(); + this.fromMap = new EventMapTree(); } Fns.prototype.get = function(name, inputPaths, fns, options) { @@ -125,40 +128,50 @@ Fns.prototype.get = function(name, inputPaths, fns, options) { Fns.prototype.start = function(name, path, inputPaths, fns, options) { fns || (fns = this.nameMap[name] || defaultFns[name]); var fn = new Fn(this.model, name, path, inputPaths, fns, options); - this.fromMap[path] = fn; + var previous = this.fromMap.setListener(fn.fromSegments, fn); + if (previous) { + this._removeInputListeners(previous); + } for (var i = 0; i < fn.inputsSegments.length; i++) { var inputSegements = fn.inputsSegments[i]; this.inputListeners.addListener(inputSegements, fn); } - this.outputListeners.addListener(fn.fromSegments, fn); return fn._onInput(); }; -Fns.prototype.stop = function(path) { - var fn = this.fromMap[path]; - if (fn) { - delete this.fromMap[path]; - for (var i = 0; i < fn.inputsSegments.length; i++) { - var inputSegements = fn.inputsSegments[i]; - this.inputListeners.removeListener(inputSegements, fn); - } - this.outputListeners.removeListener(fn.fromSegments, fn); +Fns.prototype.stop = function(segments) { + var previous = this.fromMap.deleteListener(segments); + if (previous) { + this._removeInputListeners(previous); + } +}; + +Fns.prototype.stopAll = function(segments) { + var listeners = this.fromMap.getAllListeners(segments); + for (var i = 0; i < listeners.length; i++) { + this._removeInputListeners(listeners[i]); + } + this.fromMap.deleteAllListeners(segments); +}; + +Fns.prototype._removeInputListeners = function(fn) { + for (var i = 0; i < fn.inputsSegments.length; i++) { + var inputSegements = fn.inputsSegments[i]; + this.inputListeners.removeListener(inputSegements, fn); } - return fn; }; Fns.prototype.toJSON = function() { var out = []; - for (var from in this.fromMap) { - var fn = this.fromMap[from]; + this.fromMap.forEach(function(fn) { // Don't try to bundle non-named functions that were started via // model.start directly instead of by name - if (!fn.name) continue; + if (!fn.name) return; var args = [fn.from].concat(fn.inputPaths); if (fn.options) args.push(fn.options); args.push(fn.name); out.push(args); - } + }); return out; }; diff --git a/lib/Model/ref.js b/lib/Model/ref.js index ffc4e0df0..42da73d03 100644 --- a/lib/Model/ref.js +++ b/lib/Model/ref.js @@ -1,5 +1,6 @@ -var util = require('../util'); var Model = require('./Model'); +var EventMapTree = require('./EventMapTree'); +var EventListenerTree = require('./EventListenerTree'); Model.INITS.push(function(model) { var root = model.root; @@ -48,19 +49,18 @@ function addIndexListeners(model) { onIndexChange(segments, patchMove); }); function onIndexChange(segments, patch) { - var fromMap = model._refs.fromMap; - for (var from in fromMap) { - var ref = fromMap[from]; - if (!(ref.updateIndices && - util.contains(segments, ref.toSegments) && - ref.toSegments.length > segments.length)) continue; + var toListeners = model._refs.toListeners; + var refs = toListeners.getDescendantListeners(segments); + for (var i = 0; i < refs.length; i++) { + var ref = refs[i]; + if (!ref.updateIndices) continue; var index = +ref.toSegments[segments.length]; var patched = patch(index); if (index === patched) continue; - model._refs.remove(from); + toListeners.removeListener(ref.toSegments, ref); ref.toSegments[segments.length] = '' + patched; ref.to = ref.toSegments.join('.'); - model._refs.add(ref); + toListeners.addListener(ref.toSegments, ref); } } } @@ -110,21 +110,22 @@ function addListener(model, type, fn) { // Find cases where an event is emitted on a path where a reference // is pointing. All original mutations happen on the fully dereferenced // location, so this detection only needs to happen in one direction - var toMap = model._refs.toMap; - var subpath; - for (var i = 0, len = segments.length; i < len; i++) { - subpath = (subpath) ? subpath + '.' + segments[i] : segments[i]; + var node = model._refs.toListeners; + for (var i = 0; i < segments.length; i++) { + var segment = segments[i]; + node = node.children && node.children[segment]; + if (!node) return; // If a ref is found pointing to a matching subpath, re-emit on the // place where the reference is coming from as if the mutation also // occured at that path - var refs = toMap[subpath]; + var refs = node.listeners; if (!refs) continue; // Shallow clone refs in case a ref is removed while going through // the loop refs = refs.slice(); var remaining = segments.slice(i + 1); - for (var refIndex = 0, numRefs = refs.length; refIndex < numRefs; refIndex++) { + for (var refIndex = 0; refIndex < refs.length; refIndex++) { var ref = refs[refIndex]; var dereferenced = ref.fromSegments.concat(remaining); // The value may already be up to date via object reference. If so, @@ -141,11 +142,9 @@ function addListener(model, type, fn) { } // If a ref points to a child of a matching subpath, get the value in // case it has changed and set if different - var parentToMap = model._refs.parentToMap; - var refs = parentToMap[subpath]; - if (!refs) return; - for (var refIndex = 0, numRefs = refs.length; refIndex < numRefs; refIndex++) { - var ref = refs[refIndex]; + var refs = node.getDescendantListeners([]); + for (var i = 0; i < refs.length; i++) { + var ref = refs[i]; var value = model._get(ref.toSegments); var previous = model._get(ref.fromSegments); if (previous !== value) { @@ -187,8 +186,8 @@ Model.prototype.ref = function() { throw new Error('ref must be performed under a collection ' + 'and document id. Invalid path: ' + fromPath); } - this.root._refs.remove(fromPath); - this.root._refLists.remove(fromPath); + this.root._refs.remove(ref.fromSegments); + this.root._refLists.remove(ref.fromSegments); var value = this.get(to); ref.model._set(ref.fromSegments, value); this.root._refs.add(ref); @@ -197,12 +196,11 @@ Model.prototype.ref = function() { Model.prototype.removeRef = function(subpath) { var segments = this._splitPath(subpath); - var fromPath = segments.join('.'); - this._removeRef(segments, fromPath); + this._removeRef(segments); }; -Model.prototype._removeRef = function(segments, fromPath) { - this.root._refs.remove(fromPath); - this.root._refLists.remove(fromPath); +Model.prototype._removeRef = function(segments) { + this.root._refs.remove(segments); + this.root._refLists.remove(segments); this._del(segments); }; @@ -211,15 +209,17 @@ Model.prototype.removeAllRefs = function(subpath) { this._removeAllRefs(segments); }; Model.prototype._removeAllRefs = function(segments) { - this._removeMapRefs(segments, this.root._refs.fromMap); - this._removeMapRefs(segments, this.root._refLists.fromMap); -}; -Model.prototype._removeMapRefs = function(segments, map) { - for (var from in map) { - var fromSegments = map[from].fromSegments; - if (util.contains(segments, fromSegments)) { - this._removeRef(fromSegments, from); - } + var refs = this.root._refs.fromMap.getAllListeners(segments); + for (var i = 0; i < refs.length; i++) { + var ref = refs[i]; + this.root._refs.remove(ref.fromSegments); + this._del(ref.fromSegments); + } + var refLists = this.root._refLists.fromMap.getAllListeners(segments); + for (var i = 0; i < refLists.length; i++) { + var refList = refLists[i]; + this.root._refLists.remove(refList.fromSegments); + this._del(refList.fromSegments); } }; @@ -230,16 +230,16 @@ Model.prototype.dereference = function(subpath) { Model.prototype._dereference = function(segments, forArrayMutator, ignore) { if (segments.length === 0) return segments; - var refs = this.root._refs.fromMap; - var refLists = this.root._refLists.fromMap; var doAgain; do { - var subpath = ''; + var refsNode = this.root._refs.fromMap; + var refListsNode = this.root._refLists.fromMap; doAgain = false; for (var i = 0, len = segments.length; i < len; i++) { - subpath = (subpath) ? subpath + '.' + segments[i] : segments[i]; + var segment = segments[i]; - var ref = refs[subpath]; + refsNode = refsNode && refsNode.children && refsNode.children[segment]; + var ref = refsNode && refsNode.listener; if (ref) { var remaining = segments.slice(i + 1); segments = ref.toSegments.concat(remaining); @@ -247,7 +247,8 @@ Model.prototype._dereference = function(segments, forArrayMutator, ignore) { break; } - var refList = refLists[subpath]; + refListsNode = refListsNode && refListsNode.children && refListsNode.children[segment]; + var refList = refListsNode && refListsNode.listener; if (refList && refList !== ignore) { var belowDescendant = i + 2 < len; var belowChild = i + 1 < len; @@ -274,60 +275,29 @@ function Ref(model, from, to, options) { this.to = to; this.fromSegments = from.split('.'); this.toSegments = to.split('.'); - this.parentTos = []; - for (var i = 1, len = this.toSegments.length; i < len; i++) { - var parentTo = this.toSegments.slice(0, i).join('.'); - this.parentTos.push(parentTo); - } this.updateIndices = options && options.updateIndices; } -function FromMap() {} -function ToMap() {} function Refs() { - this.fromMap = new FromMap(); - this.toMap = new ToMap(); - this.parentToMap = new ToMap(); + this.fromMap = new EventMapTree(); + this.toListeners = new EventListenerTree(); } Refs.prototype.add = function(ref) { - this.fromMap[ref.from] = ref; - listMapAdd(this.toMap, ref.to, ref); - for (var i = 0, len = ref.parentTos.length; i < len; i++) { - listMapAdd(this.parentToMap, ref.parentTos[i], ref); - } + this.fromMap.setListener(ref.fromSegments, ref); + this.toListeners.addListener(ref.toSegments, ref); }; -Refs.prototype.remove = function(from) { - var ref = this.fromMap[from]; +Refs.prototype.remove = function(segments) { + var ref = this.fromMap.deleteListener(segments); if (!ref) return; - delete this.fromMap[from]; - listMapRemove(this.toMap, ref.to, ref); - for (var i = 0, len = ref.parentTos.length; i < len; i++) { - listMapRemove(this.parentToMap, ref.parentTos[i], ref); - } - return ref; + this.toListeners.removeListener(ref.toSegments, ref); }; Refs.prototype.toJSON = function() { var out = []; - for (var from in this.fromMap) { - var ref = this.fromMap[from]; + this.fromMap.forEach(function(ref) { out.push([ref.from, ref.to]); - } + }); return out; }; - -function listMapAdd(map, name, item) { - map[name] || (map[name] = []); - map[name].push(item); -} - -function listMapRemove(map, name, item) { - var items = map[name]; - if (!items) return; - var index = items.indexOf(item); - if (index === -1) return; - items.splice(index, 1); - if (!items.length) delete map[name]; -} diff --git a/lib/Model/refList.js b/lib/Model/refList.js index 852388418..a687b5b4a 100644 --- a/lib/Model/refList.js +++ b/lib/Model/refList.js @@ -1,5 +1,6 @@ -var util = require('../util'); var Model = require('./Model'); +var EventMapTree = require('./EventMapTree'); +var EventListenerTree = require('./EventListenerTree'); Model.INITS.push(function(model) { var root = model.root; @@ -14,11 +15,26 @@ function addListener(model, type) { function refListListener(segments, eventArgs) { var pass = eventArgs[eventArgs.length - 1]; // Check for updates on or underneath paths - var fromMap = model._refLists.fromMap; - for (var from in fromMap) { - var refList = fromMap[from]; - if (pass.$refList === refList) continue; - refList.onMutation(type, segments, eventArgs); + var refLists = model._refLists.fromMap.getAffectedListeners(segments); + for (var i = 0; i < refLists.length; i++) { + var refList = refLists[i]; + if (pass.$refList !== refList) { + patchFromEvent(type, segments, eventArgs, refList); + } + }; + var refLists = model._refLists.toListeners.getAffectedListeners(segments); + for (var i = 0; i < refLists.length; i++) { + var refList = refLists[i]; + if (pass.$refList !== refList) { + patchToEvent(type, segments, eventArgs, refList); + } + }; + var refLists = model._refLists.idsListeners.getAffectedListeners(segments); + for (var i = 0; i < refLists.length; i++) { + var refList = refLists[i]; + if (pass.$refList !== refList) { + patchIdsEvent(type, segments, eventArgs, refList); + } } } } @@ -365,7 +381,7 @@ Model.prototype.refList = function() { } var idsPath = this.path(ids); var refList = new RefList(this.root, fromPath, toPath, idsPath, options); - this.root._refLists.remove(fromPath); + this.root._refLists.remove(refList.fromSegments); refList.model._setArrayDiff(refList.fromSegments, refList.get()); this.root._refLists.add(refList); return this.scope(fromPath); @@ -448,37 +464,30 @@ RefList.prototype.itemById = function(id) { RefList.prototype.idByIndex = function(index) { return this.model._get(this.idsSegments.concat(index)); }; -RefList.prototype.onMutation = function(type, segments, eventArgs) { - if (util.mayImpact(this.toSegments, segments)) { - patchToEvent(type, segments, eventArgs, this); - } else if (util.mayImpact(this.idsSegments, segments)) { - patchIdsEvent(type, segments, eventArgs, this); - } else if (util.mayImpact(this.fromSegments, segments)) { - patchFromEvent(type, segments, eventArgs, this); - } -}; - -function FromMap() {} function RefLists() { - this.fromMap = new FromMap(); + this.fromMap = new EventMapTree(); + this.toListeners = new EventListenerTree(); + this.idsListeners = new EventListenerTree(); } RefLists.prototype.add = function(refList) { - this.fromMap[refList.from] = refList; + this.fromMap.setListener(refList.fromSegments, refList); + this.toListeners.addListener(refList.toSegments, refList); + this.idsListeners.addListener(refList.idsSegments, refList); }; -RefLists.prototype.remove = function(from) { - var refList = this.fromMap[from]; - delete this.fromMap[from]; - return refList; +RefLists.prototype.remove = function(fromSegments) { + var refList = this.fromMap.deleteListener(fromSegments); + if (!refList) return; + this.toListeners.removeListener(refList.toSegments, refList); + this.idsListeners.removeListener(refList.idsSegments, refList); }; RefLists.prototype.toJSON = function() { var out = []; - for (var from in this.fromMap) { - var refList = this.fromMap[from]; + this.fromMap.forEach(function(refList) { out.push([refList.from, refList.to, refList.ids, refList.options]); - } + }); return out; }; diff --git a/test/Model/EventListenerTree.js b/test/Model/EventListenerTree.js index 56be7b3e8..6f6d70acc 100644 --- a/test/Model/EventListenerTree.js +++ b/test/Model/EventListenerTree.js @@ -2,189 +2,186 @@ var expect = require('../util').expect; var EventListenerTree = require('../../lib/Model/EventListenerTree'); describe('EventListenerTree', function() { - describe('implementation', function() { - describe('constructor', function() { - it('creates an empty tree', function() { - var tree = new EventListenerTree(); - expect(tree.listeners).equal(null); - expect(tree.children).equal(null); - }); - }); - describe('addListener', function() { - it('adds a listener object at the root', function() { - var tree = new EventListenerTree(); - var listener = {}; - tree.addListener([], listener); - expect(tree.listeners).eql([listener]); - expect(tree.children).eql(null); - }); - it('only a listener object once', function() { - var tree = new EventListenerTree(); - var listener = {}; - tree.addListener([], listener); - tree.addListener([], listener); - expect(tree.listeners).eql([listener]); - expect(tree.children).eql(null); - }); - it('adds a listener object at a path', function() { - var tree = new EventListenerTree(); - var listener = {}; - tree.addListener(['colors'], listener); - expect(tree.listeners).eql(null); - expect(tree.children.colors.listeners).eql([listener]); - }); - it('adds a listener object at a subpath', function() { - var tree = new EventListenerTree(); - var listener = {}; - tree.addListener(['colors', 'green'], listener); - expect(tree.listeners).eql(null); - expect(tree.children.colors.listeners).eql(null); - expect(tree.children.colors.children.green.listeners).eql([listener]); - }); - }); - describe('removeListener', function() { - it('can be called before addListener', function() { - var tree = new EventListenerTree(); - var listener = {}; - tree.removeListener([], listener); - expect(tree.listeners).eql(null); - expect(tree.children).eql(null); - }); - it('removes listener at root', function() { - var tree = new EventListenerTree(); - var listener = {}; - tree.addListener([], listener); - expect(tree.listeners).eql([listener]); - tree.removeListener([], listener); - expect(tree.listeners).eql(null); - }); - it('removes listener at subpath', function() { - var tree = new EventListenerTree(); - var listener = {}; - tree.addListener(['colors', 'green'], listener); - expect(tree.children.colors.children.green.listeners).eql([listener]); - tree.removeListener(['colors', 'green'], listener); - expect(tree.children).eql(null); - }); - it('does not remove listener if not found with one listener', function() { - var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - tree.addListener(['colors', 'green'], listener1); - expect(tree.children.colors.children.green.listeners).eql([listener1]); - tree.removeListener(['colors', 'green'], listener2); - expect(tree.children.colors.children.green.listeners).eql([listener1]); - }); - it('does not remove listener if not found with multiple listeners', function() { - var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - tree.addListener(['colors', 'green'], listener1); - tree.addListener(['colors', 'green'], listener2); - expect(tree.children.colors.children.green.listeners).eql([listener1, listener2]); - tree.removeListener(['colors', 'green'], listener3); - expect(tree.children.colors.children.green.listeners).eql([listener1, listener2]); - }); - it('removes listener with remaining peers', function() { - var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - tree.addListener([], listener1); - tree.addListener([], listener2); - tree.addListener([], listener3); - expect(tree.listeners).eql([listener1, listener2, listener3]); - tree.removeListener([], listener2); - expect(tree.listeners).eql([listener1, listener3]); - tree.removeListener([], listener3); - expect(tree.listeners).eql([listener1]); - tree.removeListener([], listener1); - expect(tree.listeners).eql(null); - }); - it('removes listener with remaining peer children', function() { - var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - tree.addListener(['colors'], listener1); - tree.addListener(['colors', 'green'], listener2); - expect(tree.children.colors.listeners).eql([listener1]); - expect(tree.children.colors.children.green.listeners).eql([listener2]); - tree.removeListener(['colors'], listener1); - expect(tree.children.colors.listeners).eql(null); - expect(tree.children.colors.children.green.listeners).eql([listener2]); - }); - }); - describe('removeAllListeners', function() { - it('can be called on empty root', function() { - var tree = new EventListenerTree(); - tree.removeAllListeners([]); - }); - it('can be called on missing node', function() { - var tree = new EventListenerTree(); - tree.removeAllListeners(['colors', 'green']); - }); - it('removes all listeners and children when called on root', function() { - var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - tree.addListener([], listener1); - tree.addListener(['colors'], listener2); - tree.addListener(['colors', 'green'], listener3); - tree.removeAllListeners([]); - expect(tree.listeners).eql(null); - expect(tree.children).eql(null); - }); - it('removes listeners and descendent children on path', function() { - var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - tree.addListener([], listener1); - tree.addListener(['colors'], listener2); - tree.addListener(['colors', 'green'], listener3); - tree.removeAllListeners(['colors']); - expect(tree.listeners).eql([listener1]); - expect(tree.children).eql(null); - }); + describe('addListener', function() { + it('adds a listener object at the root', function() { + var tree = new EventListenerTree(); + var listener = {}; + tree.addListener([], listener); + expect(tree.getListeners([])).eql([listener]); + expect(tree.children).eql(null); + }); + it('only a listener object once', function() { + var tree = new EventListenerTree(); + var listener = {}; + tree.addListener([], listener); + tree.addListener([], listener); + expect(tree.getListeners([])).eql([listener]); + expect(tree.children).eql(null); + }); + it('adds a listener object at a path', function() { + var tree = new EventListenerTree(); + var listener = {}; + tree.addListener(['colors'], listener); + expect(tree.getListeners([])).eql([]); + expect(tree.getListeners(['colors'])).eql([listener]); + }); + it('adds a listener object at a subpath', function() { + var tree = new EventListenerTree(); + var listener = {}; + tree.addListener(['colors', 'green'], listener); + expect(tree.getListeners([])).eql([]); + expect(tree.getListeners(['colors'])).eql([]); + expect(tree.getListeners(['colors', 'green'])).eql([listener]); }); }); - describe('forEachAffected', function() { - function expectResults(expected, done) { - var pending = expected.slice(); - return function(result) { - var value = pending.shift(); - expect(value).eql(result); - if (pending.length > 0) return; - done(); - }; - } - it('can be called without listeners', function(done) { - var tree = new EventListenerTree(); - tree.forEachAffected([], done); - done(); - }); - it('calls a callback with all direct listeners', function(done) { + describe('removeListener', function() { + it('can be called before addListener', function() { + var tree = new EventListenerTree(); + var listener = {}; + tree.removeListener(['colors', 'green'], listener); + expect(tree.getListeners(['colors', 'green'])).eql([]); + expect(tree.children).eql(null); + }); + it('removes listener at root', function() { + var tree = new EventListenerTree(); + var listener = {}; + tree.addListener([], listener); + expect(tree.getListeners([])).eql([listener]); + tree.removeListener([], listener); + expect(tree.getListeners([])).eql([]); + }); + it('removes listener at subpath', function() { + var tree = new EventListenerTree(); + var listener = {}; + tree.addListener(['colors', 'green'], listener); + expect(tree.getListeners(['colors', 'green'])).eql([listener]); + tree.removeListener(['colors', 'green'], listener); + expect(tree.children).eql(null); + }); + it('removes listener at subpath with remaining peers', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + tree.addListener(['colors', 'green'], listener1); + tree.addListener(['colors', 'red'], listener2); + tree.removeListener(['colors', 'green'], listener1); + expect(tree.getListeners(['colors', 'green'])).eql([]); + expect(tree.getListeners(['colors', 'red'])).eql([listener2]); + }); + it('does not remove listener if not found with one listener', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + tree.addListener(['colors', 'green'], listener1); + expect(tree.getListeners(['colors', 'green'])).eql([listener1]); + tree.removeListener(['colors', 'green'], listener2); + expect(tree.getListeners(['colors', 'green'])).eql([listener1]); + }); + it('does not remove listener if not found with multiple listeners', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + tree.addListener(['colors', 'green'], listener1); + tree.addListener(['colors', 'green'], listener2); + expect(tree.getListeners(['colors', 'green'])).eql([listener1, listener2]); + tree.removeListener(['colors', 'green'], listener3); + expect(tree.getListeners(['colors', 'green'])).eql([listener1, listener2]); + }); + it('removes listener with remaining peers', function() { var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; + var listener3 = 'listener3'; tree.addListener([], listener1); tree.addListener([], listener2); - var callback = expectResults([listener1, listener2], done); - tree.forEachAffected([], callback); + tree.addListener([], listener3); + expect(tree.getListeners([])).eql([listener1, listener2, listener3]); + tree.removeListener([], listener2); + expect(tree.getListeners([])).eql([listener1, listener3]); + tree.removeListener([], listener3); + expect(tree.getListeners([])).eql([listener1]); + tree.removeListener([], listener1); + expect(tree.getListeners([])).eql([]); + }); + it('removes listener with remaining peer children', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + tree.addListener(['colors'], listener1); + tree.addListener(['colors', 'green'], listener2); + expect(tree.getListeners(['colors'])).eql([listener1]); + expect(tree.getListeners(['colors', 'green'])).eql([listener2]); + tree.removeListener(['colors'], listener1); + expect(tree.getListeners(['colors'])).eql([]); + expect(tree.getListeners(['colors', 'green'])).eql([listener2]); + }); + }); + describe('removeAllListeners', function() { + it('can be called on empty root', function() { + var tree = new EventListenerTree(); + tree.removeAllListeners([]); }); - it('removeListener stops listener from being returned', function(done) { + it('can be called on missing node', function() { + var tree = new EventListenerTree(); + tree.removeAllListeners(['colors', 'green']); + }); + it('removes all listeners and children when called on root', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + tree.addListener([], listener1); + tree.addListener(['colors'], listener2); + tree.addListener(['colors', 'green'], listener3); + tree.removeAllListeners([]); + expect(tree.getListeners([])).eql([]); + expect(tree.children).eql(null); + }); + it('removes listeners and descendent children on path', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + tree.addListener([], listener1); + tree.addListener(['colors'], listener2); + tree.addListener(['colors', 'green'], listener3); + tree.removeAllListeners(['colors']); + expect(tree.getListeners([])).eql([listener1]); + expect(tree.children).eql(null); + }); + }); + describe('getAffectedListeners', function() { + it('returns empty array without listeners', function() { + var tree = new EventListenerTree(); + var affected = tree.getAffectedListeners([]); + expect(affected).eql([]); + }); + it('returns empty array on path without node', function() { + var tree = new EventListenerTree(); + var affected = tree.getAffectedListeners(['colors', 'green']); + expect(affected).eql([]); + }); + it('returns all direct listeners', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + tree.addListener([], listener1); + tree.addListener([], listener2); + var affected = tree.getAffectedListeners([]); + expect(affected).eql([listener1, listener2]); + }); + it('removeListener stops listener from being returned', function() { var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; tree.addListener([], listener1); tree.addListener([], listener2); tree.removeListener([], listener1); - var callback = expectResults([listener2], done); - tree.forEachAffected([], callback); + var affected = tree.getAffectedListeners([]); + expect(affected).eql([listener2]); }); - it('calls a callback with all descendant listeners in depth order', function(done) { + it('returns all descendant listeners', function() { var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; @@ -196,10 +193,61 @@ describe('EventListenerTree', function() { tree.addListener(['colors', 'red'], listener3); tree.addListener([], listener4); tree.addListener(['colors'], listener5); - var callback = expectResults([listener4, listener5, listener1, listener2, listener3], done); - tree.forEachAffected([], callback); + var affected = tree.getAffectedListeners([]); + expect(affected).eql([listener4, listener5, listener1, listener2, listener3]); + }); + it('returns all parent listeners in depth order', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + var listener4 = 'listener4'; + var listener5 = 'listener5'; + tree.addListener(['colors', 'green'], listener1); + tree.addListener(['colors', 'red'], listener2); + tree.addListener(['colors', 'red'], listener3); + tree.addListener([], listener4); + tree.addListener(['colors'], listener5); + var affected = tree.getAffectedListeners(['colors', 'green']); + expect(affected).eql([listener4, listener5, listener1]); + }); + it('does not return peers or peer children', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + var listener4 = 'listener4'; + var listener5 = 'listener5'; + tree.addListener([], listener1); + tree.addListener(['colors'], listener2); + tree.addListener(['colors', 'green'], listener3); + tree.addListener(['textures'], listener4); + tree.addListener(['textures', 'smooth'], listener5); + var affected = tree.getAffectedListeners(['textures']); + expect(affected).eql([listener1, listener4, listener5]); + }); + }); + describe('getDescendantListeners', function() { + it('returns empty array without listeners', function() { + var tree = new EventListenerTree(); + var affected = tree.getDescendantListeners([]); + expect(affected).eql([]); + }); + it('returns empty array on path without node', function() { + var tree = new EventListenerTree(); + var affected = tree.getDescendantListeners(['colors', 'green']); + expect(affected).eql([]); + }); + it('does not return direct listeners', function() { + var tree = new EventListenerTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + tree.addListener([], listener1); + tree.addListener([], listener2); + var affected = tree.getDescendantListeners([]); + expect(affected).eql([]); }); - it('calls a callback with all parent listeners in depth order', function(done) { + it('returns all descendant listeners', function() { var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; @@ -211,10 +259,10 @@ describe('EventListenerTree', function() { tree.addListener(['colors', 'red'], listener3); tree.addListener([], listener4); tree.addListener(['colors'], listener5); - var callback = expectResults([listener4, listener5, listener1], done); - tree.forEachAffected(['colors', 'green'], callback); + var affected = tree.getDescendantListeners([]); + expect(affected).eql([listener5, listener1, listener2, listener3]); }); - it('does not call for peers or peer children', function(done) { + it('does not return parent or peer listeners', function() { var tree = new EventListenerTree(); var listener1 = 'listener1'; var listener2 = 'listener2'; @@ -226,8 +274,8 @@ describe('EventListenerTree', function() { tree.addListener(['colors', 'green'], listener3); tree.addListener(['textures'], listener4); tree.addListener(['textures', 'smooth'], listener5); - var callback = expectResults([listener1, listener4, listener5], done); - tree.forEachAffected(['textures'], callback); + var affected = tree.getDescendantListeners(['textures']); + expect(affected).eql([listener5]); }); }); }); diff --git a/test/Model/EventMapTree.js b/test/Model/EventMapTree.js new file mode 100644 index 000000000..262a65bf7 --- /dev/null +++ b/test/Model/EventMapTree.js @@ -0,0 +1,271 @@ +var expect = require('../util').expect; +var EventMapTree = require('../../lib/Model/EventMapTree'); + +describe('EventMapTree', function() { + describe('setListener', function() { + it('sets a listener object at the root', function() { + var tree = new EventMapTree(); + var listener = {}; + tree.setListener([], listener); + expect(tree.getListener([])).equal(listener); + expect(tree.children).equal(null); + }); + it('setting returns the previous listener', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var previous = tree.setListener([], listener1); + expect(previous).equal(null); + expect(tree.getListener([])).equal(listener1); + var previous = tree.setListener([], listener2); + expect(previous).equal(listener1); + expect(tree.getListener([])).equal(listener2); + expect(tree.children).equal(null); + }); + it('sets a listener object at a path', function() { + var tree = new EventMapTree(); + var listener = {}; + tree.setListener(['colors'], listener); + expect(tree.getListener([])).equal(null); + expect(tree.getListener(['colors'])).equal(listener); + }); + it('sets a listener object at a subpath', function() { + var tree = new EventMapTree(); + var listener = {}; + tree.setListener(['colors', 'green'], listener); + expect(tree.getListener([])).equal(null); + expect(tree.getListener(['colors'])).equal(null); + expect(tree.getListener(['colors', 'green'])).equal(listener); + }); + }); + describe('deleteListener', function() { + it('can be called before setListener', function() { + var tree = new EventMapTree(); + tree.deleteListener(['colors', 'green']); + expect(tree.getListener(['colors', 'green'])).equal(null); + expect(tree.children).equal(null); + }); + it('deletes listener at root', function() { + var tree = new EventMapTree(); + var listener = {}; + tree.setListener([], listener); + expect(tree.getListener([])).equal(listener); + var previous = tree.deleteListener([]); + expect(previous).equal(listener); + expect(tree.getListener([])).equal(null); + }); + it('deletes listener at subpath', function() { + var tree = new EventMapTree(); + var listener = {}; + tree.setListener(['colors', 'green'], listener); + expect(tree.getListener(['colors', 'green'])).equal(listener); + var previous = tree.deleteListener(['colors', 'green']); + expect(previous).equal(listener); + expect(tree.children).equal(null); + }); + it('deletes listener with remaining children', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + tree.setListener(['colors'], listener1); + tree.setListener(['colors', 'green'], listener2); + expect(tree.getListener(['colors'])).equal(listener1); + expect(tree.getListener(['colors', 'green'])).equal(listener2); + tree.deleteListener(['colors']); + expect(tree.getListener(['colors'])).equal(null); + expect(tree.getListener(['colors', 'green'])).equal(listener2); + }); + it('deletes listener with remaining peers', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + tree.setListener(['colors', 'red'], listener1); + tree.setListener(['colors', 'green'], listener2); + expect(tree.getListener(['colors', 'red'])).equal(listener1); + expect(tree.getListener(['colors', 'green'])).equal(listener2); + tree.deleteListener(['colors', 'red']); + expect(tree.getListener(['colors', 'red'])).equal(null); + expect(tree.getListener(['colors', 'green'])).equal(listener2); + }); + }); + describe('deleteAllListeners', function() { + it('can be called on empty root', function() { + var tree = new EventMapTree(); + tree.deleteAllListeners([]); + }); + it('can be called on missing node', function() { + var tree = new EventMapTree(); + tree.deleteAllListeners(['colors', 'green']); + }); + it('deletes all listeners and children when called on root', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + tree.setListener([], listener1); + tree.setListener(['colors'], listener2); + tree.setListener(['colors', 'green'], listener3); + tree.deleteAllListeners([]); + expect(tree.getListener([])).equal(null); + expect(tree.children).equal(null); + }); + it('deletes listeners and descendent children on path', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + tree.setListener([], listener1); + tree.setListener(['colors'], listener2); + tree.setListener(['colors', 'green'], listener3); + tree.deleteAllListeners(['colors']); + expect(tree.getListener([])).equal(listener1); + expect(tree.children).equal(null); + }); + }); + describe('getAffectedListeners', function() { + it('returns empty array without listeners', function() { + var tree = new EventMapTree(); + var affected = tree.getAffectedListeners([]); + expect(affected).eql([]); + }); + it('returns empty array on path without node', function() { + var tree = new EventMapTree(); + var affected = tree.getAffectedListeners(['colors', 'green']); + expect(affected).eql([]); + }); + it('returns all direct listeners', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + tree.setListener([], listener1); + var affected = tree.getAffectedListeners([]); + expect(affected).eql([listener1]); + }); + it('deleteListener stops listener from being returned', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + tree.setListener([], listener1); + tree.deleteListener([], listener1); + var affected = tree.getAffectedListeners([]); + expect(affected).eql([]); + }); + it('returns all descendant listeners', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + var listener4 = 'listener4'; + tree.setListener(['colors', 'green'], listener1); + tree.setListener(['colors', 'red'], listener2); + tree.setListener([], listener3); + tree.setListener(['colors'], listener4); + var affected = tree.getAffectedListeners([]); + expect(affected).eql([listener3, listener4, listener1, listener2]); + }); + it('returns all parent listeners in depth order', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + var listener4 = 'listener4'; + tree.setListener(['colors', 'green'], listener1); + tree.setListener(['colors', 'red'], listener2); + tree.setListener([], listener3); + tree.setListener(['colors'], listener4); + var affected = tree.getAffectedListeners(['colors', 'green']); + expect(affected).eql([listener3, listener4, listener1]); + }); + it('does not return peers or peer children', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + var listener4 = 'listener4'; + var listener5 = 'listener5'; + tree.setListener([], listener1); + tree.setListener(['colors'], listener2); + tree.setListener(['colors', 'green'], listener3); + tree.setListener(['textures'], listener4); + tree.setListener(['textures', 'smooth'], listener5); + var affected = tree.getAffectedListeners(['textures']); + expect(affected).eql([listener1, listener4, listener5]); + }); + }); + describe('getAllListeners', function() { + it('returns empty array without listeners', function() { + var tree = new EventMapTree(); + var affected = tree.getAllListeners([]); + expect(affected).eql([]); + }); + it('returns empty array on path without node', function() { + var tree = new EventMapTree(); + var affected = tree.getAllListeners(['colors', 'green']); + expect(affected).eql([]); + }); + it('returns direct listener', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + tree.setListener([], listener1); + var affected = tree.getAllListeners([]); + expect(affected).eql([listener1]); + }); + it('returns all descendant listeners', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + var listener4 = 'listener4'; + tree.setListener(['colors', 'green'], listener1); + tree.setListener(['colors', 'red'], listener2); + tree.setListener([], listener3); + tree.setListener(['colors'], listener4); + var affected = tree.getAllListeners([]); + expect(affected).eql([listener3, listener4, listener1, listener2]); + }); + it('does not return parent or peer listeners', function() { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + var listener4 = 'listener4'; + var listener5 = 'listener5'; + tree.setListener([], listener1); + tree.setListener(['colors'], listener2); + tree.setListener(['colors', 'green'], listener3); + tree.setListener(['textures'], listener4); + tree.setListener(['textures', 'smooth'], listener5); + var affected = tree.getAllListeners(['textures']); + expect(affected).eql([listener4, listener5]); + }); + }); + describe('forEach', function() { + it('can be called on empty tree', function() { + var tree = new EventMapTree(); + tree.forEach(function() { + throw new Error('Unexpected call'); + }); + }); + it('calls back with direct listener', function(done) { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + tree.setListener([], listener1); + tree.forEach(function(listener) { + expect(listener).equal(listener1); + done(); + }); + }); + it('calls back with each descendant listener', function(done) { + var tree = new EventMapTree(); + var listener1 = 'listener1'; + var listener2 = 'listener2'; + var listener3 = 'listener3'; + tree.setListener(['colors'], listener1); + tree.setListener(['colors', 'green'], listener2); + tree.setListener(['colors', 'red'], listener3); + var expected = [listener1, listener2, listener3]; + tree.forEach(function(listener) { + expect(listener).equal(expected.shift()); + if (expected.length === 0) done(); + }); + }); + }); +}); diff --git a/test/Model/fn.js b/test/Model/fn.js index 9d1706f64..41869e741 100644 --- a/test/Model/fn.js +++ b/test/Model/fn.js @@ -55,8 +55,18 @@ describe('fn', function() { expect(result).to.equal(6); }); }); - describe('start and stop with getter', function() { + describe('start', function() { it('sets the output immediately on start', function() { + var model = new Model(); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + var value = model.start('_nums.sum', '_nums.a', '_nums.b', function(a, b) { + return a + b; + }); + expect(value).to.equal(6); + expect(model.get('_nums.sum')).to.equal(6); + }); + it('supports function name argument', function() { var model = new Model(); model.fn('sum', function(a, b) { return a + b; @@ -69,26 +79,24 @@ describe('fn', function() { }); it('sets the output when an input changes', function() { var model = new Model(); - model.fn('sum', function(a, b) { - return a + b; - }); model.set('_nums.a', 2); model.set('_nums.b', 4); - model.start('_nums.sum', '_nums.a', '_nums.b', 'sum'); + model.start('_nums.sum', '_nums.a', '_nums.b', function(a, b) { + return a + b; + }); expect(model.get('_nums.sum')).to.equal(6); model.set('_nums.a', 5); expect(model.get('_nums.sum')).to.equal(9); }); it('sets the output when a parent of the input changes', function() { var model = new Model(); - model.fn('sum', function(a, b) { - return a + b; - }); model.set('_nums.in', { a: 2, b: 4 }); - model.start('_nums.sum', '_nums.in.a', '_nums.in.b', 'sum'); + model.start('_nums.sum', '_nums.in.a', '_nums.in.b', function(a, b) { + return a + b; + }); expect(model.get('_nums.sum')).to.equal(6); model.set('_nums.in', { a: 5, @@ -98,16 +106,15 @@ describe('fn', function() { }); it('does not set the output when a sibling of the input changes', function() { var model = new Model(); - var count = 0; - model.fn('sum', function(a, b) { - count++; - return a + b; - }); model.set('_nums.in', { a: 2, b: 4 }); - model.start('_nums.sum', '_nums.in.a', '_nums.in.b', 'sum'); + var count = 0; + model.start('_nums.sum', '_nums.in.a', '_nums.in.b', function(a, b) { + count++; + return a + b; + }); expect(model.get('_nums.sum')).to.equal(6); expect(count).to.equal(1); model.set('_nums.in.a', 3); @@ -117,18 +124,41 @@ describe('fn', function() { expect(model.get('_nums.sum')).to.equal(7); expect(count).to.equal(2); }); + it('calling twice cleans up listeners for former function', function() { + var model = new Model(); + model.set('_nums.in', { + a: 2, + b: 4 + }); + var count = 0; + model.start('_nums.sum', '_nums.in.a', '_nums.in.b', function(a, b) { + count++; + return a + b; + }); + expect(model.get('_nums.sum')).to.equal(6); + expect(count).to.equal(1); + model.start('_nums.sum', '_nums.in.a', '_nums.in.b', function(a, b) { + return a + b; + }); + expect(model.get('_nums.sum')).to.equal(6); + expect(count).to.equal(1); + model.set('_nums.in.a', 3); + expect(model.get('_nums.sum')).to.equal(7); + expect(count).to.equal(1); + }); + }); + describe('stop', function() { it('can call stop without start', function() { var model = new Model(); model.stop('_nums.sum'); }); it('stops updating after calling stop', function() { var model = new Model(); - model.fn('sum', function(a, b) { - return a + b; - }); model.set('_nums.a', 2); model.set('_nums.b', 4); - model.start('_nums.sum', '_nums.a', '_nums.b', 'sum'); + model.start('_nums.sum', '_nums.a', '_nums.b', function(a, b) { + return a + b; + }); model.set('_nums.a', 1); expect(model.get('_nums.sum')).to.equal(5); model.stop('_nums.sum'); @@ -136,85 +166,56 @@ describe('fn', function() { expect(model.get('_nums.sum')).to.equal(5); }); }); - describe('start (array inputs) and stop with getter', function() { - it('sets the output immediately on start', function() { + describe('stopAll', function() { + it('can call without start', function() { var model = new Model(); - model.fn('sum', function(a, b) { - return a + b; - }); - model.set('_nums.a', 2); - model.set('_nums.b', 4); - var value = model.start('_nums.sum', ['_nums.a', '_nums.b'], 'sum'); - expect(value).to.equal(6); - expect(model.get('_nums.sum')).to.equal(6); + model.stopAll('_nums.sum'); }); - it('sets the output when an input changes', function() { + it('stops updating functions at matching paths', function() { var model = new Model(); model.fn('sum', function(a, b) { return a + b; }); model.set('_nums.a', 2); model.set('_nums.b', 4); - model.start('_nums.sum', ['_nums.a', '_nums.b'], 'sum'); - expect(model.get('_nums.sum')).to.equal(6); - model.set('_nums.a', 5); - expect(model.get('_nums.sum')).to.equal(9); + model.set('_nums.c', 3); + model.set('_nums.d', 7); + model.start('_continue.x', '_nums.a', '_nums.b', 'sum'); + model.start('_continue.y', '_nums.c', '_nums.d', 'sum'); + model.start('_halt.x', '_nums.a', '_nums.b', 'sum'); + model.start('_halt.y', '_nums.c', '_nums.d', 'sum'); + model.set('_nums.a', 1); + model.set('_nums.c', 10); + expect(model.get('_continue')).eql({x: 5, y: 17}); + expect(model.get('_halt')).eql({x: 5, y: 17}); + model.stopAll('_halt'); + model.set('_nums.a', 0); + model.set('_nums.c', 0); + expect(model.get('_continue')).eql({x: 4, y: 7}); + expect(model.get('_halt')).eql({x: 5, y: 17}); }); - it('sets the output when a parent of the input changes', function() { + }); + describe('start with array inputs', function() { + it('array inputs and function name', function() { var model = new Model(); model.fn('sum', function(a, b) { return a + b; }); - model.set('_nums.in', { - a: 2, - b: 4 - }); - model.start('_nums.sum', ['_nums.in.a', '_nums.in.b'], 'sum'); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + var value = model.start('_nums.sum', ['_nums.a', '_nums.b'], 'sum'); + expect(value).to.equal(6); expect(model.get('_nums.sum')).to.equal(6); - model.set('_nums.in', { - a: 5, - b: 7 - }); - expect(model.get('_nums.sum')).to.equal(12); }); - it('does not set the output when a sibling of the input changes', function() { + it('array inputs and function argument', function() { var model = new Model(); - var count = 0; - model.fn('sum', function(a, b) { - count++; + model.set('_nums.a', 2); + model.set('_nums.b', 4); + var value = model.start('_nums.sum', ['_nums.a', '_nums.b'], function(a, b) { return a + b; }); - model.set('_nums.in', { - a: 2, - b: 4 - }); - model.start('_nums.sum', ['_nums.in.a', '_nums.in.b'], 'sum'); + expect(value).to.equal(6); expect(model.get('_nums.sum')).to.equal(6); - expect(count).to.equal(1); - model.set('_nums.in.a', 3); - expect(model.get('_nums.sum')).to.equal(7); - expect(count).to.equal(2); - model.set('_nums.in.c', -1); - expect(model.get('_nums.sum')).to.equal(7); - expect(count).to.equal(2); - }); - it('can call stop without start', function() { - var model = new Model(); - model.stop('_nums.sum'); - }); - it('stops updating after calling stop', function() { - var model = new Model(); - model.fn('sum', function(a, b) { - return a + b; - }); - model.set('_nums.a', 2); - model.set('_nums.b', 4); - model.start('_nums.sum', ['_nums.a', '_nums.b'], 'sum'); - model.set('_nums.a', 1); - expect(model.get('_nums.sum')).to.equal(5); - model.stop('_nums.sum'); - model.set('_nums.a', 3); - expect(model.get('_nums.sum')).to.equal(5); }); }); describe('start with async option', function() { From 0cb0bf2a2114f2c41432163b7824d791cfa4f34b Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 6 Aug 2019 17:05:16 -0700 Subject: [PATCH 204/479] implement CollectionMap & CollectionCounter for faster cleanup by tracking size --- lib/Model/CollectionCounter.js | 68 +++++++++++++------- lib/Model/CollectionMap.js | 46 ++++++++++++++ lib/Model/collections.js | 39 ++++++------ lib/Model/contexts.js | 4 +- test/Model/CollectionCounter.js | 106 ++++++++++++++++++++++++-------- 5 files changed, 191 insertions(+), 72 deletions(-) create mode 100644 lib/Model/CollectionMap.js diff --git a/lib/Model/CollectionCounter.js b/lib/Model/CollectionCounter.js index 6b413c7e8..7f87d4e77 100644 --- a/lib/Model/CollectionCounter.js +++ b/lib/Model/CollectionCounter.js @@ -4,40 +4,64 @@ function CollectionCounter() { this.reset(); } CollectionCounter.prototype.reset = function() { + // A map of CounterMaps this.collections = {}; + // The number of id keys in the collections map + this.size = 0; }; CollectionCounter.prototype.get = function(collectionName, id) { var collection = this.collections[collectionName]; - return collection && collection[id]; + return (collection && collection.counts[id]) || 0; }; CollectionCounter.prototype.increment = function(collectionName, id) { - var collection = this.collections[collectionName] || - (this.collections[collectionName] = {}); - var count = (collection[id] || 0) + 1; - collection[id] = count; - return count; + var collection = this.collections[collectionName]; + if (!collection) { + collection = this.collections[collectionName] = new CounterMap(); + this.size++; + } + return collection.increment(id); }; CollectionCounter.prototype.decrement = function(collectionName, id) { var collection = this.collections[collectionName]; - var count = collection && collection[id]; - if (count == null) return; - if (count > 1) { - count--; - collection[id] = count; - return count; + if (!collection) return 0; + var count = collection.decrement(id); + if (collection.size < 1) { + delete this.collections[collectionName]; + this.size--; } - delete collection[id]; - // Check if the collection still has any keys - // eslint-disable-next-line no-unused-vars - for (var key in collection) return 0; - delete this.collections[collectionName]; - return 0; + return count; }; CollectionCounter.prototype.toJSON = function() { - // Check to see if we have any keys - // eslint-disable-next-line no-unused-vars - for (var key in this.collections) { - return this.collections; + // Serialize to the contained count data if any + if (this.size > 0) { + var out = {}; + for (var collectionName in this.collections) { + out[collectionName] = this.collections[collectionName].counts; + } + return out; } return; }; + +function CounterMap() { + this.counts = {}; + this.size = 0; +} +CounterMap.prototype.increment = function(key) { + var count = this.counts[key] || 0; + if (count === 0) { + this.size++; + } + return this.counts[key] = count + 1; +}; +CounterMap.prototype.decrement = function(key) { + var count = this.counts[key] || 0; + if (count > 1) { + return this.counts[key] = count - 1; + } + if (count === 1) { + delete this.counts[key]; + this.size--; + } + return 0; +}; diff --git a/lib/Model/CollectionMap.js b/lib/Model/CollectionMap.js new file mode 100644 index 000000000..70f214aa4 --- /dev/null +++ b/lib/Model/CollectionMap.js @@ -0,0 +1,46 @@ +module.exports = CollectionMap; + +function CollectionMap() { + // A map of collection names to FastMaps + this.collections = {}; +} +CollectionMap.prototype.getCollection = function(collectionName) { + var collection = this.collections[collectionName]; + return (collection && collection.values); +}; +CollectionMap.prototype.get = function(collectionName, id) { + var collection = this.collections[collectionName]; + return (collection && collection.values[id]); +}; +CollectionMap.prototype.set = function(collectionName, id, value) { + var collection = this.collections[collectionName]; + if (!collection) { + collection = this.collections[collectionName] = new FastMap(); + } + collection.set(id, value); +}; +CollectionMap.prototype.del = function(collectionName, id) { + var collection = this.collections[collectionName]; + if (collection) { + collection.del(id); + if (collection.size > 0) return; + delete this.collections[collectionName]; + } +}; + +function FastMap() { + this.values = {}; + this.size = 0; +} +FastMap.prototype.set = function(key, value) { + if (!(key in this.values)) { + this.size++; + } + this.values[key] = value; +}; +FastMap.prototype.del = function(key) { + if (key in this.values) { + this.size--; + } + delete this.values[key]; +}; diff --git a/lib/Model/collections.js b/lib/Model/collections.js index 4990fbdfa..910fcc423 100644 --- a/lib/Model/collections.js +++ b/lib/Model/collections.js @@ -66,7 +66,7 @@ Model.prototype._getDocConstructor = function() { */ Model.prototype.getOrCreateDoc = function(collectionName, id, data) { var collection = this.getOrCreateCollection(collectionName); - return collection.docs[id] || collection.add(id, data); + return collection.getOrCreateDoc(id, data); }; /** @@ -101,6 +101,7 @@ function Collection(model, name, Doc) { this.model = model; this.name = name; this.Doc = Doc; + this.size = 0; this.docs = new DocMap(); this.data = model.data[name] = new CollectionData(); } @@ -120,31 +121,27 @@ Collection.prototype.destroy = function() { delete this.model.collections[this.name]; delete this.model.data[this.name]; }; +Collection.prototype.getOrCreateDoc = function(id, data) { + var doc = this.docs[id]; + if (doc) return doc; + this.size++; + return this.add(id, data); +}; /** * Removes the document with `id` from `this` Collection. If there are no more * documents in the Collection after the given document is removed, then this - * also destroys the Collection. + * destroys the Collection. + * * @param {String} id */ Collection.prototype.remove = function(id) { - delete this.docs[id]; - delete this.data[id]; - if (noKeys(this.docs)) this.destroy(); -}; - -/** - * Returns an object that maps doc ids to fully resolved documents. - * @return {Object} - */ -Collection.prototype.get = function() { - return this.data; -}; - -function noKeys(object) { - // eslint-disable-next-line no-unused-vars - for (var key in object) { - return false; + if (!this.docs[id]) return; + this.size--; + if (this.size > 0) { + delete this.docs[id]; + delete this.data[id]; + } else { + this.destroy(); } - return true; -} +}; diff --git a/lib/Model/contexts.js b/lib/Model/contexts.js index a63f1c859..6fa44e328 100644 --- a/lib/Model/contexts.js +++ b/lib/Model/contexts.js @@ -117,14 +117,14 @@ Context.prototype.unload = function() { for (var collectionName in this.fetchedDocs.collections) { var collection = this.fetchedDocs.collections[collectionName]; for (var id in collection) { - var count = collection[id]; + var count = collection.counts[id]; while (count--) model.unfetchDoc(collectionName, id); } } for (var collectionName in this.subscribedDocs.collections) { var collection = this.subscribedDocs.collections[collectionName]; for (var id in collection) { - var count = collection[id]; + var count = collection.counts[id]; while (count--) model.unsubscribeDoc(collectionName, id); } } diff --git a/test/Model/CollectionCounter.js b/test/Model/CollectionCounter.js index 21b2ebacf..783221ed8 100644 --- a/test/Model/CollectionCounter.js +++ b/test/Model/CollectionCounter.js @@ -2,35 +2,87 @@ var expect = require('../util').expect; var CollectionCounter = require('../../lib/Model/CollectionCounter'); describe('CollectionCounter', function() { - it('increment', function() { - var counter = new CollectionCounter(); - expect(counter.get('colors', 'green')).to.be(undefined); - expect(counter.increment('colors', 'green')).to.equal(1); - expect(counter.increment('colors', 'green')).to.equal(2); - expect(counter.get('colors', 'green')).to.equal(2); + describe('increment', function() { + it('increments count for a document', function() { + var counter = new CollectionCounter(); + expect(counter.get('colors', 'green')).to.be(0); + expect(counter.increment('colors', 'green')).to.equal(1); + expect(counter.increment('colors', 'green')).to.equal(2); + expect(counter.get('colors', 'green')).to.equal(2); + }); }); - - it('toJSON', function() { - var counter = new CollectionCounter(); - expect(counter.toJSON()).to.be(undefined); - expect(counter.increment('colors', 'green')).to.equal(1); - expect(counter.increment('colors', 'green')).to.equal(2); - expect(counter.toJSON()).to.eql({ - colors: { - green: 2 - } + describe('decrement', function() { + it('has no effect on empty collection', function() { + var counter = new CollectionCounter(); + expect(counter.decrement('colors', 'green')).to.equal(0); + }); + it('has no effect on empty doc in existing collection', function() { + var counter = new CollectionCounter(); + expect(counter.increment('colors', 'red')); + expect(counter.decrement('colors', 'green')).to.equal(0); + }); + it('decrements count for a document', function() { + var counter = new CollectionCounter(); + expect(counter.increment('colors', 'green')); + expect(counter.increment('colors', 'green')); + expect(counter.decrement('colors', 'green')).to.equal(1); + expect(counter.decrement('colors', 'green')).to.equal(0); + expect(counter.get('colors', 'green')).to.be(0); + }); + it('does not affect peer document', function() { + var counter = new CollectionCounter(); + expect(counter.increment('colors', 'green')); + expect(counter.increment('colors', 'red')); + expect(counter.decrement('colors', 'green')); + expect(counter.get('colors', 'green')).to.be(0); + expect(counter.get('colors', 'red')).to.be(1); + }); + it('does not affect peer collection', function() { + var counter = new CollectionCounter(); + expect(counter.increment('colors', 'green')); + expect(counter.increment('textures', 'smooth')); + expect(counter.decrement('colors', 'green')); + expect(counter.get('colors', 'green')).to.be(0); + expect(counter.get('textures', 'smooth')).to.be(1); }); }); - - it('decrement', function() { - var counter = new CollectionCounter(); - expect(counter.increment('colors', 'green')); - expect(counter.increment('colors', 'green')); - - expect(counter.decrement('colors', 'green')).to.equal(1); - expect(counter.decrement('colors', 'green')).to.equal(0); - - expect(counter.get('colors', 'green')).to.be(undefined); - expect(counter.toJSON()).to.be(undefined); + describe('toJSON', function() { + it('returns undefined if there are no counts', function() { + var counter = new CollectionCounter(); + expect(counter.toJSON()).to.be(undefined); + }); + it('returns a nested map representing counts', function() { + var counter = new CollectionCounter(); + counter.increment('colors', 'green'); + counter.increment('colors', 'green'); + counter.increment('colors', 'red'); + counter.increment('textures', 'smooth'); + expect(counter.toJSON()).to.eql({ + colors: { + green: 2, + red: 1 + }, + textures: { + smooth: 1 + } + }); + }); + it('incrementing then decrementing returns nothing', function() { + var counter = new CollectionCounter(); + counter.increment('colors', 'green'); + counter.decrement('colors', 'green'); + expect(counter.toJSON()).to.be(undefined); + }); + it('decrementing id from collection with other keys removes key', function() { + var counter = new CollectionCounter(); + counter.increment('colors', 'green'); + counter.increment('colors', 'red'); + counter.decrement('colors', 'green'); + expect(counter.toJSON()).to.eql({ + colors: { + red: 1 + } + }); + }); }); }); From edd48bee52837a52dca1d324dd4dbe1eb433d648 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 6 Aug 2019 17:22:48 -0700 Subject: [PATCH 205/479] use collectionMap to track queries --- lib/Model/Query.js | 19 ++++++------------- lib/Model/subscriptions.js | 2 +- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 600f5bce8..7b7774ab6 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -1,5 +1,6 @@ var util = require('../util'); var Model = require('./Model'); +var CollectionMap = require('./CollectionMap'); var defaultType = require('sharedb/lib/client').types.defaultType; module.exports = Query; @@ -114,26 +115,18 @@ Model.prototype._initQueries = function(items) { }; function Queries() { - // Map is a flattened map of queries by hash. Currently used in contexts + // Flattened map of queries by hash. Currently used in contexts this.map = {}; - // Collections is a nested map of queries by collection then hash - this.collections = {}; + // Nested map of queries by collection then hash + this.collectionMap = new CollectionMap(); } Queries.prototype.add = function(query) { this.map[query.hash] = query; - var collection = this.collections[query.collectionName] || - (this.collections[query.collectionName] = {}); - collection[query.hash] = query; + this.collectionMap.set(query.collectionName, query.hash, query); }; Queries.prototype.remove = function(query) { delete this.map[query.hash]; - var collection = this.collections[query.collectionName]; - if (!collection) return; - delete collection[query.hash]; - // Check if the collection still has any keys - // eslint-disable-next-line no-unused-vars - for (var key in collection) return; - delete this.collections[query.collectionName]; + this.collectionMap.del(query.collectionName, query.hash); }; Queries.prototype.get = function(collectionName, expression, options) { var hash = queryHash(collectionName, expression, options); diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index f51440df3..2a005a6ec 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -194,7 +194,7 @@ Model.prototype._maybeUnloadDoc = function(collectionName, id) { Model.prototype._hasDocReferences = function(collectionName, id) { // Check if any fetched or subscribed queries currently have the // id in their results - var queries = this.root._queries.collections[collectionName]; + var queries = this.root._queries.collectionMap.getCollection(collectionName); if (queries) { for (var hash in queries) { var query = queries[hash]; From 61c4e37f945dbab20bf5c606f2e4bd14c7759e80 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 30 Aug 2019 17:37:18 -0700 Subject: [PATCH 206/479] remove EventMapTree#getAllListeners --- lib/Model/EventMapTree.js | 23 ++++--------------- lib/Model/fn.js | 22 ++++++++---------- lib/Model/ref.js | 26 ++++++++++----------- lib/Model/refList.js | 15 ++++++++++-- test/Model/EventMapTree.js | 47 -------------------------------------- 5 files changed, 41 insertions(+), 92 deletions(-) diff --git a/lib/Model/EventMapTree.js b/lib/Model/EventMapTree.js index 930ef810b..bd5507794 100644 --- a/lib/Model/EventMapTree.js +++ b/lib/Model/EventMapTree.js @@ -98,15 +98,18 @@ EventMapTree.prototype.deleteListener = function(segments) { }; /** - * Remove all listeners and descendent listeners for a path location + * Remove all listeners and descendent listeners for a path location. Return the + * node for the path location if any * - * @param {string[]} segments + * @param {string[]} segments + * @return {EventMapTree} */ EventMapTree.prototype.deleteAllListeners = function(segments) { var node = this._getChild(segments); if (node) { node._destroy(); } + return node; }; /** @@ -138,22 +141,6 @@ EventMapTree.prototype.getAffectedListeners = function(segments) { return listeners; }; -/** - * Return an array with each of the listeners to `segments` and descendent nodes - * - * @param {string[]} segments - * @return {Array} listeners - */ -EventMapTree.prototype.getAllListeners = function(segments) { - var listeners = []; - var node = this._getChild(segments); - if (node) { - pushListener(listeners, node); - pushDescendantListeners(listeners, node); - } - return listeners; -}; - /** * Push node's direct listener onto the passed in array if not null * diff --git a/lib/Model/fn.js b/lib/Model/fn.js index cadf44836..35f3e4e27 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -115,8 +115,14 @@ Model.prototype._stopAll = function(segments) { function Fns(model) { this.model = model; this.nameMap = model._namedFns; - this.inputListeners = new EventListenerTree(); this.fromMap = new EventMapTree(); + var inputListeners = this.inputListeners = new EventListenerTree(); + this._removeInputListeners = function(fn) { + for (var i = 0; i < fn.inputsSegments.length; i++) { + var inputSegements = fn.inputsSegments[i]; + inputListeners.removeListener(inputSegements, fn); + } + }; } Fns.prototype.get = function(name, inputPaths, fns, options) { @@ -147,17 +153,9 @@ Fns.prototype.stop = function(segments) { }; Fns.prototype.stopAll = function(segments) { - var listeners = this.fromMap.getAllListeners(segments); - for (var i = 0; i < listeners.length; i++) { - this._removeInputListeners(listeners[i]); - } - this.fromMap.deleteAllListeners(segments); -}; - -Fns.prototype._removeInputListeners = function(fn) { - for (var i = 0; i < fn.inputsSegments.length; i++) { - var inputSegements = fn.inputsSegments[i]; - this.inputListeners.removeListener(inputSegements, fn); + var node = this.fromMap.deleteAllListeners(segments); + if (node) { + node.forEach(this._removeInputListeners); } }; diff --git a/lib/Model/ref.js b/lib/Model/ref.js index 42da73d03..5fd0d3999 100644 --- a/lib/Model/ref.js +++ b/lib/Model/ref.js @@ -209,18 +209,8 @@ Model.prototype.removeAllRefs = function(subpath) { this._removeAllRefs(segments); }; Model.prototype._removeAllRefs = function(segments) { - var refs = this.root._refs.fromMap.getAllListeners(segments); - for (var i = 0; i < refs.length; i++) { - var ref = refs[i]; - this.root._refs.remove(ref.fromSegments); - this._del(ref.fromSegments); - } - var refLists = this.root._refLists.fromMap.getAllListeners(segments); - for (var i = 0; i < refLists.length; i++) { - var refList = refLists[i]; - this.root._refLists.remove(refList.fromSegments); - this._del(refList.fromSegments); - } + this.root._refs.removeAll(segments); + this.root._refLists.removeAll(segments); }; Model.prototype.dereference = function(subpath) { @@ -280,7 +270,10 @@ function Ref(model, from, to, options) { function Refs() { this.fromMap = new EventMapTree(); - this.toListeners = new EventListenerTree(); + var toListeners = this.toListeners = new EventListenerTree(); + this._removeInputListeners = function(ref) { + toListeners.removeListener(ref.toSegments, ref); + }; } Refs.prototype.add = function(ref) { @@ -294,6 +287,13 @@ Refs.prototype.remove = function(segments) { this.toListeners.removeListener(ref.toSegments, ref); }; +Refs.prototype.removeAll = function(segments) { + var node = this.fromMap.deleteAllListeners(segments); + if (node) { + node.forEach(this._removeInputListeners); + } +}; + Refs.prototype.toJSON = function() { var out = []; this.fromMap.forEach(function(ref) { diff --git a/lib/Model/refList.js b/lib/Model/refList.js index a687b5b4a..1fabde347 100644 --- a/lib/Model/refList.js +++ b/lib/Model/refList.js @@ -467,8 +467,12 @@ RefList.prototype.idByIndex = function(index) { function RefLists() { this.fromMap = new EventMapTree(); - this.toListeners = new EventListenerTree(); - this.idsListeners = new EventListenerTree(); + var toListeners = this.toListeners = new EventListenerTree(); + var idsListeners = this.idsListeners = new EventListenerTree(); + this._removeInputListeners = function(refList) { + toListeners.removeListener(refList.toSegments, refList); + idsListeners.removeListener(refList.idsSegments, refList); + }; } RefLists.prototype.add = function(refList) { @@ -484,6 +488,13 @@ RefLists.prototype.remove = function(fromSegments) { this.idsListeners.removeListener(refList.idsSegments, refList); }; +RefLists.prototype.removeAll = function(segments) { + var node = this.fromMap.deleteAllListeners(segments); + if (node) { + node.forEach(this._removeInputListeners); + } +}; + RefLists.prototype.toJSON = function() { var out = []; this.fromMap.forEach(function(refList) { diff --git a/test/Model/EventMapTree.js b/test/Model/EventMapTree.js index 262a65bf7..92dea2041 100644 --- a/test/Model/EventMapTree.js +++ b/test/Model/EventMapTree.js @@ -190,53 +190,6 @@ describe('EventMapTree', function() { expect(affected).eql([listener1, listener4, listener5]); }); }); - describe('getAllListeners', function() { - it('returns empty array without listeners', function() { - var tree = new EventMapTree(); - var affected = tree.getAllListeners([]); - expect(affected).eql([]); - }); - it('returns empty array on path without node', function() { - var tree = new EventMapTree(); - var affected = tree.getAllListeners(['colors', 'green']); - expect(affected).eql([]); - }); - it('returns direct listener', function() { - var tree = new EventMapTree(); - var listener1 = 'listener1'; - tree.setListener([], listener1); - var affected = tree.getAllListeners([]); - expect(affected).eql([listener1]); - }); - it('returns all descendant listeners', function() { - var tree = new EventMapTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - var listener4 = 'listener4'; - tree.setListener(['colors', 'green'], listener1); - tree.setListener(['colors', 'red'], listener2); - tree.setListener([], listener3); - tree.setListener(['colors'], listener4); - var affected = tree.getAllListeners([]); - expect(affected).eql([listener3, listener4, listener1, listener2]); - }); - it('does not return parent or peer listeners', function() { - var tree = new EventMapTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - var listener4 = 'listener4'; - var listener5 = 'listener5'; - tree.setListener([], listener1); - tree.setListener(['colors'], listener2); - tree.setListener(['colors', 'green'], listener3); - tree.setListener(['textures'], listener4); - tree.setListener(['textures', 'smooth'], listener5); - var affected = tree.getAllListeners(['textures']); - expect(affected).eql([listener4, listener5]); - }); - }); describe('forEach', function() { it('can be called on empty tree', function() { var tree = new EventMapTree(); From 887bc9f3b60548f8c2ecfa88fc896fae5565c087 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 30 Aug 2019 17:40:32 -0700 Subject: [PATCH 207/479] cleanup test file --- test/Model/EventListenerTree.js | 197 ++++++++++++-------------------- 1 file changed, 74 insertions(+), 123 deletions(-) diff --git a/test/Model/EventListenerTree.js b/test/Model/EventListenerTree.js index 6f6d70acc..ff1371ba4 100644 --- a/test/Model/EventListenerTree.js +++ b/test/Model/EventListenerTree.js @@ -60,61 +60,49 @@ describe('EventListenerTree', function() { }); it('removes listener at subpath with remaining peers', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - tree.addListener(['colors', 'green'], listener1); - tree.addListener(['colors', 'red'], listener2); - tree.removeListener(['colors', 'green'], listener1); + tree.addListener(['colors', 'green'], 'listener1'); + tree.addListener(['colors', 'red'], 'listener2'); + tree.removeListener(['colors', 'green'], 'listener1'); expect(tree.getListeners(['colors', 'green'])).eql([]); - expect(tree.getListeners(['colors', 'red'])).eql([listener2]); + expect(tree.getListeners(['colors', 'red'])).eql(['listener2']); }); it('does not remove listener if not found with one listener', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - tree.addListener(['colors', 'green'], listener1); - expect(tree.getListeners(['colors', 'green'])).eql([listener1]); - tree.removeListener(['colors', 'green'], listener2); - expect(tree.getListeners(['colors', 'green'])).eql([listener1]); + tree.addListener(['colors', 'green'], 'listener1'); + expect(tree.getListeners(['colors', 'green'])).eql(['listener1']); + tree.removeListener(['colors', 'green'], 'listener2'); + expect(tree.getListeners(['colors', 'green'])).eql(['listener1']); }); it('does not remove listener if not found with multiple listeners', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - tree.addListener(['colors', 'green'], listener1); - tree.addListener(['colors', 'green'], listener2); - expect(tree.getListeners(['colors', 'green'])).eql([listener1, listener2]); - tree.removeListener(['colors', 'green'], listener3); - expect(tree.getListeners(['colors', 'green'])).eql([listener1, listener2]); + tree.addListener(['colors', 'green'], 'listener1'); + tree.addListener(['colors', 'green'], 'listener2'); + expect(tree.getListeners(['colors', 'green'])).eql(['listener1', 'listener2']); + tree.removeListener(['colors', 'green'], 'listener3'); + expect(tree.getListeners(['colors', 'green'])).eql(['listener1', 'listener2']); }); it('removes listener with remaining peers', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - tree.addListener([], listener1); - tree.addListener([], listener2); - tree.addListener([], listener3); - expect(tree.getListeners([])).eql([listener1, listener2, listener3]); - tree.removeListener([], listener2); - expect(tree.getListeners([])).eql([listener1, listener3]); - tree.removeListener([], listener3); - expect(tree.getListeners([])).eql([listener1]); - tree.removeListener([], listener1); + tree.addListener([], 'listener1'); + tree.addListener([], 'listener2'); + tree.addListener([], 'listener3'); + expect(tree.getListeners([])).eql(['listener1', 'listener2', 'listener3']); + tree.removeListener([], 'listener2'); + expect(tree.getListeners([])).eql(['listener1', 'listener3']); + tree.removeListener([], 'listener3'); + expect(tree.getListeners([])).eql(['listener1']); + tree.removeListener([], 'listener1'); expect(tree.getListeners([])).eql([]); }); it('removes listener with remaining peer children', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - tree.addListener(['colors'], listener1); - tree.addListener(['colors', 'green'], listener2); - expect(tree.getListeners(['colors'])).eql([listener1]); - expect(tree.getListeners(['colors', 'green'])).eql([listener2]); - tree.removeListener(['colors'], listener1); + tree.addListener(['colors'], 'listener1'); + tree.addListener(['colors', 'green'], 'listener2'); + expect(tree.getListeners(['colors'])).eql(['listener1']); + expect(tree.getListeners(['colors', 'green'])).eql(['listener2']); + tree.removeListener(['colors'], 'listener1'); expect(tree.getListeners(['colors'])).eql([]); - expect(tree.getListeners(['colors', 'green'])).eql([listener2]); + expect(tree.getListeners(['colors', 'green'])).eql(['listener2']); }); }); describe('removeAllListeners', function() { @@ -128,26 +116,20 @@ describe('EventListenerTree', function() { }); it('removes all listeners and children when called on root', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - tree.addListener([], listener1); - tree.addListener(['colors'], listener2); - tree.addListener(['colors', 'green'], listener3); + tree.addListener([], 'listener1'); + tree.addListener(['colors'], 'listener2'); + tree.addListener(['colors', 'green'], 'listener3'); tree.removeAllListeners([]); expect(tree.getListeners([])).eql([]); expect(tree.children).eql(null); }); it('removes listeners and descendent children on path', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - tree.addListener([], listener1); - tree.addListener(['colors'], listener2); - tree.addListener(['colors', 'green'], listener3); + tree.addListener([], 'listener1'); + tree.addListener(['colors'], 'listener2'); + tree.addListener(['colors', 'green'], 'listener3'); tree.removeAllListeners(['colors']); - expect(tree.getListeners([])).eql([listener1]); + expect(tree.getListeners([])).eql(['listener1']); expect(tree.children).eql(null); }); }); @@ -164,67 +146,48 @@ describe('EventListenerTree', function() { }); it('returns all direct listeners', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - tree.addListener([], listener1); - tree.addListener([], listener2); + tree.addListener([], 'listener1'); + tree.addListener([], 'listener2'); var affected = tree.getAffectedListeners([]); - expect(affected).eql([listener1, listener2]); + expect(affected).eql(['listener1', 'listener2']); }); it('removeListener stops listener from being returned', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - tree.addListener([], listener1); - tree.addListener([], listener2); - tree.removeListener([], listener1); + tree.addListener([], 'listener1'); + tree.addListener([], 'listener2'); + tree.removeListener([], 'listener1'); var affected = tree.getAffectedListeners([]); - expect(affected).eql([listener2]); + expect(affected).eql(['listener2']); }); it('returns all descendant listeners', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - var listener4 = 'listener4'; - var listener5 = 'listener5'; - tree.addListener(['colors', 'green'], listener1); - tree.addListener(['colors', 'red'], listener2); - tree.addListener(['colors', 'red'], listener3); - tree.addListener([], listener4); - tree.addListener(['colors'], listener5); + tree.addListener(['colors', 'green'], 'listener1'); + tree.addListener(['colors', 'red'], 'listener2'); + tree.addListener(['colors', 'red'], 'listener3'); + tree.addListener([], 'listener4'); + tree.addListener(['colors'], 'listener5'); var affected = tree.getAffectedListeners([]); - expect(affected).eql([listener4, listener5, listener1, listener2, listener3]); + expect(affected).eql(['listener4', 'listener5', 'listener1', 'listener2', 'listener3']); }); it('returns all parent listeners in depth order', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - var listener4 = 'listener4'; - var listener5 = 'listener5'; - tree.addListener(['colors', 'green'], listener1); - tree.addListener(['colors', 'red'], listener2); - tree.addListener(['colors', 'red'], listener3); - tree.addListener([], listener4); - tree.addListener(['colors'], listener5); + tree.addListener(['colors', 'green'], 'listener1'); + tree.addListener(['colors', 'red'], 'listener2'); + tree.addListener(['colors', 'red'], 'listener3'); + tree.addListener([], 'listener4'); + tree.addListener(['colors'], 'listener5'); var affected = tree.getAffectedListeners(['colors', 'green']); - expect(affected).eql([listener4, listener5, listener1]); + expect(affected).eql(['listener4', 'listener5', 'listener1']); }); it('does not return peers or peer children', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - var listener4 = 'listener4'; - var listener5 = 'listener5'; - tree.addListener([], listener1); - tree.addListener(['colors'], listener2); - tree.addListener(['colors', 'green'], listener3); - tree.addListener(['textures'], listener4); - tree.addListener(['textures', 'smooth'], listener5); + tree.addListener([], 'listener1'); + tree.addListener(['colors'], 'listener2'); + tree.addListener(['colors', 'green'], 'listener3'); + tree.addListener(['textures'], 'listener4'); + tree.addListener(['textures', 'smooth'], 'listener5'); var affected = tree.getAffectedListeners(['textures']); - expect(affected).eql([listener1, listener4, listener5]); + expect(affected).eql(['listener1', 'listener4', 'listener5']); }); }); describe('getDescendantListeners', function() { @@ -240,42 +203,30 @@ describe('EventListenerTree', function() { }); it('does not return direct listeners', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - tree.addListener([], listener1); - tree.addListener([], listener2); + tree.addListener([], 'listener1'); + tree.addListener([], 'listener2'); var affected = tree.getDescendantListeners([]); expect(affected).eql([]); }); it('returns all descendant listeners', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - var listener4 = 'listener4'; - var listener5 = 'listener5'; - tree.addListener(['colors', 'green'], listener1); - tree.addListener(['colors', 'red'], listener2); - tree.addListener(['colors', 'red'], listener3); - tree.addListener([], listener4); - tree.addListener(['colors'], listener5); + tree.addListener(['colors', 'green'], 'listener1'); + tree.addListener(['colors', 'red'], 'listener2'); + tree.addListener(['colors', 'red'], 'listener3'); + tree.addListener([], 'listener4'); + tree.addListener(['colors'], 'listener5'); var affected = tree.getDescendantListeners([]); - expect(affected).eql([listener5, listener1, listener2, listener3]); + expect(affected).eql(['listener5', 'listener1', 'listener2', 'listener3']); }); it('does not return parent or peer listeners', function() { var tree = new EventListenerTree(); - var listener1 = 'listener1'; - var listener2 = 'listener2'; - var listener3 = 'listener3'; - var listener4 = 'listener4'; - var listener5 = 'listener5'; - tree.addListener([], listener1); - tree.addListener(['colors'], listener2); - tree.addListener(['colors', 'green'], listener3); - tree.addListener(['textures'], listener4); - tree.addListener(['textures', 'smooth'], listener5); + tree.addListener([], 'listener1'); + tree.addListener(['colors'], 'listener2'); + tree.addListener(['colors', 'green'], 'listener3'); + tree.addListener(['textures'], 'listener4'); + tree.addListener(['textures', 'smooth'], 'listener5'); var affected = tree.getDescendantListeners(['textures']); - expect(affected).eql([listener5]); + expect(affected).eql(['listener5']); }); }); }); From a61a07c9592c0e54406c5f6d08d68de5a042b334 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 30 Aug 2019 17:55:41 -0700 Subject: [PATCH 208/479] use FastMap to avoid iterating over keys in EventListenerTree & EventMapTree --- lib/Model/CollectionMap.js | 19 +---- lib/Model/EventListenerTree.js | 140 +++++++++++++++++---------------- lib/Model/EventMapTree.js | 118 +++++++++++++-------------- lib/Model/FastMap.js | 18 +++++ lib/Model/ref.js | 6 +- 5 files changed, 148 insertions(+), 153 deletions(-) create mode 100644 lib/Model/FastMap.js diff --git a/lib/Model/CollectionMap.js b/lib/Model/CollectionMap.js index 70f214aa4..507f021f1 100644 --- a/lib/Model/CollectionMap.js +++ b/lib/Model/CollectionMap.js @@ -1,3 +1,5 @@ +var FastMap = require('./FastMap'); + module.exports = CollectionMap; function CollectionMap() { @@ -27,20 +29,3 @@ CollectionMap.prototype.del = function(collectionName, id) { delete this.collections[collectionName]; } }; - -function FastMap() { - this.values = {}; - this.size = 0; -} -FastMap.prototype.set = function(key, value) { - if (!(key in this.values)) { - this.size++; - } - this.values[key] = value; -}; -FastMap.prototype.del = function(key) { - if (key in this.values) { - this.size--; - } - delete this.values[key]; -}; diff --git a/lib/Model/EventListenerTree.js b/lib/Model/EventListenerTree.js index 262a0c718..29fd93c57 100644 --- a/lib/Model/EventListenerTree.js +++ b/lib/Model/EventListenerTree.js @@ -1,3 +1,5 @@ +var FastMap = require('./FastMap'); + module.exports = EventListenerTree; /** @@ -19,15 +21,26 @@ function EventListenerTree(parent, segment) { * collected. This is called internally when all listeners to a node * are removed */ -EventListenerTree.prototype._destroy = function() { +EventListenerTree.prototype.destroy = function() { // For all non-root nodes, remove the reference to the node - if (this.parent) { - removeChild(this.parent, this.segment); - // For the root node, reset any references to listeners or children - } else { - this.children = null; - this.listeners = null; + var parent = this.parent; + if (parent) { + // Remove reference to this node from its parent + var children = parent.children; + if (children) { + children.del(this.segment); + if (children.size > 0) return; + parent.children = null; + } + // Destroy parent if it no longer has any dependents + if (!parent.listeners) { + parent.destroy(); + } + return; } + // For the root node, reset any references to listeners or children + this.children = null; + this.listeners = null; }; /** @@ -38,9 +51,11 @@ EventListenerTree.prototype._destroy = function() { */ EventListenerTree.prototype._getChild = function(segments) { var node = this; - for (var i = 0; i < segments.length; i++) { + for (var i = 0, len = segments.length; i < len; i++) { + var children = node.children; + if (!children) return; var segment = segments[i]; - node = node.children && node.children[segment]; + node = children.values[segment]; if (!node) return; } return node; @@ -55,13 +70,19 @@ EventListenerTree.prototype._getChild = function(segments) { */ EventListenerTree.prototype._getOrCreateChild = function(segments) { var node = this; - for (var i = 0; i < segments.length; i++) { + for (var i = 0, len = segments.length; i < len; i++) { + var children = node.children; + if (!children) { + children = node.children = new FastMap(); + } var segment = segments[i]; - if (!node.children) { - node.children = {}; + var next = children.values[segment]; + if (next) { + node = next; + } else { + node = new EventListenerTree(node, segment); + children.set(segment, node); } - var node = node.children[segment] || - (node.children[segment] = new EventListenerTree(node, segment)); } return node; }; @@ -76,10 +97,11 @@ EventListenerTree.prototype._getOrCreateChild = function(segments) { */ EventListenerTree.prototype.addListener = function(segments, listener) { var node = this._getOrCreateChild(segments); - if (node.listeners) { - var i = node.listeners.indexOf(listener); + var listeners = node.listeners; + if (listeners) { + var i = listeners.indexOf(listener); if (i === -1) { - node.listeners.push(listener); + listeners.push(listener); } } else { node.listeners = [listener]; @@ -94,19 +116,31 @@ EventListenerTree.prototype.addListener = function(segments, listener) { */ EventListenerTree.prototype.removeListener = function(segments, listener) { var node = this._getChild(segments); - if (!node || !node.listeners) return; - if (node.listeners.length === 1) { - if (node.listeners[0] === listener) { - node.listeners = null; - if (!node.children) { - node._destroy(); + if (node) { + node.removeOwnListener(listener); + } +}; + +/** + * Remove a listener from the current node + * + * @param {*} listener + */ +EventListenerTree.prototype.removeOwnListener = function(listener) { + var listeners = this.listeners; + if (!listeners) return; + if (listeners.length === 1) { + if (listeners[0] === listener) { + this.listeners = null; + if (!this.children) { + this.destroy(); } } return; } - var i = node.listeners.indexOf(listener); + var i = listeners.indexOf(listener); if (i > -1) { - node.listeners.splice(i, 1); + listeners.splice(i, 1); } }; @@ -118,7 +152,7 @@ EventListenerTree.prototype.removeListener = function(segments, listener) { EventListenerTree.prototype.removeAllListeners = function(segments) { var node = this._getChild(segments); if (node) { - node._destroy(); + node.destroy(); } }; @@ -174,9 +208,10 @@ EventListenerTree.prototype.getDescendantListeners = function(segments) { * @param {EventListenerTree} node */ function pushListeners(listeners, node) { - if (!node.listeners) return; - for (var i = 0, len = node.listeners.length; i < len; i++) { - listeners.push(node.listeners[i]); + var nodeListeners = node.listeners; + if (!nodeListeners) return; + for (var i = 0, len = nodeListeners.length; i < len; i++) { + listeners.push(nodeListeners[i]); } } @@ -191,9 +226,11 @@ function pushListeners(listeners, node) { */ function pushAncestorListeners(listeners, segments, node) { pushListeners(listeners, node); - for (var i = 0; i < segments.length; i++) { + for (var i = 0, len = segments.length; i < len; i++) { + var children = node.children; + if (!children) return; var segment = segments[i]; - node = node.children && node.children[segment]; + node = children.values[segment]; if (!node) return; pushListeners(listeners, node); } @@ -209,45 +246,10 @@ function pushAncestorListeners(listeners, segments, node) { */ function pushDescendantListeners(listeners, node) { if (!node.children) return; - for (var key in node.children) { - var child = node.children[key]; + var values = node.children.values; + for (var key in values) { + var child = values[key]; pushListeners(listeners, child); pushDescendantListeners(listeners, child); } } - -/** - * Remove the child at the specified segment from a node. Also recursively - * remove parent nodes if there are no remaining dependencies - * - * @param {EventListenerTree} node - * @param {string} segment - */ -function removeChild(node, segment) { - // Remove reference this node from its parent - if (hasOtherKeys(node.children, segment)) { - delete node.children[segment]; - return; - } - node.children = null; - - // Destroy parent if it no longer has any dependents - if (!node.listeners) { - node._destroy(); - } -} - -/** - * Return whether the object has any other property key other than the - * provided value. - * - * @param {Object} object - * @param {string} ignore - * @return {Boolean} - */ -function hasOtherKeys(object, ignore) { - for (var key in object) { - if (key !== ignore) return true; - } - return false; -} diff --git a/lib/Model/EventMapTree.js b/lib/Model/EventMapTree.js index bd5507794..3ea09d070 100644 --- a/lib/Model/EventMapTree.js +++ b/lib/Model/EventMapTree.js @@ -1,3 +1,5 @@ +var FastMap = require('./FastMap'); + module.exports = EventMapTree; /** @@ -19,15 +21,26 @@ function EventMapTree(parent, segment) { * collected. This is called internally when all listener to a node * are removed */ -EventMapTree.prototype._destroy = function() { +EventMapTree.prototype.destroy = function() { // For all non-root nodes, remove the reference to the node - if (this.parent) { - removeChild(this.parent, this.segment); - // For the root node, reset any references to listener or children - } else { - this.children = null; - this.listener = null; + var parent = this.parent; + if (parent) { + // Remove reference to this node from its parent + var children = parent.children; + if (children) { + children.del(this.segment); + if (children.size > 0) return; + parent.children = null; + } + // Destroy parent if it no longer has any dependents + if (parent.listener == null) { + parent.destroy(); + } + return; } + // For the root node, reset any references to listener or children + this.children = null; + this.listener = null; }; /** @@ -38,9 +51,11 @@ EventMapTree.prototype._destroy = function() { */ EventMapTree.prototype._getChild = function(segments) { var node = this; - for (var i = 0; i < segments.length; i++) { + for (var i = 0, len = segments.length; i < len; i++) { + var children = node.children; + if (!children) return; var segment = segments[i]; - node = node.children && node.children[segment]; + node = children.values[segment]; if (!node) return; } return node; @@ -55,13 +70,19 @@ EventMapTree.prototype._getChild = function(segments) { */ EventMapTree.prototype._getOrCreateChild = function(segments) { var node = this; - for (var i = 0; i < segments.length; i++) { + for (var i = 0, len = segments.length; i < len; i++) { + var children = node.children; + if (!children) { + children = node.children = new FastMap(); + } var segment = segments[i]; - if (!node.children) { - node.children = {}; + var next = children.values[segment]; + if (next) { + node = next; + } else { + node = new EventMapTree(node, segment); + children.set(segment, node); } - var node = node.children[segment] || - (node.children[segment] = new EventMapTree(node, segment)); } return node; }; @@ -70,8 +91,9 @@ EventMapTree.prototype._getOrCreateChild = function(segments) { * Assign a listener to a path location. Listener may be any type of value. * Return the previous listener value if any * - * @param {string[]} segments - * @param {*} listener + * @param {string[]} segments + * @param {*} listener + * @return {*} previous */ EventMapTree.prototype.setListener = function(segments, listener) { var node = this._getOrCreateChild(segments); @@ -84,7 +106,7 @@ EventMapTree.prototype.setListener = function(segments, listener) { * Remove the listener at a path location and return it * * @param {string[]} segments - * @return {*} listener + * @return {*} previous */ EventMapTree.prototype.deleteListener = function(segments) { var node = this._getChild(segments); @@ -92,7 +114,7 @@ EventMapTree.prototype.deleteListener = function(segments) { var previous = node.listener; node.listener = null; if (!node.children) { - node._destroy(); + node.destroy(); } return previous; }; @@ -107,7 +129,7 @@ EventMapTree.prototype.deleteListener = function(segments) { EventMapTree.prototype.deleteAllListeners = function(segments) { var node = this._getChild(segments); if (node) { - node._destroy(); + node.destroy(); } return node; }; @@ -164,9 +186,11 @@ function pushListener(listeners, node) { */ function pushAncestorListeners(listeners, segments, node) { pushListener(listeners, node); - for (var i = 0; i < segments.length; i++) { + for (var i = 0, len = segments.length; i < len; i++) { + var children = node.children; + if (!children) return; var segment = segments[i]; - node = node.children && node.children[segment]; + node = children.values[segment]; if (!node) return; pushListener(listeners, node); } @@ -182,8 +206,9 @@ function pushAncestorListeners(listeners, segments, node) { */ function pushDescendantListeners(listeners, node) { if (!node.children) return; - for (var key in node.children) { - var child = node.children[key]; + var values = node.children.values; + for (var key in values) { + var child = values[key]; pushListener(listeners, child); pushDescendantListeners(listeners, child); } @@ -207,8 +232,9 @@ EventMapTree.prototype.forEach = function(callback) { * @param {Function} callback */ function forListener(node, callback) { - if (node.listener != null) { - callback(node.listener); + var listener = node.listener; + if (listener != null) { + callback(listener); } } @@ -221,46 +247,10 @@ function forListener(node, callback) { */ function forDescendantListeners(node, callback) { if (!node.children) return; - for (var key in node.children) { - var child = node.children[key]; + var values = node.children.values; + for (var key in values) { + var child = values[key]; forListener(child, callback); forDescendantListeners(child, callback); } } - -/** - * Remove the child at the specified segment from a node. Also recursively - * remove parent nodes if there are no remaining dependencies - * - * @param {EventMapTree} node - * @param {string} segment - */ -function removeChild(node, segment) { - // Remove reference this node from its parent - if (hasOtherKeys(node.children, segment)) { - delete node.children[segment]; - return; - } - node.children = null; - - // Destroy parent if it no longer has any dependents - if (node.listener == null) { - node._destroy(); - } -} - -/** - * Return whether the object has any other property key other than the - * provided value. - * - * @param {Object} object - * @param {string} ignore - * @return {Boolean} - */ -function hasOtherKeys(object, ignore) { - for (var key in object) { - if (key !== ignore) return true; - } - return false; -} - diff --git a/lib/Model/FastMap.js b/lib/Model/FastMap.js new file mode 100644 index 000000000..3c43c9278 --- /dev/null +++ b/lib/Model/FastMap.js @@ -0,0 +1,18 @@ +module.exports = FastMap; + +function FastMap() { + this.values = {}; + this.size = 0; +} +FastMap.prototype.set = function(key, value) { + if (!(key in this.values)) { + this.size++; + } + return this.values[key] = value; +}; +FastMap.prototype.del = function(key) { + if (key in this.values) { + this.size--; + } + delete this.values[key]; +}; diff --git a/lib/Model/ref.js b/lib/Model/ref.js index 5fd0d3999..07504a177 100644 --- a/lib/Model/ref.js +++ b/lib/Model/ref.js @@ -113,7 +113,7 @@ function addListener(model, type, fn) { var node = model._refs.toListeners; for (var i = 0; i < segments.length; i++) { var segment = segments[i]; - node = node.children && node.children[segment]; + node = node.children && node.children.values[segment]; if (!node) return; // If a ref is found pointing to a matching subpath, re-emit on the // place where the reference is coming from as if the mutation also @@ -228,7 +228,7 @@ Model.prototype._dereference = function(segments, forArrayMutator, ignore) { for (var i = 0, len = segments.length; i < len; i++) { var segment = segments[i]; - refsNode = refsNode && refsNode.children && refsNode.children[segment]; + refsNode = refsNode && refsNode.children && refsNode.children.values[segment]; var ref = refsNode && refsNode.listener; if (ref) { var remaining = segments.slice(i + 1); @@ -237,7 +237,7 @@ Model.prototype._dereference = function(segments, forArrayMutator, ignore) { break; } - refListsNode = refListsNode && refListsNode.children && refListsNode.children[segment]; + refListsNode = refListsNode && refListsNode.children && refListsNode.children.values[segment]; var refList = refListsNode && refListsNode.listener; if (refList && refList !== ignore) { var belowDescendant = i + 2 < len; From 920ce63d1f13da2fad05a8d9f3abafc4af9ae5bb Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 30 Aug 2019 17:57:00 -0700 Subject: [PATCH 209/479] test .destroy() for EventListenerTree & EventMapTree --- test/Model/EventListenerTree.js | 34 ++++++++++++++++++++++++++++ test/Model/EventMapTree.js | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/test/Model/EventListenerTree.js b/test/Model/EventListenerTree.js index ff1371ba4..43e9f9b51 100644 --- a/test/Model/EventListenerTree.js +++ b/test/Model/EventListenerTree.js @@ -33,6 +33,40 @@ describe('EventListenerTree', function() { expect(tree.getListeners(['colors'])).eql([]); expect(tree.getListeners(['colors', 'green'])).eql([listener]); }); + describe('destroy', function() { + it('can be called on empty root', function() { + var tree = new EventListenerTree(); + tree.destroy(); + expect(tree.children).eql(null); + }); + it('removes nodes up to root', function() { + var tree = new EventListenerTree(); + var node = tree.addListener(['colors', 'green'], 'listener1'); + node.destroy(); + expect(tree.children).eql(null); + }); + it('can be called on child node repeatedly', function() { + var tree = new EventListenerTree(); + var node = tree.addListener(['colors', 'green'], 'listener1'); + node.destroy(); + node.destroy(); + }); + it('does not remove parent nodes with existing listeners', function() { + var tree = new EventListenerTree(); + tree.addListener(['colors'], 'listener1'); + var node = tree.addListener(['colors', 'green'], 'listener2'); + node.destroy(); + node.destroy(); + expect(tree.getListeners(['colors'])).eql(['listener1']); + }); + it('does not remove parent nodes with other children', function() { + var tree = new EventListenerTree(); + tree.addListener(['colors', 'red'], 'listener1'); + var node = tree.addListener(['colors', 'green'], 'listener2'); + node.destroy(); + node.destroy(); + expect(tree.getListeners(['colors', 'red'])).eql(['listener1']); + }); }); describe('removeListener', function() { it('can be called before addListener', function() { diff --git a/test/Model/EventMapTree.js b/test/Model/EventMapTree.js index 92dea2041..bce33ce0e 100644 --- a/test/Model/EventMapTree.js +++ b/test/Model/EventMapTree.js @@ -38,6 +38,45 @@ describe('EventMapTree', function() { expect(tree.getListener(['colors', 'green'])).equal(listener); }); }); + describe('destroy', function() { + it('can be called on empty root', function() { + var tree = new EventMapTree(); + tree.destroy(); + expect(tree.children).eql(null); + }); + it('removes nodes up to root', function() { + var tree = new EventMapTree(); + tree.setListener(['colors', 'green'], 'listener1'); + var node = tree._getChild(['colors', 'green']); + node.destroy(); + expect(tree.children).eql(null); + }); + it('can be called on child node repeatedly', function() { + var tree = new EventMapTree(); + tree.setListener(['colors', 'green'], 'listener1'); + var node = tree._getChild(['colors', 'green']); + node.destroy(); + node.destroy(); + }); + it('does not remove parent nodes with existing listeners', function() { + var tree = new EventMapTree(); + tree.setListener(['colors'], 'listener1'); + tree.setListener(['colors', 'green'], 'listener2'); + var node = tree._getChild(['colors', 'green']); + node.destroy(); + node.destroy(); + expect(tree.getListener(['colors'])).eql('listener1'); + }); + it('does not remove parent nodes with other children', function() { + var tree = new EventMapTree(); + tree.setListener(['colors', 'red'], 'listener1'); + tree.setListener(['colors', 'green'], 'listener2'); + var node = tree._getChild(['colors', 'green']); + node.destroy(); + node.destroy(); + expect(tree.getListener(['colors', 'red'])).eql('listener1'); + }); + }); describe('deleteListener', function() { it('can be called before setListener', function() { var tree = new EventMapTree(); From 67b99e007fd3577ed4e32802532d484eb6941d7c Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 30 Aug 2019 17:58:30 -0700 Subject: [PATCH 210/479] return tree node from EventListenerTree.addListener for efficient destruction --- lib/Model/EventListenerTree.js | 6 ++++-- test/Model/EventListenerTree.js | 35 ++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/Model/EventListenerTree.js b/lib/Model/EventListenerTree.js index 29fd93c57..1b0d0520e 100644 --- a/lib/Model/EventListenerTree.js +++ b/lib/Model/EventListenerTree.js @@ -92,8 +92,9 @@ EventListenerTree.prototype._getOrCreateChild = function(segments) { * location, and calling twice with the same segments and listener value has no * effect. Unlike EventEmitter, listener may be any type of value * - * @param {string[]} segments - * @param {*} listener + * @param {string[]} segments + * @param {*} listener + * @return {EventListenerTree} */ EventListenerTree.prototype.addListener = function(segments, listener) { var node = this._getOrCreateChild(segments); @@ -106,6 +107,7 @@ EventListenerTree.prototype.addListener = function(segments, listener) { } else { node.listeners = [listener]; } + return node; }; /** diff --git a/test/Model/EventListenerTree.js b/test/Model/EventListenerTree.js index 43e9f9b51..8a086acc4 100644 --- a/test/Model/EventListenerTree.js +++ b/test/Model/EventListenerTree.js @@ -33,6 +33,14 @@ describe('EventListenerTree', function() { expect(tree.getListeners(['colors'])).eql([]); expect(tree.getListeners(['colors', 'green'])).eql([listener]); }); + it('returns the node to which the listener was added', function() { + var tree = new EventListenerTree(); + var listener = {}; + var node = tree.addListener(['colors', 'green'], listener); + expect(node).a(EventListenerTree); + expect(node.parent.parent).equal(tree); + }); + }); describe('destroy', function() { it('can be called on empty root', function() { var tree = new EventListenerTree(); @@ -73,7 +81,6 @@ describe('EventListenerTree', function() { var tree = new EventListenerTree(); var listener = {}; tree.removeListener(['colors', 'green'], listener); - expect(tree.getListeners(['colors', 'green'])).eql([]); expect(tree.children).eql(null); }); it('removes listener at root', function() { @@ -139,6 +146,32 @@ describe('EventListenerTree', function() { expect(tree.getListeners(['colors', 'green'])).eql(['listener2']); }); }); + describe('removeOwnListener', function() { + it('can be called on node without listeners', function() { + var tree = new EventListenerTree(); + tree.removeOwnListener('listener1'); + }); + it('removes the listener at a node', function() { + var tree = new EventListenerTree(); + tree.addListener([], 'listener1'); + tree.addListener([], 'listener2'); + tree.removeOwnListener('listener1'); + expect(tree.getListeners([])).eql(['listener2']); + }); + it('removes listener from the node returned by addListener', function() { + var tree = new EventListenerTree(); + var node = tree.addListener(['colors', 'green'], 'listener1'); + node.removeOwnListener('listener1'); + expect(tree.getListeners(['colors', 'green'])).eql([]); + expect(tree.children).eql(null); + }); + it('can be called repeatedly', function() { + var tree = new EventListenerTree(); + var node = tree.addListener(['colors', 'green'], 'listener1'); + node.removeOwnListener('listener1'); + node.removeOwnListener('listener1'); + }); + }); describe('removeAllListeners', function() { it('can be called on empty root', function() { var tree = new EventListenerTree(); From ac4cd2eaa2578e3eb17b01d8462f2e001776e812 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 30 Aug 2019 17:59:26 -0700 Subject: [PATCH 211/479] implement EventListenerTree wildcards --- lib/Model/EventListenerTree.js | 67 ++++++++++++++++++++ test/Model/EventListenerTree.js | 106 ++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/lib/Model/EventListenerTree.js b/lib/Model/EventListenerTree.js index 1b0d0520e..733f77b55 100644 --- a/lib/Model/EventListenerTree.js +++ b/lib/Model/EventListenerTree.js @@ -203,6 +203,73 @@ EventListenerTree.prototype.getDescendantListeners = function(segments) { return listeners; }; +/** + * Return an array with each of the listeners to descendent nodes, not + * including listeners to this node itself + * + * @return {Array} listeners + */ +EventListenerTree.prototype.getOwnDescendantListeners = function() { + var listeners = []; + pushDescendantListeners(listeners, this); + return listeners; +}; + +/** + * Return an array with each of the listeners to `segments`, including + * treating wildcard segments ('*') and remainder segments ('**') as matches + * + * @param {string[]} segments + * @return {Array} listeners + */ +EventListenerTree.prototype.getWildcardListeners = function(segments) { + var listeners = []; + pushWildcardListeners(listeners, this, segments, 0); + return listeners; +}; + +/** + * Push listeners matching `segments`, wildcards ('*'), and remainders ('**') + * onto passed in array. Start from segments index passed in for branching + * recursion without needing to modify segments array + * + * @param {Array} listeners + * @param {EventListenerTree} node + * @param {string[]} segments + * @param {integer} start + */ +function pushWildcardListeners(listeners, node, segments, start) { + for (var i = start, len = segments.length; i < len; i++) { + var children = node.children; + if (!children) return; + pushRemainderListeners(listeners, node); + var wildcardNode = children.values['*']; + if (wildcardNode) { + pushWildcardListeners(listeners, wildcardNode, segments, i + 1); + } + var segment = segments[i]; + node = children.values[segment]; + if (!node) return; + } + if (node.children) { + pushRemainderListeners(listeners, node); + } + pushListeners(listeners, node); +}; + +/** + * Push listeners to the '**' onto the passed in array + * + * @param {Array} listeners + * @param {EventListenerTree} node + */ +function pushRemainderListeners(listeners, node) { + var remainderNode = node.children.values['**']; + if (remainderNode) { + pushListeners(listeners, remainderNode); + } +} + /** * Push direct listeners onto the passed in array * diff --git a/test/Model/EventListenerTree.js b/test/Model/EventListenerTree.js index 8a086acc4..26a84a113 100644 --- a/test/Model/EventListenerTree.js +++ b/test/Model/EventListenerTree.js @@ -296,4 +296,110 @@ describe('EventListenerTree', function() { expect(affected).eql(['listener5']); }); }); + describe('getWildcardListeners', function() { + it('returns empty array without listeners', function() { + var tree = new EventListenerTree(); + var affected = tree.getWildcardListeners([]); + expect(affected).eql([]); + }); + it('returns exact match on root', function() { + var tree = new EventListenerTree(); + tree.addListener([], 'listener1'); + var affected = tree.getWildcardListeners([]); + expect(affected).eql(['listener1']); + }); + it('returns exact match on subpath', function() { + var tree = new EventListenerTree(); + tree.addListener(['colors', 'green'], 'listener1'); + var affected = tree.getWildcardListeners(['colors', 'green']); + expect(affected).eql(['listener1']); + }); + it('does not match parent, descendant, or peer listeners', function() { + var tree = new EventListenerTree(); + tree.addListener([], 'listener1'); + tree.addListener(['colors'], 'listener2'); + tree.addListener(['colors', 'green'], 'listener3'); + tree.addListener(['colors', 'green', 'hex'], 'listener4'); + tree.addListener(['colors', 'red'], 'listener5'); + var affected = tree.getWildcardListeners(['colors', 'green']); + expect(affected).eql(['listener3']); + }); + it('returns * match on first segment', function() { + var tree = new EventListenerTree(); + tree.addListener(['*'], 'listener1'); + var affected = tree.getWildcardListeners(['colors']); + expect(affected).eql(['listener1']); + }); + it('returns * match at subpath', function() { + var tree = new EventListenerTree(); + tree.addListener(['colors', '*'], 'listener1'); + tree.addListener(['textures', '*'], 'listener2'); + var affected = tree.getWildcardListeners(['colors', 'green']); + expect(affected).eql(['listener1']); + }); + it('returns * match at parent', function() { + var tree = new EventListenerTree(); + tree.addListener(['*', 'green'], 'listener1'); + tree.addListener(['*', 'red'], 'listener2'); + var affected = tree.getWildcardListeners(['colors', 'green']); + expect(affected).eql(['listener1']); + }); + it('returns multiple * match', function() { + var tree = new EventListenerTree(); + tree.addListener(['*', '*'], 'listener1'); + var affected = tree.getWildcardListeners(['colors', 'green']); + expect(affected).eql(['listener1']); + }); + it('does not * match parent, descendant, or peer listeners', function() { + var tree = new EventListenerTree(); + tree.addListener([], 'listener1'); + tree.addListener(['*'], 'listener2'); + tree.addListener(['*', '*'], 'listener3'); + tree.addListener(['*', '*', '*'], 'listener4'); + var affected = tree.getWildcardListeners(['colors', 'green']); + expect(affected).eql(['listener3']); + }); + it('returns multiple * matches together', function() { + var tree = new EventListenerTree(); + tree.addListener(['colors', 'green'], 'listener1'); + tree.addListener(['*', 'green'], 'listener2'); + tree.addListener(['colors', '*'], 'listener3'); + tree.addListener(['colors', '*'], 'listener4'); + var affected = tree.getWildcardListeners(['colors', 'green']).sort(); + expect(affected).eql(['listener1', 'listener2', 'listener3', 'listener4']); + }); + it('returns ** match on root', function() { + var tree = new EventListenerTree(); + tree.addListener(['**'], 'listener1'); + var affected = tree.getWildcardListeners([]); + expect(affected).eql(['listener1']); + }); + it('returns ** match on first segment', function() { + var tree = new EventListenerTree(); + tree.addListener(['**'], 'listener1'); + var affected = tree.getWildcardListeners(['colors']); + expect(affected).eql(['listener1']); + }); + it('returns ** match on descendant', function() { + var tree = new EventListenerTree(); + tree.addListener(['**'], 'listener1'); + var affected = tree.getWildcardListeners(['colors', 'green']); + expect(affected).eql(['listener1']); + }); + it('returns exact match followed by **', function() { + var tree = new EventListenerTree(); + tree.addListener(['colors', '**'], 'listener1'); + var affected = tree.getWildcardListeners(['colors']); + expect(affected).eql(['listener1']); + }); + it('does not ** match peer or descendant listeners', function() { + var tree = new EventListenerTree(); + tree.addListener(['**'], 'listener1'); + tree.addListener(['colors', '**'], 'listener2'); + tree.addListener(['colors', 'green', '**'], 'listener3'); + tree.addListener(['textures', '**'], 'listener4'); + var affected = tree.getWildcardListeners(['colors']); + expect(affected).eql(['listener1', 'listener2']); + }); + }); }); From d182ebb2d2ca1379ba1755ccccf0aff617de1b39 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 30 Aug 2019 18:04:55 -0700 Subject: [PATCH 212/479] disambiguate name of CollectionMap --- lib/Model/collections.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Model/collections.js b/lib/Model/collections.js index 910fcc423..8681ffd73 100644 --- a/lib/Model/collections.js +++ b/lib/Model/collections.js @@ -2,13 +2,13 @@ var Model = require('./Model'); var LocalDoc = require('./LocalDoc'); var util = require('../util'); -function CollectionMap() {} +function ModelCollections() {} function ModelData() {} function DocMap() {} function CollectionData() {} Model.INITS.push(function(model) { - model.root.collections = new CollectionMap(); + model.root.collections = new ModelCollections(); model.root.data = new ModelData(); }); @@ -82,7 +82,7 @@ Model.prototype.destroy = function(subpath) { silentModel._removeAllFilters(segments); // Silently remove all model data within subpath if (segments.length === 0) { - this.root.collections = new CollectionMap(); + this.root.collections = new ModelCollections(); // Delete each property of data instead of creating a new object so that // it is possible to continue using a reference to the original data object var data = this.root.data; From 6d2ab33514160eff907562bd177e5958bd9c6028 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 30 Aug 2019 18:06:25 -0700 Subject: [PATCH 213/479] cleanup: should use .** only --- test/Model/fn.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Model/fn.js b/test/Model/fn.js index 41869e741..350461a6b 100644 --- a/test/Model/fn.js +++ b/test/Model/fn.js @@ -373,7 +373,7 @@ describe('fn', function() { ] }); model.start('_test.out', '_test.in', 'unity'); - model.on('all', '_test.out**', function(path, event) { + model.on('all', '_test.out.**', function(path, event) { expect(event).to.equal('move'); expect(path).to.equal('a'); done(); @@ -403,7 +403,7 @@ describe('fn', function() { ] }); model.start('_test.out', '_test.in', 'unity'); - model.on('all', '_test.in**', function(path, event) { + model.on('all', '_test.in.**', function(path, event) { expect(event).to.equal('move'); expect(path).to.equal('a'); done(); @@ -433,7 +433,7 @@ describe('fn', function() { ] }); model.start('_test.out', '_test.in', 'unity'); - model.on('all', '_test.out**', function(path, event) { + model.on('all', '_test.out.**', function(path, event) { expect(event).to.equal('change'); expect(path).to.equal('a.0.x'); done(); @@ -463,7 +463,7 @@ describe('fn', function() { ] }); model.start('_test.out', '_test.in', 'unity'); - model.on('all', '_test.in**', function(path, event) { + model.on('all', '_test.in.**', function(path, event) { expect(event).to.equal('change'); expect(path).to.equal('a.0.x'); done(); From 5876dc1c9052d54bfe0427e623983679b267496e Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 10 Sep 2019 11:44:38 -0700 Subject: [PATCH 214/479] rewrite model events on top of EventListenerTree & with event objects instead of eventArgs --- lib/Model/RemoteDoc.js | 42 +- lib/Model/events.js | 774 ++++++++++++++++++------------------- lib/Model/filter.js | 8 +- lib/Model/fn.js | 8 +- lib/Model/mutators.js | 53 ++- lib/Model/ref.js | 76 ++-- lib/Model/refList.js | 111 +++--- lib/Model/setDiff.js | 17 +- lib/Model/subscriptions.js | 5 +- test/Model/setDiff.js | 77 ++-- 10 files changed, 588 insertions(+), 583 deletions(-) diff --git a/lib/Model/RemoteDoc.js b/lib/Model/RemoteDoc.js index b4c856106..89c644d3c 100644 --- a/lib/Model/RemoteDoc.js +++ b/lib/Model/RemoteDoc.js @@ -8,6 +8,12 @@ var Doc = require('./Doc'); var util = require('../util'); +var mutationEvents = require('./events').mutationEvents; +var ChangeEvent = mutationEvents.ChangeEvent; +var LoadEvent = mutationEvents.LoadEvent; +var InsertEvent = mutationEvents.InsertEvent; +var RemoveEvent = mutationEvents.RemoveEvent; +var MoveEvent = mutationEvents.MoveEvent; module.exports = RemoteDoc; @@ -55,7 +61,8 @@ RemoteDoc.prototype._initShareDoc = function() { // so we create the appropriate event here. if (isLocal) return; delete doc.collectionData[id]; - model.emit('change', [collectionName, id], [undefined, previous, model._pass]); + var event = new ChangeEvent(undefined, previous, model._pass); + model._emitMutation([collectionName, id], event); }); shareDoc.on('create', function(isLocal) { // Local creates should not emit an event, since they only happen @@ -66,7 +73,8 @@ RemoteDoc.prototype._initShareDoc = function() { if (isLocal) return; doc._updateCollectionData(); var value = shareDoc.data; - model.emit('change', [collectionName, id], [value, undefined, model._pass]); + var event = new ChangeEvent(value, undefined, model._pass); + model._emitMutation([collectionName, id], event); }); shareDoc.on('error', function(err) { model._emitError(err, collectionName + '.' + id); @@ -76,7 +84,8 @@ RemoteDoc.prototype._initShareDoc = function() { var value = shareDoc.data; // If we subscribe to an uncreated document, no need to emit 'load' event if (value === undefined) return; - model.emit('load', [collectionName, id], [value, model._pass]); + var event = new LoadEvent(value, model._pass); + model._emitMutation([collectionName, id], event); }); this._updateCollectionData(); }; @@ -440,32 +449,37 @@ RemoteDoc.prototype._onOp = function(op) { if (defined(item.oi) || defined(item.od)) { var value = item.oi; var previous = item.od; - model.emit('change', segments, [value, previous, model._pass]); + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); // ListReplaceOp } else if (defined(item.li) && defined(item.ld)) { var value = item.li; var previous = item.ld; - model.emit('change', segments, [value, previous, model._pass]); + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); // ListInsertOp } else if (defined(item.li)) { var index = segments[segments.length - 1]; var values = [item.li]; - model.emit('insert', segments.slice(0, -1), [index, values, model._pass]); + var event = new InsertEvent(index, values, model._pass); + model._emitMutation(segments.slice(0, -1), event); // ListRemoveOp } else if (defined(item.ld)) { var index = segments[segments.length - 1]; var removed = [item.ld]; - model.emit('remove', segments.slice(0, -1), [index, removed, model._pass]); + var event = new RemoveEvent(index, removed, model._pass); + model._emitMutation(segments.slice(0, -1), event); // ListMoveOp } else if (defined(item.lm)) { var from = segments[segments.length - 1]; var to = item.lm; var howMany = 1; - model.emit('move', segments.slice(0, -1), [from, to, howMany, model._pass]); + var event = new MoveEvent(from, to, howMany, model._pass); + model._emitMutation(segments.slice(0, -1), event); // StringInsertOp } else if (defined(item.si)) { @@ -475,7 +489,8 @@ RemoteDoc.prototype._onOp = function(op) { var value = model._get(segments); var previous = value.slice(0, index) + value.slice(index + text.length); var pass = model.pass({$stringInsert: {index: index, text: text}})._pass; - model.emit('change', segments, [value, previous, pass]); + var event = new ChangeEvent(value, previous, pass); + model._emitMutation(segments, event); // StringRemoveOp } else if (defined(item.sd)) { @@ -486,13 +501,15 @@ RemoteDoc.prototype._onOp = function(op) { var value = model._get(segments); var previous = value.slice(0, index) + text + value.slice(index); var pass = model.pass({$stringRemove: {index: index, howMany: howMany}})._pass; - model.emit('change', segments, [value, previous, pass]); + var event = new ChangeEvent(value, previous, pass); + model._emitMutation(segments, event); // IncrementOp } else if (defined(item.na)) { var value = this.get(item.p); var previous = value - item.na; - model.emit('change', segments, [value, previous, model._pass]); + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); // SubtypeOp } else if (defined(item.t)) { @@ -506,7 +523,8 @@ RemoteDoc.prototype._onOp = function(op) { var type = item.t; var op = item.o; var pass = model.pass({$subtype: {type: type, op: op}})._pass; - model.emit('change', segments, [value, previous, pass]); + var event = new ChangeEvent(value, previous, pass); + model._emitMutation(segments, event); } }; diff --git a/lib/Model/events.js b/lib/Model/events.js index e5aaf3f36..0d943219a 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -1,42 +1,57 @@ // @ts-check var EventEmitter = require('events').EventEmitter; -var util = require('../util'); +var EventListenerTree = require('./EventListenerTree'); +var mergeInto = require('../util').mergeInto; /** @type any */ var Model = require('./Model'); // These events are re-emitted as 'all' events, and they are queued up and // emitted in sequence, so that events generated by other events are not // seen in a different order by later listeners -Model.MUTATOR_EVENTS = { - change: true, - insert: true, - remove: true, - move: true, - load: true, - unload: true +exports.mutationEvents = { + ChangeEvent: ChangeEvent, + LoadEvent: LoadEvent, + UnloadEvent: UnloadEvent, + InsertEvent: InsertEvent, + RemoveEvent: RemoveEvent, + MoveEvent: MoveEvent }; +exports.Passed = Passed; + Model.INITS.push(function(model) { - EventEmitter.call(this); + var root = model.root; + EventEmitter.call(root); // Set max listeners to unlimited model.setMaxListeners(0); // Used in async methods to emit an error event if a callback is not supplied. // This will throw if there is no handler for model.on('error') - model.root._defaultCallback = defaultCallback; + root._defaultCallback = defaultCallback; function defaultCallback(err) { if (err) model._emitError(err); } - model.root._mutatorEventQueue = null; - model.root._pass = new Passed({}, {}); - model.root._silent = null; - model.root._eventContext = null; + var mutationListeners = { + all: new EventListenerTree() + }; + for (var name in exports.mutationEvents) { + var eventPrototype = exports.mutationEvents[name].prototype; + mutationListeners[eventPrototype.type] = new EventListenerTree(); + mutationListeners[eventPrototype._immediateType] = new EventListenerTree(); + } + root._mutationListeners = mutationListeners; + root._emittingMutation = false; + root._mutationEventQueue = null; + root._pass = new Passed(); + root._silent = null; + root._eventContextListeners = {}; + root._eventContext = null; }); -util.mergeInto(Model.prototype, EventEmitter.prototype); +mergeInto(Model.prototype, EventEmitter.prototype); Model.prototype.wrapCallback = function(cb) { if (!cb) return this.root._defaultCallback; @@ -70,134 +85,144 @@ Model.prototype._emitError = function(err, context) { this.emit('error', err); }; -// EventEmitter.prototype.on, EventEmitter.prototype.addListener, and -// EventEmitter.prototype.once return `this`. The Model equivalents return -// the listener instead, since it is made internally for method subscriptions -// and may need to be passed to removeListener. - -Model.prototype._emit = EventEmitter.prototype.emit; -Model.prototype.emit = function(type) { - if (type === 'error') { - return this._emit.apply(this, arguments); +Model.prototype._emitMutation = function(segments, event) { + if (this._silent) return; + var root = this.root; + this._callMutationListeners(event._immediateType, segments, event); + if (root._emittingMutation) { + if (root._mutationEventQueue) { + root._mutationEventQueue.push(segments, event); + } else { + root._mutationEventQueue = [segments, event]; + } + return; } - if (Model.MUTATOR_EVENTS[type]) { - if (this._silent) return this; - // `segments` is almost definitely an array of strings. - // - // A search for `.emit(` shows that `segments` is generated from either - // `Model#_splitPath` or `Model#_dereference`, both of which return an array - // of strings. - var segments = arguments[1]; - var eventArgs = arguments[2]; - this._emit(type + 'Immediate', segments, eventArgs); - if (this.root._mutatorEventQueue) { - this.root._mutatorEventQueue.push([type, segments, eventArgs]); - return this; + root._emittingMutation = true; + this._callMutationListeners(event.type, segments, event); + this._callMutationListeners('all', segments, event); + var limit = 1000; + while (root._mutationEventQueue) { + if (--limit < 0) { + throw new Error( + 'Maximum model mutation event cycles exceeded. Most likely, an event ' + + 'listener is performing a mutation that emits an event to the same ' + + 'listener, directly or indirectly. This creates an infinite cycle.' + ); } - this.root._mutatorEventQueue = []; - this._emit(type, segments, eventArgs); - this._emit('all', segments, [type].concat(eventArgs)); - while (this.root._mutatorEventQueue.length) { - var queued = this.root._mutatorEventQueue.shift(); - type = queued[0]; - segments = queued[1]; - eventArgs = queued[2]; - this._emit(type, segments, eventArgs); - this._emit('all', segments, [type].concat(eventArgs)); + var queue = root._mutationEventQueue; + root._mutationEventQueue = null; + for (var i = 0; i < queue.length;) { + segments = queue[i++]; + event = queue[i++]; + this._callMutationListeners(event.type, segments, event); + this._callMutationListeners('all', segments, event); } - this.root._mutatorEventQueue = null; - return this; } - return this._emit.apply(this, arguments); -}; - -Model.prototype._on = EventEmitter.prototype.on; -Model.prototype.addListener = -Model.prototype.on = function(type, pattern, options, cb) { - var listener = eventListener(this, type, pattern, options, cb); - this._on(type, listener); - return listener; + root._emittingMutation = false; }; -Model.prototype.once = function(type, pattern, options, cb) { - var listener = eventListener(this, type, pattern, options, cb); - function g() { - var matches = listener.apply(null, arguments); - if (matches) this.removeListener(type, g); +Model.prototype._callMutationListeners = function(type, segments, event) { + var tree = this.root._mutationListeners[type]; + var listeners = tree.getWildcardListeners(segments); + for (var i = 0, len = listeners.length; i < len; i++) { + var fn = listeners[i].fn; + fn(segments, event); } - this._on(type, g); - return g; }; -/** - * @typedef {Object} ModelOnOptions - * @property {boolean} [useEventObjects] - If true, the listener is called with - * `cb(event: ___Event, captures: string[])`, instead of the legacy var-args - * style `cb(captures..., [eventType], eventArgs..., passed)`. - */ - -Model.prototype._removeAllListeners = EventEmitter.prototype.removeAllListeners; -Model.prototype.removeAllListeners = function(type, subpattern) { - // If a pattern is specified without an event type, remove all model event - // listeners under that pattern for all events - if (!type) { - for (var key in this._events) { - this.removeAllListeners(key, subpattern); - } - return this; - } +// EventEmitter.prototype.on, EventEmitter.prototype.addListener, and +// EventEmitter.prototype.once return `this`. The Model equivalents return +// the listener instead, since it is made internally for method subscriptions +// and may need to be passed to removeListener. - var pattern = this.path(subpattern); - // If no pattern is specified, remove all listeners like normal - if (!pattern) { - if (arguments.length === 0) { - return this._removeAllListeners(); - } - return this._removeAllListeners(type); +Model.prototype.__on = EventEmitter.prototype.on; +Model.prototype.addListener = +Model.prototype.on = function(type, arg1, arg2, arg3) { + var listener = this._addMutationListener(type, arg1, arg2, arg3); + if (listener) { + return listener; } + // Normal event + this.__on(type, arg1); + return arg1; +}; - // Remove all listeners for an event under a pattern - var listeners = this.listeners(type); - var segments = pattern.split('.'); - // Make sure to iterate in reverse, since the array might be - // mutated as listeners are removed - for (var i = listeners.length; i--;) { - var listener = listeners[i]; - if (patternContained(pattern, segments, listener)) { - this.removeListener(type, listener); - } +Model.prototype.__once = EventEmitter.prototype.once; +Model.prototype.once = function(type, arg1, arg2, arg3) { + var listener = this._addMutationListener(type, arg1, arg2, arg3); + if (listener) { + onceWrapListener(this, listener); + return listener; } - return this; + // Normal event + this.__once(type, arg1); + return arg1; }; -function patternContained(pattern, segments, listener) { - var listenerSegments = listener.patternSegments; - if (!listenerSegments) return false; - if (pattern === listener.pattern || pattern === '**') return true; - var len = segments.length; - if (len > listenerSegments.length) return false; - for (var i = 0; i < len; i++) { - if (segments[i] !== listenerSegments[i]) return false; - } - return true; +function onceWrapListener(model, listener) { + var fn = listener.fn; + listener.fn = function onceWrapper(segments, event) { + model._removeMutationListener(listener); + fn(segments, event); + }; } -Model.prototype.pass = function(object, invert) { - var model = this._child(); - model._pass = (invert) ? - new Passed(object, this._pass) : - new Passed(this._pass, object); - return model; +Model.prototype.__removeListener = EventEmitter.prototype.removeListener; +Model.prototype.removeListener = function(type, listener) { + if (this.root._mutationListeners[type]) { + this._removeMutationListener(listener); + return; + } + // Normal event + this.__removeListener(type, listener); }; -function Passed(previous, value) { - for (var key in previous) { - this[key] = previous[key]; +Model.prototype.__removeAllListeners = EventEmitter.prototype.removeAllListeners; +Model.prototype.removeAllListeners = function(type, subpath) { + var segments = this._splitPath(subpath); + this._removeAllListeners(type, segments); +}; +Model.prototype._removeAllListeners = function(type, segments) { + var mutationListeners = this.root._mutationListeners; + if (type == null) { + for (var type in mutationListeners) { + var tree = mutationListeners[type]; + tree.removeAllListeners(segments); + } + return; } - for (var key in value) { - this[key] = value[key]; + var tree = mutationListeners[type]; + if (tree) { + tree.removeAllListeners(segments); + return; } -} + // Normal event + this.__removeAllListeners(type); +}; + +function Passed() {} + +Model.prototype.pass = (Object.assign) ? + function(object, invert) { + var model = this._child(); + model._pass = (invert) ? + Object.assign(new Passed(), object, this._pass) : + Object.assign(new Passed(), this._pass, object); + return model; + } : + function(object, invert) { + var model = this._child(); + var pass = new Passed(); + if (invert) { + mergeInto(pass, object); + mergeInto(pass, this._pass); + } else { + mergeInto(pass, this._pass); + mergeInto(pass, object); + } + model._pass = pass; + return model; + }; /** * The returned Model will or won't trigger event handlers when the model emits @@ -211,311 +236,286 @@ Model.prototype.silent = function(value) { return model; }; -Model.prototype.eventContext = function(value) { +Model.prototype.eventContext = function(id) { var model = this._child(); - model._eventContext = value; + model._eventContext = id; return model; }; -Model.prototype.removeContextListeners = function(value) { - if (arguments.length === 0) { - value = this._eventContext; +Model.prototype.removeContextListeners = function() { + var id = this._eventContext; + if (id == null) return; + var map = this.root._eventContextListeners; + var listeners = map[id]; + if (!listeners) return; + delete map[id]; + for (var i = listeners.length; i--;) { + var listener = listeners[i]; + listener.node.removeOwnListener(listener); } - // Remove all events created within a given context - for (var type in this._events) { - var listeners = this.listeners(type); - // Make sure to iterate in reverse, since the array might be - // mutated as listeners are removed - for (var i = listeners.length; i--;) { - var listener = listeners[i]; - if (listener.eventContext === value) { - this.removeListener(type, listener); - } +}; + +Model.prototype._removeMutationListener = function(listener) { + listener.node.removeOwnListener(listener); + var id = this._eventContext; + if (id == null) return; + var map = this.root._eventContextListeners; + var listeners = map[id]; + if (!listeners) return; + // Always iterate though all listeners rather than breaking early. A listener + // may be in the list more than once, since model._addContextListener() + // doesn't prevent it at time of adding + for (var i = listeners.length; i--;) { + if (listeners[i] === listener) { + listeners.splice(i, 1); } } - return this; +}; + +Model.prototype._addMutationListener = function(type, arg1, arg2, arg3) { + var tree = this.root._mutationListeners[type]; + if (!tree) return; + // Create double-linked listener and tree node for later removal + var listener = getMutationListener(this, type, arg1, arg2, arg3); + var node = tree.addListener(listener.patternSegments, listener); + listener.node = node; + // Maintain an index of listeners by eventContext id + var id = this._eventContext; + if (id == null) return listener; + var map = this.root._eventContextListeners; + var listeners = map[id]; + if (listeners) { + // Unlike a typical event listener, don't check to see if a listener is + // already tracked on add. Instead, check to see if the listener might occur + // more than once when removing. Generally, listeners are expected to be + // added many at a time during rendering and removed all at once with + // model.removeContextListeners(). Individual removes are expected to be + // infrequent and individual adds are expected to be frequent + listeners.push(listener); + } else { + map[id] = [listener]; + } + return listener; }; +/** + * @typedef {Object} ModelOnOptions + * @property {boolean} [useEventObjects] - If true, the listener is called with + * `cb(event: ___Event, captures: string[])`, instead of the legacy var-args + * style `cb(captures..., [eventType], eventArgs..., passed)`. + */ + /** * @param {Model} model - * @param {string} eventType + * @param {string} type */ -function eventListener(model, eventType, arg2, arg3, arg4) { - var subpattern, options, cb; - if (arg4) { - // on(eventType, path, options, cb) - subpattern = arg2; - options = arg3; - cb = arg4; - } else if (arg3) { - // on(eventType, path, cb) - // on(eventType, options, cb) +function getMutationListener(model, type, arg1, arg2, arg3) { + var pattern, options, cb; + if (typeof arg3 === 'function') { + // on(type, subpath, options, cb) + pattern = model.path(arg1); + options = arg2; cb = arg3; - if (model.isPath(arg2)) { - subpattern = arg2; - } else { - options = arg2; + } else if (typeof arg2 === 'function') { + // on(type, subpath, cb) + // model.on('change', 'example.subpath.**', callback) + // model.at('example').on('change', 'subpath', callback) + // on(type, options, cb) + // model.at('example').on('change', {useEventObjects: true}, callback) + pattern = model.path(arg1); + if (pattern == null) { + options = arg1; } - } else { // if (arg2) - // on(eventType, cb) cb = arg2; + } else if (typeof arg1 === 'function') { + // on(type, cb) + // Normal (non-mutator) event: + // model.on('normalEvent', callback) + // Path from scoped model: + // model.at('example').on('change', callback) + // Raw event emission: + // model.on('change', callback) + pattern = model.path(); + cb = arg1; + } else { + throw new Error('No expected callback function'); } - if (options) { - if (options.useEventObjects) { - var useEventObjects = true; - } - } - - if (subpattern) { - // For signatures with pattern: - // model.on('change', 'example.subpath.**', callback) - // model.at('example').on('change', 'subpath', callback) - var pattern = model.path(subpattern); - return (useEventObjects) ? - modelEventListener(eventType, pattern, cb, model._eventContext) : - modelEventListenerLegacy(pattern, cb, model._eventContext); - } - // For signature without explicit pattern: - // model.at('example').on('change', callback) - /** @type string */ - var path = model.path(); - if (path) { - return (useEventObjects) ? - modelEventListener(eventType, path, cb, model._eventContext) : - modelEventListenerLegacy(path, cb, model._eventContext); + if (!pattern) { + // Listen to raw event emission when no path is provided + return new MutationListener(['**'], model._eventContext, cb); } - // For signature: - // model.on('normalEvent', callback) - return cb; + pattern = normalizePattern(pattern); + return (options && options.useEventObjects) ? + createMutationListener(pattern, model._eventContext, cb) : + createMutationListenerLegacy(type, pattern, model._eventContext, cb); } -/** - * Legacy version of `modelEventListener` that calls `cb` with var-args - * `(captures..., [eventType], args..., passed)` instead of new-style - * `___Event` objects. - * - * @param {string} pattern - * @param {Function} cb - * @param {*} eventContext - * @return {ModelListenerFn & ModelListenerProps} - */ -function modelEventListenerLegacy(pattern, cb, eventContext) { - var patternSegments = util.castSegments(pattern.split('.')); - var testFn = testPatternFn(pattern, patternSegments); - - /** @type ModelListenerFn */ - function modelListener(segments, eventArgs) { - var captures = testFn(segments); - if (!captures) return; - - var args = (captures.length) ? captures.concat(eventArgs) : eventArgs; - cb.apply(null, args); - return true; +function createCaptures(captureIndicies, remainingIndex, segments) { + var captures = []; + if (captureIndicies) { + for (var i = 0; i < captureIndicies.length; i++) { + var index = captureIndicies[i]; + captures.push(segments[index]); + } } - - // Used in Model#removeAllListeners - modelListener.pattern = pattern; - modelListener.patternSegments = patternSegments; - modelListener.eventContext = eventContext; - - return modelListener; -} - -/** - * Returns a function that can be passed to `EventEmitter#on`, with some - * additional properties used for `Model#removeAllListeners`. - * - * When the function is called, it checks if the event matches `patternArg`, and - * if there's a match, it calls `cb`. - * - * @param {string} eventType - * @param {string} pattern - * @param {Function} cb - * @param {*} eventContext - * @return {ModelListenerFn & ModelListenerProps} - */ -function modelEventListener(eventType, pattern, cb, eventContext) { - var patternSegments = util.castSegments(pattern.split('.')); - var testFn = testPatternFn(pattern, patternSegments); - - var eventFactory = getEventFactory(eventType); - /** @type ModelListenerFn */ - function modelListener(segments, eventArgs) { - var captures = testFn(segments); - if (!captures) return; - - var event = eventFactory(eventArgs); - cb(event, captures); - return true; + if (remainingIndex != null) { + var remainder = segments.slice(remainingIndex).join('.'); + captures.push(remainder); } - - // Used in Model#removeAllListeners - modelListener.pattern = pattern; - modelListener.patternSegments = patternSegments; - modelListener.eventContext = eventContext; - - return modelListener; + return captures; } -/** @typedef { (segments: string[], eventArgs: any[]) => (boolean | undefined) } ModelListenerFn */ -/** @typedef { {pattern: string, patternSegments: Array, eventContext: any} } ModelListenerProps */ +function MutationListener(patternSegments, eventContext, fn) { + this.patternSegments = patternSegments; + this.eventContext = eventContext; + this.fn = fn; + this.node = null; +} -/** - * Returns a factory function that creates an `___Event` object based on an - * old-style `eventArgs` array. - * - * @param {string} eventType - * @return {(eventArgs: any[]) => ChangeEvent | InsertEvent | RemoveEvent | MoveEvent | LoadEvent | UnloadEvent} - */ -function getEventFactory(eventType) { - switch (eventType) { - case 'change': - return function(eventArgs) { - return new ChangeEvent(eventArgs); - }; - case 'insert': - return function(eventArgs) { - return new InsertEvent(eventArgs); - }; - case 'remove': - return function(eventArgs) { - return new RemoveEvent(eventArgs); - }; - case 'move': - return function(eventArgs) { - return new MoveEvent(eventArgs); - }; - case 'load': - return function(eventArgs) { - return new LoadEvent(eventArgs); - }; - case 'unload': - return function(eventArgs) { - return new UnloadEvent(eventArgs); +function createMutationListener(pattern, eventContext, cb) { + var patternSegments = pattern.split('.'); + var fn; + if (patternSegments.length === 1 && patternSegments[0] === '**') { + fn = function(segments, event) { + var captures = [segments.join('.')]; + cb(event, captures); + }; + } else { + var captureIndicies, remainingIndex; + for (var i = 0; i < patternSegments.length; i++) { + var segment = patternSegments[i]; + if (segment === '*') { + if (captureIndicies) { + captureIndicies.push(i); + } else { + captureIndicies = [i]; + } + } else if (segment === '**') { + if (i !== patternSegments.length - 1) { + throw new Error('Path pattern may contain `**` at end only'); + } + remainingIndex = i; + } + } + if (captureIndicies || remainingIndex != null) { + fn = function(segments, event) { + var captures = createCaptures(captureIndicies, remainingIndex, segments); + cb(event, captures); }; - case 'all': - return function(eventArgs) { - var concreteEventType = eventArgs[0]; // 'change', 'insert', etc. - var concreteEventFactory = getEventFactory(concreteEventType); - return concreteEventFactory(eventArgs.slice(1)); + } else { + fn = function(segments, event) { + cb(event, []); }; - default: throw new Error('Unknown event: ' + eventType); + } } + return new MutationListener(patternSegments, eventContext, fn); } -// These constructors accept the `eventArgs` array format that Racer uses -// internally when calling `Model#emit`. -// -// Eventually, Racer should switch to passing these events around directly, -// but that will require updating all the places that parse the `eventArgs` -// array format, to extract things like `passed`. - -function ChangeEvent(eventArgs) { - this.value = eventArgs[0]; - this.previous = eventArgs[1]; - this.passed = eventArgs[2]; -} -ChangeEvent.prototype.type = 'change'; - -function InsertEvent(eventArgs) { - this.index = eventArgs[0]; - this.values = eventArgs[1]; - this.passed = eventArgs[2]; -} -InsertEvent.prototype.type = 'insert'; - -function RemoveEvent(eventArgs) { - this.index = eventArgs[0]; - this.removed = eventArgs[1]; - this.passed = eventArgs[2]; +function createMutationListenerLegacy(type, pattern, eventContext, cb) { + var mutationListenerAdapter = (type === 'all') ? + function(event, captures) { + var args = captures.concat(event.type, event._getArgs()); + cb.apply(null, args); + } : + function(event, captures) { + var args = captures.concat(event._getArgs()); + cb.apply(null, args); + }; + return createMutationListener(pattern, eventContext, mutationListenerAdapter); } -RemoveEvent.prototype.type = 'remove'; -function MoveEvent(eventArgs) { - this.from = eventArgs[0]; - this.to = eventArgs[1]; - this.howMany = eventArgs[2]; - this.passed = eventArgs[3]; +function ChangeEvent(value, previous, passed) { + this.value = value; + this.previous = previous; + this.passed = passed; } -MoveEvent.prototype.type = 'move'; +ChangeEvent.prototype.type = 'change'; +ChangeEvent.prototype._immediateType = 'changeImmediate'; +ChangeEvent.prototype.clone = function() { + return new ChangeEvent(this.value, this.previous, this.passed); +}; +ChangeEvent.prototype._getArgs = function() { + return [this.value, this.previous, this.passed]; +}; -function LoadEvent(eventArgs) { - this.document = eventArgs[0]; - this.passed = eventArgs[1]; +function LoadEvent(value, passed) { + this.value = value; + this.passed = passed; } LoadEvent.prototype.type = 'load'; +LoadEvent.prototype._immediateType = 'loadImmediate'; +LoadEvent.prototype.clone = function() { + return new LoadEvent(this.value, this.passed); +}; +LoadEvent.prototype._getArgs = function() { + return [this.value, this.passed]; +}; -function UnloadEvent(eventArgs) { - this.previousDocument = eventArgs[0]; - this.passed = eventArgs[1]; +function UnloadEvent(previous, passed) { + this.previous = previous; + this.passed = passed; } UnloadEvent.prototype.type = 'unload'; +UnloadEvent.prototype._immediateType = 'unloadImmediate'; +UnloadEvent.prototype.clone = function() { + return new UnloadEvent(this.previous, this.passed); +}; +UnloadEvent.prototype._getArgs = function() { + return [this.previous, this.passed]; +}; -/** - * Returns a function that tests an array of event segments against the - * `patternSegments`. (`pattern` only matters if it's exactly `'**'`.) - * - * @param {string?} pattern - * @param {Array} patternSegments - * @return {(segments: string[]) => (string[] | undefined)} A function to test - * an array of event segments. If the event segments match, an array of 0 or - * more segments captured by `'*'` / `'**'` is returned, one per wildcard. If - * the event segments don't match, `undefined` is returned. - */ -function testPatternFn(pattern, patternSegments) { - if (pattern === '**') { - return function testPattern(segments) { - return [segments.join('.')]; - }; - } +function InsertEvent(index, values, passed) { + this.index = index; + this.values = values; + this.passed = passed; +} +InsertEvent.prototype.type = 'insert'; +InsertEvent.prototype._immediateType = 'insertImmediate'; +InsertEvent.prototype.clone = function() { + return new InsertEvent(this.index, this.values, this.passed); +}; +InsertEvent.prototype._getArgs = function() { + return [this.index, this.values, this.passed]; +}; - var endingRest = stripRestWildcard(patternSegments); - - return function testPattern(segments) { - // Any pattern with more segments does not match - var patternLen = patternSegments.length; - if (patternLen > segments.length) return; - - // A pattern with the same number of segments matches if each - // of the segments are wildcards or equal. A shorter pattern matches - // if it ends in a rest wildcard and each of the corresponding - // segments are wildcards or equal. - if (patternLen === segments.length || endingRest) { - /** @type string[] */ - var captures = []; - for (var i = 0; i < patternLen; i++) { - var patternSegment = patternSegments[i]; - var segment = segments[i]; - if (patternSegment === '*' || patternSegment === '**') { - captures.push(segment); - continue; - } - if (patternSegment !== segment) return; - } - if (endingRest) { - var remainder = segments.slice(i).join('.'); - captures.push(remainder); - } - return captures; - } - }; +function RemoveEvent(index, values, passed) { + this.index = index; + this.values = values; + this.passed = passed; } +RemoveEvent.prototype.type = 'remove'; +RemoveEvent.prototype._immediateType = 'removeImmediate'; +RemoveEvent.prototype.clone = function() { + return new RemoveEvent(this.index, this.values, this.passed); +}; +RemoveEvent.prototype._getArgs = function() { + return [this.index, this.values, this.passed]; +}; -/** - * @param {Array} segments - */ -function stripRestWildcard(segments) { - // ['example', '**'] -> ['example']; return true - var lastIndex = segments.length - 1; - var lastSegment = segments[lastIndex]; - if (lastSegment === '**') { - segments.pop(); - return true; - } - // ['example', 'subpath**'] -> ['example', 'subpath']; return true - if (typeof lastSegment !== 'string') return false; - var match = /^([^\*]+)\*\*$/.exec(lastSegment); - if (!match) return false; - segments[lastIndex] = match[1]; - return true; +function MoveEvent(from, to, howMany, passed) { + this.from = from; + this.to = to; + this.howMany = howMany; + this.passed = passed; } +MoveEvent.prototype.type = 'move'; +MoveEvent.prototype._immediateType = 'moveImmediate'; +MoveEvent.prototype.clone = function() { + return new MoveEvent(this.from, this.to, this.howMany, this.passed); +}; +MoveEvent.prototype._getArgs = function() { + return [this.from, this.to, this.howMany, this.passed]; +}; + +// DEPRECATED: Normalize pattern ending in '**' to '.**', since these are +// treated equivalently. The '.**' form is preferred, and it should be enforced +// in a future version for clarity +function normalizePattern(pattern) { + var end = pattern.length - 1; + return ( + pattern.charAt(end) === '*' && + pattern.charAt(end - 1) === '*' && + pattern.charAt(end - 2) !== '.' && + pattern.charAt(end - 2) + ) ? pattern.slice(0, end - 1) + '.**' : pattern; +}; diff --git a/lib/Model/filter.js b/lib/Model/filter.js index a1412565c..1dce449f2 100644 --- a/lib/Model/filter.js +++ b/lib/Model/filter.js @@ -5,17 +5,17 @@ var defaultFns = require('./defaultFns'); Model.INITS.push(function(model) { model.root._filters = new Filters(model); model.on('all', filterListener); - function filterListener(segments, eventArgs) { - var pass = eventArgs[eventArgs.length - 1]; + function filterListener(segments, event) { + var passed = event.passed; var map = model.root._filters.fromMap; for (var path in map) { var filter = map[path]; - if (pass.$filter === filter) continue; + if (passed.$filter === filter) continue; if ( util.mayImpact(filter.segments, segments) || (filter.inputsSegments && util.mayImpactAny(filter.inputsSegments, segments)) ) { - filter.update(pass); + filter.update(passed); } } } diff --git a/lib/Model/fn.js b/lib/Model/fn.js index 35f3e4e27..47aa6e9ef 100644 --- a/lib/Model/fn.js +++ b/lib/Model/fn.js @@ -16,19 +16,19 @@ Model.INITS.push(function(model) { function addFnListener(model) { var inputListeners = model._fns.inputListeners; var fromMap = model._fns.fromMap; - model.on('all', function fnListener(segments, eventArgs) { - var pass = eventArgs[eventArgs.length - 1]; + model.on('all', function fnListener(segments, event) { + var passed = event.passed; // Mutation affecting input path var fns = inputListeners.getAffectedListeners(segments); for (var i = 0; i < fns.length; i++) { var fn = fns[i]; - if (fn !== pass.$fn) fn.onInput(pass); + if (fn !== passed.$fn) fn.onInput(passed); } // Mutation affecting output path var fns = fromMap.getAffectedListeners(segments); for (var i = 0; i < fns.length; i++) { var fn = fns[i]; - if (fn !== pass.$fn) fn.onOutput(pass); + if (fn !== passed.$fn) fn.onOutput(passed); } }); } diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index c0cbac757..eac1619a9 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -1,5 +1,10 @@ var util = require('../util'); var Model = require('./Model'); +var mutationEvents = require('./events').mutationEvents; +var ChangeEvent = mutationEvents.ChangeEvent; +var InsertEvent = mutationEvents.InsertEvent; +var RemoveEvent = mutationEvents.RemoveEvent; +var MoveEvent = mutationEvents.MoveEvent; Model.prototype._mutate = function(segments, fn, cb) { cb = this.wrapCallback(cb); @@ -45,7 +50,8 @@ Model.prototype._set = function(segments, value, cb) { // On setting the entire doc, remote docs sometimes do a copy to add the // id without it being stored in the database by ShareJS if (docSegments.length === 0) value = doc.get(docSegments); - model.emit('change', segments, [value, previous, model._pass]); + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); return previous; } return this._mutate(segments, set, cb); @@ -76,7 +82,8 @@ Model.prototype._setNull = function(segments, value, cb) { return previous; } doc.set(docSegments, value, fnCb); - model.emit('change', segments, [value, previous, model._pass]); + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); return value; } return this._mutate(segments, setNull, cb); @@ -148,7 +155,8 @@ Model.prototype._create = function(segments, value, cb) { // On creating the doc, remote docs do a copy to add the id without // it being stored in the database by ShareJS value = doc.get(); - model.emit('change', segments, [value, previous, model._pass]); + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); } this._mutate(segments, create, cb); }; @@ -234,7 +242,8 @@ Model.prototype._add = function(segments, value, cb) { // it being stored in the database by ShareJS value = doc.get(); } - model.emit('change', segments, [value, previous, model._pass]); + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); } this._mutate(segments, add, cb); return id; @@ -267,7 +276,8 @@ Model.prototype._del = function(segments, cb) { var id = segments[1]; model.root.collections[collectionName].remove(id); } - model.emit('change', segments, [undefined, previous, model._pass]); + var event = new ChangeEvent(undefined, previous, model._pass); + model._emitMutation(segments, event); return previous; } return this._mutate(segments, del, cb); @@ -310,7 +320,8 @@ Model.prototype._increment = function(segments, byNumber, cb) { function increment(doc, docSegments, fnCb) { var value = doc.increment(docSegments, byNumber, fnCb); var previous = value - byNumber; - model.emit('change', segments, [value, previous, model._pass]); + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); return value; } return this._mutate(segments, increment, cb); @@ -337,7 +348,8 @@ Model.prototype._push = function(segments, value, cb) { var model = this; function push(doc, docSegments, fnCb) { var length = doc.push(docSegments, value, fnCb); - model.emit('insert', segments, [length - 1, [value], model._pass]); + var event = new InsertEvent(length - 1, [value], model._pass); + model._emitMutation(segments, event); return length; } return this._mutate(segments, push, cb); @@ -364,7 +376,8 @@ Model.prototype._unshift = function(segments, value, cb) { var model = this; function unshift(doc, docSegments, fnCb) { var length = doc.unshift(docSegments, value, fnCb); - model.emit('insert', segments, [0, [value], model._pass]); + var event = new InsertEvent(0, [value], model._pass); + model._emitMutation(segments, event); return length; } return this._mutate(segments, unshift, cb); @@ -397,7 +410,8 @@ Model.prototype._insert = function(segments, index, values, cb) { function insert(doc, docSegments, fnCb) { var inserted = (Array.isArray(values)) ? values : [values]; var length = doc.insert(docSegments, index, inserted, fnCb); - model.emit('insert', segments, [index, inserted, model._pass]); + var event = new InsertEvent(index, inserted, model._pass); + model._emitMutation(segments, event); return length; } return this._mutate(segments, insert, cb); @@ -430,7 +444,8 @@ Model.prototype._pop = function(segments, cb) { return; } var value = doc.pop(docSegments, fnCb); - model.emit('remove', segments, [length - 1, [value], model._pass]); + var event = new RemoveEvent(length - 1, [value], model._pass); + model._emitMutation(segments, event); return value; } return this._mutate(segments, pop, cb); @@ -463,7 +478,8 @@ Model.prototype._shift = function(segments, cb) { return; } var value = doc.shift(docSegments, fnCb); - model.emit('remove', segments, [0, [value], model._pass]); + var event = new RemoveEvent(0, [value], model._pass); + model._emitMutation(segments, event); return value; } return this._mutate(segments, shift, cb); @@ -523,7 +539,8 @@ Model.prototype._remove = function(segments, index, howMany, cb) { var model = this; function remove(doc, docSegments, fnCb) { var removed = doc.remove(docSegments, index, howMany, fnCb); - model.emit('remove', segments, [index, removed, model._pass]); + var event = new RemoveEvent(index, removed, model._pass); + model._emitMutation(segments, event); return removed; } return this._mutate(segments, remove, cb); @@ -594,7 +611,8 @@ Model.prototype._move = function(segments, from, to, howMany, cb) { if (to < 0) to += len; } var moved = doc.move(docSegments, from, to, howMany, fnCb); - model.emit('move', segments, [from, to, moved.length, model._pass]); + var event = new MoveEvent(from, to, moved.length, model._pass); + model._emitMutation(segments, event); return moved; } return this._mutate(segments, move, cb); @@ -633,7 +651,8 @@ Model.prototype._stringInsert = function(segments, index, text, cb) { var previous = doc.stringInsert(docSegments, index, text, fnCb); var value = doc.get(docSegments); var pass = model.pass({$stringInsert: {index: index, text: text}})._pass; - model.emit('change', segments, [value, previous, pass]); + var event = new ChangeEvent(value, previous, pass); + model._emitMutation(segments, event); return; } return this._mutate(segments, stringInsert, cb); @@ -672,7 +691,8 @@ Model.prototype._stringRemove = function(segments, index, howMany, cb) { var previous = doc.stringRemove(docSegments, index, howMany, fnCb); var value = doc.get(docSegments); var pass = model.pass({$stringRemove: {index: index, howMany: howMany}})._pass; - model.emit('change', segments, [value, previous, pass]); + var event = new ChangeEvent(value, previous, pass); + model._emitMutation(segments, event); return; } return this._mutate(segments, stringRemove, cb); @@ -717,7 +737,8 @@ Model.prototype._subtypeSubmit = function(segments, subtype, subtypeOp, cb) { // same object returned by reference or not. This may cause change // listeners to over-trigger, but that is usually going to be better than // under-triggering - model.emit('change', segments, [value, undefined, pass]); + var event = new ChangeEvent(value, undefined, pass); + model._emitMutation(segments, event); return previous; } return this._mutate(segments, subtypeSubmit, cb); diff --git a/lib/Model/ref.js b/lib/Model/ref.js index 07504a177..81e7b88be 100644 --- a/lib/Model/ref.js +++ b/lib/Model/ref.js @@ -6,35 +6,35 @@ Model.INITS.push(function(model) { var root = model.root; root._refs = new Refs(); addIndexListeners(root); - addListener(root, 'change', refChange); - addListener(root, 'load', refLoad); - addListener(root, 'unload', refUnload); - addListener(root, 'insert', refInsert); - addListener(root, 'remove', refRemove); - addListener(root, 'move', refMove); + addListener(root, 'changeImmediate', refChange); + addListener(root, 'loadImmediate', refLoad); + addListener(root, 'unloadImmediate', refUnload); + addListener(root, 'insertImmediate', refInsert); + addListener(root, 'removeImmediate', refRemove); + addListener(root, 'moveImmediate', refMove); }); function addIndexListeners(model) { - model.on('insertImmediate', function refInsertIndex(segments, eventArgs) { - var index = eventArgs[0]; - var howMany = eventArgs[1].length; + model.on('insertImmediate', function refInsertIndex(segments, event) { + var index = event.index; + var howMany = event.values.length; function patchInsert(refIndex) { return (index <= refIndex) ? refIndex + howMany : refIndex; } onIndexChange(segments, patchInsert); }); - model.on('removeImmediate', function refRemoveIndex(segments, eventArgs) { - var index = eventArgs[0]; - var howMany = eventArgs[1].length; + model.on('removeImmediate', function refRemoveIndex(segments, event) { + var index = event.index; + var howMany = event.values.length; function patchRemove(refIndex) { return (index <= refIndex) ? refIndex - howMany : refIndex; } onIndexChange(segments, patchRemove); }); - model.on('moveImmediate', function refMoveIndex(segments, eventArgs) { - var from = eventArgs[0]; - var to = eventArgs[1]; - var howMany = eventArgs[2]; + model.on('moveImmediate', function refMoveIndex(segments, event) { + var from = event.from; + var to = event.to; + var howMany = event.howMany; function patchMove(refIndex) { // If the index was moved itself if (from <= refIndex && refIndex < from + howMany) { @@ -65,8 +65,8 @@ function addIndexListeners(model) { } } -function refChange(model, dereferenced, eventArgs, segments) { - var value = eventArgs[0]; +function refChange(model, dereferenced, event, segments) { + var value = event.value; // Detect if we are deleting vs. setting to undefined if (value === undefined) { var parentSegments = segments.slice(); @@ -79,34 +79,26 @@ function refChange(model, dereferenced, eventArgs, segments) { } model._set(dereferenced, value); } -function refLoad(model, dereferenced, eventArgs) { - var value = eventArgs[0]; - model._set(dereferenced, value); +function refLoad(model, dereferenced, event) { + model._set(dereferenced, event.value); } function refUnload(model, dereferenced) { model._del(dereferenced); } -function refInsert(model, dereferenced, eventArgs) { - var index = eventArgs[0]; - var values = eventArgs[1]; - model._insert(dereferenced, index, values); +function refInsert(model, dereferenced, event) { + model._insert(dereferenced, event.index, event.values); } -function refRemove(model, dereferenced, eventArgs) { - var index = eventArgs[0]; - var howMany = eventArgs[1].length; - model._remove(dereferenced, index, howMany); +function refRemove(model, dereferenced, event) { + model._remove(dereferenced, event.index, event.values.length); } -function refMove(model, dereferenced, eventArgs) { - var from = eventArgs[0]; - var to = eventArgs[1]; - var howMany = eventArgs[2]; - model._move(dereferenced, from, to, howMany); +function refMove(model, dereferenced, event) { + model._move(dereferenced, event.from, event.to, event.howMany); } function addListener(model, type, fn) { - model.on(type + 'Immediate', refListener); - function refListener(segments, eventArgs) { - var pass = eventArgs[eventArgs.length - 1]; + model.on(type, refListener); + function refListener(segments, event) { + var passed = event.passed; // Find cases where an event is emitted on a path where a reference // is pointing. All original mutations happen on the fully dereferenced // location, so this detection only needs to happen in one direction @@ -132,23 +124,23 @@ function addListener(model, type, fn) { // simply re-emit the event. Otherwise, perform the same mutation on // the ref's path if (model._get(dereferenced) === model._get(segments)) { - model.emit(type, dereferenced, eventArgs); + model._emitMutation(dereferenced, event); } else { - var setterModel = ref.model.pass(pass, true); + var setterModel = ref.model.pass(passed, true); setterModel._dereference = noopDereference; - fn(setterModel, dereferenced, eventArgs, segments); + fn(setterModel, dereferenced, event, segments); } } } // If a ref points to a child of a matching subpath, get the value in // case it has changed and set if different - var refs = node.getDescendantListeners([]); + var refs = node.getOwnDescendantListeners(); for (var i = 0; i < refs.length; i++) { var ref = refs[i]; var value = model._get(ref.toSegments); var previous = model._get(ref.fromSegments); if (previous !== value) { - var setterModel = ref.model.pass(pass, true); + var setterModel = ref.model.pass(passed, true); setterModel._dereference = noopDereference; setterModel._set(ref.fromSegments, value); } diff --git a/lib/Model/refList.js b/lib/Model/refList.js index 1fabde347..882bf4afd 100644 --- a/lib/Model/refList.js +++ b/lib/Model/refList.js @@ -5,35 +5,38 @@ var EventListenerTree = require('./EventListenerTree'); Model.INITS.push(function(model) { var root = model.root; root._refLists = new RefLists(); - for (var type in Model.MUTATOR_EVENTS) { - addListener(root, type); - } + addListener(root, 'changeImmediate'); + addListener(root, 'loadImmediate'); + addListener(root, 'unloadImmediate'); + addListener(root, 'insertImmediate'); + addListener(root, 'removeImmediate'); + addListener(root, 'moveImmediate'); }); function addListener(model, type) { - model.on(type + 'Immediate', refListListener); - function refListListener(segments, eventArgs) { - var pass = eventArgs[eventArgs.length - 1]; + model.on(type, refListListener); + function refListListener(segments, event) { + var passed = event.passed; // Check for updates on or underneath paths var refLists = model._refLists.fromMap.getAffectedListeners(segments); for (var i = 0; i < refLists.length; i++) { var refList = refLists[i]; - if (pass.$refList !== refList) { - patchFromEvent(type, segments, eventArgs, refList); + if (passed.$refList !== refList) { + patchFromEvent(segments, event, refList); } }; var refLists = model._refLists.toListeners.getAffectedListeners(segments); for (var i = 0; i < refLists.length; i++) { var refList = refLists[i]; - if (pass.$refList !== refList) { - patchToEvent(type, segments, eventArgs, refList); + if (passed.$refList !== refList) { + patchToEvent(segments, event, refList); } }; var refLists = model._refLists.idsListeners.getAffectedListeners(segments); for (var i = 0; i < refLists.length; i++) { var refList = refLists[i]; - if (pass.$refList !== refList) { - patchIdsEvent(type, segments, eventArgs, refList); + if (passed.$refList !== refList) { + patchIdsEvent(segments, event, refList); } } } @@ -42,29 +45,26 @@ function addListener(model, type) { /** * @param {String} type * @param {Array} segments - * @param {Array} eventArgs + * @param {Event} event * @param {RefList} refList */ -function patchFromEvent(type, segments, eventArgs, refList) { +function patchFromEvent(segments, event, refList) { + var type = event.type; var fromLength = refList.fromSegments.length; var segmentsLength = segments.length; - var pass = eventArgs[eventArgs.length - 1]; - var model = refList.model.pass(pass, true); + var model = refList.model.pass(event.passed, true); // Mutation on the `from` output itself if (segmentsLength === fromLength) { if (type === 'insert') { - var index = eventArgs[0]; - var values = eventArgs[1]; - var ids = setNewToValues(model, refList, values); - model._insert(refList.idsSegments, index, ids); + var ids = setNewToValues(model, refList, event.values); + model._insert(refList.idsSegments, event.index, ids); return; } if (type === 'remove') { - var index = eventArgs[0]; - var howMany = eventArgs[1].length; - var ids = model._remove(refList.idsSegments, index, howMany); + var howMany = event.values.length; + var ids = model._remove(refList.idsSegments, event.index, howMany); // Delete the appropriate items underneath `to` if the `deleteRemoved` // option was set true if (refList.deleteRemoved) { @@ -77,16 +77,13 @@ function patchFromEvent(type, segments, eventArgs, refList) { } if (type === 'move') { - var from = eventArgs[0]; - var to = eventArgs[1]; - var howMany = eventArgs[2]; - model._move(refList.idsSegments, from, to, howMany); + model._move(refList.idsSegments, event.from, event.to, event.howMany); return; } // Change of the entire output var values = (type === 'change') ? - eventArgs[0] : model._get(refList.fromSegments); + event.value : model._get(refList.fromSegments); // Set ids to empty list if output is set to null if (!values) { model._set(refList.idsSegments, []); @@ -162,16 +159,16 @@ function updateIdForValue(model, refList, index, value) { model._set(outSegments, id); } -function patchToEvent(type, segments, eventArgs, refList) { +function patchToEvent(segments, event, refList) { + var type = event.type; var toLength = refList.toSegments.length; var segmentsLength = segments.length; - var pass = eventArgs[eventArgs.length - 1]; - var model = refList.model.pass(pass, true); + var model = refList.model.pass(event.passed, true); // Mutation on the `to` object itself if (segmentsLength === toLength) { if (type === 'insert') { - var values = eventArgs[1]; + var values = event.values; for (var i = 0; i < values.length; i++) { var value = values[i]; var indices = refList.indicesByItem(value); @@ -185,8 +182,8 @@ function patchToEvent(type, segments, eventArgs, refList) { } if (type === 'remove') { - var removeIndex = eventArgs[0]; - var values = eventArgs[1]; + var removeIndex = event.index; + var values = event.values; var howMany = values.length; for (var i = removeIndex, len = removeIndex + howMany; i < len; i++) { var indices = refList.indicesByItem(values[i]); @@ -220,13 +217,13 @@ function patchToEvent(type, segments, eventArgs, refList) { var indices = refList.indicesByItem(value); if (!indices) return; var remaining = segments.slice(toLength + 1); + var eventClone = event.clone(); + eventClone.passed = model._pass; for (var i = 0; i < indices.length; i++) { var index = indices[i]; var dereferenced = refList.fromSegments.concat(index, remaining); dereferenced = model._dereference(dereferenced, null, refList); - eventArgs = eventArgs.slice(); - eventArgs[eventArgs.length - 1] = model._pass; - model.emit(type, dereferenced, eventArgs); + model._emitMutation(dereferenced, eventClone); } return; } @@ -236,17 +233,8 @@ function patchToEvent(type, segments, eventArgs, refList) { // If changing the item itself, it will also have to be re-set on the // array created by the refList if (type === 'change' || type === 'load' || type === 'unload') { - var value, previous; - if (type === 'change') { - value = eventArgs[0]; - previous = eventArgs[1]; - } else if (type === 'load') { - value = eventArgs[0]; - previous = undefined; - } else if (type === 'unload') { - value = undefined; - previous = eventArgs[0]; - } + var value = event.value; + var previous = event.previous; var newIndices = refList.indicesByItem(value); var oldIndices = refList.indicesByItem(previous); if (!newIndices && !oldIndices) return; @@ -273,12 +261,12 @@ function patchToEvent(type, segments, eventArgs, refList) { if (type === 'insert' || type === 'remove' || type === 'move') { // Array mutations will have already been updated via an object // reference, so only re-emit + var eventClone = event.clone(); + eventClone.passed = model._pass; for (var i = 0; i < indices.length; i++) { var dereferenced = refList.fromSegments.concat(indices[i]); dereferenced = model._dereference(dereferenced, null, refList); - eventArgs = eventArgs.slice(); - eventArgs[eventArgs.length - 1] = model._pass; - model.emit(type, dereferenced, eventArgs); + model._emitMutation(dereferenced, eventClone); } } } @@ -291,39 +279,34 @@ function equivalentArrays(a, b) { return true; } -function patchIdsEvent(type, segments, eventArgs, refList) { +function patchIdsEvent(segments, event, refList) { + var type = event.type; var idsLength = refList.idsSegments.length; var segmentsLength = segments.length; - var pass = eventArgs[eventArgs.length - 1]; - var model = refList.model.pass(pass, true); + var model = refList.model.pass(event.passed, true); // An array mutation of the ids should be mirrored with a like change in // the output array if (segmentsLength === idsLength) { if (type === 'insert') { - var index = eventArgs[0]; - var inserted = eventArgs[1]; + var inserted = event.values; var values = []; for (var i = 0; i < inserted.length; i++) { var value = refList.itemById(inserted[i]); values.push(value); } - model._insert(refList.fromSegments, index, values); + model._insert(refList.fromSegments, event.index, values); return; } if (type === 'remove') { - var index = eventArgs[0]; - var howMany = eventArgs[1].length; - model._remove(refList.fromSegments, index, howMany); + var howMany = event.values.length; + model._remove(refList.fromSegments, event.index, howMany); return; } if (type === 'move') { - var from = eventArgs[0]; - var to = eventArgs[1]; - var howMany = eventArgs[2]; - model._move(refList.fromSegments, from, to, howMany); + model._move(refList.fromSegments, event.from, event.to, event.howMany); return; } } diff --git a/lib/Model/setDiff.js b/lib/Model/setDiff.js index 02ab1759f..4f8de66f4 100644 --- a/lib/Model/setDiff.js +++ b/lib/Model/setDiff.js @@ -1,6 +1,11 @@ var util = require('../util'); var Model = require('./Model'); var arrayDiff = require('arraydiff'); +var mutationEvents = require('./events').mutationEvents; +var ChangeEvent = mutationEvents.ChangeEvent; +var InsertEvent = mutationEvents.InsertEvent; +var RemoveEvent = mutationEvents.RemoveEvent; +var MoveEvent = mutationEvents.MoveEvent; Model.prototype.setDiff = function() { var subpath, value, cb; @@ -27,7 +32,8 @@ Model.prototype._setDiff = function(segments, value, cb) { return previous; } doc.set(docSegments, value, fnCb); - model.emit('change', segments, [value, previous, model._pass]); + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); return previous; } return this._mutate(segments, setDiff, cb); @@ -153,15 +159,18 @@ Model.prototype._applyArrayDiff = function(segments, diff, cb) { if (item instanceof arrayDiff.InsertDiff) { // Insert doc.insert(docSegments, item.index, item.values, group()); - model.emit('insert', segments, [item.index, item.values, model._pass]); + var event = new InsertEvent(item.index, item.values, model._pass); + model._emitMutation(segments, event); } else if (item instanceof arrayDiff.RemoveDiff) { // Remove var removed = doc.remove(docSegments, item.index, item.howMany, group()); - model.emit('remove', segments, [item.index, removed, model._pass]); + var event = new RemoveEvent(item.index, removed, model._pass); + model._emitMutation(segments, event); } else if (item instanceof arrayDiff.MoveDiff) { // Move var moved = doc.move(docSegments, item.from, item.to, item.howMany, group()); - model.emit('move', segments, [item.from, item.to, moved.length, model._pass]); + var event = new MoveEvent(item.from, item.to, moved.length, model._pass); + model._emitMutation(segments, event); } } } diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 2a005a6ec..573bdac38 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -2,6 +2,8 @@ var util = require('../util'); var Model = require('./Model'); var Query = require('./Query'); var CollectionCounter = require('./CollectionCounter'); +var mutationEvents = require('./events').mutationEvents; +var UnloadEvent = mutationEvents.UnloadEvent; Model.INITS.push(function(model, options) { model.root.fetchOnly = options.fetchOnly; @@ -188,7 +190,8 @@ Model.prototype._maybeUnloadDoc = function(collectionName, id) { // Remove doc from Share if (doc.shareDoc) doc.shareDoc.destroy(); - this.emit('unload', [collectionName, id], [previous, this._pass]); + var event = new UnloadEvent(previous, this._pass); + this._emitMutation([collectionName, id], event); }; Model.prototype._hasDocReferences = function(collectionName, id) { diff --git a/test/Model/setDiff.js b/test/Model/setDiff.js index a25509924..0abdc3d13 100644 --- a/test/Model/setDiff.js +++ b/test/Model/setDiff.js @@ -60,14 +60,11 @@ var Model = require('../../lib/Model'); it('emits an event when changing value', function(done) { var model = new Model(); - model.on('all', function(segments, eventArgs) { - var type = eventArgs[0]; - var value = eventArgs[1]; - var previous = eventArgs[2]; + model.on('all', function(segments, event) { expect(segments).eql(['_page', 'color']); - expect(type).equal('change'); - expect(value).equal('green'); - expect(previous).equal(undefined); + expect(event.type).equal('change'); + expect(event.value).equal('green'); + expect(event.previous).equal(undefined); done(); }); model[method]('_page.color', 'green'); @@ -89,14 +86,11 @@ describe('setDiff', function() { it('emits an event when an object is set to an equivalent object', function(done) { var model = new Model(); model.set('_page.color', {name: 'green'}); - model.on('all', function(segments, eventArgs) { - var type = eventArgs[0]; - var value = eventArgs[1]; - var previous = eventArgs[2]; + model.on('all', function(segments, event) { expect(segments).eql(['_page', 'color']); - expect(type).equal('change'); - expect(value).eql({name: 'green'}); - expect(previous).eql({name: 'green'}); + expect(event.type).equal('change'); + expect(event.value).eql({name: 'green'}); + expect(event.previous).eql({name: 'green'}); done(); }); model.setDiff('_page.color', {name: 'green'}); @@ -105,14 +99,11 @@ describe('setDiff', function() { it('emits an event when an array is set to an equivalent array', function(done) { var model = new Model(); model.set('_page.list', [2, 3, 4]); - model.on('all', function(segments, eventArgs) { - var type = eventArgs[0]; - var value = eventArgs[1]; - var previous = eventArgs[2]; + model.on('all', function(segments, event) { expect(segments).eql(['_page', 'list']); - expect(type).equal('change'); - expect(value).eql([2, 3, 4]); - expect(previous).eql([2, 3, 4]); + expect(event.type).equal('change'); + expect(event.value).eql([2, 3, 4]); + expect(event.previous).eql([2, 3, 4]); done(); }); model.setDiff('_page.list', [2, 3, 4]); @@ -163,14 +154,11 @@ describe('setDiffDeep', function() { it('adds items to an array', function(done) { var model = new Model(); model.set('_page.items', [4]); - model.on('all', function(segments, eventArgs) { - var type = eventArgs[0]; - var index = eventArgs[1]; - var values = eventArgs[2]; + model.on('all', function(segments, event) { expect(segments).eql(['_page', 'items']); - expect(type).equal('insert'); - expect(values).eql([2, 3]); - expect(index).eql(0); + expect(event.type).equal('insert'); + expect(event.values).eql([2, 3]); + expect(event.index).eql(0); done(); }); model.setDiffDeep('_page.items', [2, 3, 4]); @@ -179,14 +167,11 @@ describe('setDiffDeep', function() { it('adds items to an array in an object', function(done) { var model = new Model(); model.set('_page.lists', {a: [4]}); - model.on('all', function(segments, eventArgs) { - var type = eventArgs[0]; - var index = eventArgs[1]; - var values = eventArgs[2]; + model.on('all', function(segments, event) { expect(segments).eql(['_page', 'lists', 'a']); - expect(type).equal('insert'); - expect(values).eql([2, 3]); - expect(index).eql(0); + expect(event.type).equal('insert'); + expect(event.values).eql([2, 3]); + expect(event.index).eql(0); done(); }); model.setDiffDeep('_page.lists', {a: [2, 3, 4]}); @@ -195,14 +180,11 @@ describe('setDiffDeep', function() { it('emits a delete event when a key is removed from an object', function(done) { var model = new Model(); model.set('_page.color', {hex: '#0f0', name: 'green'}); - model.on('all', function(segments, eventArgs) { - var type = eventArgs[0]; - var value = eventArgs[1]; - var previous = eventArgs[2]; + model.on('all', function(segments, event) { expect(segments).eql(['_page', 'color', 'hex']); - expect(type).equal('change'); - expect(value).equal(undefined); - expect(previous).equal('#0f0'); + expect(event.type).equal('change'); + expect(event.value).equal(undefined); + expect(event.previous).equal('#0f0'); done(); }); model.setDiffDeep('_page.color', {name: 'green'}); @@ -225,15 +207,12 @@ describe('setArrayDiff', function() { var model = new Model(); model.set('_page.list', [{a: 2}, {c: 3}, {b: 4}]); var expectedEvents = ['remove', 'insert']; - model.on('all', function(segments, eventArgs) { + model.on('all', function(segments, event) { expect(segments).eql(['_page', 'list']); - var type = eventArgs[0]; - var index = eventArgs[1]; - var values = eventArgs[2]; var expected = expectedEvents.shift(); - expect(type).equal(expected); - expect(values).eql([{a: 2}, {c: 3}, {b: 4}]); - expect(index).eql(0); + expect(event.type).equal(expected); + expect(event.values).eql([{a: 2}, {c: 3}, {b: 4}]); + expect(event.index).eql(0); if (expectedEvents.length === 0) done(); }); model.setArrayDiff('_page.list', [{a: 2}, {c: 3}, {b: 4}]); From 5d684740463c80457fefd0b55c788e073d560121 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 10 Sep 2019 11:48:16 -0700 Subject: [PATCH 215/479] remove listeners based on eventContext in model.destroy() --- lib/Model/collections.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Model/collections.js b/lib/Model/collections.js index 8681ffd73..bf1cb2e59 100644 --- a/lib/Model/collections.js +++ b/lib/Model/collections.js @@ -76,10 +76,13 @@ Model.prototype.destroy = function(subpath) { var segments = this._splitPath(subpath); // Silently remove all types of listeners within subpath var silentModel = this.silent(); - silentModel.removeAllListeners(null, subpath); + silentModel._removeAllListeners(null, segments); silentModel._removeAllRefs(segments); silentModel._stopAll(segments); silentModel._removeAllFilters(segments); + // Remove listeners created within the model's eventContext and remove the + // reference to the eventContext + silentModel.removeContextListeners(); // Silently remove all model data within subpath if (segments.length === 0) { this.root.collections = new ModelCollections(); From b01719ebea1e3ea8c6c2d93b7a86273102be51af Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 10 Sep 2019 11:49:35 -0700 Subject: [PATCH 216/479] add tests that queries remain unique by params and context for correct reference counting --- test/Model/query.js | 64 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/test/Model/query.js b/test/Model/query.js index 33ae2938d..e16f6429a 100644 --- a/test/Model/query.js +++ b/test/Model/query.js @@ -40,4 +40,68 @@ describe('query', function() { expect(query.idMap).to.only.have.keys(['a', 'b', 'c']); }); }); + + describe('instantiation', function() { + it('returns same instance when params are equivalent', function() { + var model = new Model(); + var query1 = model.query('foo', {value: 1}, {db: 'other'}); + var query2 = model.query('foo', {value: 1}, {db: 'other'}); + expect(query1).equal(query2); + }); + it('returns same instance when context and params are equivalent', function() { + var model = new Model(); + var query1 = model.context('box').query('foo', {}); + var query2 = model.context('box').query('foo', {}); + expect(query1).equal(query2); + }); + it('creates a unique query instance per collection name', function() { + var model = new Model(); + var query1 = model.query('foo', {}); + var query2 = model.query('bar', {}); + expect(query1).not.equal(query2); + }); + it('creates a unique query instance per expression', function() { + var model = new Model(); + var query1 = model.query('foo', {value: 1}); + var query2 = model.query('foo', {value: 2}); + expect(query1).not.equal(query2); + }); + it('creates a unique query instance per options', function() { + var model = new Model(); + var query1 = model.query('foo', {}, {db: 'default'}); + var query2 = model.query('foo', {}, {db: 'other'}); + expect(query1).not.equal(query2); + }); + it('creates a unique query instance per context', function() { + var model = new Model(); + var query1 = model.query('foo', {}); + var query2 = model.context('box').query('foo', {}); + expect(query1).not.equal(query2); + }); + }); + + describe('reference counting', function() { + it('fetch uses the root model context', function(done) { + var backend = racer.createBackend(); + var model = backend.createModel(); + var query = model.query('foo', {}); + query.fetch(function(err) { + if (err) return done(err); + expect(model._contexts.root.fetchedQueries[query.hash]).equal(1); + done(); + }); + }); + it('fetch of same query in different context uses the specified model context', function(done) { + var backend = racer.createBackend(); + var model = backend.createModel(); + model.query('foo', {}); + // Same query params in different context: + var query = model.context('box').query('foo', {}); + query.fetch(function(err) { + if (err) return done(err); + expect(model._contexts.box.fetchedQueries[query.hash]).equal(1); + done(); + }); + }); + }); }); From d49458312bc83f3dae36a16d49807db365f76b8a Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 10 Sep 2019 16:43:25 -0700 Subject: [PATCH 217/479] fix bug in query reference counting by including context in hash --- lib/Model/Query.js | 108 ++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 7b7774ab6..6b458b196 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -15,25 +15,23 @@ Model.prototype.query = function(collectionName, expression, options) { if (typeof options === 'string') { options = {db: options}; } - return this._getOrCreateQuery(collectionName, expression, options, Query); + expression = this.sanitizeQuery(expression); + return this._getOrCreateQuery(collectionName, expression, options); }; /** - * If an existing query is present with the same `collectionName`, `expression`, - * and `options`, then returns the existing query; otherwise, constructs and - * returns a new query using `QueryConstructor`. + * If an existing query is present with the same context, `collectionName`, + * `expression`, and `options`, then returns the existing query * * @param {string} collectionName * @param {*} expression * @param {*} options - * @param {new (model: Model, collectionName: string, expression: any, options: any) => Query} QueryConstructor - - * constructor function for a Query, to create one if not already present on this model */ -Model.prototype._getOrCreateQuery = function(collectionName, expression, options, QueryConstructor) { - expression = this.sanitizeQuery(expression); - var query = this.root._queries.get(collectionName, expression, options); +Model.prototype._getOrCreateQuery = function(collectionName, expression, options) { + var contextId = this._context.id; + var query = this.root._queries.get(contextId, collectionName, expression, options); if (query) return query; - query = new QueryConstructor(this, collectionName, expression, options); + query = new Query(this, collectionName, expression, options); this.root._queries.add(query); return query; }; @@ -62,20 +60,22 @@ Model.prototype.sanitizeQuery = function(expression) { // Called during initialization of the bundle on page load. Model.prototype._initQueries = function(items) { - var queries = this.root._queries; for (var i = 0; i < items.length; i++) { var item = items[i]; - var counts = item[0]; + var countsList = item[0]; var collectionName = item[1]; var expression = item[2]; var results = item[3] || []; var options = item[4]; var extra = item[5]; - var query = this.root._queries.get(collectionName, expression, options); - if (!query) { - query = new Query(this, collectionName, expression, options); - queries.add(query); - } + + var counts = countsList[0]; + var subscribed = counts[0] || 0; + var fetched = counts[1] || 0; + var contextId = counts[2]; + + var model = (contextId) ? this.context(contextId) : this; + var query = model._getOrCreateQuery(collectionName, expression, options); query._setExtra(extra); @@ -97,19 +97,12 @@ Model.prototype._initQueries = function(items) { query._addMapIds(ids); this._set(query.idsSegments, ids); - for (var countIndex = 0; countIndex < counts.length; countIndex++) { - var count = counts[countIndex]; - var subscribed = count[0] || 0; - var fetched = count[1] || 0; - var contextId = count[2]; - if (contextId) query.model.setContext(contextId); - while (subscribed--) { - query.subscribe(); - } - query.fetchCount += fetched; - while (fetched--) { - query.model._context.fetchQuery(query); - } + while (subscribed--) { + query.subscribe(); + } + query.fetchCount += fetched; + while (fetched--) { + query.context.fetchQuery(query); } } }; @@ -128,8 +121,8 @@ Queries.prototype.remove = function(query) { delete this.map[query.hash]; this.collectionMap.del(query.collectionName, query.hash); }; -Queries.prototype.get = function(collectionName, expression, options) { - var hash = queryHash(collectionName, expression, options); +Queries.prototype.get = function(contextId, collectionName, expression, options) { + var hash = queryHash(contextId, collectionName, expression, options); return this.map[hash]; }; Queries.prototype.toJSON = function() { @@ -144,11 +137,14 @@ Queries.prototype.toJSON = function() { }; function Query(model, collectionName, expression, options) { - this.model = model.pass({$query: this}); + // Note that a new childModel based on the root scope is created. Only the + // context from the passed in model has an effect + this.model = model.root.pass({$query: this}); + this.context = model._context; this.collectionName = collectionName; this.expression = expression; this.options = options; - this.hash = queryHash(collectionName, expression, options); + this.hash = queryHash(this.context.id, collectionName, expression, options); this.segments = ['$queries', this.hash]; this.idsSegments = ['$queries', this.hash, 'ids']; this.extraSegments = ['$queries', this.hash, 'extra']; @@ -194,7 +190,7 @@ Query.prototype.destroy = function() { Query.prototype.fetch = function(cb) { cb = this.model.wrapCallback(cb); - this.model._context.fetchQuery(this); + this.context.fetchQuery(this); this.fetchCount++; @@ -218,7 +214,7 @@ Query.prototype.fetch = function(cb) { Query.prototype.subscribe = function(cb) { cb = this.model.wrapCallback(cb); - this.model._context.subscribeQuery(this); + this.context.subscribeQuery(this); if (this.subscribeCount++) { var query = this; @@ -380,7 +376,7 @@ Query.prototype._flushSubscribeCallbacks = function(err, cb) { Query.prototype.unfetch = function(cb) { cb = this.model.wrapCallback(cb); - this.model._context.unfetchQuery(this); + this.context.unfetchQuery(this); // No effect if the query is not currently fetched if (!this.fetchCount) { @@ -406,7 +402,7 @@ Query.prototype.unfetch = function(cb) { Query.prototype.unsubscribe = function(cb) { cb = this.model.wrapCallback(cb); - this.model._context.unsubscribeQuery(this); + this.context.unsubscribeQuery(this); // No effect if the query is not currently subscribed if (!this.subscribeCount) { @@ -490,13 +486,13 @@ Query.prototype.ref = function(from) { Query.prototype.refIds = function(from) { var idsPath = this.idsSegments.join('.'); - return this.model.root.ref(from, idsPath); + return this.model.ref(from, idsPath); }; Query.prototype.refExtra = function(from, relPath) { var extraPath = this.extraSegments.join('.'); if (relPath) extraPath += '.' + relPath; - return this.model.root.ref(from, extraPath); + return this.model.ref(from, extraPath); }; Query.prototype.serialize = function() { @@ -524,24 +520,18 @@ Query.prototype.serialize = function() { } } } - var counts = []; - var contexts = this.model.root._contexts; - for (var key in contexts) { - var context = contexts[key]; - var subscribed = context.subscribedQueries[this.hash] || 0; - var fetched = context.fetchedQueries[this.hash] || 0; - if (subscribed || fetched) { - if (key !== 'root') { - counts.push([subscribed, fetched, key]); - } else if (fetched) { - counts.push([subscribed, fetched]); - } else { - counts.push([subscribed]); - } - } - } + var subscribed = this.context.subscribedQueries[this.hash] || 0; + var fetched = this.context.fetchedQueries[this.hash] || 0; + var contextId = this.context.id; + var counts = (contextId === 'root') ? + (fetched === 0) ? [subscribed] : [subscribed, fetched] : + [subscribed, fetched, contextId]; + // TODO: change counts to a less obtuse format. We don't want to change + // the serialization format unless it is known that clients are not + // depending on the old format, so this should be done in a major version + var countsList = [counts]; var serialized = [ - counts, + countsList, this.collectionName, this.expression, results, @@ -554,8 +544,8 @@ Query.prototype.serialize = function() { return serialized; }; -function queryHash(collectionName, expression, options) { - var args = [collectionName, expression, options]; +function queryHash(contextId, collectionName, expression, options) { + var args = [contextId, collectionName, expression, options]; return JSON.stringify(args).replace(/\./g, '|'); } From 5a80a4a93ab31455890fe11e4171d048c9afba89 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 11 Sep 2019 09:45:43 -0700 Subject: [PATCH 218/479] Don't create child model per ref internally & use only segments internally --- lib/Model/ref.js | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/Model/ref.js b/lib/Model/ref.js index 81e7b88be..a4d0fb67b 100644 --- a/lib/Model/ref.js +++ b/lib/Model/ref.js @@ -126,7 +126,7 @@ function addListener(model, type, fn) { if (model._get(dereferenced) === model._get(segments)) { model._emitMutation(dereferenced, event); } else { - var setterModel = ref.model.pass(passed, true); + var setterModel = model.pass(passed); setterModel._dereference = noopDereference; fn(setterModel, dereferenced, event, segments); } @@ -140,7 +140,7 @@ function addListener(model, type, fn) { var value = model._get(ref.toSegments); var previous = model._get(ref.fromSegments); if (previous !== value) { - var setterModel = ref.model.pass(passed, true); + var setterModel = model.pass(passed); setterModel._dereference = noopDereference; setterModel._set(ref.fromSegments, value); } @@ -173,19 +173,25 @@ Model.prototype.ref = function() { var toPath = this.path(to); // Make ref to reffable object, such as query or filter if (!toPath) return to.ref(fromPath); - var ref = new Ref(this.root, fromPath, toPath, options); - if (ref.fromSegments.length < 2) { + var fromSegments = fromPath.split('.'); + var toSegments = toPath.split('.'); + if (fromSegments.length < 2) { throw new Error('ref must be performed under a collection ' + 'and document id. Invalid path: ' + fromPath); } - this.root._refs.remove(ref.fromSegments); - this.root._refLists.remove(ref.fromSegments); - var value = this.get(to); - ref.model._set(ref.fromSegments, value); - this.root._refs.add(ref); + this._ref(fromSegments, toSegments, options); return this.scope(fromPath); }; +Model.prototype._ref = function(fromSegments, toSegments, options) { + this.root._refs.remove(fromSegments); + this.root._refLists.remove(fromSegments); + var value = this._get(toSegments); + this._set(fromSegments, value); + var ref = new Ref(fromSegments, toSegments, options); + this.root._refs.add(ref); +}; + Model.prototype.removeRef = function(subpath) { var segments = this._splitPath(subpath); this._removeRef(segments); @@ -251,12 +257,9 @@ function noopDereference(segments) { return segments; } -function Ref(model, from, to, options) { - this.model = model && model.pass({$ref: this}); - this.from = from; - this.to = to; - this.fromSegments = from.split('.'); - this.toSegments = to.split('.'); +function Ref(fromSegments, toSegments, options) { + this.fromSegments = fromSegments; + this.toSegments = toSegments; this.updateIndices = options && options.updateIndices; } @@ -289,7 +292,9 @@ Refs.prototype.removeAll = function(segments) { Refs.prototype.toJSON = function() { var out = []; this.fromMap.forEach(function(ref) { - out.push([ref.from, ref.to]); + var from = ref.fromSegments.join('.'); + var to = ref.toSegments.join('.'); + out.push([from, to]); }); return out; }; From 264278dc90a2897cc848b8290a9c55336d88cd54 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 11 Sep 2019 09:51:57 -0700 Subject: [PATCH 219/479] always set model._silent to boolean value --- lib/Model/events.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index 0d943219a..0cf1d1e1c 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -46,7 +46,7 @@ Model.INITS.push(function(model) { root._emittingMutation = false; root._mutationEventQueue = null; root._pass = new Passed(); - root._silent = null; + root._silent = false; root._eventContextListeners = {}; root._eventContext = null; }); @@ -232,7 +232,7 @@ Model.prototype.pass = (Object.assign) ? */ Model.prototype.silent = function(value) { var model = this._child(); - model._silent = (value == null) ? true : value; + model._silent = (value == null) ? true : !!value; return model; }; From 9c44cf5f68fd0d6b895483db062f48cd4efb04bf Mon Sep 17 00:00:00 2001 From: eve shum Date: Thu, 5 Dec 2019 15:19:15 -0800 Subject: [PATCH 220/479] fix error where shareDoc.version can be null --- lib/Model/bundle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/bundle.js b/lib/Model/bundle.js index 3381bf000..a2c4bb20b 100644 --- a/lib/Model/bundle.js +++ b/lib/Model/bundle.js @@ -59,7 +59,7 @@ function serializeCollections(root) { var doc = collection.docs[id]; var shareDoc = doc.shareDoc; var snapshot; - if (shareDoc) { + if (shareDoc && shareDoc.version !== null) { snapshot = { v: shareDoc.version, data: shareDoc.data From abc24fee7c242ff012a09d6ac6cf514b2776d2dd Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 5 Dec 2019 16:37:02 -0800 Subject: [PATCH 221/479] Add test for not bundling Share docs with null versions --- test/Model/bundle.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/Model/bundle.js diff --git a/test/Model/bundle.js b/test/Model/bundle.js new file mode 100644 index 000000000..13ac0af3e --- /dev/null +++ b/test/Model/bundle.js @@ -0,0 +1,37 @@ +var expect = require('../util').expect; +var racer = require('../../lib/index'); + +describe('bundle', function() { + it('does not serialize Share docs with null versions', function(done) { + var backend = racer.createBackend(); + var setupModel = backend.createModel(); + setupModel.add('dogs', {id: 'coco', name: 'Coco'}); + setupModel.whenNothingPending(function() { + var model1 = backend.createModel({fetchOnly: true}); + + // This creates a Share client Doc on the connection, with null version and data. + model1.connection.get('dogs', 'fido'); + // Fetching a non-existent id results in a Share Doc with version 0 and undefined data. + model1.fetch('dogs.spot'); + // This doc should be properly fetched and bundled. + model1.fetch('dogs.coco'); + + model1.whenNothingPending(function(err) { + if (err) return done(err); + model1.bundle(function(err, bundleData) { + if (err) return done(err); + // Simulate serialization of bundle data between server and client. + bundleData = JSON.parse(JSON.stringify(bundleData)); + + var model2 = backend.createModel(); + model2.unbundle(bundleData); + expect(model2.get('dogs.fido')).to.equal(undefined); + expect(model2.get('dogs.spot')).to.equal(undefined); + expect(model2.get('dogs.coco')).to.eql({id: 'coco', name: 'Coco'}); + done(); + }); + }); + }); + }); +}); + From 8ffe3329c30e9c4e6974b3910a381fdd0b2b49ad Mon Sep 17 00:00:00 2001 From: eve shum Date: Fri, 6 Dec 2019 11:10:40 -0800 Subject: [PATCH 222/479] make changes to check for null version and null type --- lib/Model/bundle.js | 18 +++++++++++------- test/Model/bundle.js | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/Model/bundle.js b/lib/Model/bundle.js index a2c4bb20b..1d5985fd1 100644 --- a/lib/Model/bundle.js +++ b/lib/Model/bundle.js @@ -59,13 +59,17 @@ function serializeCollections(root) { var doc = collection.docs[id]; var shareDoc = doc.shareDoc; var snapshot; - if (shareDoc && shareDoc.version !== null) { - snapshot = { - v: shareDoc.version, - data: shareDoc.data - }; - if (shareDoc.type !== defaultType) { - snapshot.type = doc.shareDoc.type && doc.shareDoc.type.name; + if (shareDoc) { + if (shareDoc.type == null && shareDoc.version == null) { + snapshot = undefined; + } else { + snapshot = { + v: shareDoc.version, + data: shareDoc.data + }; + if (shareDoc.type !== defaultType) { + snapshot.type = doc.shareDoc.type && doc.shareDoc.type.name; + } } } else { snapshot = doc.data; diff --git a/test/Model/bundle.js b/test/Model/bundle.js index 13ac0af3e..6041058b6 100644 --- a/test/Model/bundle.js +++ b/test/Model/bundle.js @@ -2,7 +2,7 @@ var expect = require('../util').expect; var racer = require('../../lib/index'); describe('bundle', function() { - it('does not serialize Share docs with null versions', function(done) { + it('does not serialize Share docs with null versions and null type', function(done) { var backend = racer.createBackend(); var setupModel = backend.createModel(); setupModel.add('dogs', {id: 'coco', name: 'Coco'}); From 62a0774414e055eaaef51e3f85d2f9941b3e8439 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 6 Dec 2019 13:50:07 -0800 Subject: [PATCH 223/479] 0.9.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f4a1395f3..ee1e98d5b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.9", + "version": "0.9.10", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From e86374952356956d59e6136f0a09a72630efe5ac Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 16 Dec 2019 14:34:38 -0800 Subject: [PATCH 224/479] revert changes to internal _getOrCreateQuery method to avoid unneccessary breakage --- lib/Model/Query.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 6b458b196..36b60c85b 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -15,8 +15,7 @@ Model.prototype.query = function(collectionName, expression, options) { if (typeof options === 'string') { options = {db: options}; } - expression = this.sanitizeQuery(expression); - return this._getOrCreateQuery(collectionName, expression, options); + return this._getOrCreateQuery(collectionName, expression, options, Query); }; /** @@ -27,11 +26,12 @@ Model.prototype.query = function(collectionName, expression, options) { * @param {*} expression * @param {*} options */ -Model.prototype._getOrCreateQuery = function(collectionName, expression, options) { +Model.prototype._getOrCreateQuery = function(collectionName, expression, options, QueryConstructor) { + expression = this.sanitizeQuery(expression); var contextId = this._context.id; var query = this.root._queries.get(contextId, collectionName, expression, options); if (query) return query; - query = new Query(this, collectionName, expression, options); + query = new QueryConstructor(this, collectionName, expression, options); this.root._queries.add(query); return query; }; @@ -75,7 +75,7 @@ Model.prototype._initQueries = function(items) { var contextId = counts[2]; var model = (contextId) ? this.context(contextId) : this; - var query = model._getOrCreateQuery(collectionName, expression, options); + var query = model._getOrCreateQuery(collectionName, expression, options, Query); query._setExtra(extra); From 65520305e59c8e143f3c4139db468a162bed51f9 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 16 Dec 2019 14:37:00 -0800 Subject: [PATCH 225/479] also revert comments --- lib/Model/Query.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 36b60c85b..b2541b1f3 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -20,11 +20,14 @@ Model.prototype.query = function(collectionName, expression, options) { /** * If an existing query is present with the same context, `collectionName`, - * `expression`, and `options`, then returns the existing query + * `expression`, and `options`, then returns the existing query; otherwise, + * constructs and returns a new query using `QueryConstructor`. * * @param {string} collectionName * @param {*} expression * @param {*} options + * @param {new (model: Model, collectionName: string, expression: any, options: any) => Query} QueryConstructor - + * constructor function for a Query, to create one if not already present on this model */ Model.prototype._getOrCreateQuery = function(collectionName, expression, options, QueryConstructor) { expression = this.sanitizeQuery(expression); From bc234445ffdc2298125044cd3cae9f48ca69b5d6 Mon Sep 17 00:00:00 2001 From: Melissa Skevington Date: Fri, 28 Feb 2020 14:51:42 -0800 Subject: [PATCH 226/479] Prevent using saved query if new expression doesn't match hash If a query expression is altered, the hash remains the same, which means new queries matching the hash will use a query expression that has been altered, resulting in unexpected results. Adds extra check to ensure expressions match, otherwise a new query will be added. --- lib/Model/Query.js | 7 ++++++- test/Model/query.js | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 600f5bce8..e76a1c193 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -137,7 +137,12 @@ Queries.prototype.remove = function(query) { }; Queries.prototype.get = function(collectionName, expression, options) { var hash = queryHash(collectionName, expression, options); - return this.map[hash]; + if (!this.map[hash]) return; + // Prevent getting saved query if expressions don't match + var existingExpression = this.map[hash]['expression']; + if (util.deepEqual(expression, existingExpression)) { + return this.map[hash]; + } }; Queries.prototype.toJSON = function() { var out = []; diff --git a/test/Model/query.js b/test/Model/query.js index 33ae2938d..e979e3f68 100644 --- a/test/Model/query.js +++ b/test/Model/query.js @@ -16,6 +16,23 @@ describe('query', function() { }); }); + describe('Queries', function() { + beforeEach('create in-memory backend and model', function() { + this.backend = racer.createBackend(); + this.model = this.backend.createModel(); + }); + it("creates new query if expression doesn't match saved expression", function() { + var query = this.model.query('myCollection', {arrayKey: []}); + query.fetch(); + // push value directly to query expression + query.expression.arrayKey.push('foo') + // Ensure query with same hash as original query uses expected expression + var newQuery = this.model.query('myCollection', {arrayKey: []}); + newQuery.fetch(); + expect(newQuery.expression.arrayKey).to.have.length(0); + }) + }); + describe('idMap', function() { beforeEach('create in-memory backend and model', function() { this.backend = racer.createBackend(); From 094a23397ff7e05c06875e93456582b7ab1c53fb Mon Sep 17 00:00:00 2001 From: Melissa Skevington Date: Fri, 28 Feb 2020 16:25:40 -0800 Subject: [PATCH 227/479] Deep copy expression on Query construction --- lib/Model/Query.js | 9 ++------- test/Model/query.js | 15 ++++++--------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index e76a1c193..6ca4b278e 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -137,12 +137,7 @@ Queries.prototype.remove = function(query) { }; Queries.prototype.get = function(collectionName, expression, options) { var hash = queryHash(collectionName, expression, options); - if (!this.map[hash]) return; - // Prevent getting saved query if expressions don't match - var existingExpression = this.map[hash]['expression']; - if (util.deepEqual(expression, existingExpression)) { - return this.map[hash]; - } + return this.map[hash]; }; Queries.prototype.toJSON = function() { var out = []; @@ -158,7 +153,7 @@ Queries.prototype.toJSON = function() { function Query(model, collectionName, expression, options) { this.model = model.pass({$query: this}); this.collectionName = collectionName; - this.expression = expression; + this.expression = util.deepCopy(expression); this.options = options; this.hash = queryHash(collectionName, expression, options); this.segments = ['$queries', this.hash]; diff --git a/test/Model/query.js b/test/Model/query.js index e979e3f68..df9961cbb 100644 --- a/test/Model/query.js +++ b/test/Model/query.js @@ -16,20 +16,17 @@ describe('query', function() { }); }); - describe('Queries', function() { + describe('Query', function() { beforeEach('create in-memory backend and model', function() { this.backend = racer.createBackend(); this.model = this.backend.createModel(); }); - it("creates new query if expression doesn't match saved expression", function() { - var query = this.model.query('myCollection', {arrayKey: []}); + it("Uses deep copy of query expression in Query constructor", function() { + var expression = {arrayKey: []}; + var query = this.model.query('myCollection', expression); query.fetch(); - // push value directly to query expression - query.expression.arrayKey.push('foo') - // Ensure query with same hash as original query uses expected expression - var newQuery = this.model.query('myCollection', {arrayKey: []}); - newQuery.fetch(); - expect(newQuery.expression.arrayKey).to.have.length(0); + expression.arrayKey.push('foo') + expect(query.expression.arrayKey).to.have.length(0); }) }); From a02856c5019d0f179efe3c30720d3a0f8c63cdf0 Mon Sep 17 00:00:00 2001 From: Melissa Skevington Date: Fri, 28 Feb 2020 16:40:56 -0800 Subject: [PATCH 228/479] linting --- test/Model/query.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Model/query.js b/test/Model/query.js index df9961cbb..e53bca11b 100644 --- a/test/Model/query.js +++ b/test/Model/query.js @@ -21,13 +21,13 @@ describe('query', function() { this.backend = racer.createBackend(); this.model = this.backend.createModel(); }); - it("Uses deep copy of query expression in Query constructor", function() { + it('Uses deep copy of query expression in Query constructor', function() { var expression = {arrayKey: []}; var query = this.model.query('myCollection', expression); query.fetch(); - expression.arrayKey.push('foo') + expression.arrayKey.push('foo'); expect(query.expression.arrayKey).to.have.length(0); - }) + }); }); describe('idMap', function() { From f285fe3f1443052ade7b7b530b8674a882274d02 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 28 Feb 2020 16:47:42 -0800 Subject: [PATCH 229/479] 0.9.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee1e98d5b..c74b139e1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.10", + "version": "0.9.11", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From a320772a758495e2c8ed41f396e0c9b8985f1f7d Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 20 Mar 2020 10:51:13 -0700 Subject: [PATCH 230/479] Fix regression in doc unfetch with pending ops, introduced in a637476 --- lib/Model/subscriptions.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index f51440df3..6b0049176 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -168,6 +168,7 @@ Model.prototype.unsubscribeDoc = function(collectionName, id, cb) { // Removes the document from the local model if the model no longer has any // remaining fetches or subscribes via a query or direct loading Model.prototype._maybeUnloadDoc = function(collectionName, id) { + var model = this; var doc = this.getDoc(collectionName, id); if (!doc) return; @@ -179,16 +180,22 @@ Model.prototype._maybeUnloadDoc = function(collectionName, id) { // Thus, if we remove the doc from Racer's model but don't remove it from // ShareDB, we can end up with an inconsistent state, with the data existing // in ShareDB not reflected in the racer model data - if (doc.shareDoc && doc.shareDoc.hasPending()) return; + if (doc.shareDoc && doc.shareDoc.hasPending()) { + doc.shareDoc.whenNothingPending(unloadDoc); + } else { + unloadDoc(); + } - var previous = doc.get(); + function unloadDoc() { + var previous = doc.get(); - // Remove doc from Racer - this.root.collections[collectionName].remove(id); - // Remove doc from Share - if (doc.shareDoc) doc.shareDoc.destroy(); + // Remove doc from Racer + model.root.collections[collectionName].remove(id); + // Remove doc from Share + if (doc.shareDoc) doc.shareDoc.destroy(); - this.emit('unload', [collectionName, id], [previous, this._pass]); + model.emit('unload', [collectionName, id], [previous, this._pass]); + } }; Model.prototype._hasDocReferences = function(collectionName, id) { From 5663570b0872cafb401658be9cda6856ce2f9a62 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 20 Mar 2020 12:02:28 -0700 Subject: [PATCH 231/479] Add test for doc unfetch regression introduced by a637476 --- test/Model/loading.js | 48 +++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/test/Model/loading.js b/test/Model/loading.js index af142a2dd..62ff7c089 100644 --- a/test/Model/loading.js +++ b/test/Model/loading.js @@ -2,19 +2,19 @@ var expect = require('../util').expect; var racer = require('../../lib/index'); describe('loading', function() { - describe('subscribe', function() { - beforeEach(function(done) { - this.backend = racer.createBackend(); - // Add a delay on all messages to help catch race issues - var delay = 5; - this.backend.use('receive', function(request, next) { - delay++; - setTimeout(next, delay); - }); - this.model = this.backend.createModel(); - this.model.connection.on('connected', done); + beforeEach(function(done) { + this.backend = racer.createBackend(); + // Add a delay on all messages to help catch race issues + var delay = 5; + this.backend.use('receive', function(request, next) { + delay++; + setTimeout(next, delay); }); + this.model = this.backend.createModel(); + this.model.connection.on('connected', done); + }); + describe('subscribe', function() { it('calls back simultaneous subscribes to the same document', function(done) { var doc = this.model.connection.get('colors', 'green'); expect(doc.version).equal(null); @@ -45,4 +45,30 @@ describe('loading', function() { }); }); }); + + describe('unfetch', function() { + it('unloads doc after Share doc has nothing pending', function() { + var setupModel = this.backend.createModel(); + var model = this.model; + setupModel.add('colors', {id: 'green', hex: '00ff00'}, function(err) { + if (err) return done(err); + + model.fetch('colors.green', function(err) { + if (err) return done(err); + expect(model.get('colors.green.hex')).to.equal('00ff00'); + // Queue up a pending op. + model.set('colors.green.hex', '00ee00'); + // Unfetch. The pending op prevents the doc from immedialy being unloaded. + model.unfetch('colors.green'); + // Once there's nothing pending on the model/doc... + model.whenNothingPending(function() { + // Racer doc should be unloaded. + expect(model.get('colors.green')).to.equal(undefined); + // Share doc should be unloaded too. + expect(model.connection.getExisting('colors', 'green')).to.equal(undefined); + }); + }); + }); + }); + }); }); From 56dd54e1bd4c7dd4c4c898ff22a1aea20962ac80 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 20 Mar 2020 13:59:02 -0700 Subject: [PATCH 232/479] Make new doc-unload test be async as it should be --- test/Model/loading.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Model/loading.js b/test/Model/loading.js index 62ff7c089..c50420ad6 100644 --- a/test/Model/loading.js +++ b/test/Model/loading.js @@ -47,7 +47,7 @@ describe('loading', function() { }); describe('unfetch', function() { - it('unloads doc after Share doc has nothing pending', function() { + it('unloads doc after Share doc has nothing pending', function(done) { var setupModel = this.backend.createModel(); var model = this.model; setupModel.add('colors', {id: 'green', hex: '00ff00'}, function(err) { @@ -66,6 +66,7 @@ describe('loading', function() { expect(model.get('colors.green')).to.equal(undefined); // Share doc should be unloaded too. expect(model.connection.getExisting('colors', 'green')).to.equal(undefined); + done(); }); }); }); From b9efd2d8726293a780745972a0103817b259054d Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 20 Mar 2020 14:42:39 -0700 Subject: [PATCH 233/479] 0.9.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c74b139e1..cc6f4bc16 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.11", + "version": "0.9.12", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 1723bf463cd973892fad2fbba0055eead955c19b Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 20 Mar 2020 16:09:11 -0700 Subject: [PATCH 234/479] Fix reference to this in _maybeUnloadDoc inner function The inner function was introduced in introduced in a320772. I missed a `this`. This can cause an error like `TypeError: Cannot read property '$remote' of undefined` in listeners that are checking properties the `pass` object. --- lib/Model/subscriptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 6b0049176..57c8a6f67 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -194,7 +194,7 @@ Model.prototype._maybeUnloadDoc = function(collectionName, id) { // Remove doc from Share if (doc.shareDoc) doc.shareDoc.destroy(); - model.emit('unload', [collectionName, id], [previous, this._pass]); + model.emit('unload', [collectionName, id], [previous, model._pass]); } }; From bff13ac106eedd4605e279ae40786a5bc867cd3e Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 20 Mar 2020 16:13:25 -0700 Subject: [PATCH 235/479] 0.9.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc6f4bc16..07964a53e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.12", + "version": "0.9.13", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From c7be2470bcbb1d43d1df5d19c131678573f726a5 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Tue, 24 Mar 2020 10:29:36 -0700 Subject: [PATCH 236/479] Be more defensive for delayed doc unload introduced in a320772a --- lib/Model/subscriptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 57c8a6f67..6f9a7044c 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -190,7 +190,7 @@ Model.prototype._maybeUnloadDoc = function(collectionName, id) { var previous = doc.get(); // Remove doc from Racer - model.root.collections[collectionName].remove(id); + if (model.root.collections[collectionName]) model.root.collections[collectionName].remove(id); // Remove doc from Share if (doc.shareDoc) doc.shareDoc.destroy(); From fcc16e610f2db003dbf6d9120256cca616b4588e Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Tue, 24 Mar 2020 10:36:13 -0700 Subject: [PATCH 237/479] 0.9.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07964a53e..874949775 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.13", + "version": "0.9.14", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From c14610a2ff68a753272a8777a072c9dd15d4acdf Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 25 Mar 2020 13:02:59 -0700 Subject: [PATCH 238/479] Fix deferred unload (see a320772a) when unfetching then synchronously subscribing When a doc gets unfetched, #276 adds a Share Doc whenNothingPending listener to unload the doc later, instead of doing nothing, which was the cause of a memory leak introduced in #266. However, Racer keeps reference counts itself for fetches and subscriptions, so an immediate subscribe on the same Racer doc does not affect the Share doc at all, which means the doc would still get unloaded, erroneously. This fix instead retries the Racer _maybeUnloadDoc in that scenario, instead of doing an immediate unload. That guards against Racer getting new doc references while the Share Doc whenNotingPending is in progress. --- lib/Model/subscriptions.js | 11 ++++--- test/Model/loading.js | 64 +++++++++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 6f9a7044c..b963af195 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -181,12 +181,13 @@ Model.prototype._maybeUnloadDoc = function(collectionName, id) { // ShareDB, we can end up with an inconsistent state, with the data existing // in ShareDB not reflected in the racer model data if (doc.shareDoc && doc.shareDoc.hasPending()) { - doc.shareDoc.whenNothingPending(unloadDoc); + // If the Share doc still has pending activity, retry _maybeUnloadDoc once + // the pending activity is done. + doc.shareDoc.whenNothingPending(function() { + model._maybeUnloadDoc(collectionName, id); + }); } else { - unloadDoc(); - } - - function unloadDoc() { + // Otherwise, actually do the unload. var previous = doc.get(); // Remove doc from Racer diff --git a/test/Model/loading.js b/test/Model/loading.js index c50420ad6..6f6267f2b 100644 --- a/test/Model/loading.js +++ b/test/Model/loading.js @@ -46,28 +46,56 @@ describe('loading', function() { }); }); - describe('unfetch', function() { + describe('unfetch deferred unload', function() { + beforeEach(function(done) { + this.setupModel = this.backend.createModel(); + this.setupModel.add('colors', {id: 'green', hex: '00ff00'}, done); + }); + it('unloads doc after Share doc has nothing pending', function(done) { - var setupModel = this.backend.createModel(); var model = this.model; - setupModel.add('colors', {id: 'green', hex: '00ff00'}, function(err) { + model.fetch('colors.green', function(err) { if (err) return done(err); + expect(model.get('colors.green.hex')).to.equal('00ff00'); + // Queue up a pending op. + model.set('colors.green.hex', '00ee00'); + // Unfetch. This triggers the delayed _maybeUnloadDoc. + // The pending op causes the doc unload to be delayed. + model.unfetch('colors.green'); + // Once there's nothing pending on the model/doc... + model.whenNothingPending(function() { + // Racer doc should be unloaded. + expect(model.get('colors.green')).to.equal(undefined); + // Share doc should be unloaded too. + expect(model.connection.getExisting('colors', 'green')).to.equal(undefined); + done(); + }); + }); + }); - model.fetch('colors.green', function(err) { - if (err) return done(err); - expect(model.get('colors.green.hex')).to.equal('00ff00'); - // Queue up a pending op. - model.set('colors.green.hex', '00ee00'); - // Unfetch. The pending op prevents the doc from immedialy being unloaded. - model.unfetch('colors.green'); - // Once there's nothing pending on the model/doc... - model.whenNothingPending(function() { - // Racer doc should be unloaded. - expect(model.get('colors.green')).to.equal(undefined); - // Share doc should be unloaded too. - expect(model.connection.getExisting('colors', 'green')).to.equal(undefined); - done(); - }); + it('does not unload doc if a subscribe is issued in the meantime', function(done) { + var model = this.model; + // Racer keeps its own reference counts of doc fetches/subscribes - see `_hasDocReferences`. + model.fetch('colors.green', function(err) { + if (err) return done(err); + expect(model.get('colors.green.hex')).to.equal('00ff00'); + // Queue up a pending op. + model.set('colors.green.hex', '00ee00'); + // Unfetch. This triggers the delayed _maybeUnloadDoc. + // The pending op causes the doc unload to be delayed. + model.unfetch('colors.green'); + // Immediately subscribe to the same doc. + // This causes the doc to be kept in memory, even after the unfetch completes. + model.subscribe('colors.green'); + // Once there's nothing pending on the model/doc... + model.whenNothingPending(function() { + // Racer doc should still be present due to the subscription. + expect(model.get('colors.green')).to.eql({id: 'green', hex: '00ee00'}); + // Share doc should be present too. + var shareDoc = model.connection.getExisting('colors', 'green'); + expect(shareDoc).to.have.property('data'); + expect(shareDoc.data).to.eql({id: 'green', hex: '00ee00'}); + done(); }); }); }); From 99f7de69f5ba059e753ae60dc874721c9ee40730 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 25 Mar 2020 13:06:58 -0700 Subject: [PATCH 239/479] 0.9.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 874949775..1374bc1ff 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.14", + "version": "0.9.15", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 40addba1171819d9485a80c962384e8d7566025b Mon Sep 17 00:00:00 2001 From: Melissa Skevington Date: Fri, 28 Aug 2020 17:17:24 -0700 Subject: [PATCH 240/479] Add additional signature for Model#filter that doesn't use var-args The var-args for additionalInputPaths in Model#filter make it difficult to write TypeScript type definitions. These backwards-compatible changes add alternative signatures to allow array of additionalInputPaths. model.filter(inPath1, inPath2, inPath3, ..., fn); // Old model.filter(inPath1, [inPath2, inPath3, ...], fn); // New --- lib/Model/filter.js | 23 ++++++++++++----- test/Model/filter.js | 59 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/lib/Model/filter.js b/lib/Model/filter.js index a1412565c..b43aea354 100644 --- a/lib/Model/filter.js +++ b/lib/Model/filter.js @@ -23,18 +23,29 @@ Model.INITS.push(function(model) { function parseFilterArguments(model, args) { var fn = args.pop(); - var options; - if (!model.isPath(args[args.length - 1])) { + var options, inputPaths; + var path = model.path(args.shift()); + + var last = args[args.length - 1]; + if (!model.isPath(last) && !Array.isArray(last)) { options = args.pop(); } - var path = model.path(args.shift()); - var i = args.length; + if (args.length === 1 && Array.isArray(args[0])) { + // inputPaths provided as one array: + // model.filter(inputPath, [inputPath1, inputPath2], fn) + inputPaths = args[0]; + } else { + // inputPaths provided as var-args: + // model.filter(inputPath, inputPath1, inputPath2, fn) + inputPaths = args; + } + var i = inputPaths.length; while (i--) { - args[i] = model.path(args[i]); + inputPaths[i] = model.path(inputPaths[i]); } return { path: path, - inputPaths: (args.length) ? args : null, + inputPaths: inputPaths, options: options, fn: fn }; diff --git a/test/Model/filter.js b/test/Model/filter.js index b75033722..c3dc46942 100644 --- a/test/Model/filter.js +++ b/test/Model/filter.js @@ -47,6 +47,46 @@ describe('filter', function() { var filter = model.filter('numbers', 'even').sort(); expect(filter.get()).to.eql([0, 0, 2, 4]); }); + it('supports additional input paths as var-args', function() { + var model = (new Model()).at('_page'); + var numbers = [0, 3, 4, 1, 2, 3, 0]; + for (var i = 0; i < numbers.length; i++) { + model.set('numbers.' + model.id(), numbers[i]); + } + model.set('mod', 3); + model.set('offset', 0); + var filter = model.filter('numbers', 'mod', 'offset', function(number, id, numbers, mod, offset) { + return (number % mod) === offset; + }); + expect(filter.get()).to.eql([0, 3, 3, 0]); + }); + it('supports additional input paths as array', function() { + var model = (new Model()).at('_page'); + var numbers = [0, 3, 4, 1, 2, 3, 0]; + for (var i = 0; i < numbers.length; i++) { + model.set('numbers.' + model.id(), numbers[i]); + } + model.set('mod', 3); + model.set('offset', 0); + var filter = model.filter('numbers', ['mod', 'offset'], function(number, id, numbers, mod, offset) { + return (number % mod) === offset; + }); + expect(filter.get()).to.eql([0, 3, 3, 0]); + }); + it('supports a skip option', function() { + var model = (new Model()).at('_page'); + var numbers = [0, 3, 4, 1, 2, 3, 0]; + var options = {skip: 2}; + for (var i = 0; i < numbers.length; i++) { + model.set('numbers.' + model.id(), numbers[i]); + } + model.set('mod', 3); + model.set('offset', 0); + var filter = model.filter('numbers', ['mod', 'offset'], options, function(number, id, numbers, mod, offset) { + return (number % mod) === offset; + }); + expect(filter.get()).to.eql([3, 0]); + }); }); describe('initial value set by ref', function() { it('supports filter of object', function() { @@ -141,7 +181,7 @@ describe('filter', function() { } ]); }); - it('supports additional dynamic inputs', function() { + it('supports additional dynamic inputs as var-args', function() { var model = (new Model()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { @@ -158,5 +198,22 @@ describe('filter', function() { model.set('mod', 2); expect(filter.get()).to.eql([3, 1, 3]); }); + it('supports additional dynamic inputs as array', function() { + var model = (new Model()).at('_page'); + var numbers = [0, 3, 4, 1, 2, 3, 0]; + for (var i = 0; i < numbers.length; i++) { + model.set('numbers.' + model.id(), numbers[i]); + } + model.set('mod', 3); + model.set('offset', 0); + var filter = model.filter('numbers', ['mod', 'offset'], function(number, id, numbers, mod, offset) { + return (number % mod) === offset; + }); + expect(filter.get()).to.eql([0, 3, 3, 0]); + model.set('offset', 1); + expect(filter.get()).to.eql([4, 1]); + model.set('mod', 2); + expect(filter.get()).to.eql([3, 1, 3]); + }); }); }); From 5298b1426ad2515dfb989fac942a3e2507d97171 Mon Sep 17 00:00:00 2001 From: Melissa Skevington Date: Mon, 31 Aug 2020 16:33:46 -0700 Subject: [PATCH 241/479] Return null for inputPaths if none provided --- lib/Model/filter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/filter.js b/lib/Model/filter.js index b43aea354..6d0fb0cbc 100644 --- a/lib/Model/filter.js +++ b/lib/Model/filter.js @@ -45,7 +45,7 @@ function parseFilterArguments(model, args) { } return { path: path, - inputPaths: inputPaths, + inputPaths: (inputPaths.length) ? inputPaths : null, options: options, fn: fn }; From 679821dcb6090568864586937dd9093a2bd4eaaa Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 2 Sep 2020 13:27:07 -0700 Subject: [PATCH 242/479] 0.9.16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1374bc1ff..5b1b92f88 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.15", + "version": "0.9.16", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 82b81b72eb387aa08b6c7917b09fb0409bb7dfd6 Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Wed, 25 Nov 2020 16:39:04 -0500 Subject: [PATCH 243/479] Add special case where the path is empty but we can't call the legacy mutation listener --- lib/Model/events.js | 11 ++++++++--- test/Model/events.js | 10 ++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index 0cf1d1e1c..6a2bc4fe1 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -106,7 +106,8 @@ Model.prototype._emitMutation = function(segments, event) { throw new Error( 'Maximum model mutation event cycles exceeded. Most likely, an event ' + 'listener is performing a mutation that emits an event to the same ' + - 'listener, directly or indirectly. This creates an infinite cycle.' + 'listener, directly or indirectly. This creates an infinite cycle. Queue details: \n' + + JSON.stringify(root._mutationEventQueue, null, 2) ); } var queue = root._mutationEventQueue; @@ -341,8 +342,12 @@ function getMutationListener(model, type, arg1, arg2, arg3) { throw new Error('No expected callback function'); } if (!pattern) { - // Listen to raw event emission when no path is provided - return new MutationListener(['**'], model._eventContext, cb); + if (options && options.useEventObjects) { + return createMutationListener(model.path('**'), model._eventContext, cb); + } else { + // Listen to raw event emission when no path is provided + return new MutationListener(['**'], model._eventContext, cb); + } } pattern = normalizePattern(pattern); return (options && options.useEventObjects) ? diff --git a/test/Model/events.js b/test/Model/events.js index 8d835a842..6d97f888b 100644 --- a/test/Model/events.js +++ b/test/Model/events.js @@ -143,6 +143,7 @@ describe('Model events with {useEventObjects: true}', function() { }); model.set('a', 1); }); + it('calls later listeners in the order of mutations', function(done) { var model = (new racer.Model()).at('_page'); model.on('change', 'a', function() { @@ -160,6 +161,15 @@ describe('Model events with {useEventObjects: true}', function() { }); model.set('a', 1); }); + + it('can omit the path argument when useEventObjects is true', function(done) { + var model = (new racer.Model()).at('_page'); + model.on('change', {useEventObjects: true}, function(_event, captures) { + expect(captures).to.eql('a'); + done(); + }); + model.set('a', 1); + }); }); describe('remote events', function() { From f92bba9ee835ce75d71332896e504f0c02bfc5e3 Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Mon, 30 Nov 2020 14:47:10 -0500 Subject: [PATCH 244/479] Update the pattern to model.path when useEventObjects is true Add tests to cover both cases --- lib/Model/events.js | 8 ++------ test/Model/events.js | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index 6a2bc4fe1..c1aee33d3 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -326,6 +326,7 @@ function getMutationListener(model, type, arg1, arg2, arg3) { pattern = model.path(arg1); if (pattern == null) { options = arg1; + pattern = model.path(); } cb = arg2; } else if (typeof arg1 === 'function') { @@ -342,12 +343,7 @@ function getMutationListener(model, type, arg1, arg2, arg3) { throw new Error('No expected callback function'); } if (!pattern) { - if (options && options.useEventObjects) { - return createMutationListener(model.path('**'), model._eventContext, cb); - } else { - // Listen to raw event emission when no path is provided - return new MutationListener(['**'], model._eventContext, cb); - } + return new MutationListener(['**'], model._eventContext, cb); } pattern = normalizePattern(pattern); return (options && options.useEventObjects) ? diff --git a/test/Model/events.js b/test/Model/events.js index 6d97f888b..bca8cffea 100644 --- a/test/Model/events.js +++ b/test/Model/events.js @@ -1,7 +1,7 @@ var expect = require('../util').expect; var racer = require('../../lib/index'); -describe('Model events', function() { +describe('Model events without useEventObjects', function() { describe('mutator events', function() { it('calls earlier listeners in the order of mutations', function(done) { var model = (new racer.Model()).at('_page'); @@ -20,6 +20,7 @@ describe('Model events', function() { }); model.set('a', 1); }); + it('calls later listeners in the order of mutations', function(done) { var model = (new racer.Model()).at('_page'); model.on('change', 'a', function() { @@ -37,6 +38,17 @@ describe('Model events', function() { }); model.set('a', 1); }); + + it('can omit the path argument', function(done) { + var model = (new racer.Model()).at('_page'); + + model.at('a').on('change', function(value, prev) { + expect(value).to.equal(1); + expect(prev).to.be.empty; + done(); + }); + model.set('a', 1); + }); }); describe('remote events', function() { @@ -164,8 +176,10 @@ describe('Model events with {useEventObjects: true}', function() { it('can omit the path argument when useEventObjects is true', function(done) { var model = (new racer.Model()).at('_page'); - model.on('change', {useEventObjects: true}, function(_event, captures) { - expect(captures).to.eql('a'); + + model.at('a').on('change', {useEventObjects: true}, function(_event, captures) { + expect(_event.value).to.equal(1); + expect(captures).to.have.length(0); done(); }); model.set('a', 1); From 2485e0fb02d3d1561dc6efeb5a16f0200aea62a5 Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Tue, 1 Dec 2020 11:51:09 -0500 Subject: [PATCH 245/479] Add back deleted comment --- lib/Model/events.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Model/events.js b/lib/Model/events.js index c1aee33d3..8cd6dff94 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -340,6 +340,7 @@ function getMutationListener(model, type, arg1, arg2, arg3) { pattern = model.path(); cb = arg1; } else { + // Listen to raw event emission when no path is provided throw new Error('No expected callback function'); } if (!pattern) { From b9ca11e0083c8094f7b46d4b5efffc499b440ea9 Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Tue, 1 Dec 2020 11:57:44 -0500 Subject: [PATCH 246/479] Put comment back in the right place --- lib/Model/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index 8cd6dff94..9fbe7ba82 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -340,10 +340,10 @@ function getMutationListener(model, type, arg1, arg2, arg3) { pattern = model.path(); cb = arg1; } else { - // Listen to raw event emission when no path is provided throw new Error('No expected callback function'); } if (!pattern) { + // Listen to raw event emission when no path is provided return new MutationListener(['**'], model._eventContext, cb); } pattern = normalizePattern(pattern); From fbb595d01887d1be9eecd5b49f71ceb8dae562d1 Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Wed, 2 Dec 2020 10:54:31 -0500 Subject: [PATCH 247/479] Provide the removed property on remove events for backwards compatibility --- lib/Model/events.js | 6 ++++++ test/Model/events.js | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/Model/events.js b/lib/Model/events.js index 9fbe7ba82..1b79b214f 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -482,7 +482,13 @@ InsertEvent.prototype._getArgs = function() { function RemoveEvent(index, values, passed) { this.index = index; + + // Provide both a `values` and `removed` property + // `values` is more aligned with other events, + // but `removed` is needed for backwards compatibily this.values = values; + this.removed = values; + this.passed = passed; } RemoveEvent.prototype.type = 'remove'; diff --git a/test/Model/events.js b/test/Model/events.js index bca8cffea..85e972ed3 100644 --- a/test/Model/events.js +++ b/test/Model/events.js @@ -184,6 +184,22 @@ describe('Model events with {useEventObjects: true}', function() { }); model.set('a', 1); }); + + describe('remove', function() { + it('has removed property', function(done) { + var model = (new racer.Model()).at('_page'); + model.set('a', [1, 2, 3]); + model.on('remove', '**', {useEventObjects: true}, function(_event, captures) { + expect(_event.type).to.equal('remove'); + expect(_event.index).to.equal(2); + expect(_event.removed).to.eql([3]); + expect(_event.values).to.eql([3]); + expect(captures).to.eql(['a']); + done(); + }); + model.pop('a'); + }); + }); }); describe('remote events', function() { From 679bfcc32fde9bd0415f1e919b01ea598dd3ac84 Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Wed, 2 Dec 2020 16:28:43 -0500 Subject: [PATCH 248/479] Add more event property tests --- test/Model/events.js | 81 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/test/Model/events.js b/test/Model/events.js index 85e972ed3..477aff1b9 100644 --- a/test/Model/events.js +++ b/test/Model/events.js @@ -184,21 +184,75 @@ describe('Model events with {useEventObjects: true}', function() { }); model.set('a', 1); }); + }); - describe('remove', function() { - it('has removed property', function(done) { - var model = (new racer.Model()).at('_page'); - model.set('a', [1, 2, 3]); - model.on('remove', '**', {useEventObjects: true}, function(_event, captures) { - expect(_event.type).to.equal('remove'); - expect(_event.index).to.equal(2); - expect(_event.removed).to.eql([3]); - expect(_event.values).to.eql([3]); - expect(captures).to.eql(['a']); - done(); - }); - model.pop('a'); + describe('insert and remove', function() { + var model; + before('set up', function() { + model = (new racer.Model()).at('_page'); + }); + + it('insert has expected properties', function(done) { + model.on('insert', '**', {useEventObjects: true}, function(_event, captures) { + expect(_event.type).to.equal('insert'); + expect(_event.index).to.equal(2); + expect(_event.values).to.eql([3]); + expect(_event.passed).to.eql({}); + expect(captures).to.eql(['a']); + done(); + }); + model.set('a', [1, 2]); + model.insert('a', 2, [3]); + }); + + it('remove has expected properties', function(done) { + model.on('remove', '**', {useEventObjects: true}, function(_event, captures) { + expect(_event.type).to.equal('remove'); + expect(_event.index).to.equal(2); + expect(_event.removed).to.eql([3]); + expect(_event.values).to.eql([3]); + expect(_event.passed).to.eql({}); + expect(captures).to.eql(['a']); + done(); + }); + model.pop('a'); + }); + }); + + describe('load and unload', function() { + var local, remote; + + before('set up', function() { + var backend = racer.createBackend(); + local = backend.createModel().scope('colors.green'); + remote = backend.createModel().scope('colors.green'); + }); + + it('load has expected properties', function(done) { + remote.on('load', '**', {useEventObjects: true}, function(event, captures) { + expect(event.type).to.equal('load'); + expect(event.value).to.eql({id: 'green'}); + expect(event.document).to.eql({id: 'green'}); + expect(event.passed).to.eql({'$remote': true}); + expect(captures).to.eql(['']); + done(); }); + + local.create(function() { + remote.subscribe(); + }); + }); + + it('unload has expected properties', function(done) { + remote.on('unload', '**', {useEventObjects: true}, function(event, captures) { + expect(event.type).to.equal('unload'); + expect(event.previous).to.eql({id: 'green'}); + expect(event.previousDocument).to.eql({id: 'green'}); + expect(event.passed).to.eql({}); + expect(captures).to.eql(['']); + done(); + }); + remote.unsubscribe(); }); }); @@ -207,6 +261,7 @@ describe('Model events with {useEventObjects: true}', function() { var backend = racer.createBackend(); var local = this.local = backend.createModel().scope('colors.green'); var remote = this.remote = backend.createModel().scope('colors.green'); + local.create(function(err) { if (err) return done(err); remote.subscribe(done); From eb159aaea4b3ea14f4cc217892eceb04583307a0 Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Wed, 2 Dec 2020 16:28:55 -0500 Subject: [PATCH 249/479] Fix load and unload, remove comment --- lib/Model/events.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/Model/events.js b/lib/Model/events.js index 1b79b214f..5c39e263d 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -442,6 +442,7 @@ ChangeEvent.prototype._getArgs = function() { function LoadEvent(value, passed) { this.value = value; + this.document = value; this.passed = passed; } LoadEvent.prototype.type = 'load'; @@ -455,6 +456,7 @@ LoadEvent.prototype._getArgs = function() { function UnloadEvent(previous, passed) { this.previous = previous; + this.previousDocument = previous; this.passed = passed; } UnloadEvent.prototype.type = 'unload'; @@ -482,13 +484,8 @@ InsertEvent.prototype._getArgs = function() { function RemoveEvent(index, values, passed) { this.index = index; - - // Provide both a `values` and `removed` property - // `values` is more aligned with other events, - // but `removed` is needed for backwards compatibily this.values = values; this.removed = values; - this.passed = passed; } RemoveEvent.prototype.type = 'remove'; From 0b6630ff4d39f31517e0d8e6273365dc74b886d8 Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Wed, 2 Dec 2020 16:34:01 -0500 Subject: [PATCH 250/479] Add checks for howMany property of move event --- test/Model/events.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/Model/events.js b/test/Model/events.js index 477aff1b9..e702d6072 100644 --- a/test/Model/events.js +++ b/test/Model/events.js @@ -286,52 +286,63 @@ describe('Model events with {useEventObjects: true}', function() { this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(4); expect(event.to).to.equal(0); + expect(event.howMany).to.equal(1); done(); }); this.local.move('array', 4, 0, 1); }); + it('can swap the first two items in the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(1); expect(event.to).to.equal(0); + expect(event.howMany).to.equal(1); done(); }); this.local.move('array', 1, 0, 1, function() {}); }); + it('can move an item from the begnning to the end of the array', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(0); expect(event.to).to.equal(4); + expect(event.howMany).to.equal(1); done(); }); this.local.move('array', 0, 4, 1, function() {}); }); + it('supports a negative destination index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(0); expect(event.to).to.equal(4); + expect(event.howMany).to.equal(1); done(); }); this.local.move('array', 0, -1, 1, function() {}); }); + it('supports a negative source index of -1 (for last)', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(4); expect(event.to).to.equal(2); + expect(event.howMany).to.equal(1); done(); }); this.local.move('array', -1, 2, 1, function() {}); }); + it('can move several items mid-array, with an event for each', function(done) { this.local.set('array', [0, 1, 2, 3, 4], function() {}); var events = 0; this.remote.on('move', '**', {useEventObjects: true}, function(event) { expect(event.from).to.equal(1); expect(event.to).to.equal(4); + expect(event.howMany).to.equal(1); if (++events === 2) { done(); } From f664a13dd936193e2aa7db0208a3c9386b7ac1ea Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Fri, 4 Dec 2020 14:15:13 -0500 Subject: [PATCH 251/479] Add comments to explain values vs specific names --- lib/Model/events.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/Model/events.js b/lib/Model/events.js index 5c39e263d..3bfc1cd70 100644 --- a/lib/Model/events.js +++ b/lib/Model/events.js @@ -441,6 +441,11 @@ ChangeEvent.prototype._getArgs = function() { }; function LoadEvent(value, passed) { + /** + * The documented public name of the loaded item is `document` + * However we use `value` internally so both are provided + * Using `document` is preferred + */ this.value = value; this.document = value; this.passed = passed; @@ -455,6 +460,11 @@ LoadEvent.prototype._getArgs = function() { }; function UnloadEvent(previous, passed) { + /** + * The documented public name of the unloaded item is `previousDocument` + * However we use `previous` internally so both are provided + * Using `previousDocument` is preferred + */ this.previous = previous; this.previousDocument = previous; this.passed = passed; @@ -484,6 +494,11 @@ InsertEvent.prototype._getArgs = function() { function RemoveEvent(index, values, passed) { this.index = index; + /** + * The documented public name of the removed item is `removed` + * However we use `values` internally so both are provided + * Using `removed` is preferred + */ this.values = values; this.removed = values; this.passed = passed; From b033e55b417844dc430f5e7da883a1ec689be9b8 Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Fri, 4 Dec 2020 14:57:38 -0500 Subject: [PATCH 252/479] Add additional test for calling start twice but stop once --- test/Model/fn.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/Model/fn.js b/test/Model/fn.js index 350461a6b..fb25079c4 100644 --- a/test/Model/fn.js +++ b/test/Model/fn.js @@ -147,6 +147,7 @@ describe('fn', function() { expect(count).to.equal(1); }); }); + describe('stop', function() { it('can call stop without start', function() { var model = new Model(); @@ -165,6 +166,22 @@ describe('fn', function() { model.set('_nums.a', 3); expect(model.get('_nums.sum')).to.equal(5); }); + it('stops updating when start was called twice', function() { + var model = new Model(); + model.set('_nums.a', 2); + model.set('_nums.b', 4); + model.start('_nums.sum', '_nums.a', '_nums.b', function(a, b) { + return a + b; + }); + model.start('_nums.sum', '_nums.a', '_nums.b', function(a, b) { + return a + b; + }); + model.set('_nums.a', 1); + expect(model.get('_nums.sum')).to.equal(5); + model.stop('_nums.sum'); + model.set('_nums.a', 3); + expect(model.get('_nums.sum')).to.equal(5); + }); }); describe('stopAll', function() { it('can call without start', function() { From 5a45bbd82efd4798e8e96418d3787e5ae6aa259f Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Fri, 8 Jan 2021 11:07:01 -0500 Subject: [PATCH 253/479] 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b1b92f88..33aed12c3 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "0.9.16", + "version": "1.0.0", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 1bcef0e5b3a231f4b9e2bbe323d275abb11e866f Mon Sep 17 00:00:00 2001 From: Christina Burger Date: Thu, 21 Jan 2021 17:00:52 -0500 Subject: [PATCH 254/479] Remove expectjs and replace with chai * Removed some polyfills which don't seem to be required anymore * Replaced assertions with their chai equivalents --- package.json | 2 +- test/Model/CollectionCounter.js | 16 ++++++++-------- test/Model/EventListenerTree.js | 2 +- test/Model/connection.js | 2 +- test/Model/docs.js | 2 +- test/Model/events.js | 2 +- test/Model/filter.js | 2 +- test/Model/query.js | 2 +- test/util.js | 16 +--------------- 9 files changed, 16 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 33aed12c3..19006de39 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "uuid": "^2.0.1" }, "devDependencies": { + "chai": "^4.2.0", "coveralls": "^3.0.5", "eslint": "^5.16.0", "eslint-config-google": "^0.13.0", - "expect.js": "^0.3.1", "mocha": "^6.1.4", "nyc": "^14.1.1" }, diff --git a/test/Model/CollectionCounter.js b/test/Model/CollectionCounter.js index 783221ed8..cdc0315cb 100644 --- a/test/Model/CollectionCounter.js +++ b/test/Model/CollectionCounter.js @@ -5,7 +5,7 @@ describe('CollectionCounter', function() { describe('increment', function() { it('increments count for a document', function() { var counter = new CollectionCounter(); - expect(counter.get('colors', 'green')).to.be(0); + expect(counter.get('colors', 'green')).to.equal(0); expect(counter.increment('colors', 'green')).to.equal(1); expect(counter.increment('colors', 'green')).to.equal(2); expect(counter.get('colors', 'green')).to.equal(2); @@ -27,29 +27,29 @@ describe('CollectionCounter', function() { expect(counter.increment('colors', 'green')); expect(counter.decrement('colors', 'green')).to.equal(1); expect(counter.decrement('colors', 'green')).to.equal(0); - expect(counter.get('colors', 'green')).to.be(0); + expect(counter.get('colors', 'green')).to.equal(0); }); it('does not affect peer document', function() { var counter = new CollectionCounter(); expect(counter.increment('colors', 'green')); expect(counter.increment('colors', 'red')); expect(counter.decrement('colors', 'green')); - expect(counter.get('colors', 'green')).to.be(0); - expect(counter.get('colors', 'red')).to.be(1); + expect(counter.get('colors', 'green')).to.equal(0); + expect(counter.get('colors', 'red')).to.equal(1); }); it('does not affect peer collection', function() { var counter = new CollectionCounter(); expect(counter.increment('colors', 'green')); expect(counter.increment('textures', 'smooth')); expect(counter.decrement('colors', 'green')); - expect(counter.get('colors', 'green')).to.be(0); - expect(counter.get('textures', 'smooth')).to.be(1); + expect(counter.get('colors', 'green')).to.equal(0); + expect(counter.get('textures', 'smooth')).to.equal(1); }); }); describe('toJSON', function() { it('returns undefined if there are no counts', function() { var counter = new CollectionCounter(); - expect(counter.toJSON()).to.be(undefined); + expect(counter.toJSON()).to.equal(undefined); }); it('returns a nested map representing counts', function() { var counter = new CollectionCounter(); @@ -71,7 +71,7 @@ describe('CollectionCounter', function() { var counter = new CollectionCounter(); counter.increment('colors', 'green'); counter.decrement('colors', 'green'); - expect(counter.toJSON()).to.be(undefined); + expect(counter.toJSON()).to.equal(undefined); }); it('decrementing id from collection with other keys removes key', function() { var counter = new CollectionCounter(); diff --git a/test/Model/EventListenerTree.js b/test/Model/EventListenerTree.js index 26a84a113..a41d9bccb 100644 --- a/test/Model/EventListenerTree.js +++ b/test/Model/EventListenerTree.js @@ -37,7 +37,7 @@ describe('EventListenerTree', function() { var tree = new EventListenerTree(); var listener = {}; var node = tree.addListener(['colors', 'green'], listener); - expect(node).a(EventListenerTree); + expect(node).instanceOf(EventListenerTree); expect(node.parent.parent).equal(tree); }); }); diff --git a/test/Model/connection.js b/test/Model/connection.js index 45709798b..ee01749a6 100644 --- a/test/Model/connection.js +++ b/test/Model/connection.js @@ -7,7 +7,7 @@ describe('connection', function() { var backend = racer.createBackend(); var model = backend.createModel(); var agent = model.getAgent(); - expect(agent).ok(); + expect(agent).to.be.ok; }); it('returns null once the model is disconnected', function(done) { diff --git a/test/Model/docs.js b/test/Model/docs.js index 588096a09..d326044f3 100644 --- a/test/Model/docs.js +++ b/test/Model/docs.js @@ -193,7 +193,7 @@ module.exports = function(createDoc) { var doc = createDoc(); doc.set(['friends'], {}, function() {}); doc.push(['friends'], ['x'], function(err) { - expect(err).a(TypeError); + expect(err).instanceOf(TypeError); done(); }); }); diff --git a/test/Model/events.js b/test/Model/events.js index e702d6072..18d4d1b85 100644 --- a/test/Model/events.js +++ b/test/Model/events.js @@ -44,7 +44,7 @@ describe('Model events without useEventObjects', function() { model.at('a').on('change', function(value, prev) { expect(value).to.equal(1); - expect(prev).to.be.empty; + expect(prev).not.to.exist; done(); }); model.set('a', 1); diff --git a/test/Model/filter.js b/test/Model/filter.js index c3dc46942..566cc0816 100644 --- a/test/Model/filter.js +++ b/test/Model/filter.js @@ -11,7 +11,7 @@ describe('filter', function() { }); expect(function() { filter.get(); - }).to.throwException(); + }).to.throw(Error); }); it('supports filter of object', function() { var model = (new Model()).at('_page'); diff --git a/test/Model/query.js b/test/Model/query.js index 30712a589..31408a739 100644 --- a/test/Model/query.js +++ b/test/Model/query.js @@ -51,7 +51,7 @@ describe('query', function() { {id: 'a'} ], 3); // 'a' is still present once in the results, should still be in the map. - expect(query.idMap).to.only.have.keys(['a', 'b', 'c']); + expect(query.idMap).to.have.all.keys(['a', 'b', 'c']); }); }); diff --git a/test/util.js b/test/util.js index 880b59649..bc3c0eced 100644 --- a/test/util.js +++ b/test/util.js @@ -1,5 +1,5 @@ var util = require('util'); -var expect = require('expect.js'); +var expect = require('chai').expect; exports.expect = expect; @@ -19,17 +19,3 @@ exports.inspect = function(value, depth, showHidden) { if (showHidden == null) showHidden = true; console.log(util.inspect(value, {depth: depth, showHidden: showHidden})); }; - -expect.Assertion.prototype.NaN = function() { - this.assert(this.obj !== this.obj, - 'expected ' + util.inspect(this.obj) + ' to be NaN', - 'expected ' + util.inspect(this.obj) + ' to not be NaN' - ); -}; - -expect.Assertion.prototype.null = function() { - this.assert(this.obj == null, - 'expected ' + util.inspect(this.obj) + ' to be null or undefined', - 'expected ' + util.inspect(this.obj) + ' to not be null or undefined' - ); -}; From 82fb8aec22c003a11fc221d598de3360f4dda993 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 29 Oct 2021 18:48:45 -0700 Subject: [PATCH 255/479] Support sharedb@2 alongside sharedb@1 The only change in sharedb@2 is dropping support for Node 10, so racer can continue to support both versions of sharedb --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19006de39..2ba18305d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "arraydiff": "^0.1.1", "fast-deep-equal": "^2.0.1", - "sharedb": "^1.0.0-beta", + "sharedb": "^1.0.0 || ^2.0.0", "uuid": "^2.0.1" }, "devDependencies": { From aafa799c67c9c7182d91e2517908ce6368b564ed Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 29 Oct 2021 19:09:16 -0700 Subject: [PATCH 256/479] Upgrade devDependencies: eslint 5->8, mocha 6->9, nyc 14->15 --- .eslintrc.js | 10 +++++----- .mocharc.yml | 4 ++++ package.json | 8 ++++---- test/mocha.opts | 4 ---- 4 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 .mocharc.yml delete mode 100644 test/mocha.opts diff --git a/.eslintrc.js b/.eslintrc.js index f78ae0876..cfb9c0ccf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,7 @@ // The ESLint ecmaVersion argument is inconsistently used. Some rules will ignore it entirely, so if the rule has // been set, it will still error even if it's not applicable to that version number. Since Google sets these // rules, we have to turn them off ourselves. -const DISABLED_ES6_OPTIONS = { +var DISABLED_ES6_OPTIONS = { 'no-var': 'off', 'prefer-rest-params': 'off', 'prefer-spread': 'off', @@ -9,7 +9,7 @@ const DISABLED_ES6_OPTIONS = { 'comma-dangle': ['error', 'never'] }; -const CUSTOM_RULES = { +var CUSTOM_RULES = { 'one-var': 'off', // We control our own objects and prototypes, so no need for this check 'guard-for-in': 'off', @@ -18,7 +18,7 @@ const CUSTOM_RULES = { 'indent': ['error', 2, {'SwitchCase': 1}], // Less aggressive line length than Google, which is especially useful when we have a lot of callbacks in our code 'max-len': ['error', {code: 120, tabWidth: 2, ignoreUrls: true}], - // Google overrides the default ESLint behaviour here, which is slightly better for catching erroneously unused variables + // Go back to default ESLint behaviour here, which is slightly better for catching erroneously unused variables 'no-unused-vars': ['error', {vars: 'all', args: 'after-used'}], 'require-jsdoc': 'off', 'valid-jsdoc': 'off' @@ -27,11 +27,11 @@ const CUSTOM_RULES = { module.exports = { extends: 'google', parserOptions: { - ecmaVersion: 3 + ecmaVersion: 5 }, rules: Object.assign( {}, DISABLED_ES6_OPTIONS, CUSTOM_RULES - ), + ) }; diff --git a/.mocharc.yml b/.mocharc.yml new file mode 100644 index 000000000..815f9301e --- /dev/null +++ b/.mocharc.yml @@ -0,0 +1,4 @@ +reporter: spec +timeout: 1200 +check-leaks: true +recursive: true diff --git a/package.json b/package.json index 2ba18305d..598d5b828 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,10 @@ "devDependencies": { "chai": "^4.2.0", "coveralls": "^3.0.5", - "eslint": "^5.16.0", - "eslint-config-google": "^0.13.0", - "mocha": "^6.1.4", - "nyc": "^14.1.1" + "eslint": "^8.1.0", + "eslint-config-google": "^0.14.0", + "mocha": "^9.1.3", + "nyc": "^15.1.0" }, "bugs": { "url": "https://github.com/derbyjs/racer/issues" diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index f3d32c552..000000000 --- a/test/mocha.opts +++ /dev/null @@ -1,4 +0,0 @@ ---reporter spec ---timeout 1200 ---check-leaks ---recursive From ed83cbbc73b931029c3e7649a07a115909d55442 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 29 Oct 2021 19:26:14 -0700 Subject: [PATCH 257/479] Switch to GitHub Actions for CI --- .github/workflows/test.yml | 51 ++++++++++++++++++++++++++++++++++++++ .travis.yml | 8 ------ 2 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..a895974b7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +# https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs-or-python + + +name: Test + +on: + push: + branches: + - $default-branch + pull_request: + branches: + - $default-branch + +jobs: + test: + name: Node.js ${{ matrix.node }} + runs-on: ubuntu-latest + strategy: + matrix: + node: + - 10 + - 12 + - 14 + - 16 + timeout-minutes: 5 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + cache: 'npm' + - run: npm install + - run: npm run lint + - run: npm run test-cover + # https://github.com/marketplace/actions/coveralls-github-action#complete-parallel-job-example + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + flag-name: node-${{ matrix.node }} + parallel: true + + finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Submit coverage + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d47ad2a7f..000000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: node_js -node_js: - - 10 - - 8 - - 6 -script: "npm run test-cover" -# Send coverage data to Coveralls -after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" From 635499f8edc6a384ed7d55995a37e6ae861903ec Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 29 Oct 2021 19:36:03 -0700 Subject: [PATCH 258/479] GH Actions template does not support $default-branch token --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a895974b7..c3b3e8092 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,10 +6,10 @@ name: Test on: push: branches: - - $default-branch + - master pull_request: branches: - - $default-branch + - master jobs: test: From c31cd820e2c882d5ceef289a17586736362f34de Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 29 Oct 2021 19:37:30 -0700 Subject: [PATCH 259/479] Remove npm cache as this repo doesn't use package-lock.json --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3b3e8092..96f1e052f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,6 @@ jobs: - uses: actions/setup-node@v2 with: node-version: ${{ matrix.node }} - cache: 'npm' - run: npm install - run: npm run lint - run: npm run test-cover From a86ec515b0071efdf97053eb2c446cf6ab75a9ed Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 29 Oct 2021 20:08:58 -0700 Subject: [PATCH 260/479] Only run eslint on Node >= 12 --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96f1e052f..fcfaafa29 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,7 @@ jobs: node-version: ${{ matrix.node }} - run: npm install - run: npm run lint + if: ${{ matrix.node >= 12 }} - run: npm run test-cover # https://github.com/marketplace/actions/coveralls-github-action#complete-parallel-job-example - name: Coveralls From 3030f3c2404d3b3d9a57bb2f48ceb69076b6d242 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 29 Oct 2021 20:14:14 -0700 Subject: [PATCH 261/479] Switch linting to a posttest hook --- .github/workflows/test.yml | 2 +- package.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fcfaafa29..4936aeb88 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: node-version: ${{ matrix.node }} - run: npm install - run: npm run lint - if: ${{ matrix.node >= 12 }} + if: ${{ matrix.node >= 12 }} # eslint@8 only supports Node >= 12 - run: npm run test-cover # https://github.com/marketplace/actions/coveralls-github-action#complete-parallel-job-example - name: Coveralls diff --git a/package.json b/package.json index 598d5b828..c703622e7 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,9 @@ "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", - "test": "node_modules/.bin/mocha && npm run lint", - "test-cover": "node_modules/nyc/bin/nyc.js --temp-dir=coverage -r text -r lcov node_modules/mocha/bin/_mocha && npm run lint" + "test": "node_modules/.bin/mocha", + "posttest": "npm run lint", + "test-cover": "node_modules/nyc/bin/nyc.js --temp-dir=coverage -r text -r lcov node_modules/mocha/bin/_mocha" }, "dependencies": { "arraydiff": "^0.1.1", From 22977898a9a8f15e3dd2d96094b480407d74287b Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 29 Oct 2021 20:23:18 -0700 Subject: [PATCH 262/479] 1.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c703622e7..60cd2d58f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "1.0.0", + "version": "1.0.1", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 7a654b5dbf2921caa50e894be128b102400d965a Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 25 Aug 2022 14:30:35 -0700 Subject: [PATCH 263/479] In Model#bundle, use new Contexts#toJSON method for consistency with other data bundle properties --- lib/Model/bundle.js | 2 +- lib/Model/contexts.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/Model/bundle.js b/lib/Model/bundle.js index bb059bcbd..1448d55dc 100644 --- a/lib/Model/bundle.js +++ b/lib/Model/bundle.js @@ -21,7 +21,7 @@ Model.prototype.bundle = function(cb) { clearTimeout(timeout); var bundle = { queries: root._queries.toJSON(), - contexts: root._contexts, + contexts: root._contexts.toJSON(), refs: root._refs.toJSON(), refLists: root._refLists.toJSON(), fns: root._fns.toJSON(), diff --git a/lib/Model/contexts.js b/lib/Model/contexts.js index 6fa44e328..8c3dcc64e 100644 --- a/lib/Model/contexts.js +++ b/lib/Model/contexts.js @@ -39,6 +39,16 @@ Model.prototype.unloadAll = function() { }; function Contexts() {} +Contexts.prototype.toJSON = function() { + var out = {}; + var contexts = this; + for (var key in contexts) { + if (contexts[key] instanceof Context) { + out[key] = contexts[key].toJSON(); + } + } + return out; +}; function FetchedQueries() {} function SubscribedQueries() {} From 2d74771d4083c964adf1c0696f3f1514b3916312 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 25 Aug 2022 14:41:11 -0700 Subject: [PATCH 264/479] 1.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 60cd2d58f..9d50c80c0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "1.0.1", + "version": "1.0.2", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 9b4833be34363fcb582b5852c3549fe371e32cf7 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 25 Aug 2022 15:25:33 -0700 Subject: [PATCH 265/479] Fix Contexts#unloadAll to account for addition of toJSON method --- lib/Model/contexts.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Model/contexts.js b/lib/Model/contexts.js index 8c3dcc64e..a4e8d5b89 100644 --- a/lib/Model/contexts.js +++ b/lib/Model/contexts.js @@ -34,7 +34,9 @@ Model.prototype.unload = function(id) { Model.prototype.unloadAll = function() { var contexts = this.root._contexts; for (var key in contexts) { - contexts[key].unload(); + if (contexts.hasOwnProperty(key)) { + contexts[key].unload(); + } } }; From a2db7034dc76f61586db19dc2781331bbf3ed250 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 25 Aug 2022 15:31:19 -0700 Subject: [PATCH 266/479] 1.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9d50c80c0..19229d9a7 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "1.0.2", + "version": "1.0.3", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 222357b75780f8c28250820a3c558b0e5a9c55a6 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 18 Nov 2022 12:34:41 -0800 Subject: [PATCH 267/479] Strip ref outputs from initial data bundle, to reduce size --- lib/Model/bundle.js | 3 +++ lib/Model/mutators.js | 3 +++ test/Model/bundle.js | 25 ++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/Model/bundle.js b/lib/Model/bundle.js index 1448d55dc..5056e7160 100644 --- a/lib/Model/bundle.js +++ b/lib/Model/bundle.js @@ -41,6 +41,9 @@ function stripComputed(root) { root._refLists.fromMap.forEach(function(refList) { silentModel._del(refList.fromSegments); }); + root._refs.fromMap.forEach(function(ref) { + silentModel._delNoDereference(ref.fromSegments); + }); root._fns.fromMap.forEach(function(fn) { silentModel._del(fn.fromSegments); }); diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index eac1619a9..5271e2753 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -266,6 +266,9 @@ Model.prototype.del = function() { }; Model.prototype._del = function(segments, cb) { segments = this._dereference(segments); + return this._delNoDereference(segments, cb); +}; +Model.prototype._delNoDereference = function(segments, cb) { var model = this; function del(doc, docSegments, fnCb) { var previous = doc.del(docSegments, fnCb); diff --git a/test/Model/bundle.js b/test/Model/bundle.js index 6041058b6..4152897e1 100644 --- a/test/Model/bundle.js +++ b/test/Model/bundle.js @@ -33,5 +33,28 @@ describe('bundle', function() { }); }); }); -}); + it('does not serialize ref outputs', function(done) { + var backend = racer.createBackend(); + var model = backend.createModel(); + model.add('dogs', {id: 'coco', name: 'Coco'}); + model.ref('_page.myDog', 'dogs.coco'); + model.bundle(function(err, bundleData) { + if (err) return done(err); + // Simulate serialization of bundle data between server and client + bundleData = JSON.parse(JSON.stringify(bundleData)); + + // Bundle should not have data duplicated on the ref output path + expect(bundleData.collections).to.not.have.property('_page'); + + // After unbundling, the ref should be re-created + var clientModel = backend.createModel(); + clientModel.unbundle(bundleData); + expect(clientModel.get('_page')).to.deep.equal({myDog: {id: 'coco', name: 'Coco'}}); + // Test that operation on ref output path is applied to the backing doc + clientModel.set('_page.myDog.breed', 'poodle'); + expect(clientModel.get('dogs.coco')).to.deep.equal({id: 'coco', name: 'Coco', breed: 'poodle'}); + done(); + }); + }); +}); From 55dd8f8ca2a0f6ae760f03202f31299f516df507 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 18 Nov 2022 16:43:10 -0800 Subject: [PATCH 268/479] 1.0.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19229d9a7..499c2739b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "1.0.3", + "version": "1.0.4", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From e360c821d4bd372b26c6cec04bac2a70e063bdc7 Mon Sep 17 00:00:00 2001 From: Zeus Lalkaka Date: Tue, 6 Dec 2022 18:33:30 -0500 Subject: [PATCH 269/479] Fix var reference error within Model.unload() and add regression tests. --- lib/Model/contexts.js | 6 ++-- test/Model/loading.js | 65 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/lib/Model/contexts.js b/lib/Model/contexts.js index a4e8d5b89..98f2e5c3c 100644 --- a/lib/Model/contexts.js +++ b/lib/Model/contexts.js @@ -128,21 +128,21 @@ Context.prototype.unload = function() { } for (var collectionName in this.fetchedDocs.collections) { var collection = this.fetchedDocs.collections[collectionName]; - for (var id in collection) { + for (var id in collection.counts) { var count = collection.counts[id]; while (count--) model.unfetchDoc(collectionName, id); } } for (var collectionName in this.subscribedDocs.collections) { var collection = this.subscribedDocs.collections[collectionName]; - for (var id in collection) { + for (var id in collection.counts) { var count = collection.counts[id]; while (count--) model.unsubscribeDoc(collectionName, id); } } for (var collectionName in this.createdDocs.collections) { var collection = this.createdDocs.collections[collectionName]; - for (var id in collection) { + for (var id in collection.counts) { model._maybeUnloadDoc(collectionName, id); } } diff --git a/test/Model/loading.js b/test/Model/loading.js index 6f6267f2b..02841875f 100644 --- a/test/Model/loading.js +++ b/test/Model/loading.js @@ -100,4 +100,69 @@ describe('loading', function() { }); }); }); + + describe('when using model.unload()', function() { + beforeEach(function(done) { + this.setupModel = this.backend.createModel(); + this.setupModel.add('colors', {id: 'green', hex: '00ff00'}); + this.setupModel.add('colors', {id: 'blue', hex: '0000ff'}); + this.setupModel.whenNothingPending(done); + }); + + it('unloads all documents after model has nothing pending', function(done) { + var model = this.model; + model.fetch('colors.green', 'colors.blue', function(err) { + if (err) return done(err); + expect(model.get('colors.green.hex')).to.equal('00ff00'); + expect(model.get('colors.blue.hex')).to.equal('0000ff'); + // Queue up a pending op. + model.set('colors.green.hex', '00ee00'); + // Unfetch. This triggers the delayed _maybeUnloadDoc. + // The pending op causes the doc unload to be delayed. + model.unload(); + // Once there's nothing pending on the model/doc... + model.whenNothingPending(function() { + // Racer doc should be unloaded. + expect(model.get('colors.green')).to.equal(undefined); + expect(model.get('colors.blue')).to.equal(undefined); + // Share doc should be unloaded too. + expect(model.connection.getExisting('colors', 'green')).to.equal(undefined); + expect(model.connection.getExisting('colors', 'blue')).to.equal(undefined); + done(); + }); + }); + }); + + it('only unloads documents without a pending operation or subscription', function(done) { + var model = this.model; + // Racer keeps its own reference counts of doc fetches/subscribes - see `_hasDocReferences`. + model.fetch('colors.green', 'colors.blue', function(err) { + if (err) return done(err); + expect(model.get('colors.green.hex')).to.equal('00ff00'); + expect(model.get('colors.blue.hex')).to.equal('0000ff'); + // Queue up a pending op. + model.set('colors.green.hex', '00ee00'); + // Unfetch. This triggers the delayed _maybeUnloadDoc. + // The pending op causes the doc unload to be delayed. + model.unload(); + // Immediately subscribe to the same doc. + // This causes the doc to be kept in memory, even after the unfetch completes. + model.subscribe('colors.green'); + // Once there's nothing pending on the model/doc... + model.whenNothingPending(function() { + // Racer doc should still be present due to the subscription. + expect(model.get('colors.green')).to.eql({id: 'green', hex: '00ee00'}); + // Share doc should be present too. + var shareDoc = model.connection.getExisting('colors', 'green'); + expect(shareDoc).to.have.property('data'); + expect(shareDoc.data).to.eql({id: 'green', hex: '00ee00'}); + // Racer doc for blue should be unloaded + expect(model.get('colors.blue')).to.equal(undefined); + // Share doc for blue should be unloaded too + expect(model.connection.getExisting('colors', 'blue')).to.equal(undefined); + done(); + }); + }); + }); + }); }); From 6842dfc139dcf8703b60f4648c3383c58b20f399 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Tue, 6 Dec 2022 17:13:51 -0800 Subject: [PATCH 270/479] 1.0.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 499c2739b..5e229e2bd 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "1.0.4", + "version": "1.0.5", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 8b6974b5af5abe1328dcc657c8aa4ebc16436920 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 27 Mar 2023 15:43:55 -0700 Subject: [PATCH 271/479] Support sharedb@3 in dependencies The only breaking change in sharedb@3 is dropping official Node 12 support, so Racer can pick it up alongside the older versions. https://github.com/share/sharedb/releases/tag/v3.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e229e2bd..906acdc9c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "arraydiff": "^0.1.1", "fast-deep-equal": "^2.0.1", - "sharedb": "^1.0.0 || ^2.0.0", + "sharedb": "^1.0.0 || ^2.0.0 || ^3.0.0", "uuid": "^2.0.1" }, "devDependencies": { From a2b483c73d690b42cc657ccc93c866638523bad1 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Tue, 28 Mar 2023 13:03:02 -0700 Subject: [PATCH 272/479] 1.0.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 906acdc9c..c05355a15 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "1.0.5", + "version": "1.0.6", "main": "./lib/index.js", "scripts": { "lint": "eslint --ignore-path .gitignore .", From 9107a964772a425f86a13e85f11b42ba886dfb12 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 18 May 2023 14:42:31 -0700 Subject: [PATCH 273/479] Add sharedb v4 option --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c05355a15..b8f2e4cd9 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "arraydiff": "^0.1.1", "fast-deep-equal": "^2.0.1", - "sharedb": "^1.0.0 || ^2.0.0 || ^3.0.0", + "sharedb": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0", "uuid": "^2.0.1" }, "devDependencies": { From 0de7bde164a9eb6576b65b3fd1ce863415cfca8b Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 2 Jun 2023 14:28:44 -0700 Subject: [PATCH 274/479] Promisify methods with callbacks --- .eslintrc.js | 10 ++++++++- lib/Model/bundle.js | 2 ++ lib/Model/connection.js | 8 +++++++ lib/Model/mutators.js | 36 +++++++++++++++++++++++++++++++ lib/Model/setDiff.js | 12 +++++++++++ lib/Model/subscriptions.js | 25 ++++++++++++++++++++++ lib/util.js | 6 +++++- package.json | 3 ++- test/Model/loading.js | 43 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 142 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index cfb9c0ccf..85529969f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,5 +33,13 @@ module.exports = { {}, DISABLED_ES6_OPTIONS, CUSTOM_RULES - ) + ), + overrides: [ + { + files: ['test/**/*.js'], + parserOptions: { + ecmaVersion: 2017 + } + } + ] }; diff --git a/lib/Model/bundle.js b/lib/Model/bundle.js index 5056e7160..5dfd2a7e1 100644 --- a/lib/Model/bundle.js +++ b/lib/Model/bundle.js @@ -1,5 +1,6 @@ var Model = require('./Model'); var defaultType = require('sharedb/lib/client').types.defaultType; +var promisify = require('../util').promisify; Model.BUNDLE_TIMEOUT = 10 * 1000; @@ -35,6 +36,7 @@ Model.prototype.bundle = function(cb) { cb(null, bundle); }); }; +Model.prototype.bundlePromised = promisify(Model.prototype.bundle); function stripComputed(root) { var silentModel = root.silent(); diff --git a/lib/Model/connection.js b/lib/Model/connection.js index 015c2037f..c0246b369 100644 --- a/lib/Model/connection.js +++ b/lib/Model/connection.js @@ -2,6 +2,7 @@ var Connection = require('sharedb/lib/client').Connection; var Model = require('./Model'); var LocalDoc = require('./LocalDoc'); var RemoteDoc = require('./RemoteDoc'); +var promisify = require('../util').promisify; Model.INITS.push(function(model) { model.root._preventCompose = false; @@ -51,13 +52,16 @@ Model.prototype._finishCreateConnection = function() { Model.prototype.connect = function() { this.root.socket.open(); }; + Model.prototype.disconnect = function() { this.root.socket.close(); }; + Model.prototype.reconnect = function() { this.disconnect(); this.connect(); }; + // Clean delayed disconnect Model.prototype.close = function(cb) { cb = this.wrapCallback(cb); @@ -67,6 +71,7 @@ Model.prototype.close = function(cb) { cb(); }); }; +Model.prototype.closePromised = promisify(Model.prototype.close); // Returns a reference to the ShareDB agent if it is connected directly on the // server. Will return null if the ShareDB connection has been disconnected or @@ -92,9 +97,12 @@ Model.prototype._getDocConstructor = function(name) { Model.prototype.hasPending = function() { return this.root.connection.hasPending(); }; + Model.prototype.hasWritePending = function() { return this.root.connection.hasWritePending(); }; + Model.prototype.whenNothingPending = function(cb) { return this.root.connection.whenNothingPending(cb); }; +Model.prototype.whenNothingPendingPromised = promisify(Model.prototype.whenNothingPending); diff --git a/lib/Model/mutators.js b/lib/Model/mutators.js index 5271e2753..0eb37446a 100644 --- a/lib/Model/mutators.js +++ b/lib/Model/mutators.js @@ -5,6 +5,7 @@ var ChangeEvent = mutationEvents.ChangeEvent; var InsertEvent = mutationEvents.InsertEvent; var RemoveEvent = mutationEvents.RemoveEvent; var MoveEvent = mutationEvents.MoveEvent; +var promisify = util.promisify; Model.prototype._mutate = function(segments, fn, cb) { cb = this.wrapCallback(cb); @@ -42,6 +43,8 @@ Model.prototype.set = function() { var segments = this._splitPath(subpath); return this._set(segments, value, cb); }; +Model.prototype.setPromised = promisify(Model.prototype.set); + Model.prototype._set = function(segments, value, cb) { segments = this._dereference(segments); var model = this; @@ -72,6 +75,8 @@ Model.prototype.setNull = function() { var segments = this._splitPath(subpath); return this._setNull(segments, value, cb); }; +Model.prototype.setNullPromised = promisify(Model.prototype.setNull); + Model.prototype._setNull = function(segments, value, cb) { segments = this._dereference(segments); var model = this; @@ -104,6 +109,8 @@ Model.prototype.setEach = function() { var segments = this._splitPath(subpath); return this._setEach(segments, object, cb); }; +Model.prototype.setEachPromised = promisify(Model.prototype.setEach); + Model.prototype._setEach = function(segments, object, cb) { segments = this._dereference(segments); var group = util.asyncGroup(this.wrapCallback(cb)); @@ -140,6 +147,8 @@ Model.prototype.create = function() { var segments = this._splitPath(subpath); return this._create(segments, value, cb); }; +Model.prototype.createPromised = promisify(Model.prototype.create); + Model.prototype._create = function(segments, value, cb) { segments = this._dereference(segments); if (segments.length !== 2) { @@ -188,6 +197,8 @@ Model.prototype.createNull = function() { var segments = this._splitPath(subpath); return this._createNull(segments, value, cb); }; +Model.prototype.createNullPromised = promisify(Model.prototype.createNull); + Model.prototype._createNull = function(segments, value, cb) { segments = this._dereference(segments); var doc = this.getDoc(segments[0], segments[1]); @@ -222,6 +233,8 @@ Model.prototype.add = function() { var segments = this._splitPath(subpath); return this._add(segments, value, cb); }; +Model.prototype.addPromised = promisify(Model.prototype.add); + Model.prototype._add = function(segments, value, cb) { if (typeof value !== 'object') { var message = 'add requires an object value. Invalid value: ' + value; @@ -264,6 +277,8 @@ Model.prototype.del = function() { var segments = this._splitPath(subpath); return this._del(segments, cb); }; +Model.prototype.delPromised = promisify(Model.prototype.del); + Model.prototype._del = function(segments, cb) { segments = this._dereference(segments); return this._delNoDereference(segments, cb); @@ -316,6 +331,8 @@ Model.prototype.increment = function() { var segments = this._splitPath(subpath); return this._increment(segments, byNumber, cb); }; +Model.prototype.incrementPromised = promisify(Model.prototype.increment); + Model.prototype._increment = function(segments, byNumber, cb) { segments = this._dereference(segments); if (byNumber == null) byNumber = 1; @@ -345,6 +362,8 @@ Model.prototype.push = function() { var segments = this._splitPath(subpath); return this._push(segments, value, cb); }; +Model.prototype.pushPromised = promisify(Model.prototype.push); + Model.prototype._push = function(segments, value, cb) { var forArrayMutator = true; segments = this._dereference(segments, forArrayMutator); @@ -373,6 +392,8 @@ Model.prototype.unshift = function() { var segments = this._splitPath(subpath); return this._unshift(segments, value, cb); }; +Model.prototype.unshiftPromised = promisify(Model.prototype.unshift); + Model.prototype._unshift = function(segments, value, cb) { var forArrayMutator = true; segments = this._dereference(segments, forArrayMutator); @@ -406,6 +427,8 @@ Model.prototype.insert = function() { var segments = this._splitPath(subpath); return this._insert(segments, +index, values, cb); }; +Model.prototype.insertPromised = promisify(Model.prototype.insert); + Model.prototype._insert = function(segments, index, values, cb) { var forArrayMutator = true; segments = this._dereference(segments, forArrayMutator); @@ -435,6 +458,8 @@ Model.prototype.pop = function() { var segments = this._splitPath(subpath); return this._pop(segments, cb); }; +Model.prototype.popPromised = promisify(Model.prototype.pop); + Model.prototype._pop = function(segments, cb) { var forArrayMutator = true; segments = this._dereference(segments, forArrayMutator); @@ -469,6 +494,8 @@ Model.prototype.shift = function() { var segments = this._splitPath(subpath); return this._shift(segments, cb); }; +Model.prototype.shiftPromised = promisify(Model.prototype.shift); + Model.prototype._shift = function(segments, cb) { var forArrayMutator = true; segments = this._dereference(segments, forArrayMutator); @@ -535,6 +562,8 @@ Model.prototype.remove = function() { if (index == null) index = segments.pop(); return this._remove(segments, +index, howMany, cb); }; +Model.prototype.removePromised = promisify(Model.prototype.remove); + Model.prototype._remove = function(segments, index, howMany, cb) { var forArrayMutator = true; segments = this._dereference(segments, forArrayMutator); @@ -598,6 +627,8 @@ Model.prototype.move = function() { var segments = this._splitPath(subpath); return this._move(segments, from, to, howMany, cb); }; +Model.prototype.movePromised = promisify(Model.prototype.move); + Model.prototype._move = function(segments, from, to, howMany, cb) { var forArrayMutator = true; segments = this._dereference(segments, forArrayMutator); @@ -647,6 +678,8 @@ Model.prototype.stringInsert = function() { var segments = this._splitPath(subpath); return this._stringInsert(segments, index, text, cb); }; +Model.prototype.stringInsertPromised = promisify(Model.prototype.stringInsert); + Model.prototype._stringInsert = function(segments, index, text, cb) { segments = this._dereference(segments); var model = this; @@ -687,6 +720,8 @@ Model.prototype.stringRemove = function() { var segments = this._splitPath(subpath); return this._stringRemove(segments, index, howMany, cb); }; +Model.prototype.stringRemovePromised = promisify(Model.prototype.stringRemove); + Model.prototype._stringRemove = function(segments, index, howMany, cb) { segments = this._dereference(segments); var model = this; @@ -727,6 +762,7 @@ Model.prototype.subtypeSubmit = function() { var segments = this._splitPath(subpath); return this._subtypeSubmit(segments, subtype, subtypeOp, cb); }; +Model.prototype.subtypeSubmitPromised = promisify(Model.prototype.subtypeSubmit); Model.prototype._subtypeSubmit = function(segments, subtype, subtypeOp, cb) { segments = this._dereference(segments); diff --git a/lib/Model/setDiff.js b/lib/Model/setDiff.js index 4f8de66f4..35d792968 100644 --- a/lib/Model/setDiff.js +++ b/lib/Model/setDiff.js @@ -6,6 +6,7 @@ var ChangeEvent = mutationEvents.ChangeEvent; var InsertEvent = mutationEvents.InsertEvent; var RemoveEvent = mutationEvents.RemoveEvent; var MoveEvent = mutationEvents.MoveEvent; +var promisify = util.promisify; Model.prototype.setDiff = function() { var subpath, value, cb; @@ -22,6 +23,8 @@ Model.prototype.setDiff = function() { var segments = this._splitPath(subpath); return this._setDiff(segments, value, cb); }; +Model.prototype.setDiffPromised = promisify(Model.prototype.setDiff); + Model.prototype._setDiff = function(segments, value, cb) { segments = this._dereference(segments); var model = this; @@ -54,6 +57,8 @@ Model.prototype.setDiffDeep = function() { var segments = this._splitPath(subpath); return this._setDiffDeep(segments, value, cb); }; +Model.prototype.setDiffDeepPromised = promisify(Model.prototype.setDiffDeep); + Model.prototype._setDiffDeep = function(segments, value, cb) { var before = this._get(segments); cb = this.wrapCallback(cb); @@ -62,6 +67,7 @@ Model.prototype._setDiffDeep = function(segments, value, cb) { diffDeep(this, segments, before, value, group); finished(); }; + function diffDeep(model, segments, before, after, group) { if (typeof before !== 'object' || !before || typeof after !== 'object' || !after) { @@ -120,6 +126,8 @@ Model.prototype.setArrayDiff = function() { var segments = this._splitPath(subpath); return this._setArrayDiff(segments, value, cb); }; +Model.prototype.setArrayDiffPromised = promisify(Model.prototype.setArrayDiff); + Model.prototype.setArrayDiffDeep = function() { var subpath, value, cb; if (arguments.length === 1) { @@ -135,9 +143,12 @@ Model.prototype.setArrayDiffDeep = function() { var segments = this._splitPath(subpath); return this._setArrayDiffDeep(segments, value, cb); }; +Model.prototype.setArrayDiffDeepPromised = promisify(Model.prototype.setArrayDiffDeep); + Model.prototype._setArrayDiffDeep = function(segments, value, cb) { return this._setArrayDiff(segments, value, cb, util.deepEqual); }; + Model.prototype._setArrayDiff = function(segments, value, cb, _equalFn) { var before = this._get(segments); if (before === value) return this.wrapCallback(cb)(); @@ -148,6 +159,7 @@ Model.prototype._setArrayDiff = function(segments, value, cb, _equalFn) { var diff = arrayDiff(before, value, _equalFn); this._applyArrayDiff(segments, diff, cb); }; + Model.prototype._applyArrayDiff = function(segments, diff, cb) { if (!diff.length) return this.wrapCallback(cb)(); segments = this._dereference(segments); diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 7c5c8bf25..06eb9b5c4 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -4,6 +4,7 @@ var Query = require('./Query'); var CollectionCounter = require('./CollectionCounter'); var mutationEvents = require('./events').mutationEvents; var UnloadEvent = mutationEvents.UnloadEvent; +var promisify = util.promisify; Model.INITS.push(function(model, options) { model.root.fetchOnly = options.fetchOnly; @@ -19,18 +20,38 @@ Model.prototype.fetch = function() { this._forSubscribable(arguments, 'fetch'); return this; }; +Model.prototype.fetchPromised = promisify(Model.prototype.fetch); +// Model.prototype.fetchPromised = function() { +// var args = Array.prototype.slice.apply(arguments); +// return new Promise((resolve, reject) => { +// var callback = (err) => { +// if (err) { +// reject(err); +// return; +// } +// resolve(); +// }; +// this._forSubscribable(args.concat(callback), 'fetch'); +// }); +// }; + Model.prototype.unfetch = function() { this._forSubscribable(arguments, 'unfetch'); return this; }; +Model.prototype.unfetchPromised = promisify(Model.prototype.unfetch); + Model.prototype.subscribe = function() { this._forSubscribable(arguments, 'subscribe'); return this; }; +Model.prototype.subscribePromised = promisify(Model.prototype.subscribe); + Model.prototype.unsubscribe = function() { this._forSubscribable(arguments, 'unsubscribe'); return this; }; +Model.prototype.unsubscribePromised = promisify(Model.prototype.unsubscribe); Model.prototype._forSubscribable = function(argumentsObject, method) { var args, cb; @@ -88,6 +109,7 @@ Model.prototype.fetchDoc = function(collectionName, id, cb) { var doc = this.getOrCreateDoc(collectionName, id); doc.shareDoc.fetch(cb); }; +Model.prototype.fetchDocPromised = promisify(Model.prototype.fetchDoc); Model.prototype.subscribeDoc = function(collectionName, id, cb) { cb = this.wrapCallback(cb); @@ -109,6 +131,7 @@ Model.prototype.subscribeDoc = function(collectionName, id, cb) { doc.shareDoc.subscribe(cb); } }; +Model.prototype.subscribeDocPromised = promisify(Model.prototype.subscribeDoc); Model.prototype.unfetchDoc = function(collectionName, id, cb) { cb = this.wrapCallback(cb); @@ -130,6 +153,7 @@ Model.prototype.unfetchDoc = function(collectionName, id, cb) { cb(null, 0); } }; +Model.prototype.unfetchDocPromised = promisify(Model.prototype.unfetchDoc); Model.prototype.unsubscribeDoc = function(collectionName, id, cb) { cb = this.wrapCallback(cb); @@ -166,6 +190,7 @@ Model.prototype.unsubscribeDoc = function(collectionName, id, cb) { cb(null, 0); } }; +Model.prototype.unsubscribeDocPromised = promisify(Model.prototype.unsubscribeDoc); // Removes the document from the local model if the model no longer has any // remaining fetches or subscribes via a query or direct loading diff --git a/lib/util.js b/lib/util.js index 398014b81..098ce66f2 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,4 +1,7 @@ var deepEqual = require('fast-deep-equal'); +// keep trailing slash on require to ensure node_modules/util is +// used instead of the node built-in `util` +var util = require('util/'); var isServer = process.title !== 'browser'; exports.isServer = isServer; @@ -14,9 +17,10 @@ exports.equal = equal; exports.equalsNaN = equalsNaN; exports.isArrayIndex = isArrayIndex; exports.lookup = lookup; -exports.mergeInto = mergeInto; exports.mayImpact = mayImpact; exports.mayImpactAny = mayImpactAny; +exports.mergeInto = mergeInto; +exports.promisify = util.promisify; exports.serverRequire = serverRequire; exports.serverUse = serverUse; exports.use = use; diff --git a/package.json b/package.json index b8f2e4cd9..2d39f61eb 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,14 @@ "scripts": { "lint": "eslint --ignore-path .gitignore .", "test": "node_modules/.bin/mocha", - "posttest": "npm run lint", + "checks": "npm run lint && npm test", "test-cover": "node_modules/nyc/bin/nyc.js --temp-dir=coverage -r text -r lcov node_modules/mocha/bin/_mocha" }, "dependencies": { "arraydiff": "^0.1.1", "fast-deep-equal": "^2.0.1", "sharedb": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0", + "util": "^0.12.5", "uuid": "^2.0.1" }, "devDependencies": { diff --git a/test/Model/loading.js b/test/Model/loading.js index 02841875f..82d2e6dc6 100644 --- a/test/Model/loading.js +++ b/test/Model/loading.js @@ -14,6 +14,39 @@ describe('loading', function() { this.model.connection.on('connected', done); }); + describe('fetch', function() { + beforeEach(function(done) { + this.setupModel = this.backend.createModel(); + this.setupModel.add('foo', {id: '1', name: 'foo-1'}, done); + }); + + it('calls callback after fetch completes', function(done) { + var model = this.model; + model.fetch('foo.1', function(err) { + if (err) done(err); + var doc = model.get('foo.1'); + expect(doc).to.have.property('id', '1'); + expect(doc).to.have.property('name', 'foo-1'); + done(); + }); + }); + }); + + describe('fetchPromised', function() { + beforeEach(function(done) { + this.setupModel = this.backend.createModel(); + this.setupModel.add('foo', {id: '2', name: 'foo-2'}, done); + }); + + it('resolves promise when fetch completes', async function() { + var model = this.model; + await model.fetchPromised('foo.2'); + var doc = model.get('foo.2'); + expect(doc).to.have.property('id', '2'); + expect(doc).to.have.property('name', 'foo-2'); + }); + }); + describe('subscribe', function() { it('calls back simultaneous subscribes to the same document', function(done) { var doc = this.model.connection.get('colors', 'green'); @@ -46,6 +79,16 @@ describe('loading', function() { }); }); + describe('subscribePromised', function() { + it('resolves promise when subscribe complete', async function() { + var model = this.model; + var doc = model.connection.get('colors', 'green'); + expect(doc.subscribed).equal(false); + await model.subscribePromised('colors.green'); + expect(doc.subscribed).equal(true); + }); + }); + describe('unfetch deferred unload', function() { beforeEach(function(done) { this.setupModel = this.backend.createModel(); From a0713fae29f663ff5ad28a6ef900553529647ad5 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 2 Jun 2023 15:00:26 -0700 Subject: [PATCH 275/479] Remove commented impl --- lib/Model/subscriptions.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/Model/subscriptions.js b/lib/Model/subscriptions.js index 06eb9b5c4..e9004d2c5 100644 --- a/lib/Model/subscriptions.js +++ b/lib/Model/subscriptions.js @@ -21,19 +21,6 @@ Model.prototype.fetch = function() { return this; }; Model.prototype.fetchPromised = promisify(Model.prototype.fetch); -// Model.prototype.fetchPromised = function() { -// var args = Array.prototype.slice.apply(arguments); -// return new Promise((resolve, reject) => { -// var callback = (err) => { -// if (err) { -// reject(err); -// return; -// } -// resolve(); -// }; -// this._forSubscribable(args.concat(callback), 'fetch'); -// }); -// }; Model.prototype.unfetch = function() { this._forSubscribable(arguments, 'unfetch'); From e6f97523c484a876fade866a4beb5ab8bb667e2f Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 5 Jun 2023 12:10:52 -0700 Subject: [PATCH 276/479] Add test for composition fo sequential promised ops and bundle --- test/Model/RemoteDoc.js | 25 +++++++++++++++++++++++++ test/Model/bundle.js | 12 ++++++++++++ 2 files changed, 37 insertions(+) diff --git a/test/Model/RemoteDoc.js b/test/Model/RemoteDoc.js index a09714b27..0fdaec5ce 100644 --- a/test/Model/RemoteDoc.js +++ b/test/Model/RemoteDoc.js @@ -83,5 +83,30 @@ describe('RemoteDoc', function() { }); }); }); + + describe('promised operations', function() { + beforeEach(function() { + this.backend = racer.createBackend(); + this.model = this.backend.createModel(); + }); + + it('composes sequential operations', async function () { + var model = this.model; + await model.addPromised('notes', { id: 'my-note', score: 1 }); + var $note = model.at('notes.my-note'); + var shareDoc = model.connection.get('notes', 'my-note'); + expect(shareDoc).to.have.property('version', 1); + await Promise.all([ + $note.pushPromised('labels', 'Label A'), + $note.incrementPromised('score',2), + $note.pushPromised('labels', 'Label B'), + ]); + // Writes initiated in the same event loop should be composed into a single op + expect(shareDoc).to.have.property('version', 2); + expect($note.get('labels')).to.eql(['Label A', 'Label B']); + expect($note.get('score')).to.equal(3); + }); + }); + docs(createDoc); }); diff --git a/test/Model/bundle.js b/test/Model/bundle.js index 4152897e1..eafefa54b 100644 --- a/test/Model/bundle.js +++ b/test/Model/bundle.js @@ -57,4 +57,16 @@ describe('bundle', function() { done(); }); }); + + describe('bundlePromised', function() { + it('awaits bundle completion', async function() { + var backend = racer.createBackend(); + var model = backend.createModel(); + model.add('dogs', {id: 'coco', name: 'Coco'}); + var bundled = await model.bundlePromised(); + var model2 = backend.createModel(); + model2.unbundle(bundled); + expect(model2.get('dogs.coco')).to.deep.equal(model.get('dogs.coco')); + }); + }); }); From 92fad09de4ddd3d9f49dca19e44f2fe473dcc5cf Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 5 Jun 2023 12:25:24 -0700 Subject: [PATCH 277/479] Lint fixes --- test/Model/RemoteDoc.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Model/RemoteDoc.js b/test/Model/RemoteDoc.js index 0fdaec5ce..af10b94f7 100644 --- a/test/Model/RemoteDoc.js +++ b/test/Model/RemoteDoc.js @@ -90,16 +90,16 @@ describe('RemoteDoc', function() { this.model = this.backend.createModel(); }); - it('composes sequential operations', async function () { + it('composes sequential operations', async function() { var model = this.model; - await model.addPromised('notes', { id: 'my-note', score: 1 }); + await model.addPromised('notes', {id: 'my-note', score: 1}); var $note = model.at('notes.my-note'); var shareDoc = model.connection.get('notes', 'my-note'); expect(shareDoc).to.have.property('version', 1); await Promise.all([ $note.pushPromised('labels', 'Label A'), - $note.incrementPromised('score',2), - $note.pushPromised('labels', 'Label B'), + $note.incrementPromised('score', 2), + $note.pushPromised('labels', 'Label B') ]); // Writes initiated in the same event loop should be composed into a single op expect(shareDoc).to.have.property('version', 2); From ef81d91cb5159c204306c41b62ec0010d1f049c5 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 5 Jun 2023 16:39:56 -0700 Subject: [PATCH 278/479] Add internal promisify implementation w tests --- lib/util.js | 39 ++++++++++++++++++++++++++++++++++----- test/util/util.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/lib/util.js b/lib/util.js index 098ce66f2..9f1b00a6a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,8 +1,4 @@ var deepEqual = require('fast-deep-equal'); -// keep trailing slash on require to ensure node_modules/util is -// used instead of the node built-in `util` -var util = require('util/'); - var isServer = process.title !== 'browser'; exports.isServer = isServer; @@ -20,7 +16,7 @@ exports.lookup = lookup; exports.mayImpact = mayImpact; exports.mayImpactAny = mayImpactAny; exports.mergeInto = mergeInto; -exports.promisify = util.promisify; +exports.promisify = promisify; exports.serverRequire = serverRequire; exports.serverUse = serverUse; exports.use = use; @@ -167,6 +163,39 @@ function mergeInto(to, from) { return to; } +function promisify(original) { + if (typeof original !== 'function') { + throw new TypeError('The "original" argument must be of type Function'); + } + + function fn() { + var promiseResolve, promiseReject; + var promise = new Promise(function(resolve, reject) { + promiseResolve = resolve; + promiseReject = reject; + }); + + var args = Array.prototype.slice.apply(arguments); + args.push(function(err, value) { + if (err) { + promiseReject(err); + } else { + promiseResolve(value); + } + }); + + try { + original.apply(this, args); + } catch (err) { + promiseReject(err); + } + + return promise; + } + + return fn; +} + function serverRequire(module, id) { if (!isServer) return; return module.require(id); diff --git a/test/util/util.js b/test/util/util.js index 7246fcaed..6d49a6b0f 100644 --- a/test/util/util.js +++ b/test/util/util.js @@ -31,4 +31,46 @@ describe('util', function() { expect(b).to.eql({x: 7, z: {}}); }); }); + + describe('promisify', function() { + it('wrapped functions return promise', async function() { + var targetFn = function(num, cb) { + setImmediate(function() { + cb(undefined, num); + }); + }; + var promisedFn = util.promisify(targetFn); + var promise = promisedFn(3); + expect(promise).to.be.instanceOf(Promise); + var result = await promise; + expect(result).to.equal(3); + }); + + it('wrapped functions throw errors passed to callback', async function() { + var targetFn = function(num, cb) { + setImmediate(function() { + cb(new Error(`Error ${num}`)); + }); + }; + var promisedFn = util.promisify(targetFn); + try { + await promisedFn(3); + } catch (error) { + expect(error).to.have.property('message', 'Error 3'); + } + }); + + it('wrapped functions throw on thrown error', async function() { + var targetFn = function(num) { + throw new Error(`Error ${num}`); + }; + var promisedFn = util.promisify(targetFn); + try { + console.log('pre-await'); + await promisedFn(3); + } catch (error) { + expect(error).to.have.property('message', 'Error 3'); + } + }); + }); }); From 80a6e4229d335e32a9988cd80813b34e93cef2f8 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 5 Jun 2023 17:09:12 -0700 Subject: [PATCH 279/479] Ensure tests dont pass if promise resolves --- .eslintrc.js | 1 + package.json | 3 ++- test/util/util.js | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 85529969f..fb7152865 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,6 +26,7 @@ var CUSTOM_RULES = { module.exports = { extends: 'google', + ignorePatterns: ['.gitignore'], parserOptions: { ecmaVersion: 5 }, diff --git a/package.json b/package.json index 2d39f61eb..a874db842 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "version": "1.0.6", "main": "./lib/index.js", "scripts": { - "lint": "eslint --ignore-path .gitignore .", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "node_modules/.bin/mocha", "checks": "npm run lint && npm test", "test-cover": "node_modules/nyc/bin/nyc.js --temp-dir=coverage -r text -r lcov node_modules/mocha/bin/_mocha" diff --git a/test/util/util.js b/test/util/util.js index 6d49a6b0f..b48c19723 100644 --- a/test/util/util.js +++ b/test/util/util.js @@ -55,6 +55,7 @@ describe('util', function() { var promisedFn = util.promisify(targetFn); try { await promisedFn(3); + fail('Expected promisedFn to reject, but it successfully resolved'); } catch (error) { expect(error).to.have.property('message', 'Error 3'); } @@ -66,8 +67,8 @@ describe('util', function() { }; var promisedFn = util.promisify(targetFn); try { - console.log('pre-await'); await promisedFn(3); + fail('Expected promisedFn to reject, but it successfully resolved'); } catch (error) { expect(error).to.have.property('message', 'Error 3'); } From b0694a75bbfe3c55974a0c48757f77d090f12b31 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 6 Jun 2023 11:08:12 -0700 Subject: [PATCH 280/479] Add promised versions for Query fetch, unfetch, subscribe, unsubscribe --- lib/Model/Query.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/Model/Query.js b/lib/Model/Query.js index 039dcc2ed..a5b13b8b4 100644 --- a/lib/Model/Query.js +++ b/lib/Model/Query.js @@ -2,6 +2,7 @@ var util = require('../util'); var Model = require('./Model'); var CollectionMap = require('./CollectionMap'); var defaultType = require('sharedb/lib/client').types.defaultType; +var promisify = util.promisify; module.exports = Query; @@ -215,6 +216,8 @@ Query.prototype.fetch = function(cb) { return this; }; +Query.prototype.fetchPromised = promisify(Query.prototype.fetch); + Query.prototype.subscribe = function(cb) { cb = this.model.wrapCallback(cb); this.context.subscribeQuery(this); @@ -249,6 +252,8 @@ Query.prototype.subscribe = function(cb) { return this; }; +Query.prototype.subscribePromised = promisify(Query.prototype.subscribe); + Query.prototype._subscribeCb = function(cb) { var query = this; return function subscribeCb(err, results, extra) { @@ -403,6 +408,8 @@ Query.prototype.unfetch = function(cb) { return this; }; +Query.prototype.unfetchPromised = promisify(Query.prototype.unfetch); + Query.prototype.unsubscribe = function(cb) { cb = this.model.wrapCallback(cb); this.context.unsubscribeQuery(this); @@ -439,6 +446,8 @@ Query.prototype.unsubscribe = function(cb) { return this; }; +Query.prototype.unsubscribePromised = promisify(Query.prototype.unsubscribe); + Query.prototype._getShareResults = function() { var ids = this.model._get(this.idsSegments); if (!ids) return; From 51a1fe1332aeaa65e16235aaefa791670f614ad1 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 6 Jun 2023 13:44:38 -0700 Subject: [PATCH 281/479] 1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a874db842..7f26fa671 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "1.0.6", + "version": "1.1.0", "main": "./lib/index.js", "scripts": { "lint": "eslint .", From 0a6788df4d48e7ed1fd6ee656eb75fe29a43932c Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 22 Jun 2023 16:38:59 -0700 Subject: [PATCH 282/479] Move lib to src --- {lib => src}/Backend.js | 0 {lib => src}/Model/CollectionCounter.js | 0 {lib => src}/Model/CollectionMap.js | 0 {lib => src}/Model/Doc.js | 0 {lib => src}/Model/EventListenerTree.js | 0 {lib => src}/Model/EventMapTree.js | 0 {lib => src}/Model/FastMap.js | 0 {lib => src}/Model/LocalDoc.js | 0 {lib => src}/Model/Model.js | 0 {lib => src}/Model/ModelStandalone.js | 0 {lib => src}/Model/Query.js | 0 {lib => src}/Model/RemoteDoc.js | 0 {lib => src}/Model/bundle.js | 0 {lib => src}/Model/collections.js | 0 {lib => src}/Model/connection.js | 0 {lib => src}/Model/connection.server.js | 0 {lib => src}/Model/contexts.js | 0 {lib => src}/Model/defaultFns.js | 0 {lib => src}/Model/events.js | 0 {lib => src}/Model/filter.js | 0 {lib => src}/Model/fn.js | 0 {lib => src}/Model/index.js | 0 {lib => src}/Model/mutators.js | 0 {lib => src}/Model/paths.js | 0 {lib => src}/Model/ref.js | 0 {lib => src}/Model/refList.js | 0 {lib => src}/Model/setDiff.js | 0 {lib => src}/Model/subscriptions.js | 0 {lib => src}/Model/unbundle.js | 0 {lib => src}/Racer.js | 0 {lib => src}/Racer.server.js | 0 {lib => src}/index.js | 0 {lib => src}/util.js | 0 33 files changed, 0 insertions(+), 0 deletions(-) rename {lib => src}/Backend.js (100%) rename {lib => src}/Model/CollectionCounter.js (100%) rename {lib => src}/Model/CollectionMap.js (100%) rename {lib => src}/Model/Doc.js (100%) rename {lib => src}/Model/EventListenerTree.js (100%) rename {lib => src}/Model/EventMapTree.js (100%) rename {lib => src}/Model/FastMap.js (100%) rename {lib => src}/Model/LocalDoc.js (100%) rename {lib => src}/Model/Model.js (100%) rename {lib => src}/Model/ModelStandalone.js (100%) rename {lib => src}/Model/Query.js (100%) rename {lib => src}/Model/RemoteDoc.js (100%) rename {lib => src}/Model/bundle.js (100%) rename {lib => src}/Model/collections.js (100%) rename {lib => src}/Model/connection.js (100%) rename {lib => src}/Model/connection.server.js (100%) rename {lib => src}/Model/contexts.js (100%) rename {lib => src}/Model/defaultFns.js (100%) rename {lib => src}/Model/events.js (100%) rename {lib => src}/Model/filter.js (100%) rename {lib => src}/Model/fn.js (100%) rename {lib => src}/Model/index.js (100%) rename {lib => src}/Model/mutators.js (100%) rename {lib => src}/Model/paths.js (100%) rename {lib => src}/Model/ref.js (100%) rename {lib => src}/Model/refList.js (100%) rename {lib => src}/Model/setDiff.js (100%) rename {lib => src}/Model/subscriptions.js (100%) rename {lib => src}/Model/unbundle.js (100%) rename {lib => src}/Racer.js (100%) rename {lib => src}/Racer.server.js (100%) rename {lib => src}/index.js (100%) rename {lib => src}/util.js (100%) diff --git a/lib/Backend.js b/src/Backend.js similarity index 100% rename from lib/Backend.js rename to src/Backend.js diff --git a/lib/Model/CollectionCounter.js b/src/Model/CollectionCounter.js similarity index 100% rename from lib/Model/CollectionCounter.js rename to src/Model/CollectionCounter.js diff --git a/lib/Model/CollectionMap.js b/src/Model/CollectionMap.js similarity index 100% rename from lib/Model/CollectionMap.js rename to src/Model/CollectionMap.js diff --git a/lib/Model/Doc.js b/src/Model/Doc.js similarity index 100% rename from lib/Model/Doc.js rename to src/Model/Doc.js diff --git a/lib/Model/EventListenerTree.js b/src/Model/EventListenerTree.js similarity index 100% rename from lib/Model/EventListenerTree.js rename to src/Model/EventListenerTree.js diff --git a/lib/Model/EventMapTree.js b/src/Model/EventMapTree.js similarity index 100% rename from lib/Model/EventMapTree.js rename to src/Model/EventMapTree.js diff --git a/lib/Model/FastMap.js b/src/Model/FastMap.js similarity index 100% rename from lib/Model/FastMap.js rename to src/Model/FastMap.js diff --git a/lib/Model/LocalDoc.js b/src/Model/LocalDoc.js similarity index 100% rename from lib/Model/LocalDoc.js rename to src/Model/LocalDoc.js diff --git a/lib/Model/Model.js b/src/Model/Model.js similarity index 100% rename from lib/Model/Model.js rename to src/Model/Model.js diff --git a/lib/Model/ModelStandalone.js b/src/Model/ModelStandalone.js similarity index 100% rename from lib/Model/ModelStandalone.js rename to src/Model/ModelStandalone.js diff --git a/lib/Model/Query.js b/src/Model/Query.js similarity index 100% rename from lib/Model/Query.js rename to src/Model/Query.js diff --git a/lib/Model/RemoteDoc.js b/src/Model/RemoteDoc.js similarity index 100% rename from lib/Model/RemoteDoc.js rename to src/Model/RemoteDoc.js diff --git a/lib/Model/bundle.js b/src/Model/bundle.js similarity index 100% rename from lib/Model/bundle.js rename to src/Model/bundle.js diff --git a/lib/Model/collections.js b/src/Model/collections.js similarity index 100% rename from lib/Model/collections.js rename to src/Model/collections.js diff --git a/lib/Model/connection.js b/src/Model/connection.js similarity index 100% rename from lib/Model/connection.js rename to src/Model/connection.js diff --git a/lib/Model/connection.server.js b/src/Model/connection.server.js similarity index 100% rename from lib/Model/connection.server.js rename to src/Model/connection.server.js diff --git a/lib/Model/contexts.js b/src/Model/contexts.js similarity index 100% rename from lib/Model/contexts.js rename to src/Model/contexts.js diff --git a/lib/Model/defaultFns.js b/src/Model/defaultFns.js similarity index 100% rename from lib/Model/defaultFns.js rename to src/Model/defaultFns.js diff --git a/lib/Model/events.js b/src/Model/events.js similarity index 100% rename from lib/Model/events.js rename to src/Model/events.js diff --git a/lib/Model/filter.js b/src/Model/filter.js similarity index 100% rename from lib/Model/filter.js rename to src/Model/filter.js diff --git a/lib/Model/fn.js b/src/Model/fn.js similarity index 100% rename from lib/Model/fn.js rename to src/Model/fn.js diff --git a/lib/Model/index.js b/src/Model/index.js similarity index 100% rename from lib/Model/index.js rename to src/Model/index.js diff --git a/lib/Model/mutators.js b/src/Model/mutators.js similarity index 100% rename from lib/Model/mutators.js rename to src/Model/mutators.js diff --git a/lib/Model/paths.js b/src/Model/paths.js similarity index 100% rename from lib/Model/paths.js rename to src/Model/paths.js diff --git a/lib/Model/ref.js b/src/Model/ref.js similarity index 100% rename from lib/Model/ref.js rename to src/Model/ref.js diff --git a/lib/Model/refList.js b/src/Model/refList.js similarity index 100% rename from lib/Model/refList.js rename to src/Model/refList.js diff --git a/lib/Model/setDiff.js b/src/Model/setDiff.js similarity index 100% rename from lib/Model/setDiff.js rename to src/Model/setDiff.js diff --git a/lib/Model/subscriptions.js b/src/Model/subscriptions.js similarity index 100% rename from lib/Model/subscriptions.js rename to src/Model/subscriptions.js diff --git a/lib/Model/unbundle.js b/src/Model/unbundle.js similarity index 100% rename from lib/Model/unbundle.js rename to src/Model/unbundle.js diff --git a/lib/Racer.js b/src/Racer.js similarity index 100% rename from lib/Racer.js rename to src/Racer.js diff --git a/lib/Racer.server.js b/src/Racer.server.js similarity index 100% rename from lib/Racer.server.js rename to src/Racer.server.js diff --git a/lib/index.js b/src/index.js similarity index 100% rename from lib/index.js rename to src/index.js diff --git a/lib/util.js b/src/util.js similarity index 100% rename from lib/util.js rename to src/util.js From 2c74a81fd1ad30d86784f63f28b2dfba513e8986 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 22 Jun 2023 16:40:24 -0700 Subject: [PATCH 283/479] Add tsconfig and package scripts for typescript transpilation of js sources to lib --- .gitignore | 1 + package.json | 6 ++++++ tsconfig.json | 15 +++++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 33ec369f0..e09228133 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.swp node_modules coverage +lib/ diff --git a/package.json b/package.json index 7f26fa671..1e9fc4968 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,15 @@ }, "version": "1.1.0", "main": "./lib/index.js", + "files": ["lib/**/*.js"], "scripts": { + "build": "npx tsc", "lint": "eslint .", "lint:fix": "eslint --fix .", + "pretest": "npm run build", "test": "node_modules/.bin/mocha", "checks": "npm run lint && npm test", + "prepare": "npm build", "test-cover": "node_modules/nyc/bin/nyc.js --temp-dir=coverage -r text -r lcov node_modules/mocha/bin/_mocha" }, "dependencies": { @@ -23,6 +27,8 @@ "uuid": "^2.0.1" }, "devDependencies": { + "@types/events": "^3.0.0", + "@types/node": "^20.3.1", "chai": "^4.2.0", "coveralls": "^3.0.5", "eslint": "^8.1.0", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..f1eab985b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "allowJs": true, + "ignoreDeprecations": "5.0", + "lib":[], + "module": "CommonJS", + "noImplicitUseStrict": true, + "outDir": "lib", + "sourceMap": false, + "target": "ES5" + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file From 82948ac6ee728f486aa2bd396979c945c154654e Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 22 Jun 2023 16:46:23 -0700 Subject: [PATCH 284/479] Change var name and remove jsdoc types to pass tsc build --- package.json | 5 +++-- src/Model/events.js | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 1e9fc4968..081fb4596 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ }, "version": "1.1.0", "main": "./lib/index.js", - "files": ["lib/**/*.js"], + "files": [ + "lib/**/*.js" + ], "scripts": { "build": "npx tsc", "lint": "eslint .", @@ -27,7 +29,6 @@ "uuid": "^2.0.1" }, "devDependencies": { - "@types/events": "^3.0.0", "@types/node": "^20.3.1", "chai": "^4.2.0", "coveralls": "^3.0.5", diff --git a/src/Model/events.js b/src/Model/events.js index 3bfc1cd70..500a86287 100644 --- a/src/Model/events.js +++ b/src/Model/events.js @@ -186,8 +186,8 @@ Model.prototype.removeAllListeners = function(type, subpath) { Model.prototype._removeAllListeners = function(type, segments) { var mutationListeners = this.root._mutationListeners; if (type == null) { - for (var type in mutationListeners) { - var tree = mutationListeners[type]; + for (var key in mutationListeners) { + var tree = mutationListeners[key]; tree.removeAllListeners(segments); } return; @@ -307,8 +307,8 @@ Model.prototype._addMutationListener = function(type, arg1, arg2, arg3) { */ /** - * @param {Model} model - * @param {string} type + * @param model + * @param type */ function getMutationListener(model, type, arg1, arg2, arg3) { var pattern, options, cb; From 257390254b6e0762f8bbe09349aad2af98ce97d6 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 22 Jun 2023 16:51:45 -0700 Subject: [PATCH 285/479] Add typescript as dev dependency --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 081fb4596..02181f2f6 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "eslint": "^8.1.0", "eslint-config-google": "^0.14.0", "mocha": "^9.1.3", - "nyc": "^15.1.0" + "nyc": "^15.1.0", + "typescript": "^5.1.3" }, "bugs": { "url": "https://github.com/derbyjs/racer/issues" From 84feb203e35403bcd146e5d0cf4908f45b1cc33d Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 22 Jun 2023 16:54:59 -0700 Subject: [PATCH 286/479] Update prepare script (missed "run") --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 02181f2f6..bd182bf35 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "pretest": "npm run build", "test": "node_modules/.bin/mocha", "checks": "npm run lint && npm test", - "prepare": "npm build", + "prepare": "npm run build", "test-cover": "node_modules/nyc/bin/nyc.js --temp-dir=coverage -r text -r lcov node_modules/mocha/bin/_mocha" }, "dependencies": { From ac8a897d359e0c80cf9c3002112bee46d13f92ce Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 22 Jun 2023 16:57:00 -0700 Subject: [PATCH 287/479] Remove useage of npx because node 10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bd182bf35..c6e572f74 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lib/**/*.js" ], "scripts": { - "build": "npx tsc", + "build": "node_modules/.bin/tsc", "lint": "eslint .", "lint:fix": "eslint --fix .", "pretest": "npm run build", From 1df1ec03615467b0a2f2bb37685043a9a4d55957 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 22 Jun 2023 16:59:32 -0700 Subject: [PATCH 288/479] Drop node 10 and 12 from test matrix --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4936aeb88..53920617f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,10 +18,10 @@ jobs: strategy: matrix: node: - - 10 - - 12 - 14 - 16 + - 18 + - 20 timeout-minutes: 5 steps: - uses: actions/checkout@v2 From d5f3c3cd3f73c6c9c2960252223e4aea14efada4 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 22 Jun 2023 17:02:40 -0700 Subject: [PATCH 289/479] Eslint to ignore compiler emitted lib files --- .eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index fb7152865..18030cc76 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,7 +26,7 @@ var CUSTOM_RULES = { module.exports = { extends: 'google', - ignorePatterns: ['.gitignore'], + ignorePatterns: ['.gitignore', 'lib/**/*.js'], parserOptions: { ecmaVersion: 5 }, From 6f1fc39b5876a39dd77e461a40c00ba6c121c68c Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 23 Jun 2023 16:15:18 -0700 Subject: [PATCH 290/479] Rename Model.js > Model.ts --- src/Model/{Model.js => Model.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Model/{Model.js => Model.ts} (100%) diff --git a/src/Model/Model.js b/src/Model/Model.ts similarity index 100% rename from src/Model/Model.js rename to src/Model/Model.ts From d3d8979b817e9ae58403d0c956b64aa58be23003 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 23 Jun 2023 16:19:07 -0700 Subject: [PATCH 291/479] Convert Model and ChildModel to class syntax --- src/Model/Model.ts | 103 ++++++++++++++++++++++++++++---------------- src/Model/events.js | 4 +- 2 files changed, 67 insertions(+), 40 deletions(-) diff --git a/src/Model/Model.ts b/src/Model/Model.ts index d0eaf2577..058805153 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -1,46 +1,73 @@ -var uuid = require('uuid'); +import { v4 as uuidv4 } from 'uuid'; +import { EventEmitter } from 'events'; -Model.INITS = []; +interface DebugOptions { + debugMutations?: boolean, + disableSubmit?: boolean, +} + +interface ModelOptions { + debug?: DebugOptions; +} + +type ModelInitFunction = (instance: Model, options: ModelOptions) => void; -module.exports = Model; +class Model extends EventEmitter { + static INITS: ModelInitFunction[] = []; -function Model(options) { - this.root = this; + ChildModel = ChildModel; + debug: DebugOptions; + root: Model; - var inits = Model.INITS; - if (!options) options = {}; - this.debug = options.debug || {}; - for (var i = 0; i < inits.length; i++) { - inits[i](this, options); + _at: () => Model; + _context: {}; + _eventContext: number | null; + _events: []; + _maxListeners: number; + _pass: () => void; + _preventCompose: () => void; + _silent: boolean; + + constructor(options: ModelOptions = {}) { + super(); + + this.root = this; + var inits = Model.INITS; + this.debug = options.debug || {}; + for (var i = 0; i < inits.length; i++) { + inits[i](this, options); + } } + + id() { + return uuidv4(); + } + + _child() { + return new ChildModel(this); + }; } -Model.prototype.id = function() { - return uuid.v4(); -}; - -Model.prototype._child = function() { - return new ChildModel(this); -}; - -Model.ChildModel = ChildModel; - -function ChildModel(model) { - // Shared properties should be accessed via the root. This makes inheritance - // cheap and easily extensible - this.root = model.root; - - // EventEmitter methods access these properties directly, so they must be - // inherited manually instead of via the root - this._events = model._events; - this._maxListeners = model._maxListeners; - - // Properties specific to a child instance - this._context = model._context; - this._at = model._at; - this._pass = model._pass; - this._silent = model._silent; - this._eventContext = model._eventContext; - this._preventCompose = model._preventCompose; +class ChildModel extends Model { + constructor(model: Model) { + super(); + // Shared properties should be accessed via the root. This makes inheritance + // cheap and easily extensible + this.root = model.root; + + // EventEmitter methods access these properties directly, so they must be + // inherited manually instead of via the root + this._events = model._events; + this._maxListeners = model._maxListeners; + + // Properties specific to a child instance + this._context = model._context; + this._at = model._at; + this._pass = model._pass; + this._silent = model._silent; + this._eventContext = model._eventContext; + this._preventCompose = model._preventCompose; + } } -ChildModel.prototype = new Model(); + +module.exports = Model; \ No newline at end of file diff --git a/src/Model/events.js b/src/Model/events.js index 500a86287..ac31fb090 100644 --- a/src/Model/events.js +++ b/src/Model/events.js @@ -22,7 +22,7 @@ exports.Passed = Passed; Model.INITS.push(function(model) { var root = model.root; - EventEmitter.call(root); + // EventEmitter.call(root); // Set max listeners to unlimited model.setMaxListeners(0); @@ -51,7 +51,7 @@ Model.INITS.push(function(model) { root._eventContext = null; }); -mergeInto(Model.prototype, EventEmitter.prototype); +// mergeInto(Model.prototype, EventEmitter.prototype); Model.prototype.wrapCallback = function(cb) { if (!cb) return this.root._defaultCallback; From aed59e9c53db64de9ee74deeee7fe7b3461de7ee Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 27 Jun 2023 11:18:05 -0700 Subject: [PATCH 292/479] Rename files, js => ts --- src/Model/{index.js => index.ts} | 0 src/{Racer.js => Racer.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/Model/{index.js => index.ts} (100%) rename src/{Racer.js => Racer.ts} (100%) diff --git a/src/Model/index.js b/src/Model/index.ts similarity index 100% rename from src/Model/index.js rename to src/Model/index.ts diff --git a/src/Racer.js b/src/Racer.ts similarity index 100% rename from src/Racer.js rename to src/Racer.ts From 1eabf5632d67219817167ac00b0fbbd2b019a2dc Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 27 Jun 2023 11:19:25 -0700 Subject: [PATCH 293/479] Convert Racer to class; resolve circular require w Racer.server --- src/Racer.ts | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Racer.ts b/src/Racer.ts index 95f778d73..92dbbfb20 100644 --- a/src/Racer.ts +++ b/src/Racer.ts @@ -2,29 +2,28 @@ var EventEmitter = require('events').EventEmitter; var Model = require('./Model'); var util = require('./util'); -module.exports = Racer; - -function Racer() { - EventEmitter.call(this); -} - -util.mergeInto(Racer.prototype, EventEmitter.prototype); -// Make classes accessible for use by plugins and tests -Racer.prototype.Model = Model; -Racer.prototype.util = util; +class Racer extends EventEmitter { + Model: typeof Model = Model; + util = util; + use = util.use; + serverUse = util.serverUse; -// Support plugins on racer instances -Racer.prototype.use = util.use; -Racer.prototype.serverUse = util.serverUse; + constructor() { + super(); + } -Racer.prototype.createModel = function(data) { - var model = new Model(); - if (data) { - model.createConnection(data); - model.unbundle(data); + createModel(data) { + var model = new Model(); + if (data) { + model.createConnection(data); + model.unbundle(data); + } + return model; } - return model; -}; +} + +// exports before serverRequire as Racer.server has circular require +module.exports = Racer; util.serverRequire(module, './Racer.server'); From 2caa481b2b515f865d5a2b2c8ca1afd66996ca11 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 29 Jun 2023 16:05:59 -0700 Subject: [PATCH 294/479] Rename js -> ts --- src/{Backend.js => Backend.ts} | 0 src/{Racer.server.js => Racer.server.ts} | 0 src/{index.js => index.ts} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/{Backend.js => Backend.ts} (100%) rename src/{Racer.server.js => Racer.server.ts} (100%) rename src/{index.js => index.ts} (100%) diff --git a/src/Backend.js b/src/Backend.ts similarity index 100% rename from src/Backend.js rename to src/Backend.ts diff --git a/src/Racer.server.js b/src/Racer.server.ts similarity index 100% rename from src/Racer.server.js rename to src/Racer.server.ts diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts From 0ee8c10e061cb78f1d4c0c5b82d864386c55a609 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 30 Jun 2023 14:01:27 -0700 Subject: [PATCH 295/479] Convert index, Racer, Backend to ts --- src/Backend.ts | 102 ++++++++++++++++++++++---------------------- src/Racer.server.ts | 17 ++++++-- src/Racer.ts | 5 +-- src/index.ts | 2 +- 4 files changed, 67 insertions(+), 59 deletions(-) diff --git a/src/Backend.ts b/src/Backend.ts index 2f2801d13..ca8e35efe 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -3,59 +3,61 @@ var Backend = require('sharedb').Backend; var Model = require('./Model'); var util = require('./util'); -module.exports = RacerBackend; - -function RacerBackend(racer, options) { - Backend.call(this, options); - this.racer = racer; - this.modelOptions = options && options.modelOptions; - this.on('bundle', function(browserify) { - var racerPath = path.join(__dirname, 'index.js'); - browserify.require(racerPath, {expose: 'racer'}); - }); -} -RacerBackend.prototype = Object.create(Backend.prototype); - -RacerBackend.prototype.createModel = function(options, req) { - if (this.modelOptions) { - options = (options) ? - util.mergeInto(options, this.modelOptions) : - this.modelOptions; +export class RacerBackend extends Backend { + racer: any; + modelOptions: any; + + constructor(racer: any, options: any) { + super(options); + this.racer = racer; + this.modelOptions = options && options.modelOptions; + this.on('bundle', function(browserify) { + var racerPath = path.join(__dirname, 'index.js'); + browserify.require(racerPath, {expose: 'racer'}); + }); } - var model = new Model(options); - this.emit('model', model); - model.createConnection(this, req); - return model; -}; - -RacerBackend.prototype.modelMiddleware = function() { - var backend = this; - function modelMiddleware(req, res, next) { - // Do not add a model to the request if one has been added already - if (req.model) return next(); - - // Create a new model for this request - req.model = backend.createModel({fetchOnly: true}, req); - // DEPRECATED: - req.getModel = function() { - console.warn('Warning: req.getModel() is deprecated. Please use req.model instead.'); - return req.model; - }; - // Close the model when this request ends - function closeModel() { - res.removeListener('finish', closeModel); - res.removeListener('close', closeModel); - if (req.model) req.model.close(); + createModel(options: any, req: any) { + if (this.modelOptions) { + options = (options) ? + util.mergeInto(options, this.modelOptions) : + this.modelOptions; + } + var model = new Model(options); + this.emit('model', model); + model.createConnection(this, req); + return model; + }; + + modelMiddleware() { + var backend = this; + function modelMiddleware(req, res, next) { + // Do not add a model to the request if one has been added already + if (req.model) return next(); + + // Create a new model for this request + req.model = backend.createModel({fetchOnly: true}, req); // DEPRECATED: - req.getModel = getModelUndefined; + req.getModel = function() { + console.warn('Warning: req.getModel() is deprecated. Please use req.model instead.'); + return req.model; + }; + + // Close the model when this request ends + function closeModel() { + res.removeListener('finish', closeModel); + res.removeListener('close', closeModel); + if (req.model) req.model.close(); + // DEPRECATED: + req.getModel = getModelUndefined; + } + res.on('finish', closeModel); + res.on('close', closeModel); + + next(); } - res.on('finish', closeModel); - res.on('close', closeModel); - - next(); - } - return modelMiddleware; -}; + return modelMiddleware; + }; +} function getModelUndefined() {} diff --git a/src/Racer.server.ts b/src/Racer.server.ts index 559fea0d2..200cb9cce 100644 --- a/src/Racer.server.ts +++ b/src/Racer.server.ts @@ -1,9 +1,18 @@ -var Backend = require('./Backend'); -var Racer = require('./Racer'); +import { RacerBackend } from './Backend'; +import { Racer } from './Racer'; + +declare module './Racer' { + interface Racer { + Backend: typeof RacerBackend; + version: string; + createBackend: (options:any) => RacerBackend; + } +} + +Racer.prototype.Backend = RacerBackend; -Racer.prototype.Backend = Backend; Racer.prototype.version = require('../package').version; Racer.prototype.createBackend = function(options) { - return new Backend(this, options); + return new RacerBackend(this, options); }; diff --git a/src/Racer.ts b/src/Racer.ts index 92dbbfb20..f21fb21c8 100644 --- a/src/Racer.ts +++ b/src/Racer.ts @@ -3,7 +3,7 @@ var Model = require('./Model'); var util = require('./util'); -class Racer extends EventEmitter { +export class Racer extends EventEmitter { Model: typeof Model = Model; util = util; use = util.use; @@ -23,7 +23,4 @@ class Racer extends EventEmitter { } } -// exports before serverRequire as Racer.server has circular require -module.exports = Racer; - util.serverRequire(module, './Racer.server'); diff --git a/src/index.ts b/src/index.ts index 00dc928d3..059786260 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -var Racer = require('./Racer'); +import { Racer } from './Racer'; module.exports = new Racer(); From c25f2f394d6f41f4e25f2927feb4958eca91abaf Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 30 Jun 2023 14:08:24 -0700 Subject: [PATCH 296/479] Rename util.js -> .ts --- src/{util.js => util.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{util.js => util.ts} (100%) diff --git a/src/util.js b/src/util.ts similarity index 100% rename from src/util.js rename to src/util.ts From 13fb89e85582861260a0f2b52542aa81b770e585 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 30 Jun 2023 14:22:55 -0700 Subject: [PATCH 297/479] Convert util to ts --- src/util.ts | 113 +++++++++++++++++++++++----------------------------- 1 file changed, 49 insertions(+), 64 deletions(-) diff --git a/src/util.ts b/src/util.ts index 9f1b00a6a..9d1c45f08 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,64 +1,49 @@ -var deepEqual = require('fast-deep-equal'); -var isServer = process.title !== 'browser'; -exports.isServer = isServer; - -exports.asyncGroup = asyncGroup; -exports.castSegments = castSegments; -exports.contains = contains; -exports.copy = copy; -exports.copyObject = copyObject; -exports.deepCopy = deepCopy; -exports.deepEqual = deepEqual; -exports.equal = equal; -exports.equalsNaN = equalsNaN; -exports.isArrayIndex = isArrayIndex; -exports.lookup = lookup; -exports.mayImpact = mayImpact; -exports.mayImpactAny = mayImpactAny; -exports.mergeInto = mergeInto; -exports.promisify = promisify; -exports.serverRequire = serverRequire; -exports.serverUse = serverUse; -exports.use = use; - -function asyncGroup(cb) { +export const deepEqual = require('fast-deep-equal'); +export const isServer = process.title !== 'browser'; + +export function asyncGroup(cb) { var group = new AsyncGroup(cb); return function asyncGroupAdd() { return group.add(); }; } -/** - * @constructor - * @param {Function} cb(err) - */ -function AsyncGroup(cb) { - this.cb = cb; - this.isDone = false; - this.count = 0; -} -AsyncGroup.prototype.add = function() { - this.count++; - var self = this; - return function(err) { - self.count--; - if (self.isDone) return; - if (err) { +type ErrorCallback = (err?: Error) => void; + +class AsyncGroup { + cb: ErrorCallback; + isDone: boolean; + count: number; + + constructor(cb: ErrorCallback) { + this.cb = cb; + this.isDone = false; + this.count = 0; + } + + add() { + this.count++; + const self = this; + return function(err) { + self.count--; + if (self.isDone) return; + if (err) { + self.isDone = true; + self.cb(err); + return; + } + if (self.count > 0) return; self.isDone = true; - self.cb(err); - return; - } - if (self.count > 0) return; - self.isDone = true; - self.cb(); - }; -}; + self.cb(); + }; + } +} /** * @param {Array} segments * @return {Array} */ -function castSegments(segments) { +export function castSegments(segments) { // Cast number path segments from strings to numbers for (var i = segments.length; i--;) { var segment = segments[i]; @@ -69,14 +54,14 @@ function castSegments(segments) { return segments; } -function contains(segments, testSegments) { +export function contains(segments, testSegments) { for (var i = 0; i < segments.length; i++) { if (segments[i] !== testSegments[i]) return false; } return true; } -function copy(value) { +export function copy(value) { if (value instanceof Date) return new Date(value); if (typeof value === 'object') { if (value === null) return null; @@ -86,7 +71,7 @@ function copy(value) { return value; } -function copyObject(object) { +export function copyObject(object) { var out = new object.constructor(); for (var key in object) { if (object.hasOwnProperty(key)) { @@ -96,7 +81,7 @@ function copyObject(object) { return out; } -function deepCopy(value) { +export function deepCopy(value) { if (value instanceof Date) return new Date(value); if (typeof value === 'object') { if (value === null) return null; @@ -118,20 +103,20 @@ function deepCopy(value) { return value; } -function equal(a, b) { +export function equal(a, b) { return (a === b) || (equalsNaN(a) && equalsNaN(b)); } -function equalsNaN(x) { +export function equalsNaN(x) { // eslint-disable-next-line no-self-compare return x !== x; } -function isArrayIndex(segment) { +export function isArrayIndex(segment) { return (/^[0-9]+$/).test(segment); } -function lookup(segments, value) { +export function lookup(segments, value) { if (!segments) return value; for (var i = 0, len = segments.length; i < len; i++) { @@ -141,14 +126,14 @@ function lookup(segments, value) { return value; } -function mayImpactAny(segmentsList, testSegments) { +export function mayImpactAny(segmentsList, testSegments) { for (var i = 0, len = segmentsList.length; i < len; i++) { if (mayImpact(segmentsList[i], testSegments)) return true; } return false; } -function mayImpact(segments, testSegments) { +export function mayImpact(segments, testSegments) { var len = Math.min(segments.length, testSegments.length); for (var i = 0; i < len; i++) { if (segments[i] !== testSegments[i]) return false; @@ -156,14 +141,14 @@ function mayImpact(segments, testSegments) { return true; } -function mergeInto(to, from) { +export function mergeInto(to, from) { for (var key in from) { to[key] = from[key]; } return to; } -function promisify(original) { +export function promisify(original) { if (typeof original !== 'function') { throw new TypeError('The "original" argument must be of type Function'); } @@ -196,18 +181,18 @@ function promisify(original) { return fn; } -function serverRequire(module, id) { +export function serverRequire(module, id) { if (!isServer) return; return module.require(id); } -function serverUse(module, id, options) { +export function serverUse(module, id, options) { if (!isServer) return this; var plugin = module.require(id); return this.use(plugin, options); } -function use(plugin, options) { +export function use(plugin, options) { // Don't include a plugin more than once var plugins = this._plugins || (this._plugins = []); if (plugins.indexOf(plugin) === -1) { From 0320c10b79d3cc4fcb07c269c2821df24820e99e Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 30 Jun 2023 14:33:32 -0700 Subject: [PATCH 298/479] Type intermediate var --- src/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index 9d1c45f08..7b41169ee 100644 --- a/src/util.ts +++ b/src/util.ts @@ -86,7 +86,7 @@ export function deepCopy(value) { if (typeof value === 'object') { if (value === null) return null; if (Array.isArray(value)) { - var array = []; + var array: any[] = []; for (var i = value.length; i--;) { array[i] = deepCopy(value[i]); } From 5e9bb53c904f3d8b3d93fe225374d0ab676bd11a Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 30 Jun 2023 15:24:28 -0700 Subject: [PATCH 299/479] Rename unbundle.js -> .ts --- src/Model/{unbundle.js => unbundle.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Model/{unbundle.js => unbundle.ts} (100%) diff --git a/src/Model/unbundle.js b/src/Model/unbundle.ts similarity index 100% rename from src/Model/unbundle.js rename to src/Model/unbundle.ts From ba2f0024eb7a58089c510a7233680e3641a02722 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 30 Jun 2023 15:36:47 -0700 Subject: [PATCH 300/479] Use export of Model and ChildModel --- src/Model/Model.ts | 6 ++---- src/Model/index.ts | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Model/Model.ts b/src/Model/Model.ts index 058805153..0487f9166 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -12,7 +12,7 @@ interface ModelOptions { type ModelInitFunction = (instance: Model, options: ModelOptions) => void; -class Model extends EventEmitter { +export class Model extends EventEmitter { static INITS: ModelInitFunction[] = []; ChildModel = ChildModel; @@ -48,7 +48,7 @@ class Model extends EventEmitter { }; } -class ChildModel extends Model { +export class ChildModel extends Model { constructor(model: Model) { super(); // Shared properties should be accessed via the root. This makes inheritance @@ -69,5 +69,3 @@ class ChildModel extends Model { this._preventCompose = model._preventCompose; } } - -module.exports = Model; \ No newline at end of file diff --git a/src/Model/index.ts b/src/Model/index.ts index 11acb86cf..26228e820 100644 --- a/src/Model/index.ts +++ b/src/Model/index.ts @@ -1,5 +1,5 @@ -module.exports = require('./Model'); -var util = require('../util'); +import { serverRequire } from '../util'; +export { Model } from './Model'; // Extend model on both server and client // require('./unbundle'); @@ -20,5 +20,5 @@ require('./refList'); require('./ref'); // Extend model for server // -util.serverRequire(module, './bundle'); -util.serverRequire(module, './connection.server'); +serverRequire(module, './bundle'); +serverRequire(module, './connection.server'); From d8c93f9f0494c5e56a0025a264a37a13c9fc171a Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 30 Jun 2023 15:37:33 -0700 Subject: [PATCH 301/479] Convert unbundle to ts --- src/Model/unbundle.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Model/unbundle.ts b/src/Model/unbundle.ts index f7806279c..3ddbc7121 100644 --- a/src/Model/unbundle.ts +++ b/src/Model/unbundle.ts @@ -1,4 +1,10 @@ -var Model = require('./Model'); +import { Model } from './Model'; + +declare module './Model' { + interface Model { + unbundle: (data: any) => void; + } +} Model.prototype.unbundle = function(data) { if (this.connection) this.connection.startBulk(); From 822eda02148e05fc3632f2364c7e3e7c9bf8045d Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 30 Jun 2023 15:38:00 -0700 Subject: [PATCH 302/479] Bulk rename Model/*.js to .ts --- src/Model/{CollectionCounter.js => CollectionCounter.ts} | 0 src/Model/{CollectionMap.js => CollectionMap.ts} | 0 src/Model/{Doc.js => Doc.ts} | 0 src/Model/{EventListenerTree.js => EventListenerTree.ts} | 0 src/Model/{EventMapTree.js => EventMapTree.ts} | 0 src/Model/{FastMap.js => FastMap.ts} | 0 src/Model/{LocalDoc.js => LocalDoc.ts} | 0 src/Model/{ModelStandalone.js => ModelStandalone.ts} | 0 src/Model/{Query.js => Query.ts} | 0 src/Model/{RemoteDoc.js => RemoteDoc.ts} | 0 src/Model/{bundle.js => bundle.ts} | 0 src/Model/{collections.js => collections.ts} | 0 src/Model/{connection.server.js => connection.server.ts} | 0 src/Model/{connection.js => connection.ts} | 0 src/Model/{contexts.js => contexts.ts} | 0 src/Model/{defaultFns.js => defaultFns.ts} | 0 src/Model/{events.js => events.ts} | 0 src/Model/{filter.js => filter.ts} | 0 src/Model/{fn.js => fn.ts} | 0 src/Model/{mutators.js => mutators.ts} | 0 src/Model/{paths.js => paths.ts} | 0 src/Model/{ref.js => ref.ts} | 0 src/Model/{refList.js => refList.ts} | 0 src/Model/{setDiff.js => setDiff.ts} | 0 src/Model/{subscriptions.js => subscriptions.ts} | 0 25 files changed, 0 insertions(+), 0 deletions(-) rename src/Model/{CollectionCounter.js => CollectionCounter.ts} (100%) rename src/Model/{CollectionMap.js => CollectionMap.ts} (100%) rename src/Model/{Doc.js => Doc.ts} (100%) rename src/Model/{EventListenerTree.js => EventListenerTree.ts} (100%) rename src/Model/{EventMapTree.js => EventMapTree.ts} (100%) rename src/Model/{FastMap.js => FastMap.ts} (100%) rename src/Model/{LocalDoc.js => LocalDoc.ts} (100%) rename src/Model/{ModelStandalone.js => ModelStandalone.ts} (100%) rename src/Model/{Query.js => Query.ts} (100%) rename src/Model/{RemoteDoc.js => RemoteDoc.ts} (100%) rename src/Model/{bundle.js => bundle.ts} (100%) rename src/Model/{collections.js => collections.ts} (100%) rename src/Model/{connection.server.js => connection.server.ts} (100%) rename src/Model/{connection.js => connection.ts} (100%) rename src/Model/{contexts.js => contexts.ts} (100%) rename src/Model/{defaultFns.js => defaultFns.ts} (100%) rename src/Model/{events.js => events.ts} (100%) rename src/Model/{filter.js => filter.ts} (100%) rename src/Model/{fn.js => fn.ts} (100%) rename src/Model/{mutators.js => mutators.ts} (100%) rename src/Model/{paths.js => paths.ts} (100%) rename src/Model/{ref.js => ref.ts} (100%) rename src/Model/{refList.js => refList.ts} (100%) rename src/Model/{setDiff.js => setDiff.ts} (100%) rename src/Model/{subscriptions.js => subscriptions.ts} (100%) diff --git a/src/Model/CollectionCounter.js b/src/Model/CollectionCounter.ts similarity index 100% rename from src/Model/CollectionCounter.js rename to src/Model/CollectionCounter.ts diff --git a/src/Model/CollectionMap.js b/src/Model/CollectionMap.ts similarity index 100% rename from src/Model/CollectionMap.js rename to src/Model/CollectionMap.ts diff --git a/src/Model/Doc.js b/src/Model/Doc.ts similarity index 100% rename from src/Model/Doc.js rename to src/Model/Doc.ts diff --git a/src/Model/EventListenerTree.js b/src/Model/EventListenerTree.ts similarity index 100% rename from src/Model/EventListenerTree.js rename to src/Model/EventListenerTree.ts diff --git a/src/Model/EventMapTree.js b/src/Model/EventMapTree.ts similarity index 100% rename from src/Model/EventMapTree.js rename to src/Model/EventMapTree.ts diff --git a/src/Model/FastMap.js b/src/Model/FastMap.ts similarity index 100% rename from src/Model/FastMap.js rename to src/Model/FastMap.ts diff --git a/src/Model/LocalDoc.js b/src/Model/LocalDoc.ts similarity index 100% rename from src/Model/LocalDoc.js rename to src/Model/LocalDoc.ts diff --git a/src/Model/ModelStandalone.js b/src/Model/ModelStandalone.ts similarity index 100% rename from src/Model/ModelStandalone.js rename to src/Model/ModelStandalone.ts diff --git a/src/Model/Query.js b/src/Model/Query.ts similarity index 100% rename from src/Model/Query.js rename to src/Model/Query.ts diff --git a/src/Model/RemoteDoc.js b/src/Model/RemoteDoc.ts similarity index 100% rename from src/Model/RemoteDoc.js rename to src/Model/RemoteDoc.ts diff --git a/src/Model/bundle.js b/src/Model/bundle.ts similarity index 100% rename from src/Model/bundle.js rename to src/Model/bundle.ts diff --git a/src/Model/collections.js b/src/Model/collections.ts similarity index 100% rename from src/Model/collections.js rename to src/Model/collections.ts diff --git a/src/Model/connection.server.js b/src/Model/connection.server.ts similarity index 100% rename from src/Model/connection.server.js rename to src/Model/connection.server.ts diff --git a/src/Model/connection.js b/src/Model/connection.ts similarity index 100% rename from src/Model/connection.js rename to src/Model/connection.ts diff --git a/src/Model/contexts.js b/src/Model/contexts.ts similarity index 100% rename from src/Model/contexts.js rename to src/Model/contexts.ts diff --git a/src/Model/defaultFns.js b/src/Model/defaultFns.ts similarity index 100% rename from src/Model/defaultFns.js rename to src/Model/defaultFns.ts diff --git a/src/Model/events.js b/src/Model/events.ts similarity index 100% rename from src/Model/events.js rename to src/Model/events.ts diff --git a/src/Model/filter.js b/src/Model/filter.ts similarity index 100% rename from src/Model/filter.js rename to src/Model/filter.ts diff --git a/src/Model/fn.js b/src/Model/fn.ts similarity index 100% rename from src/Model/fn.js rename to src/Model/fn.ts diff --git a/src/Model/mutators.js b/src/Model/mutators.ts similarity index 100% rename from src/Model/mutators.js rename to src/Model/mutators.ts diff --git a/src/Model/paths.js b/src/Model/paths.ts similarity index 100% rename from src/Model/paths.js rename to src/Model/paths.ts diff --git a/src/Model/ref.js b/src/Model/ref.ts similarity index 100% rename from src/Model/ref.js rename to src/Model/ref.ts diff --git a/src/Model/refList.js b/src/Model/refList.ts similarity index 100% rename from src/Model/refList.js rename to src/Model/refList.ts diff --git a/src/Model/setDiff.js b/src/Model/setDiff.ts similarity index 100% rename from src/Model/setDiff.js rename to src/Model/setDiff.ts diff --git a/src/Model/subscriptions.js b/src/Model/subscriptions.ts similarity index 100% rename from src/Model/subscriptions.js rename to src/Model/subscriptions.ts From 72a23ffc77244c7dc5b45f4036d7dddd6851acc5 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 21 Jul 2023 13:51:25 -0700 Subject: [PATCH 303/479] Convert Model to typescript (3 errors) --- src/Model/CollectionCounter.ts | 136 ++-- src/Model/CollectionMap.ts | 63 +- src/Model/Doc.ts | 41 +- src/Model/EventListenerTree.ts | 412 ++++++------ src/Model/EventMapTree.ts | 318 ++++----- src/Model/FastMap.ts | 38 +- src/Model/LocalDoc.ts | 419 ++++++------ src/Model/Model.ts | 28 +- src/Model/Query.ts | 843 ++++++++++++------------ src/Model/RemoteDoc.ts | 1103 +++++++++++++++++--------------- src/Model/bundle.ts | 18 +- src/Model/collections.ts | 141 ++-- src/Model/connection.server.ts | 10 +- src/Model/connection.ts | 31 +- src/Model/contexts.ts | 218 ++++--- src/Model/events.ts | 374 ++++++----- src/Model/filter.ts | 363 ++++++----- src/Model/fn.ts | 178 +++--- src/Model/mutators.ts | 125 +++- src/Model/paths.ts | 15 +- src/Model/ref.ts | 109 ++-- src/Model/refList.ts | 258 ++++---- src/Model/setDiff.ts | 28 +- src/Model/subscriptions.ts | 33 +- 24 files changed, 2990 insertions(+), 2312 deletions(-) diff --git a/src/Model/CollectionCounter.ts b/src/Model/CollectionCounter.ts index 7f87d4e77..72e0f4cbe 100644 --- a/src/Model/CollectionCounter.ts +++ b/src/Model/CollectionCounter.ts @@ -1,67 +1,81 @@ -module.exports = CollectionCounter; +export class CollectionCounter { + collections: Record; + size: number; + constructor() { + this.reset(); + } + + reset() { + // A map of CounterMaps + this.collections = {}; + // The number of id keys in the collections map + this.size = 0; + }; + + get(collectionName: string, id: string) { + var collection = this.collections[collectionName]; + return (collection && collection.counts[id]) || 0; + }; + + increment(collectionName: string, id: string) { + var collection = this.collections[collectionName]; + if (!collection) { + collection = this.collections[collectionName] = new CounterMap(); + this.size++; + } + return collection.increment(id); + }; -function CollectionCounter() { - this.reset(); + decrement(collectionName: string, id: string) { + var collection = this.collections[collectionName]; + if (!collection) return 0; + var count = collection.decrement(id); + if (collection.size < 1) { + delete this.collections[collectionName]; + this.size--; + } + return count; + }; + + toJSON() { + // Serialize to the contained count data if any + if (this.size > 0) { + var out = {}; + for (var collectionName in this.collections) { + out[collectionName] = this.collections[collectionName].counts; + } + return out; + } + return; + }; } -CollectionCounter.prototype.reset = function() { - // A map of CounterMaps - this.collections = {}; - // The number of id keys in the collections map - this.size = 0; -}; -CollectionCounter.prototype.get = function(collectionName, id) { - var collection = this.collections[collectionName]; - return (collection && collection.counts[id]) || 0; -}; -CollectionCounter.prototype.increment = function(collectionName, id) { - var collection = this.collections[collectionName]; - if (!collection) { - collection = this.collections[collectionName] = new CounterMap(); - this.size++; - } - return collection.increment(id); -}; -CollectionCounter.prototype.decrement = function(collectionName, id) { - var collection = this.collections[collectionName]; - if (!collection) return 0; - var count = collection.decrement(id); - if (collection.size < 1) { - delete this.collections[collectionName]; - this.size--; + +export class CounterMap { + counts: Record; + size: number; + + constructor() { + this.counts = {}; + this.size = 0; } - return count; -}; -CollectionCounter.prototype.toJSON = function() { - // Serialize to the contained count data if any - if (this.size > 0) { - var out = {}; - for (var collectionName in this.collections) { - out[collectionName] = this.collections[collectionName].counts; + + increment(key: string): number { + var count = this.counts[key] || 0; + if (count === 0) { + this.size++; } - return out; - } - return; -}; + return this.counts[key] = count + 1; + }; -function CounterMap() { - this.counts = {}; - this.size = 0; + decrement(key: string): number { + var count = this.counts[key] || 0; + if (count > 1) { + return this.counts[key] = count - 1; + } + if (count === 1) { + delete this.counts[key]; + this.size--; + } + return 0; + }; } -CounterMap.prototype.increment = function(key) { - var count = this.counts[key] || 0; - if (count === 0) { - this.size++; - } - return this.counts[key] = count + 1; -}; -CounterMap.prototype.decrement = function(key) { - var count = this.counts[key] || 0; - if (count > 1) { - return this.counts[key] = count - 1; - } - if (count === 1) { - delete this.counts[key]; - this.size--; - } - return 0; -}; diff --git a/src/Model/CollectionMap.ts b/src/Model/CollectionMap.ts index 507f021f1..08d21e30d 100644 --- a/src/Model/CollectionMap.ts +++ b/src/Model/CollectionMap.ts @@ -1,31 +1,38 @@ -var FastMap = require('./FastMap'); +import { FastMap } from './FastMap'; +import { Collection } from './collections'; -module.exports = CollectionMap; +export class CollectionMap{ + collections: Record>; -function CollectionMap() { - // A map of collection names to FastMaps - this.collections = {}; -} -CollectionMap.prototype.getCollection = function(collectionName) { - var collection = this.collections[collectionName]; - return (collection && collection.values); -}; -CollectionMap.prototype.get = function(collectionName, id) { - var collection = this.collections[collectionName]; - return (collection && collection.values[id]); -}; -CollectionMap.prototype.set = function(collectionName, id, value) { - var collection = this.collections[collectionName]; - if (!collection) { - collection = this.collections[collectionName] = new FastMap(); - } - collection.set(id, value); -}; -CollectionMap.prototype.del = function(collectionName, id) { - var collection = this.collections[collectionName]; - if (collection) { - collection.del(id); - if (collection.size > 0) return; - delete this.collections[collectionName]; + constructor() { + // A map of collection names to FastMaps + this.collections = {}; } -}; + + getCollection(collectionName) { + var collection = this.collections[collectionName]; + return (collection && collection.values); + }; + + get(collectionName, id) { + var collection = this.collections[collectionName]; + return (collection && collection.values[id]); + }; + + set(collectionName, id, value) { + var collection = this.collections[collectionName]; + if (!collection) { + collection = this.collections[collectionName] = new FastMap(); + } + collection.set(id, value); + }; + + del(collectionName, id) { + var collection = this.collections[collectionName]; + if (collection) { + collection.del(id); + if (collection.size > 0) return; + delete this.collections[collectionName]; + } + }; +} diff --git a/src/Model/Doc.ts b/src/Model/Doc.ts index 90ffdbbe6..c3f56092b 100644 --- a/src/Model/Doc.ts +++ b/src/Model/Doc.ts @@ -1,18 +1,29 @@ -module.exports = Doc; +import { type Model, type Segments } from './Model'; +import { Collection } from './collections'; -function Doc(model, collectionName, id) { - this.collectionName = collectionName; - this.id = id; - this.collectionData = model && model.data[collectionName]; -} +export class Doc { + collectionName: string; + id: string; + collectionData: Model; + data: any; + model: Model; -Doc.prototype.path = function(segments) { - var path = this.collectionName + '.' + this.id; - if (segments && segments.length) path += '.' + segments.join('.'); - return path; -}; + constructor(model: Model, collectionName: string, id: string, data: any, _collection?: Collection) { + this.collectionName = collectionName; + this.id = id; + this.data = data; + this.model = model; + this.collectionData = model && model.data[collectionName]; + } -Doc.prototype._errorMessage = function(description, segments, value) { - return description + ' at ' + this.path(segments) + ': ' + - JSON.stringify(value, null, 2); -}; + path(segments?: string[]) { + var path = this.collectionName + '.' + this.id; + if (segments && segments.length) path += '.' + segments.join('.'); + return path; + }; + + _errorMessage(description: string, segments: Segments, value: any) { + return description + ' at ' + this.path(segments) + ': ' + + JSON.stringify(value, null, 2); + }; +} diff --git a/src/Model/EventListenerTree.ts b/src/Model/EventListenerTree.ts index 733f77b55..d67d0f3c0 100644 --- a/src/Model/EventListenerTree.ts +++ b/src/Model/EventListenerTree.ts @@ -1,6 +1,5 @@ -var FastMap = require('./FastMap'); - -module.exports = EventListenerTree; +import { type Segments } from './Model'; +import { FastMap } from './FastMap'; /** * Construct a tree root when invoked without any arguments. Children nodes are @@ -9,224 +8,231 @@ module.exports = EventListenerTree; * @param {EventListenerTree} [parent] * @param {string} [segment] */ -function EventListenerTree(parent, segment) { - this.parent = parent; - this.segment = segment; - this.children = null; - this.listeners = null; -} +export class EventListenerTree { + parent?: any; + segment?: string; + children: any; + listeners: any; -/** - * Remove the reference to this node from its parent so that it can be garbage - * collected. This is called internally when all listeners to a node - * are removed - */ -EventListenerTree.prototype.destroy = function() { - // For all non-root nodes, remove the reference to the node - var parent = this.parent; - if (parent) { - // Remove reference to this node from its parent - var children = parent.children; - if (children) { - children.del(this.segment); - if (children.size > 0) return; - parent.children = null; - } - // Destroy parent if it no longer has any dependents - if (!parent.listeners) { - parent.destroy(); - } - return; + constructor(parent?: any, segment?: string) { + this.parent = parent; + this.segment = segment; + this.children = null; + this.listeners = null; } - // For the root node, reset any references to listeners or children - this.children = null; - this.listeners = null; -}; + /** + * Remove the reference to this node from its parent so that it can be garbage + * collected. This is called internally when all listeners to a node + * are removed + */ + destroy() { + // For all non-root nodes, remove the reference to the node + var parent = this.parent; + if (parent) { + // Remove reference to this node from its parent + var children = parent.children; + if (children) { + children.del(this.segment); + if (children.size > 0) return; + parent.children = null; + } + // Destroy parent if it no longer has any dependents + if (!parent.listeners) { + parent.destroy(); + } + return; + } + // For the root node, reset any references to listeners or children + this.children = null; + this.listeners = null; + }; -/** - * Get a node for a path if it exists - * - * @param {string[]} segments - * @return {EventListenerTree|undefined} - */ -EventListenerTree.prototype._getChild = function(segments) { - var node = this; - for (var i = 0, len = segments.length; i < len; i++) { - var children = node.children; - if (!children) return; - var segment = segments[i]; - node = children.values[segment]; - if (!node) return; - } - return node; -}; + /** + * Get a node for a path if it exists + * + * @param {string[]} segments + * @return {EventListenerTree|undefined} + */ + _getChild(segments: Segments) { + var node = this; + for (var i = 0, len = segments.length; i < len; i++) { + var children = node.children; + if (!children) return; + var segment = segments[i]; + node = children.values[segment]; + if (!node) return; + } + return node; + }; -/** - * If a path already has a node, return it. Otherwise, create the node and - * ancestors in a lazy manner. Return the node for the path - * - * @param {string[]} segments - * @return {EventListenerTree} - */ -EventListenerTree.prototype._getOrCreateChild = function(segments) { - var node = this; - for (var i = 0, len = segments.length; i < len; i++) { - var children = node.children; - if (!children) { - children = node.children = new FastMap(); + /** + * If a path already has a node, return it. Otherwise, create the node and + * ancestors in a lazy manner. Return the node for the path + * + * @param {string[]} segments + * @return {EventListenerTree} + */ + _getOrCreateChild(segments: Segments) { + var node = this; + for (var i = 0, len = segments.length; i < len; i++) { + var children = node.children; + if (!children) { + children = node.children = new FastMap(); + } + var segment = segments[i]; + var next = children.values[segment]; + if (next) { + node = next; + } else { + // @ts-ignore + node = new EventListenerTree(node, segment); + children.set(segment, node); + } } - var segment = segments[i]; - var next = children.values[segment]; - if (next) { - node = next; + return node; + }; + + /** + * Add a listener to a path location. Listener should be unique per path + * location, and calling twice with the same segments and listener value has no + * effect. Unlike EventEmitter, listener may be any type of value + * + * @param {string[]} segments + * @param {*} listener + * @return {EventListenerTree} + */ + addListener(segments: Segments, listener) { + var node = this._getOrCreateChild(segments); + var listeners = node.listeners; + if (listeners) { + var i = listeners.indexOf(listener); + if (i === -1) { + listeners.push(listener); + } } else { - node = new EventListenerTree(node, segment); - children.set(segment, node); + node.listeners = [listener]; } - } - return node; -}; + return node; + }; -/** - * Add a listener to a path location. Listener should be unique per path - * location, and calling twice with the same segments and listener value has no - * effect. Unlike EventEmitter, listener may be any type of value - * - * @param {string[]} segments - * @param {*} listener - * @return {EventListenerTree} - */ -EventListenerTree.prototype.addListener = function(segments, listener) { - var node = this._getOrCreateChild(segments); - var listeners = node.listeners; - if (listeners) { - var i = listeners.indexOf(listener); - if (i === -1) { - listeners.push(listener); + /** + * Remove a listener from a path location + * + * @param {string[]} segments + * @param {*} listener + */ + removeListener(segments: Segments, listener) { + var node = this._getChild(segments); + if (node) { + node.removeOwnListener(listener); } - } else { - node.listeners = [listener]; - } - return node; -}; - -/** - * Remove a listener from a path location - * - * @param {string[]} segments - * @param {*} listener - */ -EventListenerTree.prototype.removeListener = function(segments, listener) { - var node = this._getChild(segments); - if (node) { - node.removeOwnListener(listener); - } -}; + }; -/** - * Remove a listener from the current node - * - * @param {*} listener - */ -EventListenerTree.prototype.removeOwnListener = function(listener) { - var listeners = this.listeners; - if (!listeners) return; - if (listeners.length === 1) { - if (listeners[0] === listener) { - this.listeners = null; - if (!this.children) { - this.destroy(); + /** + * Remove a listener from the current node + * + * @param {*} listener + */ + removeOwnListener(listener) { + var listeners = this.listeners; + if (!listeners) return; + if (listeners.length === 1) { + if (listeners[0] === listener) { + this.listeners = null; + if (!this.children) { + this.destroy(); + } } + return; } - return; - } - var i = listeners.indexOf(listener); - if (i > -1) { - listeners.splice(i, 1); - } -}; + var i = listeners.indexOf(listener); + if (i > -1) { + listeners.splice(i, 1); + } + }; -/** - * Remove all listeners and descendent listeners for a path location - * - * @param {string[]} segments - */ -EventListenerTree.prototype.removeAllListeners = function(segments) { - var node = this._getChild(segments); - if (node) { - node.destroy(); - } -}; + /** + * Remove all listeners and descendent listeners for a path location + * + * @param {string[]} segments + */ + removeAllListeners(segments: Segments) { + var node = this._getChild(segments); + if (node) { + node.destroy(); + } + }; -/** - * Return direct listeners to `segments` - * - * @param {string[]} segments - * @return {Array} listeners - */ -EventListenerTree.prototype.getListeners = function(segments) { - var node = this._getChild(segments); - return (node && node.listeners) ? node.listeners.slice() : []; -}; + /** + * Return direct listeners to `segments` + * + * @param {string[]} segments + * @return {Array} listeners + */ + getListeners(segments: Segments) { + var node = this._getChild(segments); + return (node && node.listeners) ? node.listeners.slice() : []; + }; -/** - * Return an array with each of the listeners that may be affected by a change - * to `segments`. These are: - * 1. Listeners to each node from the root to the node for `segments` - * 2. Listeners to all descendent nodes under `segments` - * - * @param {string[]} segments - * @return {Array} listeners - */ -EventListenerTree.prototype.getAffectedListeners = function(segments) { - var listeners = []; - var node = pushAncestorListeners(listeners, segments, this); - if (node) { - pushDescendantListeners(listeners, node); - } - return listeners; -}; + /** + * Return an array with each of the listeners that may be affected by a change + * to `segments`. These are: + * 1. Listeners to each node from the root to the node for `segments` + * 2. Listeners to all descendent nodes under `segments` + * + * @param {string[]} segments + * @return {Array} listeners + */ + getAffectedListeners(segments: Segments) { + var listeners = []; + var node = pushAncestorListeners(listeners, segments, this); + if (node) { + pushDescendantListeners(listeners, node); + } + return listeners; + }; -/** - * Return an array with each of the listeners to descendent nodes, not - * including listeners to `segments` itself - * - * @param {string[]} segments - * @return {Array} listeners - */ -EventListenerTree.prototype.getDescendantListeners = function(segments) { - var listeners = []; - var node = this._getChild(segments); - if (node) { - pushDescendantListeners(listeners, node); - } - return listeners; -}; + /** + * Return an array with each of the listeners to descendent nodes, not + * including listeners to `segments` itself + * + * @param {string[]} segments + * @return {Array} listeners + */ + getDescendantListeners(segments: Segments) { + var listeners = []; + var node = this._getChild(segments); + if (node) { + pushDescendantListeners(listeners, node); + } + return listeners; + }; -/** - * Return an array with each of the listeners to descendent nodes, not - * including listeners to this node itself - * - * @return {Array} listeners - */ -EventListenerTree.prototype.getOwnDescendantListeners = function() { - var listeners = []; - pushDescendantListeners(listeners, this); - return listeners; -}; + /** + * Return an array with each of the listeners to descendent nodes, not + * including listeners to this node itself + * + * @return {Array} listeners + */ + getOwnDescendantListeners() { + var listeners = []; + pushDescendantListeners(listeners, this); + return listeners; + }; -/** - * Return an array with each of the listeners to `segments`, including - * treating wildcard segments ('*') and remainder segments ('**') as matches - * - * @param {string[]} segments - * @return {Array} listeners - */ -EventListenerTree.prototype.getWildcardListeners = function(segments) { - var listeners = []; - pushWildcardListeners(listeners, this, segments, 0); - return listeners; -}; + /** + * Return an array with each of the listeners to `segments`, including + * treating wildcard segments ('*') and remainder segments ('**') as matches + * + * @param {string[]} segments + * @return {Array} listeners + */ + getWildcardListeners(segments: Segments) { + var listeners = []; + pushWildcardListeners(listeners, this, segments, 0); + return listeners; + }; +} /** * Push listeners matching `segments`, wildcards ('*'), and remainders ('**') diff --git a/src/Model/EventMapTree.ts b/src/Model/EventMapTree.ts index 3ea09d070..e95032fe1 100644 --- a/src/Model/EventMapTree.ts +++ b/src/Model/EventMapTree.ts @@ -1,6 +1,5 @@ -var FastMap = require('./FastMap'); - -module.exports = EventMapTree; +import { type Segments } from './Model'; +import { FastMap } from './FastMap'; /** * Construct a tree root when invoked without any arguments. Children nodes are @@ -9,159 +8,177 @@ module.exports = EventMapTree; * @param {EventMapTree} [parent] * @param {string} [segment] */ -function EventMapTree(parent, segment) { - this.parent = parent; - this.segment = segment; - this.children = null; - this.listener = null; -} +export class EventMapTree { + parent?: EventMapTree; + segment?: string; + children: any; + listener: any; -/** - * Remove the reference to this node from its parent so that it can be garbage - * collected. This is called internally when all listener to a node - * are removed - */ -EventMapTree.prototype.destroy = function() { - // For all non-root nodes, remove the reference to the node - var parent = this.parent; - if (parent) { - // Remove reference to this node from its parent - var children = parent.children; - if (children) { - children.del(this.segment); - if (children.size > 0) return; - parent.children = null; - } - // Destroy parent if it no longer has any dependents - if (parent.listener == null) { - parent.destroy(); - } - return; + constructor(parent?: EventMapTree, segment?: string) { + this.parent = parent; + this.segment = segment; + this.children = null; + this.listener = null; } - // For the root node, reset any references to listener or children - this.children = null; - this.listener = null; -}; -/** - * Get a node for a path if it exists - * - * @param {string[]} segments - * @return {EventMapTree|undefined} - */ -EventMapTree.prototype._getChild = function(segments) { - var node = this; - for (var i = 0, len = segments.length; i < len; i++) { - var children = node.children; - if (!children) return; - var segment = segments[i]; - node = children.values[segment]; - if (!node) return; - } - return node; -}; + /** + * Remove the reference to this node from its parent so that it can be garbage + * collected. This is called internally when all listener to a node + * are removed + */ + destroy() { + // For all non-root nodes, remove the reference to the node + var parent = this.parent; + if (parent) { + // Remove reference to this node from its parent + var children = parent.children; + if (children) { + children.del(this.segment); + if (children.size > 0) return; + parent.children = null; + } + // Destroy parent if it no longer has any dependents + if (parent.listener == null) { + parent.destroy(); + } + return; + } + // For the root node, reset any references to listener or children + this.children = null; + this.listener = null; + }; -/** - * If a path already has a node, return it. Otherwise, create the node and - * parents in a lazy manner and return the node for the path - * - * @param {string[]} segments - * @return {EventMapTree} - */ -EventMapTree.prototype._getOrCreateChild = function(segments) { - var node = this; - for (var i = 0, len = segments.length; i < len; i++) { - var children = node.children; - if (!children) { - children = node.children = new FastMap(); + /** + * Get a node for a path if it exists + * + * @param {string[]} segments + * @return {EventMapTree|undefined} + */ + _getChild(segments: Segments) { + var node = this; + for (var i = 0, len = segments.length; i < len; i++) { + var children = node.children; + if (!children) return; + var segment = segments[i]; + node = children.values[segment]; + if (!node) return; } - var segment = segments[i]; - var next = children.values[segment]; - if (next) { - node = next; - } else { - node = new EventMapTree(node, segment); - children.set(segment, node); + return node; + }; + + /** + * If a path already has a node, return it. Otherwise, create the node and + * parents in a lazy manner and return the node for the path + * + * @param {string[]} segments + * @return {EventMapTree} + */ + _getOrCreateChild(segments: Segments) { + var node: EventMapTree = this; + for (var i = 0, len = segments.length; i < len; i++) { + var children = node.children; + if (!children) { + children = node.children = new FastMap(); + } + var segment = segments[i]; + var next = children.values[segment]; + if (next) { + node = next; + } else { + node = new EventMapTree(node, segment); + children.set(segment, node); + } } - } - return node; -}; + return node; + }; -/** - * Assign a listener to a path location. Listener may be any type of value. - * Return the previous listener value if any - * - * @param {string[]} segments - * @param {*} listener - * @return {*} previous - */ -EventMapTree.prototype.setListener = function(segments, listener) { - var node = this._getOrCreateChild(segments); - var previous = node.listener; - node.listener = listener; - return previous; -}; + /** + * Assign a listener to a path location. Listener may be any type of value. + * Return the previous listener value if any + * + * @param {string[]} segments + * @param {*} listener + * @return {*} previous + */ + setListener(segments: Segments, listener) { + var node = this._getOrCreateChild(segments); + var previous = node.listener; + node.listener = listener; + return previous; + }; -/** - * Remove the listener at a path location and return it - * - * @param {string[]} segments - * @return {*} previous - */ -EventMapTree.prototype.deleteListener = function(segments) { - var node = this._getChild(segments); - if (!node) return; - var previous = node.listener; - node.listener = null; - if (!node.children) { - node.destroy(); - } - return previous; -}; + /** + * Remove the listener at a path location and return it + * + * @param {string[]} segments + * @return {*} previous + */ + deleteListener(segments: Segments) { + var node = this._getChild(segments); + if (!node) return; + var previous = node.listener; + node.listener = null; + if (!node.children) { + node.destroy(); + } + return previous; + }; -/** - * Remove all listeners and descendent listeners for a path location. Return the - * node for the path location if any - * - * @param {string[]} segments - * @return {EventMapTree} - */ -EventMapTree.prototype.deleteAllListeners = function(segments) { - var node = this._getChild(segments); - if (node) { - node.destroy(); - } - return node; -}; + /** + * Remove all listeners and descendent listeners for a path location. Return the + * node for the path location if any + * + * @param {string[]} segments + * @return {EventMapTree} + */ + deleteAllListeners(segments: Segments) { + var node = this._getChild(segments); + if (node) { + node.destroy(); + } + return node; + }; -/** - * Return the direct listener to `segments` if any - * - * @param {string[]} segments - * @return {*} listeners - */ -EventMapTree.prototype.getListener = function(segments) { - var node = this._getChild(segments); - return (node) ? node.listener : null; -}; + /** + * Return the direct listener to `segments` if any + * + * @param {string[]} segments + * @return {*} listeners + */ + getListener(segments: Segments) { + var node = this._getChild(segments); + return (node) ? node.listener : null; + }; -/** - * Return an array with each of the listeners that may be affected by a change - * to `segments`. These are: - * 1. Listeners to each node from the root to the node for `segments` - * 2. Listeners to all descendent nodes under `segments` - * - * @param {string[]} segments - * @return {Array} listeners - */ -EventMapTree.prototype.getAffectedListeners = function(segments) { - var listeners = []; - var node = pushAncestorListeners(listeners, segments, this); - if (node) { - pushDescendantListeners(listeners, node); - } - return listeners; -}; + /** + * Return an array with each of the listeners that may be affected by a change + * to `segments`. These are: + * 1. Listeners to each node from the root to the node for `segments` + * 2. Listeners to all descendent nodes under `segments` + * + * @param {string[]} segments + * @return {Array} listeners + */ + getAffectedListeners(segments) { + var listeners = []; + var node = pushAncestorListeners(listeners, segments, this); + if (node) { + pushDescendantListeners(listeners, node); + } + return listeners; + }; + + /** + * Call the callback with each listener to the node and its decendants + * + * @param {EventMapTree} node + * @param {Function} callback + */ + forEach(callback) { + forListener(this, callback); + forDescendantListeners(this, callback); + }; +} /** * Push node's direct listener onto the passed in array if not null @@ -214,17 +231,6 @@ function pushDescendantListeners(listeners, node) { } } -/** - * Call the callback with each listener to the node and its decendants - * - * @param {EventMapTree} node - * @param {Function} callback - */ -EventMapTree.prototype.forEach = function(callback) { - forListener(this, callback); - forDescendantListeners(this, callback); -}; - /** * Call the callback with the node's direct listener if not null * diff --git a/src/Model/FastMap.ts b/src/Model/FastMap.ts index 3c43c9278..3fccfa026 100644 --- a/src/Model/FastMap.ts +++ b/src/Model/FastMap.ts @@ -1,18 +1,24 @@ -module.exports = FastMap; -function FastMap() { - this.values = {}; - this.size = 0; -} -FastMap.prototype.set = function(key, value) { - if (!(key in this.values)) { - this.size++; - } - return this.values[key] = value; -}; -FastMap.prototype.del = function(key) { - if (key in this.values) { - this.size--; +export class FastMap{ + values: Record; + size: number; + + constructor() { + this.values = {}; + this.size = 0; } - delete this.values[key]; -}; + + set(key: string, value: T) { + if (!(key in this.values)) { + this.size++; + } + return this.values[key] = value; + }; + + del(key: string) { + if (key in this.values) { + this.size--; + } + delete this.values[key]; + }; +} diff --git a/src/Model/LocalDoc.ts b/src/Model/LocalDoc.ts index eff4f4bf1..cd6e8e689 100644 --- a/src/Model/LocalDoc.ts +++ b/src/Model/LocalDoc.ts @@ -1,220 +1,219 @@ -var Doc = require('./Doc'); +import { type Model } from './Model'; +import { Doc } from './Doc'; var util = require('../util'); -module.exports = LocalDoc; - -function LocalDoc(model, collectionName, id, data) { - Doc.call(this, model, collectionName, id); - this.data = data; - this._updateCollectionData(); -} - -LocalDoc.prototype = new Doc(); - -LocalDoc.prototype._updateCollectionData = function() { - this.collectionData[this.id] = this.data; -}; - -LocalDoc.prototype.create = function(value, cb) { - if (this.data !== undefined) { - var message = this._errorMessage('create on local document with data', null, this.data); - var err = new Error(message); - return cb(err); - } - this.data = value; - this._updateCollectionData(); - cb(); -}; - -LocalDoc.prototype.set = function(segments, value, cb) { - function set(node, key) { - var previous = node[key]; - node[key] = value; - return previous; - } - return this._apply(segments, set, cb); -}; - -LocalDoc.prototype.del = function(segments, cb) { - // Don't do anything if the value is already undefined, since - // apply creates objects as it traverses, and the del method - // should not create anything - var previous = this.get(segments); - if (previous === undefined) { +export class LocalDoc extends Doc{ + constructor(model: Model, collectionName: string, id: string, data: any) { + super(model, collectionName, id, data); + Doc.call(this, model, collectionName, id); + this._updateCollectionData(); + } + + _updateCollectionData() { + this.collectionData[this.id] = this.data; + }; + + create(value: any, cb) { + if (this.data !== undefined) { + var message = this._errorMessage('create on local document with data', null, this.data); + var err = new Error(message); + return cb(err); + } + this.data = value; + this._updateCollectionData(); cb(); - return; - } - function del(node, key) { - delete node[key]; - return previous; - } - return this._apply(segments, del, cb); -}; - -LocalDoc.prototype.increment = function(segments, byNumber, cb) { - var self = this; - function validate(value) { - if (typeof value === 'number' || value == null) return; - return new TypeError(self._errorMessage( - 'increment on non-number', segments, value - )); - } - function increment(node, key) { - var value = (node[key] || 0) + byNumber; - node[key] = value; - return value; - } - return this._validatedApply(segments, validate, increment, cb); -}; - -LocalDoc.prototype.push = function(segments, value, cb) { - function push(arr) { - return arr.push(value); - } - return this._arrayApply(segments, push, cb); -}; - -LocalDoc.prototype.unshift = function(segments, value, cb) { - function unshift(arr) { - return arr.unshift(value); - } - return this._arrayApply(segments, unshift, cb); -}; - -LocalDoc.prototype.insert = function(segments, index, values, cb) { - function insert(arr) { - arr.splice.apply(arr, [index, 0].concat(values)); - return arr.length; - } - return this._arrayApply(segments, insert, cb); -}; - -LocalDoc.prototype.pop = function(segments, cb) { - function pop(arr) { - return arr.pop(); - } - return this._arrayApply(segments, pop, cb); -}; - -LocalDoc.prototype.shift = function(segments, cb) { - function shift(arr) { - return arr.shift(); - } - return this._arrayApply(segments, shift, cb); -}; - -LocalDoc.prototype.remove = function(segments, index, howMany, cb) { - function remove(arr) { - return arr.splice(index, howMany); - } - return this._arrayApply(segments, remove, cb); -}; - -LocalDoc.prototype.move = function(segments, from, to, howMany, cb) { - function move(arr) { - // Remove from old location - var values = arr.splice(from, howMany); - // Insert in new location - arr.splice.apply(arr, [to, 0].concat(values)); - return values; - } - return this._arrayApply(segments, move, cb); -}; - -LocalDoc.prototype.stringInsert = function(segments, index, value, cb) { - var self = this; - function validate(value) { - if (typeof value === 'string' || value == null) return; - return new TypeError(self._errorMessage( - 'stringInsert on non-string', segments, value - )); - } - function stringInsert(node, key) { - var previous = node[key]; - if (previous == null) { + }; + + set(segments, value, cb) { + function set(node, key) { + var previous = node[key]; node[key] = value; return previous; } - node[key] = previous.slice(0, index) + value + previous.slice(index); - return previous; - } - return this._validatedApply(segments, validate, stringInsert, cb); -}; - -LocalDoc.prototype.stringRemove = function(segments, index, howMany, cb) { - var self = this; - function validate(value) { - if (typeof value === 'string' || value == null) return; - return new TypeError(self._errorMessage( - 'stringRemove on non-string', segments, value - )); - } - function stringRemove(node, key) { - var previous = node[key]; - if (previous == null) return previous; - if (index < 0) index += previous.length; - node[key] = previous.slice(0, index) + previous.slice(index + howMany); - return previous; - } - return this._validatedApply(segments, validate, stringRemove, cb); -}; - -LocalDoc.prototype.get = function(segments) { - return util.lookup(segments, this.data); -}; - -/** - * @param {Array} segments is the array representing a path - * @param {Function} fn(node, key) applies a mutation on node[key] - * @return {Object} returns the return value of fn(node, key) - */ -LocalDoc.prototype._createImplied = function(segments, fn) { - var node = this; - var key = 'data'; - var i = 0; - var nextKey = segments[i++]; - while (nextKey != null) { - // Get or create implied object or array - node = node[key] || (node[key] = /^\d+$/.test(nextKey) ? [] : {}); - key = nextKey; - nextKey = segments[i++]; - } - return fn(node, key); -}; - -LocalDoc.prototype._apply = function(segments, fn, cb) { - var out = this._createImplied(segments, fn); - this._updateCollectionData(); - cb(); - return out; -}; - -LocalDoc.prototype._validatedApply = function(segments, validate, fn, cb) { - var out = this._createImplied(segments, function(node, key) { - var err = validate(node[key]); - if (err) return cb(err); + return this._apply(segments, set, cb); + }; + + del(segments, cb) { + // Don't do anything if the value is already undefined, since + // apply creates objects as it traverses, and the del method + // should not create anything + var previous = this.get(segments); + if (previous === undefined) { + cb(); + return; + } + function del(node, key) { + delete node[key]; + return previous; + } + return this._apply(segments, del, cb); + }; + + increment(segments, byNumber, cb) { + var self = this; + function validate(value) { + if (typeof value === 'number' || value == null) return; + return new TypeError(self._errorMessage( + 'increment on non-number', segments, value + )); + } + function increment(node, key) { + var value = (node[key] || 0) + byNumber; + node[key] = value; + return value; + } + return this._validatedApply(segments, validate, increment, cb); + }; + + push(segments, value, cb) { + function push(arr) { + return arr.push(value); + } + return this._arrayApply(segments, push, cb); + }; + + unshift(segments, value, cb) { + function unshift(arr) { + return arr.unshift(value); + } + return this._arrayApply(segments, unshift, cb); + }; + + insert(segments, index, values, cb) { + function insert(arr) { + arr.splice.apply(arr, [index, 0].concat(values)); + return arr.length; + } + return this._arrayApply(segments, insert, cb); + }; + + pop(segments, cb) { + function pop(arr) { + return arr.pop(); + } + return this._arrayApply(segments, pop, cb); + }; + + shift(segments, cb) { + function shift(arr) { + return arr.shift(); + } + return this._arrayApply(segments, shift, cb); + }; + + remove(segments, index, howMany, cb) { + function remove(arr) { + return arr.splice(index, howMany); + } + return this._arrayApply(segments, remove, cb); + }; + + move(segments, from, to, howMany, cb) { + function move(arr) { + // Remove from old location + var values = arr.splice(from, howMany); + // Insert in new location + arr.splice.apply(arr, [to, 0].concat(values)); + return values; + } + return this._arrayApply(segments, move, cb); + }; + + stringInsert(segments, index, value, cb) { + var self = this; + function validate(value) { + if (typeof value === 'string' || value == null) return; + return new TypeError(self._errorMessage( + 'stringInsert on non-string', segments, value + )); + } + function stringInsert(node, key) { + var previous = node[key]; + if (previous == null) { + node[key] = value; + return previous; + } + node[key] = previous.slice(0, index) + value + previous.slice(index); + return previous; + } + return this._validatedApply(segments, validate, stringInsert, cb); + }; + + stringRemove(segments, index, howMany, cb) { + var self = this; + function validate(value) { + if (typeof value === 'string' || value == null) return; + return new TypeError(self._errorMessage( + 'stringRemove on non-string', segments, value + )); + } + function stringRemove(node, key) { + var previous = node[key]; + if (previous == null) return previous; + if (index < 0) index += previous.length; + node[key] = previous.slice(0, index) + previous.slice(index + howMany); + return previous; + } + return this._validatedApply(segments, validate, stringRemove, cb); + }; + + get(segments) { + return util.lookup(segments, this.data); + }; + + /** + * @param {Array} segments is the array representing a path + * @param {Function} fn(node, key) applies a mutation on node[key] + * @return {Object} returns the return value of fn(node, key) + */ + _createImplied(segments, fn) { + var node = this; + var key = 'data'; + var i = 0; + var nextKey = segments[i++]; + while (nextKey != null) { + // Get or create implied object or array + node = node[key] || (node[key] = /^\d+$/.test(nextKey) ? [] : {}); + key = nextKey; + nextKey = segments[i++]; + } return fn(node, key); - }); - this._updateCollectionData(); - cb(); - return out; -}; - -LocalDoc.prototype._arrayApply = function(segments, fn, cb) { - // Lookup a pointer to the property or nested property & - // return the current value or create a new array - var arr = this._createImplied(segments, nodeCreateArray); - - if (!Array.isArray(arr)) { - var message = this._errorMessage(fn.name + ' on non-array', segments, arr); - var err = new TypeError(message); - return cb(err); - } - var out = fn(arr); - this._updateCollectionData(); - cb(); - return out; -}; + }; + + _apply(segments, fn, cb) { + var out = this._createImplied(segments, fn); + this._updateCollectionData(); + cb(); + return out; + }; + + _validatedApply(segments, validate, fn, cb) { + var out = this._createImplied(segments, function(node, key) { + var err = validate(node[key]); + if (err) return cb(err); + return fn(node, key); + }); + this._updateCollectionData(); + cb(); + return out; + }; + + _arrayApply(segments, fn, cb) { + // Lookup a pointer to the property or nested property & + // return the current value or create a new array + var arr = this._createImplied(segments, nodeCreateArray); + + if (!Array.isArray(arr)) { + var message = this._errorMessage(fn.name + ' on non-array', segments, arr); + var err = new TypeError(message); + return cb(err); + } + var out = fn(arr); + this._updateCollectionData(); + cb(); + return out; + }; +} function nodeCreateArray(node, key) { var node = node[key] || (node[key] = []); diff --git a/src/Model/Model.ts b/src/Model/Model.ts index 0487f9166..85856c7a8 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -1,15 +1,25 @@ import { v4 as uuidv4 } from 'uuid'; import { EventEmitter } from 'events'; +import { Context } from 'vm'; -interface DebugOptions { - debugMutations?: boolean, - disableSubmit?: boolean, -} +declare module './Model' { + interface DebugOptions { + debugMutations?: boolean, + disableSubmit?: boolean, + remoteMutations?: boolean, + } + + interface ModelOptions { + debug?: DebugOptions; + fetchOnly?: boolean; + unloadDelay?: number; + bundleTimeout?: number; + } -interface ModelOptions { - debug?: DebugOptions; + type ErrorCallback = (err?: Error) => void; } + type ModelInitFunction = (instance: Model, options: ModelOptions) => void; export class Model extends EventEmitter { @@ -20,12 +30,12 @@ export class Model extends EventEmitter { root: Model; _at: () => Model; - _context: {}; + _context: Context; _eventContext: number | null; _events: []; _maxListeners: number; - _pass: () => void; - _preventCompose: () => void; + _pass: any; + _preventCompose: boolean; _silent: boolean; constructor(options: ModelOptions = {}) { diff --git a/src/Model/Query.ts b/src/Model/Query.ts index a5b13b8b4..9c0963bed 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -1,20 +1,29 @@ -var util = require('../util'); -var Model = require('./Model'); -var CollectionMap = require('./CollectionMap'); +import { type Context } from './contexts'; +import { ErrorCallback, Model, type Segments } from './Model'; +import { CollectionMap } from './CollectionMap'; var defaultType = require('sharedb/lib/client').types.defaultType; +var util = require('../util'); var promisify = util.promisify; -module.exports = Query; +declare module './Model' { + interface Model { + _queries: Queries; + query(collectionName, expression, options): Query; + _getOrCreateQuery(collectionName, expression, options, QueryConstructor): Query; + sanitizeQuery(expression): Query; + _initQueries(items): void; + } +} -Model.INITS.push(function(model) { +Model.INITS.push(function (model) { model.root._queries = new Queries(); }); -Model.prototype.query = function(collectionName, expression, options) { +Model.prototype.query = function (collectionName, expression, options) { // DEPRECATED: Passing in a string as the third argument specifies the db // option for backward compatibility if (typeof options === 'string') { - options = {db: options}; + options = { db: options }; } return this._getOrCreateQuery(collectionName, expression, options, Query); }; @@ -30,7 +39,7 @@ Model.prototype.query = function(collectionName, expression, options) { * @param {new (model: Model, collectionName: string, expression: any, options: any) => Query} QueryConstructor - * constructor function for a Query, to create one if not already present on this model */ -Model.prototype._getOrCreateQuery = function(collectionName, expression, options, QueryConstructor) { +Model.prototype._getOrCreateQuery = function (collectionName, expression, options, QueryConstructor) { expression = this.sanitizeQuery(expression); var contextId = this._context.id; var query = this.root._queries.get(contextId, collectionName, expression, options); @@ -46,7 +55,7 @@ Model.prototype._getOrCreateQuery = function(collectionName, expression, options // filter and absence means that all values are accepted. We aren't checking // for cycles, which aren't allowed in JSON, so this could throw a max call // stack error -Model.prototype.sanitizeQuery = function(expression) { +Model.prototype.sanitizeQuery = function (expression) { if (expression && typeof expression === 'object') { for (var key in expression) { if (expression.hasOwnProperty(key)) { @@ -63,7 +72,7 @@ Model.prototype.sanitizeQuery = function(expression) { }; // Called during initialization of the bundle on page load. -Model.prototype._initQueries = function(items) { +Model.prototype._initQueries = function (items) { for (var i = 0; i < items.length; i++) { var item = items[i]; var countsList = item[0]; @@ -95,7 +104,7 @@ Model.prototype._initQueries = function(items) { var id = result[2] || data.id; var type = result[3]; ids.push(id); - var snapshot = {data: data, v: v, type: type}; + var snapshot = { data: data, v: v, type: type }; this.getOrCreateDoc(collectionName, id, snapshot); } query._addMapIds(ids); @@ -111,451 +120,477 @@ Model.prototype._initQueries = function(items) { } }; -function Queries() { - // Flattened map of queries by hash. Currently used in contexts - this.map = {}; - // Nested map of queries by collection then hash - this.collectionMap = new CollectionMap(); -} -Queries.prototype.add = function(query) { - this.map[query.hash] = query; - this.collectionMap.set(query.collectionName, query.hash, query); -}; -Queries.prototype.remove = function(query) { - delete this.map[query.hash]; - this.collectionMap.del(query.collectionName, query.hash); -}; -Queries.prototype.get = function(contextId, collectionName, expression, options) { - var hash = queryHash(contextId, collectionName, expression, options); - return this.map[hash]; -}; -Queries.prototype.toJSON = function() { - var out = []; - for (var hash in this.map) { - var query = this.map[hash]; - if (query.subscribeCount || query.fetchCount) { - out.push(query.serialize()); - } +export class Queries { + map: Record; + collectionMap: CollectionMap; + + constructor() { + // Flattened map of queries by hash. Currently used in contexts + this.map = {}; + // Nested map of queries by collection then hash + this.collectionMap = new CollectionMap(); } - return out; -}; -function Query(model, collectionName, expression, options) { - // Note that a new childModel based on the root scope is created. Only the - // context from the passed in model has an effect - this.model = model.root.pass({$query: this}); - this.context = model._context; - this.collectionName = collectionName; - this.expression = util.deepCopy(expression); - this.options = options; - this.hash = queryHash(this.context.id, collectionName, expression, options); - this.segments = ['$queries', this.hash]; - this.idsSegments = ['$queries', this.hash, 'ids']; - this.extraSegments = ['$queries', this.hash, 'extra']; - - this._pendingSubscribeCallbacks = []; - - // These are used to help cleanup appropriately when calling unsubscribe and - // unfetch. A query won't be fully cleaned up until unfetch and unsubscribe - // are called the same number of times that fetch and subscribe were called. - this.subscribeCount = 0; - this.fetchCount = 0; - - this.created = false; - this.shareQuery = null; - - // idMap is checked in maybeUnload to see if the query is currently holding - // a reference to an id in its results set. This map is duplicative of the - // actual results id list stored in the model, but we are maintaining it, - // because otherwise maybeUnload would be looping through the entire results - // set of each query on the same collection for every doc checked - // - // Map of id -> count of ids - this.idMap = {}; -} + add(query: Query) { + this.map[query.hash] = query; + this.collectionMap.set(query.collectionName, query.hash, query); + }; -Query.prototype.create = function() { - this.created = true; - this.model.root._queries.add(this); -}; + remove(query: Query) { + delete this.map[query.hash]; + this.collectionMap.del(query.collectionName, query.hash); + }; + + get(contextId, collectionName, expression, options) { + var hash = queryHash(contextId, collectionName, expression, options); + return this.map[hash]; + }; + + toJSON() { + var out = []; + for (var hash in this.map) { + var query = this.map[hash]; + if (query.subscribeCount || query.fetchCount) { + out.push(query.serialize()); + } + } + return out; + }; +} -Query.prototype.destroy = function() { - var ids = this.getIds(); - this.created = false; - if (this.shareQuery) { - this.shareQuery.destroy(); +class Query { + model: Model; + context: Context; + collectionName: string; + expression: any; + options: any; + hash: string; + segments: Segments; + idsSegments: string[]; + extraSegments: string[]; + _pendingSubscribeCallbacks: any[]; + subscribeCount: number; + fetchCount: number; + created: boolean; + shareQuery: any | null; + idMap: Record; + + constructor(model, collectionName, expression, options) { + // Note that a new childModel based on the root scope is created. Only the + // context from the passed in model has an effect + this.model = model.root.pass({ $query: this }); + this.context = model._context; + this.collectionName = collectionName; + this.expression = util.deepCopy(expression); + this.options = options; + this.hash = queryHash(this.context.id, collectionName, expression, options); + this.segments = ['$queries', this.hash]; + this.idsSegments = ['$queries', this.hash, 'ids']; + this.extraSegments = ['$queries', this.hash, 'extra']; + + this._pendingSubscribeCallbacks = []; + + // These are used to help cleanup appropriately when calling unsubscribe and + // unfetch. A query won't be fully cleaned up until unfetch and unsubscribe + // are called the same number of times that fetch and subscribe were called. + this.subscribeCount = 0; + this.fetchCount = 0; + + this.created = false; this.shareQuery = null; - } - this.model.root._queries.remove(this); - this.idMap = {}; - this.model._del(this.segments); - this._maybeUnloadDocs(ids); -}; -Query.prototype.fetch = function(cb) { - cb = this.model.wrapCallback(cb); - this.context.fetchQuery(this); + // idMap is checked in maybeUnload to see if the query is currently holding + // a reference to an id in its results set. This map is duplicative of the + // actual results id list stored in the model, but we are maintaining it, + // because otherwise maybeUnload would be looping through the entire results + // set of each query on the same collection for every doc checked + // + // Map of id -> count of ids + this.idMap = {}; + } - this.fetchCount++; + create() { + this.created = true; + this.model.root._queries.add(this); + }; - if (!this.created) this.create(); + destroy() { + var ids = this.getIds(); + this.created = false; + if (this.shareQuery) { + this.shareQuery.destroy(); + this.shareQuery = null; + } + this.model.root._queries.remove(this); + this.idMap = {}; + this.model._del(this.segments); + this._maybeUnloadDocs(ids); + }; - var query = this; - function fetchCb(err, results, extra) { - if (err) return cb(err); - query._setExtra(extra); - query._setResults(results); - cb(); - } - this.model.root.connection.createFetchQuery( - this.collectionName, - this.expression, - this.options, - fetchCb - ); - return this; -}; + fetch(cb) { + cb = this.model.wrapCallback(cb); + this.context.fetchQuery(this); -Query.prototype.fetchPromised = promisify(Query.prototype.fetch); + this.fetchCount++; -Query.prototype.subscribe = function(cb) { - cb = this.model.wrapCallback(cb); - this.context.subscribeQuery(this); + if (!this.created) this.create(); - if (this.subscribeCount++) { var query = this; - process.nextTick(function() { - var data = query.model._get(query.segments); - if (data) { - cb(); - } else { - query._pendingSubscribeCallbacks.push(cb); - } - }); + function fetchCb(err, results, extra) { + if (err) return cb(err); + query._setExtra(extra); + query._setResults(results); + cb(); + } + this.model.root.connection.createFetchQuery( + this.collectionName, + this.expression, + this.options, + fetchCb + ); return this; - } + }; - if (!this.created) this.create(); + fetchPromised = promisify(Query.prototype.fetch); - var options = (this.options) ? util.copy(this.options) : {}; - options.results = this._getShareResults(); + subscribe(cb) { + cb = this.model.wrapCallback(cb); + this.context.subscribeQuery(this); - // When doing server-side rendering, we actually do a fetch the first time - // that subscribe is called, but keep track of the state as if subscribe - // were called for proper initialization in the client - if (this.model.root.fetchOnly) { - this._shareFetchedSubscribe(options, cb); - } else { - this._shareSubscribe(options, cb); - } + if (this.subscribeCount++) { + var query = this; + process.nextTick(function () { + var data = query.model._get(query.segments); + if (data) { + cb(); + } else { + query._pendingSubscribeCallbacks.push(cb); + } + }); + return this; + } - return this; -}; + if (!this.created) this.create(); -Query.prototype.subscribePromised = promisify(Query.prototype.subscribe); + var options = (this.options) ? util.copy(this.options) : {}; + options.results = this._getShareResults(); -Query.prototype._subscribeCb = function(cb) { - var query = this; - return function subscribeCb(err, results, extra) { - if (err) return query._flushSubscribeCallbacks(err, cb); - query._setExtra(extra); - query._setResults(results); - query._flushSubscribeCallbacks(null, cb); + // When doing server-side rendering, we actually do a fetch the first time + // that subscribe is called, but keep track of the state as if subscribe + // were called for proper initialization in the client + if (this.model.root.fetchOnly) { + this._shareFetchedSubscribe(options, cb); + } else { + this._shareSubscribe(options, cb); + } + + return this; }; -}; -Query.prototype._shareFetchedSubscribe = function(options, cb) { - this.model.root.connection.createFetchQuery( - this.collectionName, - this.expression, - options, - this._subscribeCb(cb) - ); -}; + subscribePromised = promisify(Query.prototype.subscribe); -Query.prototype._shareSubscribe = function(options, cb) { - var query = this; - // Sanity check, though this shouldn't happen - if (this.shareQuery) { - this.shareQuery.destroy(); - } - this.shareQuery = this.model.root.connection.createSubscribeQuery( - this.collectionName, - this.expression, - options, - this._subscribeCb(cb) - ); - this.shareQuery.on('insert', function(shareDocs, index) { - var ids = resultsIds(shareDocs); - query._addMapIds(ids); - query.model._insert(query.idsSegments, index, ids); - }); - this.shareQuery.on('remove', function(shareDocs, index) { - var ids = resultsIds(shareDocs); - query._removeMapIds(ids); - query.model._remove(query.idsSegments, index, shareDocs.length); - }); - this.shareQuery.on('move', function(shareDocs, from, to) { - query.model._move(query.idsSegments, from, to, shareDocs.length); - }); - this.shareQuery.on('extra', function(extra) { - query.model._setDiffDeep(query.extraSegments, extra); - }); - this.shareQuery.on('error', function(err) { - query.model._emitError(err, query.hash); - }); -}; + _subscribeCb(cb) { + var query = this; + return function subscribeCb(err, results, extra) { + if (err) return query._flushSubscribeCallbacks(err, cb); + query._setExtra(extra); + query._setResults(results); + query._flushSubscribeCallbacks(null, cb); + }; + }; -Query.prototype._removeMapIds = function(ids) { - for (var i = ids.length; i--;) { - var id = ids[i]; - if (this.idMap[id] > 1) { - this.idMap[id]--; - } else { - delete this.idMap[id]; - } - } - // Technically this isn't quite right and we might not wait the full unload - // delay if someone else calls maybeUnload for the same doc id. However, - // it is a lot easier to implement than delaying the removal until later and - // dealing with adds that might happen in the meantime. This will probably - // work to avoid thrashing subscribe/unsubscribe in expected cases - if (this.model.root.unloadDelay) { + _shareFetchedSubscribe(options, cb) { + this.model.root.connection.createFetchQuery( + this.collectionName, + this.expression, + options, + this._subscribeCb(cb) + ); + }; + + _shareSubscribe(options, cb) { var query = this; - setTimeout(function() { - query._maybeUnloadDocs(ids); - }, this.model.root.unloadDelay); - return; - } - this._maybeUnloadDocs(ids); -}; -Query.prototype._addMapIds = function(ids) { - for (var i = ids.length; i--;) { - var id = ids[i]; - this.idMap[id] = (this.idMap[id] || 0) + 1; - } -}; -Query.prototype._diffMapIds = function(ids) { - var addedIds = []; - var removedIds = []; - var newMap = {}; - for (var i = ids.length; i--;) { - var id = ids[i]; - newMap[id] = true; - if (this.idMap[id]) continue; - addedIds.push(id); - } - for (var id in this.idMap) { - if (newMap[id]) continue; - removedIds.push(id); - } - if (addedIds.length) this._addMapIds(addedIds); - if (removedIds.length) this._removeMapIds(removedIds); -}; -Query.prototype._setExtra = function(extra) { - if (extra === undefined) return; - this.model._setDiffDeep(this.extraSegments, extra); -}; -Query.prototype._setResults = function(results) { - var ids = resultsIds(results); - this._setResultIds(ids); -}; -Query.prototype._setResultIds = function(ids) { - this._diffMapIds(ids); - this.model._setArrayDiff(this.idsSegments, ids); -}; -Query.prototype._maybeUnloadDocs = function(ids) { - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - this.model._maybeUnloadDoc(this.collectionName, id); - } -}; + // Sanity check, though this shouldn't happen + if (this.shareQuery) { + this.shareQuery.destroy(); + } + this.shareQuery = this.model.root.connection.createSubscribeQuery( + this.collectionName, + this.expression, + options, + this._subscribeCb(cb) + ); + this.shareQuery.on('insert', function (shareDocs, index) { + var ids = resultsIds(shareDocs); + query._addMapIds(ids); + query.model._insert(query.idsSegments, index, ids); + }); + this.shareQuery.on('remove', function (shareDocs, index) { + var ids = resultsIds(shareDocs); + query._removeMapIds(ids); + query.model._remove(query.idsSegments, index, shareDocs.length); + }); + this.shareQuery.on('move', function (shareDocs, from, to) { + query.model._move(query.idsSegments, from, to, shareDocs.length); + }); + this.shareQuery.on('extra', function (extra) { + query.model._setDiffDeep(query.extraSegments, extra); + }); + this.shareQuery.on('error', function (err) { + query.model._emitError(err, query.hash); + }); + }; -// Flushes `_pendingSubscribeCallbacks`, calling each callback in the array, -// with an optional error to pass into each. `_pendingSubscribeCallbacks` will -// be empty after this runs. -Query.prototype._flushSubscribeCallbacks = function(err, cb) { - cb(err); - var pendingCallback; - while ((pendingCallback = this._pendingSubscribeCallbacks.shift())) { - pendingCallback(err); - } -}; + _removeMapIds(ids) { + for (var i = ids.length; i--;) { + var id = ids[i]; + if (this.idMap[id] > 1) { + this.idMap[id]--; + } else { + delete this.idMap[id]; + } + } + // Technically this isn't quite right and we might not wait the full unload + // delay if someone else calls maybeUnload for the same doc id. However, + // it is a lot easier to implement than delaying the removal until later and + // dealing with adds that might happen in the meantime. This will probably + // work to avoid thrashing subscribe/unsubscribe in expected cases + if (this.model.root.unloadDelay) { + var query = this; + setTimeout(function () { + query._maybeUnloadDocs(ids); + }, this.model.root.unloadDelay); + return; + } + this._maybeUnloadDocs(ids); + }; + _addMapIds(ids) { + for (var i = ids.length; i--;) { + var id = ids[i]; + this.idMap[id] = (this.idMap[id] || 0) + 1; + } + }; + _diffMapIds(ids) { + var addedIds = []; + var removedIds = []; + var newMap = {}; + for (var i = ids.length; i--;) { + var id = ids[i]; + newMap[id] = true; + if (this.idMap[id]) continue; + addedIds.push(id); + } + for (let id in this.idMap) { + if (newMap[id]) continue; + removedIds.push(id); + } + if (addedIds.length) this._addMapIds(addedIds); + if (removedIds.length) this._removeMapIds(removedIds); + }; + _setExtra(extra) { + if (extra === undefined) return; + this.model._setDiffDeep(this.extraSegments, extra); + }; + _setResults(results) { + var ids = resultsIds(results); + this._setResultIds(ids); + }; + _setResultIds(ids) { + this._diffMapIds(ids); + this.model._setArrayDiff(this.idsSegments, ids); + }; + _maybeUnloadDocs(ids) { + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + this.model._maybeUnloadDoc(this.collectionName, id); + } + }; + + // Flushes `_pendingSubscribeCallbacks`, calling each callback in the array, + // with an optional error to pass into each. `_pendingSubscribeCallbacks` will + // be empty after this runs. + _flushSubscribeCallbacks(err, cb) { + cb(err); + var pendingCallback; + while ((pendingCallback = this._pendingSubscribeCallbacks.shift())) { + pendingCallback(err); + } + }; + + unfetch(cb?: (err?: Error, count?: number) => void) { + cb = this.model.wrapCallback(cb); + this.context.unfetchQuery(this); -Query.prototype.unfetch = function(cb) { - cb = this.model.wrapCallback(cb); - this.context.unfetchQuery(this); + // No effect if the query is not currently fetched + if (!this.fetchCount) { + cb(); + return this; + } - // No effect if the query is not currently fetched - if (!this.fetchCount) { - cb(); + var query = this; + if (this.model.root.unloadDelay) { + setTimeout(finishUnfetchQuery, this.model.root.unloadDelay); + } else { + finishUnfetchQuery(); + } + function finishUnfetchQuery() { + var count = --query.fetchCount; + if (count) return cb(null, count); + // Cleanup when no fetches or subscribes remain + if (!query.subscribeCount) query.destroy(); + cb(null, 0); + } return this; - } + }; - var query = this; - if (this.model.root.unloadDelay) { - setTimeout(finishUnfetchQuery, this.model.root.unloadDelay); - } else { - finishUnfetchQuery(); - } - function finishUnfetchQuery() { - var count = --query.fetchCount; - if (count) return cb(null, count); - // Cleanup when no fetches or subscribes remain - if (!query.subscribeCount) query.destroy(); - cb(null, 0); - } - return this; -}; + unfetchPromised = promisify(Query.prototype.unfetch); -Query.prototype.unfetchPromised = promisify(Query.prototype.unfetch); + unsubscribe(cb?: (err?: Error, count?: number) => void) { + cb = this.model.wrapCallback(cb); + this.context.unsubscribeQuery(this); -Query.prototype.unsubscribe = function(cb) { - cb = this.model.wrapCallback(cb); - this.context.unsubscribeQuery(this); + // No effect if the query is not currently subscribed + if (!this.subscribeCount) { + cb(); + return this; + } - // No effect if the query is not currently subscribed - if (!this.subscribeCount) { - cb(); - return this; - } + var query = this; + if (this.model.root.unloadDelay) { + setTimeout(finishUnsubscribeQuery, this.model.root.unloadDelay); + } else { + finishUnsubscribeQuery(); + } + function finishUnsubscribeQuery() { + var count = --query.subscribeCount; + if (count) return cb(null, count); - var query = this; - if (this.model.root.unloadDelay) { - setTimeout(finishUnsubscribeQuery, this.model.root.unloadDelay); - } else { - finishUnsubscribeQuery(); - } - function finishUnsubscribeQuery() { - var count = --query.subscribeCount; - if (count) return cb(null, count); + if (query.shareQuery) { + query.shareQuery.destroy(); + query.shareQuery = null; + } - if (query.shareQuery) { - query.shareQuery.destroy(); - query.shareQuery = null; + unsubscribeQueryCallback(); + } + function unsubscribeQueryCallback(err?: Error) { + if (err) return cb(err); + // Cleanup when no fetches or subscribes remain + if (!query.fetchCount) query.destroy(); + cb(null, 0); } + return this; + }; - unsubscribeQueryCallback(); - } - function unsubscribeQueryCallback(err) { - if (err) return cb(err); - // Cleanup when no fetches or subscribes remain - if (!query.fetchCount) query.destroy(); - cb(null, 0); - } - return this; -}; + unsubscribePromised = promisify(Query.prototype.unsubscribe); -Query.prototype.unsubscribePromised = promisify(Query.prototype.unsubscribe); + _getShareResults() { + var ids = this.model._get(this.idsSegments); + if (!ids) return; -Query.prototype._getShareResults = function() { - var ids = this.model._get(this.idsSegments); - if (!ids) return; + var collection = this.model.getCollection(this.collectionName); + if (!collection) return; - var collection = this.model.getCollection(this.collectionName); - if (!collection) return; + var results = []; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var doc = collection.docs[id]; + results.push(doc && doc.shareDoc); + } + return results; + }; - var results = []; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var doc = collection.docs[id]; - results.push(doc && doc.shareDoc); - } - return results; -}; + get() { + var results = []; + var data = this.model._get(this.segments); + if (!data) { + console.warn('You must fetch or subscribe to a query before getting its results.'); + return results; + } + var ids = data.ids; + if (!ids) return results; -Query.prototype.get = function() { - var results = []; - var data = this.model._get(this.segments); - if (!data) { - console.warn('You must fetch or subscribe to a query before getting its results.'); + var collection = this.model.getCollection(this.collectionName); + for (var i = 0, l = ids.length; i < l; i++) { + var id = ids[i]; + var doc = collection && collection.docs[id]; + results.push(doc && doc.get()); + } return results; - } - var ids = data.ids; - if (!ids) return results; - - var collection = this.model.getCollection(this.collectionName); - for (var i = 0, l = ids.length; i < l; i++) { - var id = ids[i]; - var doc = collection && collection.docs[id]; - results.push(doc && doc.get()); - } - return results; -}; + }; -Query.prototype.getIds = function() { - return this.model._get(this.idsSegments) || []; -}; + getIds() { + return this.model._get(this.idsSegments) || []; + }; -Query.prototype.getExtra = function() { - return this.model._get(this.extraSegments); -}; + getExtra() { + return this.model._get(this.extraSegments); + }; -Query.prototype.ref = function(from) { - var idsPath = this.idsSegments.join('.'); - return this.model.refList(from, this.collectionName, idsPath); -}; + ref(from) { + var idsPath = this.idsSegments.join('.'); + return this.model.refList(from, this.collectionName, idsPath); + }; -Query.prototype.refIds = function(from) { - var idsPath = this.idsSegments.join('.'); - return this.model.ref(from, idsPath); -}; + refIds(from) { + var idsPath = this.idsSegments.join('.'); + return this.model.ref(from, idsPath); + }; -Query.prototype.refExtra = function(from, relPath) { - var extraPath = this.extraSegments.join('.'); - if (relPath) extraPath += '.' + relPath; - return this.model.ref(from, extraPath); -}; + refExtra(from, relPath) { + var extraPath = this.extraSegments.join('.'); + if (relPath) extraPath += '.' + relPath; + return this.model.ref(from, extraPath); + }; -Query.prototype.serialize = function() { - var ids = this.getIds(); - var collection = this.model.getCollection(this.collectionName); - var results; - if (collection) { - results = []; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var doc = collection.docs[id]; - if (doc) { - delete collection.docs[id]; - var data = doc.shareDoc.data; - var result = [data, doc.shareDoc.version]; - if (!data || data.id !== id) { - result[2] = id; - } - if (doc.shareDoc.type !== defaultType) { - result[3] = doc.shareDoc.type && doc.shareDoc.type.name; + serialize() { + var ids = this.getIds(); + var collection = this.model.getCollection(this.collectionName); + var results; + if (collection) { + results = []; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var doc = collection.docs[id]; + if (doc) { + delete collection.docs[id]; + var data = doc.shareDoc.data; + var result = [data, doc.shareDoc.version]; + if (!data || data.id !== id) { + result[2] = id; + } + if (doc.shareDoc.type !== defaultType) { + result[3] = doc.shareDoc.type && doc.shareDoc.type.name; + } + results.push(result); + } else { + results.push(id); } - results.push(result); - } else { - results.push(id); } } - } - var subscribed = this.context.subscribedQueries[this.hash] || 0; - var fetched = this.context.fetchedQueries[this.hash] || 0; - var contextId = this.context.id; - var counts = (contextId === 'root') ? - (fetched === 0) ? [subscribed] : [subscribed, fetched] : - [subscribed, fetched, contextId]; - // TODO: change counts to a less obtuse format. We don't want to change - // the serialization format unless it is known that clients are not - // depending on the old format, so this should be done in a major version - var countsList = [counts]; - var serialized = [ - countsList, - this.collectionName, - this.expression, - results, - this.options, - this.getExtra() - ]; - while (serialized[serialized.length - 1] == null) { - serialized.pop(); - } - return serialized; -}; - + var subscribed = this.context.subscribedQueries[this.hash] || 0; + var fetched = this.context.fetchedQueries[this.hash] || 0; + var contextId = this.context.id; + var counts = (contextId === 'root') ? + (fetched === 0) ? [subscribed] : [subscribed, fetched] : + [subscribed, fetched, contextId]; + // TODO: change counts to a less obtuse format. We don't want to change + // the serialization format unless it is known that clients are not + // depending on the old format, so this should be done in a major version + var countsList = [counts]; + var serialized = [ + countsList, + this.collectionName, + this.expression, + results, + this.options, + this.getExtra() + ]; + while (serialized[serialized.length - 1] == null) { + serialized.pop(); + } + return serialized; + }; +} function queryHash(contextId, collectionName, expression, options) { var args = [contextId, collectionName, expression, options]; return JSON.stringify(args).replace(/\./g, '|'); diff --git a/src/Model/RemoteDoc.ts b/src/Model/RemoteDoc.ts index 89c644d3c..a6bf8aa26 100644 --- a/src/Model/RemoteDoc.ts +++ b/src/Model/RemoteDoc.ts @@ -6,7 +6,9 @@ * 2. It maps incoming ShareJS operations to Racer events. */ -var Doc = require('./Doc'); +import { Doc } from './Doc'; +import { type Collection } from './collections'; +import { type Model } from './Model'; var util = require('../util'); var mutationEvents = require('./events').mutationEvents; var ChangeEvent = mutationEvents.ChangeEvent; @@ -15,565 +17,638 @@ var InsertEvent = mutationEvents.InsertEvent; var RemoveEvent = mutationEvents.RemoveEvent; var MoveEvent = mutationEvents.MoveEvent; -module.exports = RemoteDoc; - -function RemoteDoc(model, collectionName, id, snapshot, collection) { - // This is a bit messy, but we have to immediately register this doc on the - // collection that added it, so that when we create the shareDoc and the - // connection emits the 'doc' event, we'll find this doc instead of - // creating a new one - if (collection) collection.docs[id] = this; - - Doc.call(this, model, collectionName, id); - this.model = model.pass({$remote: true}); - this.debugMutations = model.root.debug.remoteMutations; - - // Get or create the Share document. Note that we must have already added - // this doc to the collection to avoid creating a duplicate doc - this.shareDoc = model.root.connection.get(collectionName, id); - this.shareDoc.ingestSnapshot(snapshot); - this._initShareDoc(); -} +export class RemoteDoc extends Doc { + debugMutations: boolean; + shareDoc: any; + + constructor(model: Model, collectionName: string, id: string, snapshot: any, collection: Collection) { + super(model, collectionName, id, collection); + // This is a bit messy, but we have to immediately register this doc on the + // collection that added it, so that when we create the shareDoc and the + // connection emits the 'doc' event, we'll find this doc instead of + // creating a new one + if (collection) collection.docs[id] = this; + + Doc.call(this, model, collectionName, id); + this.model = model.pass({ $remote: true }); + this.debugMutations = model.root.debug.remoteMutations; + + // Get or create the Share document. Note that we must have already added + // this doc to the collection to avoid creating a duplicate doc + this.shareDoc = model.root.connection.get(collectionName, id); + this.shareDoc.ingestSnapshot(snapshot); + this._initShareDoc(); + } + + _initShareDoc() { + var doc = this; + var model = this.model; + var collectionName = this.collectionName; + var id = this.id; + var shareDoc = this.shareDoc; + // Override submitOp to disable all writes and perform a dry-run + if (model.root.debug.disableSubmit) { + shareDoc.submitOp = function () { }; + shareDoc.create = function () { }; + shareDoc.del = function () { }; + } + // Subscribe to doc events + shareDoc.on('op', function (op, isLocal) { + // Don't emit on local operations, since they are emitted in the mutator + if (isLocal) return; + doc._updateCollectionData(); + doc._onOp(op); + }); + shareDoc.on('del', function (previous, isLocal) { + // Calling the shareDoc.del method does not emit an operation event, + // so we create the appropriate event here. + if (isLocal) return; + delete doc.collectionData[id]; + var event = new ChangeEvent(undefined, previous, model._pass); + model._emitMutation([collectionName, id], event); + }); + shareDoc.on('create', function (isLocal) { + // Local creates should not emit an event, since they only happen + // implicitly as a result of another mutation, and that operation will + // emit the appropriate event. Remote creates can set the snapshot data + // without emitting an operation event, so an event needs to be emitted + // for them. + if (isLocal) return; + doc._updateCollectionData(); + var value = shareDoc.data; + var event = new ChangeEvent(value, undefined, model._pass); + model._emitMutation([collectionName, id], event); + }); + shareDoc.on('error', function (err) { + model._emitError(err, collectionName + '.' + id); + }); + shareDoc.on('load', function () { + doc._updateCollectionData(); + var value = shareDoc.data; + // If we subscribe to an uncreated document, no need to emit 'load' event + if (value === undefined) return; + var event = new LoadEvent(value, model._pass); + model._emitMutation([collectionName, id], event); + }); + this._updateCollectionData(); + }; -RemoteDoc.prototype = new Doc(); - -RemoteDoc.prototype._initShareDoc = function() { - var doc = this; - var model = this.model; - var collectionName = this.collectionName; - var id = this.id; - var shareDoc = this.shareDoc; - // Override submitOp to disable all writes and perform a dry-run - if (model.root.debug.disableSubmit) { - shareDoc.submitOp = function() {}; - shareDoc.create = function() {}; - shareDoc.del = function() {}; - } - // Subscribe to doc events - shareDoc.on('op', function(op, isLocal) { - // Don't emit on local operations, since they are emitted in the mutator - if (isLocal) return; - doc._updateCollectionData(); - doc._onOp(op); - }); - shareDoc.on('del', function(previous, isLocal) { - // Calling the shareDoc.del method does not emit an operation event, - // so we create the appropriate event here. - if (isLocal) return; - delete doc.collectionData[id]; - var event = new ChangeEvent(undefined, previous, model._pass); - model._emitMutation([collectionName, id], event); - }); - shareDoc.on('create', function(isLocal) { - // Local creates should not emit an event, since they only happen - // implicitly as a result of another mutation, and that operation will - // emit the appropriate event. Remote creates can set the snapshot data - // without emitting an operation event, so an event needs to be emitted - // for them. - if (isLocal) return; - doc._updateCollectionData(); - var value = shareDoc.data; - var event = new ChangeEvent(value, undefined, model._pass); - model._emitMutation([collectionName, id], event); - }); - shareDoc.on('error', function(err) { - model._emitError(err, collectionName + '.' + id); - }); - shareDoc.on('load', function() { - doc._updateCollectionData(); - var value = shareDoc.data; - // If we subscribe to an uncreated document, no need to emit 'load' event - if (value === undefined) return; - var event = new LoadEvent(value, model._pass); - model._emitMutation([collectionName, id], event); - }); - this._updateCollectionData(); -}; - -RemoteDoc.prototype._updateCollectionData = function() { - var data = this.shareDoc.data; - if (typeof data === 'object' && !Array.isArray(data) && data !== null) { - data.id = this.id; - } - this.collectionData[this.id] = data; -}; + _updateCollectionData() { + var data = this.shareDoc.data; + if (typeof data === 'object' && !Array.isArray(data) && data !== null) { + data.id = this.id; + } + this.collectionData[this.id] = data; + }; -RemoteDoc.prototype.create = function(value, cb) { - if (this.debugMutations) { - console.log('RemoteDoc create', this.path(), value); - } - // We copy the snapshot data at time of create to prevent the id added - // outside of ShareJS from getting stored in the data - var data = util.deepCopy(value); - if (data) delete data.id; - this.shareDoc.create(data, cb); - // The id value will get added to the data that was passed in - this.shareDoc.data = value; - this._updateCollectionData(); - this.model._context.createDoc(this.collectionName, this.id); - return; -}; - -RemoteDoc.prototype.set = function(segments, value, cb) { - if (this.debugMutations) { - console.log('RemoteDoc set', this.path(segments), value); - } - var previous = this._createImplied(segments); - var lastSegment = segments[segments.length - 1]; - if (previous instanceof ImpliedOp) { - previous.value[lastSegment] = value; - this.shareDoc.submitOp(previous.op, cb); + create(value, cb) { + if (this.debugMutations) { + console.log('RemoteDoc create', this.path(), value); + } + // We copy the snapshot data at time of create to prevent the id added + // outside of ShareJS from getting stored in the data + var data = util.deepCopy(value); + if (data) delete data.id; + this.shareDoc.create(data, cb); + // The id value will get added to the data that was passed in + this.shareDoc.data = value; this._updateCollectionData(); + this.model._context.createDoc(this.collectionName, this.id); return; - } - var op = (util.isArrayIndex(lastSegment)) ? - [new ListReplaceOp(segments.slice(0, -1), lastSegment, previous, value)] : - [new ObjectReplaceOp(segments, previous, value)]; - this.shareDoc.submitOp(op, cb); - this._updateCollectionData(); - return previous; -}; - -RemoteDoc.prototype.del = function(segments, cb) { - if (this.debugMutations) { - console.log('RemoteDoc del', this.path(segments)); - } - if (segments.length === 0) { - var previous = this.get(); - this.shareDoc.del(cb); - delete this.collectionData[this.id]; - return previous; - } - // Don't do anything if the value is already undefined, since - // the del method should not create anything - var previous = this.get(segments); - if (previous === undefined) { - cb(); - return; - } - var op = [new ObjectDeleteOp(segments, previous)]; - this.shareDoc.submitOp(op, cb); - this._updateCollectionData(); - return previous; -}; - -RemoteDoc.prototype.increment = function(segments, byNumber, cb) { - if (this.debugMutations) { - console.log('RemoteDoc increment', this.path(segments), byNumber); - } - var previous = this._createImplied(segments); - if (previous instanceof ImpliedOp) { - var lastSegment = segments[segments.length - 1]; - previous.value[lastSegment] = byNumber; - this.shareDoc.submitOp(previous.op, cb); - this._updateCollectionData(); - return byNumber; - } - if (previous == null) { + }; + + set(segments, value, cb) { + if (this.debugMutations) { + console.log('RemoteDoc set', this.path(segments), value); + } + var previous = this._createImplied(segments); var lastSegment = segments[segments.length - 1]; + if (previous instanceof ImpliedOp) { + previous.value[lastSegment] = value; + this.shareDoc.submitOp(previous.op, cb); + this._updateCollectionData(); + return; + } var op = (util.isArrayIndex(lastSegment)) ? - [new ListInsertOp(segments.slice(0, -1), lastSegment, byNumber)] : - [new ObjectInsertOp(segments, byNumber)]; + [new ListReplaceOp(segments.slice(0, -1), lastSegment, previous, value)] : + [new ObjectReplaceOp(segments, previous, value)]; this.shareDoc.submitOp(op, cb); this._updateCollectionData(); - return byNumber; - } - var op = [new IncrementOp(segments, byNumber)]; - this.shareDoc.submitOp(op, cb); - this._updateCollectionData(); - return previous + byNumber; -}; - -RemoteDoc.prototype.push = function(segments, value, cb) { - if (this.debugMutations) { - console.log('RemoteDoc push', this.path(segments), value); - } - var shareDoc = this.shareDoc; - function push(arr, fnCb) { - var op = [new ListInsertOp(segments, arr.length, value)]; - shareDoc.submitOp(op, fnCb); - return arr.length; - } - return this._arrayApply(segments, push, cb); -}; + return previous; + }; -RemoteDoc.prototype.unshift = function(segments, value, cb) { - if (this.debugMutations) { - console.log('RemoteDoc unshift', this.path(segments), value); - } - var shareDoc = this.shareDoc; - function unshift(arr, fnCb) { - var op = [new ListInsertOp(segments, 0, value)]; - shareDoc.submitOp(op, fnCb); - return arr.length; - } - return this._arrayApply(segments, unshift, cb); -}; + del(segments, cb) { + if (this.debugMutations) { + console.log('RemoteDoc del', this.path(segments)); + } + if (segments.length === 0) { + var previous = this.get(); + this.shareDoc.del(cb); + delete this.collectionData[this.id]; + return previous; + } + // Don't do anything if the value is already undefined, since + // the del method should not create anything + var previous = this.get(segments); + if (previous === undefined) { + cb(); + return; + } + var op = [new ObjectDeleteOp(segments, previous)]; + this.shareDoc.submitOp(op, cb); + this._updateCollectionData(); + return previous; + }; -RemoteDoc.prototype.insert = function(segments, index, values, cb) { - if (this.debugMutations) { - console.log('RemoteDoc insert', this.path(segments), index, values); - } - var shareDoc = this.shareDoc; - function insert(arr, fnCb) { - var op = createInsertOp(segments, index, values); - shareDoc.submitOp(op, fnCb); - return arr.length; - } - return this._arrayApply(segments, insert, cb); -}; + increment(segments, byNumber, cb) { + if (this.debugMutations) { + console.log('RemoteDoc increment', this.path(segments), byNumber); + } + var previous = this._createImplied(segments); + if (previous instanceof ImpliedOp) { + var lastSegment = segments[segments.length - 1]; + previous.value[lastSegment] = byNumber; + this.shareDoc.submitOp(previous.op, cb); + this._updateCollectionData(); + return byNumber; + } + if (previous == null) { + var lastSegment = segments[segments.length - 1]; + const op = (util.isArrayIndex(lastSegment)) ? + [new ListInsertOp(segments.slice(0, -1), lastSegment, byNumber)] : + [new ObjectInsertOp(segments, byNumber)]; + this.shareDoc.submitOp(op, cb); + this._updateCollectionData(); + return byNumber; + } + const op = [new IncrementOp(segments, byNumber)]; + this.shareDoc.submitOp(op, cb); + this._updateCollectionData(); + return previous + byNumber; + }; -function createInsertOp(segments, index, values) { - if (!Array.isArray(values)) { - return [new ListInsertOp(segments, index, values)]; - } - var op = []; - for (var i = 0, len = values.length; i < len; i++) { - op.push(new ListInsertOp(segments, index++, values[i])); - } - return op; -} + push(segments, value, cb) { + if (this.debugMutations) { + console.log('RemoteDoc push', this.path(segments), value); + } + var shareDoc = this.shareDoc; + function push(arr, fnCb) { + var op = [new ListInsertOp(segments, arr.length, value)]; + shareDoc.submitOp(op, fnCb); + return arr.length; + } + return this._arrayApply(segments, push, cb); + }; -RemoteDoc.prototype.pop = function(segments, cb) { - if (this.debugMutations) { - console.log('RemoteDoc pop', this.path(segments)); - } - var shareDoc = this.shareDoc; - function pop(arr, fnCb) { - var index = arr.length - 1; - var value = arr[index]; - var op = [new ListRemoveOp(segments, index, value)]; - shareDoc.submitOp(op, fnCb); - return value; - } - return this._arrayApply(segments, pop, cb); -}; + unshift(segments, value, cb) { + if (this.debugMutations) { + console.log('RemoteDoc unshift', this.path(segments), value); + } + var shareDoc = this.shareDoc; + function unshift(arr, fnCb) { + var op = [new ListInsertOp(segments, 0, value)]; + shareDoc.submitOp(op, fnCb); + return arr.length; + } + return this._arrayApply(segments, unshift, cb); + }; -RemoteDoc.prototype.shift = function(segments, cb) { - if (this.debugMutations) { - console.log('RemoteDoc shift', this.path(segments)); - } - var shareDoc = this.shareDoc; - function shift(arr, fnCb) { - var value = arr[0]; - var op = [new ListRemoveOp(segments, 0, value)]; - shareDoc.submitOp(op, fnCb); - return value; - } - return this._arrayApply(segments, shift, cb); -}; + insert(segments, index, values, cb) { + if (this.debugMutations) { + console.log('RemoteDoc insert', this.path(segments), index, values); + } + var shareDoc = this.shareDoc; + function insert(arr, fnCb) { + var op = createInsertOp(segments, index, values); + shareDoc.submitOp(op, fnCb); + return arr.length; + } + return this._arrayApply(segments, insert, cb); + }; -RemoteDoc.prototype.remove = function(segments, index, howMany, cb) { - if (this.debugMutations) { - console.log('RemoteDoc remove', this.path(segments), index, howMany); - } - var shareDoc = this.shareDoc; - function remove(arr, fnCb) { - var values = arr.slice(index, index + howMany); - var op = []; - for (var i = 0, len = values.length; i < len; i++) { - op.push(new ListRemoveOp(segments, index, values[i])); - } - shareDoc.submitOp(op, fnCb); - return values; - } - return this._arrayApply(segments, remove, cb); -}; + pop(segments, cb) { + if (this.debugMutations) { + console.log('RemoteDoc pop', this.path(segments)); + } + var shareDoc = this.shareDoc; + function pop(arr, fnCb) { + var index = arr.length - 1; + var value = arr[index]; + var op = [new ListRemoveOp(segments, index, value)]; + shareDoc.submitOp(op, fnCb); + return value; + } + return this._arrayApply(segments, pop, cb); + }; -RemoteDoc.prototype.move = function(segments, from, to, howMany, cb) { - if (this.debugMutations) { - console.log('RemoteDoc move', this.path(segments), from, to, howMany); - } - var shareDoc = this.shareDoc; - function move(arr, fnCb) { - // Get the return value - var values = arr.slice(from, from + howMany); + shift(segments, cb) { + if (this.debugMutations) { + console.log('RemoteDoc shift', this.path(segments)); + } + var shareDoc = this.shareDoc; + function shift(arr, fnCb) { + var value = arr[0]; + var op = [new ListRemoveOp(segments, 0, value)]; + shareDoc.submitOp(op, fnCb); + return value; + } + return this._arrayApply(segments, shift, cb); + }; - // Build an op that moves each item individually - var op = []; - for (var i = 0; i < howMany; i++) { - op.push(new ListMoveOp(segments, (from < to) ? from : from + howMany - 1, (from < to) ? to + howMany - 1 : to)); + remove(segments, index, howMany, cb) { + if (this.debugMutations) { + console.log('RemoteDoc remove', this.path(segments), index, howMany); + } + var shareDoc = this.shareDoc; + function remove(arr, fnCb) { + var values = arr.slice(index, index + howMany); + var op = []; + for (var i = 0, len = values.length; i < len; i++) { + op.push(new ListRemoveOp(segments, index, values[i])); + } + shareDoc.submitOp(op, fnCb); + return values; } - shareDoc.submitOp(op, fnCb); + return this._arrayApply(segments, remove, cb); + }; - return values; - } - return this._arrayApply(segments, move, cb); -}; + move(segments, from, to, howMany, cb) { + if (this.debugMutations) { + console.log('RemoteDoc move', this.path(segments), from, to, howMany); + } + var shareDoc = this.shareDoc; + function move(arr, fnCb) { + // Get the return value + var values = arr.slice(from, from + howMany); + + // Build an op that moves each item individually + var op = []; + for (var i = 0; i < howMany; i++) { + op.push(new ListMoveOp(segments, (from < to) ? from : from + howMany - 1, (from < to) ? to + howMany - 1 : to)); + } + shareDoc.submitOp(op, fnCb); -RemoteDoc.prototype.stringInsert = function(segments, index, value, cb) { - if (this.debugMutations) { - console.log('RemoteDoc stringInsert', this.path(segments), index, value); - } - var previous = this._createImplied(segments); - if (previous instanceof ImpliedOp) { - var lastSegment = segments[segments.length - 1]; - previous.value[lastSegment] = value; - this.shareDoc.submitOp(previous.op, cb); + return values; + } + return this._arrayApply(segments, move, cb); + }; + + stringInsert(segments, index, value, cb) { + if (this.debugMutations) { + console.log('RemoteDoc stringInsert', this.path(segments), index, value); + } + var previous = this._createImplied(segments); + if (previous instanceof ImpliedOp) { + var lastSegment = segments[segments.length - 1]; + previous.value[lastSegment] = value; + this.shareDoc.submitOp(previous.op, cb); + this._updateCollectionData(); + return; + } + if (previous == null) { + var lastSegment = segments[segments.length - 1]; + const op = (util.isArrayIndex(lastSegment)) ? + [new ListInsertOp(segments.slice(0, -1), lastSegment, value)] : + [new ObjectInsertOp(segments, value)]; + this.shareDoc.submitOp(op, cb); + this._updateCollectionData(); + return previous; + } + const op = [new StringInsertOp(segments, index, value)]; + this.shareDoc.submitOp(op, cb); this._updateCollectionData(); - return; - } - if (previous == null) { - var lastSegment = segments[segments.length - 1]; - var op = (util.isArrayIndex(lastSegment)) ? - [new ListInsertOp(segments.slice(0, -1), lastSegment, value)] : - [new ObjectInsertOp(segments, value)]; + return previous; + }; + + stringRemove(segments, index, howMany, cb) { + if (this.debugMutations) { + console.log('RemoteDoc stringRemove', this.path(segments), index, howMany); + } + var previous = this._createImplied(segments); + if (previous instanceof ImpliedOp) return; + if (previous == null) return previous; + var removed = previous.slice(index, index + howMany); + var op = [new StringRemoveOp(segments, index, removed)]; this.shareDoc.submitOp(op, cb); this._updateCollectionData(); return previous; - } - var op = [new StringInsertOp(segments, index, value)]; - this.shareDoc.submitOp(op, cb); - this._updateCollectionData(); - return previous; -}; - -RemoteDoc.prototype.stringRemove = function(segments, index, howMany, cb) { - if (this.debugMutations) { - console.log('RemoteDoc stringRemove', this.path(segments), index, howMany); - } - var previous = this._createImplied(segments); - if (previous instanceof ImpliedOp) return; - if (previous == null) return previous; - var removed = previous.slice(index, index + howMany); - var op = [new StringRemoveOp(segments, index, removed)]; - this.shareDoc.submitOp(op, cb); - this._updateCollectionData(); - return previous; -}; - -RemoteDoc.prototype.subtypeSubmit = function(segments, subtype, subtypeOp, cb) { - if (this.debugMutations) { - console.log('RemoteDoc subtypeSubmit', this.path(segments), subtype, subtypeOp); - } - var previous = this._createImplied(segments); - if (previous instanceof ImpliedOp) { - this.shareDoc.submitOp(previous.op); - previous = undefined; - } - var op = new SubtypeOp(segments, subtype, subtypeOp); - this.shareDoc.submitOp(op, cb); - this._updateCollectionData(); - return previous; -}; - -RemoteDoc.prototype.get = function(segments) { - return util.lookup(segments, this.shareDoc.data); -}; - -RemoteDoc.prototype._createImplied = function(segments) { - if (!this.shareDoc.type) { - throw new Error('Mutation on uncreated remote document'); - } - var parent = this.shareDoc; - var key = 'data'; - var node = parent[key]; - var i = 0; - var nextKey = segments[i++]; - var op, value; - while (nextKey != null) { - if (!node) { - if (op) { - value = value[key] = util.isArrayIndex(nextKey) ? [] : {}; - } else { - value = util.isArrayIndex(nextKey) ? [] : {}; - if (Array.isArray(parent)) { - if (key >= parent.length) { - op = new ListInsertOp(segments.slice(0, i - 2), key, value); + }; + + subtypeSubmit(segments, subtype, subtypeOp, cb) { + if (this.debugMutations) { + console.log('RemoteDoc subtypeSubmit', this.path(segments), subtype, subtypeOp); + } + var previous = this._createImplied(segments); + if (previous instanceof ImpliedOp) { + this.shareDoc.submitOp(previous.op); + previous = undefined; + } + var op = new SubtypeOp(segments, subtype, subtypeOp); + this.shareDoc.submitOp(op, cb); + this._updateCollectionData(); + return previous; + }; + + get(segments?: string[]) { + return util.lookup(segments, this.shareDoc.data); + }; + + _createImplied(segments): any { + if (!this.shareDoc.type) { + throw new Error('Mutation on uncreated remote document'); + } + var parent = this.shareDoc; + var key = 'data'; + var node = parent[key]; + var i = 0; + var nextKey = segments[i++]; + var op, value; + while (nextKey != null) { + if (!node) { + if (op) { + value = value[key] = util.isArrayIndex(nextKey) ? [] : {}; + } else { + value = util.isArrayIndex(nextKey) ? [] : {}; + if (Array.isArray(parent)) { + // @ts-ignore + if (key >= parent.length) { + op = new ListInsertOp(segments.slice(0, i - 2), key, value); + } else { + op = new ListReplaceOp(segments.slice(0, i - 2), key, node, value); + } } else { - op = new ListReplaceOp(segments.slice(0, i - 2), key, node, value); + op = new ObjectInsertOp(segments.slice(0, i - 1), value); } - } else { - op = new ObjectInsertOp(segments.slice(0, i - 1), value); } + node = value; } - node = value; + parent = node; + key = nextKey; + node = parent[key]; + nextKey = segments[i++]; + } + if (op) return new ImpliedOp(op, value); + return node; + }; + + _arrayApply(segments, fn, cb) { + var arr = this._createImplied(segments); + if (arr instanceof ImpliedOp) { + this.shareDoc.submitOp(arr.op); + arr = this.get(segments); + } + if (arr == null) { + var lastSegment = segments[segments.length - 1]; + var op = (util.isArrayIndex(lastSegment)) ? + [new ListInsertOp(segments.slice(0, -1), lastSegment, [])] : + [new ObjectInsertOp(segments, [])]; + this.shareDoc.submitOp(op); + arr = this.get(segments); } - parent = node; - key = nextKey; - node = parent[key]; - nextKey = segments[i++]; - } - if (op) return new ImpliedOp(op, value); - return node; -}; -function ImpliedOp(op, value) { - this.op = op; - this.value = value; + if (!Array.isArray(arr)) { + var message = this._errorMessage(fn.name + ' on non-array', segments, arr); + var err = new TypeError(message); + return cb(err); + } + var out = fn(arr, cb); + this._updateCollectionData(); + return out; + }; + + _onOp(op) { + var item; + if (op.length === 1) { + // ShareDB docs shatter json0 ops into single components during apply + item = op[0]; + } else if (op.length === 0) { + // Ignore no-ops + return; + } else { + try { + op = JSON.stringify(op); + } catch (err) { } + throw new Error('Received op with multiple components from ShareDB ' + op); + } + var segments = [this.collectionName, this.id].concat(item.p); + var model = this.model; + + // ObjectReplaceOp, ObjectInsertOp, or ObjectDeleteOp + if (defined(item.oi) || defined(item.od)) { + var value = item.oi; + var previous = item.od; + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); + + // ListReplaceOp + } else if (defined(item.li) && defined(item.ld)) { + var value = item.li; + var previous = item.ld; + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); + + // ListInsertOp + } else if (defined(item.li)) { + var index = segments[segments.length - 1]; + var values = [item.li]; + var event = new InsertEvent(index, values, model._pass); + model._emitMutation(segments.slice(0, -1), event); + + // ListRemoveOp + } else if (defined(item.ld)) { + var index = segments[segments.length - 1]; + var removed = [item.ld]; + var event = new RemoveEvent(index, removed, model._pass); + model._emitMutation(segments.slice(0, -1), event); + + // ListMoveOp + } else if (defined(item.lm)) { + var from = segments[segments.length - 1]; + var to = item.lm; + var howMany = 1; + var event = new MoveEvent(from, to, howMany, model._pass); + model._emitMutation(segments.slice(0, -1), event); + + // StringInsertOp + } else if (defined(item.si)) { + var index = segments[segments.length - 1]; + var text = item.si; + segments = segments.slice(0, -1); + var value = model._get(segments); + var previous = value.slice(0, index) + value.slice(index + text.length); + var pass = model.pass({ $stringInsert: { index: index, text: text } })._pass; + var event = new ChangeEvent(value, previous, pass); + model._emitMutation(segments, event); + + // StringRemoveOp + } else if (defined(item.sd)) { + var index = segments[segments.length - 1]; + var text = item.sd; + const howMany = text.length; + segments = segments.slice(0, -1); + var value = model._get(segments); + var previous = value.slice(0, index) + text + value.slice(index); + var pass = model.pass({ $stringRemove: { index: index, howMany: howMany } })._pass; + var event = new ChangeEvent(value, previous, pass); + model._emitMutation(segments, event); + + // IncrementOp + } else if (defined(item.na)) { + var value = this.get(item.p); + const previous = value - item.na; + var event = new ChangeEvent(value, previous, model._pass); + model._emitMutation(segments, event); + + // SubtypeOp + } else if (defined(item.t)) { + var value = this.get(item.p); + // Since this is generic to all subtypes, we don't know how to get a copy + // of the previous value efficiently. We could make a copy eagerly, but + // given that embedded types are likely to be used for custom editors, + // we'll assume they primarily use the returned op and are unlikely to + // need the previous snapshot data + var previous = undefined; + var type = item.t; + var op = item.o; + var pass = model.pass({ $subtype: { type: type, op: op } })._pass; + var event = new ChangeEvent(value, previous, pass); + model._emitMutation(segments, event); + } + }; } -RemoteDoc.prototype._arrayApply = function(segments, fn, cb) { - var arr = this._createImplied(segments); - if (arr instanceof ImpliedOp) { - this.shareDoc.submitOp(arr.op); - arr = this.get(segments); +function createInsertOp(segments, index, values) { + if (!Array.isArray(values)) { + return [new ListInsertOp(segments, index, values)]; } - if (arr == null) { - var lastSegment = segments[segments.length - 1]; - var op = (util.isArrayIndex(lastSegment)) ? - [new ListInsertOp(segments.slice(0, -1), lastSegment, [])] : - [new ObjectInsertOp(segments, [])]; - this.shareDoc.submitOp(op); - arr = this.get(segments); + var op = []; + for (var i = 0, len = values.length; i < len; i++) { + op.push(new ListInsertOp(segments, index++, values[i])); } + return op; +} - if (!Array.isArray(arr)) { - var message = this._errorMessage(fn.name + ' on non-array', segments, arr); - var err = new TypeError(message); - return cb(err); - } - var out = fn(arr, cb); - this._updateCollectionData(); - return out; -}; - -RemoteDoc.prototype._onOp = function(op) { - var item; - if (op.length === 1) { - // ShareDB docs shatter json0 ops into single components during apply - item = op[0]; - } else if (op.length === 0) { - // Ignore no-ops - return; - } else { - try { - op = JSON.stringify(op); - } catch (err) {} - throw new Error('Received op with multiple components from ShareDB ' + op); - } - var segments = [this.collectionName, this.id].concat(item.p); - var model = this.model; - - // ObjectReplaceOp, ObjectInsertOp, or ObjectDeleteOp - if (defined(item.oi) || defined(item.od)) { - var value = item.oi; - var previous = item.od; - var event = new ChangeEvent(value, previous, model._pass); - model._emitMutation(segments, event); - - // ListReplaceOp - } else if (defined(item.li) && defined(item.ld)) { - var value = item.li; - var previous = item.ld; - var event = new ChangeEvent(value, previous, model._pass); - model._emitMutation(segments, event); - - // ListInsertOp - } else if (defined(item.li)) { - var index = segments[segments.length - 1]; - var values = [item.li]; - var event = new InsertEvent(index, values, model._pass); - model._emitMutation(segments.slice(0, -1), event); - - // ListRemoveOp - } else if (defined(item.ld)) { - var index = segments[segments.length - 1]; - var removed = [item.ld]; - var event = new RemoveEvent(index, removed, model._pass); - model._emitMutation(segments.slice(0, -1), event); - - // ListMoveOp - } else if (defined(item.lm)) { - var from = segments[segments.length - 1]; - var to = item.lm; - var howMany = 1; - var event = new MoveEvent(from, to, howMany, model._pass); - model._emitMutation(segments.slice(0, -1), event); - - // StringInsertOp - } else if (defined(item.si)) { - var index = segments[segments.length - 1]; - var text = item.si; - segments = segments.slice(0, -1); - var value = model._get(segments); - var previous = value.slice(0, index) + value.slice(index + text.length); - var pass = model.pass({$stringInsert: {index: index, text: text}})._pass; - var event = new ChangeEvent(value, previous, pass); - model._emitMutation(segments, event); - - // StringRemoveOp - } else if (defined(item.sd)) { - var index = segments[segments.length - 1]; - var text = item.sd; - var howMany = text.length; - segments = segments.slice(0, -1); - var value = model._get(segments); - var previous = value.slice(0, index) + text + value.slice(index); - var pass = model.pass({$stringRemove: {index: index, howMany: howMany}})._pass; - var event = new ChangeEvent(value, previous, pass); - model._emitMutation(segments, event); - - // IncrementOp - } else if (defined(item.na)) { - var value = this.get(item.p); - var previous = value - item.na; - var event = new ChangeEvent(value, previous, model._pass); - model._emitMutation(segments, event); - - // SubtypeOp - } else if (defined(item.t)) { - var value = this.get(item.p); - // Since this is generic to all subtypes, we don't know how to get a copy - // of the previous value efficiently. We could make a copy eagerly, but - // given that embedded types are likely to be used for custom editors, - // we'll assume they primarily use the returned op and are unlikely to - // need the previous snapshot data - var previous = undefined; - var type = item.t; - var op = item.o; - var pass = model.pass({$subtype: {type: type, op: op}})._pass; - var event = new ChangeEvent(value, previous, pass); - model._emitMutation(segments, event); +class ImpliedOp { + op: any; + value: any; + + constructor(op, value) { + this.op = op; + this.value = value; } -}; +} -function ObjectReplaceOp(segments, before, after) { - this.p = util.castSegments(segments); - this.od = before; - this.oi = (after === undefined) ? null : after; +class ObjectReplaceOp { + p: any; + od: any; + oi: any; + + constructor(segments, before, after) { + this.p = util.castSegments(segments); + this.od = before; + this.oi = (after === undefined) ? null : after; + } } -function ObjectInsertOp(segments, value) { - this.p = util.castSegments(segments); - this.oi = (value === undefined) ? null : value; + +class ObjectInsertOp { + p: any; + oi: any; + + constructor(segments, value) { + this.p = util.castSegments(segments); + this.oi = (value === undefined) ? null : value; + } } -function ObjectDeleteOp(segments, value) { - this.p = util.castSegments(segments); - this.od = (value === undefined) ? null : value; + +class ObjectDeleteOp { + p: any; + od: any; + + constructor(segments, value) { + this.p = util.castSegments(segments); + this.od = (value === undefined) ? null : value; + } } -function ListReplaceOp(segments, index, before, after) { - this.p = util.castSegments(segments.concat(index)); - this.ld = before; - this.li = (after === undefined) ? null : after; + +class ListReplaceOp { + p: any; + ld: any; + li: any; + + constructor(segments, index, before, after) { + this.p = util.castSegments(segments.concat(index)); + this.ld = before; + this.li = (after === undefined) ? null : after; + } } -function ListInsertOp(segments, index, value) { - this.p = util.castSegments(segments.concat(index)); - this.li = (value === undefined) ? null : value; + +class ListInsertOp { + p: any; + li: any; + + constructor(segments, index, value) { + this.p = util.castSegments(segments.concat(index)); + this.li = (value === undefined) ? null : value; + } } -function ListRemoveOp(segments, index, value) { - this.p = util.castSegments(segments.concat(index)); - this.ld = (value === undefined) ? null : value; + +class ListRemoveOp { + p: any; + ld: any; + + constructor(segments, index, value) { + this.p = util.castSegments(segments.concat(index)); + this.ld = (value === undefined) ? null : value; + } } -function ListMoveOp(segments, from, to) { - this.p = util.castSegments(segments.concat(from)); - this.lm = to; + +class ListMoveOp { + p: any; + lm: any; + + constructor(segments, from, to) { + this.p = util.castSegments(segments.concat(from)); + this.lm = to; + } } -function StringInsertOp(segments, index, value) { - this.p = util.castSegments(segments.concat(index)); - this.si = value; + +class StringInsertOp { + p: any; + si: any; + constructor(segments, index, value) { + this.p = util.castSegments(segments.concat(index)); + this.si = value; + } } -function StringRemoveOp(segments, index, value) { - this.p = util.castSegments(segments.concat(index)); - this.sd = value; + +class StringRemoveOp { + p: any; + sd: string; + constructor(segments, index: number, value: string) { + this.p = util.castSegments(segments.concat(index)); + this.sd = value; + } } -function IncrementOp(segments, byNumber) { - this.p = util.castSegments(segments); - this.na = byNumber; + +class IncrementOp { + p: any; + na: any; + constructor(segments, byNumber) { + this.p = util.castSegments(segments); + this.na = byNumber; + } } -function SubtypeOp(segments, subtype, subtypeOp) { - this.p = util.castSegments(segments); - this.t = subtype; - this.o = subtypeOp; + +class SubtypeOp { + p: any; + t: any; + o: any; + + constructor(segments, subtype, subtypeOp) { + this.p = util.castSegments(segments); + this.t = subtype; + this.o = subtypeOp; + } } function defined(value) { diff --git a/src/Model/bundle.ts b/src/Model/bundle.ts index 5dfd2a7e1..f45c95db4 100644 --- a/src/Model/bundle.ts +++ b/src/Model/bundle.ts @@ -1,11 +1,20 @@ -var Model = require('./Model'); +import { Model } from './Model'; var defaultType = require('sharedb/lib/client').types.defaultType; var promisify = require('../util').promisify; -Model.BUNDLE_TIMEOUT = 10 * 1000; +declare module './Model' { + interface Model { + bundleTimeout: number; + + bundle(cb: (err?: Error, bundle?: any) => void): void; + bundlePromised(): Promise; + } +} + +const BUNDLE_TIMEOUT = 10 * 1000; Model.INITS.push(function(model, options) { - model.root.bundleTimeout = options.bundleTimeout || Model.BUNDLE_TIMEOUT; + model.root.bundleTimeout = options.bundleTimeout || BUNDLE_TIMEOUT; }); Model.prototype.bundle = function(cb) { @@ -21,13 +30,14 @@ Model.prototype.bundle = function(cb) { root.whenNothingPending(function finishBundle() { clearTimeout(timeout); var bundle = { + collections: undefined, queries: root._queries.toJSON(), contexts: root._contexts.toJSON(), refs: root._refs.toJSON(), refLists: root._refLists.toJSON(), fns: root._fns.toJSON(), filters: root._filters.toJSON(), - nodeEnv: process.env.NODE_ENV + nodeEnv: process.env.NODE_ENV, }; stripComputed(root); bundle.collections = serializeCollections(root); diff --git a/src/Model/collections.ts b/src/Model/collections.ts index bf1cb2e59..42744ca91 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -1,11 +1,34 @@ -var Model = require('./Model'); +import { Doc } from './Doc'; +import { Model } from './Model'; var LocalDoc = require('./LocalDoc'); var util = require('../util'); -function ModelCollections() {} -function ModelData() {} -function DocMap() {} -function CollectionData() {} +export class ModelCollections { + docs: Record; +} +export class ModelData {} +export class DocMap {} +export class CollectionData {} + +declare module './Model' { + interface Model { + collections: ModelCollections; + data: ModelData; + + getCollection(collecitonName: string): ModelCollections; + getDoc(collecitonName: string, id: string): any | undefined; + get(subpath: string): any; + _get(segments: Segments): any; + getCopy(subpath: string): any; + _getCopy(segments: Segments): any; + getDeepCopy(subpath: string): any; + _getDeepCopy(segments: Segments): any; + getOrCreateCollection(name: string): Collection; + _getDocConstructor(): DocConstructor; + getOrCreateDoc(collectionName: string, id: string, data: any); + destroy(subpath: string): void; + } +} Model.INITS.push(function(model) { model.root.collections = new ModelCollections(); @@ -15,33 +38,41 @@ Model.INITS.push(function(model) { Model.prototype.getCollection = function(collectionName) { return this.root.collections[collectionName]; }; + Model.prototype.getDoc = function(collectionName, id) { var collection = this.root.collections[collectionName]; return collection && collection.docs[id]; }; + Model.prototype.get = function(subpath) { var segments = this._splitPath(subpath); return this._get(segments); }; + Model.prototype._get = function(segments) { return util.lookup(segments, this.root.data); }; + Model.prototype.getCopy = function(subpath) { var segments = this._splitPath(subpath); return this._getCopy(segments); }; + Model.prototype._getCopy = function(segments) { var value = this._get(segments); return util.copy(value); }; + Model.prototype.getDeepCopy = function(subpath) { var segments = this._splitPath(subpath); return this._getDeepCopy(segments); }; + Model.prototype._getDeepCopy = function(segments) { var value = this._get(segments); return util.deepCopy(value); }; + Model.prototype.getOrCreateCollection = function(name) { var collection = this.root.collections[name]; if (collection) return collection; @@ -50,6 +81,7 @@ Model.prototype.getOrCreateCollection = function(name) { this.root.collections[name] = collection; return collection; }; + Model.prototype._getDocConstructor = function() { // Only create local documents. This is overriden in ./connection.js, so that // the RemoteDoc behavior can be selectively included @@ -100,51 +132,62 @@ Model.prototype.destroy = function(subpath) { } }; -function Collection(model, name, Doc) { - this.model = model; - this.name = name; - this.Doc = Doc; - this.size = 0; - this.docs = new DocMap(); - this.data = model.data[name] = new CollectionData(); -} +export class Collection { + model: Model; + name: string; + size: number; + docs: DocMap; + data: CollectionData; + Doc: typeof Doc; -/** - * Adds a document with `id` and `data` to `this` Collection. - * @param {String} id - * @param {Object} data - * @return {LocalDoc|RemoteDoc} doc - */ -Collection.prototype.add = function(id, data) { - var doc = new this.Doc(this.model, this.name, id, data, this); - this.docs[id] = doc; - return doc; -}; -Collection.prototype.destroy = function() { - delete this.model.collections[this.name]; - delete this.model.data[this.name]; -}; -Collection.prototype.getOrCreateDoc = function(id, data) { - var doc = this.docs[id]; - if (doc) return doc; - this.size++; - return this.add(id, data); -}; + constructor(model: Model, name: string, docClass: typeof Doc) { + this.model = model; + this.name = name; + this.Doc = docClass; + this.size = 0; + this.docs = new DocMap(); + this.data = model.data[name] = new CollectionData(); + } -/** - * Removes the document with `id` from `this` Collection. If there are no more - * documents in the Collection after the given document is removed, then this - * destroys the Collection. - * - * @param {String} id - */ -Collection.prototype.remove = function(id) { - if (!this.docs[id]) return; - this.size--; - if (this.size > 0) { - delete this.docs[id]; - delete this.data[id]; - } else { - this.destroy(); + /** + * Adds a document with `id` and `data` to `this` Collection. + * @param {String} id + * @param {Object} data + * @return {LocalDoc|RemoteDoc} doc + */ + add(id, data) { + var doc = new this.Doc(this.model, this.name, id, data, this); + this.docs[id] = doc; + return doc; + }; + + destroy() { + delete this.model.collections[this.name]; + delete this.model.data[this.name]; + }; + + getOrCreateDoc(id, data) { + var doc = this.docs[id]; + if (doc) return doc; + this.size++; + return this.add(id, data); + }; + + /** + * Removes the document with `id` from `this` Collection. If there are no more + * documents in the Collection after the given document is removed, then this + * destroys the Collection. + * + * @param {String} id + */ + remove(id: string) { + if (!this.docs[id]) return; + this.size--; + if (this.size > 0) { + delete this.docs[id]; + delete this.data[id]; + } else { + this.destroy(); + } } }; diff --git a/src/Model/connection.server.ts b/src/Model/connection.server.ts index bb8ff6938..d9f3c50c4 100644 --- a/src/Model/connection.server.ts +++ b/src/Model/connection.server.ts @@ -1,4 +1,12 @@ -var Model = require('./Model'); +import { Model } from './Model'; + +declare module './Model' { + interface Model { + createConnection(backend: any, req: any): void; + connect(): void; + connection: any; + } +} Model.prototype.createConnection = function(backend, req) { this.root.backend = backend; diff --git a/src/Model/connection.ts b/src/Model/connection.ts index c0246b369..f11677c9b 100644 --- a/src/Model/connection.ts +++ b/src/Model/connection.ts @@ -1,9 +1,32 @@ var Connection = require('sharedb/lib/client').Connection; -var Model = require('./Model'); -var LocalDoc = require('./LocalDoc'); -var RemoteDoc = require('./RemoteDoc'); +import { Model } from './Model'; +import { type Doc} from './Doc'; +import { LocalDoc} from './LocalDoc'; +import {RemoteDoc} from './RemoteDoc'; var promisify = require('../util').promisify; +declare module './Model' { + interface DocConstructor { + new (model: Model, collectionName: string, id: string, data: any): DocConstructor; + } + interface Model { + _finishCreateConnection(): void; + _getDocConstructor(name: string): DocConstructor; + _isLocal(name: string): boolean; + allowCompose(): Model; + close(cb: (err?: Error) => void): void; + closePromised: Promise; + disconnect(): void; + getAgent(): any; + hasPending(): boolean; + hasWritePending(): boolean; + preventCompose(): Model; + reconnect(): void; + whenNothingPending(cb: () => void): void; + whenNothingPendingPromised(): Promise; + } +} + Model.INITS.push(function(model) { model.root._preventCompose = false; }); @@ -90,7 +113,7 @@ Model.prototype._isLocal = function(name) { return firstCharcter === '_' || firstCharcter === '$'; }; -Model.prototype._getDocConstructor = function(name) { +Model.prototype._getDocConstructor = function(name: string) { return (this._isLocal(name)) ? LocalDoc : RemoteDoc; }; diff --git a/src/Model/contexts.ts b/src/Model/contexts.ts index 98f2e5c3c..d9ef73b7d 100644 --- a/src/Model/contexts.ts +++ b/src/Model/contexts.ts @@ -2,8 +2,19 @@ * Contexts are useful for keeping track of the origin of subscribes. */ -var Model = require('./Model'); -var CollectionCounter = require('./CollectionCounter'); +import { Model } from './Model'; +import { CollectionCounter} from './CollectionCounter'; + +declare module './Model' { + interface Model { + _contexts: Contexts; + context(id: string): Model; + setContext(id: string): void; + getOrCreateContext(id: string): Context; + unload(id: string): void; + unloadAll(): void; + } +} Model.INITS.push(function(model) { model.root._contexts = new Contexts(); @@ -40,70 +51,118 @@ Model.prototype.unloadAll = function() { } }; -function Contexts() {} -Contexts.prototype.toJSON = function() { - var out = {}; - var contexts = this; - for (var key in contexts) { - if (contexts[key] instanceof Context) { - out[key] = contexts[key].toJSON(); +export class Contexts { + toJSON = function() { + var out = {}; + var contexts = this; + for (var key in contexts) { + if (contexts[key] instanceof Context) { + out[key] = contexts[key].toJSON(); + } } - } - return out; -}; + return out; + }; +} -function FetchedQueries() {} -function SubscribedQueries() {} +class FetchedQueries {} +class SubscribedQueries {} -function Context(model, id) { - this.model = model; - this.id = id; - this.fetchedDocs = new CollectionCounter(); - this.subscribedDocs = new CollectionCounter(); - this.createdDocs = new CollectionCounter(); - this.fetchedQueries = new FetchedQueries(); - this.subscribedQueries = new SubscribedQueries(); -} +export class Context{ + model: Model; + id: string; + fetchedDocs: CollectionCounter; + subscribedDocs: CollectionCounter; + createdDocs: CollectionCounter; + fetchedQueries: FetchedQueries; + subscribedQueries: SubscribedQueries; -Context.prototype.toJSON = function() { - var fetchedDocs = this.fetchedDocs.toJSON(); - var subscribedDocs = this.subscribedDocs.toJSON(); - var createdDocs = this.createdDocs.toJSON(); - if (!fetchedDocs && !subscribedDocs && !createdDocs) return; - return { - fetchedDocs: fetchedDocs, - subscribedDocs: subscribedDocs, - createdDocs: createdDocs - }; -}; + constructor(model: Model, id: string) { + this.model = model; + this.id = id; + this.fetchedDocs = new CollectionCounter(); + this.subscribedDocs = new CollectionCounter(); + this.createdDocs = new CollectionCounter(); + this.fetchedQueries = new FetchedQueries(); + this.subscribedQueries = new SubscribedQueries(); + } -Context.prototype.fetchDoc = function(collectionName, id) { - this.fetchedDocs.increment(collectionName, id); -}; -Context.prototype.subscribeDoc = function(collectionName, id) { - this.subscribedDocs.increment(collectionName, id); -}; -Context.prototype.unfetchDoc = function(collectionName, id) { - this.fetchedDocs.decrement(collectionName, id); -}; -Context.prototype.unsubscribeDoc = function(collectionName, id) { - this.subscribedDocs.decrement(collectionName, id); -}; -Context.prototype.createDoc = function(collectionName, id) { - this.createdDocs.increment(collectionName, id); -}; -Context.prototype.fetchQuery = function(query) { - mapIncrement(this.fetchedQueries, query.hash); -}; -Context.prototype.subscribeQuery = function(query) { - mapIncrement(this.subscribedQueries, query.hash); -}; -Context.prototype.unfetchQuery = function(query) { - mapDecrement(this.fetchedQueries, query.hash); -}; -Context.prototype.unsubscribeQuery = function(query) { - mapDecrement(this.subscribedQueries, query.hash); -}; + toJSON() { + var fetchedDocs = this.fetchedDocs.toJSON(); + var subscribedDocs = this.subscribedDocs.toJSON(); + var createdDocs = this.createdDocs.toJSON(); + if (!fetchedDocs && !subscribedDocs && !createdDocs) return; + return { + fetchedDocs: fetchedDocs, + subscribedDocs: subscribedDocs, + createdDocs: createdDocs + }; + }; + + fetchDoc(collectionName, id) { + this.fetchedDocs.increment(collectionName, id); + }; + subscribeDoc(collectionName, id) { + this.subscribedDocs.increment(collectionName, id); + }; + unfetchDoc(collectionName, id) { + this.fetchedDocs.decrement(collectionName, id); + }; + unsubscribeDoc(collectionName, id) { + this.subscribedDocs.decrement(collectionName, id); + }; + createDoc(collectionName, id) { + this.createdDocs.increment(collectionName, id); + }; + fetchQuery(query) { + mapIncrement(this.fetchedQueries, query.hash); + }; + subscribeQuery(query) { + mapIncrement(this.subscribedQueries, query.hash); + }; + unfetchQuery(query) { + mapDecrement(this.fetchedQueries, query.hash); + }; + unsubscribeQuery(query) { + mapDecrement(this.subscribedQueries, query.hash); + }; + + unload() { + var model = this.model; + for (var hash in this.fetchedQueries) { + var query = model.root._queries.map[hash]; + if (!query) continue; + var count = this.fetchedQueries[hash]; + while (count--) query.unfetch(); + } + for (var hash in this.subscribedQueries) { + var query = model.root._queries.map[hash]; + if (!query) continue; + var count = this.subscribedQueries[hash]; + while (count--) query.unsubscribe(); + } + for (var collectionName in this.fetchedDocs.collections) { + var collection = this.fetchedDocs.collections[collectionName]; + for (var id in collection.counts) { + var count = collection.counts[id]; + while (count--) model.unfetchDoc(collectionName, id); + } + } + for (var collectionName in this.subscribedDocs.collections) { + var collection = this.subscribedDocs.collections[collectionName]; + for (var id in collection.counts) { + var count = collection.counts[id]; + while (count--) model.unsubscribeDoc(collectionName, id); + } + } + for (var collectionName in this.createdDocs.collections) { + var collection = this.createdDocs.collections[collectionName]; + for (var id in collection.counts) { + model._maybeUnloadDoc(collectionName, id); + } + } + this.createdDocs.reset(); + }; +} function mapIncrement(map, key) { map[key] = (map[key] || 0) + 1; } @@ -111,40 +170,3 @@ function mapDecrement(map, key) { map[key] && map[key]--; if (!map[key]) delete map[key]; } - -Context.prototype.unload = function() { - var model = this.model; - for (var hash in this.fetchedQueries) { - var query = model.root._queries.map[hash]; - if (!query) continue; - var count = this.fetchedQueries[hash]; - while (count--) query.unfetch(); - } - for (var hash in this.subscribedQueries) { - var query = model.root._queries.map[hash]; - if (!query) continue; - var count = this.subscribedQueries[hash]; - while (count--) query.unsubscribe(); - } - for (var collectionName in this.fetchedDocs.collections) { - var collection = this.fetchedDocs.collections[collectionName]; - for (var id in collection.counts) { - var count = collection.counts[id]; - while (count--) model.unfetchDoc(collectionName, id); - } - } - for (var collectionName in this.subscribedDocs.collections) { - var collection = this.subscribedDocs.collections[collectionName]; - for (var id in collection.counts) { - var count = collection.counts[id]; - while (count--) model.unsubscribeDoc(collectionName, id); - } - } - for (var collectionName in this.createdDocs.collections) { - var collection = this.createdDocs.collections[collectionName]; - for (var id in collection.counts) { - model._maybeUnloadDoc(collectionName, id); - } - } - this.createdDocs.reset(); -}; diff --git a/src/Model/events.ts b/src/Model/events.ts index ac31fb090..c87bc773e 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -1,26 +1,42 @@ // @ts-check -var EventEmitter = require('events').EventEmitter; -var EventListenerTree = require('./EventListenerTree'); +import { EventEmitter } from 'events'; +import { EventListenerTree } from './EventListenerTree'; var mergeInto = require('../util').mergeInto; /** @type any */ -var Model = require('./Model'); - -// These events are re-emitted as 'all' events, and they are queued up and -// emitted in sequence, so that events generated by other events are not -// seen in a different order by later listeners -exports.mutationEvents = { - ChangeEvent: ChangeEvent, - LoadEvent: LoadEvent, - UnloadEvent: UnloadEvent, - InsertEvent: InsertEvent, - RemoveEvent: RemoveEvent, - MoveEvent: MoveEvent -}; - -exports.Passed = Passed; +import { Model } from './Model'; + +declare module './Model' { + interface Model { + _defaultCallback(err?: Error): void; + _emitError(err: Error, context?: any): void; + wrapCallback(cb: ErrorCallback); + _mutationListeners: Record; + _emittingMutation: boolean; + _mutationEventQueue: null; + _eventContextListeners: Record; + _emitMutation(segments: Segments, event: any): void; + _callMutationListeners(type: string, segments: Segments, event: any): void; + __on: typeof EventEmitter.prototype.on; + addListener(event: string, listener: any, arg2?: any, arg3?: any): any; + on(event: string, listener: any, arg2?: any, arg3?: any): any; + __once: typeof EventEmitter.prototype.once; + once(event: string, listener: any, arg2?: any, arg3?: any): any; + __removeListener: typeof EventEmitter.prototype.removeListener; + removeListener(type: string, listener: any): void; + __removeAllListeners: typeof EventEmitter.prototype.removeAllListeners; + removeAllListeners(type: string, subpath: string): void; + _removeAllListeners(type: string, segments: Segments): void; + pass(object: any, invert?: boolean): Model; + silent(value?: boolean): Model; + eventContext(id: string): Model; + removeContextListeners(): void; + _removeMutationListener(listener: MutationListener): void; + _addMutationListener(type: string, arg1: any, arg2: any, arg3: any): MutationListener; + } +} -Model.INITS.push(function(model) { +Model.INITS.push(function (model: Model) { var root = model.root; // EventEmitter.call(root); @@ -53,7 +69,7 @@ Model.INITS.push(function(model) { // mergeInto(Model.prototype, EventEmitter.prototype); -Model.prototype.wrapCallback = function(cb) { +Model.prototype.wrapCallback = function (cb) { if (!cb) return this.root._defaultCallback; var model = this; return function wrappedCallback() { @@ -65,17 +81,19 @@ Model.prototype.wrapCallback = function(cb) { }; }; -Model.prototype._emitError = function(err, context) { +Model.prototype._emitError = function (err, context) { var message = (err.message) ? err.message : (typeof err === 'string') ? err : 'Unknown model error'; if (context) { message += ' ' + context; } + // @ts-ignore if (err.data) { try { + // @ts-ignore message += ' ' + JSON.stringify(err.data); - } catch (stringifyErr) {} + } catch (stringifyErr) { } } if (err instanceof Error) { err.message = message; @@ -85,7 +103,7 @@ Model.prototype._emitError = function(err, context) { this.emit('error', err); }; -Model.prototype._emitMutation = function(segments, event) { +Model.prototype._emitMutation = function (segments, event) { if (this._silent) return; var root = this.root; this._callMutationListeners(event._immediateType, segments, event); @@ -122,7 +140,7 @@ Model.prototype._emitMutation = function(segments, event) { root._emittingMutation = false; }; -Model.prototype._callMutationListeners = function(type, segments, event) { +Model.prototype._callMutationListeners = function (type, segments, event) { var tree = this.root._mutationListeners[type]; var listeners = tree.getWildcardListeners(segments); for (var i = 0, len = listeners.length; i < len; i++) { @@ -138,18 +156,18 @@ Model.prototype._callMutationListeners = function(type, segments, event) { Model.prototype.__on = EventEmitter.prototype.on; Model.prototype.addListener = -Model.prototype.on = function(type, arg1, arg2, arg3) { - var listener = this._addMutationListener(type, arg1, arg2, arg3); - if (listener) { - return listener; - } - // Normal event - this.__on(type, arg1); - return arg1; -}; + Model.prototype.on = function (type, arg1, arg2, arg3) { + var listener = this._addMutationListener(type, arg1, arg2, arg3); + if (listener) { + return listener; + } + // Normal event + this.__on(type, arg1); + return arg1; + }; Model.prototype.__once = EventEmitter.prototype.once; -Model.prototype.once = function(type, arg1, arg2, arg3) { +Model.prototype.once = function (type, arg1, arg2, arg3) { var listener = this._addMutationListener(type, arg1, arg2, arg3); if (listener) { onceWrapListener(this, listener); @@ -169,7 +187,7 @@ function onceWrapListener(model, listener) { } Model.prototype.__removeListener = EventEmitter.prototype.removeListener; -Model.prototype.removeListener = function(type, listener) { +Model.prototype.removeListener = function (type, listener) { if (this.root._mutationListeners[type]) { this._removeMutationListener(listener); return; @@ -179,11 +197,11 @@ Model.prototype.removeListener = function(type, listener) { }; Model.prototype.__removeAllListeners = EventEmitter.prototype.removeAllListeners; -Model.prototype.removeAllListeners = function(type, subpath) { +Model.prototype.removeAllListeners = function (type, subpath) { var segments = this._splitPath(subpath); this._removeAllListeners(type, segments); }; -Model.prototype._removeAllListeners = function(type, segments) { +Model.prototype._removeAllListeners = function (type, segments) { var mutationListeners = this.root._mutationListeners; if (type == null) { for (var key in mutationListeners) { @@ -201,17 +219,17 @@ Model.prototype._removeAllListeners = function(type, segments) { this.__removeAllListeners(type); }; -function Passed() {} +export class Passed { } Model.prototype.pass = (Object.assign) ? - function(object, invert) { + function (object, invert) { var model = this._child(); model._pass = (invert) ? Object.assign(new Passed(), object, this._pass) : Object.assign(new Passed(), this._pass, object); return model; } : - function(object, invert) { + function (object, invert) { var model = this._child(); var pass = new Passed(); if (invert) { @@ -231,19 +249,19 @@ Model.prototype.pass = (Object.assign) ? * @param {Boolean|Null} value defaults to true * @return {Model} */ -Model.prototype.silent = function(value) { +Model.prototype.silent = function (value) { var model = this._child(); model._silent = (value == null) ? true : !!value; return model; }; -Model.prototype.eventContext = function(id) { +Model.prototype.eventContext = function (id) { var model = this._child(); model._eventContext = id; return model; }; -Model.prototype.removeContextListeners = function() { +Model.prototype.removeContextListeners = function () { var id = this._eventContext; if (id == null) return; var map = this.root._eventContextListeners; @@ -256,7 +274,7 @@ Model.prototype.removeContextListeners = function() { } }; -Model.prototype._removeMutationListener = function(listener) { +Model.prototype._removeMutationListener = function (listener) { listener.node.removeOwnListener(listener); var id = this._eventContext; if (id == null) return; @@ -273,7 +291,7 @@ Model.prototype._removeMutationListener = function(listener) { } }; -Model.prototype._addMutationListener = function(type, arg1, arg2, arg3) { +Model.prototype._addMutationListener = function (type, arg1, arg2, arg3) { var tree = this.root._mutationListeners[type]; if (!tree) return; // Create double-linked listener and tree node for later removal @@ -367,18 +385,25 @@ function createCaptures(captureIndicies, remainingIndex, segments) { return captures; } -function MutationListener(patternSegments, eventContext, fn) { - this.patternSegments = patternSegments; - this.eventContext = eventContext; - this.fn = fn; - this.node = null; +class MutationListener { + patternSegments: string[]; + eventContext: any; + fn: any; + node: any | null; + + constructor(patternSegments, eventContext, fn) { + this.patternSegments = patternSegments; + this.eventContext = eventContext; + this.fn = fn; + this.node = null; + } } function createMutationListener(pattern, eventContext, cb) { var patternSegments = pattern.split('.'); var fn; if (patternSegments.length === 1 && patternSegments[0] === '**') { - fn = function(segments, event) { + fn = function (segments, event) { var captures = [segments.join('.')]; cb(event, captures); }; @@ -400,12 +425,12 @@ function createMutationListener(pattern, eventContext, cb) { } } if (captureIndicies || remainingIndex != null) { - fn = function(segments, event) { + fn = function (segments, event) { var captures = createCaptures(captureIndicies, remainingIndex, segments); cb(event, captures); }; } else { - fn = function(segments, event) { + fn = function (segments, event) { cb(event, []); }; } @@ -415,117 +440,167 @@ function createMutationListener(pattern, eventContext, cb) { function createMutationListenerLegacy(type, pattern, eventContext, cb) { var mutationListenerAdapter = (type === 'all') ? - function(event, captures) { + function (event, captures) { var args = captures.concat(event.type, event._getArgs()); cb.apply(null, args); } : - function(event, captures) { + function (event, captures) { var args = captures.concat(event._getArgs()); cb.apply(null, args); }; return createMutationListener(pattern, eventContext, mutationListenerAdapter); } -function ChangeEvent(value, previous, passed) { - this.value = value; - this.previous = previous; - this.passed = passed; +class ChangeEvent { + type = 'change'; + _immediateType = 'changeImmediate'; + value: any; + previous: any; + passed: any; + + constructor(value, previous, passed) { + this.value = value; + this.previous = previous; + this.passed = passed; + } + + clone() { + return new ChangeEvent(this.value, this.previous, this.passed); + }; + + _getArgs() { + return [this.value, this.previous, this.passed]; + }; } -ChangeEvent.prototype.type = 'change'; -ChangeEvent.prototype._immediateType = 'changeImmediate'; -ChangeEvent.prototype.clone = function() { - return new ChangeEvent(this.value, this.previous, this.passed); -}; -ChangeEvent.prototype._getArgs = function() { - return [this.value, this.previous, this.passed]; -}; -function LoadEvent(value, passed) { - /** - * The documented public name of the loaded item is `document` - * However we use `value` internally so both are provided - * Using `document` is preferred - */ - this.value = value; - this.document = value; - this.passed = passed; +class LoadEvent { + type = 'load'; + _immediateType = 'loadImmediate'; + value: any; + document: any; + passed: any; + + constructor(value, passed) { + /** + * The documented public name of the loaded item is `document` + * However we use `value` internally so both are provided + * Using `document` is preferred + */ + this.value = value; + this.document = value; + this.passed = passed; + } + + clone() { + return new LoadEvent(this.value, this.passed); + }; + + _getArgs() { + return [this.value, this.passed]; + }; } -LoadEvent.prototype.type = 'load'; -LoadEvent.prototype._immediateType = 'loadImmediate'; -LoadEvent.prototype.clone = function() { - return new LoadEvent(this.value, this.passed); -}; -LoadEvent.prototype._getArgs = function() { - return [this.value, this.passed]; -}; -function UnloadEvent(previous, passed) { - /** - * The documented public name of the unloaded item is `previousDocument` - * However we use `previous` internally so both are provided - * Using `previousDocument` is preferred - */ - this.previous = previous; - this.previousDocument = previous; - this.passed = passed; +class UnloadEvent { + type = 'unload'; + _immediateType = 'unloadImmediate'; + previous: any; + previousDocument: any; + passed: any; + + constructor(previous, passed) { + /** + * The documented public name of the unloaded item is `previousDocument` + * However we use `previous` internally so both are provided + * Using `previousDocument` is preferred + */ + this.previous = previous; + this.previousDocument = previous; + this.passed = passed; + } + + clone() { + return new UnloadEvent(this.previous, this.passed); + }; + + _getArgs() { + return [this.previous, this.passed]; + }; } -UnloadEvent.prototype.type = 'unload'; -UnloadEvent.prototype._immediateType = 'unloadImmediate'; -UnloadEvent.prototype.clone = function() { - return new UnloadEvent(this.previous, this.passed); -}; -UnloadEvent.prototype._getArgs = function() { - return [this.previous, this.passed]; -}; -function InsertEvent(index, values, passed) { - this.index = index; - this.values = values; - this.passed = passed; +class InsertEvent { + type = 'insert'; + _immediateType = 'insertImmediate'; + index: number; + values: any; + passed: any; + + constructor(index, values, passed) { + this.index = index; + this.values = values; + this.passed = passed; + } + + clone() { + return new InsertEvent(this.index, this.values, this.passed); + }; + + _getArgs() { + return [this.index, this.values, this.passed]; + }; } -InsertEvent.prototype.type = 'insert'; -InsertEvent.prototype._immediateType = 'insertImmediate'; -InsertEvent.prototype.clone = function() { - return new InsertEvent(this.index, this.values, this.passed); -}; -InsertEvent.prototype._getArgs = function() { - return [this.index, this.values, this.passed]; -}; -function RemoveEvent(index, values, passed) { - this.index = index; - /** - * The documented public name of the removed item is `removed` - * However we use `values` internally so both are provided - * Using `removed` is preferred - */ - this.values = values; - this.removed = values; - this.passed = passed; +class RemoveEvent { + _immediateType = 'removeImmediate'; + index: number; + passed: any; + removed: any; + type = 'remove'; + values: any; + + constructor(index, values, passed) { + this.index = index; + /** + * The documented public name of the removed item is `removed` + * However we use `values` internally so both are provided + * Using `removed` is preferred + */ + this.values = values; + this.removed = values; + this.passed = passed; + } + + clone() { + return new RemoveEvent(this.index, this.values, this.passed); + }; + + _getArgs() { + return [this.index, this.values, this.passed]; + }; } -RemoveEvent.prototype.type = 'remove'; -RemoveEvent.prototype._immediateType = 'removeImmediate'; -RemoveEvent.prototype.clone = function() { - return new RemoveEvent(this.index, this.values, this.passed); -}; -RemoveEvent.prototype._getArgs = function() { - return [this.index, this.values, this.passed]; -}; -function MoveEvent(from, to, howMany, passed) { - this.from = from; - this.to = to; - this.howMany = howMany; - this.passed = passed; +class MoveEvent { + _immediateType = 'moveImmediate'; + from: any; + howMany: number; + passed: any; + to: any; + type = 'move'; + + constructor(from, to, howMany, passed) { + this.from = from; + this.to = to; + this.howMany = howMany; + this.passed = passed; + } + + clone() { + return new MoveEvent(this.from, this.to, this.howMany, this.passed); + }; + + _getArgs() { + return [this.from, this.to, this.howMany, this.passed]; + }; } -MoveEvent.prototype.type = 'move'; -MoveEvent.prototype._immediateType = 'moveImmediate'; -MoveEvent.prototype.clone = function() { - return new MoveEvent(this.from, this.to, this.howMany, this.passed); -}; -MoveEvent.prototype._getArgs = function() { - return [this.from, this.to, this.howMany, this.passed]; -}; // DEPRECATED: Normalize pattern ending in '**' to '.**', since these are // treated equivalently. The '.**' form is preferred, and it should be enforced @@ -539,3 +614,16 @@ function normalizePattern(pattern) { pattern.charAt(end - 2) ) ? pattern.slice(0, end - 1) + '.**' : pattern; }; + + +// These events are re-emitted as 'all' events, and they are queued up and +// emitted in sequence, so that events generated by other events are not +// seen in a different order by later listeners +export const mutationEvents = { + ChangeEvent, + LoadEvent, + UnloadEvent, + InsertEvent, + RemoveEvent, + MoveEvent +}; \ No newline at end of file diff --git a/src/Model/filter.ts b/src/Model/filter.ts index 2180f0e79..e3516448f 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -1,8 +1,18 @@ var util = require('../util'); -var Model = require('./Model'); +import { Model, type Segments } from './Model'; var defaultFns = require('./defaultFns'); -Model.INITS.push(function(model) { +declare module './Model' { + interface Model { + _filters: Filters; + filter: () => any; + sort: () => any; + removeAllFilters: (subpath: string) => void; + _removeAllFilters: (segments: Segments) => void; + } +} + +Model.INITS.push(function(model: Model) { model.root._filters = new Filters(model); model.on('all', filterListener); function filterListener(segments, event) { @@ -88,182 +98,207 @@ Model.prototype._removeAllFilters = function(segments) { } }; -function FromMap() {} -function Filters(model) { - this.model = model; - this.fromMap = new FromMap(); -} - -Filters.prototype.add = function(path, filterFn, sortFn, inputPaths, options) { - return new Filter(this, path, filterFn, sortFn, inputPaths, options); -}; +class FromMap {} -Filters.prototype.toJSON = function() { - var out = []; - for (var from in this.fromMap) { - var filter = this.fromMap[from]; - // Don't try to bundle if functions were passed directly instead of by name - if (!filter.bundle) continue; - var args = [from, filter.path, filter.filterName, filter.sortName, filter.inputPaths]; - if (filter.options) args.push(filter.options); - out.push(args); +class Filters{ + model: Model; + fromMap: FromMap; + constructor(model) { + this.model = model; + this.fromMap = new FromMap(); } - return out; -}; -function Filter(filters, path, filterFn, sortFn, inputPaths, options) { - this.filters = filters; - this.model = filters.model.pass({$filter: this}); - this.path = path; - this.segments = path.split('.'); - this.filterName = null; - this.sortName = null; - this.bundle = true; - this.filterFn = null; - this.sortFn = null; - this.inputPaths = inputPaths; - this.inputsSegments = null; - if (inputPaths) { - this.inputsSegments = []; - for (var i = 0; i < this.inputPaths.length; i++) { - var segments = this.inputPaths[i].split('.'); - this.inputsSegments.push(segments); + add(path, filterFn, sortFn, inputPaths, options) { + return new Filter(this, path, filterFn, sortFn, inputPaths, options); + }; + + toJSON() { + var out = []; + for (var from in this.fromMap) { + var filter = this.fromMap[from]; + // Don't try to bundle if functions were passed directly instead of by name + if (!filter.bundle) continue; + var args = [from, filter.path, filter.filterName, filter.sortName, filter.inputPaths]; + if (filter.options) args.push(filter.options); + out.push(args); } - } - this.options = options; - this.skip = options && options.skip; - this.limit = options && options.limit; - if (filterFn) this.filter(filterFn); - if (sortFn) this.sort(sortFn); - this.idsSegments = null; - this.from = null; - this.fromSegments = null; + return out; + }; } -Filter.prototype.filter = function(fn) { - if (typeof fn === 'function') { - this.filterFn = fn; - this.bundle = false; - return this; - } else if (typeof fn === 'string') { - this.filterName = fn; - this.filterFn = this.model.root._namedFns[fn] || defaultFns[fn]; - if (!this.filterFn) { - throw new TypeError('Filter function not found: ' + fn); - } - } - return this; -}; +class Filter { + bundle: boolean; + filterFn: any; + filterName: string; + filters: any; + from: string; + fromSegments: string[] + idsSegments: string[]; + inputPaths: any; + inputsSegments: string[]; + limit: number; + model: Model; + options: any; + path: string; + segments: Segments; + skip: number; + sortFn: any; + sortName: string; -Filter.prototype.sort = function(fn) { - if (!fn) fn = 'asc'; - if (typeof fn === 'function') { - this.sortFn = fn; - this.bundle = false; - return this; - } else if (typeof fn === 'string') { - this.sortName = fn; - this.sortFn = this.model.root._namedFns[fn] || defaultFns[fn]; - if (!this.sortFn) { - throw new TypeError('Sort function not found: ' + fn); + constructor(filters, path, filterFn, sortFn, inputPaths, options) { + this.filters = filters; + this.model = filters.model.pass({$filter: this}); + this.path = path; + this.segments = path.split('.'); + this.filterName = null; + this.sortName = null; + this.bundle = true; + this.filterFn = null; + this.sortFn = null; + this.inputPaths = inputPaths; + this.inputsSegments = null; + if (inputPaths) { + this.inputsSegments = []; + for (var i = 0; i < this.inputPaths.length; i++) { + var segments = this.inputPaths[i].split('.'); + this.inputsSegments.push(segments); + } } + this.options = options; + this.skip = options && options.skip; + this.limit = options && options.limit; + if (filterFn) this.filter(filterFn); + if (sortFn) this.sort(sortFn); + this.idsSegments = null; + this.from = null; + this.fromSegments = null; } - return this; -}; - -Filter.prototype._slice = function(results) { - if (this.skip == null && this.limit == null) return results; - var begin = this.skip || 0; - // A limit of zero is equivalent to setting no limit - var end; - if (this.limit) end = begin + this.limit; - return results.slice(begin, end); -}; - -Filter.prototype.getInputs = function() { - if (!this.inputsSegments) return; - var inputs = []; - for (var i = 0, len = this.inputsSegments.length; i < len; i++) { - var input = this.model._get(this.inputsSegments[i]); - inputs.push(input); - } - return inputs; -}; -Filter.prototype.callFilter = function(items, key, inputs) { - var item = items[key]; - return (inputs) ? - this.filterFn.apply(this.model, [item, key, items].concat(inputs)) : - this.filterFn.call(this.model, item, key, items); -}; - -Filter.prototype.ids = function() { - var items = this.model._get(this.segments); - var ids = []; - if (!items) return ids; - if (Array.isArray(items)) { - throw new Error('model.filter is not currently supported on arrays'); - } - if (this.filterFn) { - var inputs = this.getInputs(); - for (var key in items) { - if (items.hasOwnProperty(key) && this.callFilter(items, key, inputs)) { - ids.push(key); + filter(fn) { + if (typeof fn === 'function') { + this.filterFn = fn; + this.bundle = false; + return this; + } else if (typeof fn === 'string') { + this.filterName = fn; + this.filterFn = this.model.root._namedFns[fn] || defaultFns[fn]; + if (!this.filterFn) { + throw new TypeError('Filter function not found: ' + fn); } } - } else { - ids = Object.keys(items); - } - var sortFn = this.sortFn; - if (sortFn) { - ids.sort(function(a, b) { - return sortFn(items[a], items[b]); - }); - } - return this._slice(ids); -}; + return this; + }; -Filter.prototype.get = function() { - var items = this.model._get(this.segments); - var results = []; - if (Array.isArray(items)) { - throw new Error('model.filter is not currently supported on arrays'); - } - if (this.filterFn) { - var inputs = this.getInputs(); - for (var key in items) { - if (items.hasOwnProperty(key) && this.callFilter(items, key, inputs)) { - results.push(items[key]); + sort(fn) { + if (!fn) fn = 'asc'; + if (typeof fn === 'function') { + this.sortFn = fn; + this.bundle = false; + return this; + } else if (typeof fn === 'string') { + this.sortName = fn; + this.sortFn = this.model.root._namedFns[fn] || defaultFns[fn]; + if (!this.sortFn) { + throw new TypeError('Sort function not found: ' + fn); } } - } else { - for (var key in items) { - if (items.hasOwnProperty(key)) { - results.push(items[key]); + return this; + }; + + _slice(results) { + if (this.skip == null && this.limit == null) return results; + var begin = this.skip || 0; + // A limit of zero is equivalent to setting no limit + var end; + if (this.limit) end = begin + this.limit; + return results.slice(begin, end); + }; + + getInputs() { + if (!this.inputsSegments) return; + var inputs = []; + for (var i = 0, len = this.inputsSegments.length; i < len; i++) { + var input = this.model._get(this.inputsSegments[i]); + inputs.push(input); + } + return inputs; + }; + + callFilter(items, key, inputs) { + var item = items[key]; + return (inputs) ? + this.filterFn.apply(this.model, [item, key, items].concat(inputs)) : + this.filterFn.call(this.model, item, key, items); + }; + + ids() { + var items = this.model._get(this.segments); + var ids = []; + if (!items) return ids; + if (Array.isArray(items)) { + throw new Error('model.filter is not currently supported on arrays'); + } + if (this.filterFn) { + var inputs = this.getInputs(); + for (var key in items) { + if (items.hasOwnProperty(key) && this.callFilter(items, key, inputs)) { + ids.push(key); + } } + } else { + ids = Object.keys(items); } - } - if (this.sortFn) results.sort(this.sortFn); - return this._slice(results); -}; - -Filter.prototype.update = function(pass) { - var ids = this.ids(); - this.model.pass(pass, true)._setArrayDiff(this.idsSegments, ids); -}; - -Filter.prototype.ref = function(from) { - from = this.model.path(from); - this.from = from; - this.fromSegments = from.split('.'); - this.filters.fromMap[from] = this; - this.idsSegments = ['$filters', from.replace(/\./g, '|')]; - this.update(); - return this.model.refList(from, this.path, this.idsSegments.join('.')); -}; - -Filter.prototype.destroy = function() { - delete this.filters.fromMap[this.from]; - this.model._removeRef(this.idsSegments); - this.model._del(this.idsSegments); -}; + var sortFn = this.sortFn; + if (sortFn) { + ids.sort(function(a, b) { + return sortFn(items[a], items[b]); + }); + } + return this._slice(ids); + }; + + get() { + var items = this.model._get(this.segments); + var results = []; + if (Array.isArray(items)) { + throw new Error('model.filter is not currently supported on arrays'); + } + if (this.filterFn) { + var inputs = this.getInputs(); + for (var key in items) { + if (items.hasOwnProperty(key) && this.callFilter(items, key, inputs)) { + results.push(items[key]); + } + } + } else { + for (var key in items) { + if (items.hasOwnProperty(key)) { + results.push(items[key]); + } + } + } + if (this.sortFn) results.sort(this.sortFn); + return this._slice(results); + }; + + update(pass?: any) { + var ids = this.ids(); + this.model.pass(pass, true)._setArrayDiff(this.idsSegments, ids); + }; + + ref(from) { + from = this.model.path(from); + this.from = from; + this.fromSegments = from.split('.'); + this.filters.fromMap[from] = this; + this.idsSegments = ['$filters', from.replace(/\./g, '|')]; + this.update(); + return this.model.refList(from, this.path, this.idsSegments.join('.')); + }; + + destroy() { + delete this.filters.fromMap[this.from]; + this.model._removeRef(this.idsSegments); + this.model._del(this.idsSegments); + }; +} diff --git a/src/Model/fn.ts b/src/Model/fn.ts index 47aa6e9ef..8d88e75ce 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -1,12 +1,28 @@ -var util = require('../util'); -var Model = require('./Model'); +import { Model, type Segments } from './Model'; +import { EventListenerTree } from './EventListenerTree'; +import { EventMapTree } from './EventMapTree'; var defaultFns = require('./defaultFns'); -var EventListenerTree = require('./EventListenerTree'); -var EventMapTree = require('./EventMapTree'); +var util = require('../util'); + +class NamedFns { } + +declare module './Model' { + interface Model { + _namedFns: NamedFns; + _fns: Fns; + fn(name: string, fns: Fns): void; + evaluate(): any; + start(): any; + stop(subpath: string): void; + _stop(segments: Segments): void; + stopAll(subpath: string): void; + _stopAll(segments: Segments): void; + + } +} -function NamedFns() {} -Model.INITS.push(function(model) { +Model.INITS.push(function (model) { var root = model.root; root._namedFns = new NamedFns(); root._fns = new Fns(root); @@ -33,7 +49,7 @@ function addFnListener(model) { }); } -Model.prototype.fn = function(name, fns) { +Model.prototype.fn = function (name, fns) { this.root._namedFns[name] = fns; }; @@ -84,97 +100,105 @@ function parseStartArguments(model, args, hasPath) { }; } -Model.prototype.evaluate = function() { +Model.prototype.evaluate = function () { var args = Array.prototype.slice.call(arguments); var parsed = parseStartArguments(this, args, false); return this.root._fns.get(parsed.name, parsed.inputPaths, parsed.fns, parsed.options); }; -Model.prototype.start = function() { +Model.prototype.start = function () { var args = Array.prototype.slice.call(arguments); var parsed = parseStartArguments(this, args, true); return this.root._fns.start(parsed.name, parsed.path, parsed.inputPaths, parsed.fns, parsed.options); }; -Model.prototype.stop = function(subpath) { +Model.prototype.stop = function (subpath) { var segments = this._splitPath(subpath); this._stop(segments); }; -Model.prototype._stop = function(segments) { +Model.prototype._stop = function (segments) { this.root._fns.stop(segments); }; -Model.prototype.stopAll = function(subpath) { +Model.prototype.stopAll = function (subpath) { var segments = this._splitPath(subpath); this._stopAll(segments); }; -Model.prototype._stopAll = function(segments) { +Model.prototype._stopAll = function (segments) { this.root._fns.stopAll(segments); }; -function Fns(model) { - this.model = model; - this.nameMap = model._namedFns; - this.fromMap = new EventMapTree(); - var inputListeners = this.inputListeners = new EventListenerTree(); - this._removeInputListeners = function(fn) { +class Fns { + model: Model; + nameMap: NamedFns; + fromMap: EventMapTree; + inputListeners: EventListenerTree; + + constructor(model: Model) { + this.model = model; + this.nameMap = model._namedFns; + this.fromMap = new EventMapTree(); + this.inputListeners = new EventListenerTree(); + } + + _removeInputListeners(fn) { for (var i = 0; i < fn.inputsSegments.length; i++) { var inputSegements = fn.inputsSegments[i]; - inputListeners.removeListener(inputSegements, fn); + this.inputListeners.removeListener(inputSegements, fn); } }; -} - -Fns.prototype.get = function(name, inputPaths, fns, options) { - fns || (fns = this.nameMap[name] || defaultFns[name]); - var fn = new Fn(this.model, name, null, inputPaths, fns, options); - return fn.get(); -}; - -Fns.prototype.start = function(name, path, inputPaths, fns, options) { - fns || (fns = this.nameMap[name] || defaultFns[name]); - var fn = new Fn(this.model, name, path, inputPaths, fns, options); - var previous = this.fromMap.setListener(fn.fromSegments, fn); - if (previous) { - this._removeInputListeners(previous); - } - for (var i = 0; i < fn.inputsSegments.length; i++) { - var inputSegements = fn.inputsSegments[i]; - this.inputListeners.addListener(inputSegements, fn); - } - return fn._onInput(); -}; - -Fns.prototype.stop = function(segments) { - var previous = this.fromMap.deleteListener(segments); - if (previous) { - this._removeInputListeners(previous); - } -}; -Fns.prototype.stopAll = function(segments) { - var node = this.fromMap.deleteAllListeners(segments); - if (node) { - node.forEach(this._removeInputListeners); - } -}; - -Fns.prototype.toJSON = function() { - var out = []; - this.fromMap.forEach(function(fn) { - // Don't try to bundle non-named functions that were started via - // model.start directly instead of by name - if (!fn.name) return; - var args = [fn.from].concat(fn.inputPaths); - if (fn.options) args.push(fn.options); - args.push(fn.name); - out.push(args); - }); - return out; -}; + get(name: string, inputPaths: any, fns: any, options: any) { + fns || (fns = this.nameMap[name] || defaultFns[name]); + var fn = new Fn(this.model, name, null, inputPaths, fns, options); + return fn.get(); + }; + + start(name: string, path: string, inputPaths: any, fns: any, options: any) { + fns || (fns = this.nameMap[name] || defaultFns[name]); + var fn = new Fn(this.model, name, path, inputPaths, fns, options); + var previous = this.fromMap.setListener(fn.fromSegments, fn); + if (previous) { + this._removeInputListeners(previous); + } + for (var i = 0; i < fn.inputsSegments.length; i++) { + var inputSegements = fn.inputsSegments[i]; + this.inputListeners.addListener(inputSegements, fn); + } + return fn._onInput(); + }; + + stop(segments: Segments) { + var previous = this.fromMap.deleteListener(segments); + if (previous) { + this._removeInputListeners(previous); + } + }; + + stopAll(segments: Segments) { + var node = this.fromMap.deleteAllListeners(segments); + if (node) { + node.forEach(this._removeInputListeners); + } + }; + + toJSON() { + var out = []; + this.fromMap.forEach(function (fn) { + // Don't try to bundle non-named functions that were started via + // model.start directly instead of by name + if (!fn.name) return; + var args = [fn.from].concat(fn.inputPaths); + if (fn.options) args.push(fn.options); + args.push(fn.name); + out.push(args); + }); + return out; + }; +} function Fn(model, name, from, inputPaths, fns, options) { - this.model = model.pass({$fn: this}); + this.model = model.pass({ $fn: this }); this.name = name; this.from = from; this.inputPaths = inputPaths; @@ -203,7 +227,7 @@ function Fn(model, name, from, inputPaths, fns, options) { this.eventPending = false; } -Fn.prototype.apply = function(fn, inputs) { +Fn.prototype.apply = function (fn, inputs) { for (var i = 0, len = this.inputsSegments.length; i < len; i++) { var input = this.model._get(this.inputsSegments[i]); inputs.push(this.copyInput ? util.deepCopy(input) : input); @@ -211,11 +235,11 @@ Fn.prototype.apply = function(fn, inputs) { return fn.apply(this.model, inputs); }; -Fn.prototype.get = function() { +Fn.prototype.get = function () { return this.apply(this.getFn, []); }; -Fn.prototype.set = function(value, pass) { +Fn.prototype.set = function (value, pass) { if (!this.setFn) return; var out = this.apply(this.setFn, [value]); if (!out) return; @@ -227,12 +251,12 @@ Fn.prototype.set = function(value, pass) { } }; -Fn.prototype.onInput = function(pass) { +Fn.prototype.onInput = function (pass) { if (this.async) { if (this.eventPending) return; this.eventPending = true; var fn = this; - process.nextTick(function() { + process.nextTick(function () { fn._onInput(pass); fn.eventPending = false; }); @@ -241,18 +265,18 @@ Fn.prototype.onInput = function(pass) { return this._onInput(pass); }; -Fn.prototype._onInput = function(pass) { +Fn.prototype._onInput = function (pass) { var value = (this.copyOutput) ? util.deepCopy(this.get()) : this.get(); this._setValue(this.model.pass(pass, true), this.fromSegments, value); return value; }; -Fn.prototype.onOutput = function(pass) { +Fn.prototype.onOutput = function (pass) { var value = this.model._get(this.fromSegments); return this.set(value, pass); }; -Fn.prototype._setValue = function(model, segments, value) { +Fn.prototype._setValue = function (model, segments, value) { if (this.mode === 'diffDeep') { model._setDiffDeep(segments, value); } else if (this.mode === 'arrayDeep') { diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 0eb37446a..e0eb56137 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -1,5 +1,5 @@ var util = require('../util'); -var Model = require('./Model'); +import { Model } from './Model'; var mutationEvents = require('./events').mutationEvents; var ChangeEvent = mutationEvents.ChangeEvent; var InsertEvent = mutationEvents.InsertEvent; @@ -7,6 +7,129 @@ var RemoveEvent = mutationEvents.RemoveEvent; var MoveEvent = mutationEvents.MoveEvent; var promisify = util.promisify; +declare module './Model' { + interface Model { + _mutate(segments, fn, cb): void; + set(value: any): void; + set(subpath: string, value: any, cb?: ErrorCallback): void; + setPromised(value: any): Promise; + setPromised(subpath: string, value: any): Promise; + _set(segments: Segments, value: any, cb?: ErrorCallback): void; + setNull(value: any): void; + setNull(subpath: string, value: any, cb?: ErrorCallback): void; + setNullPromised(value: any): Promise; + setNullPromised(subpath: string, value: any): Promise; + _setNull(segments: Segments, value: any, cb?: ErrorCallback): void; + setEach(value: any): void; + setEach(subpath: string, value: any, cb?: ErrorCallback): void; + setEachPromised(value: any): Promise; + setEachPromised(subpath: string, value: any): Promise; + _setEach(segments: Segments, value: any, cb?: ErrorCallback): void; + + create(value: any): void; + create(subpath: string, value: any, cb?: ErrorCallback): void; + createPromised(value: any): Promise; + createPromised(subpath: string, value: any): Promise; + _create(segments: Segments, value: any, cb?: ErrorCallback): void; + + createNull(value: any): void; + createNull(subpath: string, value: any, cb?: ErrorCallback): void; + createNullPromised(value: any): Promise; + createNullPromised(subpath: string, value: any): Promise; + _createNull(segments: Segments, value: any, cb?: ErrorCallback): void; + + add(value: any): void; + add(subpath: string, value: any, cb?: ErrorCallback): void; + addPromised(value: any): Promise; + addPromised(subpath: string, value: any): Promise; + _add(segments: Segments, value: any, cb?: ErrorCallback): void; + + del(value: any): void; + del(subpath: string, value: any, cb?: ErrorCallback): void; + delPromised(value: any): Promise; + delPromised(subpath: string, value: any): Promise; + _del(segments: Segments, value?: any, cb?: ErrorCallback): void; + + _delNoDereference(segments: Segments, cb?: ErrorCallback): void; + + increment(value: any): void; + increment(subpath: string, value: any, cb?: ErrorCallback): void; + incrementPromised(value: any): Promise; + incrementPromised(subpath: string, value: any): Promise; + _increment(segments: Segments, value: any, cb?: ErrorCallback): void; + + push(value: any): void; + push(subpath: string, value: any, cb?: ErrorCallback): void; + pushPromised(value: any): Promise; + pushPromised(subpath: string, value: any): Promise; + _push(segments: Segments, value: any, cb?: ErrorCallback): void; + + unshift(value: any): void; + unshift(subpath: string, value: any, cb?: ErrorCallback): void; + unshiftPromised(value: any): Promise; + unshiftPromised(subpath: string, value: any): Promise; + _unshift(segments: Segments, value: any, cb?: ErrorCallback): void; + + insert(value: any, index: number): void; + insert(subpath: string, index: number, value: any, cb?: ErrorCallback): void; + insertPromised(value: any, index: number): Promise; + insertPromised(subpath: string, index: number, value: any): Promise; + _insert(segments: Segments, index: number, value: any, cb?: ErrorCallback): void; + + pop(value: any): void; + pop(subpath: string, value: any, cb?: ErrorCallback): void; + popPromised(value: any): Promise; + popPromised(subpath: string, value: any): Promise; + _pop(segments: Segments, value: any, cb?: ErrorCallback): void; + + shift(subpath: string, cb?: ErrorCallback): void; + shiftPromised(subpath?: string): Promise; + _shift(segments: Segments, cb?: ErrorCallback): void; + + remove(index: number, cb?: ErrorCallback): void; + remove(subpath: string, cb?: ErrorCallback): void; + remove(index: number, howMany: number, cb?: ErrorCallback): void; + remove(subpath: string, index: number, cb?: ErrorCallback): void; + remove(subpath: string, index: number, howMany: number, cb?: ErrorCallback): void; + removePromised(index: number): Promise; + removePromised(subpath: string): Promise; + removePromised(index: number, howMany: number): Promise; + removePromised(subpath: string, index: number): Promise; + removePromised(subpath: string, index: number, howMany: number): void; + _remove(segments: Segments, index: number, howMany: number, cb?: ErrorCallback): void; + + move(from: number, to: number, cb?: ErrorCallback): void; + move(from: number, to: number, howMany: number, cb?: ErrorCallback): void; + move(subpath: string, from: number, to: number, cb?: ErrorCallback): void; + move(subpath: string, from: number, to: number, howmany: number, cb?: ErrorCallback): void; + movePromised(from: number, to: number): Promise; + movePromised(from: number, to: number, howMany: number): Promise; + movePromised(subpath: string, from: number, to: number): Promise; + movePromised(subpath: string, from: number, to: number, howmany: number): Promise; + _move(segments: Segments, from: number, to: number, owMany: number, cb?: ErrorCallback): void; + + stringInsert(index: number, text: string, cb?: ErrorCallback): void; + stringInsert(subpath: string, index: number, text: string, cb?: ErrorCallback): void; + stringInsertPromised(index: number, text: string): Promise; + stringInsertPromised(subpath: string, index: number, text: string): Promise; + _stringInsert(segments: Segments, index: number,text: string, cb?: ErrorCallback): void; + + stringRemove(index: number, howMany: number, cb?: ErrorCallback): void; + stringRemove(subpath: string, index: number, cb?: ErrorCallback): void; + stringRemove(subpath: string, index: number, howMany: number, cb?: ErrorCallback): void; + stringRemovePromised(index: number, howMany: number): Promise; + stringRemovePromised(subpath: string, index: number): Promise; + stringRemovePromised(subpath: string, index: number, howMany: number): Promise; + _stringRemove(segments: Segments, index: number, howMany: number, cb?: ErrorCallback): void; + + subtypeSubmit(subtype: any, subtypeOp: any, cb?: ErrorCallback): void; + subtypeSubmit(subpath: string, subtype: any, subtypeOp: any, cb?: ErrorCallback): void; + subtypeSubmitPromised(subtype: any, subtypeOp: any): Promise; + subtypeSubmitPromised(subpath: string, subtype: any, subtypeOp: any): Promise; + _subtypeSubmit(segments: Segments, subtype: any, subtypeOp: any, cb?: ErrorCallback): void; + } +} + Model.prototype._mutate = function(segments, fn, cb) { cb = this.wrapCallback(cb); var collectionName = segments[0]; diff --git a/src/Model/paths.ts b/src/Model/paths.ts index 97b348c10..75bf183d0 100644 --- a/src/Model/paths.ts +++ b/src/Model/paths.ts @@ -1,7 +1,19 @@ -var Model = require('./Model'); +import { Model } from './Model'; exports.mixin = {}; +declare module './Model' { + interface Model { + _splitPath(subpath: string): string[]; + path(subpath: string): Model; + isPath(subpath: string): boolean; + scope(subpath: string): Model; + at(subpath: string): Model; + parent(levels?: number): Model; + leaf(path: string): string; + } +} + Model.prototype._splitPath = function(subpath) { var path = this.path(subpath); return (path && path.split('.')) || []; @@ -20,6 +32,7 @@ Model.prototype.path = function(subpath) { if (typeof subpath === 'string' || typeof subpath === 'number') { return (this._at) ? this._at + '.' + subpath : '' + subpath; } + // @ts-ignore if (typeof subpath.path === 'function') return subpath.path(); }; diff --git a/src/Model/ref.ts b/src/Model/ref.ts index a4d0fb67b..60b42f124 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -1,6 +1,27 @@ -var Model = require('./Model'); -var EventMapTree = require('./EventMapTree'); -var EventListenerTree = require('./EventListenerTree'); +import { EventListenerTree } from './EventListenerTree'; +import { EventMapTree } from './EventMapTree'; +import { Model } from './Model'; + +declare module './Model' { + type Segments = any; + + interface Model { + _refs: any; + _refLists: any; + canRefTo(value: any): boolean; + _canRefTo(from: Segments, to: Segments, options: any): boolean; + ref(to: Segments): void; + ref(from: Segments, to: Segments, options?: any): void; + _ref(from: Segments, to: Segments, options: any): any; + removeRef(subpath: string): void; + _removeRef(segments: Segments): void; + removeAllRefs(subpath: string): void; + _removeAllRefs(segments: Segments): void; + dereference(subpath: string): Segments; + _dereference(segments: Segments, forArrayMutator: any, ignore: boolean): Segments; + } +} + Model.INITS.push(function(model) { var root = model.root; @@ -257,44 +278,56 @@ function noopDereference(segments) { return segments; } -function Ref(fromSegments, toSegments, options) { - this.fromSegments = fromSegments; - this.toSegments = toSegments; - this.updateIndices = options && options.updateIndices; -} +export class Ref { + fromSegments: string[]; + toSegments: string[]; + updateIndices: boolean; -function Refs() { - this.fromMap = new EventMapTree(); - var toListeners = this.toListeners = new EventListenerTree(); - this._removeInputListeners = function(ref) { - toListeners.removeListener(ref.toSegments, ref); - }; + constructor(fromSegments, toSegments, options) { + this.fromSegments = fromSegments; + this.toSegments = toSegments; + this.updateIndices = options && options.updateIndices; + } } -Refs.prototype.add = function(ref) { - this.fromMap.setListener(ref.fromSegments, ref); - this.toListeners.addListener(ref.toSegments, ref); -}; - -Refs.prototype.remove = function(segments) { - var ref = this.fromMap.deleteListener(segments); - if (!ref) return; - this.toListeners.removeListener(ref.toSegments, ref); -}; +export class Refs { + fromMap: EventMapTree; + toListeners: EventListenerTree; -Refs.prototype.removeAll = function(segments) { - var node = this.fromMap.deleteAllListeners(segments); - if (node) { - node.forEach(this._removeInputListeners); + constructor() { + this.fromMap = new EventMapTree(); + this.toListeners = new EventListenerTree(); } -}; -Refs.prototype.toJSON = function() { - var out = []; - this.fromMap.forEach(function(ref) { - var from = ref.fromSegments.join('.'); - var to = ref.toSegments.join('.'); - out.push([from, to]); - }); - return out; -}; + _removeInputListeners(ref: Ref) { + this.toListeners.removeListener(ref.toSegments, ref); + }; + + add(ref) { + this.fromMap.setListener(ref.fromSegments, ref); + this.toListeners.addListener(ref.toSegments, ref); + }; + + remove(segments) { + var ref = this.fromMap.deleteListener(segments); + if (!ref) return; + this.toListeners.removeListener(ref.toSegments, ref); + }; + + removeAll(segments) { + var node = this.fromMap.deleteAllListeners(segments); + if (node) { + node.forEach(this._removeInputListeners); + } + }; + + toJSON() { + var out = []; + this.fromMap.forEach(function(ref) { + var from = ref.fromSegments.join('.'); + var to = ref.toSegments.join('.'); + out.push([from, to]); + }); + return out; + }; +} diff --git a/src/Model/refList.ts b/src/Model/refList.ts index 882bf4afd..72765f790 100644 --- a/src/Model/refList.ts +++ b/src/Model/refList.ts @@ -1,6 +1,13 @@ -var Model = require('./Model'); -var EventMapTree = require('./EventMapTree'); -var EventListenerTree = require('./EventListenerTree'); +import { EventListenerTree } from './EventListenerTree'; +import { EventMapTree } from './EventMapTree'; +import { Model } from './Model'; + +declare module './Model' { + interface Model { + refList(to: any, ids: any, options?: any): RefList; + refList(from: any, to: any, ids: any, options?: any): RefList; + } +} Model.INITS.push(function(model) { var root = model.root; @@ -57,14 +64,14 @@ function patchFromEvent(segments, event, refList) { // Mutation on the `from` output itself if (segmentsLength === fromLength) { if (type === 'insert') { - var ids = setNewToValues(model, refList, event.values); + const ids = setNewToValues(model, refList, event.values); model._insert(refList.idsSegments, event.index, ids); return; } if (type === 'remove') { - var howMany = event.values.length; - var ids = model._remove(refList.idsSegments, event.index, howMany); + const howMany = event.values.length; + const ids = model._remove(refList.idsSegments, event.index, howMany); // Delete the appropriate items underneath `to` if the `deleteRemoved` // option was set true if (refList.deleteRemoved) { @@ -182,7 +189,7 @@ function patchToEvent(segments, event, refList) { } if (type === 'remove') { - var removeIndex = event.index; + var removeIndex = event.index as number; var values = event.values; var howMany = values.length; for (var i = removeIndex, len = removeIndex + howMany; i < len; i++) { @@ -370,118 +377,143 @@ Model.prototype.refList = function() { return this.scope(fromPath); }; -function RefList(model, from, to, ids, options) { - this.model = model && model.pass({$refList: this}); - this.from = from; - this.to = to; - this.ids = ids; - this.fromSegments = from && from.split('.'); - this.toSegments = to && to.split('.'); - this.idsSegments = ids && ids.split('.'); - this.options = options; - this.deleteRemoved = options && options.deleteRemoved; -} - -// The default implementation assumes that the ids array is a flat list of -// keys on the to object. Ideally, this mapping could be customized via -// inheriting from RefList and overriding these methods without having to -// modify the above event handling code. -// -// In the default refList implementation, `key` and `id` are equal. -// -// Terms in the below methods: -// `item` - Object on the `to` path, which gets mirrored on the `from` path -// `key` - The property under `to` at which an item is located -// `id` - String or object in the array at the `ids` path -// `index` - The index of an id, which corresponds to an index on `from` -RefList.prototype.get = function() { - var ids = this.model._get(this.idsSegments); - if (!ids) return []; - var items = this.model._get(this.toSegments); - var out = []; - for (var i = 0; i < ids.length; i++) { - var key = ids[i]; - out.push(items && items[key]); +export class RefList{ + model: Model; + from: any; + to: any; + ids: string[]; + fromSegments: any; + toSegments: any; + idsSegments: any; + options: any; + deleteRemoved: boolean; + + constructor(model: Model, from, to, ids, options) { + this.model = model && model.pass({$refList: this}); + this.from = from; + this.to = to; + this.ids = ids; + this.fromSegments = from && from.split('.'); + this.toSegments = to && to.split('.'); + this.idsSegments = ids && ids.split('.'); + this.options = options; + this.deleteRemoved = options && options.deleteRemoved; } - return out; -}; -RefList.prototype.dereference = function(segments, i) { - var remaining = segments.slice(i + 1); - var key = this.idByIndex(remaining[0]); - if (key == null) return []; - remaining[0] = key; - return this.toSegments.concat(remaining); -}; -RefList.prototype.toSegmentsByItem = function(item) { - var key = this.idByItem(item); - if (key === undefined) return; - return this.toSegments.concat(key); -}; -RefList.prototype.idByItem = function(item) { - if (item && item.id) return item.id; - var items = this.model._get(this.toSegments); - for (var key in items) { - if (item === items[key]) return key; - } -}; -RefList.prototype.indicesByItem = function(item) { - var id = this.idByItem(item); - var ids = this.model._get(this.idsSegments); - if (!ids) return; - var indices; - var index = -1; - for (;;) { - index = ids.indexOf(id, index + 1); - if (index === -1) break; - if (indices) { - indices.push(index); - } else { - indices = [index]; + + // The default implementation assumes that the ids array is a flat list of + // keys on the to object. Ideally, this mapping could be customized via + // inheriting from RefList and overriding these methods without having to + // modify the above event handling code. + // + // In the default refList implementation, `key` and `id` are equal. + // + // Terms in the below methods: + // `item` - Object on the `to` path, which gets mirrored on the `from` path + // `key` - The property under `to` at which an item is located + // `id` - String or object in the array at the `ids` path + // `index` - The index of an id, which corresponds to an index on `from` + get() { + var ids = this.model._get(this.idsSegments); + if (!ids) return []; + var items = this.model._get(this.toSegments); + var out = []; + for (var i = 0; i < ids.length; i++) { + var key = ids[i]; + out.push(items && items[key]); } - } - return indices; -}; -RefList.prototype.itemById = function(id) { - return this.model._get(this.toSegments.concat(id)); -}; -RefList.prototype.idByIndex = function(index) { - return this.model._get(this.idsSegments.concat(index)); -}; + return out; + }; -function RefLists() { - this.fromMap = new EventMapTree(); - var toListeners = this.toListeners = new EventListenerTree(); - var idsListeners = this.idsListeners = new EventListenerTree(); - this._removeInputListeners = function(refList) { - toListeners.removeListener(refList.toSegments, refList); - idsListeners.removeListener(refList.idsSegments, refList); + dereference(segments, i) { + var remaining = segments.slice(i + 1); + var key = this.idByIndex(remaining[0]); + if (key == null) return []; + remaining[0] = key; + return this.toSegments.concat(remaining); }; -} -RefLists.prototype.add = function(refList) { - this.fromMap.setListener(refList.fromSegments, refList); - this.toListeners.addListener(refList.toSegments, refList); - this.idsListeners.addListener(refList.idsSegments, refList); -}; + toSegmentsByItem(item) { + var key = this.idByItem(item); + if (key === undefined) return; + return this.toSegments.concat(key); + }; -RefLists.prototype.remove = function(fromSegments) { - var refList = this.fromMap.deleteListener(fromSegments); - if (!refList) return; - this.toListeners.removeListener(refList.toSegments, refList); - this.idsListeners.removeListener(refList.idsSegments, refList); -}; + idByItem(item) { + if (item && item.id) return item.id; + var items = this.model._get(this.toSegments); + for (var key in items) { + if (item === items[key]) return key; + } + }; + + indicesByItem(item) { + var id = this.idByItem(item); + var ids = this.model._get(this.idsSegments); + if (!ids) return; + var indices; + var index = -1; + for (;;) { + index = ids.indexOf(id, index + 1); + if (index === -1) break; + if (indices) { + indices.push(index); + } else { + indices = [index]; + } + } + return indices; + }; + + itemById(id: string) { + return this.model._get(this.toSegments.concat(id)); + }; + + idByIndex(index: number) { + return this.model._get(this.idsSegments.concat(index)); + }; +} + +export class RefLists{ + fromMap: EventMapTree; + toListeners: EventListenerTree; + idsListeners: EventListenerTree; -RefLists.prototype.removeAll = function(segments) { - var node = this.fromMap.deleteAllListeners(segments); - if (node) { - node.forEach(this._removeInputListeners); + constructor() { + this.fromMap = new EventMapTree(); + var toListeners = this.toListeners = new EventListenerTree(); + var idsListeners = this.idsListeners = new EventListenerTree(); } -}; -RefLists.prototype.toJSON = function() { - var out = []; - this.fromMap.forEach(function(refList) { - out.push([refList.from, refList.to, refList.ids, refList.options]); - }); - return out; -}; + add(refList) { + this.fromMap.setListener(refList.fromSegments, refList); + this.toListeners.addListener(refList.toSegments, refList); + this.idsListeners.addListener(refList.idsSegments, refList); + }; + + remove(fromSegments) { + var refList = this.fromMap.deleteListener(fromSegments); + if (!refList) return; + this.toListeners.removeListener(refList.toSegments, refList); + this.idsListeners.removeListener(refList.idsSegments, refList); + }; + + removeAll(segments) { + var node = this.fromMap.deleteAllListeners(segments); + if (node) { + node.forEach(this._removeInputListeners); + } + }; + + toJSONn() { + var out = []; + this.fromMap.forEach(function(refList) { + out.push([refList.from, refList.to, refList.ids, refList.options]); + }); + return out; + }; + + _removeInputListeners(refList) { + this.toListeners.removeListener(refList.toSegments, refList); + this.idsListeners.removeListener(refList.idsSegments, refList); + }; +} diff --git a/src/Model/setDiff.ts b/src/Model/setDiff.ts index 35d792968..68e7455a8 100644 --- a/src/Model/setDiff.ts +++ b/src/Model/setDiff.ts @@ -1,5 +1,5 @@ var util = require('../util'); -var Model = require('./Model'); +import { Model } from './Model'; var arrayDiff = require('arraydiff'); var mutationEvents = require('./events').mutationEvents; var ChangeEvent = mutationEvents.ChangeEvent; @@ -8,6 +8,32 @@ var RemoveEvent = mutationEvents.RemoveEvent; var MoveEvent = mutationEvents.MoveEvent; var promisify = util.promisify; +declare module './Model' { + interface Model { + setDiff(value: any); + setDiff(subpath: string, value: any, cb?: (err: Error) => void): void; + setDiffPromised(value: any): Promise; + setDiffPromised(subpath: string, value: any): Promise; + _setDiff(segments: Segments, value: any, cb: (err: Error) => void): void; + setDiffDeep(value: any): void; + setDiffDeep(subpath: string, value: any, cb?: (err: Error) => void): void; + setDiffDeepPromised(value: any): Promise; + setDiffDeepPromised(subpathj: string, valiue: any): Promise; + _setDiffDeep(segments: Segments, value: any, cb?: (err: Error) => void): void; + setArrayDiff(value: any): void; + setArrayDiff(subpath: string, value: any, cb?: (err: Error) => void): void; + setArrayDiffPromised(value: any): Promise; + setArrayDiffPromised(subpath: string, value: any): Promise; + setArrayDiffDeep(value: any): void; + setArrayDiffDeep(subpath: string, value: any, cb?: (err: Error) => void): void; + setArrayDiffDeepPromised(value: any): Promise; + setArrayDiffDeepPromised(subpath: string, value: any): Promise; + _setArrayDiffDeep(segments: Segments, value: any, cb?: (err: Error) => void): void; + _setArrayDiff(segments: Segments, value: any, cb?: (err: Error) => void, equalFn?: any): void; + _applyArrayDiff(segments: Segments, diff: any, cb?: (err: Error) => void): void; + } +} + Model.prototype.setDiff = function() { var subpath, value, cb; if (arguments.length === 1) { diff --git a/src/Model/subscriptions.ts b/src/Model/subscriptions.ts index e9004d2c5..c89e46559 100644 --- a/src/Model/subscriptions.ts +++ b/src/Model/subscriptions.ts @@ -1,11 +1,40 @@ var util = require('../util'); -var Model = require('./Model'); +import { CollectionName } from 'sharedb/lib/sharedb'; +import { Model } from './Model'; var Query = require('./Query'); var CollectionCounter = require('./CollectionCounter'); var mutationEvents = require('./events').mutationEvents; var UnloadEvent = mutationEvents.UnloadEvent; var promisify = util.promisify; +declare module './Model' { + interface Model { + fetch(): Model; + fetchPromised(): Promise; + unfetch(): Model; + unfetchPromised(): Promise; + subscribe(): void; + subscribePromised(): Promise; + unsubscribe(): Model; + unsubscribePromised(): Promise; + _forSubscribable(argumentsObject: any, method: any): void; + fetchDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; + fetchDocPromised(collecitonName: string, id: string): Promise; + subscribeDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; + subscribeDocPromised(collecitonName: string, id: string): Promise; + unfetchDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; + unfetchDocPromised(collecitonName: string, id: string): Promise; + unsubscribeDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; + unsubscribeDocPromised(collecitonName: string, id: string): Promise; + _maybeUnloadDoc(collecitonName: string, id: string): void; + _hasDocReferences(collecitonName: string, id: string): boolean; + fetchOnly: boolean; + unloadDelay: number; + _fetchedDocs: typeof CollectionCounter; + _subscribedDocs: typeof CollectionCounter; + } +} + Model.INITS.push(function(model, options) { model.root.fetchOnly = options.fetchOnly; model.root.unloadDelay = options.unloadDelay || (util.isServer) ? 0 : 1000; @@ -171,7 +200,7 @@ Model.prototype.unsubscribeDoc = function(collectionName, id, cb) { shareDoc.unsubscribe(unsubscribeDocCallback); } } - function unsubscribeDocCallback(err) { + function unsubscribeDocCallback(err?: Error) { model._maybeUnloadDoc(collectionName, id); if (err) return cb(err); cb(null, 0); From e6b5736a863ec05abb6bdfd5d283215ef4376369 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 21 Jul 2023 14:21:13 -0700 Subject: [PATCH 304/479] Fix return type on removeListener --- src/Model/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index c87bc773e..727e301cf 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -23,7 +23,7 @@ declare module './Model' { __once: typeof EventEmitter.prototype.once; once(event: string, listener: any, arg2?: any, arg3?: any): any; __removeListener: typeof EventEmitter.prototype.removeListener; - removeListener(type: string, listener: any): void; + removeListener(type: string, listener: any): this; __removeAllListeners: typeof EventEmitter.prototype.removeAllListeners; removeAllListeners(type: string, subpath: string): void; _removeAllListeners(type: string, segments: Segments): void; From 31e997dbac46f409d96d927532ee02256371fc24 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 21 Jul 2023 14:33:07 -0700 Subject: [PATCH 305/479] Do not extend EventEmitter as overridden methods have different signatures --- src/Model/Model.ts | 4 +--- src/Model/events.ts | 7 ++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Model/Model.ts b/src/Model/Model.ts index 85856c7a8..1615ce114 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -22,7 +22,7 @@ declare module './Model' { type ModelInitFunction = (instance: Model, options: ModelOptions) => void; -export class Model extends EventEmitter { +export class Model { static INITS: ModelInitFunction[] = []; ChildModel = ChildModel; @@ -39,8 +39,6 @@ export class Model extends EventEmitter { _silent: boolean; constructor(options: ModelOptions = {}) { - super(); - this.root = this; var inits = Model.INITS; this.debug = options.debug || {}; diff --git a/src/Model/events.ts b/src/Model/events.ts index 727e301cf..b3e162fab 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -23,7 +23,7 @@ declare module './Model' { __once: typeof EventEmitter.prototype.once; once(event: string, listener: any, arg2?: any, arg3?: any): any; __removeListener: typeof EventEmitter.prototype.removeListener; - removeListener(type: string, listener: any): this; + removeListener(type: string, listener: any): void; __removeAllListeners: typeof EventEmitter.prototype.removeAllListeners; removeAllListeners(type: string, subpath: string): void; _removeAllListeners(type: string, segments: Segments): void; @@ -33,12 +33,13 @@ declare module './Model' { removeContextListeners(): void; _removeMutationListener(listener: MutationListener): void; _addMutationListener(type: string, arg1: any, arg2: any, arg3: any): MutationListener; + setMaxListeners(limit: number): void; } } Model.INITS.push(function (model: Model) { var root = model.root; - // EventEmitter.call(root); + EventEmitter.call(root); // Set max listeners to unlimited model.setMaxListeners(0); @@ -67,7 +68,7 @@ Model.INITS.push(function (model: Model) { root._eventContext = null; }); -// mergeInto(Model.prototype, EventEmitter.prototype); +mergeInto(Model.prototype, EventEmitter.prototype); Model.prototype.wrapCallback = function (cb) { if (!cb) return this.root._defaultCallback; From 39d7805e887e4a19666725f59e34a6f68bdadbcd Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 21 Jul 2023 14:40:13 -0700 Subject: [PATCH 306/479] Resolve _getDocConstructor return error --- src/Model/collections.ts | 3 +-- src/Model/connection.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Model/collections.ts b/src/Model/collections.ts index 42744ca91..7e8a11a83 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -24,7 +24,6 @@ declare module './Model' { getDeepCopy(subpath: string): any; _getDeepCopy(segments: Segments): any; getOrCreateCollection(name: string): Collection; - _getDocConstructor(): DocConstructor; getOrCreateDoc(collectionName: string, id: string, data: any); destroy(subpath: string): void; } @@ -82,7 +81,7 @@ Model.prototype.getOrCreateCollection = function(name) { return collection; }; -Model.prototype._getDocConstructor = function() { +Model.prototype._getDocConstructor = function(name: string) { // Only create local documents. This is overriden in ./connection.js, so that // the RemoteDoc behavior can be selectively included return LocalDoc; diff --git a/src/Model/connection.ts b/src/Model/connection.ts index f11677c9b..b659634e3 100644 --- a/src/Model/connection.ts +++ b/src/Model/connection.ts @@ -7,11 +7,11 @@ var promisify = require('../util').promisify; declare module './Model' { interface DocConstructor { - new (model: Model, collectionName: string, id: string, data: any): DocConstructor; + new (any: unknown[]): DocConstructor; } interface Model { _finishCreateConnection(): void; - _getDocConstructor(name: string): DocConstructor; + _getDocConstructor(name: string): any; _isLocal(name: string): boolean; allowCompose(): Model; close(cb: (err?: Error) => void): void; From b4eef54df6d02f3ae669215a22c7788af49c1d3b Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 21 Jul 2023 16:09:20 -0700 Subject: [PATCH 307/479] Fixes for promised returns and change requires to imports --- src/Backend.ts | 22 ++++++++-------- src/Model/CollectionCounter.ts | 1 + src/Model/Query.ts | 26 +++++++++---------- src/Model/connection.server.ts | 2 +- src/Model/events.ts | 46 +++++++++++++++++----------------- src/Model/subscriptions.ts | 35 +++++++++++++------------- src/Racer.ts | 9 +++---- 7 files changed, 70 insertions(+), 71 deletions(-) diff --git a/src/Backend.ts b/src/Backend.ts index ca8e35efe..e9548346b 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -1,7 +1,7 @@ -var path = require('path'); +import { Model } from './Model'; +import * as path from 'path'; +import * as util from './util'; var Backend = require('sharedb').Backend; -var Model = require('./Model'); -var util = require('./util'); export class RacerBackend extends Backend { racer: any; @@ -11,9 +11,9 @@ export class RacerBackend extends Backend { super(options); this.racer = racer; this.modelOptions = options && options.modelOptions; - this.on('bundle', function(browserify) { + this.on('bundle', function (browserify) { var racerPath = path.join(__dirname, 'index.js'); - browserify.require(racerPath, {expose: 'racer'}); + browserify.require(racerPath, { expose: 'racer' }); }); } @@ -34,15 +34,15 @@ export class RacerBackend extends Backend { function modelMiddleware(req, res, next) { // Do not add a model to the request if one has been added already if (req.model) return next(); - + // Create a new model for this request - req.model = backend.createModel({fetchOnly: true}, req); + req.model = backend.createModel({ fetchOnly: true }, req); // DEPRECATED: - req.getModel = function() { + req.getModel = function () { console.warn('Warning: req.getModel() is deprecated. Please use req.model instead.'); return req.model; }; - + // Close the model when this request ends function closeModel() { res.removeListener('finish', closeModel); @@ -53,11 +53,11 @@ export class RacerBackend extends Backend { } res.on('finish', closeModel); res.on('close', closeModel); - + next(); } return modelMiddleware; }; } -function getModelUndefined() {} +function getModelUndefined() { } diff --git a/src/Model/CollectionCounter.ts b/src/Model/CollectionCounter.ts index 72e0f4cbe..6db724810 100644 --- a/src/Model/CollectionCounter.ts +++ b/src/Model/CollectionCounter.ts @@ -1,6 +1,7 @@ export class CollectionCounter { collections: Record; size: number; + constructor() { this.reset(); } diff --git a/src/Model/Query.ts b/src/Model/Query.ts index 9c0963bed..015f6e1da 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -15,11 +15,11 @@ declare module './Model' { } } -Model.INITS.push(function (model) { +Model.INITS.push(function(model) { model.root._queries = new Queries(); }); -Model.prototype.query = function (collectionName, expression, options) { +Model.prototype.query = function(collectionName, expression, options) { // DEPRECATED: Passing in a string as the third argument specifies the db // option for backward compatibility if (typeof options === 'string') { @@ -39,7 +39,7 @@ Model.prototype.query = function (collectionName, expression, options) { * @param {new (model: Model, collectionName: string, expression: any, options: any) => Query} QueryConstructor - * constructor function for a Query, to create one if not already present on this model */ -Model.prototype._getOrCreateQuery = function (collectionName, expression, options, QueryConstructor) { +Model.prototype._getOrCreateQuery = function(collectionName, expression, options, QueryConstructor) { expression = this.sanitizeQuery(expression); var contextId = this._context.id; var query = this.root._queries.get(contextId, collectionName, expression, options); @@ -55,7 +55,7 @@ Model.prototype._getOrCreateQuery = function (collectionName, expression, option // filter and absence means that all values are accepted. We aren't checking // for cycles, which aren't allowed in JSON, so this could throw a max call // stack error -Model.prototype.sanitizeQuery = function (expression) { +Model.prototype.sanitizeQuery = function(expression) { if (expression && typeof expression === 'object') { for (var key in expression) { if (expression.hasOwnProperty(key)) { @@ -72,7 +72,7 @@ Model.prototype.sanitizeQuery = function (expression) { }; // Called during initialization of the bundle on page load. -Model.prototype._initQueries = function (items) { +Model.prototype._initQueries = function(items) { for (var i = 0; i < items.length; i++) { var item = items[i]; var countsList = item[0]; @@ -158,7 +158,7 @@ export class Queries { }; } -class Query { +export class Query { model: Model; context: Context; collectionName: string; @@ -259,7 +259,7 @@ class Query { if (this.subscribeCount++) { var query = this; - process.nextTick(function () { + process.nextTick(function() { var data = query.model._get(query.segments); if (data) { cb(); @@ -320,23 +320,23 @@ class Query { options, this._subscribeCb(cb) ); - this.shareQuery.on('insert', function (shareDocs, index) { + this.shareQuery.on('insert', function(shareDocs, index) { var ids = resultsIds(shareDocs); query._addMapIds(ids); query.model._insert(query.idsSegments, index, ids); }); - this.shareQuery.on('remove', function (shareDocs, index) { + this.shareQuery.on('remove', function(shareDocs, index) { var ids = resultsIds(shareDocs); query._removeMapIds(ids); query.model._remove(query.idsSegments, index, shareDocs.length); }); - this.shareQuery.on('move', function (shareDocs, from, to) { + this.shareQuery.on('move', function(shareDocs, from, to) { query.model._move(query.idsSegments, from, to, shareDocs.length); }); - this.shareQuery.on('extra', function (extra) { + this.shareQuery.on('extra', function(extra) { query.model._setDiffDeep(query.extraSegments, extra); }); - this.shareQuery.on('error', function (err) { + this.shareQuery.on('error', function(err) { query.model._emitError(err, query.hash); }); }; @@ -357,7 +357,7 @@ class Query { // work to avoid thrashing subscribe/unsubscribe in expected cases if (this.model.root.unloadDelay) { var query = this; - setTimeout(function () { + setTimeout(function() { query._maybeUnloadDocs(ids); }, this.model.root.unloadDelay); return; diff --git a/src/Model/connection.server.ts b/src/Model/connection.server.ts index d9f3c50c4..f85afbb2a 100644 --- a/src/Model/connection.server.ts +++ b/src/Model/connection.server.ts @@ -2,7 +2,7 @@ import { Model } from './Model'; declare module './Model' { interface Model { - createConnection(backend: any, req: any): void; + createConnection(backend: any, req?: any): void; connect(): void; connection: any; } diff --git a/src/Model/events.ts b/src/Model/events.ts index b3e162fab..07e0075d5 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -10,7 +10,7 @@ declare module './Model' { interface Model { _defaultCallback(err?: Error): void; _emitError(err: Error, context?: any): void; - wrapCallback(cb: ErrorCallback); + wrapCallback(cb: ErrorCallback): ErrorCallback; _mutationListeners: Record; _emittingMutation: boolean; _mutationEventQueue: null; @@ -37,7 +37,7 @@ declare module './Model' { } } -Model.INITS.push(function (model: Model) { +Model.INITS.push(function(model: Model) { var root = model.root; EventEmitter.call(root); @@ -70,7 +70,7 @@ Model.INITS.push(function (model: Model) { mergeInto(Model.prototype, EventEmitter.prototype); -Model.prototype.wrapCallback = function (cb) { +Model.prototype.wrapCallback = function(cb) { if (!cb) return this.root._defaultCallback; var model = this; return function wrappedCallback() { @@ -82,7 +82,7 @@ Model.prototype.wrapCallback = function (cb) { }; }; -Model.prototype._emitError = function (err, context) { +Model.prototype._emitError = function(err, context) { var message = (err.message) ? err.message : (typeof err === 'string') ? err : 'Unknown model error'; @@ -104,7 +104,7 @@ Model.prototype._emitError = function (err, context) { this.emit('error', err); }; -Model.prototype._emitMutation = function (segments, event) { +Model.prototype._emitMutation = function(segments, event) { if (this._silent) return; var root = this.root; this._callMutationListeners(event._immediateType, segments, event); @@ -141,7 +141,7 @@ Model.prototype._emitMutation = function (segments, event) { root._emittingMutation = false; }; -Model.prototype._callMutationListeners = function (type, segments, event) { +Model.prototype._callMutationListeners = function(type, segments, event) { var tree = this.root._mutationListeners[type]; var listeners = tree.getWildcardListeners(segments); for (var i = 0, len = listeners.length; i < len; i++) { @@ -157,7 +157,7 @@ Model.prototype._callMutationListeners = function (type, segments, event) { Model.prototype.__on = EventEmitter.prototype.on; Model.prototype.addListener = - Model.prototype.on = function (type, arg1, arg2, arg3) { + Model.prototype.on = function(type, arg1, arg2, arg3) { var listener = this._addMutationListener(type, arg1, arg2, arg3); if (listener) { return listener; @@ -168,7 +168,7 @@ Model.prototype.addListener = }; Model.prototype.__once = EventEmitter.prototype.once; -Model.prototype.once = function (type, arg1, arg2, arg3) { +Model.prototype.once = function(type, arg1, arg2, arg3) { var listener = this._addMutationListener(type, arg1, arg2, arg3); if (listener) { onceWrapListener(this, listener); @@ -188,7 +188,7 @@ function onceWrapListener(model, listener) { } Model.prototype.__removeListener = EventEmitter.prototype.removeListener; -Model.prototype.removeListener = function (type, listener) { +Model.prototype.removeListener = function(type, listener) { if (this.root._mutationListeners[type]) { this._removeMutationListener(listener); return; @@ -198,11 +198,11 @@ Model.prototype.removeListener = function (type, listener) { }; Model.prototype.__removeAllListeners = EventEmitter.prototype.removeAllListeners; -Model.prototype.removeAllListeners = function (type, subpath) { +Model.prototype.removeAllListeners = function(type, subpath) { var segments = this._splitPath(subpath); this._removeAllListeners(type, segments); }; -Model.prototype._removeAllListeners = function (type, segments) { +Model.prototype._removeAllListeners = function(type, segments) { var mutationListeners = this.root._mutationListeners; if (type == null) { for (var key in mutationListeners) { @@ -223,14 +223,14 @@ Model.prototype._removeAllListeners = function (type, segments) { export class Passed { } Model.prototype.pass = (Object.assign) ? - function (object, invert) { + function(object, invert) { var model = this._child(); model._pass = (invert) ? Object.assign(new Passed(), object, this._pass) : Object.assign(new Passed(), this._pass, object); return model; } : - function (object, invert) { + function(object, invert) { var model = this._child(); var pass = new Passed(); if (invert) { @@ -250,19 +250,19 @@ Model.prototype.pass = (Object.assign) ? * @param {Boolean|Null} value defaults to true * @return {Model} */ -Model.prototype.silent = function (value) { +Model.prototype.silent = function(value) { var model = this._child(); model._silent = (value == null) ? true : !!value; return model; }; -Model.prototype.eventContext = function (id) { +Model.prototype.eventContext = function(id) { var model = this._child(); model._eventContext = id; return model; }; -Model.prototype.removeContextListeners = function () { +Model.prototype.removeContextListeners = function() { var id = this._eventContext; if (id == null) return; var map = this.root._eventContextListeners; @@ -275,7 +275,7 @@ Model.prototype.removeContextListeners = function () { } }; -Model.prototype._removeMutationListener = function (listener) { +Model.prototype._removeMutationListener = function(listener) { listener.node.removeOwnListener(listener); var id = this._eventContext; if (id == null) return; @@ -292,7 +292,7 @@ Model.prototype._removeMutationListener = function (listener) { } }; -Model.prototype._addMutationListener = function (type, arg1, arg2, arg3) { +Model.prototype._addMutationListener = function(type, arg1, arg2, arg3) { var tree = this.root._mutationListeners[type]; if (!tree) return; // Create double-linked listener and tree node for later removal @@ -404,7 +404,7 @@ function createMutationListener(pattern, eventContext, cb) { var patternSegments = pattern.split('.'); var fn; if (patternSegments.length === 1 && patternSegments[0] === '**') { - fn = function (segments, event) { + fn = function(segments, event) { var captures = [segments.join('.')]; cb(event, captures); }; @@ -426,12 +426,12 @@ function createMutationListener(pattern, eventContext, cb) { } } if (captureIndicies || remainingIndex != null) { - fn = function (segments, event) { + fn = function(segments, event) { var captures = createCaptures(captureIndicies, remainingIndex, segments); cb(event, captures); }; } else { - fn = function (segments, event) { + fn = function(segments, event) { cb(event, []); }; } @@ -441,11 +441,11 @@ function createMutationListener(pattern, eventContext, cb) { function createMutationListenerLegacy(type, pattern, eventContext, cb) { var mutationListenerAdapter = (type === 'all') ? - function (event, captures) { + function(event, captures) { var args = captures.concat(event.type, event._getArgs()); cb.apply(null, args); } : - function (event, captures) { + function(event, captures) { var args = captures.concat(event._getArgs()); cb.apply(null, args); }; diff --git a/src/Model/subscriptions.ts b/src/Model/subscriptions.ts index c89e46559..847fe8f60 100644 --- a/src/Model/subscriptions.ts +++ b/src/Model/subscriptions.ts @@ -1,37 +1,36 @@ -var util = require('../util'); -import { CollectionName } from 'sharedb/lib/sharedb'; import { Model } from './Model'; -var Query = require('./Query'); -var CollectionCounter = require('./CollectionCounter'); -var mutationEvents = require('./events').mutationEvents; -var UnloadEvent = mutationEvents.UnloadEvent; -var promisify = util.promisify; +import { CollectionCounter } from './CollectionCounter'; +import { mutationEvents } from './events'; +import { Query } from './Query'; +import * as util from '../util'; +const UnloadEvent = mutationEvents.UnloadEvent; +const promisify = util.promisify; declare module './Model' { interface Model { fetch(): Model; - fetchPromised(): Promise; + fetchPromised(): Promise; unfetch(): Model; - unfetchPromised(): Promise; + unfetchPromised(): Promise; subscribe(): void; - subscribePromised(): Promise; + subscribePromised(): Promise; unsubscribe(): Model; - unsubscribePromised(): Promise; - _forSubscribable(argumentsObject: any, method: any): void; + unsubscribePromised(): Promise; + _forSubscribable(argumentsObject: any, method: any): void; fetchDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; - fetchDocPromised(collecitonName: string, id: string): Promise; + fetchDocPromised(collecitonName: string, id: string): Promise; subscribeDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; - subscribeDocPromised(collecitonName: string, id: string): Promise; + subscribeDocPromised(collecitonName: string, id: string): Promise; unfetchDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; - unfetchDocPromised(collecitonName: string, id: string): Promise; + unfetchDocPromised(collecitonName: string, id: string): Promise; unsubscribeDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; - unsubscribeDocPromised(collecitonName: string, id: string): Promise; + unsubscribeDocPromised(collecitonName: string, id: string): Promise; _maybeUnloadDoc(collecitonName: string, id: string): void; _hasDocReferences(collecitonName: string, id: string): boolean; fetchOnly: boolean; unloadDelay: number; - _fetchedDocs: typeof CollectionCounter; - _subscribedDocs: typeof CollectionCounter; + _fetchedDocs: CollectionCounter; + _subscribedDocs: CollectionCounter; } } diff --git a/src/Racer.ts b/src/Racer.ts index f21fb21c8..6f3a73a78 100644 --- a/src/Racer.ts +++ b/src/Racer.ts @@ -1,10 +1,9 @@ -var EventEmitter = require('events').EventEmitter; -var Model = require('./Model'); -var util = require('./util'); - +import { EventEmitter } from 'events'; +import { Model } from './Model'; +import * as util from './util'; export class Racer extends EventEmitter { - Model: typeof Model = Model; + Model = Model; util = util; use = util.use; serverUse = util.serverUse; From d922deaefe81cbf6f15c9ca0649a6352e63c2a67 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 24 Jul 2023 15:37:21 -0700 Subject: [PATCH 308/479] Update requires for new exports --- src/Model/contexts.ts | 12 ++++++------ test/Model/CollectionCounter.js | 2 +- test/Model/EventListenerTree.js | 2 +- test/Model/EventMapTree.js | 2 +- test/Model/LocalDoc.js | 2 +- test/Model/filter.js | 2 +- test/Model/fn.js | 2 +- test/Model/path.js | 2 +- test/Model/query.js | 2 +- test/Model/ref.js | 2 +- test/Model/refList.js | 2 +- test/Model/setDiff.js | 2 +- 12 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Model/contexts.ts b/src/Model/contexts.ts index d9ef73b7d..150f41294 100644 --- a/src/Model/contexts.ts +++ b/src/Model/contexts.ts @@ -3,7 +3,7 @@ */ import { Model } from './Model'; -import { CollectionCounter} from './CollectionCounter'; +import { CollectionCounter } from './CollectionCounter'; declare module './Model' { interface Model { @@ -64,10 +64,10 @@ export class Contexts { }; } -class FetchedQueries {} -class SubscribedQueries {} +class FetchedQueries { } +class SubscribedQueries { } -export class Context{ +export class Context { model: Model; id: string; fetchedDocs: CollectionCounter; @@ -97,7 +97,7 @@ export class Context{ createdDocs: createdDocs }; }; - + fetchDoc(collectionName, id) { this.fetchedDocs.increment(collectionName, id); }; @@ -125,7 +125,7 @@ export class Context{ unsubscribeQuery(query) { mapDecrement(this.subscribedQueries, query.hash); }; - + unload() { var model = this.model; for (var hash in this.fetchedQueries) { diff --git a/test/Model/CollectionCounter.js b/test/Model/CollectionCounter.js index cdc0315cb..76e575e9e 100644 --- a/test/Model/CollectionCounter.js +++ b/test/Model/CollectionCounter.js @@ -1,5 +1,5 @@ var expect = require('../util').expect; -var CollectionCounter = require('../../lib/Model/CollectionCounter'); +var {CollectionCounter} = require('../../lib/Model/CollectionCounter'); describe('CollectionCounter', function() { describe('increment', function() { diff --git a/test/Model/EventListenerTree.js b/test/Model/EventListenerTree.js index a41d9bccb..5e1d66657 100644 --- a/test/Model/EventListenerTree.js +++ b/test/Model/EventListenerTree.js @@ -1,5 +1,5 @@ var expect = require('../util').expect; -var EventListenerTree = require('../../lib/Model/EventListenerTree'); +var {EventListenerTree} = require('../../lib/Model/EventListenerTree'); describe('EventListenerTree', function() { describe('addListener', function() { diff --git a/test/Model/EventMapTree.js b/test/Model/EventMapTree.js index bce33ce0e..02a60fe21 100644 --- a/test/Model/EventMapTree.js +++ b/test/Model/EventMapTree.js @@ -1,5 +1,5 @@ var expect = require('../util').expect; -var EventMapTree = require('../../lib/Model/EventMapTree'); +var {EventMapTree} = require('../../lib/Model/EventMapTree'); describe('EventMapTree', function() { describe('setListener', function() { diff --git a/test/Model/LocalDoc.js b/test/Model/LocalDoc.js index 704289bb7..3022da7b3 100644 --- a/test/Model/LocalDoc.js +++ b/test/Model/LocalDoc.js @@ -1,5 +1,5 @@ var expect = require('../util').expect; -var LocalDoc = require('../../lib/Model/LocalDoc'); +var {LocalDoc} = require('../../lib/Model/LocalDoc'); var docs = require('./docs'); describe('LocalDoc', function() { diff --git a/test/Model/filter.js b/test/Model/filter.js index 566cc0816..234168682 100644 --- a/test/Model/filter.js +++ b/test/Model/filter.js @@ -1,5 +1,5 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model'); +var Model = require('../../lib/Model').Model; describe('filter', function() { describe('getting', function() { diff --git a/test/Model/fn.js b/test/Model/fn.js index fb25079c4..a10790b37 100644 --- a/test/Model/fn.js +++ b/test/Model/fn.js @@ -1,5 +1,5 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model'); +var Model = require('../../lib/Model').Model; describe('fn', function() { describe('evaluate', function() { diff --git a/test/Model/path.js b/test/Model/path.js index fb94371f1..ea3953542 100644 --- a/test/Model/path.js +++ b/test/Model/path.js @@ -1,5 +1,5 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model'); +var Model = require('../../lib/Model').Model; describe('path methods', function() { describe('path', function() { diff --git a/test/Model/query.js b/test/Model/query.js index 31408a739..3d5e63592 100644 --- a/test/Model/query.js +++ b/test/Model/query.js @@ -1,6 +1,6 @@ var expect = require('../util').expect; var racer = require('../../lib'); -var Model = require('../../lib/Model'); +var Model = require('../../lib/Model').Model; describe('query', function() { describe('sanitizeQuery', function() { diff --git a/test/Model/ref.js b/test/Model/ref.js index 73bf49589..2156c3916 100644 --- a/test/Model/ref.js +++ b/test/Model/ref.js @@ -1,5 +1,5 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model'); +var Model = require('../../lib/Model').Model; describe('ref', function() { function expectEvents(pattern, model, done, events) { diff --git a/test/Model/refList.js b/test/Model/refList.js index 857d7f873..51f5fb1c2 100644 --- a/test/Model/refList.js +++ b/test/Model/refList.js @@ -1,5 +1,5 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model'); +var Model = require('../../lib/Model').Model; describe('refList', function() { function setup(options) { diff --git a/test/Model/setDiff.js b/test/Model/setDiff.js index 0abdc3d13..ea7d56d57 100644 --- a/test/Model/setDiff.js +++ b/test/Model/setDiff.js @@ -1,5 +1,5 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model'); +var Model = require('../../lib/Model').Model; ['setDiff', 'setDiffDeep', 'setArrayDiff', 'setArrayDiffDeep'].forEach(function(method) { describe(method + ' common diff functionality', function() { From adab97160cb6cc50c8160bf8c80f68f1bf62ec5d Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 24 Jul 2023 15:39:16 -0700 Subject: [PATCH 309/479] Use prototype for type and _immediateType as expected by inits --- src/Model/events.ts | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 07e0075d5..99ccb6f46 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -54,8 +54,8 @@ Model.INITS.push(function(model: Model) { var mutationListeners = { all: new EventListenerTree() }; - for (var name in exports.mutationEvents) { - var eventPrototype = exports.mutationEvents[name].prototype; + for (var name in mutationEvents) { + var eventPrototype = mutationEvents[name].prototype; mutationListeners[eventPrototype.type] = new EventListenerTree(); mutationListeners[eventPrototype._immediateType] = new EventListenerTree(); } @@ -142,7 +142,7 @@ Model.prototype._emitMutation = function(segments, event) { }; Model.prototype._callMutationListeners = function(type, segments, event) { - var tree = this.root._mutationListeners[type]; + var tree = this.root._mutationListeners[type]; var listeners = tree.getWildcardListeners(segments); for (var i = 0, len = listeners.length; i < len; i++) { var fn = listeners[i].fn; @@ -453,8 +453,8 @@ function createMutationListenerLegacy(type, pattern, eventContext, cb) { } class ChangeEvent { - type = 'change'; - _immediateType = 'changeImmediate'; + declare type: string; + declare _immediateType: string; value: any; previous: any; passed: any; @@ -473,10 +473,12 @@ class ChangeEvent { return [this.value, this.previous, this.passed]; }; } +ChangeEvent.prototype.type = 'change'; +ChangeEvent.prototype._immediateType = 'changeImmediate'; class LoadEvent { - type = 'load'; - _immediateType = 'loadImmediate'; + declare type: string; + declare _immediateType: string; value: any; document: any; passed: any; @@ -500,10 +502,12 @@ class LoadEvent { return [this.value, this.passed]; }; } +LoadEvent.prototype.type = 'load'; +LoadEvent.prototype._immediateType = 'load'; class UnloadEvent { - type = 'unload'; - _immediateType = 'unloadImmediate'; + declare type: string; + declare _immediateType: string; previous: any; previousDocument: any; passed: any; @@ -527,10 +531,12 @@ class UnloadEvent { return [this.previous, this.passed]; }; } +UnloadEvent.prototype.type = 'unload'; +UnloadEvent.prototype._immediateType = 'unloadImmediate'; class InsertEvent { - type = 'insert'; - _immediateType = 'insertImmediate'; + declare type: string; + declare _immediateType: string; index: number; values: any; passed: any; @@ -549,13 +555,15 @@ class InsertEvent { return [this.index, this.values, this.passed]; }; } +InsertEvent.prototype.type = 'insert'; +InsertEvent.prototype._immediateType = 'insertImmediate'; class RemoveEvent { - _immediateType = 'removeImmediate'; + declare type: string; + declare _immediateType: string; index: number; passed: any; removed: any; - type = 'remove'; values: any; constructor(index, values, passed) { @@ -578,14 +586,16 @@ class RemoveEvent { return [this.index, this.values, this.passed]; }; } +RemoveEvent.prototype.type = 'remove'; +RemoveEvent.prototype._immediateType = 'removeImmediate'; class MoveEvent { - _immediateType = 'moveImmediate'; + declare type: string; + declare _immediateType: string; from: any; howMany: number; passed: any; to: any; - type = 'move'; constructor(from, to, howMany, passed) { this.from = from; @@ -602,6 +612,8 @@ class MoveEvent { return [this.from, this.to, this.howMany, this.passed]; }; } +MoveEvent.prototype.type = 'move'; +MoveEvent.prototype._immediateType = 'moveImmediate'; // DEPRECATED: Normalize pattern ending in '**' to '.**', since these are // treated equivalently. The '.**' form is preferred, and it should be enforced @@ -626,5 +638,5 @@ export const mutationEvents = { UnloadEvent, InsertEvent, RemoveEvent, - MoveEvent + MoveEvent, }; \ No newline at end of file From 664d58c6787102213491c2658ef0def638747bdd Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 24 Jul 2023 16:09:48 -0700 Subject: [PATCH 310/479] Use exports and import defaultFns --- src/Model/defaultFns.ts | 14 ++++++-------- src/Model/filter.ts | 2 +- src/Model/fn.ts | 3 +-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Model/defaultFns.ts b/src/Model/defaultFns.ts index 3c06aeb4f..f749fdd0e 100644 --- a/src/Model/defaultFns.ts +++ b/src/Model/defaultFns.ts @@ -1,10 +1,4 @@ -var defaultFns = module.exports = new DefaultFns(); -defaultFns.reverse = new FnPair(getReverse, setReverse); -defaultFns.asc = asc; -defaultFns.desc = desc; - -function DefaultFns() {} function FnPair(get, set) { this.get = get; this.set = set; @@ -13,16 +7,20 @@ function FnPair(get, set) { function getReverse(array) { return array && array.slice().reverse(); } + function setReverse(values) { return {0: getReverse(values)}; } -function asc(a, b) { +export const reverse = new FnPair(getReverse, setReverse); + +export function asc(a, b) { if (a < b) return -1; if (a > b) return 1; return 0; } -function desc(a, b) { + +export function desc(a, b) { if (a > b) return -1; if (a < b) return 1; return 0; diff --git a/src/Model/filter.ts b/src/Model/filter.ts index e3516448f..51eabb836 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -1,6 +1,6 @@ var util = require('../util'); import { Model, type Segments } from './Model'; -var defaultFns = require('./defaultFns'); +import * as defaultFns from './defaultFns'; declare module './Model' { interface Model { diff --git a/src/Model/fn.ts b/src/Model/fn.ts index 8d88e75ce..c2862b779 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -1,7 +1,7 @@ import { Model, type Segments } from './Model'; import { EventListenerTree } from './EventListenerTree'; import { EventMapTree } from './EventMapTree'; -var defaultFns = require('./defaultFns'); +import * as defaultFns from './defaultFns'; var util = require('../util'); class NamedFns { } @@ -21,7 +21,6 @@ declare module './Model' { } } - Model.INITS.push(function (model) { var root = model.root; root._namedFns = new NamedFns(); From abc6285d4a439077f0e1b36b1d76f1c16135ccef Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 24 Jul 2023 16:10:50 -0700 Subject: [PATCH 311/479] Fix type in method name --- src/Model/refList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/refList.ts b/src/Model/refList.ts index 72765f790..de7097dcc 100644 --- a/src/Model/refList.ts +++ b/src/Model/refList.ts @@ -504,7 +504,7 @@ export class RefLists{ } }; - toJSONn() { + toJSON() { var out = []; this.fromMap.forEach(function(refList) { out.push([refList.from, refList.to, refList.ids, refList.options]); From 6bb3517533199d9e756da734e25548b2917b8838 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 24 Jul 2023 16:50:00 -0700 Subject: [PATCH 312/479] Fix handling of _removeInputListeners in foreach --- src/Model/fn.ts | 3 +-- src/Model/ref.ts | 2 +- src/Model/refList.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Model/fn.ts b/src/Model/fn.ts index c2862b779..6a5b448c4 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -17,7 +17,6 @@ declare module './Model' { _stop(segments: Segments): void; stopAll(subpath: string): void; _stopAll(segments: Segments): void; - } } @@ -177,7 +176,7 @@ class Fns { stopAll(segments: Segments) { var node = this.fromMap.deleteAllListeners(segments); if (node) { - node.forEach(this._removeInputListeners); + node.forEach(node => this._removeInputListeners(node)); } }; diff --git a/src/Model/ref.ts b/src/Model/ref.ts index 60b42f124..b8eeb602d 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -317,7 +317,7 @@ export class Refs { removeAll(segments) { var node = this.fromMap.deleteAllListeners(segments); if (node) { - node.forEach(this._removeInputListeners); + node.forEach(node => this._removeInputListeners(node)); } }; diff --git a/src/Model/refList.ts b/src/Model/refList.ts index de7097dcc..74e7d2cc0 100644 --- a/src/Model/refList.ts +++ b/src/Model/refList.ts @@ -500,7 +500,7 @@ export class RefLists{ removeAll(segments) { var node = this.fromMap.deleteAllListeners(segments); if (node) { - node.forEach(this._removeInputListeners); + node.forEach(node => this._removeInputListeners(node)); } }; From 86858957b9176de51b1a1de6b703bb2473b93358 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 24 Jul 2023 16:59:08 -0700 Subject: [PATCH 313/479] Fix inccorect immediate event type --- src/Model/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 99ccb6f46..1ec9c9135 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -503,7 +503,7 @@ class LoadEvent { }; } LoadEvent.prototype.type = 'load'; -LoadEvent.prototype._immediateType = 'load'; +LoadEvent.prototype._immediateType = 'loadImmediate'; class UnloadEvent { declare type: string; From 9deebf30c24a9081932720a5196db50c208e0d89 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 19 Oct 2023 11:45:17 -0700 Subject: [PATCH 314/479] Remove unused import --- src/Model/Model.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Model/Model.ts b/src/Model/Model.ts index 1615ce114..a4a9e812a 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -1,5 +1,4 @@ import { v4 as uuidv4 } from 'uuid'; -import { EventEmitter } from 'events'; import { Context } from 'vm'; declare module './Model' { From dda1a149335d2159de27aadd7075a9e7204e5045 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 19 Oct 2023 11:46:01 -0700 Subject: [PATCH 315/479] Add methods return id in callback --- src/Model/mutators.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index e0eb56137..460ade137 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -40,9 +40,9 @@ declare module './Model' { add(value: any): void; add(subpath: string, value: any, cb?: ErrorCallback): void; - addPromised(value: any): Promise; - addPromised(subpath: string, value: any): Promise; - _add(segments: Segments, value: any, cb?: ErrorCallback): void; + addPromised(value: any): Promise; + addPromised(subpath: string, value: any): Promise; + _add(segments: Segments, value: any, cb?: (err?: Error, id?: string) => void): void; del(value: any): void; del(subpath: string, value: any, cb?: ErrorCallback): void; @@ -381,7 +381,7 @@ Model.prototype._add = function(segments, value, cb) { var event = new ChangeEvent(value, previous, model._pass); model._emitMutation(segments, event); } - this._mutate(segments, add, cb); + this._mutate(segments, add, (err) => { cb(err, id) }); return id; }; From e4dc61c181ac5d99cd928904fadc90be867f86d9 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 23 Oct 2023 16:36:59 -0700 Subject: [PATCH 316/479] Revert "Add methods return id in callback" This reverts commit dda1a149335d2159de27aadd7075a9e7204e5045. --- src/Model/mutators.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 460ade137..e0eb56137 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -40,9 +40,9 @@ declare module './Model' { add(value: any): void; add(subpath: string, value: any, cb?: ErrorCallback): void; - addPromised(value: any): Promise; - addPromised(subpath: string, value: any): Promise; - _add(segments: Segments, value: any, cb?: (err?: Error, id?: string) => void): void; + addPromised(value: any): Promise; + addPromised(subpath: string, value: any): Promise; + _add(segments: Segments, value: any, cb?: ErrorCallback): void; del(value: any): void; del(subpath: string, value: any, cb?: ErrorCallback): void; @@ -381,7 +381,7 @@ Model.prototype._add = function(segments, value, cb) { var event = new ChangeEvent(value, previous, model._pass); model._emitMutation(segments, event); } - this._mutate(segments, add, (err) => { cb(err, id) }); + this._mutate(segments, add, cb); return id; }; From da0beb4d365ca4b3d8fb62e38bca689241f18a5f Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 11 Dec 2023 16:41:55 -0800 Subject: [PATCH 317/479] Testing with exported anmespace and types --- package.json | 2 +- src/index.ts | 29 ++++++++++++++++++++++++++++- tsconfig.json | 4 +++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c6e572f74..e23d2be87 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "version": "1.1.0", "main": "./lib/index.js", "files": [ - "lib/**/*.js" + "lib/" ], "scripts": { "build": "node_modules/.bin/tsc", diff --git a/src/index.ts b/src/index.ts index 059786260..f6460b789 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,29 @@ import { Racer } from './Racer'; -module.exports = new Racer(); +import { Model } from './Model'; +import * as util from './util'; +import { RacerBackend } from './Backend'; +import { Query } from './Model/Query'; + +// module.exports = new Racer(); +const { use, serverUse } = util; + +export { Model }; +export { Query }; +export { Racer }; +export { RacerBackend }; +export { use, serverUse }; +export { util }; +export const racer = new Racer(); + +export function createModel(data) { + var model = new Model(); + if (data) { + model.createConnection(data); + model.unbundle(data); + } + return model; +} + +export function createBackend(options) { + return new RacerBackend(racer, options); +}; diff --git a/tsconfig.json b/tsconfig.json index f1eab985b..f75922da7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,8 +6,10 @@ "module": "CommonJS", "noImplicitUseStrict": true, "outDir": "lib", + "target": "ES5", "sourceMap": false, - "target": "ES5" + "declaration": true, + "declarationMap": false, }, "include": [ "src/**/*" From 1329fedf156ebd0ac6af64a98d34df3cf23af3d1 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 12 Dec 2023 11:03:46 -0800 Subject: [PATCH 318/479] Use scoped package name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e23d2be87..c0f8926b5 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "racer", + "name": "@derbyjs/racer", "description": "Realtime model synchronization engine for Node.js", "homepage": "https://github.com/derbyjs/racer", "repository": { From d39ed3be70a2377e43b923044fb6d30fa61ecfa5 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 14 Dec 2023 13:55:31 -0800 Subject: [PATCH 319/479] Drop node 14 support --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 53920617f..f08e29773 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,6 @@ jobs: strategy: matrix: node: - - 14 - 16 - 18 - 20 From 20a75be429386800e9840e0b7bba2bc263a4e052 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 21 Dec 2023 14:43:38 -0800 Subject: [PATCH 320/479] Resolve Segment types --- src/Model/Doc.ts | 5 +++-- src/Model/EventListenerTree.ts | 2 +- src/Model/EventMapTree.ts | 2 +- src/Model/Model.ts | 1 - src/Model/Query.ts | 4 +++- src/Model/collections.ts | 1 + src/Model/events.ts | 5 +++-- src/Model/filter.ts | 9 +++++---- src/Model/fn.ts | 3 ++- src/Model/index.ts | 1 + src/Model/mutators.ts | 3 +++ src/Model/paths.ts | 2 +- src/Model/ref.ts | 21 +++++++++++++-------- src/Model/setDiff.ts | 1 + src/Model/types.ts | 17 +++++++++++++++++ src/index.ts | 2 ++ 16 files changed, 57 insertions(+), 22 deletions(-) create mode 100644 src/Model/types.ts diff --git a/src/Model/Doc.ts b/src/Model/Doc.ts index c3f56092b..885e7a791 100644 --- a/src/Model/Doc.ts +++ b/src/Model/Doc.ts @@ -1,4 +1,5 @@ -import { type Model, type Segments } from './Model'; +import { type Model } from './Model'; +import { type Segments } from './types'; import { Collection } from './collections'; export class Doc { @@ -16,7 +17,7 @@ export class Doc { this.collectionData = model && model.data[collectionName]; } - path(segments?: string[]) { + path(segments?: Segments) { var path = this.collectionName + '.' + this.id; if (segments && segments.length) path += '.' + segments.join('.'); return path; diff --git a/src/Model/EventListenerTree.ts b/src/Model/EventListenerTree.ts index d67d0f3c0..bbd4d7100 100644 --- a/src/Model/EventListenerTree.ts +++ b/src/Model/EventListenerTree.ts @@ -1,4 +1,4 @@ -import { type Segments } from './Model'; +import { type Segments } from './types'; import { FastMap } from './FastMap'; /** diff --git a/src/Model/EventMapTree.ts b/src/Model/EventMapTree.ts index e95032fe1..e15460834 100644 --- a/src/Model/EventMapTree.ts +++ b/src/Model/EventMapTree.ts @@ -1,4 +1,4 @@ -import { type Segments } from './Model'; +import { type Segments } from './types'; import { FastMap } from './FastMap'; /** diff --git a/src/Model/Model.ts b/src/Model/Model.ts index a4a9e812a..3eb4568a8 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -18,7 +18,6 @@ declare module './Model' { type ErrorCallback = (err?: Error) => void; } - type ModelInitFunction = (instance: Model, options: ModelOptions) => void; export class Model { diff --git a/src/Model/Query.ts b/src/Model/Query.ts index 015f6e1da..8cb3863c4 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -1,6 +1,8 @@ import { type Context } from './contexts'; -import { ErrorCallback, Model, type Segments } from './Model'; +import { type Segments } from './types'; +import { Model } from './Model'; import { CollectionMap } from './CollectionMap'; + var defaultType = require('sharedb/lib/client').types.defaultType; var util = require('../util'); var promisify = util.promisify; diff --git a/src/Model/collections.ts b/src/Model/collections.ts index 7e8a11a83..f05e8f69c 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -1,3 +1,4 @@ +import { type Segments } from './types'; import { Doc } from './Doc'; import { Model } from './Model'; var LocalDoc = require('./LocalDoc'); diff --git a/src/Model/events.ts b/src/Model/events.ts index 1ec9c9135..85095cca8 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -2,10 +2,11 @@ import { EventEmitter } from 'events'; import { EventListenerTree } from './EventListenerTree'; -var mergeInto = require('../util').mergeInto; -/** @type any */ +import { type Segments } from './types'; import { Model } from './Model'; +var mergeInto = require('../util').mergeInto; + declare module './Model' { interface Model { _defaultCallback(err?: Error): void; diff --git a/src/Model/filter.ts b/src/Model/filter.ts index 51eabb836..69e58dbbf 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -1,5 +1,6 @@ var util = require('../util'); -import { Model, type Segments } from './Model'; +import { Model } from './Model'; +import { type Segments } from './types'; import * as defaultFns from './defaultFns'; declare module './Model' { @@ -126,16 +127,16 @@ class Filters{ }; } -class Filter { +export class Filter { bundle: boolean; filterFn: any; filterName: string; filters: any; from: string; fromSegments: string[] - idsSegments: string[]; + idsSegments: Segments; inputPaths: any; - inputsSegments: string[]; + inputsSegments: Segments[]; limit: number; model: Model; options: any; diff --git a/src/Model/fn.ts b/src/Model/fn.ts index 6a5b448c4..f988742d9 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -1,4 +1,5 @@ -import { Model, type Segments } from './Model'; +import { type Segments } from './types'; +import { Model } from './Model'; import { EventListenerTree } from './EventListenerTree'; import { EventMapTree } from './EventMapTree'; import * as defaultFns from './defaultFns'; diff --git a/src/Model/index.ts b/src/Model/index.ts index 26228e820..f6666f4cb 100644 --- a/src/Model/index.ts +++ b/src/Model/index.ts @@ -1,5 +1,6 @@ import { serverRequire } from '../util'; export { Model } from './Model'; +export { ModelData } from './collections'; // Extend model on both server and client // require('./unbundle'); diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index e0eb56137..89d3c45d6 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -1,6 +1,9 @@ var util = require('../util'); import { Model } from './Model'; +import { type Segments } from './types'; + var mutationEvents = require('./events').mutationEvents; + var ChangeEvent = mutationEvents.ChangeEvent; var InsertEvent = mutationEvents.InsertEvent; var RemoveEvent = mutationEvents.RemoveEvent; diff --git a/src/Model/paths.ts b/src/Model/paths.ts index 75bf183d0..9fd8be5b6 100644 --- a/src/Model/paths.ts +++ b/src/Model/paths.ts @@ -5,7 +5,7 @@ exports.mixin = {}; declare module './Model' { interface Model { _splitPath(subpath: string): string[]; - path(subpath: string): Model; + path(subpath: string | number | Model): string; isPath(subpath: string): boolean; scope(subpath: string): Model; at(subpath: string): Model; diff --git a/src/Model/ref.ts b/src/Model/ref.ts index b8eeb602d..ae2e61a2e 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -1,17 +1,20 @@ import { EventListenerTree } from './EventListenerTree'; import { EventMapTree } from './EventMapTree'; import { Model } from './Model'; +import { type Segments } from './types'; +import { type Filter } from './filter'; +import { type Query } from './Query'; -declare module './Model' { - type Segments = any; +type Refable = string | number | Model | Query | Filter; +declare module './Model' { interface Model { _refs: any; _refLists: any; - canRefTo(value: any): boolean; - _canRefTo(from: Segments, to: Segments, options: any): boolean; - ref(to: Segments): void; - ref(from: Segments, to: Segments, options?: any): void; + _canRefTo(value: Refable): boolean; + // _canRefTo(from: Segments, to: Segments, options: any): boolean; + ref(to: Refable): void; + ref(from: string | number, to: Refable, options?: any): void; _ref(from: Segments, to: Segments, options: any): any; removeRef(subpath: string): void; _removeRef(segments: Segments): void; @@ -170,11 +173,12 @@ function addListener(model, type, fn) { } Model.prototype._canRefTo = function(value) { - return this.isPath(value) || (value && typeof value.ref === 'function'); + return this.isPath(value) || (value && typeof (value as any).ref === 'function'); }; Model.prototype.ref = function() { var from, to, options; + // to could be pathlike, model, query, or filter if (arguments.length === 1) { to = arguments[0]; } else if (arguments.length === 2) { @@ -245,7 +249,8 @@ Model.prototype._dereference = function(segments, forArrayMutator, ignore) { var refListsNode = this.root._refLists.fromMap; doAgain = false; for (var i = 0, len = segments.length; i < len; i++) { - var segment = segments[i]; + // @TODO: resolve type for Segments + var segment = segments[i] as string; refsNode = refsNode && refsNode.children && refsNode.children.values[segment]; var ref = refsNode && refsNode.listener; diff --git a/src/Model/setDiff.ts b/src/Model/setDiff.ts index 68e7455a8..ad4d0cbe9 100644 --- a/src/Model/setDiff.ts +++ b/src/Model/setDiff.ts @@ -1,5 +1,6 @@ var util = require('../util'); import { Model } from './Model'; +import { type Segments } from './types'; var arrayDiff = require('arraydiff'); var mutationEvents = require('./events').mutationEvents; var ChangeEvent = mutationEvents.ChangeEvent; diff --git a/src/Model/types.ts b/src/Model/types.ts new file mode 100644 index 000000000..d7345d215 --- /dev/null +++ b/src/Model/types.ts @@ -0,0 +1,17 @@ + + // | { ref: any }; +/** + * + export type Path = string | number; + export type PathSegment = string | number; + export type PathLike = Path | Model; + */ + + +// could be +// ['foo', 3, 'bar'] +// always converted to string internally +export type Segment = string; + +// PathLike +export type Segments = Array; diff --git a/src/index.ts b/src/index.ts index f6460b789..2a3a3b028 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,11 +3,13 @@ import { Model } from './Model'; import * as util from './util'; import { RacerBackend } from './Backend'; import { Query } from './Model/Query'; +import { ModelData } from './Model' // module.exports = new Racer(); const { use, serverUse } = util; export { Model }; +export { type ModelData } export { Query }; export { Racer }; export { RacerBackend }; From f21109d0150c74861b1d938bbe33432d03abf0a4 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 11 Jan 2024 14:48:37 -0800 Subject: [PATCH 321/479] Update types; add reference to server types for Model --- src/Model/Model.ts | 2 +- src/Model/bundle.ts | 1 - src/Model/collections.ts | 2 +- src/Model/events.ts | 18 +++++++++--------- src/Model/index.ts | 31 +++++++++++++++++-------------- src/index.ts | 25 ++++++++++++++----------- 6 files changed, 42 insertions(+), 37 deletions(-) diff --git a/src/Model/Model.ts b/src/Model/Model.ts index 3eb4568a8..336362ab7 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -27,7 +27,7 @@ export class Model { debug: DebugOptions; root: Model; - _at: () => Model; + _at: string; _context: Context; _eventContext: number | null; _events: []; diff --git a/src/Model/bundle.ts b/src/Model/bundle.ts index f45c95db4..0b9c860fa 100644 --- a/src/Model/bundle.ts +++ b/src/Model/bundle.ts @@ -5,7 +5,6 @@ var promisify = require('../util').promisify; declare module './Model' { interface Model { bundleTimeout: number; - bundle(cb: (err?: Error, bundle?: any) => void): void; bundlePromised(): Promise; } diff --git a/src/Model/collections.ts b/src/Model/collections.ts index f05e8f69c..1aa6015de 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -26,7 +26,7 @@ declare module './Model' { _getDeepCopy(segments: Segments): any; getOrCreateCollection(name: string): Collection; getOrCreateDoc(collectionName: string, id: string, data: any); - destroy(subpath: string): void; + destroy(subpath?: string): void; } } diff --git a/src/Model/events.ts b/src/Model/events.ts index 85095cca8..baca8b2de 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -158,15 +158,15 @@ Model.prototype._callMutationListeners = function(type, segments, event) { Model.prototype.__on = EventEmitter.prototype.on; Model.prototype.addListener = - Model.prototype.on = function(type, arg1, arg2, arg3) { - var listener = this._addMutationListener(type, arg1, arg2, arg3); - if (listener) { - return listener; - } - // Normal event - this.__on(type, arg1); - return arg1; - }; +Model.prototype.on = function(type, arg1, arg2, arg3) { + var listener = this._addMutationListener(type, arg1, arg2, arg3); + if (listener) { + return listener; + } + // Normal event + this.__on(type, arg1); + return arg1; +}; Model.prototype.__once = EventEmitter.prototype.once; Model.prototype.once = function(type, arg1, arg2, arg3) { diff --git a/src/Model/index.ts b/src/Model/index.ts index f6666f4cb..f4060459d 100644 --- a/src/Model/index.ts +++ b/src/Model/index.ts @@ -1,24 +1,27 @@ +/// +/// + import { serverRequire } from '../util'; export { Model } from './Model'; export { ModelData } from './collections'; // Extend model on both server and client // -require('./unbundle'); -require('./events'); -require('./paths'); -require('./collections'); -require('./mutators'); -require('./setDiff'); +import './unbundle'; +import './events'; +import './paths'; +import './collections'; +import './mutators'; +import './setDiff'; -require('./connection'); -require('./subscriptions'); -require('./Query'); -require('./contexts'); +import './connection'; +import './subscriptions'; +import './Query'; +import './contexts'; -require('./fn'); -require('./filter'); -require('./refList'); -require('./ref'); +import './fn'; +import './filter'; +import './refList'; +import './ref'; // Extend model for server // serverRequire(module, './bundle'); diff --git a/src/index.ts b/src/index.ts index 2a3a3b028..276853519 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,23 @@ import { Racer } from './Racer'; -import { Model } from './Model'; +import { Model, ModelData } from './Model'; import * as util from './util'; + import { RacerBackend } from './Backend'; -import { Query } from './Model/Query'; -import { ModelData } from './Model' +export { Query } from './Model/Query'; +export { ChildModel } from './Model/Model'; -// module.exports = new Racer(); const { use, serverUse } = util; -export { Model }; -export { type ModelData } -export { Query }; -export { Racer }; -export { RacerBackend }; -export { use, serverUse }; -export { util }; +export { + Model, + ModelData, + Racer, + RacerBackend, + use, + serverUse, + util, +}; + export const racer = new Racer(); export function createModel(data) { From 181837eede0771db1873667e72ed7bf51f93bd85 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 11 Jan 2024 15:49:43 -0800 Subject: [PATCH 322/479] Revert package name --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c0f8926b5..e664f7369 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@derbyjs/racer", + "name": "racer", "description": "Realtime model synchronization engine for Node.js", "homepage": "https://github.com/derbyjs/racer", "repository": { @@ -9,7 +9,7 @@ "version": "1.1.0", "main": "./lib/index.js", "files": [ - "lib/" + "lib/*" ], "scripts": { "build": "node_modules/.bin/tsc", @@ -21,6 +21,7 @@ "prepare": "npm run build", "test-cover": "node_modules/nyc/bin/nyc.js --temp-dir=coverage -r text -r lcov node_modules/mocha/bin/_mocha" }, + "types": "./lib/index.d.ts", "dependencies": { "arraydiff": "^0.1.1", "fast-deep-equal": "^2.0.1", From 850f73d44a18f5a8daf6f5b415937315359bd975 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 16 Jan 2024 14:52:18 -0800 Subject: [PATCH 323/479] Fix Context class toJSON method so key space not polluted for Model.unloadAll() --- src/Model/contexts.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Model/contexts.ts b/src/Model/contexts.ts index 150f41294..0f63159c5 100644 --- a/src/Model/contexts.ts +++ b/src/Model/contexts.ts @@ -45,19 +45,21 @@ Model.prototype.unload = function(id) { Model.prototype.unloadAll = function() { var contexts = this.root._contexts; for (var key in contexts) { + const currentContext = contexts[key]; if (contexts.hasOwnProperty(key)) { - contexts[key].unload(); + currentContext.unload(); } } }; export class Contexts { - toJSON = function() { - var out = {}; + toJSON() { + var out: Record = {}; var contexts = this; for (var key in contexts) { - if (contexts[key] instanceof Context) { - out[key] = contexts[key].toJSON(); + const currentContext = contexts[key]; + if (currentContext instanceof Context) { + out[key] = currentContext.toJSON(); } } return out; From 319c32066353cf5ace54bc38507a9ffe54386f9e Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 16 Jan 2024 15:13:07 -0800 Subject: [PATCH 324/479] Packge name to use scope --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e664f7369..9845f0372 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "racer", + "name": "@derbyjs/racer", "description": "Realtime model synchronization engine for Node.js", "homepage": "https://github.com/derbyjs/racer", "repository": { From b49d5de96ee84b16d32a9bbc4254a99553723b34 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 16 Jan 2024 16:15:47 -0800 Subject: [PATCH 325/479] 2.0.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9845f0372..1b6a65127 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, - "version": "1.1.0", + "version": "2.0.0-beta.1", "main": "./lib/index.js", "files": [ "lib/*" From 5c74b6810ce41f083882c57a520f389964c31070 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 16 Jan 2024 16:24:33 -0800 Subject: [PATCH 326/479] Add publishConfig public to package.json --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 1b6a65127..805d60c43 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "type": "git", "url": "git://github.com/derbyjs/racer.git" }, + "publishConfig": { + "access": "public" + }, "version": "2.0.0-beta.1", "main": "./lib/index.js", "files": [ From 30c9efc716ae50254bb9c133633c5c140dfb0dcb Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 18 Jan 2024 12:07:41 -0800 Subject: [PATCH 327/479] Use unscoped package name ofr ease of integration testing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 805d60c43..286e52fcd 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@derbyjs/racer", + "name": "racer", "description": "Realtime model synchronization engine for Node.js", "homepage": "https://github.com/derbyjs/racer", "repository": { From 1a276d7de91aef753197dd58bcf31d042185befb Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 18 Jan 2024 12:08:33 -0800 Subject: [PATCH 328/479] 2.0.0-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 286e52fcd..ad7c9e86e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "main": "./lib/index.js", "files": [ "lib/*" From e30a6a3884629363e22713da7e84a929538f90ab Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 18 Jan 2024 15:28:45 -0800 Subject: [PATCH 329/479] Refactor to export RootModel --- src/Backend.ts | 3 ++- src/Model/Model.ts | 29 ++++++++++++++------- src/Model/collections.ts | 11 ++++---- src/Model/connection.ts | 4 ++- src/Model/events.ts | 1 + src/Model/index.ts | 2 +- src/index.ts | 2 +- test/Model/events.js | 14 +++++----- test/Model/filter.js | 28 ++++++++++---------- test/Model/fn.js | 56 ++++++++++++++++++++-------------------- test/Model/path.js | 20 +++++++------- test/Model/query.js | 18 ++++++------- test/Model/ref.js | 38 +++++++++++++-------------- test/Model/refList.js | 24 ++++++++--------- test/Model/setDiff.js | 48 +++++++++++++++++----------------- 15 files changed, 157 insertions(+), 141 deletions(-) diff --git a/src/Backend.ts b/src/Backend.ts index e9548346b..77ee8b8dc 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -1,6 +1,7 @@ import { Model } from './Model'; import * as path from 'path'; import * as util from './util'; +import { RootModel } from './Model/Model'; var Backend = require('sharedb').Backend; export class RacerBackend extends Backend { @@ -23,7 +24,7 @@ export class RacerBackend extends Backend { util.mergeInto(options, this.modelOptions) : this.modelOptions; } - var model = new Model(options); + var model = new RootModel(options); this.emit('model', model); model.createConnection(this, req); return model; diff --git a/src/Model/Model.ts b/src/Model/Model.ts index 336362ab7..a4b80ccb7 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -1,5 +1,8 @@ import { v4 as uuidv4 } from 'uuid'; import { Context } from 'vm'; +import { RacerBackend } from '../Backend'; +import { type Connection } from './connection'; +import { type ModelData } from './collections'; declare module './Model' { interface DebugOptions { @@ -25,7 +28,8 @@ export class Model { ChildModel = ChildModel; debug: DebugOptions; - root: Model; + root: RootModel; + data: ModelData; _at: string; _context: Context; @@ -36,14 +40,6 @@ export class Model { _preventCompose: boolean; _silent: boolean; - constructor(options: ModelOptions = {}) { - this.root = this; - var inits = Model.INITS; - this.debug = options.debug || {}; - for (var i = 0; i < inits.length; i++) { - inits[i](this, options); - } - } id() { return uuidv4(); @@ -54,6 +50,21 @@ export class Model { }; } +export class RootModel extends Model { + backend: RacerBackend; + connection: Connection; + + constructor(options: ModelOptions = {}) { + super(); + this.root = this; + var inits = Model.INITS; + this.debug = options.debug || {}; + for (var i = 0; i < inits.length; i++) { + inits[i](this, options); + } + } +} + export class ChildModel extends Model { constructor(model: Model) { super(); diff --git a/src/Model/collections.ts b/src/Model/collections.ts index 1aa6015de..b0b8a530f 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -1,6 +1,6 @@ import { type Segments } from './types'; import { Doc } from './Doc'; -import { Model } from './Model'; +import { Model, RootModel } from './Model'; var LocalDoc = require('./LocalDoc'); var util = require('../util'); @@ -12,10 +12,11 @@ export class DocMap {} export class CollectionData {} declare module './Model' { - interface Model { + interface RootModel { collections: ModelCollections; data: ModelData; - + } + interface Model { getCollection(collecitonName: string): ModelCollections; getDoc(collecitonName: string, id: string): any | undefined; get(subpath: string): any; @@ -133,14 +134,14 @@ Model.prototype.destroy = function(subpath) { }; export class Collection { - model: Model; + model: RootModel; name: string; size: number; docs: DocMap; data: CollectionData; Doc: typeof Doc; - constructor(model: Model, name: string, docClass: typeof Doc) { + constructor(model: RootModel, name: string, docClass: typeof Doc) { this.model = model; this.name = name; this.Doc = docClass; diff --git a/src/Model/connection.ts b/src/Model/connection.ts index b659634e3..0cd72b67b 100644 --- a/src/Model/connection.ts +++ b/src/Model/connection.ts @@ -1,10 +1,12 @@ -var Connection = require('sharedb/lib/client').Connection; +import { Connection } from 'sharedb/lib/client'; import { Model } from './Model'; import { type Doc} from './Doc'; import { LocalDoc} from './LocalDoc'; import {RemoteDoc} from './RemoteDoc'; var promisify = require('../util').promisify; +export { type Connection }; + declare module './Model' { interface DocConstructor { new (any: unknown[]): DocConstructor; diff --git a/src/Model/events.ts b/src/Model/events.ts index baca8b2de..61cec40e1 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -55,6 +55,7 @@ Model.INITS.push(function(model: Model) { var mutationListeners = { all: new EventListenerTree() }; + for (var name in mutationEvents) { var eventPrototype = mutationEvents[name].prototype; mutationListeners[eventPrototype.type] = new EventListenerTree(); diff --git a/src/Model/index.ts b/src/Model/index.ts index f4060459d..ed16b1dc3 100644 --- a/src/Model/index.ts +++ b/src/Model/index.ts @@ -2,7 +2,7 @@ /// import { serverRequire } from '../util'; -export { Model } from './Model'; +export { Model, ChildModel, RootModel } from './Model'; export { ModelData } from './collections'; // Extend model on both server and client // diff --git a/src/index.ts b/src/index.ts index 276853519..9f7df2b2c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import * as util from './util'; import { RacerBackend } from './Backend'; export { Query } from './Model/Query'; -export { ChildModel } from './Model/Model'; +export { ChildModel, RootModel } from './Model'; const { use, serverUse } = util; diff --git a/test/Model/events.js b/test/Model/events.js index 18d4d1b85..e58a19698 100644 --- a/test/Model/events.js +++ b/test/Model/events.js @@ -4,7 +4,7 @@ var racer = require('../../lib/index'); describe('Model events without useEventObjects', function() { describe('mutator events', function() { it('calls earlier listeners in the order of mutations', function(done) { - var model = (new racer.Model()).at('_page'); + var model = (new racer.RootModel()).at('_page'); var expectedPaths = ['a', 'b', 'c']; model.on('change', '**', function(path) { expect(path).to.equal(expectedPaths.shift()); @@ -22,7 +22,7 @@ describe('Model events without useEventObjects', function() { }); it('calls later listeners in the order of mutations', function(done) { - var model = (new racer.Model()).at('_page'); + var model = (new racer.RootModel()).at('_page'); model.on('change', 'a', function() { model.set('b', 2); }); @@ -40,7 +40,7 @@ describe('Model events without useEventObjects', function() { }); it('can omit the path argument', function(done) { - var model = (new racer.Model()).at('_page'); + var model = (new racer.RootModel()).at('_page'); model.at('a').on('change', function(value, prev) { expect(value).to.equal(1); @@ -139,7 +139,7 @@ describe('Model events without useEventObjects', function() { describe('Model events with {useEventObjects: true}', function() { describe('mutator events', function() { it('calls earlier listeners in the order of mutations', function(done) { - var model = (new racer.Model()).at('_page'); + var model = (new racer.RootModel()).at('_page'); var expectedPaths = ['a', 'b', 'c']; model.on('change', '**', {useEventObjects: true}, function(_event, captures) { expect(captures).to.eql([expectedPaths.shift()]); @@ -157,7 +157,7 @@ describe('Model events with {useEventObjects: true}', function() { }); it('calls later listeners in the order of mutations', function(done) { - var model = (new racer.Model()).at('_page'); + var model = (new racer.RootModel()).at('_page'); model.on('change', 'a', function() { model.set('b', 2); }); @@ -175,7 +175,7 @@ describe('Model events with {useEventObjects: true}', function() { }); it('can omit the path argument when useEventObjects is true', function(done) { - var model = (new racer.Model()).at('_page'); + var model = (new racer.RootModel()).at('_page'); model.at('a').on('change', {useEventObjects: true}, function(_event, captures) { expect(_event.value).to.equal(1); @@ -189,7 +189,7 @@ describe('Model events with {useEventObjects: true}', function() { describe('insert and remove', function() { var model; before('set up', function() { - model = (new racer.Model()).at('_page'); + model = (new racer.RootModel()).at('_page'); }); it('insert has expected properties', function(done) { diff --git a/test/Model/filter.js b/test/Model/filter.js index 234168682..2b032d2a5 100644 --- a/test/Model/filter.js +++ b/test/Model/filter.js @@ -1,10 +1,10 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model').Model; +var RootModel = require('../../lib/Model').RootModel; describe('filter', function() { describe('getting', function() { it('does not support array', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); var filter = model.filter('numbers', function(number) { return (number % 2) === 0; @@ -14,7 +14,7 @@ describe('filter', function() { }).to.throw(Error); }); it('supports filter of object', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -25,7 +25,7 @@ describe('filter', function() { expect(filter.get()).to.eql([0, 4, 2, 0]); }); it('supports sort of object', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -36,7 +36,7 @@ describe('filter', function() { expect(filter.get()).to.eql([4, 3, 3, 2, 1, 0, 0]); }); it('supports filter and sort of object', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -48,7 +48,7 @@ describe('filter', function() { expect(filter.get()).to.eql([0, 0, 2, 4]); }); it('supports additional input paths as var-args', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -61,7 +61,7 @@ describe('filter', function() { expect(filter.get()).to.eql([0, 3, 3, 0]); }); it('supports additional input paths as array', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -74,7 +74,7 @@ describe('filter', function() { expect(filter.get()).to.eql([0, 3, 3, 0]); }); it('supports a skip option', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; var options = {skip: 2}; for (var i = 0; i < numbers.length; i++) { @@ -90,7 +90,7 @@ describe('filter', function() { }); describe('initial value set by ref', function() { it('supports filter of object', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -102,7 +102,7 @@ describe('filter', function() { expect(model.get('out')).to.eql([0, 4, 2, 0]); }); it('supports sort of object', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -114,7 +114,7 @@ describe('filter', function() { expect(model.get('out')).to.eql([4, 3, 3, 2, 1, 0, 0]); }); it('supports filter and sort of object', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -129,7 +129,7 @@ describe('filter', function() { }); describe('ref updates as items are modified', function() { it('supports filter of object', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var greenId = model.add('colors', { name: 'green', primary: true @@ -182,7 +182,7 @@ describe('filter', function() { ]); }); it('supports additional dynamic inputs as var-args', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); @@ -199,7 +199,7 @@ describe('filter', function() { expect(filter.get()).to.eql([3, 1, 3]); }); it('supports additional dynamic inputs as array', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; for (var i = 0; i < numbers.length; i++) { model.set('numbers.' + model.id(), numbers[i]); diff --git a/test/Model/fn.js b/test/Model/fn.js index a10790b37..5e53bad41 100644 --- a/test/Model/fn.js +++ b/test/Model/fn.js @@ -1,10 +1,10 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model').Model; +var RootModel = require('../../lib/Model').RootModel; describe('fn', function() { describe('evaluate', function() { it('supports fn with a getter function', function() { - var model = new Model(); + var model = new RootModel(); model.fn('sum', function(a, b) { return a + b; }); @@ -14,7 +14,7 @@ describe('fn', function() { expect(result).to.equal(6); }); it('supports fn with an object', function() { - var model = new Model(); + var model = new RootModel(); model.fn('sum', { get: function(a, b) { return a + b; @@ -26,7 +26,7 @@ describe('fn', function() { expect(result).to.equal(6); }); it('supports fn with variable arguments', function() { - var model = new Model(); + var model = new RootModel(); model.fn('sum', function() { var sum = 0; var i = arguments.length; @@ -42,7 +42,7 @@ describe('fn', function() { expect(result).to.equal(13); }); it('supports scoped model paths', function() { - var model = new Model(); + var model = new RootModel(); model.fn('sum', function(a, b) { return a + b; }); @@ -57,7 +57,7 @@ describe('fn', function() { }); describe('start', function() { it('sets the output immediately on start', function() { - var model = new Model(); + var model = new RootModel(); model.set('_nums.a', 2); model.set('_nums.b', 4); var value = model.start('_nums.sum', '_nums.a', '_nums.b', function(a, b) { @@ -67,7 +67,7 @@ describe('fn', function() { expect(model.get('_nums.sum')).to.equal(6); }); it('supports function name argument', function() { - var model = new Model(); + var model = new RootModel(); model.fn('sum', function(a, b) { return a + b; }); @@ -78,7 +78,7 @@ describe('fn', function() { expect(model.get('_nums.sum')).to.equal(6); }); it('sets the output when an input changes', function() { - var model = new Model(); + var model = new RootModel(); model.set('_nums.a', 2); model.set('_nums.b', 4); model.start('_nums.sum', '_nums.a', '_nums.b', function(a, b) { @@ -89,7 +89,7 @@ describe('fn', function() { expect(model.get('_nums.sum')).to.equal(9); }); it('sets the output when a parent of the input changes', function() { - var model = new Model(); + var model = new RootModel(); model.set('_nums.in', { a: 2, b: 4 @@ -105,7 +105,7 @@ describe('fn', function() { expect(model.get('_nums.sum')).to.equal(12); }); it('does not set the output when a sibling of the input changes', function() { - var model = new Model(); + var model = new RootModel(); model.set('_nums.in', { a: 2, b: 4 @@ -125,7 +125,7 @@ describe('fn', function() { expect(count).to.equal(2); }); it('calling twice cleans up listeners for former function', function() { - var model = new Model(); + var model = new RootModel(); model.set('_nums.in', { a: 2, b: 4 @@ -150,11 +150,11 @@ describe('fn', function() { describe('stop', function() { it('can call stop without start', function() { - var model = new Model(); + var model = new RootModel(); model.stop('_nums.sum'); }); it('stops updating after calling stop', function() { - var model = new Model(); + var model = new RootModel(); model.set('_nums.a', 2); model.set('_nums.b', 4); model.start('_nums.sum', '_nums.a', '_nums.b', function(a, b) { @@ -167,7 +167,7 @@ describe('fn', function() { expect(model.get('_nums.sum')).to.equal(5); }); it('stops updating when start was called twice', function() { - var model = new Model(); + var model = new RootModel(); model.set('_nums.a', 2); model.set('_nums.b', 4); model.start('_nums.sum', '_nums.a', '_nums.b', function(a, b) { @@ -185,11 +185,11 @@ describe('fn', function() { }); describe('stopAll', function() { it('can call without start', function() { - var model = new Model(); + var model = new RootModel(); model.stopAll('_nums.sum'); }); it('stops updating functions at matching paths', function() { - var model = new Model(); + var model = new RootModel(); model.fn('sum', function(a, b) { return a + b; }); @@ -214,7 +214,7 @@ describe('fn', function() { }); describe('start with array inputs', function() { it('array inputs and function name', function() { - var model = new Model(); + var model = new RootModel(); model.fn('sum', function(a, b) { return a + b; }); @@ -225,7 +225,7 @@ describe('fn', function() { expect(model.get('_nums.sum')).to.equal(6); }); it('array inputs and function argument', function() { - var model = new Model(); + var model = new RootModel(); model.set('_nums.a', 2); model.set('_nums.b', 4); var value = model.start('_nums.sum', ['_nums.a', '_nums.b'], function(a, b) { @@ -237,7 +237,7 @@ describe('fn', function() { }); describe('start with async option', function() { it('sets the output immediately on start', function() { - var model = new Model(); + var model = new RootModel(); model.fn('sum', function(a, b) { return a + b; }); @@ -248,7 +248,7 @@ describe('fn', function() { expect(model.get('_nums.sum')).to.equal(6); }); it('async sets the output when an input changes', function(done) { - var model = new Model(); + var model = new RootModel(); model.fn('sum', function(a, b) { return a + b; }); @@ -266,7 +266,7 @@ describe('fn', function() { }); }); it('debouncing gets reset', function(done) { - var model = new Model(); + var model = new RootModel(); model.fn('sum', function(a, b) { return a + b; }); @@ -289,7 +289,7 @@ describe('fn', function() { }); }); it('no async sets the output multiple times when an input changes multiple times', function() { - var model = new Model(); + var model = new RootModel(); var calls = 0; model.fn('sum', function(a, b) { calls++; @@ -311,7 +311,7 @@ describe('fn', function() { expect(calls).to.equal(5); }); it('async sets the output when an input changes multiple times', function(done) { - var model = new Model(); + var model = new RootModel(); var calls = 0; model.fn('sum', function(a, b) { calls++; @@ -340,7 +340,7 @@ describe('fn', function() { }); describe('setter', function() { it('sets the input when the output changes', function() { - var model = new Model(); + var model = new RootModel(); model.fn('fullName', { get: function(first, last) { return first + ' ' + last; @@ -369,7 +369,7 @@ describe('fn', function() { }); describe('event mirroring', function() { it('emits move event on output when input changes', function(done) { - var model = new Model(); + var model = new RootModel(); model.fn('unity', { get: function(value) { return value; @@ -399,7 +399,7 @@ describe('fn', function() { expect(model.get('_test.out')).to.eql(model.get('_test.in')); }); it('emits move event on input when output changes', function(done) { - var model = new Model(); + var model = new RootModel(); model.fn('unity', { get: function(value) { return value; @@ -429,7 +429,7 @@ describe('fn', function() { expect(model.get('_test.out')).to.eql(model.get('_test.in')); }); it('emits granular change event under an array when input changes', function(done) { - var model = new Model(); + var model = new RootModel(); model.fn('unity', { get: function(value) { return value; @@ -459,7 +459,7 @@ describe('fn', function() { expect(model.get('_test.out')).to.eql(model.get('_test.in')); }); it('emits granular change event under an array when output changes', function(done) { - var model = new Model(); + var model = new RootModel(); model.fn('unity', { get: function(value) { return value; diff --git a/test/Model/path.js b/test/Model/path.js index ea3953542..730ba36ff 100644 --- a/test/Model/path.js +++ b/test/Model/path.js @@ -1,34 +1,34 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model').Model; +var RootModel = require('../../lib/Model').RootModel; describe('path methods', function() { describe('path', function() { it('returns empty string for model without scope', function() { - var model = new Model(); + var model = new RootModel(); expect(model.path()).equal(''); }); }); describe('scope', function() { it('returns a child model with the absolute scope', function() { - var model = new Model(); + var model = new RootModel(); var scoped = model.scope('foo.bar.baz'); expect(model.path()).equal(''); expect(scoped.path()).equal('foo.bar.baz'); }); it('supports segments as separate arguments', function() { - var model = new Model(); + var model = new RootModel(); var scoped = model.scope('foo', 'bar', 'baz'); expect(model.path()).equal(''); expect(scoped.path()).equal('foo.bar.baz'); }); it('overrides a previous scope', function() { - var model = new Model(); + var model = new RootModel(); var scoped = model.scope('foo', 'bar', 'baz'); var scoped2 = scoped.scope('colors', 4); expect(scoped2.path()).equal('colors.4'); }); it('supports no arguments', function() { - var model = new Model(); + var model = new RootModel(); var scoped = model.scope('foo', 'bar', 'baz'); var scoped2 = scoped.scope(); expect(scoped2.path()).equal(''); @@ -36,25 +36,25 @@ describe('path methods', function() { }); describe('at', function() { it('returns a child model with the relative scope', function() { - var model = new Model(); + var model = new RootModel(); var scoped = model.at('foo.bar.baz'); expect(model.path()).equal(''); expect(scoped.path()).equal('foo.bar.baz'); }); it('supports segments as separate arguments', function() { - var model = new Model(); + var model = new RootModel(); var scoped = model.at('foo', 'bar', 'baz'); expect(model.path()).equal(''); expect(scoped.path()).equal('foo.bar.baz'); }); it('overrides a previous scope', function() { - var model = new Model(); + var model = new RootModel(); var scoped = model.at('colors'); var scoped2 = scoped.at(4); expect(scoped2.path()).equal('colors.4'); }); it('supports no arguments', function() { - var model = new Model(); + var model = new RootModel(); var scoped = model.at('foo', 'bar', 'baz'); var scoped2 = scoped.at(); expect(scoped2.path()).equal('foo.bar.baz'); diff --git a/test/Model/query.js b/test/Model/query.js index 3d5e63592..aa81ec4b1 100644 --- a/test/Model/query.js +++ b/test/Model/query.js @@ -1,16 +1,16 @@ var expect = require('../util').expect; var racer = require('../../lib'); -var Model = require('../../lib/Model').Model; +var RootModel = require('../../lib/Model').RootModel; describe('query', function() { describe('sanitizeQuery', function() { it('replaces undefined with null in object query expressions', function() { - var model = new Model(); + var model = new RootModel(); var query = model.query('foo', {x: undefined, y: 'foo'}); expect(query.expression).eql({x: null, y: 'foo'}); }); it('replaces undefined with null in nested object query expressions', function() { - var model = new Model(); + var model = new RootModel(); var query = model.query('foo', [{x: undefined}, {x: {y: undefined, z: 0}}]); expect(query.expression).eql([{x: null}, {x: {y: null, z: 0}}]); }); @@ -57,37 +57,37 @@ describe('query', function() { describe('instantiation', function() { it('returns same instance when params are equivalent', function() { - var model = new Model(); + var model = new RootModel(); var query1 = model.query('foo', {value: 1}, {db: 'other'}); var query2 = model.query('foo', {value: 1}, {db: 'other'}); expect(query1).equal(query2); }); it('returns same instance when context and params are equivalent', function() { - var model = new Model(); + var model = new RootModel(); var query1 = model.context('box').query('foo', {}); var query2 = model.context('box').query('foo', {}); expect(query1).equal(query2); }); it('creates a unique query instance per collection name', function() { - var model = new Model(); + var model = new RootModel(); var query1 = model.query('foo', {}); var query2 = model.query('bar', {}); expect(query1).not.equal(query2); }); it('creates a unique query instance per expression', function() { - var model = new Model(); + var model = new RootModel(); var query1 = model.query('foo', {value: 1}); var query2 = model.query('foo', {value: 2}); expect(query1).not.equal(query2); }); it('creates a unique query instance per options', function() { - var model = new Model(); + var model = new RootModel(); var query1 = model.query('foo', {}, {db: 'default'}); var query2 = model.query('foo', {}, {db: 'other'}); expect(query1).not.equal(query2); }); it('creates a unique query instance per context', function() { - var model = new Model(); + var model = new RootModel(); var query1 = model.query('foo', {}); var query2 = model.context('box').query('foo', {}); expect(query1).not.equal(query2); diff --git a/test/Model/ref.js b/test/Model/ref.js index 2156c3916..8eab1621e 100644 --- a/test/Model/ref.js +++ b/test/Model/ref.js @@ -1,5 +1,5 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model').Model; +var RootModel = require('../../lib/Model').RootModel; describe('ref', function() { function expectEvents(pattern, model, done, events) { @@ -11,7 +11,7 @@ describe('ref', function() { } describe('event emission', function() { it('re-emits on a reffed path', function(done) { - var model = new Model(); + var model = new RootModel(); model.ref('_page.color', '_page.colors.green'); model.on('change', '_page.color', function(value) { expect(value).to.equal('#0f0'); @@ -20,7 +20,7 @@ describe('ref', function() { model.set('_page.colors.green', '#0f0'); }); it('also emits on the original path', function(done) { - var model = new Model(); + var model = new RootModel(); model.ref('_page.color', '_page.colors.green'); model.on('change', '_page.colors.green', function(value) { expect(value).to.equal('#0f0'); @@ -29,7 +29,7 @@ describe('ref', function() { model.set('_page.colors.green', '#0f0'); }); it('re-emits on a child of a reffed path', function(done) { - var model = new Model(); + var model = new RootModel(); model.ref('_page.color', '_page.colors.green'); model.on('change', '_page.color.*', function(capture, value) { expect(capture).to.equal('hex'); @@ -39,7 +39,7 @@ describe('ref', function() { model.set('_page.colors.green.hex', '#0f0'); }); it('re-emits when a parent is changed', function(done) { - var model = new Model(); + var model = new RootModel(); model.ref('_page.color', '_page.colors.green'); model.on('change', '_page.color', function(value) { expect(value).to.equal('#0e0'); @@ -50,7 +50,7 @@ describe('ref', function() { }); }); it('re-emits on a ref to a ref', function(done) { - var model = new Model(); + var model = new RootModel(); model.ref('_page.myFavorite', '_page.color'); model.ref('_page.color', '_page.colors.green'); model.on('change', '_page.myFavorite', function(value) { @@ -60,7 +60,7 @@ describe('ref', function() { model.set('_page.colors.green', '#0f0'); }); it('re-emits on multiple reffed paths', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.colors.green', '#0f0'); model.ref('_page.favorites.my', '_page.colors.green'); model.ref('_page.favorites.your', '_page.colors.green'); @@ -80,14 +80,14 @@ describe('ref', function() { }); describe('get', function() { it('gets from a reffed path', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.colors.green', '#0f0'); expect(model.get('_page.color')).to.equal(undefined); model.ref('_page.color', '_page.colors.green'); expect(model.get('_page.color')).to.equal('#0f0'); }); it('gets from a child of a reffed path', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.colors.green.hex', '#0f0'); model.ref('_page.color', '_page.colors.green'); expect(model.get('_page.color')).to.eql({ @@ -96,7 +96,7 @@ describe('ref', function() { expect(model.get('_page.color.hex')).to.equal('#0f0'); }); it('gets from a ref to a ref', function() { - var model = new Model(); + var model = new RootModel(); model.ref('_page.myFavorite', '_page.color'); model.ref('_page.color', '_page.colors.green'); model.set('_page.colors.green', '#0f0'); @@ -105,7 +105,7 @@ describe('ref', function() { }); describe('event/add ordering', function() { it('ref results are propogated when set in reponse to an event', function() { - var model = new Model(); + var model = new RootModel(); model.on('change', '_page.start', function() { model.ref('_page.myColor', '_page.color'); model.ref('_page.yourColor', '_page.color'); @@ -116,7 +116,7 @@ describe('ref', function() { expect(model.get('_page.myColor')).to.equal('green'); }); it('can create refList in event callback', function() { - var model = new Model(); + var model = new RootModel(); model.on('change', '_page.start', function() { model.set('_page.colors', { red: '#f00', @@ -134,7 +134,7 @@ describe('ref', function() { // for a given toPath - "_page.color" in this case - it's possible for one // of the refs to be removed. Modifying during iteration can cause issues // if not handled correctly. - var model = new Model(); + var model = new RootModel(); model.ref('_page.ref1', '_page.color'); model.ref('_page.ref2', '_page.color'); model.set('_page.color', 'red'); @@ -156,7 +156,7 @@ describe('ref', function() { }); describe('updateIndices option', function() { it('updates a ref when an array insert happens at the `to` path', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.colors', ['red', 'green', 'blue']); model.ref('_page.color', '_page.colors.1', {updateIndices: true}); expect(model.get('_page.color')).to.equal('green'); @@ -168,7 +168,7 @@ describe('ref', function() { expect(model.get('_page.color')).to.equal('green'); }); it('updates a ref when an array remove happens at the `to` path', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.colors', ['red', 'blue', 'purple', 'cyan', 'green', 'yellow']); model.ref('_page.color', '_page.colors.4', {updateIndices: true}); expect(model.get('_page.color')).to.equal('green'); @@ -180,7 +180,7 @@ describe('ref', function() { expect(model.get('_page.color')).to.equal('green'); }); it('updates a ref when an array move happens at the `to` path', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.colors', ['red', 'blue', 'purple', 'green', 'cyan', 'yellow']); model.ref('_page.color', '_page.colors.3', {updateIndices: true}); expect(model.get('_page.color')).to.equal('green'); @@ -200,7 +200,7 @@ describe('ref', function() { expect(model.get('_page.color')).to.equal('green'); }); it('updates a ref when an array insert happens within the `to` path', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.colors', [ {name: 'red'}, {name: 'green'}, @@ -216,7 +216,7 @@ describe('ref', function() { expect(model.get('_page.color')).to.equal('green'); }); it('updates a ref when an array remove happens within the `to` path', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.colors', [ {name: 'red'}, {name: 'blue'}, @@ -235,7 +235,7 @@ describe('ref', function() { expect(model.get('_page.color')).to.equal('green'); }); it('updates a ref when an array move happens within the `to` path', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.colors', [ {name: 'red'}, {name: 'blue'}, diff --git a/test/Model/refList.js b/test/Model/refList.js index 51f5fb1c2..a60fac517 100644 --- a/test/Model/refList.js +++ b/test/Model/refList.js @@ -1,9 +1,9 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model').Model; +var RootModel = require('../../lib/Model').RootModel; describe('refList', function() { function setup(options) { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); model.set('colors', { green: { id: 'green', @@ -38,7 +38,7 @@ describe('refList', function() { } describe('sets output on initial call', function() { it('sets the initial value to empty array if no inputs', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); model.refList('empty', 'colors', 'noIds'); expect(model.get('empty')).to.eql([]); }); @@ -63,7 +63,7 @@ describe('refList', function() { }); describe('updates on `ids` mutations', function() { it('updates the value when `ids` is set', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); model.set('colors', { green: { id: 'green', @@ -96,7 +96,7 @@ describe('refList', function() { ]); }); it('emits on `from` when `ids` is set', function(done) { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); model.set('colors', { green: { id: 'green', @@ -335,7 +335,7 @@ describe('refList', function() { }); describe('emits events involving multiple refLists', function() { it('removes data from a refList pointing to data in another refList', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); var tagId = model.add('tags', { text: 'hi' }); @@ -352,7 +352,7 @@ describe('refList', function() { }); describe('updates on `to` mutations', function() { it('updates the value when `to` is set', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); model.set('ids', ['red', 'green', 'red']); model.refList('list', 'colors', 'ids'); expect(model.get('list')).to.eql([undefined, undefined, undefined]); @@ -385,7 +385,7 @@ describe('refList', function() { ]); }); it('emits on `from` when `to` is set', function(done) { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); model.set('ids', ['red', 'green', 'red']); model.refList('list', 'colors', 'ids'); expectFromEvents(model, done, [ @@ -429,7 +429,7 @@ describe('refList', function() { }); }); it('updates the value when `to` children are set', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); model.set('ids', ['red', 'green', 'red']); model.refList('list', 'colors', 'ids'); model.set('colors.green', { @@ -478,7 +478,7 @@ describe('refList', function() { ]); }); it('emits on `from` when `to` children are set', function(done) { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); model.set('ids', ['red', 'green', 'red']); model.refList('list', 'colors', 'ids'); expectFromEvents(model, done, [ @@ -572,7 +572,7 @@ describe('refList', function() { model.set('colors.red.rgb.0', 238); }); it('updates the value when inserting on `to` children', function() { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); model.set('nums', { even: [2, 4, 6], odd: [1, 3] @@ -584,7 +584,7 @@ describe('refList', function() { expect(model.get('list')).to.eql([[2, 4, 6, 8], [1, 3], [2, 4, 6, 8]]); }); it('emits on `from` when inserting on `to` children', function(done) { - var model = (new Model()).at('_page'); + var model = (new RootModel()).at('_page'); model.set('nums', { even: [2, 4, 6], odd: [1, 3] diff --git a/test/Model/setDiff.js b/test/Model/setDiff.js index ea7d56d57..f5513482d 100644 --- a/test/Model/setDiff.js +++ b/test/Model/setDiff.js @@ -1,65 +1,65 @@ var expect = require('../util').expect; -var Model = require('../../lib/Model').Model; +var RootModel = require('../../lib/Model').RootModel; ['setDiff', 'setDiffDeep', 'setArrayDiff', 'setArrayDiffDeep'].forEach(function(method) { describe(method + ' common diff functionality', function() { it('sets the value when undefined', function() { - var model = new Model(); + var model = new RootModel(); model[method]('_page.color', 'green'); expect(model.get('_page.color')).to.equal('green'); }); it('changes the value', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.color', 'green'); model[method]('_page.color', 'red'); expect(model.get('_page.color')).to.equal('red'); }); it('changes an object', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.color', {hex: '#0f0', name: 'green'}); model[method]('_page.color', {hex: '#f00', name: 'red'}); expect(model.get('_page.color')).to.eql({hex: '#f00', name: 'red'}); }); it('deletes keys from an object', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.color', {hex: '#0f0', name: 'green'}); model[method]('_page.color', {name: 'green'}); expect(model.get('_page.color')).to.eql({name: 'green'}); }); it('adds items to an array', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.items', [4]); model[method]('_page.items', [2, 3, 4]); expect(model.get('_page.items')).to.eql([2, 3, 4]); }); it('removes items in an array', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.items', [2, 3, 4]); model[method]('_page.items', [3, 4]); expect(model.get('_page.items')).to.eql([3, 4]); }); it('moves items in an array', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.items', [2, 3, 4]); model[method]('_page.items', [3, 4, 2]); expect(model.get('_page.items')).to.eql([3, 4, 2]); }); it('adds items to an array in an object', function() { - var model = new Model(); + var model = new RootModel(); model.set('_page.lists', {a: [4]}); model[method]('_page.lists', {a: [2, 3, 4]}); expect(model.get('_page.lists')).to.eql({a: [2, 3, 4]}); }); it('emits an event when changing value', function(done) { - var model = new Model(); + var model = new RootModel(); model.on('all', function(segments, event) { expect(segments).eql(['_page', 'color']); expect(event.type).equal('change'); @@ -71,7 +71,7 @@ var Model = require('../../lib/Model').Model; }); it('does not emit an event when value is not changed', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.color', 'green'); model.on('all', function() { done(new Error('unexpected event emission')); @@ -84,7 +84,7 @@ var Model = require('../../lib/Model').Model; describe('setDiff', function() { it('emits an event when an object is set to an equivalent object', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.color', {name: 'green'}); model.on('all', function(segments, event) { expect(segments).eql(['_page', 'color']); @@ -97,7 +97,7 @@ describe('setDiff', function() { }); it('emits an event when an array is set to an equivalent array', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.list', [2, 3, 4]); model.on('all', function(segments, event) { expect(segments).eql(['_page', 'list']); @@ -112,7 +112,7 @@ describe('setDiff', function() { describe('setDiffDeep', function() { it('does not emit an event when an object is set to an equivalent object', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.color', {name: 'green'}); model.on('all', function() { done(new Error('unexpected event emission')); @@ -122,7 +122,7 @@ describe('setDiffDeep', function() { }); it('does not emit an event when an array is set to an equivalent array', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.list', [2, 3, 4]); model.on('all', function() { done(new Error('unexpected event emission')); @@ -132,7 +132,7 @@ describe('setDiffDeep', function() { }); it('does not emit an event when a deep object / array is set to an equivalent value', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.lists', {a: [2, 3], b: [1], _meta: {foo: 'bar'}}); model.on('all', function() { done(new Error('unexpected event emission')); @@ -142,7 +142,7 @@ describe('setDiffDeep', function() { }); it('equivalent objects ignore key order', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.lists', {a: [2, 3], b: [1]}); model.on('all', function() { done(new Error('unexpected event emission')); @@ -152,7 +152,7 @@ describe('setDiffDeep', function() { }); it('adds items to an array', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.items', [4]); model.on('all', function(segments, event) { expect(segments).eql(['_page', 'items']); @@ -165,7 +165,7 @@ describe('setDiffDeep', function() { }); it('adds items to an array in an object', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.lists', {a: [4]}); model.on('all', function(segments, event) { expect(segments).eql(['_page', 'lists', 'a']); @@ -178,7 +178,7 @@ describe('setDiffDeep', function() { }); it('emits a delete event when a key is removed from an object', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.color', {hex: '#0f0', name: 'green'}); model.on('all', function(segments, event) { expect(segments).eql(['_page', 'color', 'hex']); @@ -194,7 +194,7 @@ describe('setDiffDeep', function() { describe('setArrayDiff', function() { it('does not emit an event when an array is set to an equivalent array', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.list', [2, 3, 4]); model.on('all', function() { done(new Error('unexpected event emission')); @@ -204,7 +204,7 @@ describe('setArrayDiff', function() { }); it('emits an event when objects in an array are set to an equivalent array', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.list', [{a: 2}, {c: 3}, {b: 4}]); var expectedEvents = ['remove', 'insert']; model.on('all', function(segments, event) { @@ -221,7 +221,7 @@ describe('setArrayDiff', function() { describe('setArrayDiffDeep', function() { it('does not emit an event when an array is set to an equivalent array', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.list', [2, 3, 4]); model.on('all', function() { done(new Error('unexpected event emission')); @@ -231,7 +231,7 @@ describe('setArrayDiffDeep', function() { }); it('does not emit an event when objects in an array are set to an equivalent array', function(done) { - var model = new Model(); + var model = new RootModel(); model.set('_page.list', [{a: 2}, {c: 3}, {b: 4}]); model.on('all', function() { done(new Error('unexpected event emission')); From e22aea145e2eaee758dbd1d98fcf772d94ea6fc0 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 26 Jan 2024 14:45:46 -0800 Subject: [PATCH 330/479] Additional type refinements; export addditonal types from root --- package.json | 1 + src/Backend.ts | 6 +-- src/Model/Model.ts | 14 +++--- src/Model/Query.ts | 92 +++++++++++++++++++++----------------- src/Model/collections.ts | 14 +++--- src/Model/events.ts | 61 ++++++++++++++++--------- src/Model/index.ts | 2 +- src/Model/paths.ts | 9 ++-- src/Model/subscriptions.ts | 34 +++++++++----- src/Racer.ts | 4 +- src/index.ts | 14 ++++-- 11 files changed, 151 insertions(+), 100 deletions(-) diff --git a/package.json b/package.json index ad7c9e86e..3fa812b43 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "devDependencies": { "@types/node": "^20.3.1", + "@types/sharedb": "^3.3.9", "chai": "^4.2.0", "coveralls": "^3.0.5", "eslint": "^8.1.0", diff --git a/src/Backend.ts b/src/Backend.ts index 77ee8b8dc..dfbd4aff6 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -1,14 +1,14 @@ import { Model } from './Model'; import * as path from 'path'; import * as util from './util'; -import { RootModel } from './Model/Model'; +import { ModelOptions, RootModel } from './Model/Model'; var Backend = require('sharedb').Backend; export class RacerBackend extends Backend { racer: any; modelOptions: any; - constructor(racer: any, options: any) { + constructor(racer: any, options?: { modelOptions?: ModelOptions }) { super(options); this.racer = racer; this.modelOptions = options && options.modelOptions; @@ -18,7 +18,7 @@ export class RacerBackend extends Backend { }); } - createModel(options: any, req: any) { + createModel(options?: ModelOptions, req?: any) { if (this.modelOptions) { options = (options) ? util.mergeInto(options, this.modelOptions) : diff --git a/src/Model/Model.ts b/src/Model/Model.ts index a4b80ccb7..bad707ba5 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -1,9 +1,11 @@ import { v4 as uuidv4 } from 'uuid'; -import { Context } from 'vm'; +import { type Context } from './contexts'; import { RacerBackend } from '../Backend'; import { type Connection } from './connection'; import { type ModelData } from './collections'; +export type UUID = string; + declare module './Model' { interface DebugOptions { debugMutations?: boolean, @@ -23,13 +25,13 @@ declare module './Model' { type ModelInitFunction = (instance: Model, options: ModelOptions) => void; -export class Model { +export class Model { static INITS: ModelInitFunction[] = []; ChildModel = ChildModel; debug: DebugOptions; root: RootModel; - data: ModelData; + data: T; _at: string; _context: Context; @@ -41,7 +43,7 @@ export class Model { _silent: boolean; - id() { + id(): UUID { return uuidv4(); } @@ -50,7 +52,7 @@ export class Model { }; } -export class RootModel extends Model { +export class RootModel extends Model { backend: RacerBackend; connection: Connection; @@ -65,7 +67,7 @@ export class RootModel extends Model { } } -export class ChildModel extends Model { +export class ChildModel extends Model { constructor(model: Model) { super(); // Shared properties should be accessed via the root. This makes inheritance diff --git a/src/Model/Query.ts b/src/Model/Query.ts index 8cb3863c4..340c35a32 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -1,19 +1,28 @@ import { type Context } from './contexts'; import { type Segments } from './types'; -import { Model } from './Model'; +import { ChildModel, ErrorCallback, Model } from './Model'; import { CollectionMap } from './CollectionMap'; +import { ModelData } from '.'; +import { Doc } from 'sharedb'; var defaultType = require('sharedb/lib/client').types.defaultType; var util = require('../util'); var promisify = util.promisify; +export type QueryOptions = string | { db: any }; + +interface QueryCtor { + new (model: Model, collectionName: string, expression: any, options: QueryOptions): Query; +} + declare module './Model' { interface Model { - _queries: Queries; - query(collectionName, expression, options): Query; - _getOrCreateQuery(collectionName, expression, options, QueryConstructor): Query; + query(collectionName: string, expression, options?: QueryOptions): Query; sanitizeQuery(expression): Query; - _initQueries(items): void; + + _getOrCreateQuery(collectionName: string, expression, options: QueryOptions, QueryConstructor: QueryCtor): Query; + _initQueries(items: any[]): void; + _queries: Queries; } } @@ -21,7 +30,7 @@ Model.INITS.push(function(model) { model.root._queries = new Queries(); }); -Model.prototype.query = function(collectionName, expression, options) { +Model.prototype.query = function(collectionName: string, expression, options?: QueryOptions) { // DEPRECATED: Passing in a string as the third argument specifies the db // option for backward compatibility if (typeof options === 'string') { @@ -74,20 +83,22 @@ Model.prototype.sanitizeQuery = function(expression) { }; // Called during initialization of the bundle on page load. -Model.prototype._initQueries = function(items) { +Model.prototype._initQueries = function(items: any[][]) { for (var i = 0; i < items.length; i++) { var item = items[i]; - var countsList = item[0]; - var collectionName = item[1]; - var expression = item[2]; - var results = item[3] || []; - var options = item[4]; - var extra = item[5]; - - var counts = countsList[0]; - var subscribed = counts[0] || 0; - var fetched = counts[1] || 0; - var contextId = counts[2]; + const [countsList, collectionName, expression, results=[], options, extra] = item; + // var countsList = item[0]; + // var collectionName = item[1]; + // var expression = item[2]; + // var results = item[3] || []; + // var options = item[4]; + // var extra = item[5]; + const [counts] = countsList; + // var counts = countsList[0]; + var [subscribed = 0, fetched = 0, contextId] = counts; + // var subscribed = counts[0] || 0; + // var fetched = counts[1] || 0; + // var contextId = counts[2]; var model = (contextId) ? this.context(contextId) : this; var query = model._getOrCreateQuery(collectionName, expression, options, Query); @@ -160,24 +171,25 @@ export class Queries { }; } -export class Query { - model: Model; - context: Context; +export class Query { collectionName: string; + context: Context; + created: boolean; expression: any; - options: any; - hash: string; - segments: Segments; - idsSegments: string[]; extraSegments: string[]; - _pendingSubscribeCallbacks: any[]; - subscribeCount: number; fetchCount: number; - created: boolean; - shareQuery: any | null; + hash: string; idMap: Record; + idsSegments: string[]; + model: Model; + options: any; + segments: Segments; + shareQuery: any | null; + subscribeCount: number; + + _pendingSubscribeCallbacks: any[]; - constructor(model, collectionName, expression, options) { + constructor(model: ChildModel, collectionName: string, expression: any, options?: any) { // Note that a new childModel based on the root scope is created. Only the // context from the passed in model has an effect this.model = model.root.pass({ $query: this }); @@ -229,7 +241,7 @@ export class Query { this._maybeUnloadDocs(ids); }; - fetch(cb) { + fetch(cb: ErrorCallback) { cb = this.model.wrapCallback(cb); this.context.fetchQuery(this); @@ -238,11 +250,11 @@ export class Query { if (!this.created) this.create(); var query = this; - function fetchCb(err, results, extra) { + function fetchCb(err: Error, results: any[], extra?: string[]) { if (err) return cb(err); query._setExtra(extra); query._setResults(results); - cb(); + cb(); } this.model.root.connection.createFetchQuery( this.collectionName, @@ -293,7 +305,7 @@ export class Query { _subscribeCb(cb) { var query = this; - return function subscribeCb(err, results, extra) { + return function subscribeCb(err: Error, results: Doc[], extra?: any) { if (err) return query._flushSubscribeCallbacks(err, cb); query._setExtra(extra); query._setResults(results); @@ -303,10 +315,10 @@ export class Query { _shareFetchedSubscribe(options, cb) { this.model.root.connection.createFetchQuery( - this.collectionName, + this.collectionName, this.expression, options, - this._subscribeCb(cb) + this._subscribeCb(cb), ); }; @@ -320,7 +332,7 @@ export class Query { this.collectionName, this.expression, options, - this._subscribeCb(cb) + this._subscribeCb(cb), ); this.shareQuery.on('insert', function(shareDocs, index) { var ids = resultsIds(shareDocs); @@ -372,7 +384,7 @@ export class Query { this.idMap[id] = (this.idMap[id] || 0) + 1; } }; - _diffMapIds(ids) { + _diffMapIds(ids: string[]) { var addedIds = []; var removedIds = []; var newMap = {}; @@ -389,7 +401,7 @@ export class Query { if (addedIds.length) this._addMapIds(addedIds); if (removedIds.length) this._removeMapIds(removedIds); }; - _setExtra(extra) { + _setExtra(extra: string[]) { if (extra === undefined) return; this.model._setDiffDeep(this.extraSegments, extra); }; @@ -397,7 +409,7 @@ export class Query { var ids = resultsIds(results); this._setResultIds(ids); }; - _setResultIds(ids) { + _setResultIds(ids: string[]) { this._diffMapIds(ids); this.model._setArrayDiff(this.idsSegments, ids); }; diff --git a/src/Model/collections.ts b/src/Model/collections.ts index b0b8a530f..6fda9bfd3 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -17,17 +17,19 @@ declare module './Model' { data: ModelData; } interface Model { + destroy(subpath?: string): void; + get(subpath?: string): any; + get(subpath?: string): T; getCollection(collecitonName: string): ModelCollections; - getDoc(collecitonName: string, id: string): any | undefined; - get(subpath: string): any; - _get(segments: Segments): any; getCopy(subpath: string): any; - _getCopy(segments: Segments): any; getDeepCopy(subpath: string): any; - _getDeepCopy(segments: Segments): any; + getDoc(collecitonName: string, id: string): any | undefined; getOrCreateCollection(name: string): Collection; getOrCreateDoc(collectionName: string, id: string, data: any); - destroy(subpath?: string): void; + + _get(segments: Segments): any; + _getCopy(segments: Segments): any; + _getDeepCopy(segments: Segments): any; } } diff --git a/src/Model/events.ts b/src/Model/events.ts index 61cec40e1..7f2eda2e5 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -4,37 +4,55 @@ import { EventEmitter } from 'events'; import { EventListenerTree } from './EventListenerTree'; import { type Segments } from './types'; import { Model } from './Model'; - -var mergeInto = require('../util').mergeInto; +import { mergeInto } from '../util'; + +export type ModelEvent = + | ChangeEvent + | InsertEvent + | RemoveEvent + | MoveEvent + | LoadEvent + | UnloadEvent; + +export interface ModelOnEventMap { + change: ChangeEvent; + insert: InsertEvent; + remove: RemoveEvent; + move: MoveEvent; + load: LoadEvent; + unload: UnloadEvent; + all: ModelEvent; +} declare module './Model' { interface Model { - _defaultCallback(err?: Error): void; - _emitError(err: Error, context?: any): void; - wrapCallback(cb: ErrorCallback): ErrorCallback; - _mutationListeners: Record; - _emittingMutation: boolean; - _mutationEventQueue: null; - _eventContextListeners: Record; - _emitMutation(segments: Segments, event: any): void; - _callMutationListeners(type: string, segments: Segments, event: any): void; - __on: typeof EventEmitter.prototype.on; addListener(event: string, listener: any, arg2?: any, arg3?: any): any; + eventContext(id: string): Model; on(event: string, listener: any, arg2?: any, arg3?: any): any; - __once: typeof EventEmitter.prototype.once; once(event: string, listener: any, arg2?: any, arg3?: any): any; - __removeListener: typeof EventEmitter.prototype.removeListener; + pass(object: any, invert?: boolean): Model; + removeAllListeners(type: string, subpath: string): void; + removeContextListeners(): void; removeListener(type: string, listener: any): void; + setMaxListeners(limit: number): void; + silent(value?: boolean): Model; + wrapCallback(cb: ErrorCallback): ErrorCallback; + + __on: typeof EventEmitter.prototype.on; + __once: typeof EventEmitter.prototype.once; __removeAllListeners: typeof EventEmitter.prototype.removeAllListeners; - removeAllListeners(type: string, subpath: string): void; + __removeListener: typeof EventEmitter.prototype.removeListener; + _addMutationListener(type: string, arg1: any, arg2: any, arg3: any): MutationListener; + _callMutationListeners(type: string, segments: Segments, event: any): void; + _defaultCallback(err?: Error): void; + _emitError(err: Error, context?: any): void; + _emitMutation(segments: Segments, event: any): void; + _emittingMutation: boolean; + _eventContextListeners: Record; + _mutationEventQueue: null; + _mutationListeners: Record; _removeAllListeners(type: string, segments: Segments): void; - pass(object: any, invert?: boolean): Model; - silent(value?: boolean): Model; - eventContext(id: string): Model; - removeContextListeners(): void; _removeMutationListener(listener: MutationListener): void; - _addMutationListener(type: string, arg1: any, arg2: any, arg3: any): MutationListener; - setMaxListeners(limit: number): void; } } @@ -156,7 +174,6 @@ Model.prototype._callMutationListeners = function(type, segments, event) { // EventEmitter.prototype.once return `this`. The Model equivalents return // the listener instead, since it is made internally for method subscriptions // and may need to be passed to removeListener. - Model.prototype.__on = EventEmitter.prototype.on; Model.prototype.addListener = Model.prototype.on = function(type, arg1, arg2, arg3) { diff --git a/src/Model/index.ts b/src/Model/index.ts index ed16b1dc3..20139d283 100644 --- a/src/Model/index.ts +++ b/src/Model/index.ts @@ -2,7 +2,7 @@ /// import { serverRequire } from '../util'; -export { Model, ChildModel, RootModel } from './Model'; +export { Model, ChildModel, RootModel, ModelOptions, type UUID } from './Model'; export { ModelData } from './collections'; // Extend model on both server and client // diff --git a/src/Model/paths.ts b/src/Model/paths.ts index 9fd8be5b6..85b2ae9d1 100644 --- a/src/Model/paths.ts +++ b/src/Model/paths.ts @@ -3,12 +3,13 @@ import { Model } from './Model'; exports.mixin = {}; declare module './Model' { - interface Model { + interface Model { _splitPath(subpath: string): string[]; path(subpath: string | number | Model): string; isPath(subpath: string): boolean; - scope(subpath: string): Model; - at(subpath: string): Model; + scope(subpath: string): ChildModel; + scope(): ChildModel; + at(subpath: string): ChildModel; parent(levels?: number): Model; leaf(path: string): string; } @@ -40,7 +41,7 @@ Model.prototype.isPath = function(subpath) { return this.path(subpath) != null; }; -Model.prototype.scope = function(path) { +Model.prototype.scope = function(path?) { if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { path = path + '.' + arguments[i]; diff --git a/src/Model/subscriptions.ts b/src/Model/subscriptions.ts index 847fe8f60..aa3339c89 100644 --- a/src/Model/subscriptions.ts +++ b/src/Model/subscriptions.ts @@ -6,30 +6,40 @@ import * as util from '../util'; const UnloadEvent = mutationEvents.UnloadEvent; const promisify = util.promisify; +/** + * A path string, a `Model`, or a `Query`. + */ +export type Subscribable = string | Model | Query; + declare module './Model' { interface Model { - fetch(): Model; + fetch(callback?: ErrorCallback): Model; fetchPromised(): Promise; - unfetch(): Model; - unfetchPromised(): Promise; - subscribe(): void; - subscribePromised(): Promise; - unsubscribe(): Model; - unsubscribePromised(): Promise; - _forSubscribable(argumentsObject: any, method: any): void; fetchDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; fetchDocPromised(collecitonName: string, id: string): Promise; + fetchOnly: boolean; + + subscribe(callback?: ErrorCallback): void; + subscribe(subscribable: Subscribable, callback?: ErrorCallback): void; + subscribePromised(): Promise; subscribeDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; subscribeDocPromised(collecitonName: string, id: string): Promise; + + unfetch(): Model; + unfetchPromised(): Promise; unfetchDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; unfetchDocPromised(collecitonName: string, id: string): Promise; + unloadDelay: number; + + unsubscribe(): Model; + unsubscribePromised(): Promise; unsubscribeDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; unsubscribeDocPromised(collecitonName: string, id: string): Promise; - _maybeUnloadDoc(collecitonName: string, id: string): void; - _hasDocReferences(collecitonName: string, id: string): boolean; - fetchOnly: boolean; - unloadDelay: number; + _fetchedDocs: CollectionCounter; + _forSubscribable(argumentsObject: any, method: any): void; + _hasDocReferences(collecitonName: string, id: string): boolean; + _maybeUnloadDoc(collecitonName: string, id: string): void; _subscribedDocs: CollectionCounter; } } diff --git a/src/Racer.ts b/src/Racer.ts index 6f3a73a78..16109f18e 100644 --- a/src/Racer.ts +++ b/src/Racer.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import { Model } from './Model'; +import { Model, RootModel } from './Model'; import * as util from './util'; export class Racer extends EventEmitter { @@ -13,7 +13,7 @@ export class Racer extends EventEmitter { } createModel(data) { - var model = new Model(); + var model = new RootModel(); if (data) { model.createConnection(data); model.unbundle(data); diff --git a/src/index.ts b/src/index.ts index 9f7df2b2c..c871ca6ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,24 @@ import { Racer } from './Racer'; import { Model, ModelData } from './Model'; import * as util from './util'; +import type { ShareDBOptions } from 'sharedb'; import { RacerBackend } from './Backend'; +import { ModelOptions, RootModel } from './Model'; export { Query } from './Model/Query'; -export { ChildModel, RootModel } from './Model'; +export { ChildModel, type UUID } from './Model'; const { use, serverUse } = util; +type BackendOptions = { modelOptions?: ModelOptions } & ShareDBOptions; + export { Model, ModelData, + ModelOptions, Racer, RacerBackend, + RootModel, use, serverUse, util, @@ -21,7 +27,7 @@ export { export const racer = new Racer(); export function createModel(data) { - var model = new Model(); + var model = new RootModel(); if (data) { model.createConnection(data); model.unbundle(data); @@ -29,6 +35,6 @@ export function createModel(data) { return model; } -export function createBackend(options) { +export function createBackend(options?: BackendOptions) { return new RacerBackend(racer, options); -}; +} From 77e2533f0c05e1e785530a5e0403697a1ded2153 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 29 Jan 2024 14:23:28 -0800 Subject: [PATCH 331/479] Ensure promisify methods return Promise --- src/Model/subscriptions.ts | 26 ++++++++++++++++---------- src/util.ts | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Model/subscriptions.ts b/src/Model/subscriptions.ts index aa3339c89..99288268d 100644 --- a/src/Model/subscriptions.ts +++ b/src/Model/subscriptions.ts @@ -12,29 +12,35 @@ const promisify = util.promisify; export type Subscribable = string | Model | Query; declare module './Model' { - interface Model { - fetch(callback?: ErrorCallback): Model; - fetchPromised(): Promise; + interface Model { + fetch(items: Subscribable[], cb?: ErrorCallback): Model; + fetch(item: Subscribable, cb?: ErrorCallback): Model; + fetch(cb?: ErrorCallback): Model; + + fetchPromised(items: Subscribable[]): Promise; + fetchPromised(item: Subscribable): Promise; + fetchPromised(): Promise; + fetchDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; - fetchDocPromised(collecitonName: string, id: string): Promise; + fetchDocPromised(collecitonName: string, id: string): Promise; fetchOnly: boolean; subscribe(callback?: ErrorCallback): void; subscribe(subscribable: Subscribable, callback?: ErrorCallback): void; - subscribePromised(): Promise; + subscribePromised(): Promise; subscribeDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; - subscribeDocPromised(collecitonName: string, id: string): Promise; + subscribeDocPromised(collecitonName: string, id: string): Promise; unfetch(): Model; - unfetchPromised(): Promise; + unfetchPromised(): Promise; unfetchDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; - unfetchDocPromised(collecitonName: string, id: string): Promise; + unfetchDocPromised(collecitonName: string, id: string): Promise; unloadDelay: number; unsubscribe(): Model; - unsubscribePromised(): Promise; + unsubscribePromised(): Promise; unsubscribeDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; - unsubscribeDocPromised(collecitonName: string, id: string): Promise; + unsubscribeDocPromised(collecitonName: string, id: string): Promise; _fetchedDocs: CollectionCounter; _forSubscribable(argumentsObject: any, method: any): void; diff --git a/src/util.ts b/src/util.ts index 7b41169ee..89960084a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -155,7 +155,7 @@ export function promisify(original) { function fn() { var promiseResolve, promiseReject; - var promise = new Promise(function(resolve, reject) { + var promise = new Promise(function(resolve, reject) { promiseResolve = resolve; promiseReject = reject; }); From 7dfa054e1bed49133669fdfad61fb2f24a536d6c Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 29 Jan 2024 14:27:05 -0800 Subject: [PATCH 332/479] Add override signatures and add doc blocks --- src/Model/subscriptions.ts | 58 +++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/Model/subscriptions.ts b/src/Model/subscriptions.ts index 99288268d..b939ec3d5 100644 --- a/src/Model/subscriptions.ts +++ b/src/Model/subscriptions.ts @@ -13,6 +13,14 @@ export type Subscribable = string | Model | Query; declare module './Model' { interface Model { + /** + * Retrieve data from the server, loading it into the model. + * + * @param items + * @param cb + * + * @see https://derbyjs.com/docs/derby-0.10/models/backends#loading-data-into-a-model + */ fetch(items: Subscribable[], cb?: ErrorCallback): Model; fetch(item: Subscribable, cb?: ErrorCallback): Model; fetch(cb?: ErrorCallback): Model; @@ -25,19 +33,61 @@ declare module './Model' { fetchDocPromised(collecitonName: string, id: string): Promise; fetchOnly: boolean; - subscribe(callback?: ErrorCallback): void; - subscribe(subscribable: Subscribable, callback?: ErrorCallback): void; + /** + * Retrieve data from the server, loading it into the model. In addition, + * subscribe to the items, such that updates from any other client will + * automatically get reflected in this client's model. + * + * Any item that's already subscribed will not result in a network call. + * + * @param items + * @param cb + * + * @see https://derbyjs.com/docs/derby-0.10/models/backends#loading-data-into-a-model + */ + subscribe(items: Subscribable[], cb?: ErrorCallback): Model; + subscribe(item: Subscribable, cb?: ErrorCallback): Model; + subscribe(cb?: ErrorCallback): Model; + subscribePromised(): Promise; subscribeDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; subscribeDocPromised(collecitonName: string, id: string): Promise; - unfetch(): Model; + /** + * The reverse of `#fetch`, marking the items as no longer needed in the + * model. + * + * @param items + * @param cb + * + * @see https://derbyjs.com/docs/derby-0.10/models/backends#loading-data-into-a-model + */ + unfetch(items: Subscribable[], cb?: ErrorCallback): Model; + unfetch(item: Subscribable, cb?: ErrorCallback): Model; + unfetch(cb?: ErrorCallback): Model; + + unfetchPromised(items: Subscribable[]): Promise; + unfetchPromised(item: Subscribable): Promise; unfetchPromised(): Promise; + unfetchDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; unfetchDocPromised(collecitonName: string, id: string): Promise; unloadDelay: number; - unsubscribe(): Model; + + /** + * The reverse of `#subscribe`, marking the items as no longer needed in the + * model. + * + * @param items + * @param cb + * + * @see https://derbyjs.com/docs/derby-0.10/models/backends#loading-data-into-a-model + */ + unsubscribe(items: Subscribable[], cb?: ErrorCallback): Model; + unsubscribe(item: Subscribable, cb?: ErrorCallback): Model; + unsubscribe(cb?: ErrorCallback): Model; + unsubscribePromised(): Promise; unsubscribeDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; unsubscribeDocPromised(collecitonName: string, id: string): Promise; From 240dd7a772e05dea56563ba21132c8889d72bd06 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 29 Jan 2024 14:30:15 -0800 Subject: [PATCH 333/479] Export Subscribable type --- src/Model/index.ts | 1 + src/index.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Model/index.ts b/src/Model/index.ts index 20139d283..5083fc94b 100644 --- a/src/Model/index.ts +++ b/src/Model/index.ts @@ -4,6 +4,7 @@ import { serverRequire } from '../util'; export { Model, ChildModel, RootModel, ModelOptions, type UUID } from './Model'; export { ModelData } from './collections'; +export { type Subscribable } from './subscriptions'; // Extend model on both server and client // import './unbundle'; diff --git a/src/index.ts b/src/index.ts index c871ca6ea..3887ae757 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import type { ShareDBOptions } from 'sharedb'; import { RacerBackend } from './Backend'; import { ModelOptions, RootModel } from './Model'; export { Query } from './Model/Query'; -export { ChildModel, type UUID } from './Model'; +export { ChildModel, type UUID, type Subscribable } from './Model'; const { use, serverUse } = util; From 15c8f0c745ab3f1344830da03a39a301dbd242a6 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 29 Jan 2024 14:48:00 -0800 Subject: [PATCH 334/479] Add utility types --- src/index.ts | 1 + src/types.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/types.ts diff --git a/src/index.ts b/src/index.ts index 3887ae757..172f8e071 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { RacerBackend } from './Backend'; import { ModelOptions, RootModel } from './Model'; export { Query } from './Model/Query'; export { ChildModel, type UUID, type Subscribable } from './Model'; +export type { ReadonlyDeep } from './types'; const { use, serverUse } = util; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..d64774c29 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,64 @@ +import { Model } from "./Model"; + +// +// Simple and utility types +// +export type UUID = string; +export type Path = string | number; +export type PathSegment = string | number; +export type PathLike = Path | Model; + +export type Primitive = boolean | number | string | null | undefined; + +/** + * Transforms a JSON-compatible type `T` such that it and any nested arrays + * or basic objects are read-only. + * + * Warnings: + * * Instances of most classes could still be modified via methods, aside from + * built-in `Array`s, which are transformed to `ReadonlyArray`s with no + * mutator methods. + * * This only affects static type-checking and is not a guarantee of run-time + * immutability. Values with this type could still be modified if casted + * to `any`, passed into a function with an `any` signature, or by untyped + * JavaScript. + * + * Use `deepCopy(value)` to get a fully mutable copy of a `ReadonlyDeep`. + */ +export type ReadonlyDeep = T extends Primitive ? T : { readonly [K in keyof T]: ReadonlyDeep }; + +/** + * Transforms a JSON-compatible type `T` such that top-level properties remain + * mutable if they were before, but nested arrays or basic objects become + * read-only. + * + * Warning: This does not fully guarantee immutability. See `ReadonlyDeep` for + * more details. + */ +export type ShallowCopiedValue = T extends Primitive ? T : { [K in keyof T]: ReadonlyDeep }; + +/** + * Transforms the input JSON-compatible type `T` to be fully mutable, if it + * isn't already. + * + * This should only be used for the return type of a deep-copy function. Do + * not manually cast using this type otherwise. + */ +export type MutableDeep = T extends ReadonlyDeep + ? Date + : T extends object + ? { -readonly [K in keyof T]: MutableDeep } + : T; + +/** + * Transforms the input JSON-compatible type `T` such that its top-level + * properties are mutable, if they weren't already. + * + * This should only be used for the return type of a shallow-copy function. Do + * not manually cast using this type otherwise. + */ +export type MutableShallow = T extends ShallowCopiedValue + ? Date + : T extends object + ? { -readonly [K in keyof T]: T[K] } + : T; From 608aa0d7589515d805295f409546cfddfd976e55 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 30 Jan 2024 14:03:14 -0800 Subject: [PATCH 335/479] Add subscribePromised overrides; publish utility types --- src/Model/subscriptions.ts | 3 +++ src/index.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Model/subscriptions.ts b/src/Model/subscriptions.ts index b939ec3d5..9c610c8c3 100644 --- a/src/Model/subscriptions.ts +++ b/src/Model/subscriptions.ts @@ -49,7 +49,10 @@ declare module './Model' { subscribe(item: Subscribable, cb?: ErrorCallback): Model; subscribe(cb?: ErrorCallback): Model; + subscribePromised(items: Subscribable[]): Promise; + subscribePromised(item: Subscribable): Promise; subscribePromised(): Promise; + subscribeDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; subscribeDocPromised(collecitonName: string, id: string): Promise; diff --git a/src/index.ts b/src/index.ts index 172f8e071..e6eabe9bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { RacerBackend } from './Backend'; import { ModelOptions, RootModel } from './Model'; export { Query } from './Model/Query'; export { ChildModel, type UUID, type Subscribable } from './Model'; -export type { ReadonlyDeep } from './types'; +export type { ReadonlyDeep, Path, PathLike, PathSegment } from './types'; const { use, serverUse } = util; From 3f0323e5bb0703e0f6d4fadcb553abf26063a31e Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 30 Jan 2024 16:37:06 -0800 Subject: [PATCH 336/479] Use @types/sharedb with ShareDBOptions type --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3fa812b43..67da94b06 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "devDependencies": { "@types/node": "^20.3.1", - "@types/sharedb": "^3.3.9", + "@types/sharedb": "^3.3.10", "chai": "^4.2.0", "coveralls": "^3.0.5", "eslint": "^8.1.0", From 0dfea7a1a5048ff67a0700640da10905322f4140 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 30 Jan 2024 16:38:42 -0800 Subject: [PATCH 337/479] Prefer export {} over import and re-export for easier module augmentation in other libraries --- src/index.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index e6eabe9bf..861414618 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,19 @@ import { Racer } from './Racer'; -import { Model, ModelData } from './Model'; import * as util from './util'; import type { ShareDBOptions } from 'sharedb'; import { RacerBackend } from './Backend'; import { ModelOptions, RootModel } from './Model'; + export { Query } from './Model/Query'; -export { ChildModel, type UUID, type Subscribable } from './Model'; +export { Model, ChildModel, ModelData, type UUID, type Subscribable } from './Model'; export type { ReadonlyDeep, Path, PathLike, PathSegment } from './types'; const { use, serverUse } = util; -type BackendOptions = { modelOptions?: ModelOptions } & ShareDBOptions; +export type BackendOptions = { modelOptions?: ModelOptions } & ShareDBOptions; export { - Model, - ModelData, ModelOptions, Racer, RacerBackend, From 0d51599a1991c8e1b3d6a4dbc8f869d434a4ac2b Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 30 Jan 2024 16:40:56 -0800 Subject: [PATCH 338/479] 2.0.0-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 67da94b06..21a4f574b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "main": "./lib/index.js", "files": [ "lib/*" From f863c9781480a50ad5a980f81ffe77df3128362c Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 5 Feb 2024 17:14:21 -0800 Subject: [PATCH 339/479] Type refinements --- src/Model/Doc.ts | 4 +- src/Model/Query.ts | 20 +++++----- src/Model/collections.ts | 86 +++++++++++++++++++++++++++++++--------- src/util.ts | 12 +++--- 4 files changed, 87 insertions(+), 35 deletions(-) diff --git a/src/Model/Doc.ts b/src/Model/Doc.ts index 885e7a791..999e605b8 100644 --- a/src/Model/Doc.ts +++ b/src/Model/Doc.ts @@ -3,10 +3,10 @@ import { type Segments } from './types'; import { Collection } from './collections'; export class Doc { - collectionName: string; - id: string; collectionData: Model; + collectionName: string; data: any; + id: string; model: Model; constructor(model: Model, collectionName: string, id: string, data: any, _collection?: Collection) { diff --git a/src/Model/Query.ts b/src/Model/Query.ts index 340c35a32..fdb6a2c86 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -3,7 +3,9 @@ import { type Segments } from './types'; import { ChildModel, ErrorCallback, Model } from './Model'; import { CollectionMap } from './CollectionMap'; import { ModelData } from '.'; -import { Doc } from 'sharedb'; +import type { Doc } from './Doc'; +import type { Doc as ShareDBDoc } from 'sharedb'; +import type { RemoteDoc } from './RemoteDoc'; var defaultType = require('sharedb/lib/client').types.defaultType; var util = require('../util'); @@ -267,7 +269,7 @@ export class Query { fetchPromised = promisify(Query.prototype.fetch); - subscribe(cb) { + subscribe(cb: ErrorCallback) { cb = this.model.wrapCallback(cb); this.context.subscribeQuery(this); @@ -303,9 +305,9 @@ export class Query { subscribePromised = promisify(Query.prototype.subscribe); - _subscribeCb(cb) { + _subscribeCb(cb: ErrorCallback) { var query = this; - return function subscribeCb(err: Error, results: Doc[], extra?: any) { + return function subscribeCb(err: Error, results: ShareDBDoc[], extra?: any) { if (err) return query._flushSubscribeCallbacks(err, cb); query._setExtra(extra); query._setResults(results); @@ -405,7 +407,7 @@ export class Query { if (extra === undefined) return; this.model._setDiffDeep(this.extraSegments, extra); }; - _setResults(results) { + _setResults(results: ShareDBDoc[]) { var ids = resultsIds(results); this._setResultIds(ids); }; @@ -507,7 +509,7 @@ export class Query { var results = []; for (var i = 0; i < ids.length; i++) { var id = ids[i]; - var doc = collection.docs[id]; + var doc = collection.docs[id] as RemoteDoc; results.push(doc && doc.shareDoc); } return results; @@ -526,7 +528,7 @@ export class Query { var collection = this.model.getCollection(this.collectionName); for (var i = 0, l = ids.length; i < l; i++) { var id = ids[i]; - var doc = collection && collection.docs[id]; + var doc = (collection && collection.docs[id]) as RemoteDoc; results.push(doc && doc.get()); } return results; @@ -564,7 +566,7 @@ export class Query { results = []; for (var i = 0; i < ids.length; i++) { var id = ids[i]; - var doc = collection.docs[id]; + var doc = collection.docs[id] as RemoteDoc; if (doc) { delete collection.docs[id]; var data = doc.shareDoc.data; @@ -610,7 +612,7 @@ function queryHash(contextId, collectionName, expression, options) { return JSON.stringify(args).replace(/\./g, '|'); } -function resultsIds(results) { +function resultsIds(results: { id: string }[]): string[] { var ids = []; for (var i = 0; i < results.length; i++) { var shareDoc = results[i]; diff --git a/src/Model/collections.ts b/src/Model/collections.ts index 6fda9bfd3..c32c38403 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -1,28 +1,78 @@ import { type Segments } from './types'; import { Doc } from './Doc'; import { Model, RootModel } from './Model'; +import { JSONObject } from 'sharedb/lib/sharedb'; +import { VerifyJsonWebKeyInput } from 'crypto'; +import { Path, ReadonlyDeep, ShallowCopiedValue } from '../types'; var LocalDoc = require('./LocalDoc'); var util = require('../util'); export class ModelCollections { docs: Record; } -export class ModelData {} -export class DocMap {} -export class CollectionData {} + +/** Root model data */ +export class ModelData { + [collectionName: string]: CollectionData; +} + +class DocMap { + [id: string]: Doc; +} + +/** Dictionary of document id to document data */ +export class CollectionData { + [id: string]: T; +} declare module './Model' { interface RootModel { collections: ModelCollections; data: ModelData; } - interface Model { + interface Model { destroy(subpath?: string): void; - get(subpath?: string): any; - get(subpath?: string): T; - getCollection(collecitonName: string): ModelCollections; - getCopy(subpath: string): any; - getDeepCopy(subpath: string): any; + + /** + * Gets the value located at this model's path or a relative subpath. + * + * If no value exists at the path, this returns `undefined`. + * + * _Note:_ The value is returned by reference, and object values should not + * be directly modified - use the Model mutator methods instead. The + * TypeScript compiler will enforce no direct modifications, but there are + * no runtime guards, which means JavaScript source code could still + * improperly make direct modifications. + * + * @param subpath + */ + get(subpath: Path): ReadonlyDeep | undefined; + get(): ReadonlyDeep | undefined; + + getCollection(collectionName: string): Collection; + + /** + * Gets a shallow copy of the value located at this model's path or a relative + * subpath. + * + * If no value exists at the path, this returns `undefined`. + * + * @param subpath + */ + getCopy(subpath: Path): ShallowCopiedValue | undefined; + getCopy(): ShallowCopiedValue | undefined; + + /** + * Gets a deep copy of the value located at this model's path or a relative + * subpath. + * + * If no value exists at the path, this returns `undefined`. + * + * @param subpath + */ + getDeepCopy(subpath: Path): S | undefined; + getDeepCopy(): T | undefined; + getDoc(collecitonName: string, id: string): any | undefined; getOrCreateCollection(name: string): Collection; getOrCreateDoc(collectionName: string, id: string, data: any); @@ -47,18 +97,18 @@ Model.prototype.getDoc = function(collectionName, id) { return collection && collection.docs[id]; }; -Model.prototype.get = function(subpath) { +Model.prototype.get = function(subpath?: Path) { var segments = this._splitPath(subpath); - return this._get(segments); + return this._get(segments) as ReadonlyDeep; }; Model.prototype._get = function(segments) { return util.lookup(segments, this.root.data); }; -Model.prototype.getCopy = function(subpath) { +Model.prototype.getCopy = function(subpath?: Path) { var segments = this._splitPath(subpath); - return this._getCopy(segments); + return this._getCopy(segments) as ReadonlyDeep; }; Model.prototype._getCopy = function(segments) { @@ -66,9 +116,9 @@ Model.prototype._getCopy = function(segments) { return util.copy(value); }; -Model.prototype.getDeepCopy = function(subpath) { +Model.prototype.getDeepCopy = function(subpath?: Path) { var segments = this._splitPath(subpath); - return this._getDeepCopy(segments); + return this._getDeepCopy(segments) as S; }; Model.prototype._getDeepCopy = function(segments) { @@ -135,12 +185,12 @@ Model.prototype.destroy = function(subpath) { } }; -export class Collection { +export class Collection { model: RootModel; name: string; size: number; docs: DocMap; - data: CollectionData; + data: CollectionData; Doc: typeof Doc; constructor(model: RootModel, name: string, docClass: typeof Doc) { @@ -149,7 +199,7 @@ export class Collection { this.Doc = docClass; this.size = 0; this.docs = new DocMap(); - this.data = model.data[name] = new CollectionData(); + this.data = model.data[name] = new CollectionData(); } /** diff --git a/src/util.ts b/src/util.ts index 89960084a..4fe7aa7ed 100644 --- a/src/util.ts +++ b/src/util.ts @@ -112,11 +112,11 @@ export function equalsNaN(x) { return x !== x; } -export function isArrayIndex(segment) { +export function isArrayIndex(segment: string): boolean { return (/^[0-9]+$/).test(segment); } -export function lookup(segments, value) { +export function lookup(segments: string[], value: unknown): unknown { if (!segments) return value; for (var i = 0, len = segments.length; i < len; i++) { @@ -126,14 +126,14 @@ export function lookup(segments, value) { return value; } -export function mayImpactAny(segmentsList, testSegments) { +export function mayImpactAny(segmentsList: string[][], testSegments: string[]) { for (var i = 0, len = segmentsList.length; i < len; i++) { if (mayImpact(segmentsList[i], testSegments)) return true; } return false; } -export function mayImpact(segments, testSegments) { +export function mayImpact(segments: string[], testSegments: string[]): boolean { var len = Math.min(segments.length, testSegments.length); for (var i = 0; i < len; i++) { if (segments[i] !== testSegments[i]) return false; @@ -186,13 +186,13 @@ export function serverRequire(module, id) { return module.require(id); } -export function serverUse(module, id, options) { +export function serverUse(module, id: string, options?: unknown) { if (!isServer) return this; var plugin = module.require(id); return this.use(plugin, options); } -export function use(plugin, options) { +export function use(plugin, options?: unknown) { // Don't include a plugin more than once var plugins = this._plugins || (this._plugins = []); if (plugins.indexOf(plugin) === -1) { From b4896b1b2a346b5a75b4c671a030d9998705467a Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 6 Feb 2024 13:07:53 -0800 Subject: [PATCH 340/479] 2.0.0-beta.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 21a4f574b..ac0511249 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.3", + "version": "2.0.0-beta.4", "main": "./lib/index.js", "files": [ "lib/*" From 521530bb2365d45a4f1a17abfc24dba9a47c4c05 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 12 Feb 2024 16:58:59 -0800 Subject: [PATCH 341/479] Refine types for filter, sort, start --- src/Model/filter.ts | 81 ++++++++++++++++++++++++++-- src/Model/fn.ts | 126 +++++++++++++++++++++++++++++++++++++++++--- src/Model/ref.ts | 2 +- 3 files changed, 197 insertions(+), 12 deletions(-) diff --git a/src/Model/filter.ts b/src/Model/filter.ts index 69e58dbbf..ce6f4874b 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -2,13 +2,84 @@ var util = require('../util'); import { Model } from './Model'; import { type Segments } from './types'; import * as defaultFns from './defaultFns'; +import { PathLike } from '../types'; + +interface PaginationOptions { + skip: number; + limit: number; +} declare module './Model' { interface Model { - _filters: Filters; - filter: () => any; - sort: () => any; + /** + * Creates a live-updating list from items in an object, which results in + * automatically updating as the input items change. + * + * @param inputPath - Path pointing to an object or array. The path's value is + * retrieved via model.get(), and each item checked against filter function + * @param additionalInputPaths - Other parameters can be set in the model, and + * the filter function will be re-evaluated when these parameters change as well. + * @param options + * skip - The number of first results to skip + * limit - The maximum number of results. A limit of zero is equivalent to no limit. + * @param fn - A function or the name of a function defined via model.fn(). The function + * should have the arguments function(item, key, object, additionalInputs...) + * + * @see https://derbyjs.com/docs/derby-0.10/models/filters-and-sorts + */ + filter( + inputPath: PathLike, + additionalInputPaths: PathLike[], + options: PaginationOptions, + fn: (item: S, key: string, object: { [key: string]: S }) => boolean + ): Filter; + filter( + inputPath: PathLike, + additionalInputPaths: PathLike[], + fn: (item: S, key: string, object: { [key: string]: S }) => boolean + ): Filter; + filter( + inputPath: PathLike, + options: PaginationOptions, + fn: (item: S, key: string, object: { [key: string]: S }) => boolean + ): Filter; + filter( + inputPath: PathLike, + fn: (item: S, key: string, object: { [key: string]: S }) => boolean + ): Filter; + removeAllFilters: (subpath: string) => void; + + /** + * Creates a live-updating list from items in an object, which results in + * automatically updating as the input items change. The results are sorted by ascending order (default) or by a provided 'fn' parameter. + * + * @param inputPath - Path pointing to an object or array. The path's value is + * retrieved via model.get(), and each item checked against filter function + * @param additionalInputPaths - Other parameters can be set in the model, and + * the filter function will be re-evaluated when these parameters change as well. + * @param options + * skip - The number of first results to skip + * limit - The maximum number of results. A limit of zero is equivalent to no limit. + * @param fn - A function or the name of a function defined via model.fn(). + * + * @see https://derbyjs.com/docs/derby-0.10/models/filters-and-sorts + */ + sort( + inputPath: PathLike, + additionalInputPaths: PathLike[], + options: PaginationOptions, + fn: (a: S, b: S) => number + ): Filter; + sort( + inputPath: PathLike, + additionalInputPaths: PathLike[], + fn: (a: S, b: S) => number + ): Filter; + sort(inputPath: PathLike, options: PaginationOptions, fn: (a: S, b: S) => number): Filter; + sort(inputPath: PathLike, fn: (a: S, b: S) => number): Filter; + + _filters: Filters; _removeAllFilters: (segments: Segments) => void; } } @@ -127,7 +198,7 @@ class Filters{ }; } -export class Filter { +export class Filter { bundle: boolean; filterFn: any; filterName: string; @@ -138,7 +209,7 @@ export class Filter { inputPaths: any; inputsSegments: Segments[]; limit: number; - model: Model; + model: Model; options: any; path: string; segments: Segments; diff --git a/src/Model/fn.ts b/src/Model/fn.ts index f988742d9..7236e5b09 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -3,20 +3,134 @@ import { Model } from './Model'; import { EventListenerTree } from './EventListenerTree'; import { EventMapTree } from './EventMapTree'; import * as defaultFns from './defaultFns'; +import { PathLike, ReadonlyDeep } from '../types'; var util = require('../util'); class NamedFns { } +type StartFnParam = string | number | boolean | null | undefined | ReadonlyDeep; + +interface ModelStartOptions { + /** + * Whether to deep-copy the input/output of the reactive function. + * + * - `output` (default) + * - `input` + * - `both` + * - `none` + */ + copy?: 'output' | 'input' | 'both' | 'none'; + /** + * Comparison mode for the output of the reactive function, when determining + * whether and how to update the output path based on the function's return + * value. + * + * - `'diffDeep'` (default) - Do a recursive deep-equal comparison on old + * and new output values, attempting to issue fine-grained ops on subpaths + * where possible. + * - `'diff` - Do an identity comparison (`===`) on the output value, and do + * a simple set if old and new outputs are different. + * - `'arrayDeep'` - Compare old and new arrays item-by-item using a + * deep-equal comparison for each item, issuing top-level array insert, + * remove,, and move ops as needed. Unlike `'diffDeep'`, this will _not_ + * issue ops inside array items. + * - `'array'` - Compare old and new arrays item-by-item using identity + * comparison (`===`) for each item, issuing top-level array insert, + * remove,, and move ops as needed. + */ + mode?: 'diffDeep' | 'diff' | 'arrayDeep' | 'array'; + /** + * If true, then upon input changes, defer evaluation of the function to the + * next tick, instead of immediately evaluating the function upon each input + * change. + * + * _Warning:_ Avoid using `async: true` if there's any controller code that + * does a `model.get()` on the output path or on any paths downstream of the + * output, since changes to an input path won't immediately result in the + * output being updated. + */ + async?: boolean; +} + declare module './Model' { interface Model { - _namedFns: NamedFns; - _fns: Fns; - fn(name: string, fns: Fns): void; - evaluate(): any; - start(): any; + /** + * Call the function with the values at the input paths, returning the value + * on completion. Unlike `start`, this only occurs once and does not create + * a listener for updating based on changes. + * + * The function should be a pure function - it should always return the same + * result given the same inputs, and it should be side-effect free. + * + * @param inputPaths + * @param options + * @param fn + * + * @see https://derbyjs.com/docs/derby-0.10/models/reactive-functions + */ + evaluate( + inputPaths: PathLike[], + options: ModelStartOptions, + fn: (...inputs: Ins) => Out + ): Out; + evaluate( + inputPaths: PathLike[], + fn: (...inputs: Ins) => Out + ): Out; + + /** + * Defines a named reactive function. + * + * It's not recommended to use this in most cases. Instead, to share reactive functions, + * have the components import a shared function to pass to `model.start`. + * + * @param name name of the function to define + * @param fn either a reactive function that accepts inputs and returns output, or + * a `{ get: Function; set: Function }` object defining a two-way reactive function + */ + fn( + name: string, + fn: (...inputs: Ins) => Out | + { + get(...inputs: Ins): Out; + set(output: Out, ...inputs: Ins): void + } + ): void; + + /** + * Call the function with the values at the input paths, writing the return + * value to the output path. In addition, whenever any of the input values + * change, re-invoke the function and set the new return value to the output + * path. + * + * The function should be a pure function - it should always return the same + * result given the same inputs, and it should be side-effect free. + * + * @param outputPath + * @param inputPaths + * @param options + * @param fn - function or the name of function defined via model.fn() + * + * @see https://derbyjs.com/docs/derby-0.10/models/reactive-functions + */ + start( + outputPath: PathLike, + inputPaths: PathLike[], + options: ModelStartOptions, + fn: ((...inputs: Ins) => Out) | string + ): Out; + start( + outputPath: PathLike, + inputPaths: PathLike[], + fn: ((...inputs: Ins) => Out) | string + ): Out; + stop(subpath: string): void; - _stop(segments: Segments): void; stopAll(subpath: string): void; + + _fns: Fns; + _namedFns: NamedFns; + _stop(segments: Segments): void; _stopAll(segments: Segments): void; } } diff --git a/src/Model/ref.ts b/src/Model/ref.ts index ae2e61a2e..130895d42 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -5,7 +5,7 @@ import { type Segments } from './types'; import { type Filter } from './filter'; import { type Query } from './Query'; -type Refable = string | number | Model | Query | Filter; +type Refable = string | number | Model | Query | Filter; declare module './Model' { interface Model { From b1c546fa43414a2080bd4a3ba239822fa053abbc Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 12 Feb 2024 17:09:13 -0800 Subject: [PATCH 342/479] Allow model.start to directly accept 2-way reactive functions --- src/Model/fn.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Model/fn.ts b/src/Model/fn.ts index 7236e5b09..e165a0539 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -10,6 +10,13 @@ class NamedFns { } type StartFnParam = string | number | boolean | null | undefined | ReadonlyDeep; +type ModelFn = + (...inputs: Ins) => Out | + { + get(...inputs: Ins): Out, + set(output: Out, ...inputs: Ins): void, + }; + interface ModelStartOptions { /** * Whether to deep-copy the input/output of the reactive function. @@ -109,20 +116,22 @@ declare module './Model' { * @param outputPath * @param inputPaths * @param options - * @param fn - function or the name of function defined via model.fn() - * + * @param fn - a reactive function that accepts inputs and returns output; + * a `{ get: Function; set: Function }` object defining a two-way reactive function; + * or the name of a function defined via model.fn() + * * @see https://derbyjs.com/docs/derby-0.10/models/reactive-functions */ start( outputPath: PathLike, inputPaths: PathLike[], options: ModelStartOptions, - fn: ((...inputs: Ins) => Out) | string + fn: ModelFn | string ): Out; start( outputPath: PathLike, inputPaths: PathLike[], - fn: ((...inputs: Ins) => Out) | string + fn: ModelFn | string ): Out; stop(subpath: string): void; From 95adb598f86f4540c8a4dc58b468fd0cb82fb7e4 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 12 Feb 2024 17:33:35 -0800 Subject: [PATCH 343/479] Refine connection types --- src/Model/connection.ts | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Model/connection.ts b/src/Model/connection.ts index 0cd72b67b..376fedfa6 100644 --- a/src/Model/connection.ts +++ b/src/Model/connection.ts @@ -3,6 +3,7 @@ import { Model } from './Model'; import { type Doc} from './Doc'; import { LocalDoc} from './LocalDoc'; import {RemoteDoc} from './RemoteDoc'; +import type Agent = require('sharedb/lib/agent'); var promisify = require('../util').promisify; export { type Connection }; @@ -11,21 +12,37 @@ declare module './Model' { interface DocConstructor { new (any: unknown[]): DocConstructor; } - interface Model { - _finishCreateConnection(): void; - _getDocConstructor(name: string): any; - _isLocal(name: string): boolean; - allowCompose(): Model; + interface Model { + /** Returns a child model where ShareDB operations are always composed. */ + allowCompose(): ChildModel; close(cb: (err?: Error) => void): void; closePromised: Promise; disconnect(): void; - getAgent(): any; + + /** + * Returns a reference to the ShareDB agent if it is connected directly on the + * server. Will return null if the ShareDB connection has been disconnected or + * if we are not in the same process and we do not have a reference to the + * server-side agent object + */ + getAgent(): Agent; + hasPending(): boolean; hasWritePending(): boolean; - preventCompose(): Model; + /** Returns a child model where ShareDB operations are never composed. */ + preventCompose(): ChildModel; reconnect(): void; + + /** + * Calls the callback once all pending operations, fetches, and subscribes + * have settled. + */ whenNothingPending(cb: () => void): void; whenNothingPendingPromised(): Promise; + + _finishCreateConnection(): void; + _getDocConstructor(name: string): any; + _isLocal(name: string): boolean; } } From be9186a43d47a0ba01f3c780f265f92f5b72c90b Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 13 Feb 2024 08:46:59 -0800 Subject: [PATCH 344/479] 2.0.0-beta.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac0511249..eccf710d8 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "main": "./lib/index.js", "files": [ "lib/*" From 5c205bd928252dc6ae6db2564b6602a99b589dfe Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 15 Feb 2024 14:47:14 -0800 Subject: [PATCH 345/479] Fix miscalled superclass --- src/Model/Doc.ts | 2 +- src/Model/LocalDoc.ts | 1 - src/Model/RemoteDoc.ts | 4 +--- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Model/Doc.ts b/src/Model/Doc.ts index 999e605b8..6ad40848b 100644 --- a/src/Model/Doc.ts +++ b/src/Model/Doc.ts @@ -9,7 +9,7 @@ export class Doc { id: string; model: Model; - constructor(model: Model, collectionName: string, id: string, data: any, _collection?: Collection) { + constructor(model: Model, collectionName: string, id: string, data?: any, _collection?: Collection) { this.collectionName = collectionName; this.id = id; this.data = data; diff --git a/src/Model/LocalDoc.ts b/src/Model/LocalDoc.ts index cd6e8e689..435822741 100644 --- a/src/Model/LocalDoc.ts +++ b/src/Model/LocalDoc.ts @@ -5,7 +5,6 @@ var util = require('../util'); export class LocalDoc extends Doc{ constructor(model: Model, collectionName: string, id: string, data: any) { super(model, collectionName, id, data); - Doc.call(this, model, collectionName, id); this._updateCollectionData(); } diff --git a/src/Model/RemoteDoc.ts b/src/Model/RemoteDoc.ts index a6bf8aa26..066ec328c 100644 --- a/src/Model/RemoteDoc.ts +++ b/src/Model/RemoteDoc.ts @@ -22,14 +22,12 @@ export class RemoteDoc extends Doc { shareDoc: any; constructor(model: Model, collectionName: string, id: string, snapshot: any, collection: Collection) { - super(model, collectionName, id, collection); + super(model, collectionName, id); // This is a bit messy, but we have to immediately register this doc on the // collection that added it, so that when we create the shareDoc and the // connection emits the 'doc' event, we'll find this doc instead of // creating a new one if (collection) collection.docs[id] = this; - - Doc.call(this, model, collectionName, id); this.model = model.pass({ $remote: true }); this.debugMutations = model.root.debug.remoteMutations; From e8c9bb4055ca874b41092b46f16d9af3ba4ab599 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 15 Feb 2024 14:49:38 -0800 Subject: [PATCH 346/479] 2.0.0-beta.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eccf710d8..fa5874229 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.5", + "version": "2.0.0-beta.6", "main": "./lib/index.js", "files": [ "lib/*" From 583ba0aa05809947fdb4c29ea86b7ccd324da6a3 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 21 Feb 2024 14:19:27 -0800 Subject: [PATCH 347/479] 2.0.0-beta.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fa5874229..f6b24f298 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.6", + "version": "2.0.0-beta.7", "main": "./lib/index.js", "files": [ "lib/*" From 922285ec6308cae5781de2d4e24e6157fbb41d67 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 21 Feb 2024 14:26:08 -0800 Subject: [PATCH 348/479] Rexport racer util form root --- src/Model/contexts.ts | 41 ++++++++++++++++++++++++++++++++++++----- src/index.ts | 2 +- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/Model/contexts.ts b/src/Model/contexts.ts index 0f63159c5..e1749d9ce 100644 --- a/src/Model/contexts.ts +++ b/src/Model/contexts.ts @@ -6,13 +6,44 @@ import { Model } from './Model'; import { CollectionCounter } from './CollectionCounter'; declare module './Model' { - interface Model { - _contexts: Contexts; - context(id: string): Model; - setContext(id: string): void; + interface Model { + /** + * Creates a new child model with a specific named data-loading context. The + * child model has the same scoped path as this model. + * + * Contexts are used to track counts of fetches and subscribes, so that all + * data relating to a context can be unloaded all at once, without having to + * manually track loaded data. + * + * Contexts are in a global namespace for each root model, so calling + * `model.context(contextId)` from two different places will return child + * models that both refer to the same context. + * + * @param contextId - context id + * + * @see https://derbyjs.com/docs/derby-0.10/models/data-loading-contexts + */ + context(contextId: string): ChildModel; getOrCreateContext(id: string): Context; - unload(id: string): void; + setContext(id: string): void; + + /** + * Unloads data for this model's context, or for a specific named context. + * + * @param contextId - optional context to unload; defaults to this model's context + * + * @see https://derbyjs.com/docs/derby-0.10/models/data-loading-contexts + */ + unload(contextId?: string): void; + + /** + * Unloads data for all model contexts. + * + * @see https://derbyjs.com/docs/derby-0.10/models/data-loading-contexts + */ unloadAll(): void; + + _contexts: Contexts; } } diff --git a/src/index.ts b/src/index.ts index 861414618..1bc1f183d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { ModelOptions, RootModel } from './Model'; export { Query } from './Model/Query'; export { Model, ChildModel, ModelData, type UUID, type Subscribable } from './Model'; export type { ReadonlyDeep, Path, PathLike, PathSegment } from './types'; +export * as util from './util'; const { use, serverUse } = util; @@ -20,7 +21,6 @@ export { RootModel, use, serverUse, - util, }; export const racer = new Racer(); From a883eb45a4bbcf2c14249f0adc95114edaa29655 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 21 Feb 2024 14:27:40 -0800 Subject: [PATCH 349/479] 2.0.0-beta.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f6b24f298..ea71a3e99 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "main": "./lib/index.js", "files": [ "lib/*" From 04eb1669d05e02f61532abba8910e87fa5a84b96 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 5 Mar 2024 13:43:57 -0800 Subject: [PATCH 350/479] Export Context; fix _ref sig return type --- src/Model/ref.ts | 6 +++--- src/index.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Model/ref.ts b/src/Model/ref.ts index 130895d42..a585043f4 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -13,9 +13,9 @@ declare module './Model' { _refLists: any; _canRefTo(value: Refable): boolean; // _canRefTo(from: Segments, to: Segments, options: any): boolean; - ref(to: Refable): void; - ref(from: string | number, to: Refable, options?: any): void; - _ref(from: Segments, to: Segments, options: any): any; + ref(to: Refable): ChildModel; + ref(from: string | number, to: Refable, options?: any): ChildModel; + _ref(from: Segments, to: Segments, options: any): void; removeRef(subpath: string): void; _removeRef(segments: Segments): void; removeAllRefs(subpath: string): void; diff --git a/src/index.ts b/src/index.ts index 1bc1f183d..46b1a4a7b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { ModelOptions, RootModel } from './Model'; export { Query } from './Model/Query'; export { Model, ChildModel, ModelData, type UUID, type Subscribable } from './Model'; +export { Context } from './Model/contexts'; export type { ReadonlyDeep, Path, PathLike, PathSegment } from './types'; export * as util from './util'; From 935eb932e72cf6eab8d17fcbe863eb66740fb90a Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 6 Mar 2024 08:41:18 -0800 Subject: [PATCH 351/479] 2.0.0-beta.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ea71a3e99..b6521e4e6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "main": "./lib/index.js", "files": [ "lib/*" From 112fd9bd634604eae0521d4400fbee13b1ae970e Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 7 Mar 2024 16:17:36 -0800 Subject: [PATCH 352/479] Use combined model options and sharedb options correctly --- src/Backend.ts | 4 ++-- src/Model/Model.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Backend.ts b/src/Backend.ts index dfbd4aff6..0543faa69 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -2,13 +2,13 @@ import { Model } from './Model'; import * as path from 'path'; import * as util from './util'; import { ModelOptions, RootModel } from './Model/Model'; -var Backend = require('sharedb').Backend; +import Backend = require('sharedb'); export class RacerBackend extends Backend { racer: any; modelOptions: any; - constructor(racer: any, options?: { modelOptions?: ModelOptions }) { + constructor(racer: any, options?: { modelOptions?: ModelOptions } & Backend.ShareDBOptions) { super(options); this.racer = racer; this.modelOptions = options && options.modelOptions; diff --git a/src/Model/Model.ts b/src/Model/Model.ts index bad707ba5..87d5f0cd8 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -3,6 +3,7 @@ import { type Context } from './contexts'; import { RacerBackend } from '../Backend'; import { type Connection } from './connection'; import { type ModelData } from './collections'; +import { ShareDBOptions } from 'sharedb'; export type UUID = string; From c9c4b79d94c6cc7dc7f4ba248dc6bcd6a037c893 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 7 Mar 2024 16:18:53 -0800 Subject: [PATCH 353/479] Remove JSONObject constraint for more forgiving type --- src/Model/collections.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/collections.ts b/src/Model/collections.ts index c32c38403..d24637832 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -21,7 +21,7 @@ class DocMap { } /** Dictionary of document id to document data */ -export class CollectionData { +export class CollectionData { [id: string]: T; } From f379611290a6ea2e1db0ab6b4c046f2e16ecf397 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 7 Mar 2024 16:20:03 -0800 Subject: [PATCH 354/479] Use Path and PathLike to allow number indexing --- src/Model/events.ts | 1 + src/Model/paths.ts | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 7f2eda2e5..74fb64821 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -5,6 +5,7 @@ import { EventListenerTree } from './EventListenerTree'; import { type Segments } from './types'; import { Model } from './Model'; import { mergeInto } from '../util'; +import { PathLike } from '../types'; export type ModelEvent = | ChangeEvent diff --git a/src/Model/paths.ts b/src/Model/paths.ts index 85b2ae9d1..cb489dc42 100644 --- a/src/Model/paths.ts +++ b/src/Model/paths.ts @@ -1,15 +1,16 @@ import { Model } from './Model'; +import type { Path, PathLike } from '../types'; exports.mixin = {}; declare module './Model' { interface Model { _splitPath(subpath: string): string[]; - path(subpath: string | number | Model): string; + path(subpath?: PathLike): string; isPath(subpath: string): boolean; - scope(subpath: string): ChildModel; + scope(subpath: Path): ChildModel; scope(): ChildModel; - at(subpath: string): ChildModel; + at(subpath: Path): ChildModel; parent(levels?: number): Model; leaf(path: string): string; } From 0c2f0c2fbb43ca9f4e5c544424cd26adc24fccbc Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 7 Mar 2024 16:20:37 -0800 Subject: [PATCH 355/479] Fix return types for add --- src/Model/mutators.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 89d3c45d6..52d37424f 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -41,13 +41,13 @@ declare module './Model' { createNullPromised(subpath: string, value: any): Promise; _createNull(segments: Segments, value: any, cb?: ErrorCallback): void; - add(value: any): void; - add(subpath: string, value: any, cb?: ErrorCallback): void; + add(value: any): string; + add(subpath: string, value: any, cb?: ErrorCallback): string; addPromised(value: any): Promise; addPromised(subpath: string, value: any): Promise; - _add(segments: Segments, value: any, cb?: ErrorCallback): void; + _add(segments: Segments, value: any, cb?: ErrorCallback): string; - del(value: any): void; + del(value?: any): void; del(subpath: string, value: any, cb?: ErrorCallback): void; delPromised(value: any): Promise; delPromised(subpath: string, value: any): Promise; From 90519cc81ee00e32709c98fd3da2b805cdff9d2e Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 7 Mar 2024 16:21:04 -0800 Subject: [PATCH 356/479] Export CollectionData and utility Callback types --- src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.ts b/src/index.ts index 46b1a4a7b..fd62bff66 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,12 +9,15 @@ export { Query } from './Model/Query'; export { Model, ChildModel, ModelData, type UUID, type Subscribable } from './Model'; export { Context } from './Model/contexts'; export type { ReadonlyDeep, Path, PathLike, PathSegment } from './types'; +export type { CollectionData } from './Model/collections'; export * as util from './util'; const { use, serverUse } = util; export type BackendOptions = { modelOptions?: ModelOptions } & ShareDBOptions; +export type Callback = (error?: Error) => void; + export { ModelOptions, Racer, From 9be5f0cb8f4f393a0f56346635a2f04a2afc7504 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 7 Mar 2024 16:21:24 -0800 Subject: [PATCH 357/479] Fix event method types --- src/Model/events.ts | 93 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 74fb64821..7de907a4f 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -29,12 +29,92 @@ declare module './Model' { interface Model { addListener(event: string, listener: any, arg2?: any, arg3?: any): any; eventContext(id: string): Model; - on(event: string, listener: any, arg2?: any, arg3?: any): any; - once(event: string, listener: any, arg2?: any, arg3?: any): any; - pass(object: any, invert?: boolean): Model; + + /** + * Listen to Racer events matching a certain path or path pattern. + * + * `pathPattern` is a path pattern that will filter emitted events, calling + * the handler function only when a mutator matches the pattern. + * + * Path patterns support a single segment wildcard `'*'` anywhere in a path, + * and a multi-segment wildcard `'**'` at the end of the path. The + * multi-segment wildcard alone `'**'` matches all paths. + * + * Examples of path patterns: + * * `'notes.abc-123.author'` - Trigger on a direct modification to a + * specific note's `author`. Will not trigger if a sub-property of the + * author is modified or if the entire note is replaced. + * * `notes.*.author` - Trigger on a direct modification to any note's + * `author`. Will not trigger if a sub-property of the author is modified + * or if an entire note is replaced. + * * `notes.*.author.**` - Trigger on a modification to any note's `author` + * or any sub-property of `author`. Will not trigger if an entire note is + * replaced. + * + * @param eventType + * @param pathPattern + * @param options + * @param listener + * + * @see https://derbyjs.com/docs/derby-0.10/models/events + */ + on( + eventType: T, + pathPattern: PathLike, + options: { useEventObjects: true }, + listener: (event: ModelOnEventMap[T], captures: Array) => void + ): Function; + on( + eventType: T, + options: { useEventObjects: true }, + listener: (event: ModelOnEventMap[T], captures: Array) => void + ): Function; + on( + eventType: 'all', + listener: (segments: string[], event: ModelOnEventMap[keyof ModelOnEventMap]) => void + ): Function; + on( + eventType: 'error', + listener: (error: Error) => void + ): Function; + + + /** + * Listen to Racer events matching a certain path or path pattern, removing + * the listener after it gets triggered once. + * + * @param eventType + * @param pathPattern + * @param options + * @param listener + * + * @see https://derbyjs.com/docs/derby-0.10/components/events + */ + once( + eventType: T, + pathPattern: string, + options: { useEventObjects: true }, + listener: (event: ModelOnEventMap[T], captures: Array) => void + ): Function; + once( + eventType: T, + options: { useEventObjects: true }, + listener: (event: ModelOnEventMap[T], captures: Array) => void + ): Function; + + /** + * Passes data to event listeners + * + * @param object - An object whose properties will each be set on the passed argument + * @returns back a model scoped to the same path + */ + pass(object: object, invert?: boolean): Model; + removeAllListeners(type: string, subpath: string): void; removeContextListeners(): void; - removeListener(type: string, listener: any): void; + + removeListener(eventType: keyof ModelOnEventMap, listener: Function): void; + setMaxListeners(limit: number): void; silent(value?: boolean): Model; wrapCallback(cb: ErrorCallback): ErrorCallback; @@ -176,8 +256,8 @@ Model.prototype._callMutationListeners = function(type, segments, event) { // the listener instead, since it is made internally for method subscriptions // and may need to be passed to removeListener. Model.prototype.__on = EventEmitter.prototype.on; -Model.prototype.addListener = -Model.prototype.on = function(type, arg1, arg2, arg3) { +// @ts-expect-error ignore method overload issues +Model.prototype.addListener = Model.prototype.on = function(type, arg1, arg2, arg3) { var listener = this._addMutationListener(type, arg1, arg2, arg3); if (listener) { return listener; @@ -188,6 +268,7 @@ Model.prototype.on = function(type, arg1, arg2, arg3) { }; Model.prototype.__once = EventEmitter.prototype.once; +// @ts-expect-error ignore method overload issues Model.prototype.once = function(type, arg1, arg2, arg3) { var listener = this._addMutationListener(type, arg1, arg2, arg3); if (listener) { From aa8e63d643530eab4a78854a207ab6b969210714 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 8 Mar 2024 10:01:53 -0800 Subject: [PATCH 358/479] Add generic param to Query.getExtra hinting shape of results --- src/Model/Query.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/Query.ts b/src/Model/Query.ts index fdb6a2c86..ce2534e3a 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -538,8 +538,8 @@ export class Query { return this.model._get(this.idsSegments) || []; }; - getExtra() { - return this.model._get(this.extraSegments); + getExtra() { + return this.model._get(this.extraSegments) as T; }; ref(from) { From 3624c52f4a7279627574bfb804969f95ecf68166 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 8 Mar 2024 10:07:07 -0800 Subject: [PATCH 359/479] 2.0.0-beta.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b6521e4e6..47a618553 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "main": "./lib/index.js", "files": [ "lib/*" From 94ea9abc906511b9646eebf5f63868c4a89feb87 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 09:34:26 -0700 Subject: [PATCH 360/479] Type wrapCallback to accept optional calback (it wraps a default callback if not provided) --- src/Model/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 7de907a4f..b1f90bb41 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -117,7 +117,7 @@ declare module './Model' { setMaxListeners(limit: number): void; silent(value?: boolean): Model; - wrapCallback(cb: ErrorCallback): ErrorCallback; + wrapCallback(cb?: ErrorCallback): ErrorCallback; __on: typeof EventEmitter.prototype.on; __once: typeof EventEmitter.prototype.once; From 8375d5dcf78adc23ca83bccc5e30ad0e4cc7ea68 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 09:36:21 -0700 Subject: [PATCH 361/479] Export event types --- src/Model/events.ts | 12 ++++++------ src/index.ts | 5 ++--- src/types.ts | 5 +++++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index b1f90bb41..c262f5abd 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -553,7 +553,7 @@ function createMutationListenerLegacy(type, pattern, eventContext, cb) { return createMutationListener(pattern, eventContext, mutationListenerAdapter); } -class ChangeEvent { +export class ChangeEvent { declare type: string; declare _immediateType: string; value: any; @@ -577,7 +577,7 @@ class ChangeEvent { ChangeEvent.prototype.type = 'change'; ChangeEvent.prototype._immediateType = 'changeImmediate'; -class LoadEvent { +export class LoadEvent { declare type: string; declare _immediateType: string; value: any; @@ -606,7 +606,7 @@ class LoadEvent { LoadEvent.prototype.type = 'load'; LoadEvent.prototype._immediateType = 'loadImmediate'; -class UnloadEvent { +export class UnloadEvent { declare type: string; declare _immediateType: string; previous: any; @@ -635,7 +635,7 @@ class UnloadEvent { UnloadEvent.prototype.type = 'unload'; UnloadEvent.prototype._immediateType = 'unloadImmediate'; -class InsertEvent { +export class InsertEvent { declare type: string; declare _immediateType: string; index: number; @@ -659,7 +659,7 @@ class InsertEvent { InsertEvent.prototype.type = 'insert'; InsertEvent.prototype._immediateType = 'insertImmediate'; -class RemoveEvent { +export class RemoveEvent { declare type: string; declare _immediateType: string; index: number; @@ -690,7 +690,7 @@ class RemoveEvent { RemoveEvent.prototype.type = 'remove'; RemoveEvent.prototype._immediateType = 'removeImmediate'; -class MoveEvent { +export class MoveEvent { declare type: string; declare _immediateType: string; from: any; diff --git a/src/index.ts b/src/index.ts index fd62bff66..0ce5b3cab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,8 @@ import { ModelOptions, RootModel } from './Model'; export { Query } from './Model/Query'; export { Model, ChildModel, ModelData, type UUID, type Subscribable } from './Model'; export { Context } from './Model/contexts'; -export type { ReadonlyDeep, Path, PathLike, PathSegment } from './types'; +export { type ModelOnEventMap, type ModelEvent, ChangeEvent, InsertEvent, LoadEvent, MoveEvent, RemoveEvent, UnloadEvent } from './Model/events'; +export type { Callback, ReadonlyDeep, Path, PathLike, PathSegment, Primitive } from './types'; export type { CollectionData } from './Model/collections'; export * as util from './util'; @@ -16,8 +17,6 @@ const { use, serverUse } = util; export type BackendOptions = { modelOptions?: ModelOptions } & ShareDBOptions; -export type Callback = (error?: Error) => void; - export { ModelOptions, Racer, diff --git a/src/types.ts b/src/types.ts index d64774c29..1fb841fe7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,11 @@ export type PathLike = Path | Model; export type Primitive = boolean | number | string | null | undefined; +/** If `T` is an array, produces the type of the array items. */ +export type ArrayItemType = T extends Array ? U : never; + +export type Callback = (error?: Error) => void; + /** * Transforms a JSON-compatible type `T` such that it and any nested arrays * or basic objects are read-only. From 0fb59afbbeb44f6f928946cad8f074217cbc94bf Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 09:37:44 -0700 Subject: [PATCH 362/479] Add generic parameter to util.promisfy to allow for typing return types if needed --- src/util.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util.ts b/src/util.ts index 4fe7aa7ed..08eb5d1f4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -148,20 +148,20 @@ export function mergeInto(to, from) { return to; } -export function promisify(original) { +export function promisify(original) { if (typeof original !== 'function') { throw new TypeError('The "original" argument must be of type Function'); } function fn() { var promiseResolve, promiseReject; - var promise = new Promise(function(resolve, reject) { + var promise = new Promise(function(resolve, reject) { promiseResolve = resolve; promiseReject = reject; }); var args = Array.prototype.slice.apply(arguments); - args.push(function(err, value) { + args.push(function(err: Error, value: T) { if (err) { promiseReject(err); } else { From 90ce009e98d5e3c45324022680a5cd6fefd12a08 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 11:50:00 -0700 Subject: [PATCH 363/479] Use DefaultType placeholder for default generic param on Model (naming TBD) --- src/Model/Model.ts | 12 +++++++----- src/index.ts | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Model/Model.ts b/src/Model/Model.ts index 87d5f0cd8..6f8ea7432 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -3,10 +3,12 @@ import { type Context } from './contexts'; import { RacerBackend } from '../Backend'; import { type Connection } from './connection'; import { type ModelData } from './collections'; -import { ShareDBOptions } from 'sharedb'; +import { Primitive } from '../types'; export type UUID = string; +export type DefualtType = {}; + declare module './Model' { interface DebugOptions { debugMutations?: boolean, @@ -24,9 +26,9 @@ declare module './Model' { type ErrorCallback = (err?: Error) => void; } -type ModelInitFunction = (instance: Model, options: ModelOptions) => void; +type ModelInitFunction = (instance: RootModel, options: ModelOptions) => void; -export class Model { +export class Model { static INITS: ModelInitFunction[] = []; ChildModel = ChildModel; @@ -68,8 +70,8 @@ export class RootModel extends Model { } } -export class ChildModel extends Model { - constructor(model: Model) { +export class ChildModel extends Model { + constructor(model: Model) { super(); // Shared properties should be accessed via the root. This makes inheritance // cheap and easily extensible diff --git a/src/index.ts b/src/index.ts index 0ce5b3cab..8e2540997 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import { RacerBackend } from './Backend'; import { ModelOptions, RootModel } from './Model'; export { Query } from './Model/Query'; -export { Model, ChildModel, ModelData, type UUID, type Subscribable } from './Model'; +export { Model, ChildModel, ModelData, type UUID, type Subscribable, type DefualtType } from './Model'; export { Context } from './Model/contexts'; export { type ModelOnEventMap, type ModelEvent, ChangeEvent, InsertEvent, LoadEvent, MoveEvent, RemoveEvent, UnloadEvent } from './Model/events'; export type { Callback, ReadonlyDeep, Path, PathLike, PathSegment, Primitive } from './types'; From c05d50a4c49cf52e9ec96de8e63d7fd5f15a9f17 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 11:52:41 -0700 Subject: [PATCH 364/479] Export RefOptions interface; refine refs method signature typing --- src/Model/ref.ts | 73 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/src/Model/ref.ts b/src/Model/ref.ts index a585043f4..c127d63e5 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -4,24 +4,73 @@ import { Model } from './Model'; import { type Segments } from './types'; import { type Filter } from './filter'; import { type Query } from './Query'; +import { PathLike } from '../types'; -type Refable = string | number | Model | Query | Filter; +type Refable = string | number | Model | Query | Filter; + +export interface RefOptions { + updateIndices: boolean; +} declare module './Model' { interface Model { - _refs: any; - _refLists: any; + /** + * Creates an array at `outputPath` that consists of references to all the + * objects at `collectionPath` that have ids matching the ids at `idsPath`. + * The array is automatically updated based on changes to the input paths. + * + * @param outputPath - Path at which to create the ref list. This must be + * under a local collection, typically `'_page'` or a component model. + * @param collectionPath - Path to a Racer collection or a collection-like + * object, where each id string key maps to an object value with matching + * `id` property. + * @param idsPath - Path to an array of string ids + * @param options + * @param options.deleteRemoved - If true, then objects from the source + * collection will be deleted if the corresponding item is removed from + * the refList's output path + * + * @see https://derbyjs.com/docs/derby-0.10/models/references + */ + refList(outputPath: PathLike, collectionPath: PathLike, idsPath: PathLike, options?: { deleteRemoved?: boolean }): ChildModel; + _canRefTo(value: Refable): boolean; - // _canRefTo(from: Segments, to: Segments, options: any): boolean; - ref(to: Refable): ChildModel; - ref(from: string | number, to: Refable, options?: any): ChildModel; - _ref(from: Segments, to: Segments, options: any): void; - removeRef(subpath: string): void; + // _canRefTo(from: Segments, to: Segments, options: RefOptions): boolean; + + /** + * Creates a reference at `path` pointing to another path `to`. Like a + * symlink, any reads/writes on `path` will work as if they were done on + * `path` directly. + * + * @param path - Location at which to create the reference. This must be + * under a local collection, typically `'_page'` or a component model. + * @param to - Location that the reference points to + * @return a model scoped to `path` + * + * @see https://derbyjs.com/docs/derby-0.10/models/references + */ + ref(to: PathLike): ChildModel; + ref(path: PathLike, to: PathLike, options?: RefOptions): ChildModel; + _ref(from: Segments, to: Segments, options?: RefOptions): void; + + /** + * Removes a model reference. + * + * @param path - Location of the reference to remove + * + * @see https://derbyjs.com/docs/derby-0.10/models/references + */ + removeRef(path: PathLike): void; _removeRef(segments: Segments): void; - removeAllRefs(subpath: string): void; + + removeAllRefs(subpath: PathLike): void; _removeAllRefs(segments: Segments): void; + dereference(subpath: string): Segments; _dereference(segments: Segments, forArrayMutator: any, ignore: boolean): Segments; + + _refs: any; + _refLists: any; } } @@ -284,11 +333,11 @@ function noopDereference(segments) { } export class Ref { - fromSegments: string[]; - toSegments: string[]; + fromSegments: Segments; + toSegments: Segments; updateIndices: boolean; - constructor(fromSegments, toSegments, options) { + constructor(fromSegments: Segments, toSegments: Segments, options?: RefOptions) { this.fromSegments = fromSegments; this.toSegments = toSegments; this.updateIndices = options && options.updateIndices; From 7e597f70b225341a0666e3bd4c463a47e368f9d2 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 11:53:38 -0700 Subject: [PATCH 365/479] Refine setDiff method signature typing --- src/Model/setDiff.ts | 98 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 22 deletions(-) diff --git a/src/Model/setDiff.ts b/src/Model/setDiff.ts index ad4d0cbe9..83de78fce 100644 --- a/src/Model/setDiff.ts +++ b/src/Model/setDiff.ts @@ -1,4 +1,5 @@ var util = require('../util'); +import { Callback, Path, ReadonlyDeep } from '../types'; import { Model } from './Model'; import { type Segments } from './types'; var arrayDiff = require('arraydiff'); @@ -10,28 +11,81 @@ var MoveEvent = mutationEvents.MoveEvent; var promisify = util.promisify; declare module './Model' { - interface Model { - setDiff(value: any); - setDiff(subpath: string, value: any, cb?: (err: Error) => void): void; - setDiffPromised(value: any): Promise; - setDiffPromised(subpath: string, value: any): Promise; - _setDiff(segments: Segments, value: any, cb: (err: Error) => void): void; - setDiffDeep(value: any): void; - setDiffDeep(subpath: string, value: any, cb?: (err: Error) => void): void; - setDiffDeepPromised(value: any): Promise; - setDiffDeepPromised(subpathj: string, valiue: any): Promise; - _setDiffDeep(segments: Segments, value: any, cb?: (err: Error) => void): void; - setArrayDiff(value: any): void; - setArrayDiff(subpath: string, value: any, cb?: (err: Error) => void): void; - setArrayDiffPromised(value: any): Promise; - setArrayDiffPromised(subpath: string, value: any): Promise; - setArrayDiffDeep(value: any): void; - setArrayDiffDeep(subpath: string, value: any, cb?: (err: Error) => void): void; - setArrayDiffDeepPromised(value: any): Promise; - setArrayDiffDeepPromised(subpath: string, value: any): Promise; - _setArrayDiffDeep(segments: Segments, value: any, cb?: (err: Error) => void): void; - _setArrayDiff(segments: Segments, value: any, cb?: (err: Error) => void, equalFn?: any): void; - _applyArrayDiff(segments: Segments, diff: any, cb?: (err: Error) => void): void; + interface Model { + /** + * Sets the value at this model's path or a relative subpath, if different + * from the current value based on a strict equality comparison (`===`). + * + * If a callback is provided, it's called when the write is committed or + * fails. + * + * @param subpath + * @param value + * @returns the value previously at the path + */ + setDiff(subpath: Path, value: S, cb?: Callback): ReadonlyDeep | undefined; + setDiff(value: T): ReadonlyDeep | undefined; + setDiffPromised(subpath: string, value: S): Promise; + _setDiff(segments: Segments, value: any, cb?: (err: Error) => void): void; + + /** + * Sets the value at this model's path or a relative subpath, if different + * from the current value based on a recursive deep equal comparison. + * + * This attempts to issue fine-grained ops on subpaths if possible. + * + * If a callback is provided, it's called when the write is committed or + * fails. + * + * @param subpath + * @param value + * @returns the value previously at the path + */ + setDiffDeep(subpath: Path, value: S, cb?: Callback): ReadonlyDeep | undefined; + setDiffDeep(value: T): ReadonlyDeep | undefined; + setDiffDeepPromised(subpath: Path, value: S): Promise; + _setDiffDeep(segments: Segments, value: any, cb?: (err: Error) => void): void; + + /** + * Sets the array value at this model's path or a relative subpath, based on + * a strict equality comparison (`===`) between array items. + * + * This only issues array insert, remove, and move operations. + * + * If a callback is provided, it's called when the write is committed or + * fails. + * + * @param subpath + * @param value + * @returns the value previously at the path + */ + setArrayDiff(subpath: Path, value: S, cb?: Callback): S; + setArrayDiff(value: S): S; + setArrayDiffPromised(subpath: Path, value: S): Promise; + _setArrayDiff(segments: Segments, value: any, cb?: (err: Error) => void, equalFn?: any): void; + + /** + * Sets the array value at this model's path or a relative subpath, based on + * a deep equality comparison between array items. + * + * This only issues array insert, remove, and move operations. Unlike + * `setDiffDeep`, this will never issue fine-grained ops inside of array + * items. + * + * If a callback is provided, it's called when the write is committed or + * fails. + * + * @param subpath + * @param value + * @returns the value previously at the path + */ + setArrayDiffDeep(subpath: Path, value: S, cb?: Callback): S; + setArrayDiffDeep(value: S): S; + setArrayDiffDeepPromised(subpath: Path, value: S): Promise; + _setArrayDiffDeep(segments: Segments, value: any, cb?: (err: Error) => void): void; + + _setArrayDiff(segments: Segments, value: any, cb?: (err: Error) => void, equalFn?: any): S; + _applyArrayDiff(segments: Segments, diff: any, cb?: (err: Error) => void): S; } } From 8683880aed1f6ec2d7cdf46a7ea89b8452610266 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 11:54:47 -0700 Subject: [PATCH 366/479] Refine path method signature typing --- src/Model/paths.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Model/paths.ts b/src/Model/paths.ts index cb489dc42..19915d87b 100644 --- a/src/Model/paths.ts +++ b/src/Model/paths.ts @@ -1,22 +1,24 @@ -import { Model } from './Model'; +import { ChildModel, Model } from './Model'; import type { Path, PathLike } from '../types'; exports.mixin = {}; declare module './Model' { interface Model { - _splitPath(subpath: string): string[]; - path(subpath?: PathLike): string; + at(): ChildModel; + at(subpath: Path): ChildModel; isPath(subpath: string): boolean; - scope(subpath: Path): ChildModel; - scope(): ChildModel; - at(subpath: Path): ChildModel; - parent(levels?: number): Model; leaf(path: string): string; + parent(levels?: number): Model; + path(subpath?: PathLike): string; + scope(): ChildModel; + scope(subpath: Path): ChildModel; + + _splitPath(subpath: string): string[]; } } -Model.prototype._splitPath = function(subpath) { +Model.prototype._splitPath = function(subpath?: Path): string[] { var path = this.path(subpath); return (path && path.split('.')) || []; }; @@ -29,7 +31,7 @@ Model.prototype._splitPath = function(subpath) { * @return {String} absolute path * @api public */ -Model.prototype.path = function(subpath) { +Model.prototype.path = function(subpath?: Path): string { if (subpath == null || subpath === '') return (this._at) ? this._at : ''; if (typeof subpath === 'string' || typeof subpath === 'number') { return (this._at) ? this._at + '.' + subpath : '' + subpath; @@ -38,11 +40,11 @@ Model.prototype.path = function(subpath) { if (typeof subpath.path === 'function') return subpath.path(); }; -Model.prototype.isPath = function(subpath) { +Model.prototype.isPath = function(subpath?: Path): boolean { return this.path(subpath) != null; }; -Model.prototype.scope = function(path?) { +Model.prototype.scope = function(path?: Path): ChildModel { if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { path = path + '.' + arguments[i]; @@ -64,7 +66,7 @@ Model.prototype.scope = function(path?) { * @return {Model} a scoped model * @api public */ -Model.prototype.at = function(subpath) { +Model.prototype.at = function(subpath?: Path) { if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { subpath = subpath + '.' + arguments[i]; From 6f695fef1c4244c5c00ce717b5d3501824dfb5c5 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 11:55:40 -0700 Subject: [PATCH 367/479] Allow optional filter function passed to filter --- src/Model/filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/filter.ts b/src/Model/filter.ts index ce6f4874b..68c1efb05 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -45,7 +45,7 @@ declare module './Model' { ): Filter; filter( inputPath: PathLike, - fn: (item: S, key: string, object: { [key: string]: S }) => boolean + fn?: (item: S, key: string, object: { [key: string]: S }) => boolean ): Filter; removeAllFilters: (subpath: string) => void; From 47589ab10bf90355d62828989bbc06cf4b3aa789 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 11:56:07 -0700 Subject: [PATCH 368/479] Use Path type --- src/Model/Doc.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Model/Doc.ts b/src/Model/Doc.ts index 6ad40848b..bec5ebf15 100644 --- a/src/Model/Doc.ts +++ b/src/Model/Doc.ts @@ -1,6 +1,6 @@ import { type Model } from './Model'; -import { type Segments } from './types'; import { Collection } from './collections'; +import { Path } from '../types'; export class Doc { collectionData: Model; @@ -17,13 +17,13 @@ export class Doc { this.collectionData = model && model.data[collectionName]; } - path(segments?: Segments) { + path(segments?: Path[]) { var path = this.collectionName + '.' + this.id; if (segments && segments.length) path += '.' + segments.join('.'); return path; }; - _errorMessage(description: string, segments: Segments, value: any) { + _errorMessage(description: string, segments: Path[], value: any) { return description + ' at ' + this.path(segments) + ': ' + JSON.stringify(value, null, 2); }; From 4140ca23225bc7b8b0f0495b29fe9af3d64ae347 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 11:59:19 -0700 Subject: [PATCH 369/479] Use Callback type for callback typing --- src/Model/LocalDoc.ts | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/Model/LocalDoc.ts b/src/Model/LocalDoc.ts index 435822741..a57523c57 100644 --- a/src/Model/LocalDoc.ts +++ b/src/Model/LocalDoc.ts @@ -1,5 +1,6 @@ import { type Model } from './Model'; import { Doc } from './Doc'; +import { Callback, Path } from '../types'; var util = require('../util'); export class LocalDoc extends Doc{ @@ -12,7 +13,7 @@ export class LocalDoc extends Doc{ this.collectionData[this.id] = this.data; }; - create(value: any, cb) { + create(value: any, cb?: Callback) { if (this.data !== undefined) { var message = this._errorMessage('create on local document with data', null, this.data); var err = new Error(message); @@ -23,7 +24,7 @@ export class LocalDoc extends Doc{ cb(); }; - set(segments, value, cb) { + set(segments: Path, value: any, cb?: Callback) { function set(node, key) { var previous = node[key]; node[key] = value; @@ -32,7 +33,7 @@ export class LocalDoc extends Doc{ return this._apply(segments, set, cb); }; - del(segments, cb) { + del(segments: Path, cb?: Callback) { // Don't do anything if the value is already undefined, since // apply creates objects as it traverses, and the del method // should not create anything @@ -48,7 +49,7 @@ export class LocalDoc extends Doc{ return this._apply(segments, del, cb); }; - increment(segments, byNumber, cb) { + increment(segments, byNumber, cb?: Callback) { var self = this; function validate(value) { if (typeof value === 'number' || value == null) return; @@ -64,21 +65,21 @@ export class LocalDoc extends Doc{ return this._validatedApply(segments, validate, increment, cb); }; - push(segments, value, cb) { + push(segments: Path, value: unknown, cb?: Callback) { function push(arr) { return arr.push(value); } return this._arrayApply(segments, push, cb); }; - unshift(segments, value, cb) { + unshift(segments: Path, value: unknown, cb?: Callback) { function unshift(arr) { return arr.unshift(value); } return this._arrayApply(segments, unshift, cb); }; - insert(segments, index, values, cb) { + insert(segments: Path, index: number, values, cb?: Callback) { function insert(arr) { arr.splice.apply(arr, [index, 0].concat(values)); return arr.length; @@ -86,28 +87,28 @@ export class LocalDoc extends Doc{ return this._arrayApply(segments, insert, cb); }; - pop(segments, cb) { + pop(segments: Path, cb?: Callback) { function pop(arr) { return arr.pop(); } return this._arrayApply(segments, pop, cb); }; - shift(segments, cb) { + shift(segments: Path, cb?: Callback) { function shift(arr) { return arr.shift(); } return this._arrayApply(segments, shift, cb); }; - remove(segments, index, howMany, cb) { + remove(segments: Path, index: number, howMany: number, cb?: Callback) { function remove(arr) { return arr.splice(index, howMany); } return this._arrayApply(segments, remove, cb); }; - move(segments, from, to, howMany, cb) { + move(segments, from, to, howMany, cb?: Callback) { function move(arr) { // Remove from old location var values = arr.splice(from, howMany); @@ -118,7 +119,7 @@ export class LocalDoc extends Doc{ return this._arrayApply(segments, move, cb); }; - stringInsert(segments, index, value, cb) { + stringInsert(segments, index, value, cb?: Callback) { var self = this; function validate(value) { if (typeof value === 'string' || value == null) return; @@ -138,7 +139,7 @@ export class LocalDoc extends Doc{ return this._validatedApply(segments, validate, stringInsert, cb); }; - stringRemove(segments, index, howMany, cb) { + stringRemove(segments: Path[], index: number, howMany: number, cb?: Callback) { var self = this; function validate(value) { if (typeof value === 'string' || value == null) return; @@ -156,7 +157,7 @@ export class LocalDoc extends Doc{ return this._validatedApply(segments, validate, stringRemove, cb); }; - get(segments) { + get(segments?: Path) { return util.lookup(segments, this.data); }; @@ -179,14 +180,14 @@ export class LocalDoc extends Doc{ return fn(node, key); }; - _apply(segments, fn, cb) { + _apply(segments, fn, cb?: Callback) { var out = this._createImplied(segments, fn); this._updateCollectionData(); cb(); return out; }; - _validatedApply(segments, validate, fn, cb) { + _validatedApply(segments, validate, fn, cb?: Callback) { var out = this._createImplied(segments, function(node, key) { var err = validate(node[key]); if (err) return cb(err); @@ -197,7 +198,7 @@ export class LocalDoc extends Doc{ return out; }; - _arrayApply(segments, fn, cb) { + _arrayApply(segments, fn, cb?: Callback) { // Lookup a pointer to the property or nested property & // return the current value or create a new array var arr = this._createImplied(segments, nodeCreateArray); From 557cc24da939aee46cd3cf95097d0316725a8f69 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 12:00:16 -0700 Subject: [PATCH 370/479] Fix Query retrun types and optional args --- src/Model/Query.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Model/Query.ts b/src/Model/Query.ts index ce2534e3a..354c03c87 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -19,8 +19,8 @@ interface QueryCtor { declare module './Model' { interface Model { - query(collectionName: string, expression, options?: QueryOptions): Query; - sanitizeQuery(expression): Query; + query(collectionName: string, expression, options?: QueryOptions): Query; + sanitizeQuery(expression: any): any; _getOrCreateQuery(collectionName: string, expression, options: QueryOptions, QueryConstructor: QueryCtor): Query; _initQueries(items: any[]): void; @@ -552,7 +552,7 @@ export class Query { return this.model.ref(from, idsPath); }; - refExtra(from, relPath) { + refExtra(from, relPath?) { var extraPath = this.extraSegments.join('.'); if (relPath) extraPath += '.' + relPath; return this.model.ref(from, extraPath); From 286e137e78e54460b56d781dae82ec1b79db5a5b Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 12:37:23 -0700 Subject: [PATCH 371/479] Unify Segment and Path type (temp) --- src/Model/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Model/types.ts b/src/Model/types.ts index d7345d215..a5a591662 100644 --- a/src/Model/types.ts +++ b/src/Model/types.ts @@ -7,11 +7,13 @@ export type PathLike = Path | Model; */ +import { Path } from "../types"; + // could be // ['foo', 3, 'bar'] // always converted to string internally -export type Segment = string; +export type Segment = Path; // PathLike export type Segments = Array; From 4edfd0a7d5af411aafbdd321c079dc5284cfcfd8 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 12:38:19 -0700 Subject: [PATCH 372/479] Query subscribe callback is optional --- src/Model/Query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Query.ts b/src/Model/Query.ts index 354c03c87..367795a23 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -269,7 +269,7 @@ export class Query { fetchPromised = promisify(Query.prototype.fetch); - subscribe(cb: ErrorCallback) { + subscribe(cb?: ErrorCallback) { cb = this.model.wrapCallback(cb); this.context.subscribeQuery(this); From 701073d573a40e6f5a766a0a61c5c59f28314c49 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 12:39:06 -0700 Subject: [PATCH 373/479] Re-export DefaultType from Model/index --- src/Model/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/index.ts b/src/Model/index.ts index 5083fc94b..209cd3aa2 100644 --- a/src/Model/index.ts +++ b/src/Model/index.ts @@ -2,7 +2,7 @@ /// import { serverRequire } from '../util'; -export { Model, ChildModel, RootModel, ModelOptions, type UUID } from './Model'; +export { Model, ChildModel, RootModel, ModelOptions, type UUID, type DefualtType } from './Model'; export { ModelData } from './collections'; export { type Subscribable } from './subscriptions'; From 17a4ad9595654a9600979b19c347c1f26351a3f1 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 12:40:01 -0700 Subject: [PATCH 374/479] Refine mutator method types --- src/Model/mutators.ts | 117 +++++++++++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 41 deletions(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 52d37424f..42c38e2d4 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -1,4 +1,5 @@ -var util = require('../util'); +import type { Callback, Path, ArrayItemType } from '../types'; +import * as util from '../util'; import { Model } from './Model'; import { type Segments } from './types'; @@ -11,18 +12,20 @@ var MoveEvent = mutationEvents.MoveEvent; var promisify = util.promisify; declare module './Model' { - interface Model { + interface Model { _mutate(segments, fn, cb): void; - set(value: any): void; - set(subpath: string, value: any, cb?: ErrorCallback): void; - setPromised(value: any): Promise; - setPromised(subpath: string, value: any): Promise; - _set(segments: Segments, value: any, cb?: ErrorCallback): void; - setNull(value: any): void; - setNull(subpath: string, value: any, cb?: ErrorCallback): void; - setNullPromised(value: any): Promise; - setNullPromised(subpath: string, value: any): Promise; - _setNull(segments: Segments, value: any, cb?: ErrorCallback): void; + set(value: any): S; + set(subpath: string, value: any, cb?: ErrorCallback): S; + setPromised(value: any): Promise; + setPromised(subpath: string, value: any): Promise; + _set(segments: Segments, value: any, cb?: ErrorCallback): S; + + setNull(value: any): S; + setNull(subpath: string, value: any, cb?: ErrorCallback): S; + setNullPromised(value: any): Promise; + setNullPromised(subpath: string, value: any): Promise; + _setNull(segments: Segments, value: any, cb?: ErrorCallback): S; + setEach(value: any): void; setEach(subpath: string, value: any, cb?: ErrorCallback): void; setEachPromised(value: any): Promise; @@ -43,29 +46,37 @@ declare module './Model' { add(value: any): string; add(subpath: string, value: any, cb?: ErrorCallback): string; - addPromised(value: any): Promise; - addPromised(subpath: string, value: any): Promise; + addPromised(value: any): Promise; + addPromised(subpath: string, value: any): Promise; _add(segments: Segments, value: any, cb?: ErrorCallback): string; - del(value?: any): void; - del(subpath: string, value: any, cb?: ErrorCallback): void; - delPromised(value: any): Promise; - delPromised(subpath: string, value: any): Promise; - _del(segments: Segments, value?: any, cb?: ErrorCallback): void; + /** + * Deletes the value at this model's path or a relative subpath. + * + * If a callback is provided, it's called when the write is committed or + * fails. + * + * @param subpath + * @returns the old value at the path + */ + del(subpath: Path, cb?: Callback): S | undefined; + del(cb?: Callback): T | undefined; + delPromised(subpath: string): Promise; + _del(segments: Segments, cb?: ErrorCallback): S; _delNoDereference(segments: Segments, cb?: ErrorCallback): void; - increment(value: any): void; - increment(subpath: string, value: any, cb?: ErrorCallback): void; - incrementPromised(value: any): Promise; - incrementPromised(subpath: string, value: any): Promise; - _increment(segments: Segments, value: any, cb?: ErrorCallback): void; + increment(value?: number): number; + increment(subpath: string, value?: number, cb?: ErrorCallback): number; + incrementPromised(value?: number): Promise; + incrementPromised(subpath: string, value?: number): Promise; + _increment(segments: Segments, value: number, cb?: ErrorCallback): number; - push(value: any): void; - push(subpath: string, value: any, cb?: ErrorCallback): void; - pushPromised(value: any): Promise; - pushPromised(subpath: string, value: any): Promise; - _push(segments: Segments, value: any, cb?: ErrorCallback): void; + push(value: any): number; + push(subpath: string, value: any, cb?: ErrorCallback): number; + pushPromised(value: any): Promise; + pushPromised(subpath: string, value: any): Promise; + _push(segments: Segments, value: any, cb?: ErrorCallback): number; unshift(value: any): void; unshift(subpath: string, value: any, cb?: ErrorCallback): void; @@ -73,27 +84,49 @@ declare module './Model' { unshiftPromised(subpath: string, value: any): Promise; _unshift(segments: Segments, value: any, cb?: ErrorCallback): void; - insert(value: any, index: number): void; + insert(index: number, value: any): void; insert(subpath: string, index: number, value: any, cb?: ErrorCallback): void; insertPromised(value: any, index: number): Promise; insertPromised(subpath: string, index: number, value: any): Promise; _insert(segments: Segments, index: number, value: any, cb?: ErrorCallback): void; - pop(value: any): void; - pop(subpath: string, value: any, cb?: ErrorCallback): void; + /** + * Removes an item from the end of the array at this model's path or a + * relative subpath. + * + * If a callback is provided, it's called when the write is committed or + * fails. + * + * @param subpath + * @returns the removed item + */ + pop(subpath: Path, cb?: Callback): V | undefined; + pop>(cb?: Callback): V | undefined; popPromised(value: any): Promise; popPromised(subpath: string, value: any): Promise; _pop(segments: Segments, value: any, cb?: ErrorCallback): void; - shift(subpath: string, cb?: ErrorCallback): void; + shift(subpath?: string, cb?: ErrorCallback): void; shiftPromised(subpath?: string): Promise; _shift(segments: Segments, cb?: ErrorCallback): void; - remove(index: number, cb?: ErrorCallback): void; - remove(subpath: string, cb?: ErrorCallback): void; - remove(index: number, howMany: number, cb?: ErrorCallback): void; - remove(subpath: string, index: number, cb?: ErrorCallback): void; - remove(subpath: string, index: number, howMany: number, cb?: ErrorCallback): void; + /** + * Removes one or more items from the array at this model's path or a + * relative subpath. + * + * If a callback is provided, it's called when the write is committed or + * fails. + * + * @param subpath + * @param index - 0-based index at which to start removing items + * @param howMany - Number of items to remove. Defaults to `1`. + * @returns array of the removed items + */ + remove(subpath: Path, index: number, howMany?: number, cb?: Callback): V[]; + // Calling `remove(n)` with one argument on a model pointing to a + // non-array results in `N` being `never`, but it still compiles. Is + // there a way to disallow that? + remove>(index: number, howMany?: number, cb?: Callback): V[]; removePromised(index: number): Promise; removePromised(subpath: string): Promise; removePromised(index: number, howMany: number): Promise; @@ -293,7 +326,7 @@ Model.prototype._create = function(segments, value, cb) { var event = new ChangeEvent(value, previous, model._pass); model._emitMutation(segments, event); } - this._mutate(segments, create, cb); + return this._mutate(segments, create, cb); }; Model.prototype.createNull = function() { @@ -329,7 +362,7 @@ Model.prototype._createNull = function(segments, value, cb) { segments = this._dereference(segments); var doc = this.getDoc(segments[0], segments[1]); if (doc && doc.get() != null) return; - this._create(segments, value, cb); + return this._create(segments, value, cb); }; Model.prototype.add = function() { @@ -403,12 +436,14 @@ Model.prototype.del = function() { var segments = this._splitPath(subpath); return this._del(segments, cb); }; + Model.prototype.delPromised = promisify(Model.prototype.del); Model.prototype._del = function(segments, cb) { segments = this._dereference(segments); return this._delNoDereference(segments, cb); }; + Model.prototype._delNoDereference = function(segments, cb) { var model = this; function del(doc, docSegments, fnCb) { @@ -488,7 +523,7 @@ Model.prototype.push = function() { var segments = this._splitPath(subpath); return this._push(segments, value, cb); }; -Model.prototype.pushPromised = promisify(Model.prototype.push); +Model.prototype.pushPromised = promisify(Model.prototype.push); Model.prototype._push = function(segments, value, cb) { var forArrayMutator = true; From 4f3c12ada5064683711fef4cee44e0b056ccded9 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 12:47:14 -0700 Subject: [PATCH 375/479] Add getArray method --- src/Model/collections.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Model/collections.ts b/src/Model/collections.ts index d24637832..2f04fc6fd 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -46,8 +46,11 @@ declare module './Model' { * * @param subpath */ - get(subpath: Path): ReadonlyDeep | undefined; get(): ReadonlyDeep | undefined; + get(subpath?: Path): ReadonlyDeep | undefined; + + getArray(): ReadonlyArray>; + getArray(subPath?: Path): ReadonlyArray>; getCollection(collectionName: string): Collection; @@ -102,6 +105,10 @@ Model.prototype.get = function(subpath?: Path) { return this._get(segments) as ReadonlyDeep; }; +Model.prototype.getArray = function(subpath?: Path) { + return this.get(subpath) || []; +} + Model.prototype._get = function(segments) { return util.lookup(segments, this.root.data); }; From ea932859d73109466c7b230f7020bcda1cd88eca Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 12:49:54 -0700 Subject: [PATCH 376/479] Refine event on method types --- src/Model/EventMapTree.ts | 2 +- src/Model/events.ts | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Model/EventMapTree.ts b/src/Model/EventMapTree.ts index e15460834..33bf409a5 100644 --- a/src/Model/EventMapTree.ts +++ b/src/Model/EventMapTree.ts @@ -85,7 +85,7 @@ export class EventMapTree { if (next) { node = next; } else { - node = new EventMapTree(node, segment); + node = new EventMapTree(node, segment.toString()); children.set(segment, node); } } diff --git a/src/Model/events.ts b/src/Model/events.ts index c262f5abd..5cc02c5cd 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -26,9 +26,9 @@ export interface ModelOnEventMap { } declare module './Model' { - interface Model { + interface Model { addListener(event: string, listener: any, arg2?: any, arg3?: any): any; - eventContext(id: string): Model; + eventContext(id: string): ChildModel; /** * Listen to Racer events matching a certain path or path pattern. @@ -63,20 +63,32 @@ declare module './Model' { pathPattern: PathLike, options: { useEventObjects: true }, listener: (event: ModelOnEventMap[T], captures: Array) => void - ): Function; + ): () => void; on( eventType: T, options: { useEventObjects: true }, listener: (event: ModelOnEventMap[T], captures: Array) => void - ): Function; + ): () => void; + + // TODO review this calling w/o options if options useEventObjects should go away + // without any "legacy" events use case + on( + eventType: T, + pathPattern: PathLike, + listener: (event: ModelOnEventMap[T], captures: Array) => void + ): () => void; + on( + eventType: T, + listener: (event: ModelOnEventMap[T], captures: Array) => void + ): () => void; on( eventType: 'all', listener: (segments: string[], event: ModelOnEventMap[keyof ModelOnEventMap]) => void - ): Function; + ): () => void; on( eventType: 'error', listener: (error: Error) => void - ): Function; + ): () => void; /** From ea4828da35eb55532a15d41a2744bd18c1924f63 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 18 Mar 2024 12:51:17 -0700 Subject: [PATCH 377/479] 2.0.0-beta.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 47a618553..71f9069f1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "main": "./lib/index.js", "files": [ "lib/*" From d01c39327d7d93d62fdaea388a215955464366c2 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 21 Mar 2024 16:27:54 -0700 Subject: [PATCH 378/479] Change default model type paramater to unknown --- src/Model/Model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Model.ts b/src/Model/Model.ts index 6f8ea7432..35b56469c 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -7,7 +7,7 @@ import { Primitive } from '../types'; export type UUID = string; -export type DefualtType = {}; +export type DefualtType = unknown; declare module './Model' { interface DebugOptions { From dbefd3078ed4ca6dac09adafe21549d65e29dba5 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 21 Mar 2024 16:28:31 -0700 Subject: [PATCH 379/479] Refine query typing --- src/Model/Query.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Model/Query.ts b/src/Model/Query.ts index 367795a23..8dafa64c5 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -515,7 +515,7 @@ export class Query { return results; }; - get() { + get(): Array { var results = []; var data = this.model._get(this.segments); if (!data) { @@ -534,11 +534,11 @@ export class Query { return results; }; - getIds() { + getIds(): string[] { return this.model._get(this.idsSegments) || []; }; - getExtra() { + getExtra(): T { return this.model._get(this.extraSegments) as T; }; @@ -607,6 +607,7 @@ export class Query { return serialized; }; } + function queryHash(contextId, collectionName, expression, options) { var args = [contextId, collectionName, expression, options]; return JSON.stringify(args).replace(/\./g, '|'); From 026eb27ab37ae13378533e56468f98a54f61d0e2 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 21 Mar 2024 16:29:55 -0700 Subject: [PATCH 380/479] Refine types for event handlers; differentiate legacy and object-event handler types --- src/Model/events.ts | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 5cc02c5cd..2a2a263ef 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -25,6 +25,29 @@ export interface ModelOnEventMap { all: ModelEvent; } +export interface ModelOnEventLegacyListers { + change: (captures: string, currentValue, prevValue, passed) => void; + insert: (captures: string, howMany: number, items, passed) => void; + remove: (captures: string, howMany: number, items, passed) => void; + move: (captures: string, fromIndex: number, toIndex: number, howMany: number, passed) => void; + load: (capture: string, item) => void; + unload: (captures: string, item) => void; + all: (captures: string, eventName: string, ...args) => void; +} + +/** + * Racer emits captures like + * 'foo.bar.1' + * Derby emits captures like + * ['foo', 'bar', '1'] + * */ +type EventCaptures = string | string[]; +/** + * With `useEventObjects: true` captures are emmitted as + * ['foo.bar.1'] + * */ +type EventObjectCaptures = string[]; + declare module './Model' { interface Model { addListener(event: string, listener: any, arg2?: any, arg3?: any): any; @@ -62,28 +85,28 @@ declare module './Model' { eventType: T, pathPattern: PathLike, options: { useEventObjects: true }, - listener: (event: ModelOnEventMap[T], captures: Array) => void + listener: (event: ModelOnEventMap[T], captures: EventObjectCaptures) => void ): () => void; on( eventType: T, options: { useEventObjects: true }, - listener: (event: ModelOnEventMap[T], captures: Array) => void + listener: (event: ModelOnEventMap[T], captures: EventObjectCaptures) => void ): () => void; // TODO review this calling w/o options if options useEventObjects should go away // without any "legacy" events use case - on( + on( eventType: T, pathPattern: PathLike, - listener: (event: ModelOnEventMap[T], captures: Array) => void + listener: ModelOnEventLegacyListers[T] ): () => void; - on( + on( eventType: T, - listener: (event: ModelOnEventMap[T], captures: Array) => void + listener: ModelOnEventLegacyListers[T] ): () => void; on( eventType: 'all', - listener: (segments: string[], event: ModelOnEventMap[keyof ModelOnEventMap]) => void + listener: (captures: EventCaptures, event: ModelOnEventMap[keyof ModelOnEventMap]) => void ): () => void; on( eventType: 'error', From 30892f3895e99a153acd88c76dfcaed39572c62c Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 21 Mar 2024 16:30:31 -0700 Subject: [PATCH 381/479] Refine return types for pass and silent methods --- src/Model/events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 2a2a263ef..16bf18adf 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -143,7 +143,7 @@ declare module './Model' { * @param object - An object whose properties will each be set on the passed argument * @returns back a model scoped to the same path */ - pass(object: object, invert?: boolean): Model; + pass(object: object, invert?: boolean): Model; removeAllListeners(type: string, subpath: string): void; removeContextListeners(): void; @@ -151,7 +151,7 @@ declare module './Model' { removeListener(eventType: keyof ModelOnEventMap, listener: Function): void; setMaxListeners(limit: number): void; - silent(value?: boolean): Model; + silent(value?: boolean): Model; wrapCallback(cb?: ErrorCallback): ErrorCallback; __on: typeof EventEmitter.prototype.on; From 4bc1c2becc8978f2cd732fc7f67ced144680c62a Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 21 Mar 2024 16:31:27 -0700 Subject: [PATCH 382/479] Ensure event classes can be type discrimated by type attribute by making read only --- src/Model/events.ts | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 16bf18adf..b047bf061 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -589,8 +589,8 @@ function createMutationListenerLegacy(type, pattern, eventContext, cb) { } export class ChangeEvent { - declare type: string; - declare _immediateType: string; + readonly type = 'change'; + readonly _immediateType = 'changeImmediate'; value: any; previous: any; passed: any; @@ -609,12 +609,10 @@ export class ChangeEvent { return [this.value, this.previous, this.passed]; }; } -ChangeEvent.prototype.type = 'change'; -ChangeEvent.prototype._immediateType = 'changeImmediate'; export class LoadEvent { - declare type: string; - declare _immediateType: string; + readonly type = 'load'; + readonly _immediateType = 'loadImmediate'; value: any; document: any; passed: any; @@ -638,12 +636,10 @@ export class LoadEvent { return [this.value, this.passed]; }; } -LoadEvent.prototype.type = 'load'; -LoadEvent.prototype._immediateType = 'loadImmediate'; export class UnloadEvent { - declare type: string; - declare _immediateType: string; + readonly type = 'unload'; + readonly _immediateType = 'unloadImmediate'; previous: any; previousDocument: any; passed: any; @@ -667,12 +663,10 @@ export class UnloadEvent { return [this.previous, this.passed]; }; } -UnloadEvent.prototype.type = 'unload'; -UnloadEvent.prototype._immediateType = 'unloadImmediate'; export class InsertEvent { - declare type: string; - declare _immediateType: string; + readonly type = 'insert'; + readonly _immediateType = 'insertImmediate'; index: number; values: any; passed: any; @@ -691,12 +685,10 @@ export class InsertEvent { return [this.index, this.values, this.passed]; }; } -InsertEvent.prototype.type = 'insert'; -InsertEvent.prototype._immediateType = 'insertImmediate'; export class RemoveEvent { - declare type: string; - declare _immediateType: string; + readonly type = 'remove'; + readonly _immediateType = 'removeImmediate'; index: number; passed: any; removed: any; @@ -722,12 +714,10 @@ export class RemoveEvent { return [this.index, this.values, this.passed]; }; } -RemoveEvent.prototype.type = 'remove'; -RemoveEvent.prototype._immediateType = 'removeImmediate'; export class MoveEvent { - declare type: string; - declare _immediateType: string; + readonly type = 'move'; + readonly _immediateType = 'moveImmediate'; from: any; howMany: number; passed: any; @@ -748,8 +738,6 @@ export class MoveEvent { return [this.from, this.to, this.howMany, this.passed]; }; } -MoveEvent.prototype.type = 'move'; -MoveEvent.prototype._immediateType = 'moveImmediate'; // DEPRECATED: Normalize pattern ending in '**' to '.**', since these are // treated equivalently. The '.**' form is preferred, and it should be enforced From 9381f87b9aa783b55b8bafc8cfb1dd72f3cd7708 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 21 Mar 2024 16:32:36 -0700 Subject: [PATCH 383/479] Fix method typing for set, setNull, setDiff, at, scope --- src/Model/mutators.ts | 28 ++++++++++++++-------------- src/Model/paths.ts | 7 ++++--- src/Model/setDiff.ts | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 42c38e2d4..74d9854d2 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -14,17 +14,17 @@ var promisify = util.promisify; declare module './Model' { interface Model { _mutate(segments, fn, cb): void; - set(value: any): S; - set(subpath: string, value: any, cb?: ErrorCallback): S; - setPromised(value: any): Promise; - setPromised(subpath: string, value: any): Promise; - _set(segments: Segments, value: any, cb?: ErrorCallback): S; - - setNull(value: any): S; - setNull(subpath: string, value: any, cb?: ErrorCallback): S; - setNullPromised(value: any): Promise; - setNullPromised(subpath: string, value: any): Promise; - _setNull(segments: Segments, value: any, cb?: ErrorCallback): S; + set(value: T): T | undefined; + set(subpath: string, value: any, cb?: ErrorCallback): S | undefined; + setPromised(value: T): Promise; + setPromised(subpath: string, value: any): Promise; + _set(segments: Segments, value: any, cb?: ErrorCallback): S | undefined; + + setNull(value: T): T | undefined; + setNull(subpath: string, value: S, cb?: ErrorCallback): S | undefined; + setNullPromised(value: T): Promise; + setNullPromised(subpath: string, value: S): Promise; + _setNull(segments: Segments, value: S, cb?: ErrorCallback): S | undefined; setEach(value: any): void; setEach(subpath: string, value: any, cb?: ErrorCallback): void; @@ -106,9 +106,9 @@ declare module './Model' { popPromised(subpath: string, value: any): Promise; _pop(segments: Segments, value: any, cb?: ErrorCallback): void; - shift(subpath?: string, cb?: ErrorCallback): void; - shiftPromised(subpath?: string): Promise; - _shift(segments: Segments, cb?: ErrorCallback): void; + shift(subpath?: string, cb?: ErrorCallback): S; + shiftPromised(subpath?: string): Promise; + _shift(segments: Segments, cb?: ErrorCallback): S; /** * Removes one or more items from the array at this model's path or a diff --git a/src/Model/paths.ts b/src/Model/paths.ts index 19915d87b..a1443add5 100644 --- a/src/Model/paths.ts +++ b/src/Model/paths.ts @@ -1,18 +1,19 @@ import { ChildModel, Model } from './Model'; import type { Path, PathLike } from '../types'; +import { ModelData } from './collections'; exports.mixin = {}; declare module './Model' { interface Model { at(): ChildModel; - at(subpath: Path): ChildModel; + at(subpath: Path): ChildModel; isPath(subpath: string): boolean; leaf(path: string): string; parent(levels?: number): Model; path(subpath?: PathLike): string; - scope(): ChildModel; - scope(subpath: Path): ChildModel; + scope(): ChildModel; + scope(subpath: Path): ChildModel; _splitPath(subpath: string): string[]; } diff --git a/src/Model/setDiff.ts b/src/Model/setDiff.ts index 83de78fce..68d3eee8a 100644 --- a/src/Model/setDiff.ts +++ b/src/Model/setDiff.ts @@ -24,7 +24,7 @@ declare module './Model' { * @returns the value previously at the path */ setDiff(subpath: Path, value: S, cb?: Callback): ReadonlyDeep | undefined; - setDiff(value: T): ReadonlyDeep | undefined; + setDiff(value: T | undefined): ReadonlyDeep | undefined; setDiffPromised(subpath: string, value: S): Promise; _setDiff(segments: Segments, value: any, cb?: (err: Error) => void): void; From f7095f5f5466683aa2fec82df22d97020c3b4245 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 21 Mar 2024 16:33:13 -0700 Subject: [PATCH 384/479] Re-export ModelOptions --- src/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8e2540997..39b6d67e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,10 +3,10 @@ import * as util from './util'; import type { ShareDBOptions } from 'sharedb'; import { RacerBackend } from './Backend'; -import { ModelOptions, RootModel } from './Model'; +import { RootModel, type ModelOptions } from './Model'; export { Query } from './Model/Query'; -export { Model, ChildModel, ModelData, type UUID, type Subscribable, type DefualtType } from './Model'; +export { Model, ChildModel, ModelData, type UUID, type Subscribable, type DefualtType, type ModelOptions } from './Model'; export { Context } from './Model/contexts'; export { type ModelOnEventMap, type ModelEvent, ChangeEvent, InsertEvent, LoadEvent, MoveEvent, RemoveEvent, UnloadEvent } from './Model/events'; export type { Callback, ReadonlyDeep, Path, PathLike, PathSegment, Primitive } from './types'; @@ -18,7 +18,6 @@ const { use, serverUse } = util; export type BackendOptions = { modelOptions?: ModelOptions } & ShareDBOptions; export { - ModelOptions, Racer, RacerBackend, RootModel, From 6a4d2ee7419876f29b08b24bcf5cd7010146dc49 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 21 Mar 2024 17:34:06 -0700 Subject: [PATCH 385/479] Refactor castSegments + add tests --- src/util.ts | 27 +++++++++++++++------------ test/util/util.js | 41 +++++++++++++++++++++++++++++++---------- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/util.ts b/src/util.ts index 08eb5d1f4..9b5a3b879 100644 --- a/src/util.ts +++ b/src/util.ts @@ -39,19 +39,22 @@ class AsyncGroup { } } -/** - * @param {Array} segments - * @return {Array} - */ -export function castSegments(segments) { - // Cast number path segments from strings to numbers - for (var i = segments.length; i--;) { - var segment = segments[i]; - if (typeof segment === 'string' && isArrayIndex(segment)) { - segments[i] = +segment; - } +function castSegment(segment: string): string | number { + return (typeof segment === 'string' && isArrayIndex(segment)) + ? +segment // sneaky op to convert numeric string to number + : segment; +} + +export function castSegments(segments: Readonly) { + if (typeof segments === 'string') { + return castSegment(segments); } - return segments; + // Cast number path segments from strings to numbers + return segments.map(segment => + Array.isArray(segment) + ? castSegments(segment) + : castSegment(segment) + ); } export function contains(segments, testSegments) { diff --git a/test/util/util.js b/test/util/util.js index b48c19723..520f35355 100644 --- a/test/util/util.js +++ b/test/util/util.js @@ -2,14 +2,14 @@ var expect = require('../util').expect; var util = require('../../lib/util'); describe('util', function() { - describe('util.mergeInto', function() { - it('merges empty objects', function() { + describe('util.mergeInto', () => { + it('merges empty objects', () => { var a = {}; var b = {}; expect(util.mergeInto(a, b)).to.eql({}); }); - it('merges an empty object with a populated object', function() { + it('merges an empty object with a populated object', () => { var fn = function(x) { return x++; }; @@ -18,7 +18,7 @@ describe('util', function() { expect(util.mergeInto(a, b)).to.eql({x: 's', y: [1, 3], fn: fn}); }); - it('merges a populated object with a populated object', function() { + it('merges a populated object with a populated object', () => { var fn = function(x) { return x++; }; @@ -32,10 +32,10 @@ describe('util', function() { }); }); - describe('promisify', function() { - it('wrapped functions return promise', async function() { + describe('promisify', () => { + it('wrapped functions return promise', async () => { var targetFn = function(num, cb) { - setImmediate(function() { + setImmediate(() => { cb(undefined, num); }); }; @@ -46,9 +46,9 @@ describe('util', function() { expect(result).to.equal(3); }); - it('wrapped functions throw errors passed to callback', async function() { + it('wrapped functions throw errors passed to callback', async () => { var targetFn = function(num, cb) { - setImmediate(function() { + setImmediate(() => { cb(new Error(`Error ${num}`)); }); }; @@ -61,7 +61,7 @@ describe('util', function() { } }); - it('wrapped functions throw on thrown error', async function() { + it('wrapped functions throw on thrown error', async () => { var targetFn = function(num) { throw new Error(`Error ${num}`); }; @@ -74,4 +74,25 @@ describe('util', function() { } }); }); + + describe('castSegments', () => { + it('passes non-numeric strings as-is', () => { + const segments = ['foo', 'bar']; + const actual = util.castSegments(segments); + expect(actual).to.eql(['foo', 'bar']); + expect(actual).to.not.equal(segments); // args not mutated + }); + + it('ensures numeric path segments are returned as numbers', () => { + const segments = ['foo', '3', 3]; + const actual = util.castSegments(segments); + expect(actual).to.eql(['foo', 3, 3]); + expect(actual).to.not.equal(segments); // args not mutated + }); + + it('handles plain strings', () => { + expect(util.castSegments('foo.bar')).to.eql('foo.bar'); + expect(util.castSegments('6')).to.eql(6); + }); + }); }); From 47f7d4d8cdcfb3e36ab45963e131fe7c91b1bec5 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 21 Mar 2024 18:05:17 -0700 Subject: [PATCH 386/479] Change event classes back to use prototype so as to not break event tree setup --- src/Model/events.ts | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index b047bf061..b3fa6c66b 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -589,8 +589,8 @@ function createMutationListenerLegacy(type, pattern, eventContext, cb) { } export class ChangeEvent { - readonly type = 'change'; - readonly _immediateType = 'changeImmediate'; + declare type: 'change'; + declare _immediateType: 'changeImmediate'; value: any; previous: any; passed: any; @@ -609,10 +609,12 @@ export class ChangeEvent { return [this.value, this.previous, this.passed]; }; } +ChangeEvent.prototype.type = 'change'; +ChangeEvent.prototype._immediateType = 'changeImmediate'; export class LoadEvent { - readonly type = 'load'; - readonly _immediateType = 'loadImmediate'; + declare type: 'load'; + declare _immediateType: 'loadImmediate'; value: any; document: any; passed: any; @@ -636,10 +638,12 @@ export class LoadEvent { return [this.value, this.passed]; }; } +LoadEvent.prototype.type = 'load'; +LoadEvent.prototype._immediateType = 'loadImmediate'; export class UnloadEvent { - readonly type = 'unload'; - readonly _immediateType = 'unloadImmediate'; + declare type: 'unload'; + declare _immediateType: 'unloadImmediate'; previous: any; previousDocument: any; passed: any; @@ -663,10 +667,12 @@ export class UnloadEvent { return [this.previous, this.passed]; }; } +UnloadEvent.prototype.type = 'unload'; +UnloadEvent.prototype._immediateType = 'unloadImmediate'; export class InsertEvent { - readonly type = 'insert'; - readonly _immediateType = 'insertImmediate'; + declare type: 'insert'; + declare _immediateType: 'insertImmediate'; index: number; values: any; passed: any; @@ -685,10 +691,12 @@ export class InsertEvent { return [this.index, this.values, this.passed]; }; } +InsertEvent.prototype.type = 'insert'; +InsertEvent.prototype._immediateType = 'insertImmediate'; export class RemoveEvent { - readonly type = 'remove'; - readonly _immediateType = 'removeImmediate'; + declare type: 'remove'; + declare _immediateType: 'removeImmediate'; index: number; passed: any; removed: any; @@ -714,10 +722,12 @@ export class RemoveEvent { return [this.index, this.values, this.passed]; }; } +RemoveEvent.prototype.type = 'remove'; +RemoveEvent.prototype._immediateType = 'removeImmediate'; export class MoveEvent { - readonly type = 'move'; - readonly _immediateType = 'moveImmediate'; + declare type: 'move'; + declare _immediateType: 'moveImmediate'; from: any; howMany: number; passed: any; @@ -738,6 +748,8 @@ export class MoveEvent { return [this.from, this.to, this.howMany, this.passed]; }; } +MoveEvent.prototype.type = 'move'; +MoveEvent.prototype._immediateType = 'moveImmediate'; // DEPRECATED: Normalize pattern ending in '**' to '.**', since these are // treated equivalently. The '.**' form is preferred, and it should be enforced From b11607d6f7530de1cdbd9d7cd77c3dbade230d03 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 21 Mar 2024 18:07:25 -0700 Subject: [PATCH 387/479] 2.0.0-beta.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 71f9069f1..250dea499 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "main": "./lib/index.js", "files": [ "lib/*" From 7c8bfbb24efde899ccefb658dcbf258ac0df5133 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 22 Mar 2024 12:11:42 -0700 Subject: [PATCH 388/479] Add optinoal callback param to mutator events with value as first arg --- src/Model/mutators.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 74d9854d2..3cbf9041c 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -14,37 +14,37 @@ var promisify = util.promisify; declare module './Model' { interface Model { _mutate(segments, fn, cb): void; - set(value: T): T | undefined; + set(value: T, cb?: ErrorCallback): T | undefined; set(subpath: string, value: any, cb?: ErrorCallback): S | undefined; setPromised(value: T): Promise; setPromised(subpath: string, value: any): Promise; _set(segments: Segments, value: any, cb?: ErrorCallback): S | undefined; - setNull(value: T): T | undefined; + setNull(value: T, cb?: ErrorCallback): T | undefined; setNull(subpath: string, value: S, cb?: ErrorCallback): S | undefined; setNullPromised(value: T): Promise; setNullPromised(subpath: string, value: S): Promise; _setNull(segments: Segments, value: S, cb?: ErrorCallback): S | undefined; - setEach(value: any): void; + setEach(value: any, cb?: ErrorCallback): void; setEach(subpath: string, value: any, cb?: ErrorCallback): void; setEachPromised(value: any): Promise; setEachPromised(subpath: string, value: any): Promise; _setEach(segments: Segments, value: any, cb?: ErrorCallback): void; - create(value: any): void; + create(value: any, cb?: ErrorCallback): void; create(subpath: string, value: any, cb?: ErrorCallback): void; createPromised(value: any): Promise; createPromised(subpath: string, value: any): Promise; _create(segments: Segments, value: any, cb?: ErrorCallback): void; - createNull(value: any): void; + createNull(value: any, cb?: ErrorCallback): void; createNull(subpath: string, value: any, cb?: ErrorCallback): void; createNullPromised(value: any): Promise; createNullPromised(subpath: string, value: any): Promise; _createNull(segments: Segments, value: any, cb?: ErrorCallback): void; - add(value: any): string; + add(value: any, cb?: ErrorCallback): string; add(subpath: string, value: any, cb?: ErrorCallback): string; addPromised(value: any): Promise; addPromised(subpath: string, value: any): Promise; From ee23c0dc83ec99c9dabe11f3854b63b7ab90bd2a Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 22 Mar 2024 13:30:41 -0700 Subject: [PATCH 389/479] Change subpath types form string to Path --- src/Model/collections.ts | 2 +- src/Model/events.ts | 4 +-- src/Model/filter.ts | 4 +-- src/Model/fn.ts | 6 ++-- src/Model/mutators.ts | 78 ++++++++++++++++++++-------------------- src/Model/paths.ts | 4 +-- src/Model/ref.ts | 4 +-- src/Model/setDiff.ts | 2 +- 8 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/Model/collections.ts b/src/Model/collections.ts index 2f04fc6fd..f14f3d644 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -31,7 +31,7 @@ declare module './Model' { data: ModelData; } interface Model { - destroy(subpath?: string): void; + destroy(subpath?: Path): void; /** * Gets the value located at this model's path or a relative subpath. diff --git a/src/Model/events.ts b/src/Model/events.ts index b3fa6c66b..21e78e7e6 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -5,7 +5,7 @@ import { EventListenerTree } from './EventListenerTree'; import { type Segments } from './types'; import { Model } from './Model'; import { mergeInto } from '../util'; -import { PathLike } from '../types'; +import type { Path, PathLike } from '../types'; export type ModelEvent = | ChangeEvent @@ -145,7 +145,7 @@ declare module './Model' { */ pass(object: object, invert?: boolean): Model; - removeAllListeners(type: string, subpath: string): void; + removeAllListeners(type: string, subpath: Path): void; removeContextListeners(): void; removeListener(eventType: keyof ModelOnEventMap, listener: Function): void; diff --git a/src/Model/filter.ts b/src/Model/filter.ts index 68c1efb05..5bd3927af 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -2,7 +2,7 @@ var util = require('../util'); import { Model } from './Model'; import { type Segments } from './types'; import * as defaultFns from './defaultFns'; -import { PathLike } from '../types'; +import type { Path, PathLike } from '../types'; interface PaginationOptions { skip: number; @@ -48,7 +48,7 @@ declare module './Model' { fn?: (item: S, key: string, object: { [key: string]: S }) => boolean ): Filter; - removeAllFilters: (subpath: string) => void; + removeAllFilters: (subpath: Path) => void; /** * Creates a live-updating list from items in an object, which results in diff --git a/src/Model/fn.ts b/src/Model/fn.ts index e165a0539..ce653f8f9 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -3,7 +3,7 @@ import { Model } from './Model'; import { EventListenerTree } from './EventListenerTree'; import { EventMapTree } from './EventMapTree'; import * as defaultFns from './defaultFns'; -import { PathLike, ReadonlyDeep } from '../types'; +import type { Path, PathLike, ReadonlyDeep } from '../types'; var util = require('../util'); class NamedFns { } @@ -134,8 +134,8 @@ declare module './Model' { fn: ModelFn | string ): Out; - stop(subpath: string): void; - stopAll(subpath: string): void; + stop(subpath: Path): void; + stopAll(subpath: Path): void; _fns: Fns; _namedFns: NamedFns; diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 3cbf9041c..3db1a1f07 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -15,39 +15,39 @@ declare module './Model' { interface Model { _mutate(segments, fn, cb): void; set(value: T, cb?: ErrorCallback): T | undefined; - set(subpath: string, value: any, cb?: ErrorCallback): S | undefined; + set(subpath: Path, value: any, cb?: ErrorCallback): S | undefined; setPromised(value: T): Promise; - setPromised(subpath: string, value: any): Promise; + setPromised(subpath: Path, value: any): Promise; _set(segments: Segments, value: any, cb?: ErrorCallback): S | undefined; setNull(value: T, cb?: ErrorCallback): T | undefined; - setNull(subpath: string, value: S, cb?: ErrorCallback): S | undefined; + setNull(subpath: Path, value: S, cb?: ErrorCallback): S | undefined; setNullPromised(value: T): Promise; - setNullPromised(subpath: string, value: S): Promise; + setNullPromised(subpath: Path, value: S): Promise; _setNull(segments: Segments, value: S, cb?: ErrorCallback): S | undefined; setEach(value: any, cb?: ErrorCallback): void; - setEach(subpath: string, value: any, cb?: ErrorCallback): void; + setEach(subpath: Path, value: any, cb?: ErrorCallback): void; setEachPromised(value: any): Promise; - setEachPromised(subpath: string, value: any): Promise; + setEachPromised(subpath: Path, value: any): Promise; _setEach(segments: Segments, value: any, cb?: ErrorCallback): void; create(value: any, cb?: ErrorCallback): void; - create(subpath: string, value: any, cb?: ErrorCallback): void; + create(subpath: Path, value: any, cb?: ErrorCallback): void; createPromised(value: any): Promise; - createPromised(subpath: string, value: any): Promise; + createPromised(subpath: Path, value: any): Promise; _create(segments: Segments, value: any, cb?: ErrorCallback): void; createNull(value: any, cb?: ErrorCallback): void; - createNull(subpath: string, value: any, cb?: ErrorCallback): void; + createNull(subpath: Path, value: any, cb?: ErrorCallback): void; createNullPromised(value: any): Promise; - createNullPromised(subpath: string, value: any): Promise; + createNullPromised(subpath: Path, value: any): Promise; _createNull(segments: Segments, value: any, cb?: ErrorCallback): void; add(value: any, cb?: ErrorCallback): string; - add(subpath: string, value: any, cb?: ErrorCallback): string; + add(subpath: Path, value: any, cb?: ErrorCallback): string; addPromised(value: any): Promise; - addPromised(subpath: string, value: any): Promise; + addPromised(subpath: Path, value: any): Promise; _add(segments: Segments, value: any, cb?: ErrorCallback): string; /** @@ -61,33 +61,33 @@ declare module './Model' { */ del(subpath: Path, cb?: Callback): S | undefined; del(cb?: Callback): T | undefined; - delPromised(subpath: string): Promise; + delPromised(subpath: Path): Promise; _del(segments: Segments, cb?: ErrorCallback): S; _delNoDereference(segments: Segments, cb?: ErrorCallback): void; increment(value?: number): number; - increment(subpath: string, value?: number, cb?: ErrorCallback): number; + increment(subpath: Path, value?: number, cb?: ErrorCallback): number; incrementPromised(value?: number): Promise; - incrementPromised(subpath: string, value?: number): Promise; + incrementPromised(subpath: Path, value?: number): Promise; _increment(segments: Segments, value: number, cb?: ErrorCallback): number; push(value: any): number; - push(subpath: string, value: any, cb?: ErrorCallback): number; + push(subpath: Path, value: any, cb?: ErrorCallback): number; pushPromised(value: any): Promise; - pushPromised(subpath: string, value: any): Promise; + pushPromised(subpath: Path, value: any): Promise; _push(segments: Segments, value: any, cb?: ErrorCallback): number; unshift(value: any): void; - unshift(subpath: string, value: any, cb?: ErrorCallback): void; + unshift(subpath: Path, value: any, cb?: ErrorCallback): void; unshiftPromised(value: any): Promise; - unshiftPromised(subpath: string, value: any): Promise; + unshiftPromised(subpath: Path, value: any): Promise; _unshift(segments: Segments, value: any, cb?: ErrorCallback): void; insert(index: number, value: any): void; - insert(subpath: string, index: number, value: any, cb?: ErrorCallback): void; + insert(subpath: Path, index: number, value: any, cb?: ErrorCallback): void; insertPromised(value: any, index: number): Promise; - insertPromised(subpath: string, index: number, value: any): Promise; + insertPromised(subpath: Path, index: number, value: any): Promise; _insert(segments: Segments, index: number, value: any, cb?: ErrorCallback): void; /** @@ -103,11 +103,11 @@ declare module './Model' { pop(subpath: Path, cb?: Callback): V | undefined; pop>(cb?: Callback): V | undefined; popPromised(value: any): Promise; - popPromised(subpath: string, value: any): Promise; + popPromised(subpath: Path, value: any): Promise; _pop(segments: Segments, value: any, cb?: ErrorCallback): void; - shift(subpath?: string, cb?: ErrorCallback): S; - shiftPromised(subpath?: string): Promise; + shift(subpath?: Path, cb?: ErrorCallback): S; + shiftPromised(subpath?: Path): Promise; _shift(segments: Segments, cb?: ErrorCallback): S; /** @@ -128,40 +128,40 @@ declare module './Model' { // there a way to disallow that? remove>(index: number, howMany?: number, cb?: Callback): V[]; removePromised(index: number): Promise; - removePromised(subpath: string): Promise; + removePromised(subpath: Path): Promise; removePromised(index: number, howMany: number): Promise; - removePromised(subpath: string, index: number): Promise; - removePromised(subpath: string, index: number, howMany: number): void; + removePromised(subpath: Path, index: number): Promise; + removePromised(subpath: Path, index: number, howMany: number): void; _remove(segments: Segments, index: number, howMany: number, cb?: ErrorCallback): void; move(from: number, to: number, cb?: ErrorCallback): void; move(from: number, to: number, howMany: number, cb?: ErrorCallback): void; - move(subpath: string, from: number, to: number, cb?: ErrorCallback): void; - move(subpath: string, from: number, to: number, howmany: number, cb?: ErrorCallback): void; + move(subpath: Path, from: number, to: number, cb?: ErrorCallback): void; + move(subpath: Path, from: number, to: number, howmany: number, cb?: ErrorCallback): void; movePromised(from: number, to: number): Promise; movePromised(from: number, to: number, howMany: number): Promise; - movePromised(subpath: string, from: number, to: number): Promise; - movePromised(subpath: string, from: number, to: number, howmany: number): Promise; + movePromised(subpath: Path, from: number, to: number): Promise; + movePromised(subpath: Path, from: number, to: number, howmany: number): Promise; _move(segments: Segments, from: number, to: number, owMany: number, cb?: ErrorCallback): void; stringInsert(index: number, text: string, cb?: ErrorCallback): void; - stringInsert(subpath: string, index: number, text: string, cb?: ErrorCallback): void; + stringInsert(subpath: Path, index: number, text: string, cb?: ErrorCallback): void; stringInsertPromised(index: number, text: string): Promise; - stringInsertPromised(subpath: string, index: number, text: string): Promise; + stringInsertPromised(subpath: Path, index: number, text: string): Promise; _stringInsert(segments: Segments, index: number,text: string, cb?: ErrorCallback): void; stringRemove(index: number, howMany: number, cb?: ErrorCallback): void; - stringRemove(subpath: string, index: number, cb?: ErrorCallback): void; - stringRemove(subpath: string, index: number, howMany: number, cb?: ErrorCallback): void; + stringRemove(subpath: Path, index: number, cb?: ErrorCallback): void; + stringRemove(subpath: Path, index: number, howMany: number, cb?: ErrorCallback): void; stringRemovePromised(index: number, howMany: number): Promise; - stringRemovePromised(subpath: string, index: number): Promise; - stringRemovePromised(subpath: string, index: number, howMany: number): Promise; + stringRemovePromised(subpath: Path, index: number): Promise; + stringRemovePromised(subpath: Path, index: number, howMany: number): Promise; _stringRemove(segments: Segments, index: number, howMany: number, cb?: ErrorCallback): void; subtypeSubmit(subtype: any, subtypeOp: any, cb?: ErrorCallback): void; - subtypeSubmit(subpath: string, subtype: any, subtypeOp: any, cb?: ErrorCallback): void; + subtypeSubmit(subpath: Path, subtype: any, subtypeOp: any, cb?: ErrorCallback): void; subtypeSubmitPromised(subtype: any, subtypeOp: any): Promise; - subtypeSubmitPromised(subpath: string, subtype: any, subtypeOp: any): Promise; + subtypeSubmitPromised(subpath: Path, subtype: any, subtypeOp: any): Promise; _subtypeSubmit(segments: Segments, subtype: any, subtypeOp: any, cb?: ErrorCallback): void; } } diff --git a/src/Model/paths.ts b/src/Model/paths.ts index a1443add5..3598aa030 100644 --- a/src/Model/paths.ts +++ b/src/Model/paths.ts @@ -8,14 +8,14 @@ declare module './Model' { interface Model { at(): ChildModel; at(subpath: Path): ChildModel; - isPath(subpath: string): boolean; + isPath(subpath: Path): boolean; leaf(path: string): string; parent(levels?: number): Model; path(subpath?: PathLike): string; scope(): ChildModel; scope(subpath: Path): ChildModel; - _splitPath(subpath: string): string[]; + _splitPath(subpath: Path): string[]; } } diff --git a/src/Model/ref.ts b/src/Model/ref.ts index c127d63e5..397765641 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -4,7 +4,7 @@ import { Model } from './Model'; import { type Segments } from './types'; import { type Filter } from './filter'; import { type Query } from './Query'; -import { PathLike } from '../types'; +import type { Path, PathLike } from '../types'; type Refable = string | number | Model | Query | Filter; @@ -66,7 +66,7 @@ declare module './Model' { removeAllRefs(subpath: PathLike): void; _removeAllRefs(segments: Segments): void; - dereference(subpath: string): Segments; + dereference(subpath: Path): Segments; _dereference(segments: Segments, forArrayMutator: any, ignore: boolean): Segments; _refs: any; diff --git a/src/Model/setDiff.ts b/src/Model/setDiff.ts index 68d3eee8a..ebd15b385 100644 --- a/src/Model/setDiff.ts +++ b/src/Model/setDiff.ts @@ -25,7 +25,7 @@ declare module './Model' { */ setDiff(subpath: Path, value: S, cb?: Callback): ReadonlyDeep | undefined; setDiff(value: T | undefined): ReadonlyDeep | undefined; - setDiffPromised(subpath: string, value: S): Promise; + setDiffPromised(subpath: Path, value: S): Promise; _setDiff(segments: Segments, value: any, cb?: (err: Error) => void): void; /** From 76ffe7a4de6201d1bebe1a16990a5cdf2080f7cd Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 22 Mar 2024 13:31:51 -0700 Subject: [PATCH 390/479] 2.0.0-beta.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 250dea499..40836b94a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "main": "./lib/index.js", "files": [ "lib/*" From e9d5ef89a78f0838d93eaeb4fca90944d4a424e1 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 22 Mar 2024 13:47:50 -0700 Subject: [PATCH 391/479] Path methods accept PathLike --- src/Model/paths.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Model/paths.ts b/src/Model/paths.ts index 3598aa030..6f911abe8 100644 --- a/src/Model/paths.ts +++ b/src/Model/paths.ts @@ -7,19 +7,19 @@ exports.mixin = {}; declare module './Model' { interface Model { at(): ChildModel; - at(subpath: Path): ChildModel; - isPath(subpath: Path): boolean; + at(subpath: PathLike): ChildModel; + isPath(subpath: PathLike): boolean; leaf(path: string): string; parent(levels?: number): Model; path(subpath?: PathLike): string; scope(): ChildModel; scope(subpath: Path): ChildModel; - _splitPath(subpath: Path): string[]; + _splitPath(subpath: PathLike): string[]; } } -Model.prototype._splitPath = function(subpath?: Path): string[] { +Model.prototype._splitPath = function(subpath?: PathLike): string[] { var path = this.path(subpath); return (path && path.split('.')) || []; }; @@ -32,20 +32,19 @@ Model.prototype._splitPath = function(subpath?: Path): string[] { * @return {String} absolute path * @api public */ -Model.prototype.path = function(subpath?: Path): string { +Model.prototype.path = function(subpath?: PathLike): string { if (subpath == null || subpath === '') return (this._at) ? this._at : ''; if (typeof subpath === 'string' || typeof subpath === 'number') { return (this._at) ? this._at + '.' + subpath : '' + subpath; } - // @ts-ignore if (typeof subpath.path === 'function') return subpath.path(); }; -Model.prototype.isPath = function(subpath?: Path): boolean { +Model.prototype.isPath = function(subpath?: PathLike): boolean { return this.path(subpath) != null; }; -Model.prototype.scope = function(path?: Path): ChildModel { +Model.prototype.scope = function(path?: PathLike): ChildModel { if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { path = path + '.' + arguments[i]; From 8a54e90361ff673dede06adfb44020a254ef4f13 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 28 Mar 2024 15:21:46 -0700 Subject: [PATCH 392/479] Allow addional keys for QueryOptions --- src/Model/Query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Query.ts b/src/Model/Query.ts index 8dafa64c5..8aec41e2c 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -11,7 +11,7 @@ var defaultType = require('sharedb/lib/client').types.defaultType; var util = require('../util'); var promisify = util.promisify; -export type QueryOptions = string | { db: any }; +export type QueryOptions = string | { db: any, [key: string]: unknown }; interface QueryCtor { new (model: Model, collectionName: string, expression: any, options: QueryOptions): Query; From 0050436ad935de3f40ebbb6f47160b57c2341ff5 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 28 Mar 2024 15:22:46 -0700 Subject: [PATCH 393/479] Doc comment for push --- src/Model/mutators.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 3db1a1f07..ec55ea263 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -72,6 +72,13 @@ declare module './Model' { incrementPromised(subpath: Path, value?: number): Promise; _increment(segments: Segments, value: number, cb?: ErrorCallback): number; + /** + * Push a value to a model array + * + * @param subpath + * @param value + * @returns the length of the array + */ push(value: any): number; push(subpath: Path, value: any, cb?: ErrorCallback): number; pushPromised(value: any): Promise; From 654ec892936151acf79f5a25c2b047bd72f098ae Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 1 Apr 2024 12:45:16 -0700 Subject: [PATCH 394/479] Extract FilterFn and SortFn defs making FilterFn allow null --- src/Model/filter.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Model/filter.ts b/src/Model/filter.ts index 5bd3927af..c1b127297 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -9,6 +9,9 @@ interface PaginationOptions { limit: number; } +type FilterFn = ((item: S, key: string, object: { [key: string]: S }) => boolean) | null; +type SortFn = (a: S, B: S) => number; + declare module './Model' { interface Model { /** @@ -31,21 +34,21 @@ declare module './Model' { inputPath: PathLike, additionalInputPaths: PathLike[], options: PaginationOptions, - fn: (item: S, key: string, object: { [key: string]: S }) => boolean + fn: FilterFn ): Filter; filter( inputPath: PathLike, additionalInputPaths: PathLike[], - fn: (item: S, key: string, object: { [key: string]: S }) => boolean + fn: FilterFn ): Filter; filter( inputPath: PathLike, options: PaginationOptions, - fn: (item: S, key: string, object: { [key: string]: S }) => boolean + fn: FilterFn ): Filter; filter( inputPath: PathLike, - fn?: (item: S, key: string, object: { [key: string]: S }) => boolean + fn: FilterFn ): Filter; removeAllFilters: (subpath: Path) => void; @@ -69,15 +72,15 @@ declare module './Model' { inputPath: PathLike, additionalInputPaths: PathLike[], options: PaginationOptions, - fn: (a: S, b: S) => number + fn: SortFn ): Filter; sort( inputPath: PathLike, additionalInputPaths: PathLike[], - fn: (a: S, b: S) => number + fn: SortFn ): Filter; - sort(inputPath: PathLike, options: PaginationOptions, fn: (a: S, b: S) => number): Filter; - sort(inputPath: PathLike, fn: (a: S, b: S) => number): Filter; + sort(inputPath: PathLike, options: PaginationOptions, fn: SortFn): Filter; + sort(inputPath: PathLike, fn: SortFn): Filter; _filters: Filters; _removeAllFilters: (segments: Segments) => void; @@ -180,7 +183,7 @@ class Filters{ this.fromMap = new FromMap(); } - add(path, filterFn, sortFn, inputPaths, options) { + add(path: Path, filterFn, sortFn, inputPaths, options) { return new Filter(this, path, filterFn, sortFn, inputPaths, options); }; From 3f882f54f2bb54f3fa8b58c23efc376e86a5ca57 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 1 Apr 2024 12:52:22 -0700 Subject: [PATCH 395/479] PaginationOptions skip and limit are optional --- src/Model/filter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/filter.ts b/src/Model/filter.ts index c1b127297..f1bf7c730 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -5,8 +5,8 @@ import * as defaultFns from './defaultFns'; import type { Path, PathLike } from '../types'; interface PaginationOptions { - skip: number; - limit: number; + skip?: number; + limit?: number; } type FilterFn = ((item: S, key: string, object: { [key: string]: S }) => boolean) | null; From 226754e4424cbb1512b938f4c8623750d72500da Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 1 Apr 2024 12:59:13 -0700 Subject: [PATCH 396/479] Added comments reuse case variations from TS typedef --- test/Model/filter.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/Model/filter.js b/test/Model/filter.js index 2b032d2a5..0c5672b71 100644 --- a/test/Model/filter.js +++ b/test/Model/filter.js @@ -3,6 +3,7 @@ var RootModel = require('../../lib/Model').RootModel; describe('filter', function() { describe('getting', function() { + // this isn't clear as unspported use it('does not support array', function() { var model = (new RootModel()).at('_page'); model.set('numbers', [0, 3, 4, 1, 2, 3, 0]); @@ -13,6 +14,7 @@ describe('filter', function() { filter.get(); }).to.throw(Error); }); + it('supports filter of object', function() { var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; @@ -24,6 +26,8 @@ describe('filter', function() { }); expect(filter.get()).to.eql([0, 4, 2, 0]); }); + + // sort keyword not supported by TS typedef it('supports sort of object', function() { var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; @@ -35,6 +39,9 @@ describe('filter', function() { filter = model.sort('numbers', 'desc'); expect(filter.get()).to.eql([4, 3, 3, 2, 1, 0, 0]); }); + + // magic keyword 'even'? + // not supported by TS typdefs it('supports filter and sort of object', function() { var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; @@ -47,6 +54,10 @@ describe('filter', function() { var filter = model.filter('numbers', 'even').sort(); expect(filter.get()).to.eql([0, 0, 2, 4]); }); + + // This case needs to go away + // vargs hard to deduce type + // hard to type callback fn args properly it('supports additional input paths as var-args', function() { var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; @@ -60,6 +71,9 @@ describe('filter', function() { }); expect(filter.get()).to.eql([0, 3, 3, 0]); }); + + // supported by typescript typedefs as PathLike[] + // although filterfn not typed for vargs handling of this it('supports additional input paths as array', function() { var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; @@ -73,6 +87,7 @@ describe('filter', function() { }); expect(filter.get()).to.eql([0, 3, 3, 0]); }); + it('supports a skip option', function() { var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; @@ -88,6 +103,7 @@ describe('filter', function() { expect(filter.get()).to.eql([3, 0]); }); }); + describe('initial value set by ref', function() { it('supports filter of object', function() { var model = (new RootModel()).at('_page'); @@ -101,6 +117,7 @@ describe('filter', function() { filter.ref('_page.out'); expect(model.get('out')).to.eql([0, 4, 2, 0]); }); + it('supports sort of object', function() { var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; @@ -113,6 +130,7 @@ describe('filter', function() { filter.ref('_page.out'); expect(model.get('out')).to.eql([4, 3, 3, 2, 1, 0, 0]); }); + it('supports filter and sort of object', function() { var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; @@ -127,6 +145,7 @@ describe('filter', function() { expect(model.get('out')).to.eql([0, 0, 2, 4]); }); }); + describe('ref updates as items are modified', function() { it('supports filter of object', function() { var model = (new RootModel()).at('_page'); @@ -181,6 +200,7 @@ describe('filter', function() { } ]); }); + it('supports additional dynamic inputs as var-args', function() { var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; @@ -198,6 +218,7 @@ describe('filter', function() { model.set('mod', 2); expect(filter.get()).to.eql([3, 1, 3]); }); + it('supports additional dynamic inputs as array', function() { var model = (new RootModel()).at('_page'); var numbers = [0, 3, 4, 1, 2, 3, 0]; From e4c0ff7b8a001b345d8e5c93989b8100d7baf56e Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 1 Apr 2024 13:00:38 -0700 Subject: [PATCH 397/479] 2.0.0-beta.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 40836b94a..b37af9510 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "main": "./lib/index.js", "files": [ "lib/*" From 22bd7baea1e200dc56b6f78cab3862bdb8e810ae Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 2 Apr 2024 15:57:39 -0700 Subject: [PATCH 398/479] Add additional vararg case for add() --- src/Model/mutators.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index ec55ea263..047fa97bd 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -385,11 +385,17 @@ Model.prototype.add = function() { } } else if (arguments.length === 2) { if (typeof arguments[1] === 'function') { + // (value, callback) value = arguments[0]; cb = arguments[1]; - } else { + } else if (typeof arguments[0] === 'string' && typeof arguments[1] === 'object') { + // (path, value) subpath = arguments[0]; value = arguments[1]; + } else { + // (value, null) + value = arguments[0]; + cb = arguments[1]; } } else { subpath = arguments[0]; From acd3221de598e20cea2a6b31cd30c696a1dd0c34 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 2 Apr 2024 16:50:07 -0700 Subject: [PATCH 399/479] Allow optional filter fn while allowing named funciton usage (not yet reflected in types, but used in test) --- src/Model/filter.ts | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/Model/filter.ts b/src/Model/filter.ts index f1bf7c730..3327c9cd6 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -9,7 +9,9 @@ interface PaginationOptions { limit?: number; } -type FilterFn = ((item: S, key: string, object: { [key: string]: S }) => boolean) | null; +type FilterFn = + | ((item: S, key: string, object: { [key: string]: S }) => boolean) + | null; type SortFn = (a: S, B: S) => number; declare module './Model' { @@ -34,21 +36,21 @@ declare module './Model' { inputPath: PathLike, additionalInputPaths: PathLike[], options: PaginationOptions, - fn: FilterFn + fn?: FilterFn ): Filter; filter( inputPath: PathLike, additionalInputPaths: PathLike[], - fn: FilterFn + fn?: FilterFn ): Filter; filter( inputPath: PathLike, options: PaginationOptions, - fn: FilterFn + fn?: FilterFn ): Filter; filter( inputPath: PathLike, - fn: FilterFn + fn?: FilterFn ): Filter; removeAllFilters: (subpath: Path) => void; @@ -107,11 +109,28 @@ Model.INITS.push(function(model: Model) { }); function parseFilterArguments(model, args) { - var fn = args.pop(); - var options, inputPaths; + let fn, options, inputPaths; + // first arg always path var path = model.path(args.shift()); - - var last = args[args.length - 1]; + if (!args.length) { + return { + path: path, + inputPaths: null, + options: options, + fn: () => true, + }; + } + let last = args[args.length - 1]; + if (typeof last === 'function') { + // fn null if optional + // filter can be string + fn = args.pop(); + } + if (args.length && fn == null) { + // named function + fn = args.pop(); + } + last = args[args.length - 1]; if (!model.isPath(last) && !Array.isArray(last)) { options = args.pop(); } From 231910bae5eb7b513f2b49894287451a25167cb3 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 2 Apr 2024 16:51:03 -0700 Subject: [PATCH 400/479] 2.0.0-beta.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b37af9510..2124aec74 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "main": "./lib/index.js", "files": [ "lib/*" From ff9adb7da5e15f859eb1357c52be7aae85d472b1 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 8 Apr 2024 10:52:02 -0700 Subject: [PATCH 401/479] Remove recent add for legacy listeners --- src/Model/events.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 21e78e7e6..8ac57b030 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -25,16 +25,6 @@ export interface ModelOnEventMap { all: ModelEvent; } -export interface ModelOnEventLegacyListers { - change: (captures: string, currentValue, prevValue, passed) => void; - insert: (captures: string, howMany: number, items, passed) => void; - remove: (captures: string, howMany: number, items, passed) => void; - move: (captures: string, fromIndex: number, toIndex: number, howMany: number, passed) => void; - load: (capture: string, item) => void; - unload: (captures: string, item) => void; - all: (captures: string, eventName: string, ...args) => void; -} - /** * Racer emits captures like * 'foo.bar.1' @@ -93,17 +83,6 @@ declare module './Model' { listener: (event: ModelOnEventMap[T], captures: EventObjectCaptures) => void ): () => void; - // TODO review this calling w/o options if options useEventObjects should go away - // without any "legacy" events use case - on( - eventType: T, - pathPattern: PathLike, - listener: ModelOnEventLegacyListers[T] - ): () => void; - on( - eventType: T, - listener: ModelOnEventLegacyListers[T] - ): () => void; on( eventType: 'all', listener: (captures: EventCaptures, event: ModelOnEventMap[keyof ModelOnEventMap]) => void From aae0fb455541f5d6accc7b07731a92037f79ce98 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 8 Apr 2024 16:58:11 -0700 Subject: [PATCH 402/479] Type fixes for createModel(), InsertEvent, RemoveEvent, Subscribable --- src/Model/events.ts | 7 ++++--- src/Model/subscriptions.ts | 2 +- src/index.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 8ac57b030..d6ef3a6ee 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -653,7 +653,7 @@ export class InsertEvent { declare type: 'insert'; declare _immediateType: 'insertImmediate'; index: number; - values: any; + values: any[]; passed: any; constructor(index, values, passed) { @@ -678,8 +678,9 @@ export class RemoveEvent { declare _immediateType: 'removeImmediate'; index: number; passed: any; - removed: any; - values: any; + removed: any[]; + /** @deprecated Use `removed` instead */ + values: any[]; constructor(index, values, passed) { this.index = index; diff --git a/src/Model/subscriptions.ts b/src/Model/subscriptions.ts index 9c610c8c3..9c230d6fb 100644 --- a/src/Model/subscriptions.ts +++ b/src/Model/subscriptions.ts @@ -9,7 +9,7 @@ const promisify = util.promisify; /** * A path string, a `Model`, or a `Query`. */ -export type Subscribable = string | Model | Query; +export type Subscribable = string | Model | Query; declare module './Model' { interface Model { diff --git a/src/index.ts b/src/index.ts index 39b6d67e1..16b43459e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,7 +27,7 @@ export { export const racer = new Racer(); -export function createModel(data) { +export function createModel(data?) { var model = new RootModel(); if (data) { model.createConnection(data); From 336fab3817f55e625ec2b00688c290dda4b8df9c Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 8 Apr 2024 16:59:25 -0700 Subject: [PATCH 403/479] Type fixes for setPromised, setNullPromised, setDiffPromised - they all resolve to nothing --- src/Model/mutators.ts | 12 ++++++------ src/Model/setDiff.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 047fa97bd..568ca7bd4 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -16,14 +16,14 @@ declare module './Model' { _mutate(segments, fn, cb): void; set(value: T, cb?: ErrorCallback): T | undefined; set(subpath: Path, value: any, cb?: ErrorCallback): S | undefined; - setPromised(value: T): Promise; - setPromised(subpath: Path, value: any): Promise; + setPromised(value: T): Promise; + setPromised(subpath: Path, value: any): Promise; _set(segments: Segments, value: any, cb?: ErrorCallback): S | undefined; setNull(value: T, cb?: ErrorCallback): T | undefined; setNull(subpath: Path, value: S, cb?: ErrorCallback): S | undefined; - setNullPromised(value: T): Promise; - setNullPromised(subpath: Path, value: S): Promise; + setNullPromised(value: T): Promise; + setNullPromised(subpath: Path, value: any): Promise; _setNull(segments: Segments, value: S, cb?: ErrorCallback): S | undefined; setEach(value: any, cb?: ErrorCallback): void; @@ -61,7 +61,7 @@ declare module './Model' { */ del(subpath: Path, cb?: Callback): S | undefined; del(cb?: Callback): T | undefined; - delPromised(subpath: Path): Promise; + delPromised(subpath: Path): Promise; _del(segments: Segments, cb?: ErrorCallback): S; _delNoDereference(segments: Segments, cb?: ErrorCallback): void; @@ -114,7 +114,7 @@ declare module './Model' { _pop(segments: Segments, value: any, cb?: ErrorCallback): void; shift(subpath?: Path, cb?: ErrorCallback): S; - shiftPromised(subpath?: Path): Promise; + shiftPromised(subpath?: Path): Promise; _shift(segments: Segments, cb?: ErrorCallback): S; /** diff --git a/src/Model/setDiff.ts b/src/Model/setDiff.ts index ebd15b385..5819ea0ca 100644 --- a/src/Model/setDiff.ts +++ b/src/Model/setDiff.ts @@ -25,7 +25,7 @@ declare module './Model' { */ setDiff(subpath: Path, value: S, cb?: Callback): ReadonlyDeep | undefined; setDiff(value: T | undefined): ReadonlyDeep | undefined; - setDiffPromised(subpath: Path, value: S): Promise; + setDiffPromised(subpath: Path, value: any): Promise; _setDiff(segments: Segments, value: any, cb?: (err: Error) => void): void; /** From 7f50106a30fc02d32fa442ad65cf68cb7e896830 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 9 Apr 2024 12:56:56 -0700 Subject: [PATCH 404/479] Handle case of destructuring item results passed as null needed to default to empty array (using dfault initializer only works when undefined) --- src/Model/Query.ts | 47 ++++++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/src/Model/Query.ts b/src/Model/Query.ts index 8aec41e2c..b0407a376 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -86,40 +86,37 @@ Model.prototype.sanitizeQuery = function(expression) { // Called during initialization of the bundle on page load. Model.prototype._initQueries = function(items: any[][]) { - for (var i = 0; i < items.length; i++) { - var item = items[i]; - const [countsList, collectionName, expression, results=[], options, extra] = item; - // var countsList = item[0]; - // var collectionName = item[1]; - // var expression = item[2]; - // var results = item[3] || []; - // var options = item[4]; - // var extra = item[5]; - const [counts] = countsList; - // var counts = countsList[0]; - var [subscribed = 0, fetched = 0, contextId] = counts; - // var subscribed = counts[0] || 0; - // var fetched = counts[1] || 0; - // var contextId = counts[2]; - - var model = (contextId) ? this.context(contextId) : this; - var query = model._getOrCreateQuery(collectionName, expression, options, Query); + for (let i = 0; i < items.length; i++) { + const [countsList, collectionName, expression, _results, options, extra] = items[i]; + // const countsList = item[0]; + // const collectionName = item[1]; + // const expression = item[2]; + const results = _results || []; + // const options = item[4]; + // const extra = item[5]; + const [_subscribed, _fetched, contextId] = countsList[0]; + let subscribed = _subscribed || 0; + let fetched = _fetched || 0; + // const contextId = counts[2]; + + const model = (contextId) ? this.context(contextId) : this; + const query = model._getOrCreateQuery(collectionName, expression, options, Query); query._setExtra(extra); - var ids = []; + const ids = []; for (var resultIndex = 0; resultIndex < results.length; resultIndex++) { - var result = results[resultIndex]; + const result = results[resultIndex]; if (typeof result === 'string') { ids.push(result); continue; } - var data = result[0]; - var v = result[1]; - var id = result[2] || data.id; - var type = result[3]; + const data = result[0]; + const v = result[1]; + const id = result[2] || data.id; + const type = result[3]; ids.push(id); - var snapshot = { data: data, v: v, type: type }; + const snapshot = { data: data, v: v, type: type }; this.getOrCreateDoc(collectionName, id, snapshot); } query._addMapIds(ids); From f61f30052b571b0ac3fc5803b3711462deb252ba Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 9 Apr 2024 16:01:59 -0700 Subject: [PATCH 405/479] 2.0.0-beta.16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2124aec74..339130858 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.15", + "version": "2.0.0-beta.16", "main": "./lib/index.js", "files": [ "lib/*" From cd9736f911de6386c3a843616433dfdef9047172 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 19 Apr 2024 14:37:09 -0700 Subject: [PATCH 406/479] Remove deprecated getModel method on request from middleware --- src/Backend.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Backend.ts b/src/Backend.ts index 0543faa69..e507b937f 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -38,19 +38,12 @@ export class RacerBackend extends Backend { // Create a new model for this request req.model = backend.createModel({ fetchOnly: true }, req); - // DEPRECATED: - req.getModel = function () { - console.warn('Warning: req.getModel() is deprecated. Please use req.model instead.'); - return req.model; - }; // Close the model when this request ends function closeModel() { res.removeListener('finish', closeModel); res.removeListener('close', closeModel); if (req.model) req.model.close(); - // DEPRECATED: - req.getModel = getModelUndefined; } res.on('finish', closeModel); res.on('close', closeModel); From a35f067a27dd6302af0b0307d2070b518fefb2f6 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Tue, 23 Apr 2024 16:47:55 -0700 Subject: [PATCH 407/479] Rmove getArray --- src/Model/collections.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Model/collections.ts b/src/Model/collections.ts index f14f3d644..c641db26a 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -48,9 +48,6 @@ declare module './Model' { */ get(): ReadonlyDeep | undefined; get(subpath?: Path): ReadonlyDeep | undefined; - - getArray(): ReadonlyArray>; - getArray(subPath?: Path): ReadonlyArray>; getCollection(collectionName: string): Collection; @@ -105,10 +102,6 @@ Model.prototype.get = function(subpath?: Path) { return this._get(segments) as ReadonlyDeep; }; -Model.prototype.getArray = function(subpath?: Path) { - return this.get(subpath) || []; -} - Model.prototype._get = function(segments) { return util.lookup(segments, this.root.data); }; From 359f66a4d64e335ad37bb2d4fd17392dc213b694 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 24 Apr 2024 12:06:43 -0700 Subject: [PATCH 408/479] 2.0.0-beta.17 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 339130858..c87d966e9 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.16", + "version": "2.0.0-beta.17", "main": "./lib/index.js", "files": [ "lib/*" From 6aab9963775320826ffeffb4fd602e74f6c0a12b Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 09:54:25 -0700 Subject: [PATCH 409/479] Make close callback optional --- src/Model/connection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/connection.ts b/src/Model/connection.ts index 376fedfa6..922006968 100644 --- a/src/Model/connection.ts +++ b/src/Model/connection.ts @@ -15,7 +15,7 @@ declare module './Model' { interface Model { /** Returns a child model where ShareDB operations are always composed. */ allowCompose(): ChildModel; - close(cb: (err?: Error) => void): void; + close(cb?: (err?: Error) => void): void; closePromised: Promise; disconnect(): void; From ecf0e5f3ebafa7ae1101b3c53984ee0feb3da733 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 10:29:38 -0700 Subject: [PATCH 410/479] Add generic param to filter.get() --- src/Model/filter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/filter.ts b/src/Model/filter.ts index 3327c9cd6..f6a0cc3ff 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -325,7 +325,7 @@ export class Filter { this.filterFn.call(this.model, item, key, items); }; - ids() { + ids(): string[] { var items = this.model._get(this.segments); var ids = []; if (!items) return ids; @@ -351,7 +351,7 @@ export class Filter { return this._slice(ids); }; - get() { + get(): S[] { var items = this.model._get(this.segments); var results = []; if (Array.isArray(items)) { From de7611ca087a2716b1a20806d159aeaf60ac9f7d Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 11:11:19 -0700 Subject: [PATCH 411/479] Fix closePromised type --- src/Model/connection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/connection.ts b/src/Model/connection.ts index 922006968..7b342a1b4 100644 --- a/src/Model/connection.ts +++ b/src/Model/connection.ts @@ -16,7 +16,7 @@ declare module './Model' { /** Returns a child model where ShareDB operations are always composed. */ allowCompose(): ChildModel; close(cb?: (err?: Error) => void): void; - closePromised: Promise; + closePromised: () => Promise; disconnect(): void; /** From ff00e1f4cbac68f02758288c5f69bd78a5f7a885 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 11:47:31 -0700 Subject: [PATCH 412/479] Remove unused import; loosen QueryOptions type (db not required) --- src/Model/Query.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Model/Query.ts b/src/Model/Query.ts index b0407a376..7f29ec1c6 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -3,7 +3,6 @@ import { type Segments } from './types'; import { ChildModel, ErrorCallback, Model } from './Model'; import { CollectionMap } from './CollectionMap'; import { ModelData } from '.'; -import type { Doc } from './Doc'; import type { Doc as ShareDBDoc } from 'sharedb'; import type { RemoteDoc } from './RemoteDoc'; @@ -11,7 +10,7 @@ var defaultType = require('sharedb/lib/client').types.defaultType; var util = require('../util'); var promisify = util.promisify; -export type QueryOptions = string | { db: any, [key: string]: unknown }; +export type QueryOptions = string | { [key: string]: unknown }; interface QueryCtor { new (model: Model, collectionName: string, expression: any, options: QueryOptions): Query; From 5b31ca2a3770fda977c9e9b83ddc07af58b0cd03 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 11:56:02 -0700 Subject: [PATCH 413/479] 2.0.0-beta.18 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c87d966e9..78755504c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.17", + "version": "2.0.0-beta.18", "main": "./lib/index.js", "files": [ "lib/*" From 39adda2293f87a0b67996354fabd3f108b2b13d8 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 14:06:24 -0700 Subject: [PATCH 414/479] Remove unused import --- src/Model/collections.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Model/collections.ts b/src/Model/collections.ts index c641db26a..1ec61f372 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -2,7 +2,6 @@ import { type Segments } from './types'; import { Doc } from './Doc'; import { Model, RootModel } from './Model'; import { JSONObject } from 'sharedb/lib/sharedb'; -import { VerifyJsonWebKeyInput } from 'crypto'; import { Path, ReadonlyDeep, ShallowCopiedValue } from '../types'; var LocalDoc = require('./LocalDoc'); var util = require('../util'); From 92350c38b366373aa58410e6b5aea01a6c3001df Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 14:11:14 -0700 Subject: [PATCH 415/479] Type to and from --- src/Model/events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index d6ef3a6ee..f92e9896b 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -708,10 +708,10 @@ RemoveEvent.prototype._immediateType = 'removeImmediate'; export class MoveEvent { declare type: 'move'; declare _immediateType: 'moveImmediate'; - from: any; + from: number; howMany: number; passed: any; - to: any; + to: number; constructor(from, to, howMany, passed) { this.from = from; From 9fcfb6a6f14db884dfd0efc28e578db360719564 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 14:14:59 -0700 Subject: [PATCH 416/479] Move on "all" (w/o path) and "error" handlers to RootModel --- src/Model/events.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index f92e9896b..7746ee041 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -39,6 +39,17 @@ type EventCaptures = string | string[]; type EventObjectCaptures = string[]; declare module './Model' { + interface RootModel { + on( + eventType: 'all', + listener: (captures: EventCaptures, event: ModelOnEventMap[keyof ModelOnEventMap]) => void + ): () => void; + on( + eventType: 'error', + listener: (error: Error) => void + ): () => void; + } + interface Model { addListener(event: string, listener: any, arg2?: any, arg3?: any): any; eventContext(id: string): ChildModel; @@ -83,16 +94,6 @@ declare module './Model' { listener: (event: ModelOnEventMap[T], captures: EventObjectCaptures) => void ): () => void; - on( - eventType: 'all', - listener: (captures: EventCaptures, event: ModelOnEventMap[keyof ModelOnEventMap]) => void - ): () => void; - on( - eventType: 'error', - listener: (error: Error) => void - ): () => void; - - /** * Listen to Racer events matching a certain path or path pattern, removing * the listener after it gets triggered once. From a4ae98c06f07f1ebd6ee0dacdab5d57e18a18863 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 14:19:18 -0700 Subject: [PATCH 417/479] Remove EventCaptures type; update all path type --- src/Model/events.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 7746ee041..4b8b06a78 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -25,13 +25,6 @@ export interface ModelOnEventMap { all: ModelEvent; } -/** - * Racer emits captures like - * 'foo.bar.1' - * Derby emits captures like - * ['foo', 'bar', '1'] - * */ -type EventCaptures = string | string[]; /** * With `useEventObjects: true` captures are emmitted as * ['foo.bar.1'] @@ -42,7 +35,7 @@ declare module './Model' { interface RootModel { on( eventType: 'all', - listener: (captures: EventCaptures, event: ModelOnEventMap[keyof ModelOnEventMap]) => void + listener: (pathSegments: string[], event: ModelOnEventMap[keyof ModelOnEventMap]) => void ): () => void; on( eventType: 'error', From 255213e4907187112672aa31f62937815e6141cf Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 14:35:56 -0700 Subject: [PATCH 418/479] Duplicate Model on signatures in RootModel to handle overrides --- src/Model/events.ts | 14 +++++++++++++- src/Model/filter.ts | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 4b8b06a78..4206f7e32 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -33,6 +33,18 @@ type EventObjectCaptures = string[]; declare module './Model' { interface RootModel { + // duplicated w on signatures below due to how TS handles overrides + on( + eventType: T, + pathPattern: PathLike, + options: { useEventObjects: true }, + listener: (event: ModelOnEventMap[T], captures: EventObjectCaptures) => void + ): () => void; + on( + eventType: T, + options: { useEventObjects: true }, + listener: (event: ModelOnEventMap[T], captures: EventObjectCaptures) => void + ): () => void; on( eventType: 'all', listener: (pathSegments: string[], event: ModelOnEventMap[keyof ModelOnEventMap]) => void @@ -145,7 +157,7 @@ declare module './Model' { } } -Model.INITS.push(function(model: Model) { +Model.INITS.push(function(model) { var root = model.root; EventEmitter.call(root); diff --git a/src/Model/filter.ts b/src/Model/filter.ts index f6a0cc3ff..d3d882e87 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -1,5 +1,5 @@ var util = require('../util'); -import { Model } from './Model'; +import { Model, RootModel } from './Model'; import { type Segments } from './types'; import * as defaultFns from './defaultFns'; import type { Path, PathLike } from '../types'; @@ -89,7 +89,7 @@ declare module './Model' { } } -Model.INITS.push(function(model: Model) { +Model.INITS.push(function(model) { model.root._filters = new Filters(model); model.on('all', filterListener); function filterListener(segments, event) { From 8ba7f685e67fc9b2d94c2ac6f785d2944c704346 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 14:38:40 -0700 Subject: [PATCH 419/479] 2.0.0-beta.19 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78755504c..89538f628 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.18", + "version": "2.0.0-beta.19", "main": "./lib/index.js", "files": [ "lib/*" From 8139098cefae6aa7d39122ba31141132fee02109 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 15:00:42 -0700 Subject: [PATCH 420/479] FilterFn required in all cases; additionally add string as type for named functions --- src/Model/filter.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Model/filter.ts b/src/Model/filter.ts index d3d882e87..810029ffa 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -11,6 +11,7 @@ interface PaginationOptions { type FilterFn = | ((item: S, key: string, object: { [key: string]: S }) => boolean) + | string | null; type SortFn = (a: S, B: S) => number; @@ -36,21 +37,21 @@ declare module './Model' { inputPath: PathLike, additionalInputPaths: PathLike[], options: PaginationOptions, - fn?: FilterFn + fn: FilterFn ): Filter; filter( inputPath: PathLike, additionalInputPaths: PathLike[], - fn?: FilterFn + fn: FilterFn ): Filter; filter( inputPath: PathLike, options: PaginationOptions, - fn?: FilterFn + fn: FilterFn ): Filter; filter( inputPath: PathLike, - fn?: FilterFn + fn: FilterFn ): Filter; removeAllFilters: (subpath: Path) => void; From 2a88194f3edcf01560eff0e0163401c1295094ab Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 15:05:07 -0700 Subject: [PATCH 421/479] Return to previous implementation now that filter fn always required to have a value --- src/Model/filter.ts | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/Model/filter.ts b/src/Model/filter.ts index 810029ffa..39eea58f2 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -110,28 +110,10 @@ Model.INITS.push(function(model) { }); function parseFilterArguments(model, args) { - let fn, options, inputPaths; - // first arg always path + var fn = args.pop(); + var options, inputPaths; var path = model.path(args.shift()); - if (!args.length) { - return { - path: path, - inputPaths: null, - options: options, - fn: () => true, - }; - } - let last = args[args.length - 1]; - if (typeof last === 'function') { - // fn null if optional - // filter can be string - fn = args.pop(); - } - if (args.length && fn == null) { - // named function - fn = args.pop(); - } - last = args[args.length - 1]; + var last = args[args.length - 1]; if (!model.isPath(last) && !Array.isArray(last)) { options = args.pop(); } From fbc1d05a63ab1a4056a1c11d732f2f09be661659 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 15:37:42 -0700 Subject: [PATCH 422/479] Update doc links to new addresses --- src/Model/contexts.ts | 6 +++--- src/Model/events.ts | 4 ++-- src/Model/filter.ts | 4 ++-- src/Model/fn.ts | 4 ++-- src/Model/ref.ts | 6 +++--- src/Model/subscriptions.ts | 8 ++++---- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Model/contexts.ts b/src/Model/contexts.ts index e1749d9ce..ffd9763f1 100644 --- a/src/Model/contexts.ts +++ b/src/Model/contexts.ts @@ -21,7 +21,7 @@ declare module './Model' { * * @param contextId - context id * - * @see https://derbyjs.com/docs/derby-0.10/models/data-loading-contexts + * @see https://derbyjs.github.io/derby/models/contexts */ context(contextId: string): ChildModel; getOrCreateContext(id: string): Context; @@ -32,14 +32,14 @@ declare module './Model' { * * @param contextId - optional context to unload; defaults to this model's context * - * @see https://derbyjs.com/docs/derby-0.10/models/data-loading-contexts + * @see https://derbyjs.github.io/derby/models/contexts */ unload(contextId?: string): void; /** * Unloads data for all model contexts. * - * @see https://derbyjs.com/docs/derby-0.10/models/data-loading-contexts + * @see https://derbyjs.github.io/derby/models/contexts */ unloadAll(): void; diff --git a/src/Model/events.ts b/src/Model/events.ts index 4206f7e32..5212397c5 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -85,7 +85,7 @@ declare module './Model' { * @param options * @param listener * - * @see https://derbyjs.com/docs/derby-0.10/models/events + * @see https://derbyjs.github.io/derby/models/events */ on( eventType: T, @@ -108,7 +108,7 @@ declare module './Model' { * @param options * @param listener * - * @see https://derbyjs.com/docs/derby-0.10/components/events + * @see https://derbyjs.github.io/derby/components/events */ once( eventType: T, diff --git a/src/Model/filter.ts b/src/Model/filter.ts index 39eea58f2..a2dc886d0 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -31,7 +31,7 @@ declare module './Model' { * @param fn - A function or the name of a function defined via model.fn(). The function * should have the arguments function(item, key, object, additionalInputs...) * - * @see https://derbyjs.com/docs/derby-0.10/models/filters-and-sorts + * @see https://derbyjs.github.io/derby/models/filters-sorts */ filter( inputPath: PathLike, @@ -69,7 +69,7 @@ declare module './Model' { * limit - The maximum number of results. A limit of zero is equivalent to no limit. * @param fn - A function or the name of a function defined via model.fn(). * - * @see https://derbyjs.com/docs/derby-0.10/models/filters-and-sorts + * @see https://derbyjs.github.io/derby/models/filters-and-sorts */ sort( inputPath: PathLike, diff --git a/src/Model/fn.ts b/src/Model/fn.ts index ce653f8f9..7cf97efaa 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -73,7 +73,7 @@ declare module './Model' { * @param options * @param fn * - * @see https://derbyjs.com/docs/derby-0.10/models/reactive-functions + * @see https://derbyjs.github.io/derby/models/reactive-functions */ evaluate( inputPaths: PathLike[], @@ -120,7 +120,7 @@ declare module './Model' { * a `{ get: Function; set: Function }` object defining a two-way reactive function; * or the name of a function defined via model.fn() * - * @see https://derbyjs.com/docs/derby-0.10/models/reactive-functions + * @see https://derbyjs.github.io/derby/models/reactive-functions */ start( outputPath: PathLike, diff --git a/src/Model/ref.ts b/src/Model/ref.ts index 397765641..4650fca9a 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -30,7 +30,7 @@ declare module './Model' { * collection will be deleted if the corresponding item is removed from * the refList's output path * - * @see https://derbyjs.com/docs/derby-0.10/models/references + * @see https://derbyjs.github.io/derby/models/refs */ refList(outputPath: PathLike, collectionPath: PathLike, idsPath: PathLike, options?: { deleteRemoved?: boolean }): ChildModel; @@ -47,7 +47,7 @@ declare module './Model' { * @param to - Location that the reference points to * @return a model scoped to `path` * - * @see https://derbyjs.com/docs/derby-0.10/models/references + * @see https://derbyjs.github.io/derby/models/refs */ ref(to: PathLike): ChildModel; ref(path: PathLike, to: PathLike, options?: RefOptions): ChildModel; @@ -58,7 +58,7 @@ declare module './Model' { * * @param path - Location of the reference to remove * - * @see https://derbyjs.com/docs/derby-0.10/models/references + * @see https://derbyjs.github.io/derby/models/refs */ removeRef(path: PathLike): void; _removeRef(segments: Segments): void; diff --git a/src/Model/subscriptions.ts b/src/Model/subscriptions.ts index 9c230d6fb..2cb7bd785 100644 --- a/src/Model/subscriptions.ts +++ b/src/Model/subscriptions.ts @@ -19,7 +19,7 @@ declare module './Model' { * @param items * @param cb * - * @see https://derbyjs.com/docs/derby-0.10/models/backends#loading-data-into-a-model + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model */ fetch(items: Subscribable[], cb?: ErrorCallback): Model; fetch(item: Subscribable, cb?: ErrorCallback): Model; @@ -43,7 +43,7 @@ declare module './Model' { * @param items * @param cb * - * @see https://derbyjs.com/docs/derby-0.10/models/backends#loading-data-into-a-model + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model */ subscribe(items: Subscribable[], cb?: ErrorCallback): Model; subscribe(item: Subscribable, cb?: ErrorCallback): Model; @@ -63,7 +63,7 @@ declare module './Model' { * @param items * @param cb * - * @see https://derbyjs.com/docs/derby-0.10/models/backends#loading-data-into-a-model + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model */ unfetch(items: Subscribable[], cb?: ErrorCallback): Model; unfetch(item: Subscribable, cb?: ErrorCallback): Model; @@ -85,7 +85,7 @@ declare module './Model' { * @param items * @param cb * - * @see https://derbyjs.com/docs/derby-0.10/models/backends#loading-data-into-a-model + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model */ unsubscribe(items: Subscribable[], cb?: ErrorCallback): Model; unsubscribe(item: Subscribable, cb?: ErrorCallback): Model; From 147e1c9730dcac718a79c37594a49ce7e5eff8c1 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 16:07:41 -0700 Subject: [PATCH 423/479] Update doc url --- src/Model/filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/filter.ts b/src/Model/filter.ts index a2dc886d0..677cf5627 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -69,7 +69,7 @@ declare module './Model' { * limit - The maximum number of results. A limit of zero is equivalent to no limit. * @param fn - A function or the name of a function defined via model.fn(). * - * @see https://derbyjs.github.io/derby/models/filters-and-sorts + * @see https://derbyjs.github.io/derby/models/filters-sorts */ sort( inputPath: PathLike, From 2d4a30e884db54312711ca96f861342ec16b30aa Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 25 Apr 2024 16:09:58 -0700 Subject: [PATCH 424/479] Make fetch callback optional as is everywhere else --- src/Model/Query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Query.ts b/src/Model/Query.ts index 7f29ec1c6..9f2afeb1e 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -239,7 +239,7 @@ export class Query { this._maybeUnloadDocs(ids); }; - fetch(cb: ErrorCallback) { + fetch(cb?: ErrorCallback) { cb = this.model.wrapCallback(cb); this.context.fetchQuery(this); From 54628e51ef18a52b95e1946819643907771e4020 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 26 Apr 2024 13:11:20 -0700 Subject: [PATCH 425/479] Ensure promised methods typed as Promise --- src/Model/mutators.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 568ca7bd4..5b46bb22a 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -68,8 +68,8 @@ declare module './Model' { increment(value?: number): number; increment(subpath: Path, value?: number, cb?: ErrorCallback): number; - incrementPromised(value?: number): Promise; - incrementPromised(subpath: Path, value?: number): Promise; + incrementPromised(value?: number): Promise; + incrementPromised(subpath: Path, value?: number): Promise; _increment(segments: Segments, value: number, cb?: ErrorCallback): number; /** @@ -81,8 +81,8 @@ declare module './Model' { */ push(value: any): number; push(subpath: Path, value: any, cb?: ErrorCallback): number; - pushPromised(value: any): Promise; - pushPromised(subpath: Path, value: any): Promise; + pushPromised(value: any): Promise; + pushPromised(subpath: Path, value: any): Promise; _push(segments: Segments, value: any, cb?: ErrorCallback): number; unshift(value: any): void; @@ -536,7 +536,7 @@ Model.prototype.push = function() { var segments = this._splitPath(subpath); return this._push(segments, value, cb); }; -Model.prototype.pushPromised = promisify(Model.prototype.push); +Model.prototype.pushPromised = promisify(Model.prototype.push); Model.prototype._push = function(segments, value, cb) { var forArrayMutator = true; From d60661aade0eeddd9ed6e8c437a4e081825d7135 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 26 Apr 2024 13:24:23 -0700 Subject: [PATCH 426/479] Remove returns from create and createNull --- src/Model/mutators.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 5b46bb22a..0839cbb65 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -310,8 +310,8 @@ Model.prototype.create = function() { value = arguments[1]; cb = arguments[2]; } - var segments = this._splitPath(subpath); - return this._create(segments, value, cb); + const segments = this._splitPath(subpath); + this._create(segments, value, cb); }; Model.prototype.createPromised = promisify(Model.prototype.create); @@ -333,7 +333,7 @@ Model.prototype._create = function(segments, value, cb) { var event = new ChangeEvent(value, previous, model._pass); model._emitMutation(segments, event); } - return this._mutate(segments, create, cb); + this._mutate(segments, create, cb); }; Model.prototype.createNull = function() { @@ -361,7 +361,7 @@ Model.prototype.createNull = function() { cb = arguments[2]; } var segments = this._splitPath(subpath); - return this._createNull(segments, value, cb); + this._createNull(segments, value, cb); }; Model.prototype.createNullPromised = promisify(Model.prototype.createNull); @@ -369,7 +369,7 @@ Model.prototype._createNull = function(segments, value, cb) { segments = this._dereference(segments); var doc = this.getDoc(segments[0], segments[1]); if (doc && doc.get() != null) return; - return this._create(segments, value, cb); + this._create(segments, value, cb); }; Model.prototype.add = function() { From e63049f4b189d349c2773b5510c89910f6f10893 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 26 Apr 2024 13:42:54 -0700 Subject: [PATCH 427/479] Consolidate types; remove Model/types --- src/Model/EventListenerTree.ts | 2 +- src/Model/EventMapTree.ts | 2 +- src/Model/Query.ts | 2 +- src/Model/collections.ts | 3 +-- src/Model/events.ts | 3 +-- src/Model/filter.ts | 5 ++--- src/Model/fn.ts | 3 +-- src/Model/mutators.ts | 3 +-- src/Model/ref.ts | 3 +-- src/Model/setDiff.ts | 5 +++-- src/Model/types.ts | 19 ------------------- src/types.ts | 1 + src/util.ts | 2 +- 13 files changed, 15 insertions(+), 38 deletions(-) delete mode 100644 src/Model/types.ts diff --git a/src/Model/EventListenerTree.ts b/src/Model/EventListenerTree.ts index bbd4d7100..1fe79e6bd 100644 --- a/src/Model/EventListenerTree.ts +++ b/src/Model/EventListenerTree.ts @@ -1,4 +1,4 @@ -import { type Segments } from './types'; +import { type Segments } from '../types'; import { FastMap } from './FastMap'; /** diff --git a/src/Model/EventMapTree.ts b/src/Model/EventMapTree.ts index 33bf409a5..f8172e693 100644 --- a/src/Model/EventMapTree.ts +++ b/src/Model/EventMapTree.ts @@ -1,4 +1,4 @@ -import { type Segments } from './types'; +import { type Segments } from '../types'; import { FastMap } from './FastMap'; /** diff --git a/src/Model/Query.ts b/src/Model/Query.ts index 9f2afeb1e..5e355190d 100644 --- a/src/Model/Query.ts +++ b/src/Model/Query.ts @@ -1,5 +1,5 @@ import { type Context } from './contexts'; -import { type Segments } from './types'; +import { type Segments } from '../types'; import { ChildModel, ErrorCallback, Model } from './Model'; import { CollectionMap } from './CollectionMap'; import { ModelData } from '.'; diff --git a/src/Model/collections.ts b/src/Model/collections.ts index 1ec61f372..1f9531b96 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -1,8 +1,7 @@ -import { type Segments } from './types'; import { Doc } from './Doc'; import { Model, RootModel } from './Model'; import { JSONObject } from 'sharedb/lib/sharedb'; -import { Path, ReadonlyDeep, ShallowCopiedValue } from '../types'; +import type { Path, ReadonlyDeep, ShallowCopiedValue, Segments } from '../types'; var LocalDoc = require('./LocalDoc'); var util = require('../util'); diff --git a/src/Model/events.ts b/src/Model/events.ts index 5212397c5..32c4d9048 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -2,10 +2,9 @@ import { EventEmitter } from 'events'; import { EventListenerTree } from './EventListenerTree'; -import { type Segments } from './types'; import { Model } from './Model'; import { mergeInto } from '../util'; -import type { Path, PathLike } from '../types'; +import type { Path, PathLike, Segments } from '../types'; export type ModelEvent = | ChangeEvent diff --git a/src/Model/filter.ts b/src/Model/filter.ts index 677cf5627..928dc1346 100644 --- a/src/Model/filter.ts +++ b/src/Model/filter.ts @@ -1,8 +1,7 @@ var util = require('../util'); -import { Model, RootModel } from './Model'; -import { type Segments } from './types'; +import { Model } from './Model'; import * as defaultFns from './defaultFns'; -import type { Path, PathLike } from '../types'; +import type { Path, PathLike, Segments } from '../types'; interface PaginationOptions { skip?: number; diff --git a/src/Model/fn.ts b/src/Model/fn.ts index 7cf97efaa..459e09157 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -1,9 +1,8 @@ -import { type Segments } from './types'; import { Model } from './Model'; import { EventListenerTree } from './EventListenerTree'; import { EventMapTree } from './EventMapTree'; import * as defaultFns from './defaultFns'; -import type { Path, PathLike, ReadonlyDeep } from '../types'; +import type { Path, PathLike, ReadonlyDeep, Segments } from '../types'; var util = require('../util'); class NamedFns { } diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 0839cbb65..2f477c194 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -1,7 +1,6 @@ -import type { Callback, Path, ArrayItemType } from '../types'; +import type { Callback, Path, ArrayItemType, Segments } from '../types'; import * as util from '../util'; import { Model } from './Model'; -import { type Segments } from './types'; var mutationEvents = require('./events').mutationEvents; diff --git a/src/Model/ref.ts b/src/Model/ref.ts index 4650fca9a..4149bf8d2 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -1,10 +1,9 @@ import { EventListenerTree } from './EventListenerTree'; import { EventMapTree } from './EventMapTree'; import { Model } from './Model'; -import { type Segments } from './types'; import { type Filter } from './filter'; import { type Query } from './Query'; -import type { Path, PathLike } from '../types'; +import type { Path, PathLike, Segments } from '../types'; type Refable = string | number | Model | Query | Filter; diff --git a/src/Model/setDiff.ts b/src/Model/setDiff.ts index 5819ea0ca..83fd33f6b 100644 --- a/src/Model/setDiff.ts +++ b/src/Model/setDiff.ts @@ -1,7 +1,8 @@ -var util = require('../util'); +import * as util from '../util'; import { Callback, Path, ReadonlyDeep } from '../types'; import { Model } from './Model'; -import { type Segments } from './types'; +import { type Segments } from '../types'; + var arrayDiff = require('arraydiff'); var mutationEvents = require('./events').mutationEvents; var ChangeEvent = mutationEvents.ChangeEvent; diff --git a/src/Model/types.ts b/src/Model/types.ts deleted file mode 100644 index a5a591662..000000000 --- a/src/Model/types.ts +++ /dev/null @@ -1,19 +0,0 @@ - - // | { ref: any }; -/** - * - export type Path = string | number; - export type PathSegment = string | number; - export type PathLike = Path | Model; - */ - -import { Path } from "../types"; - - -// could be -// ['foo', 3, 'bar'] -// always converted to string internally -export type Segment = Path; - -// PathLike -export type Segments = Array; diff --git a/src/types.ts b/src/types.ts index 1fb841fe7..dc3adcee5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ export type UUID = string; export type Path = string | number; export type PathSegment = string | number; export type PathLike = Path | Model; +export type Segments = Array; export type Primitive = boolean | number | string | null | undefined; diff --git a/src/util.ts b/src/util.ts index 9b5a3b879..77e5d38cf 100644 --- a/src/util.ts +++ b/src/util.ts @@ -24,7 +24,7 @@ class AsyncGroup { add() { this.count++; const self = this; - return function(err) { + return function(err?: Error) { self.count--; if (self.isDone) return; if (err) { From 3e8fc8da01064b4f16146bea1e9080126d0fe2ad Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 29 Apr 2024 09:23:45 -0700 Subject: [PATCH 428/479] 2.0.0-beta.20 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 89538f628..f5e99621c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.19", + "version": "2.0.0-beta.20", "main": "./lib/index.js", "files": [ "lib/*" From 290868457ca8912db64748246b480d71bf52aadc Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 29 Apr 2024 14:03:41 -0700 Subject: [PATCH 429/479] Add typings for *Immediate event listeners, mostly used by Derby --- src/Model/events.ts | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/Model/events.ts b/src/Model/events.ts index 32c4d9048..ea488b77e 100644 --- a/src/Model/events.ts +++ b/src/Model/events.ts @@ -14,15 +14,12 @@ export type ModelEvent = | LoadEvent | UnloadEvent; -export interface ModelOnEventMap { - change: ChangeEvent; - insert: InsertEvent; - remove: RemoveEvent; - move: MoveEvent; - load: LoadEvent; - unload: UnloadEvent; - all: ModelEvent; -} +export type ModelOnEventMap = { + [eventName in ModelEvent['type']]: Extract; +}; +export type ModelOnImmediateEventMap = { + [eventName in ModelEvent['_immediateType']]: Extract; +}; /** * With `useEventObjects: true` captures are emmitted as @@ -39,11 +36,26 @@ declare module './Model' { options: { useEventObjects: true }, listener: (event: ModelOnEventMap[T], captures: EventObjectCaptures) => void ): () => void; + on( + eventType: 'all', + pathPattern: PathLike, + options: { useEventObjects: true }, + listener: (event: ModelEvent, captures: EventObjectCaptures) => void + ): () => void; on( eventType: T, options: { useEventObjects: true }, listener: (event: ModelOnEventMap[T], captures: EventObjectCaptures) => void ): () => void; + on( + eventType: 'all', + options: { useEventObjects: true }, + listener: (event: ModelEvent, captures: EventObjectCaptures) => void + ): () => void; + on( + eventType: T, + listener: (pathSegments: string[], event: ModelOnImmediateEventMap[T]) => void + ): () => void; on( eventType: 'all', listener: (pathSegments: string[], event: ModelOnEventMap[keyof ModelOnEventMap]) => void @@ -92,11 +104,22 @@ declare module './Model' { options: { useEventObjects: true }, listener: (event: ModelOnEventMap[T], captures: EventObjectCaptures) => void ): () => void; + on( + eventType: 'all', + pathPattern: PathLike, + options: { useEventObjects: true }, + listener: (event: ModelEvent, captures: EventObjectCaptures) => void + ): () => void; on( eventType: T, options: { useEventObjects: true }, listener: (event: ModelOnEventMap[T], captures: EventObjectCaptures) => void ): () => void; + on( + eventType: 'all', + options: { useEventObjects: true }, + listener: (event: ModelEvent, captures: EventObjectCaptures) => void + ): () => void; /** * Listen to Racer events matching a certain path or path pattern, removing @@ -132,7 +155,7 @@ declare module './Model' { removeAllListeners(type: string, subpath: Path): void; removeContextListeners(): void; - removeListener(eventType: keyof ModelOnEventMap, listener: Function): void; + removeListener(eventType: keyof ModelOnEventMap | keyof ModelOnImmediateEventMap | 'all', listener: Function): void; setMaxListeners(limit: number): void; silent(value?: boolean): Model; From 67745a3b9b1e70f6a481c4fe97c0bff436d62584 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 29 Apr 2024 14:05:45 -0700 Subject: [PATCH 430/479] castSegments: Remove unnecessary handling of nested array and non-array cases that don't actually occur --- src/util.ts | 13 +++---------- test/util/util.js | 5 ----- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/util.ts b/src/util.ts index 77e5d38cf..66c40448f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -39,22 +39,15 @@ class AsyncGroup { } } -function castSegment(segment: string): string | number { +function castSegment(segment: string | number): string | number { return (typeof segment === 'string' && isArrayIndex(segment)) ? +segment // sneaky op to convert numeric string to number : segment; } -export function castSegments(segments: Readonly) { - if (typeof segments === 'string') { - return castSegment(segments); - } +export function castSegments(segments: Readonly>) { // Cast number path segments from strings to numbers - return segments.map(segment => - Array.isArray(segment) - ? castSegments(segment) - : castSegment(segment) - ); + return segments.map(segment => castSegment(segment)); } export function contains(segments, testSegments) { diff --git a/test/util/util.js b/test/util/util.js index 520f35355..2131af056 100644 --- a/test/util/util.js +++ b/test/util/util.js @@ -89,10 +89,5 @@ describe('util', function() { expect(actual).to.eql(['foo', 3, 3]); expect(actual).to.not.equal(segments); // args not mutated }); - - it('handles plain strings', () => { - expect(util.castSegments('foo.bar')).to.eql('foo.bar'); - expect(util.castSegments('6')).to.eql(6); - }); }); }); From 4a2d06f30fe70836a01c5a7ab53818e4c084d144 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Mon, 29 Apr 2024 14:09:40 -0700 Subject: [PATCH 431/479] 2.0.0-beta.21 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f5e99621c..daa74e0e1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.20", + "version": "2.0.0-beta.21", "main": "./lib/index.js", "files": [ "lib/*" From d106e83af9b52577f7320c6b33ccca5fbdf4bb28 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 1 May 2024 11:29:58 -0700 Subject: [PATCH 432/479] RacerBackend type-only export and serverRequire backend in createBackend --- src/Backend.ts | 1 - src/index.ts | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Backend.ts b/src/Backend.ts index e507b937f..6b88cd15e 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -1,4 +1,3 @@ -import { Model } from './Model'; import * as path from 'path'; import * as util from './util'; import { ModelOptions, RootModel } from './Model/Model'; diff --git a/src/index.ts b/src/index.ts index 16b43459e..9902df815 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { Racer } from './Racer'; import * as util from './util'; import type { ShareDBOptions } from 'sharedb'; -import { RacerBackend } from './Backend'; +import { type RacerBackend } from './Backend'; import { RootModel, type ModelOptions } from './Model'; export { Query } from './Model/Query'; @@ -37,5 +37,9 @@ export function createModel(data?) { } export function createBackend(options?: BackendOptions) { - return new RacerBackend(racer, options); + const backendModule = util.serverRequire(module, './Backend'); + if (backendModule == null) { + throw new Error('racer.createBackend can only be called in server node process'); + } + return new backendModule.RacerBackend(racer, options); } From 0a40f23612d58e72162e67b8c2c7c73cb6154618 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 1 May 2024 11:33:22 -0700 Subject: [PATCH 433/479] 2.0.0-beta.22 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index daa74e0e1..7e3fc3373 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.21", + "version": "2.0.0-beta.22", "main": "./lib/index.js", "files": [ "lib/*" From 179edb8faabc8fb8fb555b86107964e16547d87f Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 1 May 2024 12:01:33 -0700 Subject: [PATCH 434/479] 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7e3fc3373..7f578dad8 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0-beta.22", + "version": "2.0.0", "main": "./lib/index.js", "files": [ "lib/*" From 2b97d8040770d2e0eba41fba92304bd35be6acb0 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 10 May 2024 11:04:31 -0700 Subject: [PATCH 435/479] Make path optional on delPromised --- src/Model/mutators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 2f477c194..b9029ade2 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -60,7 +60,7 @@ declare module './Model' { */ del(subpath: Path, cb?: Callback): S | undefined; del(cb?: Callback): T | undefined; - delPromised(subpath: Path): Promise; + delPromised(subpath?: Path): Promise; _del(segments: Segments, cb?: ErrorCallback): S; _delNoDereference(segments: Segments, cb?: ErrorCallback): void; From 3e0d4995dd50fa57413d1807ab5ecdfe02b97e83 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 10 May 2024 11:09:53 -0700 Subject: [PATCH 436/479] 2.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f578dad8..dbf3f0e72 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.0", + "version": "2.0.1", "main": "./lib/index.js", "files": [ "lib/*" From 6c0fe35ee143f6a5990f906095463eadd97badf2 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 22 May 2024 10:54:15 -0700 Subject: [PATCH 437/479] Pass id of newly created doc in callback form add and resolve with id for addPromised --- src/Model/mutators.ts | 30 ++++++++++++++++++++---------- test/Model/mutators.js | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 test/Model/mutators.js diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index b9029ade2..76365e701 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -10,6 +10,8 @@ var RemoveEvent = mutationEvents.RemoveEvent; var MoveEvent = mutationEvents.MoveEvent; var promisify = util.promisify; +type ValueCallback = ((error: Error | null | undefined, value?: T) => void); + declare module './Model' { interface Model { _mutate(segments, fn, cb): void; @@ -43,11 +45,11 @@ declare module './Model' { createNullPromised(subpath: Path, value: any): Promise; _createNull(segments: Segments, value: any, cb?: ErrorCallback): void; - add(value: any, cb?: ErrorCallback): string; - add(subpath: Path, value: any, cb?: ErrorCallback): string; + add(value: any, cb?: ValueCallback): string; + add(subpath: Path, value: any, cb?: ValueCallback): string; addPromised(value: any): Promise; addPromised(subpath: Path, value: any): Promise; - _add(segments: Segments, value: any, cb?: ErrorCallback): string; + _add(segments: Segments, value: any, cb?: ValueCallback): string; /** * Deletes the value at this model's path or a relative subpath. @@ -404,20 +406,23 @@ Model.prototype.add = function() { var segments = this._splitPath(subpath); return this._add(segments, value, cb); }; -Model.prototype.addPromised = promisify(Model.prototype.add); +Model.prototype.addPromised = promisify(Model.prototype.add); Model.prototype._add = function(segments, value, cb) { if (typeof value !== 'object') { var message = 'add requires an object value. Invalid value: ' + value; cb = this.wrapCallback(cb); - return cb(new Error(message)); + cb(new Error(message)); + return; } - var id = value.id || this.id(); + + const id = value.id || this.id(); value.id = id; segments = this._dereference(segments.concat(id)); - var model = this; + const model = this; + function add(doc, docSegments, fnCb) { - var previous; + let previous; if (docSegments.length) { previous = doc.set(docSegments, value, fnCb); } else { @@ -426,10 +431,15 @@ Model.prototype._add = function(segments, value, cb) { // it being stored in the database by ShareJS value = doc.get(); } - var event = new ChangeEvent(value, previous, model._pass); + const event = new ChangeEvent(value, previous, model._pass); model._emitMutation(segments, event); } - this._mutate(segments, add, cb); + + const callbackWithId = (cb != null) + ? (err: Error) => { cb(err, id); } + : null; + + this._mutate(segments, add, callbackWithId); return id; }; diff --git a/test/Model/mutators.js b/test/Model/mutators.js new file mode 100644 index 000000000..6d8493c29 --- /dev/null +++ b/test/Model/mutators.js @@ -0,0 +1,23 @@ +const {expect} = require('chai'); +const {RootModel} = require('../../lib/Model'); + +describe('mutatotrs', () => { + describe('add', () => { + const guidRegExp = new RegExp(/[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}/); + it('returns created id in callback', () => { + const model = new RootModel(); + model.add('_test_doc', {name: 'foo'}, (error, id) => { + expect(error).to.not.exist; + expect(id).not.to.be.undefined; + expect(id).to.match(guidRegExp, 'Excpected a GUID-like Id'); + }); + }); + + it('resolves promised add with id', async () => { + const model = new RootModel(); + const id = await model.addPromised('_test_doc', {name: 'bar'}); + expect(id).not.to.be.undefined; + expect(id).to.match(guidRegExp, 'Excpected a GUID-like Id'); + }); + }); +}); From c7e4a36776ef01d0ec27e905d066dfca1b480866 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 22 May 2024 12:08:06 -0700 Subject: [PATCH 438/479] Add getValues method for fetching array of docs from collection --- src/Model/collections.ts | 13 +++++++++++++ test/Model/collections.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 test/Model/collections.js diff --git a/src/Model/collections.ts b/src/Model/collections.ts index 1f9531b96..facdbdae8 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -2,6 +2,7 @@ import { Doc } from './Doc'; import { Model, RootModel } from './Model'; import { JSONObject } from 'sharedb/lib/sharedb'; import type { Path, ReadonlyDeep, ShallowCopiedValue, Segments } from '../types'; + var LocalDoc = require('./LocalDoc'); var util = require('../util'); @@ -78,6 +79,14 @@ declare module './Model' { _get(segments: Segments): any; _getCopy(segments: Segments): any; _getDeepCopy(segments: Segments): any; + + /** + * Gets array of values of collection at this models path or relative subpath + * + * If no values exist at subpath, an empty array is returned + * @param subpath + */ + getValues(subpath?: Path): ReadonlyDeep[]; } } @@ -124,6 +133,10 @@ Model.prototype._getDeepCopy = function(segments) { return util.deepCopy(value); }; +Model.prototype.getValues = function(subpath?: Path) { + return this.filter(subpath, null).get(); +} + Model.prototype.getOrCreateCollection = function(name) { var collection = this.root.collections[name]; if (collection) return collection; diff --git a/test/Model/collections.js b/test/Model/collections.js new file mode 100644 index 000000000..622990811 --- /dev/null +++ b/test/Model/collections.js @@ -0,0 +1,30 @@ +const {expect} = require('../util'); +const {RootModel} = require('../../lib'); + +describe('collections', () => { + describe('getValues', () => { + it('returns array of values from collection', () => { + const model = new RootModel(); + model.add('_test_docs', {name: 'foo'}); + model.add('_test_docs', {name: 'bar'}); + + const values = model.getValues('_test_docs'); + + expect(values).to.be.instanceOf(Array); + expect(values).to.have.lengthOf(2); + + ['foo', 'bar'].forEach((value, index) => { + expect(values[index]).to.have.property('name', value); + }); + }); + + it('return empty array when no values at subpath', () => { + const model = new RootModel(); + + const values = model.getValues('_test_docs'); + + expect(values).to.be.instanceOf(Array); + expect(values).to.have.lengthOf(0); + }); + }); +}); From 1b7b47cb140ec0c2fad9d4b4a7ddfbb74c793ae9 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 22 May 2024 14:29:15 -0700 Subject: [PATCH 439/479] Add methods getOrDefault and getOrThrow --- src/Model/collections.ts | 28 ++++++++++++++++++++++++++++ test/Model/collections.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 test/Model/collections.js diff --git a/src/Model/collections.ts b/src/Model/collections.ts index 1f9531b96..43c18b7de 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -2,6 +2,7 @@ import { Doc } from './Doc'; import { Model, RootModel } from './Model'; import { JSONObject } from 'sharedb/lib/sharedb'; import type { Path, ReadonlyDeep, ShallowCopiedValue, Segments } from '../types'; + var LocalDoc = require('./LocalDoc'); var util = require('../util'); @@ -75,6 +76,21 @@ declare module './Model' { getOrCreateCollection(name: string): Collection; getOrCreateDoc(collectionName: string, id: string, data: any); + /** + * Get a value that may be undefined but ensure value is returned + * + * @param subpath + * @param defaultValue value to return if no value at subpath + */ + getOrDefault(subpath: Path, defaultValue: S): ReadonlyDeep; + + /** + * Get a value and throw error if undefined + * + * @param subpath + */ + getOrThrow(subpath: Path): ReadonlyDeep; + _get(segments: Segments): any; _getCopy(segments: Segments): any; _getDeepCopy(segments: Segments): any; @@ -152,6 +168,18 @@ Model.prototype.getOrCreateDoc = function(collectionName, id, data) { return collection.getOrCreateDoc(id, data); }; +Model.prototype.getOrDefault = function(subpath: Path, defaultValue: S) { + return this.get(subpath) ?? defaultValue as ReadonlyDeep; +}; + +Model.prototype.getOrThrow = function(subpath?: Path) { + const value = this.get(subpath); + if (value === undefined) { + throw new Error(`No value at path ${subpath}`) + } + return value; +}; + /** * @param {String} subpath */ diff --git a/test/Model/collections.js b/test/Model/collections.js new file mode 100644 index 000000000..b04edcf52 --- /dev/null +++ b/test/Model/collections.js @@ -0,0 +1,36 @@ +const {expect} = require('../util'); +const {RootModel} = require('../../lib'); + +describe('collections', () => { + describe('getOrDefault', () => { + it('returns value if defined', () => { + const model = new RootModel(); + model.add('_test_doc', {name: 'foo'}); + const value = model.getOrDefault('_test_doc', {name: 'bar'}); + expect(value).not.to.be.undefined; + }); + + it('returns defuault value if undefined', () => { + const model = new RootModel(); + const defaultValue = {name: 'bar'}; + const value = model.getOrDefault('_test_doc', defaultValue); + expect(value).not.to.be.undefined; + expect(value.name).to.equal('bar'); + expect(value).to.eql(defaultValue); + }); + }); + + describe('getOrThrow', () => { + it('returns value if defined', () => { + const model = new RootModel(); + model.add('_test_doc', {name: 'foo'}); + const value = model.getOrThrow('_test_doc', {name: 'bar'}); + expect(value).not.to.be.undefined; + }); + + it('thows if value undefined', () => { + const model = new RootModel(); + expect(() => model.getOrThrow('_test_doc', {name: 'bar'})).to.throw(`No value at path _test_doc`); + }); + }); +}); From 9b275471199d1c66de9e1f5fa0e6e4e327acb517 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 22 May 2024 14:50:24 -0700 Subject: [PATCH 440/479] Use full path in eror for getOrThrow --- src/Model/collections.ts | 3 ++- test/Model/collections.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Model/collections.ts b/src/Model/collections.ts index 43c18b7de..e90e75617 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -175,7 +175,8 @@ Model.prototype.getOrDefault = function(subpath: Path, defaultValue: S) { Model.prototype.getOrThrow = function(subpath?: Path) { const value = this.get(subpath); if (value === undefined) { - throw new Error(`No value at path ${subpath}`) + const fullpath = [this._at, subpath].filter(Boolean).join('.'); + throw new Error(`No value at path ${fullpath}`) } return value; }; diff --git a/test/Model/collections.js b/test/Model/collections.js index b04edcf52..33e10d1a8 100644 --- a/test/Model/collections.js +++ b/test/Model/collections.js @@ -31,6 +31,7 @@ describe('collections', () => { it('thows if value undefined', () => { const model = new RootModel(); expect(() => model.getOrThrow('_test_doc', {name: 'bar'})).to.throw(`No value at path _test_doc`); + expect(() => model.scope('_test').getOrThrow('doc.1', {name: 'bar'})).to.throw(`No value at path _test.doc.1`); }); }); }); From 1efc5dfe0e54ccdbcd0855f3a0fbafe32e602c2b Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 23 May 2024 11:26:59 -0700 Subject: [PATCH 441/479] Better doc verbage Co-authored-by: Eric Hwang --- src/Model/collections.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/collections.ts b/src/Model/collections.ts index e90e75617..e6a779340 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -77,7 +77,7 @@ declare module './Model' { getOrCreateDoc(collectionName: string, id: string, data: any); /** - * Get a value that may be undefined but ensure value is returned + * Gets value at the path if not nullish, otherwise returns provided default value * * @param subpath * @param defaultValue value to return if no value at subpath From 74633c290979c922d30899169ef7ae8dbe9c4e3a Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 23 May 2024 11:41:35 -0700 Subject: [PATCH 442/479] Ensure default or thorwn error for null values; add test cases for same --- src/Model/collections.ts | 4 ++-- test/Model/collections.js | 25 +++++++++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Model/collections.ts b/src/Model/collections.ts index e90e75617..0654b55f9 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -174,8 +174,8 @@ Model.prototype.getOrDefault = function(subpath: Path, defaultValue: S) { Model.prototype.getOrThrow = function(subpath?: Path) { const value = this.get(subpath); - if (value === undefined) { - const fullpath = [this._at, subpath].filter(Boolean).join('.'); + if (value == null) { + const fullpath = this.path(subpath); throw new Error(`No value at path ${fullpath}`) } return value; diff --git a/test/Model/collections.js b/test/Model/collections.js index 33e10d1a8..6ef6b78bb 100644 --- a/test/Model/collections.js +++ b/test/Model/collections.js @@ -18,20 +18,37 @@ describe('collections', () => { expect(value.name).to.equal('bar'); expect(value).to.eql(defaultValue); }); + + it('returns defult value if null', () => { + const model = new RootModel(); + const id = model.add('_test_doc', {name: null}); + const defaultValue = 'bar'; + const value = model.getOrDefault(`_test_doc.${id}.name`, defaultValue); + expect(value).not.to.be.null; + expect(value).to.equal('bar'); + expect(value).to.eql(defaultValue); + }); }); describe('getOrThrow', () => { it('returns value if defined', () => { const model = new RootModel(); model.add('_test_doc', {name: 'foo'}); - const value = model.getOrThrow('_test_doc', {name: 'bar'}); + const value = model.getOrThrow('_test_doc'); expect(value).not.to.be.undefined; }); - it('thows if value undefined', () => { + it('throws if value undefined', () => { + const model = new RootModel(); + expect(() => model.getOrThrow('_test_doc')).to.throw(`No value at path _test_doc`); + expect(() => model.scope('_test').getOrThrow('doc.1')).to.throw(`No value at path _test.doc.1`); + }); + + it('throws if value null', () => { const model = new RootModel(); - expect(() => model.getOrThrow('_test_doc', {name: 'bar'})).to.throw(`No value at path _test_doc`); - expect(() => model.scope('_test').getOrThrow('doc.1', {name: 'bar'})).to.throw(`No value at path _test.doc.1`); + const id = model.add('_test_doc', {name: null}); + expect(model.getOrThrow(`_test_doc.${id}`)).to.eql({id, name: null}); + expect(() => model.getOrThrow(`_test_doc.${id}.name`)).to.throw(`No value at path _test_doc`); }); }); }); From f9361c1087b62f8bbb510a602ef609ede4fc5c1a Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 23 May 2024 12:32:42 -0700 Subject: [PATCH 443/479] Update getValues logic; add test case for error on non-object value --- src/Model/collections.ts | 11 +++++++++-- test/Model/collections.js | 13 ++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Model/collections.ts b/src/Model/collections.ts index facdbdae8..874000c41 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -81,7 +81,7 @@ declare module './Model' { _getDeepCopy(segments: Segments): any; /** - * Gets array of values of collection at this models path or relative subpath + * Gets array of values of collection at this model's path or relative subpath * * If no values exist at subpath, an empty array is returned * @param subpath @@ -134,7 +134,14 @@ Model.prototype._getDeepCopy = function(segments) { }; Model.prototype.getValues = function(subpath?: Path) { - return this.filter(subpath, null).get(); + const value = this.get(subpath); + if (value == null) { + return []; + } + if (typeof value !== 'object') { + throw new Error(`Found non-object type for getValues('${this.path(subpath)}')`); + } + return Object.values(value) as ReadonlyDeep[]; } Model.prototype.getOrCreateCollection = function(name) { diff --git a/test/Model/collections.js b/test/Model/collections.js index 622990811..a51a68f21 100644 --- a/test/Model/collections.js +++ b/test/Model/collections.js @@ -7,12 +7,9 @@ describe('collections', () => { const model = new RootModel(); model.add('_test_docs', {name: 'foo'}); model.add('_test_docs', {name: 'bar'}); - const values = model.getValues('_test_docs'); - expect(values).to.be.instanceOf(Array); expect(values).to.have.lengthOf(2); - ['foo', 'bar'].forEach((value, index) => { expect(values[index]).to.have.property('name', value); }); @@ -20,11 +17,17 @@ describe('collections', () => { it('return empty array when no values at subpath', () => { const model = new RootModel(); - const values = model.getValues('_test_docs'); - expect(values).to.be.instanceOf(Array); expect(values).to.have.lengthOf(0); }); + + it('throws error if non-object result at path', () => { + const model = new RootModel(); + const id = model.add('_colors', {rgb: 3}); + expect( + () => model.getValues(`_colors.${id}.rgb`), + ).to.throw(`Found non-object type for getValues('_colors.${id}.rgb')`); + }); }); }); From c7548ba4433b690febf4ab103ce242ec117cc5ce Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 23 May 2024 12:34:36 -0700 Subject: [PATCH 444/479] Better doc verbage --- src/Model/collections.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Model/collections.ts b/src/Model/collections.ts index 2b8643fb7..a5ea066d3 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -85,8 +85,9 @@ declare module './Model' { getOrDefault(subpath: Path, defaultValue: S): ReadonlyDeep; /** - * Get a value and throw error if undefined + * Gets the value located at this model's path or a relative subpath. * + * If no value exists at the path, or the value is nullish (null or undefined), this will throw an error. * @param subpath */ getOrThrow(subpath: Path): ReadonlyDeep; From 519ee8012bd7362cc238d97766a9075610c68b16 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 23 May 2024 13:17:28 -0700 Subject: [PATCH 445/479] Lint fix --- test/Model/collections.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Model/collections.js b/test/Model/collections.js index a51a68f21..1e5337065 100644 --- a/test/Model/collections.js +++ b/test/Model/collections.js @@ -26,7 +26,7 @@ describe('collections', () => { const model = new RootModel(); const id = model.add('_colors', {rgb: 3}); expect( - () => model.getValues(`_colors.${id}.rgb`), + () => model.getValues(`_colors.${id}.rgb`) ).to.throw(`Found non-object type for getValues('_colors.${id}.rgb')`); }); }); From f1d9dc76aa98f4d19ca86599db83aff788503bb1 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 23 May 2024 13:25:16 -0700 Subject: [PATCH 446/479] Typo fixes --- test/Model/mutators.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Model/mutators.js b/test/Model/mutators.js index 6d8493c29..589ba3d93 100644 --- a/test/Model/mutators.js +++ b/test/Model/mutators.js @@ -1,7 +1,7 @@ const {expect} = require('chai'); const {RootModel} = require('../../lib/Model'); -describe('mutatotrs', () => { +describe('mutators', () => { describe('add', () => { const guidRegExp = new RegExp(/[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}/); it('returns created id in callback', () => { @@ -9,7 +9,7 @@ describe('mutatotrs', () => { model.add('_test_doc', {name: 'foo'}, (error, id) => { expect(error).to.not.exist; expect(id).not.to.be.undefined; - expect(id).to.match(guidRegExp, 'Excpected a GUID-like Id'); + expect(id).to.match(guidRegExp, 'Expected a GUID-like Id'); }); }); @@ -17,7 +17,7 @@ describe('mutatotrs', () => { const model = new RootModel(); const id = await model.addPromised('_test_doc', {name: 'bar'}); expect(id).not.to.be.undefined; - expect(id).to.match(guidRegExp, 'Excpected a GUID-like Id'); + expect(id).to.match(guidRegExp, 'Expected a GUID-like Id'); }); }); }); From 8ec99d1825149938db77e1e429a52fa868ef31fc Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 23 May 2024 13:35:26 -0700 Subject: [PATCH 447/479] Change to use let --- src/Model/mutators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 76365e701..789659b25 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -410,7 +410,7 @@ Model.prototype.addPromised = promisify(Model.prototype.add); Model.prototype._add = function(segments, value, cb) { if (typeof value !== 'object') { - var message = 'add requires an object value. Invalid value: ' + value; + let message = 'add requires an object value. Invalid value: ' + value; cb = this.wrapCallback(cb); cb(new Error(message)); return; From 4e19a9ca9ca7315ab305fe72885413c0bd51ff9e Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 23 May 2024 16:34:57 -0700 Subject: [PATCH 448/479] Change ValueCallback value to non-optional; rename wrapped callback in _add --- src/Model/mutators.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 789659b25..55b4789e0 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -10,7 +10,7 @@ var RemoveEvent = mutationEvents.RemoveEvent; var MoveEvent = mutationEvents.MoveEvent; var promisify = util.promisify; -type ValueCallback = ((error: Error | null | undefined, value?: T) => void); +type ValueCallback = ((error: Error | null | undefined, value: T) => void); declare module './Model' { interface Model { @@ -411,8 +411,8 @@ Model.prototype.addPromised = promisify(Model.prototype.add); Model.prototype._add = function(segments, value, cb) { if (typeof value !== 'object') { let message = 'add requires an object value. Invalid value: ' + value; - cb = this.wrapCallback(cb); - cb(new Error(message)); + const errorCallback = this.wrapCallback(cb); + errorCallback(new Error(message)); return; } From 703b7fe7904379d9fe2825446a407d4cbf8edf0a Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 24 May 2024 11:58:09 -0700 Subject: [PATCH 449/479] Fix typo --- test/Model/collections.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Model/collections.js b/test/Model/collections.js index 6ef6b78bb..7a7abe92d 100644 --- a/test/Model/collections.js +++ b/test/Model/collections.js @@ -19,7 +19,7 @@ describe('collections', () => { expect(value).to.eql(defaultValue); }); - it('returns defult value if null', () => { + it('returns default value if null', () => { const model = new RootModel(); const id = model.add('_test_doc', {name: null}); const defaultValue = 'bar'; From 9aaa382a3be64a6c8ffbc2c17c7fa4b41f87f7b0 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 24 May 2024 12:21:18 -0700 Subject: [PATCH 450/479] Update CHANGELOG.md [skip ci] --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..26491c596 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# v2.1.0 (Fri May 24 2024) + +#### 🚀 Enhancement + +- Add methods getOrDefault and getOrThrow [#307](https://github.com/derbyjs/racer/pull/307) ([@craigbeck](https://github.com/craigbeck)) +- Add getValues method for fetching array of docs from collection [#306](https://github.com/derbyjs/racer/pull/306) ([@craigbeck](https://github.com/craigbeck)) + +#### 🐛 Bug Fix + +- Resolve `addPromised` with new doc `id` [#305](https://github.com/derbyjs/racer/pull/305) ([@craigbeck](https://github.com/craigbeck)) + +#### Authors: 1 + +- Craig Beck ([@craigbeck](https://github.com/craigbeck)) From 9d75c1a00b6b1c5e6ab57c2a362796193d35e5a3 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 24 May 2024 12:22:17 -0700 Subject: [PATCH 451/479] Update contributors [skip ci] --- .all-contributorsrc | 25 +++++++++++++++++++++++++ README.md | 23 +++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .all-contributorsrc diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 000000000..873575d4b --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,25 @@ +{ + "projectName": "racer", + "projectOwner": "derbyjs", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md" + ], + "imageSize": 100, + "commit": true, + "commitConvention": "none", + "contributors": [ + { + "login": "craigbeck", + "name": "Craig Beck", + "avatar_url": "https://avatars.githubusercontent.com/u/1620605?v=4", + "profile": "http://craigbeck.io/", + "contributions": [ + "test", + "code" + ] + } + ], + "contributorsPerLine": 7 +} diff --git a/README.md b/README.md index ddd8e5f62..fe3e7f0b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # Racer + +[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) + Racer is a realtime model synchronization engine for Node.js. By leveraging [ShareDB](https://github.com/share/sharedb), multiple users can interact with the same data in realtime via Operational Transformation, a sophisticated conflict resolution algorithm that works in realtime and with offline clients. ShareDB also supports PubSub across multiple servers for horizontal scaling. Clients can express data subscriptions and fetches in terms of queries and specific documents, so different clients can be subscribed to different overlapping sets of data. On top of this sophisticated backend, Racer provides a simple model and event interface for writing application logic. @@ -86,3 +89,23 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## Contributors ✨ + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + +

Craig Beck

⚠️ 💻
+ + + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! \ No newline at end of file From 51af009b4e899695bfe01bd2623bdbf25c048fa6 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 24 May 2024 12:24:58 -0700 Subject: [PATCH 452/479] Ignore .env file --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e09228133..6b679a8cb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ node_modules coverage lib/ + +.env From f8b82565e922f5f07684d2a50d4d71f7ef71c18a Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 24 May 2024 12:25:52 -0700 Subject: [PATCH 453/479] Update CHANGELOG.md [skip ci] --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26491c596..def0f3701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,27 @@ - Resolve `addPromised` with new doc `id` [#305](https://github.com/derbyjs/racer/pull/305) ([@craigbeck](https://github.com/craigbeck)) +#### ⚠️ Pushed to `master` + +- Ignore .env file ([@craigbeck](https://github.com/craigbeck)) + +#### Authors: 1 + +- Craig Beck ([@craigbeck](https://github.com/craigbeck)) + +--- + +# v2.1.0 (Fri May 24 2024) + +#### 🚀 Enhancement + +- Add methods getOrDefault and getOrThrow [#307](https://github.com/derbyjs/racer/pull/307) ([@craigbeck](https://github.com/craigbeck)) +- Add getValues method for fetching array of docs from collection [#306](https://github.com/derbyjs/racer/pull/306) ([@craigbeck](https://github.com/craigbeck)) + +#### 🐛 Bug Fix + +- Resolve `addPromised` with new doc `id` [#305](https://github.com/derbyjs/racer/pull/305) ([@craigbeck](https://github.com/craigbeck)) + #### Authors: 1 - Craig Beck ([@craigbeck](https://github.com/craigbeck)) From 1c10bbab9ac167053c928a761fec18ee13adec8e Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 24 May 2024 12:26:46 -0700 Subject: [PATCH 454/479] Bump version to: 2.1.0 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dbf3f0e72..b8608e2bd 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.1", + "version": "2.1.0", "main": "./lib/index.js", "files": [ "lib/*" From 65f8bea91ea00fffb3560b87828c0e684c0f54c0 Mon Sep 17 00:00:00 2001 From: Deon Groenewald Date: Mon, 27 May 2024 12:30:40 -0400 Subject: [PATCH 455/479] Fix breaking change in types introduced by `ValueCallback` --- src/Model/mutators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 55b4789e0..6c7d85434 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -10,7 +10,7 @@ var RemoveEvent = mutationEvents.RemoveEvent; var MoveEvent = mutationEvents.MoveEvent; var promisify = util.promisify; -type ValueCallback = ((error: Error | null | undefined, value: T) => void); +type ValueCallback = ((error: Error | undefined, value: T) => void); declare module './Model' { interface Model { From 7590930195961fb08e302c3053b218488172457f Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 27 May 2024 13:03:40 -0700 Subject: [PATCH 456/479] 2.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b8608e2bd..5a4d1fe93 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.1.0", + "version": "2.1.1", "main": "./lib/index.js", "files": [ "lib/*" From 6ec7fcba88431860788582773563d34d8d48e335 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 24 May 2024 12:26:46 -0700 Subject: [PATCH 457/479] Add jsdoc comments; rename function args to match --- package.json | 2 +- src/Backend.ts | 16 ++++++++++++++++ src/Model/Model.ts | 15 ++++++++++++++- src/Model/contexts.ts | 36 +++++++++++++++++++++++++----------- src/Model/mutators.ts | 43 +++++++++++++++++++++++++++++++++++++++++-- src/Model/paths.ts | 6 ++++++ src/index.ts | 12 ++++++++++++ src/util.ts | 24 +++++++++++++++++++++++- 8 files changed, 138 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index dbf3f0e72..b8608e2bd 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.0.1", + "version": "2.1.0", "main": "./lib/index.js", "files": [ "lib/*" diff --git a/src/Backend.ts b/src/Backend.ts index 6b88cd15e..82d636663 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -3,6 +3,9 @@ import * as util from './util'; import { ModelOptions, RootModel } from './Model/Model'; import Backend = require('sharedb'); +/** + * RacerBackend extends ShareDb Backend + */ export class RacerBackend extends Backend { racer: any; modelOptions: any; @@ -17,6 +20,13 @@ export class RacerBackend extends Backend { }); } + /** + * Create new `RootModel` + * + * @param options - optional model options + * @param request - optional request context See {@link sharedb.listen} for details. + * @returns a new root model + */ createModel(options?: ModelOptions, req?: any) { if (this.modelOptions) { options = (options) ? @@ -29,6 +39,12 @@ export class RacerBackend extends Backend { return model; }; + /** + * Model middleware that creates and attaches a `Model` to the `request` + * and attaches listeners to response for closing model on response completion + * + * @returns an Express middleware function + */ modelMiddleware() { var backend = this; function modelMiddleware(req, res, next) { diff --git a/src/Model/Model.ts b/src/Model/Model.ts index 35b56469c..7595ed4b1 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -28,6 +28,9 @@ declare module './Model' { type ModelInitFunction = (instance: RootModel, options: ModelOptions) => void; +/** + * Base class for Racer models + */ export class Model { static INITS: ModelInitFunction[] = []; @@ -45,7 +48,11 @@ export class Model { _preventCompose: boolean; _silent: boolean; - + /** + * Creates a new Racer UUID + * + * @returns a new Racer UUID. + * */ id(): UUID { return uuidv4(); } @@ -55,6 +62,9 @@ export class Model { }; } +/** + * RootModel is the model that holds all data and maintains connection info + */ export class RootModel extends Model { backend: RacerBackend; connection: Connection; @@ -70,6 +80,9 @@ export class RootModel extends Model { } } +/** + * Model for some subset of the data + */ export class ChildModel extends Model { constructor(model: Model) { super(); diff --git a/src/Model/contexts.ts b/src/Model/contexts.ts index ffd9763f1..95fdaf46d 100644 --- a/src/Model/contexts.ts +++ b/src/Model/contexts.ts @@ -24,8 +24,22 @@ declare module './Model' { * @see https://derbyjs.github.io/derby/models/contexts */ context(contextId: string): ChildModel; - getOrCreateContext(id: string): Context; - setContext(id: string): void; + /** + * Get the named context or create a new named context if it doesnt exist. + * + * @param contextId context id + * + * @see https://derbyjs.github.io/derby/models/contexts + */ + getOrCreateContext(contextId: string): Context; + /** + * Set the named context to use for this model. + * + * @param contextId context id + * + * @see https://derbyjs.github.io/derby/models/contexts + */ + setContext(contextId: string): void; /** * Unloads data for this model's context, or for a specific named context. @@ -52,24 +66,24 @@ Model.INITS.push(function(model) { model.root.setContext('root'); }); -Model.prototype.context = function(id) { +Model.prototype.context = function(contextId) { var model = this._child(); - model.setContext(id); + model.setContext(contextId); return model; }; -Model.prototype.setContext = function(id) { - this._context = this.getOrCreateContext(id); +Model.prototype.setContext = function(contextId) { + this._context = this.getOrCreateContext(contextId); }; -Model.prototype.getOrCreateContext = function(id) { - var context = this.root._contexts[id] || - (this.root._contexts[id] = new Context(this, id)); +Model.prototype.getOrCreateContext = function(contextId) { + var context = this.root._contexts[contextId] || + (this.root._contexts[contextId] = new Context(this, contextId)); return context; }; -Model.prototype.unload = function(id) { - var context = (id) ? this.root._contexts[id] : this._context; +Model.prototype.unload = function(contextId) { + var context = (contextId) ? this.root._contexts[contextId] : this._context; context && context.unload(); }; diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 55b4789e0..0912e0cb2 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -44,15 +44,38 @@ declare module './Model' { createNullPromised(value: any): Promise; createNullPromised(subpath: Path, value: any): Promise; _createNull(segments: Segments, value: any, cb?: ErrorCallback): void; - + + /** + * Adds a document to the collection, under an id subpath corresponding + * to either the document's `id` property if present, or else a new randomly + * generated id. + * + * If a callback is provided, it's called when the write is committed or + * fails. + * + * @param value - Document to add + * @param cb - Optional callback + */ add(value: any, cb?: ValueCallback): string; + /** + * Adds a document to the collection, under an id subpath corresponding + * to either the document's `id` property if present, or else a new randomly + * generated id. + * + * If a callback is provided, it's called when the write is committed or + * fails. + * + * @param path - Optional Collection under which to add the document + * @param value - Document to add + * @param cb - Optional callback + */ add(subpath: Path, value: any, cb?: ValueCallback): string; addPromised(value: any): Promise; addPromised(subpath: Path, value: any): Promise; _add(segments: Segments, value: any, cb?: ValueCallback): string; /** - * Deletes the value at this model's path or a relative subpath. + * Deletes the value at this relative subpath. * * If a callback is provided, it's called when the write is committed or * fails. @@ -61,7 +84,23 @@ declare module './Model' { * @returns the old value at the path */ del(subpath: Path, cb?: Callback): S | undefined; + /** + * Deletes the value at this model's path. + * + * If a callback is provided, it's called when the write is committed or + * fails. + * + * @returns the old value at the path + */ del(cb?: Callback): T | undefined; + /** + * Deletes the value at this relative subpath. If not provided deletes at model path + * + * Promise resolves when commit succeeds, rejected on commit failure. + * + * @param subpath - Optional subpath to delete + * @returns promise + */ delPromised(subpath?: Path): Promise; _del(segments: Segments, cb?: ErrorCallback): S; diff --git a/src/Model/paths.ts b/src/Model/paths.ts index 6f911abe8..c6a31078b 100644 --- a/src/Model/paths.ts +++ b/src/Model/paths.ts @@ -6,8 +6,14 @@ exports.mixin = {}; declare module './Model' { interface Model { + /** + * Returns a ChildModel scoped to a relative subpath under this model's path. + * + * @param subpath + */ at(): ChildModel; at(subpath: PathLike): ChildModel; + isPath(subpath: PathLike): boolean; leaf(path: string): string; parent(levels?: number): Model; diff --git a/src/index.ts b/src/index.ts index 9902df815..15e2d8c93 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,12 @@ export { export const racer = new Racer(); +/** + * Creates new RootModel + * + * @param data - Optional Data to initialize model with + * @returns RootModel + */ export function createModel(data?) { var model = new RootModel(); if (data) { @@ -36,6 +42,12 @@ export function createModel(data?) { return model; } +/** + * Creates racer backend. Can only be called in server process and throws error if called in browser. + * + * @param options - Optional + * @returns racer backend + */ export function createBackend(options?: BackendOptions) { const backendModule = util.serverRequire(module, './Backend'); if (backendModule == null) { diff --git a/src/util.ts b/src/util.ts index 66c40448f..685d2e689 100644 --- a/src/util.ts +++ b/src/util.ts @@ -177,18 +177,40 @@ export function promisify(original) { return fn; } +/** + * Conditionally require module only if in server process. No-op when called in browser. + * + * @param module + * @param id + * @returns module or undefined + */ export function serverRequire(module, id) { if (!isServer) return; return module.require(id); } +/** + * Use plugin only if invoked in server process. + * + * @param module + * @param id + * @param options - Optional + * @returns + */ export function serverUse(module, id: string, options?: unknown) { if (!isServer) return this; var plugin = module.require(id); return this.use(plugin, options); } -export function use(plugin, options?: unknown) { +/** + * Use plugin + * + * @param plugin + * @param options - Optional options passed to plugin + * @returns + */ +export function use(plugin: (arg0: unknown, options?: unknown) => void, options?: unknown) { // Don't include a plugin more than once var plugins = this._plugins || (this._plugins = []); if (plugins.indexOf(plugin) === -1) { From ff5d0ca88ee9c236c889ab1bb67e75a62cacbfb3 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 12 Jun 2024 12:20:22 -0700 Subject: [PATCH 458/479] Update jsdoc; correct link refs; fix param names to match code --- src/Backend.ts | 21 +++++++++++++------- src/Model/Model.ts | 4 ++++ src/Model/collections.ts | 15 ++++++++++++-- src/Model/index.ts | 2 +- src/Model/mutators.ts | 17 ++++++++++++---- src/Model/paths.ts | 43 ++++++++++++++++++++++++++++++++++++++-- src/Model/ref.ts | 31 +++++++++++++++++++++++------ src/index.ts | 8 +++----- src/util.ts | 24 ++++++++++++++++++++++ 9 files changed, 138 insertions(+), 27 deletions(-) diff --git a/src/Backend.ts b/src/Backend.ts index 82d636663..32415dc45 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -1,16 +1,23 @@ import * as path from 'path'; import * as util from './util'; -import { ModelOptions, RootModel } from './Model/Model'; +import { type ModelOptions, RootModel } from './Model/Model'; import Backend = require('sharedb'); +export type BackendOptions = { modelOptions?: ModelOptions } & Backend.ShareDBOptions; + /** * RacerBackend extends ShareDb Backend */ export class RacerBackend extends Backend { racer: any; - modelOptions: any; + modelOptions: ModelOptions; - constructor(racer: any, options?: { modelOptions?: ModelOptions } & Backend.ShareDBOptions) { + /** + * + * @param racer - Racer instance + * @param options - Model and SharedB options + */ + constructor(racer: any, options?: BackendOptions) { super(options); this.racer = racer; this.modelOptions = options && options.modelOptions; @@ -23,11 +30,11 @@ export class RacerBackend extends Backend { /** * Create new `RootModel` * - * @param options - optional model options - * @param request - optional request context See {@link sharedb.listen} for details. + * @param options - Optional model options + * @param request - Optional request context See {@link Backend.listen} for details. * @returns a new root model */ - createModel(options?: ModelOptions, req?: any) { + createModel(options?: ModelOptions, request?: any) { if (this.modelOptions) { options = (options) ? util.mergeInto(options, this.modelOptions) : @@ -35,7 +42,7 @@ export class RacerBackend extends Backend { } var model = new RootModel(options); this.emit('model', model); - model.createConnection(this, req); + model.createConnection(this, request); return model; }; diff --git a/src/Model/Model.ts b/src/Model/Model.ts index 7595ed4b1..5befccc6d 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -30,6 +30,8 @@ type ModelInitFunction = (instance: RootModel, options: ModelOptions) => void; /** * Base class for Racer models + * + * @typeParam T - Type of data the Model contains */ export class Model { static INITS: ModelInitFunction[] = []; @@ -82,6 +84,8 @@ export class RootModel extends Model { /** * Model for some subset of the data + * + * @typeParam T - type of data the ChildModel contains. */ export class ChildModel extends Model { constructor(model: Model) { diff --git a/src/Model/collections.ts b/src/Model/collections.ts index 871bcf995..d8da68ed0 100644 --- a/src/Model/collections.ts +++ b/src/Model/collections.ts @@ -33,7 +33,19 @@ declare module './Model' { destroy(subpath?: Path): void; /** - * Gets the value located at this model's path or a relative subpath. + * Gets the value located at this model's path. + * + * If no value exists at the path, this returns `undefined`. + * + * _Note:_ The value is returned by reference, and object values should not + * be directly modified - use the Model mutator methods instead. The + * TypeScript compiler will enforce no direct modifications, but there are + * no runtime guards, which means JavaScript source code could still + * improperly make direct modifications. + */ + get(): ReadonlyDeep | undefined; + /** + * Gets the value located at a relative subpath. * * If no value exists at the path, this returns `undefined`. * @@ -45,7 +57,6 @@ declare module './Model' { * * @param subpath */ - get(): ReadonlyDeep | undefined; get(subpath?: Path): ReadonlyDeep | undefined; getCollection(collectionName: string): Collection; diff --git a/src/Model/index.ts b/src/Model/index.ts index 209cd3aa2..435e907c6 100644 --- a/src/Model/index.ts +++ b/src/Model/index.ts @@ -2,7 +2,7 @@ /// import { serverRequire } from '../util'; -export { Model, ChildModel, RootModel, ModelOptions, type UUID, type DefualtType } from './Model'; +export { Model, ChildModel, RootModel, type ModelOptions, type UUID, type DefualtType } from './Model'; export { ModelData } from './collections'; export { type Subscribable } from './subscriptions'; diff --git a/src/Model/mutators.ts b/src/Model/mutators.ts index 0912e0cb2..f26d27e1d 100644 --- a/src/Model/mutators.ts +++ b/src/Model/mutators.ts @@ -65,7 +65,7 @@ declare module './Model' { * If a callback is provided, it's called when the write is committed or * fails. * - * @param path - Optional Collection under which to add the document + * @param subpath - Optional Collection under which to add the document * @param value - Document to add * @param cb - Optional callback */ @@ -80,6 +80,7 @@ declare module './Model' { * If a callback is provided, it's called when the write is committed or * fails. * + * @typeParam S - Type of the data returned by delete operation * @param subpath * @returns the old value at the path */ @@ -90,6 +91,7 @@ declare module './Model' { * If a callback is provided, it's called when the write is committed or * fails. * + * @typeParam T - Type of the data returned by delete operation * @returns the old value at the path */ del(cb?: Callback): T | undefined; @@ -115,11 +117,17 @@ declare module './Model' { /** * Push a value to a model array * - * @param subpath * @param value * @returns the length of the array */ push(value: any): number; + /** + * Push a value to a model array at subpath + * + * @param subpath + * @param value + * @returns the length of the array + */ push(subpath: Path, value: any, cb?: ErrorCallback): number; pushPromised(value: any): Promise; pushPromised(subpath: Path, value: any): Promise; @@ -143,7 +151,7 @@ declare module './Model' { * * If a callback is provided, it's called when the write is committed or * fails. - * + * @typeParam V - type of data at subpath * @param subpath * @returns the removed item */ @@ -164,7 +172,8 @@ declare module './Model' { * If a callback is provided, it's called when the write is committed or * fails. * - * @param subpath + * @typeParam - Type of data targeted by remove operation + * @param subpath - Subpath to remove * @param index - 0-based index at which to start removing items * @param howMany - Number of items to remove. Defaults to `1`. * @returns array of the removed items diff --git a/src/Model/paths.ts b/src/Model/paths.ts index c6a31078b..43dada9c1 100644 --- a/src/Model/paths.ts +++ b/src/Model/paths.ts @@ -6,21 +6,60 @@ exports.mixin = {}; declare module './Model' { interface Model { + /** + * Returns a ChildModel scoped to the root path. + */ + at(): ChildModel; + /** * Returns a ChildModel scoped to a relative subpath under this model's path. * + * @typeParam S - type of data at subpath * @param subpath */ - at(): ChildModel; at(subpath: PathLike): ChildModel; - + + /** + * Check if subpath is a PathLike + * + * @param subpath + * @returns boolean + */ isPath(subpath: PathLike): boolean; + leaf(path: string): string; + + /** + * Get the parent {levels} up from current model or root model + * @param levels - number of levels to traverse the tree + * @returns parent or root model + */ parent(levels?: number): Model; + + /** + * Get full path to given subpath + * + * @param subpath - PathLike subpath + */ path(subpath?: PathLike): string; + + /** + * Returns a ChildModel scoped to the root path. + * + * @returns ChildModel + */ scope(): ChildModel; + + /** + * Returns a ChildModel scoped to an absolute path. + * + * @typeParam S - Type of data at subpath + * @param subpath - Path of GhildModel to scope to + * @returns ChildModel + */ scope(subpath: Path): ChildModel; + /** @private */ _splitPath(subpath: PathLike): string[]; } } diff --git a/src/Model/ref.ts b/src/Model/ref.ts index 4149bf8d2..c349d03d1 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -8,9 +8,20 @@ import type { Path, PathLike, Segments } from '../types'; type Refable = string | number | Model | Query | Filter; export interface RefOptions { + /** + * If true, indicies will be updated. + */ updateIndices: boolean; } +export interface RefListOptions { + /** + * If true, then objects from the source collection will be deleted if the + * corresponding item is removed from the refList's output path. + */ + deleteRemoved: boolean, +} + declare module './Model' { interface Model { /** @@ -24,18 +35,26 @@ declare module './Model' { * object, where each id string key maps to an object value with matching * `id` property. * @param idsPath - Path to an array of string ids - * @param options - * @param options.deleteRemoved - If true, then objects from the source - * collection will be deleted if the corresponding item is removed from - * the refList's output path + * @param options - Optional * * @see https://derbyjs.github.io/derby/models/refs */ - refList(outputPath: PathLike, collectionPath: PathLike, idsPath: PathLike, options?: { deleteRemoved?: boolean }): ChildModel; + refList(outputPath: PathLike, collectionPath: PathLike, idsPath: PathLike, options?: RefListOptions): ChildModel; _canRefTo(value: Refable): boolean; // _canRefTo(from: Segments, to: Segments, options: RefOptions): boolean; + /** + * Creates a reference for this model pointing to another path `to`. Like a + * symlink, any reads/writes on this `to` ref will work as if they were done on + * `path` directly. + * + * @param to - Location that the reference points to + * @return a model scoped to `path` + * + * @see https://derbyjs.github.io/derby/models/refs + */ + ref(to: PathLike): ChildModel; /** * Creates a reference at `path` pointing to another path `to`. Like a * symlink, any reads/writes on `path` will work as if they were done on @@ -44,11 +63,11 @@ declare module './Model' { * @param path - Location at which to create the reference. This must be * under a local collection, typically `'_page'` or a component model. * @param to - Location that the reference points to + * @params options - Optional {@link RefOptions} * @return a model scoped to `path` * * @see https://derbyjs.github.io/derby/models/refs */ - ref(to: PathLike): ChildModel; ref(path: PathLike, to: PathLike, options?: RefOptions): ChildModel; _ref(from: Segments, to: Segments, options?: RefOptions): void; diff --git a/src/index.ts b/src/index.ts index 15e2d8c93..8f5dd48a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,9 @@ import { Racer } from './Racer'; import * as util from './util'; import type { ShareDBOptions } from 'sharedb'; -import { type RacerBackend } from './Backend'; -import { RootModel, type ModelOptions } from './Model'; +export { type RacerBackend, type BackendOptions } from './Backend'; +import { RootModel } from './Model'; +import { type BackendOptions } from './Backend'; export { Query } from './Model/Query'; export { Model, ChildModel, ModelData, type UUID, type Subscribable, type DefualtType, type ModelOptions } from './Model'; @@ -15,11 +16,8 @@ export * as util from './util'; const { use, serverUse } = util; -export type BackendOptions = { modelOptions?: ModelOptions } & ShareDBOptions; - export { Racer, - RacerBackend, RootModel, use, serverUse, diff --git a/src/util.ts b/src/util.ts index 685d2e689..f593de63c 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,16 @@ + +/** @private */ export const deepEqual = require('fast-deep-equal'); + +/** + * Checks process.title is not equal to 'browser' + * + * Set as 'browser' via build tools (e.g. webpack) to package + * browser specific code to bundle + */ export const isServer = process.title !== 'browser'; +/** @private */ export function asyncGroup(cb) { var group = new AsyncGroup(cb); return function asyncGroupAdd() { @@ -39,17 +49,20 @@ class AsyncGroup { } } +/** @private */ function castSegment(segment: string | number): string | number { return (typeof segment === 'string' && isArrayIndex(segment)) ? +segment // sneaky op to convert numeric string to number : segment; } +/** @private */ export function castSegments(segments: Readonly>) { // Cast number path segments from strings to numbers return segments.map(segment => castSegment(segment)); } +/** @private */ export function contains(segments, testSegments) { for (var i = 0; i < segments.length; i++) { if (segments[i] !== testSegments[i]) return false; @@ -57,6 +70,7 @@ export function contains(segments, testSegments) { return true; } +/** @private */ export function copy(value) { if (value instanceof Date) return new Date(value); if (typeof value === 'object') { @@ -67,6 +81,7 @@ export function copy(value) { return value; } +/** @private */ export function copyObject(object) { var out = new object.constructor(); for (var key in object) { @@ -77,6 +92,7 @@ export function copyObject(object) { return out; } +/** @private */ export function deepCopy(value) { if (value instanceof Date) return new Date(value); if (typeof value === 'object') { @@ -99,19 +115,23 @@ export function deepCopy(value) { return value; } +/** @private */ export function equal(a, b) { return (a === b) || (equalsNaN(a) && equalsNaN(b)); } +/** @private */ export function equalsNaN(x) { // eslint-disable-next-line no-self-compare return x !== x; } +/** @private */ export function isArrayIndex(segment: string): boolean { return (/^[0-9]+$/).test(segment); } +/** @private */ export function lookup(segments: string[], value: unknown): unknown { if (!segments) return value; @@ -122,6 +142,7 @@ export function lookup(segments: string[], value: unknown): unknown { return value; } +/** @private */ export function mayImpactAny(segmentsList: string[][], testSegments: string[]) { for (var i = 0, len = segmentsList.length; i < len; i++) { if (mayImpact(segmentsList[i], testSegments)) return true; @@ -129,6 +150,7 @@ export function mayImpactAny(segmentsList: string[][], testSegments: string[]) { return false; } +/** @private */ export function mayImpact(segments: string[], testSegments: string[]): boolean { var len = Math.min(segments.length, testSegments.length); for (var i = 0; i < len; i++) { @@ -137,6 +159,7 @@ export function mayImpact(segments: string[], testSegments: string[]): boolean { return true; } +/** @private */ export function mergeInto(to, from) { for (var key in from) { to[key] = from[key]; @@ -144,6 +167,7 @@ export function mergeInto(to, from) { return to; } +/** @private */ export function promisify(original) { if (typeof original !== 'function') { throw new TypeError('The "original" argument must be of type Function'); From 5c2330d3c8e4f8f0784fdc71fbb0fe78e1629f06 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 12 Jun 2024 12:30:55 -0700 Subject: [PATCH 459/479] Add typedoc deps and plugin for excluding underscore prefixed symbols from docs --- package.json | 4 ++++ typedoc.json | 17 +++++++++++++++++ typedocExcludeUnderscore.mjs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 typedoc.json create mode 100644 typedocExcludeUnderscore.mjs diff --git a/package.json b/package.json index b8608e2bd..e4b2e16fe 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ ], "scripts": { "build": "node_modules/.bin/tsc", + "docs": "node_modules/.bin/typedoc", "lint": "eslint .", "lint:fix": "eslint --fix .", "pretest": "npm run build", @@ -41,6 +42,9 @@ "eslint-config-google": "^0.14.0", "mocha": "^9.1.3", "nyc": "^15.1.0", + "typedoc": "^0.25.13", + "typedoc-plugin-mdn-links": "^3.1.28", + "typedoc-plugin-missing-exports": "^2.2.0", "typescript": "^5.1.3" }, "bugs": { diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 000000000..b6eb77476 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,17 @@ +{ + "entryPoints": ["src/index.ts"], + "excludeNotDocumented": false, + "plugin":[ + "typedoc-plugin-mdn-links", + "typedoc-plugin-missing-exports", + "./typedocExcludeUnderscore.mjs" + ], + "out": "docs/api", + "visibilityFilters": { + "protected": false, + "private": false, + "inherited": true, + "external": false + }, + "excludePrivate": true + } \ No newline at end of file diff --git a/typedocExcludeUnderscore.mjs b/typedocExcludeUnderscore.mjs new file mode 100644 index 000000000..22d1b7087 --- /dev/null +++ b/typedocExcludeUnderscore.mjs @@ -0,0 +1,30 @@ +import { Converter, ReflectionFlag, ReflectionKind } from "typedoc"; +import camelCase from "camelcase"; + +/** + * @param {Readonly} app + */ +export function load(app) { + /** + * Create declaration event handler that sets symbols with underscore-prefixed names + * to private to exclude from generated documentation. + * + * Due to "partial class" style of code in use, otherwise private properties and methods - + * prefixed with underscore - are effectively declared public so they can be accessed in other + * files used to build class - e.g. Model. This marks anything prefixed with an underscore and + * no doc comment as private. + * + * @param {import('typedoc').Context} context + * @param {import('typedoc').DeclarationReflection} reflection + */ + function handleCreateDeclaration(context, reflection) { + if (!reflection.name.startsWith('_')) { + return; + } + if (!reflection.comment) { + reflection.setFlag(ReflectionFlag.Private); + } + } + + app.converter.on(Converter.EVENT_CREATE_DECLARATION, handleCreateDeclaration); +} \ No newline at end of file From d33c5065404b4d299371f60d61218ab4e5a796a7 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 12 Jun 2024 16:56:25 -0700 Subject: [PATCH 460/479] Additional jsdoc; subscriptions, ModelOptions --- src/Model/Model.ts | 10 ++ src/Model/subscriptions.ts | 206 ++++++++++++++++++++++++++++++++++--- src/util.ts | 2 + 3 files changed, 202 insertions(+), 16 deletions(-) diff --git a/src/Model/Model.ts b/src/Model/Model.ts index 5befccc6d..c07777c47 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -11,14 +11,24 @@ export type DefualtType = unknown; declare module './Model' { interface DebugOptions { + /** Enables browser side logging of ShareDB operations */ debugMutations?: boolean, + /** Disable submitting of local operations to remote backend */ disableSubmit?: boolean, remoteMutations?: boolean, } interface ModelOptions { + /** see {@link DebugOptions} */ debug?: DebugOptions; + /** Ensure read-only access of model data */ fetchOnly?: boolean; + /** + * Delay in milliseconds before actuallyunloading data after `unload` called + * + * Default ot 0 on server, and 1000ms for browser. Runtime value can be inspected + * on {@link RootModel.unloadDelay} + */ unloadDelay?: number; bundleTimeout?: number; } diff --git a/src/Model/subscriptions.ts b/src/Model/subscriptions.ts index 2cb7bd785..ca6be621b 100644 --- a/src/Model/subscriptions.ts +++ b/src/Model/subscriptions.ts @@ -16,21 +16,69 @@ declare module './Model' { /** * Retrieve data from the server, loading it into the model. * - * @param items - * @param cb + * @param items - Items to fetch + * @param cb - Callback called when operation completed * * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model */ fetch(items: Subscribable[], cb?: ErrorCallback): Model; + /** + * Retrieve data from the server, loading it into the model. + * + * @param item - Item to fetch + * @param cb - Callback called when operation completed + * + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model + */ fetch(item: Subscribable, cb?: ErrorCallback): Model; + /** + * Retrieve data from the server, loading it into the model. + * + * @param cb - Callback called when operation completed + * + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model + */ fetch(cb?: ErrorCallback): Model; + /** + * Promised version of {@link Model.fetch}. Instead of a callback, returns a promise + * that is resolved when operation completed + * + * @param items + */ fetchPromised(items: Subscribable[]): Promise; + /** + * Promised version of {@link Model.fetch}. Instead of a callback, returns a promise + * that is resolved when operation completed + * + * @param item + */ fetchPromised(item: Subscribable): Promise; + /** + * Promised version of {@link Model.fetch}. Instead of a callback, returns a promise + * that is resolved when operation completed + */ fetchPromised(): Promise; + /** + * Retrieve data from the server, loading it into the model. + * + * @param collecitonName - Name of colleciton to load item to + * @param id - Id of doc to load + * @param callback - Callback called when operation completed + * + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model + */ fetchDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; + /** + * Promised version of {@link Model.fetchDoc}. Instead of a callback, returns a promise + * that is resolved when operation completed + * + * @param collecitonName - Name of colleciton to load item to + * @param id - Id of doc to load + */ fetchDocPromised(collecitonName: string, id: string): Promise; + fetchOnly: boolean; /** @@ -40,60 +88,186 @@ declare module './Model' { * * Any item that's already subscribed will not result in a network call. * - * @param items - * @param cb + * @param items - Item to subscribe to + * @param cb - Callback called when operation completed * * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model */ subscribe(items: Subscribable[], cb?: ErrorCallback): Model; + /** + * Retrieve data from the server, loading it into the model. In addition, + * subscribe to the items, such that updates from any other client will + * automatically get reflected in this client's model. + * + * Any item that's already subscribed will not result in a network call. + * + * @param item - Item to subscribe to + * @param cb - Callback called when operation completed + * + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model + */ subscribe(item: Subscribable, cb?: ErrorCallback): Model; + /** + * Retrieve data from the server, loading it into the model. In addition, + * subscribe to the items, such that updates from any other client will + * automatically get reflected in this client's model. + * + * Any item that's already subscribed will not result in a network call. + * + * @param cb - Callback called when operation completed + * + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model + */ subscribe(cb?: ErrorCallback): Model; - + /** + * Promised version of {@link Model.subscribe}. Instead of a callback, returns a promise + * that is resolved when operation completed + * + * @param items - Items to subscribe to + */ subscribePromised(items: Subscribable[]): Promise; + /** + * Promised version of {@link Model.subscribe}. Instead of a callback, returns a promise + * that is resolved when operation completed + * + * @param item - Item to subscribe to + */ subscribePromised(item: Subscribable): Promise; + /** + * Promised version of {@link Model.subscribe}. Instead of a callback, returns a promise + * that is resolved when operation completed + */ subscribePromised(): Promise; subscribeDoc(collecitonName: string, id: string, callback?: ErrorCallback): void; subscribeDocPromised(collecitonName: string, id: string): Promise; /** - * The reverse of `#fetch`, marking the items as no longer needed in the + * The reverse of {@link Model.fetch}, marking the items as no longer needed in the * model. * - * @param items - * @param cb + * @param items - Items to unfetch + * @param cb - Optional Called after operation completed * * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model */ unfetch(items: Subscribable[], cb?: ErrorCallback): Model; + /** + * The reverse of {@link Model.fetch}, marking the items as no longer needed in the + * model. + * + * @param item - Item to unfetch + * @param cb - Optional Called after operation completed + * + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model + */ unfetch(item: Subscribable, cb?: ErrorCallback): Model; + /** + * The reverse of {@link Model.fetch}, marking the items as no longer needed in the + * model. + * + * @param cb - Optional Called after operation completed + * + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model + */ unfetch(cb?: ErrorCallback): Model; + /** + * Promised unfetch. See {@link Model.unfetch}. Instead of a callback, returns a promise + * that is resolved when operation completed + * + * @param items - Items to unfetch + * @returns Promise + */ unfetchPromised(items: Subscribable[]): Promise; + /** + * Promised unfetch. See {@link Model.unfetch}. Instead of a callback, returns a promise + * that is resolved when operation completed + * + * @param item - Item to unfetch + * @returns Promise + */ unfetchPromised(item: Subscribable): Promise; + /** + * Promised {@link Model.unfetch}. Instead of taking a callback, returns a promise + * that is resolved when operation completed + * + * @returns Promise + */ unfetchPromised(): Promise; - unfetchDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; - unfetchDocPromised(collecitonName: string, id: string): Promise; + /** + * Unfetch a document give collection name and document id + * + * @param collectionName - Collection name + * @param id - Document id to be unfeched + */ + unfetchDoc(collectionName: string, id: string, callback?: (err?: Error, count?: number) => void): void; + /** + * Promised {@link Model.unfetchDoc}. Instead of taking a callback, returns a promise + * that is resolved when operation completed + * + * @param collectionName - Collection name + * @param id - Document id to be unfeched + * @returns Promise + */ + unfetchDocPromised(collectionName: string, id: string): Promise; + /** + * Delay in milliseconds before model data actually unloaded after call to {@link Model.unload} + */ unloadDelay: number; - /** - * The reverse of `#subscribe`, marking the items as no longer needed in the + * The reverse of {@link Model.subscribe}, marking the items as no longer needed in the * model. * - * @param items - * @param cb + * @param items - The items to unsubscribe + * @param cb - Optional Called after operation completed * * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model */ unsubscribe(items: Subscribable[], cb?: ErrorCallback): Model; + /** + * The reverse of {@link Model.subscribe}, marking the items as no longer needed in the + * model. + * + * @param item - The item to unsubscribe + * @param cb - Optional Called after operation completed + * + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model + */ unsubscribe(item: Subscribable, cb?: ErrorCallback): Model; + /** + * The reverse of {@link Model.subscribe}, marking the items as no longer needed in the + * model. + * + * @param cb - Optional Called after operation completed + * + * @see https://derbyjs.github.io/derby/models/backends#loading-data-into-a-model + */ unsubscribe(cb?: ErrorCallback): Model; + /** + * Promised version of {@link Model.unsubscribe}. Instead of taking a callback, returns a promise + * that is resolved when operation completed + */ unsubscribePromised(): Promise; - unsubscribeDoc(collecitonName: string, id: string, callback?: (err?: Error, count?: number) => void): void; - unsubscribeDocPromised(collecitonName: string, id: string): Promise; + + /** + * Unsubscribe document by collection name and id + * + * @param collectionName - Name of collection containting document + * @param id - Document id to unsubscribe + * @param callback - Optional Called after operation completed + */ + unsubscribeDoc(collectionName: string, id: string, callback?: (err?: Error, count?: number) => void): void; + /** + * Promised version of {@link Model.unsubscribeDoc} + * + * @param collectionName - Name of collection containting document + * @param id - Document id to unsbscribe + */ + unsubscribeDocPromised(collectionName: string, id: string): Promise; _fetchedDocs: CollectionCounter; _forSubscribable(argumentsObject: any, method: any): void; diff --git a/src/util.ts b/src/util.ts index f593de63c..9774563fb 100644 --- a/src/util.ts +++ b/src/util.ts @@ -7,6 +7,8 @@ export const deepEqual = require('fast-deep-equal'); * * Set as 'browser' via build tools (e.g. webpack) to package * browser specific code to bundle + * + * see {@link https://github.com/derbyjs/derby-webpack/blob/main/createConfig.js#L95 | derby-webpack} */ export const isServer = process.title !== 'browser'; From 474d1ec41e6708618034f78841e6ba24c859fd19 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 12 Jun 2024 17:06:14 -0700 Subject: [PATCH 461/479] Link model and query --- src/Model/subscriptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/subscriptions.ts b/src/Model/subscriptions.ts index ca6be621b..420d6144e 100644 --- a/src/Model/subscriptions.ts +++ b/src/Model/subscriptions.ts @@ -7,7 +7,7 @@ const UnloadEvent = mutationEvents.UnloadEvent; const promisify = util.promisify; /** - * A path string, a `Model`, or a `Query`. + * A path string, a {@link Model}, or a {@link Query}. */ export type Subscribable = string | Model | Query; From 7a8733ca9d9a5e040fe3ae1d7db10539b4c089ad Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Wed, 12 Jun 2024 17:06:33 -0700 Subject: [PATCH 462/479] Change output doc directory --- typedoc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typedoc.json b/typedoc.json index b6eb77476..f7968cc07 100644 --- a/typedoc.json +++ b/typedoc.json @@ -6,7 +6,7 @@ "typedoc-plugin-missing-exports", "./typedocExcludeUnderscore.mjs" ], - "out": "docs/api", + "out": "docs", "visibilityFilters": { "protected": false, "private": false, From 511667b8d32a250b52109c5d000fe9d94f475a33 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 14 Jun 2024 12:27:51 -0700 Subject: [PATCH 463/479] Create gh-pages.yml Setup gh-pages action for publishing typedoc generated API docs --- .github/workflows/gh-pages.yml | 62 ++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/gh-pages.yml diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 000000000..d4393bf1f --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,62 @@ +# Based on https://github.com/actions/starter-workflows/blob/main/pages/jekyll.yml +name: Build typedoc docs site, Deploy to Pages when on default branch + +env: + DOCS_DIR: docs + +on: + # Run workflow on any branch push. + # Conditionals are used to only trigger deploy on the default branch. + push: + # Uncomment to only run on specific branch pushes. + # branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment per branch, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow the deployments to complete. +concurrency: + group: "pages-${{ github.ref }}" + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Setup Pages + id: pages + uses: actions/configure-pages@v4 + - name: Build with Typedoc + run: npm i && npm run docs + - name: Upload artifact + if: github.ref == 'refs/heads/master' # Only upload when on default branch + uses: actions/upload-pages-artifact@v3 + with: + path: "./${{ env.DOCS_DIR }}" + + # Deployment job + deploy: + if: github.ref == 'refs/heads/master' # Only deploy when on default branch + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From 42a44e27aaeb5fa633a3e03ef43ecbd0063d283c Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 14 Jun 2024 13:24:05 -0700 Subject: [PATCH 464/479] Move ReflitOptions --- src/Backend.ts | 2 +- src/Model/Model.ts | 4 ++-- src/Model/ref.ts | 9 +-------- src/Model/refList.ts | 16 ++++++++++++---- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Backend.ts b/src/Backend.ts index 32415dc45..2282b34f4 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -47,7 +47,7 @@ export class RacerBackend extends Backend { }; /** - * Model middleware that creates and attaches a `Model` to the `request` + * Model middleware that creates and attaches a {@link Model} to the `request` * and attaches listeners to response for closing model on response completion * * @returns an Express middleware function diff --git a/src/Model/Model.ts b/src/Model/Model.ts index c07777c47..675a328a2 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -24,9 +24,9 @@ declare module './Model' { /** Ensure read-only access of model data */ fetchOnly?: boolean; /** - * Delay in milliseconds before actuallyunloading data after `unload` called + * Delay in milliseconds before actually unloading data after `unload` called * - * Default ot 0 on server, and 1000ms for browser. Runtime value can be inspected + * Default to 0 on server, and 1000ms for browser. Runtime value can be inspected * on {@link RootModel.unloadDelay} */ unloadDelay?: number; diff --git a/src/Model/ref.ts b/src/Model/ref.ts index c349d03d1..530aff53b 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -4,6 +4,7 @@ import { Model } from './Model'; import { type Filter } from './filter'; import { type Query } from './Query'; import type { Path, PathLike, Segments } from '../types'; +import { type RefListOptions } from './refList'; type Refable = string | number | Model | Query | Filter; @@ -14,14 +15,6 @@ export interface RefOptions { updateIndices: boolean; } -export interface RefListOptions { - /** - * If true, then objects from the source collection will be deleted if the - * corresponding item is removed from the refList's output path. - */ - deleteRemoved: boolean, -} - declare module './Model' { interface Model { /** diff --git a/src/Model/refList.ts b/src/Model/refList.ts index 74e7d2cc0..e4cfababf 100644 --- a/src/Model/refList.ts +++ b/src/Model/refList.ts @@ -2,10 +2,18 @@ import { EventListenerTree } from './EventListenerTree'; import { EventMapTree } from './EventMapTree'; import { Model } from './Model'; +export interface RefListOptions { + /** + * If true, then objects from the source collection will be deleted if the + * corresponding item is removed from the refList's output path. + */ + deleteRemoved: boolean, +} + declare module './Model' { interface Model { - refList(to: any, ids: any, options?: any): RefList; - refList(from: any, to: any, ids: any, options?: any): RefList; + refList(to: any, ids: any, options?: RefListOptions): RefList; + refList(from: any, to: any, ids: any, options?: RefListOptions): RefList; } } @@ -385,10 +393,10 @@ export class RefList{ fromSegments: any; toSegments: any; idsSegments: any; - options: any; + options?: RefListOptions; deleteRemoved: boolean; - constructor(model: Model, from, to, ids, options) { + constructor(model: Model, from, to, ids, options?: RefListOptions) { this.model = model && model.pass({$refList: this}); this.from = from; this.to = to; From c4c53a0489bf0323d911966cde08f1525314fdd0 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 14 Jun 2024 14:30:49 -0700 Subject: [PATCH 465/479] Remove alpha disclaimer; remove contributor info (readdress later w comprehensive list) --- README.md | 42 +++--------------------------------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index fe3e7f0b9..ca14e6272 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,7 @@ # Racer - -[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) - Racer is a realtime model synchronization engine for Node.js. By leveraging [ShareDB](https://github.com/share/sharedb), multiple users can interact with the same data in realtime via Operational Transformation, a sophisticated conflict resolution algorithm that works in realtime and with offline clients. ShareDB also supports PubSub across multiple servers for horizontal scaling. Clients can express data subscriptions and fetches in terms of queries and specific documents, so different clients can be subscribed to different overlapping sets of data. On top of this sophisticated backend, Racer provides a simple model and event interface for writing application logic. - [![Build Status](https://travis-ci.org/derbyjs/racer.svg?branch=master)](https://travis-ci.org/derbyjs/racer) - [![Coverage Status](https://coveralls.io/repos/github/derbyjs/racer/badge.svg?branch=master)](https://coveralls.io/github/derbyjs/racer?branch=master) - -## Disclaimer - -Racer is alpha software. If you are interested in contributing, please reach out to [Nate](https://github.com/nateps). - ## Demos There are currently two demos, which are included in the [racer-examples](https://github.com/derbyjs/racer-examples) repo. @@ -40,16 +30,9 @@ There are currently two demos, which are included in the [racer-examples](https: * **Solr queries** – (Under development) A Solr adapter will support updating Solr indices as data change and queries for realtime updated search results. -## Future features - - * **Browser local storage** – Pending changes and offline model data will also sync to HTML5 localStorage for persistent offline usage. - - * **Validation** – An implementation of shared and non-shared schema-based validation is planned. - - ## Installation -Racer requires [Node v0.10](http://nodejs.org/). You will also need to have a [MongoDB](http://docs.mongodb.org/manual/installation/) and a [Redis](http://redis.io/download) server running on your machine. The examples will connect via the default configurations. +Racer requires [Node v16](http://nodejs.org/). You will also need to have a [MongoDB](http://docs.mongodb.org/manual/installation/) and a [Redis](http://redis.io/download) server running on your machine. The examples will connect via the default configurations. ``` $ npm install racer @@ -67,10 +50,10 @@ $ npm test Racer can be used independently as shown in the examples, but Racer and Derby are designed to work especially well together. Racer can also be used along with other MVC frameworks, such as Angular. -For now, Racer is mostly documented along with Derby. See the Derby [model docs](http://derbyjs.com/docs/derby-0.6/models). +For now, Racer is mostly documented along with Derby. See the Derby [model docs](https://derbyjs.github.io/derby/models). ### MIT License -Copyright (c) 2011 by Brian Noguchi and Nate Smith +Copyright (c) 2024 by Brian Noguchi and Nate Smith Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -90,22 +73,3 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## Contributors ✨ - -Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): - - - - - - - - -

Craig Beck

⚠️ 💻
- - - - - - -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! \ No newline at end of file From 29b09cd4fcce4aa5e47f91e7452b4ec47f335152 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 18 Jul 2024 13:52:46 -0700 Subject: [PATCH 466/479] [TS typings] Allow model.ref's to param to be a Query or Filter, to match runtime behavior --- src/Model/ref.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/ref.ts b/src/Model/ref.ts index 530aff53b..a9ebfaf96 100644 --- a/src/Model/ref.ts +++ b/src/Model/ref.ts @@ -47,7 +47,7 @@ declare module './Model' { * * @see https://derbyjs.github.io/derby/models/refs */ - ref(to: PathLike): ChildModel; + ref(to: Refable): ChildModel; /** * Creates a reference at `path` pointing to another path `to`. Like a * symlink, any reads/writes on `path` will work as if they were done on @@ -61,7 +61,7 @@ declare module './Model' { * * @see https://derbyjs.github.io/derby/models/refs */ - ref(path: PathLike, to: PathLike, options?: RefOptions): ChildModel; + ref(path: PathLike, to: Refable, options?: RefOptions): ChildModel; _ref(from: Segments, to: Segments, options?: RefOptions): void; /** From 2d2de0907aac7f17f8aa34c7539e984c7ee0286e Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Thu, 18 Jul 2024 16:00:22 -0700 Subject: [PATCH 467/479] Deprecate model fn; fix type for fn args --- src/Model/fn.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Model/fn.ts b/src/Model/fn.ts index 459e09157..01b17a883 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -7,7 +7,7 @@ var util = require('../util'); class NamedFns { } -type StartFnParam = string | number | boolean | null | undefined | ReadonlyDeep; +type StartFnParam = unknown; type ModelFn = (...inputs: Ins) => Out | @@ -90,6 +90,10 @@ declare module './Model' { * It's not recommended to use this in most cases. Instead, to share reactive functions, * have the components import a shared function to pass to `model.start`. * + * @deprecated The use of named funcitons is deprecated. With typescript and modern tooling + * you get better type information, code navigation, and refactoring support that is lost + * when using named functions. + * * @param name name of the function to define * @param fn either a reactive function that accepts inputs and returns output, or * a `{ get: Function; set: Function }` object defining a two-way reactive function From aaad8dc2666e30c653be2967d46da45c3e9cfbe9 Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 19 Jul 2024 11:39:45 -0700 Subject: [PATCH 468/479] Update doc comment w suggested Co-authored-by: Eric Hwang --- src/Model/fn.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Model/fn.ts b/src/Model/fn.ts index 01b17a883..6d15b9ff9 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -90,9 +90,8 @@ declare module './Model' { * It's not recommended to use this in most cases. Instead, to share reactive functions, * have the components import a shared function to pass to `model.start`. * - * @deprecated The use of named funcitons is deprecated. With typescript and modern tooling - * you get better type information, code navigation, and refactoring support that is lost - * when using named functions. + * @deprecated The use of named functions is deprecated. Instead, to share a reactive function, + * you should export it and then require/import it into each file that needs to use it. * * @param name name of the function to define * @param fn either a reactive function that accepts inputs and returns output, or From 3c7a0146a2217898801b09dd445c967998ec3cdd Mon Sep 17 00:00:00 2001 From: Craig Beck Date: Fri, 19 Jul 2024 14:02:20 -0700 Subject: [PATCH 469/479] 2.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b78e733e..191547d78 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.1.1", + "version": "2.2.0", "main": "./lib/index.js", "files": [ "lib/*" From 669dc87d5ffb4530ef5e09ca1dabeced8303ba95 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 22 Jul 2024 14:25:40 -0700 Subject: [PATCH 470/479] [TS typings] Fix typing for util.use(plugin, options) to allow strongly typed plugin functions Resolves error introduced in racer@2.2.0 where Derby app.use plugins had TS compile errors --- src/util.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/util.ts b/src/util.ts index 9774563fb..4d1a276f0 100644 --- a/src/util.ts +++ b/src/util.ts @@ -229,6 +229,8 @@ export function serverUse(module, id: string, options?: unknown) { return this.use(plugin, options); } +type Plugin = (pluginHost: T, options: O) => void; + /** * Use plugin * @@ -236,9 +238,9 @@ export function serverUse(module, id: string, options?: unknown) { * @param options - Optional options passed to plugin * @returns */ -export function use(plugin: (arg0: unknown, options?: unknown) => void, options?: unknown) { +export function use(this: T, plugin: Plugin, options?: O) { // Don't include a plugin more than once - var plugins = this._plugins || (this._plugins = []); + var plugins = (this as any)._plugins || ((this as any)._plugins = []); if (plugins.indexOf(plugin) === -1) { plugins.push(plugin); plugin(this, options); From cd397f6cee132e23da46e1e84c23e88724fbc7ce Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 22 Jul 2024 14:29:42 -0700 Subject: [PATCH 471/479] Fix errors with TS 5.5, switch typescript to '~' dependency specifier since TypeScript doesn't use semver - Remove noImplicitUseStrict compiler flag, since TS 5.5 no longer supports it - Put preserve="true" on triple-slash reference directives used to load types for server-only model methods, since TS 5.5 strips the directives without the new preserve attribute --- package.json | 2 +- src/Model/index.ts | 4 ++-- tsconfig.json | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 191547d78..68bb3cde6 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "typedoc": "^0.25.13", "typedoc-plugin-mdn-links": "^3.1.28", "typedoc-plugin-missing-exports": "^2.2.0", - "typescript": "^5.1.3" + "typescript": "~5.5" }, "bugs": { "url": "https://github.com/derbyjs/racer/issues" diff --git a/src/Model/index.ts b/src/Model/index.ts index 435e907c6..9c7e143c0 100644 --- a/src/Model/index.ts +++ b/src/Model/index.ts @@ -1,5 +1,5 @@ -/// -/// +/// +/// import { serverRequire } from '../util'; export { Model, ChildModel, RootModel, type ModelOptions, type UUID, type DefualtType } from './Model'; diff --git a/tsconfig.json b/tsconfig.json index f75922da7..2fa026cdf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,8 @@ { "compilerOptions": { "allowJs": true, - "ignoreDeprecations": "5.0", "lib":[], "module": "CommonJS", - "noImplicitUseStrict": true, "outDir": "lib", "target": "ES5", "sourceMap": false, From db2917cc20b23e3e39bfbc37a29dabf870e4c056 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 22 Jul 2024 14:42:38 -0700 Subject: [PATCH 472/479] [docs] Update to typedoc 0.26, for TS 5.5 compatibility --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 68bb3cde6..8bd9eea22 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,9 @@ "eslint-config-google": "^0.14.0", "mocha": "^9.1.3", "nyc": "^15.1.0", - "typedoc": "^0.25.13", + "typedoc": "^0.26.5", "typedoc-plugin-mdn-links": "^3.1.28", - "typedoc-plugin-missing-exports": "^2.2.0", + "typedoc-plugin-missing-exports": "^3.0.0", "typescript": "~5.5" }, "bugs": { From c2baad308f0bd47c96978cd2269ef9fa0b52d72a Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 22 Jul 2024 14:51:39 -0700 Subject: [PATCH 473/479] 2.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8bd9eea22..8bfb3119e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.2.0", + "version": "2.2.1", "main": "./lib/index.js", "files": [ "lib/*" From f1ea581e34d6a575d3498a750e6495cf2e5b7f6b Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 22 Jul 2024 15:36:43 -0700 Subject: [PATCH 474/479] Use typescript@5.4 without "use strict;" directives, instead of latest typescript@5.5 The "use strict;" appears to cause some issues with LocalDoc in certain cases. Pinning to TS 5.4 now to get things fixed, will investigate an isolated repro and long-term fix later. --- package.json | 2 +- tsconfig.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8bfb3119e..fdf5c4397 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "typedoc": "^0.26.5", "typedoc-plugin-mdn-links": "^3.1.28", "typedoc-plugin-missing-exports": "^3.0.0", - "typescript": "~5.5" + "typescript": "~5.4.5" }, "bugs": { "url": "https://github.com/derbyjs/racer/issues" diff --git a/tsconfig.json b/tsconfig.json index 2fa026cdf..f75922da7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,10 @@ { "compilerOptions": { "allowJs": true, + "ignoreDeprecations": "5.0", "lib":[], "module": "CommonJS", + "noImplicitUseStrict": true, "outDir": "lib", "target": "ES5", "sourceMap": false, From e723582144257e319832c065e0689336012a7abd Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 22 Jul 2024 15:36:51 -0700 Subject: [PATCH 475/479] 2.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fdf5c4397..75650438a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.2.1", + "version": "2.2.2", "main": "./lib/index.js", "files": [ "lib/*" From 2cd4ebffbee70bd2cf551c7a59a19c05fe04e34b Mon Sep 17 00:00:00 2001 From: Sophia Sam Date: Wed, 11 Sep 2024 11:14:09 -0400 Subject: [PATCH 476/479] Fix model.start type for 2-way funcs --- src/Model/fn.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Model/fn.ts b/src/Model/fn.ts index 6d15b9ff9..f0d215c5d 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -10,10 +10,10 @@ class NamedFns { } type StartFnParam = unknown; type ModelFn = - (...inputs: Ins) => Out | + ((...inputs: Ins) => Out) | { - get(...inputs: Ins): Out, - set(output: Out, ...inputs: Ins): void, + get(...inputs: Ins): Out; + set(output: Out, ...inputs: Ins): {[key: number] : Ins[number]} | Ins[] | null; }; interface ModelStartOptions { From f09052a4a17b2092c9f60dd6e65478afa45143ac Mon Sep 17 00:00:00 2001 From: Sophia Sam Date: Wed, 11 Sep 2024 11:25:56 -0400 Subject: [PATCH 477/479] Change to commas --- src/Model/fn.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Model/fn.ts b/src/Model/fn.ts index f0d215c5d..68a72a320 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -12,8 +12,8 @@ type StartFnParam = unknown; type ModelFn = ((...inputs: Ins) => Out) | { - get(...inputs: Ins): Out; - set(output: Out, ...inputs: Ins): {[key: number] : Ins[number]} | Ins[] | null; + get(...inputs: Ins): Out, + set(output: Out, ...inputs: Ins): {[key: number] : Ins[number]} | Ins[] | null, }; interface ModelStartOptions { From 55bd241f805e0e4052041ab1d41809f20af62647 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 12 Sep 2024 15:15:39 -0700 Subject: [PATCH 478/479] Further updates to 2-way reactive function set() types - Allow `set()` to return a partial array covering first N inputs to update, and remove extra brackets in `Ins[]` return type since `Ins` is already an array - Individually validate types of each item of of `{ 0: in0, 2: in2, ... }` object return value, instead of using an index signature that allows values to be any of the inputs' types --- src/Model/fn.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Model/fn.ts b/src/Model/fn.ts index 68a72a320..c66ba73d0 100644 --- a/src/Model/fn.ts +++ b/src/Model/fn.ts @@ -9,11 +9,20 @@ class NamedFns { } type StartFnParam = unknown; +// From type-fest: https://github.com/sindresorhus/type-fest +type ArrayIndices = + Exclude['length'], Element['length']>; + +type TwoWayReactiveFnSetReturnType = + Partial | + Partial<{ [K in Extract, number>]: Ins[K] }> | + null; + type ModelFn = ((...inputs: Ins) => Out) | { - get(...inputs: Ins): Out, - set(output: Out, ...inputs: Ins): {[key: number] : Ins[number]} | Ins[] | null, + get(...inputs: Ins): Out; + set(output: Out, ...inputs: Ins): TwoWayReactiveFnSetReturnType; }; interface ModelStartOptions { @@ -99,11 +108,7 @@ declare module './Model' { */ fn( name: string, - fn: (...inputs: Ins) => Out | - { - get(...inputs: Ins): Out; - set(output: Out, ...inputs: Ins): void - } + fn: ModelFn ): void; /** From 96e92f731c683c160e348931a3034f4b1d7224ce Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 13 Sep 2024 11:28:38 -0700 Subject: [PATCH 479/479] 2.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 75650438a..c06850b5b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "2.2.2", + "version": "2.3.0", "main": "./lib/index.js", "files": [ "lib/*"