diff --git a/dist b/dist index c44c8605..5da36f07 160000 --- a/dist +++ b/dist @@ -1 +1 @@ -Subproject commit c44c86054ef1df42d782a67cba1f679207d07ed1 +Subproject commit 5da36f07cebfdcad53b845c0c3924c22f4894f2f diff --git a/src/SpecHelper.js b/src/SpecHelper.js index a1c70a0c..484b29fd 100644 --- a/src/SpecHelper.js +++ b/src/SpecHelper.js @@ -9,6 +9,7 @@ var Y = require('./y.js') require('../../y-memory/src/Memory.js')(Y) require('../../y-array/src/Array.js')(Y) +require('../../y-map/src/Map.js')(Y) require('../../y-indexeddb/src/IndexedDB.js')(Y) module.exports = Y diff --git a/src/Types/Map.js b/src/Types/Map.js deleted file mode 100644 index dc0de35b..00000000 --- a/src/Types/Map.js +++ /dev/null @@ -1,314 +0,0 @@ -/* @flow */ -'use strict' - -module.exports = function (Y /* :any */) { - class YMap { - /* :: - _model: Id; - os: Y.AbstractDatabase; - map: Object; - contents: any; - opContents: Object; - eventHandler: Function; - */ - constructor (os, model, contents, opContents) { - this._model = model.id - this.os = os - this.map = Y.utils.copyObject(model.map) - this.contents = contents - this.opContents = opContents - this.eventHandler = new Y.utils.EventHandler(ops => { - var userEvents = [] - for (var i in ops) { - var op = ops[i] - var oldValue - // key is the name to use to access (op)content - var key = op.struct === 'Delete' ? op.key : op.parentSub - - // compute oldValue - if (this.opContents[key] != null) { - let prevType = this.opContents[key] - oldValue = () => {// eslint-disable-line - return new Promise((resolve) => { - this.os.requestTransaction(function *() {// eslint-disable-line - var type = yield* this.getType(prevType) - resolve(type) - }) - }) - } - } else { - oldValue = this.contents[key] - } - // compute op event - if (op.struct === 'Insert') { - if (op.left === null) { - if (op.opContent != null) { - delete this.contents[key] - if (op.deleted) { - delete this.opContents[key] - } else { - this.opContents[key] = op.opContent - } - } else { - delete this.opContents[key] - if (op.deleted) { - delete this.contents[key] - } else { - this.contents[key] = op.content - } - } - this.map[key] = op.id - var insertEvent - if (oldValue === undefined) { - insertEvent = { - name: key, - object: this, - type: 'add' - } - } else { - insertEvent = { - name: key, - object: this, - oldValue: oldValue, - type: 'update' - } - } - userEvents.push(insertEvent) - } - } else if (op.struct === 'Delete') { - if (Y.utils.compareIds(this.map[key], op.target)) { - delete this.opContents[key] - delete this.contents[key] - var deleteEvent = { - name: key, - object: this, - oldValue: oldValue, - type: 'delete' - } - userEvents.push(deleteEvent) - } - } else { - throw new Error('Unexpected Operation!') - } - } - this.eventHandler.callEventListeners(userEvents) - }) - } - get (key) { - // return property. - // if property does not exist, return null - // if property is a type, return a promise - if (key == null) { - throw new Error('You must specify key!') - } - if (this.opContents[key] == null) { - return this.contents[key] - } else { - return new Promise((resolve) => { - var oid = this.opContents[key] - this.os.requestTransaction(function *() { - var type = yield* this.getType(oid) - resolve(type) - }) - }) - } - } - /* - If there is a primitive (not a custom type), then return it. - Returns all primitive values, if propertyName is specified! - Note: modifying the return value could result in inconsistencies! - -- so make sure to copy it first! - */ - getPrimitive (key) { - if (key == null) { - return Y.utils.copyObject(this.contents) - } else { - return this.contents[key] - } - } - delete (key) { - var right = this.map[key] - if (right != null) { - var del = { - target: right, - struct: 'Delete' - } - var eventHandler = this.eventHandler - var modDel = Y.utils.copyObject(del) - modDel.key = key - eventHandler.awaitAndPrematurelyCall([modDel]) - this.os.requestTransaction(function *() { - yield* this.applyCreatedOperations([del]) - eventHandler.awaitedDeletes(1) - }) - } - } - set (key, value) { - // set property. - // if property is a type, return a promise - // if not, apply immediately on this type an call event - - var right = this.map[key] || null - var insert /* :any */ = { - left: null, - right: right, - origin: null, - parent: this._model, - parentSub: key, - struct: 'Insert' - } - return new Promise((resolve) => { - if (value instanceof Y.utils.CustomType) { - // construct a new type - this.os.requestTransaction(function *() { - var typeid = yield* value.createType.call(this) - var type = yield* this.getType(typeid) - insert.opContent = typeid - insert.id = this.store.getNextOpId() - yield* this.applyCreatedOperations([insert]) - resolve(type) - }) - } else { - insert.content = value - insert.id = this.os.getNextOpId() - var eventHandler = this.eventHandler - eventHandler.awaitAndPrematurelyCall([insert]) - this.os.requestTransaction(function *() { - yield* this.applyCreatedOperations([insert]) - eventHandler.awaitedInserts(1) - }) - resolve(value) - } - }) - } - observe (f) { - this.eventHandler.addEventListener(f) - } - unobserve (f) { - this.eventHandler.removeEventListener(f) - } - /* - Observe a path. - - E.g. - ``` - o.set('textarea', Y.TextBind) - o.observePath(['textarea'], function(t){ - // is called whenever textarea is replaced - t.bind(textarea) - }) - - returns a Promise that contains a function that removes the observer from the path. - */ - observePath (path, f) { - var self = this - function observeProperty (events) { - // call f whenever path changes - for (var i = 0; i < events.length; i++) { - var event = events[i] - if (event.name === propertyName) { - // call this also for delete events! - var property = self.get(propertyName) - if (property instanceof Promise) { - property.then(f) - } else { - f(property) - } - } - } - } - - if (path.length < 1) { - throw new Error('Path must contain at least one element!') - } else if (path.length === 1) { - var propertyName = path[0] - var property = self.get(propertyName) - if (property instanceof Promise) { - property.then(f) - } else { - f(property) - } - this.observe(observeProperty) - return Promise.resolve(function () { - self.unobserve(f) - }) - } else { - var deleteChildObservers - var resetObserverPath = function () { - var promise = self.get(path[0]) - if (!promise instanceof Promise) { - // its either not defined or a primitive value - promise = self.set(path[0], Y.Map) - } - return promise.then(function (map) { - return map.observePath(path.slice(1), f) - }).then(function (_deleteChildObservers) { - // update deleteChildObservers - deleteChildObservers = _deleteChildObservers - return Promise.resolve() // Promise does not return anything - }) - } - var observer = function (events) { - for (var e in events) { - var event = events[e] - if (event.name === path[0]) { - if (deleteChildObservers != null) { - deleteChildObservers() - } - if (event.type === 'add' || event.type === 'update') { - resetObserverPath() - } - // TODO: what about the delete events? - } - } - } - self.observe(observer) - return resetObserverPath().then( - // this promise contains a function that deletes all the child observers - // and how to unobserve the observe from this object - new Promise.resolve(function () { // eslint-disable-line - if (deleteChildObservers != null) { - deleteChildObservers() - } - self.unobserve(observer) - }) - ) - } - } - * _changed (transaction, op) { - if (op.struct === 'Delete') { - var target = yield* transaction.getOperation(op.target) - op.key = target.parentSub - } - this.eventHandler.receivedOp(op) - } - } - Y.Map = new Y.utils.CustomType({ - class: YMap, - createType: function * YMapCreator () { - var modelid = this.store.getNextOpId() - var model = { - map: {}, - struct: 'Map', - type: 'Map', - id: modelid - } - yield* this.applyCreatedOperations([model]) - return modelid - }, - initType: function * YMapInitializer (os, model) { - var contents = {} - var opContents = {} - var map = model.map - for (var name in map) { - var op = yield* this.getOperation(map[name]) - if (op.opContent != null) { - opContents[name] = op.opContent - } else { - contents[name] = op.content - } - } - return new YMap(os, model, contents, opContents) - } - }) -} diff --git a/src/Types/Map.spec.js b/src/Types/Map.spec.js deleted file mode 100644 index 4a8b73f0..00000000 --- a/src/Types/Map.spec.js +++ /dev/null @@ -1,221 +0,0 @@ -/* global createUsers, databases, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, describeManyTimes */ -/* eslint-env browser,jasmine */ -'use strict' - -var Y = require('../SpecHelper.js') -var numberOfYMapTests = 50 -var repeatMapTeasts = 1 - -for (let database of databases) { - describe(`Map Type (DB: ${database})`, function () { - var y1, y2, y3, y4, flushAll - - beforeEach(async(function * (done) { - yield createUsers(this, 5, database) - y1 = this.users[0].share.root - y2 = this.users[1].share.root - y3 = this.users[2].share.root - y4 = this.users[3].share.root - flushAll = Y.utils.globalRoom.flushAll - done() - })) - afterEach(async(function * (done) { - yield compareAllUsers(this.users) - done() - }), 5000) - - describe('Basic tests', function () { - it('Basic get&set of Map property (converge via sync)', async(function * (done) { - y1.set('stuff', 'stuffy') - expect(y1.get('stuff')).toEqual('stuffy') - yield flushAll() - for (var key in this.users) { - var u = this.users[key].share.root - expect(u.get('stuff')).toEqual('stuffy') - } - done() - })) - it('Map can set custom types (Map)', async(function * (done) { - var map = yield y1.set('Map', Y.Map) - map.set('one', 1) - map = yield y1.get('Map') - expect(map.get('one')).toEqual(1) - done() - })) - it('Map can set custom types (Array)', async(function * (done) { - var array = yield y1.set('Array', Y.Array) - array.insert(0, [1, 2, 3]) - array = yield y1.get('Array') - expect(array.toArray()).toEqual([1, 2, 3]) - done() - })) - it('Basic get&set of Map property (converge via update)', async(function * (done) { - yield flushAll() - y1.set('stuff', 'stuffy') - expect(y1.get('stuff')).toEqual('stuffy') - - yield flushAll() - for (var key in this.users) { - var r = this.users[key].share.root - expect(r.get('stuff')).toEqual('stuffy') - } - done() - })) - it('Basic get&set of Map property (handle conflict)', async(function * (done) { - yield flushAll() - y1.set('stuff', 'c0') - y2.set('stuff', 'c1') - - yield flushAll() - for (var key in this.users) { - var u = this.users[key] - expect(u.share.root.get('stuff')).toEqual('c0') - } - done() - })) - it('Basic get&set&delete of Map property (handle conflict)', async(function * (done) { - yield flushAll() - y1.set('stuff', 'c0') - y1.delete('stuff') - y2.set('stuff', 'c1') - yield flushAll() - - for (var key in this.users) { - var u = this.users[key] - expect(u.share.root.get('stuff')).toBeUndefined() - } - done() - })) - it('Basic get&set of Map property (handle three conflicts)', async(function * (done) { - yield flushAll() - y1.set('stuff', 'c0') - y2.set('stuff', 'c1') - y2.set('stuff', 'c2') - y3.set('stuff', 'c3') - yield flushAll() - - for (var key in this.users) { - var u = this.users[key] - expect(u.share.root.get('stuff')).toEqual('c0') - } - done() - })) - it('Basic get&set&delete of Map property (handle three conflicts)', async(function * (done) { - yield flushAll() - y1.set('stuff', 'c0') - y2.set('stuff', 'c1') - y2.set('stuff', 'c2') - y3.set('stuff', 'c3') - yield flushAll() - y1.set('stuff', 'deleteme') - y1.delete('stuff') - y2.set('stuff', 'c1') - y3.set('stuff', 'c2') - y4.set('stuff', 'c3') - yield flushAll() - - for (var key in this.users) { - var u = this.users[key] - expect(u.share.root.get('stuff')).toBeUndefined() - } - done() - })) - it('observePath properties', async(function * (done) { - y1.observePath(['map'], function (map) { - if (map != null) { - map.set('yay', 4) - } - }) - yield y2.set('map', Y.Map) - yield flushAll() - var map = yield y3.get('map') - expect(map.get('yay')).toEqual(4) - done() - })) - it('throws add & update & delete events (with type and primitive content)', async(function * (done) { - var event - yield flushAll() - y1.observe(function (e) { - event = e // just put it on event, should be thrown synchronously anyway - }) - y1.set('stuff', 4) - expect(event).toEqual([{ - type: 'add', - object: y1, - name: 'stuff' - }]) - // update, oldValue is in contents - yield y1.set('stuff', Y.Array) - expect(event).toEqual([{ - type: 'update', - object: y1, - name: 'stuff', - oldValue: 4 - }]) - y1.get('stuff').then(function (replacedArray) { - // update, oldValue is in opContents - y1.set('stuff', 5) - var getYArray = event[0].oldValue - expect(typeof getYArray.constructor === 'function').toBeTruthy() - getYArray().then(function (array) { - expect(array).toEqual(replacedArray) - - // delete - y1.delete('stuff') - expect(event).toEqual([{ - type: 'delete', - name: 'stuff', - object: y1, - oldValue: 5 - }]) - done() - }) - }) - })) - }) - describeManyTimes(repeatMapTeasts, `${numberOfYMapTests} Random tests`, function () { - var randomMapTransactions = [ - function set (map) { - map.set('somekey', getRandomNumber()) - }, - function delete_ (map) { - map.delete('somekey') - } - ] - function compareMapValues (maps) { - var firstMap - for (var map of maps) { - var val = map.getPrimitive() - if (firstMap == null) { - firstMap = val - } else { - expect(val).toEqual(firstMap) - } - } - } - beforeEach(async(function * (done) { - yield y1.set('Map', Y.Map) - yield flushAll() - - var promises = [] - for (var u = 0; u < this.users.length; u++) { - promises.push(this.users[u].share.root.get('Map')) - } - this.maps = yield Promise.all(promises) - done() - })) - it(`succeed after ${numberOfYMapTests} actions, no GC, all users disconnecting/reconnecting`, async(function * (done) { - yield applyRandomTransactionsAllRejoinNoGC(this.users, this.maps, randomMapTransactions, numberOfYMapTests) - yield flushAll() - yield compareMapValues(this.maps) - done() - })) - it(`succeed after ${numberOfYMapTests} actions, GC, user[0] is not disconnecting`, async(function * (done) { - yield applyRandomTransactionsWithGC(this.users, this.maps, randomMapTransactions, numberOfYMapTests) - yield flushAll() - yield compareMapValues(this.maps) - done() - })) - }) - }) -} diff --git a/src/y.js b/src/y.js index ab2ce1a8..b2d596f7 100644 --- a/src/y.js +++ b/src/y.js @@ -58,8 +58,6 @@ function requestModules (modules) { return Promise.all(promises) } -require('./Types/Map.js')(Y) - /* :: type MemoryOptions = { name: 'memory'