diff --git a/.eslintrc b/.eslintrc index e84964e0ca2f4eaf026d40562ffa8896fd589304..9de476084b18a12da83d42a423018ea5ae293608 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,16 +4,21 @@ }, "rules": { "strict": 0, - "camelcase": [1, {"properties": "never"}] + "camelcase": [1, {"properties": "never"}], + "no-underscore-dangle": 0 }, "parser": "babel-eslint", "globals": { "OperationStore": true, "AbstractOperationStore": true, "AbstractTransaction": true, + "AbstractConnector": true, "Transaction": true, "IndexedDB": true, "IDBRequest": true, - "GeneratorFunction": true + "GeneratorFunction": true, + "Y": true, + "setTimeout": true, + "setInterval": true } } diff --git a/src/Connector.js b/src/Connector.js new file mode 100644 index 0000000000000000000000000000000000000000..11a4275d10fb22a2df79a89a09b4e947fe83301b --- /dev/null +++ b/src/Connector.js @@ -0,0 +1,223 @@ + +class AbstractConnector { + /* + opts + .role : String Role of this client ("master" or "slave") + .userId : String that uniquely defines the user. + */ + constructor (opts) { + if (opts == null){ + opts = {}; + } + if (opts.role == null || opts.role === "master") { + this.role = "master"; + } else if (opts.role === "slave") { + this.role = "slave"; + } else { + throw new Error("Role must be either 'master' or 'slave'!"); + } + this.role = opts.role; + this.connections = {}; + this.userEventListeners = []; + this.whenSyncedListeners = []; + this.currentSyncTarget = null; + } + setUserId (userId) { + this.os.setUserId(userId); + } + onUserEvent (f) { + this.userEventListeners.push(f); + } + userLeft (user : string) { + delete this.connections[user]; + if (user === this.currentSyncTarget){ + this.currentSyncTarget = null; + this.findNextSyncTarget(); + } + for (var f of this.userEventListeners){ + f({ + action: "userLeft", + user: user + }); + } + } + userJoined (user, role) { + if(role == null){ + throw new Error("You must specify the role of the joined user!"); + } + if (this.connections[user] != null) { + throw new Error("This user already joined!"); + } + this.connections[user] = { + isSynced: false, + role: role + }; + for (var f of this.userEventListeners) { + f({ + action: "userJoined", + user: user, + role: role + }); + } + } + // Execute a function _when_ we are connected. + // If not connected, wait until connected + whenSynced (f) { + if (this.isSynced === true) { + f(); + } else { + this.whenSyncedListeners.push(f); + } + } + // returns false, if there is no sync target + // true otherwise + findNextSyncTarget () { + if (this.currentSyncTarget != null && this.connections[this.currentSyncTarget].isSynced === false) { + throw new Error("The current sync has not finished!") + } + + for (var uid in this.connections) { + var u = this.connections[uid]; + if (!u.isSynced) { + this.currentSyncTarget = uid; + this.send(uid, { + type: "sync step 1", + stateVector: hb.getStateVector() + }); + return true; + } + } + // set the state to synced! + if (!this.isSynced) { + this.isSynced = true; + for (var f of this.whenSyncedListeners) { + f() + } + this.whenSyncedListeners = null; + } return false; + } + // You received a raw message, and you know that it is intended for to Yjs. Then call this function. + receiveMessage (sender, m) { + if (m.type === "sync step 1") { + // TODO: make transaction, stream the ops + var ops = yield* this.os.getOperations(m.stateVector); + // TODO: compare against m.sv! + var sv = yield* this.getStateVector(); + this.send (sender, { + type: "sync step 2" + os: ops, + stateVector: sv + }); + this.syncingClients.push(sender); + setTimeout(()=>{ + this.syncingClients = this.syncingClients.filter(function(client){ + return client !== sender; + }); + this.send(sender, { + type: "sync done" + }) + }, this.syncingClientDuration); + } else if (m.type === "sync step 2") { + var ops = this.os.getOperations(m.stateVector); + this.broadcast { + type: "update", + ops: ops + } + } else if (m.type === "sync done") { + this.connections[sender].isSynced = true; + this.findNextSyncTarget(); + } + } else if (m.type === "update") { + for (var client of this.syncingClients) { + this.send(client, m); + } + this.os.apply(m.ops); + } + } + // Currently, the HB encodes operations as JSON. For the moment I want to keep it + // that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want + // too much overhead. Y is very likely to get changed a lot in the future + // + // Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable) + // we encode the JSON as XML. + // + // When the HB support encoding as XML, the format should look pretty much like this. + // + // does not support primitive values as array elements + // expects an ltx (less than xml) object + parseMessageFromXml (m) { + function parseArray (node) { + for (var n of node.children){ + if (n.getAttribute("isArray") === "true") { + return parseArray(n); + } else { + return parseObject(n); + } + } + } + function parseObject (node) { + var json = {}; + for (name in node.attrs) { + var value = node.attrs[name]; + var int = parseInt(value); + if (isNaN(int) or (""+int) !== value){ + json[name] = value; + } else { + json[name] = int; + } + } + for (n in node.children){ + var name = n.name; + if (n.getAttribute("isArray") === "true") { + json[name] = parseArray(n); + } else { + json[name] = parseObject(n); + } + } + return json; + } + parseObject(node); + } + // encode message in xml + // we use string because Strophe only accepts an "xml-string".. + // So {a:4,b:{c:5}} will look like + // <y a="4"> + // <b c="5"></b> + // </y> + // m - ltx element + // json - Object + encodeMessageToXml (m, json) { + // attributes is optional + function encodeObject (m, json) { + for (name in json) { + var value = json[name]; + if (name == null) { + // nop + } else if (value.constructor === Object) { + encodeObject(m.c(name), value); + } else if (value.constructor === Array) { + encodeArray(m.c(name), value); + } else { + m.setAttribute(name, value); + } + } + } + function encodeArray (m, array) { + m.setAttribute("isArray", "true"); + for (var e of array) { + if (e.constructor === Object) { + encodeObject(m.c("array-element"), e); + } else { + encodeArray(m.c("array-element"), e); + } + } + } + if (json.constructor === Object) { + encodeObject(m.c("y", {xmlns:"http://y.ninja/connector-stanza"}), json); + } else if (json.constructor === Array) { + encodeArray(m.c("y", {xmlns:"http://y.ninja/connector-stanza"}), json); + } else { + throw new Error("I can't encode this json!"); + } + } +} diff --git a/src/Connectors.js b/src/Connectors.js deleted file mode 100644 index 96b9905dfb914d32e3d96cd8f721df2890d6b66b..0000000000000000000000000000000000000000 --- a/src/Connectors.js +++ /dev/null @@ -1,446 +0,0 @@ - - -(function(){ - function WebRTC(webrtc_options){ - if(webrtc_options === undefined){ - throw new Error("webrtc_options must not be undefined!") - } - var room = webrtc_options.room; - - // connect per default to our server - if(webrtc_options.url === undefined){ - webrtc_options.url = "https://yatta.ninja:8888"; - } - - var swr = new SimpleWebRTC(webrtc_options); - this.swr = swr; - var self = this; - - var channel; - - swr.once('connectionReady',function(user_id){ - // SimpleWebRTC (swr) is initialized - swr.joinRoom(room); - - swr.once('joinedRoom', function(){ - // the client joined the specified room - - // initialize the connector with the required parameters. - // You always should specify `role`, `syncMethod`, and `user_id` - self.init({ - role : "slave", - syncMethod : "syncAll", - user_id : user_id - }); - var i; - // notify the connector class about all the users that already - // joined the session - for(i in self.swr.webrtc.peers){ - self.userJoined(self.swr.webrtc.peers[i].id, "slave"); - } - - - swr.on("channelMessage", function(peer, room, message){ - // The client received a message - // Check if the connector is already initialized, - // only then forward the message to the connector class - if(self.is_initialized && message.type === "yjs"){ - self.receiveMessage(peer.id, message.payload); - } - }); - }); - - swr.on("createdPeer", function(peer){ - // a new peer/client joined the session. - // Notify the connector class, if the connector - // is already initialized - if(self.is_initialized){ - // note: Since the WebRTC Connector only supports the SyncAll - // syncmethod, every client is a slave. - self.userJoined(peer.id, "slave"); - } - }); - - swr.on("peerStreamRemoved",function(peer){ - // a client left the session. - // Notify the connector class, if the connector - // is already initialized - if(self.is_initialized){ - self.userLeft(peer.id); - } - }); - }); - } - - // Specify how to send a message to a specific user (by uid) - WebRTC.prototype.send = function(uid, message){ - var self = this; - // we have to make sure that the message is sent under all circumstances - var send = function(){ - // check if the clients still exists - var peer = self.swr.webrtc.getPeers(uid)[0]; - var success; - if(peer){ - // success is true, if the message is successfully sent - success = peer.sendDirectly("simplewebrtc", "yjs", message); - } - if(!success){ - // resend the message if it didn't work - window.setTimeout(send,500); - } - }; - // try to send the message - send(); - }; - - // specify how to broadcast a message to all users - // (it may send the message back to itself). - // The webrtc connecor tries to send it to every single clients directly - WebRTC.prototype.broadcast = function(message){ - this.swr.sendDirectlyToAll("simplewebrtc","yjs",message); - }; - - Y.Connectors.WebRTC = WebRTC; -})() - - -var connectorAdapter = (){ - # - # @params new Connector(options) - # @param options.syncMethod {String} is either "syncAll" or "master-slave". - # @param options.role {String} The role of this client - # (slave or master (only used when syncMethod is master-slave)) - # @param options.perform_send_again {Boolean} Whetehr to whether to resend the HB after some time period. This reduces sync errors, but has some overhead (optional) - # - init: (options)-> - req = (name, choices)=> - if options[name]? - if (not choices?) or choices.some((c)->c is options[name]) - @[name] = options[name] - else - throw new Error "You can set the '"+name+"' option to one of the following choices: "+JSON.encode(choices) - else - throw new Error "You must specify "+name+", when initializing the Connector!" - - req "syncMethod", ["syncAll", "master-slave"] - req "role", ["master", "slave"] - req "user_id" - @on_user_id_set?(@user_id) - - # whether to resend the HB after some time period. This reduces sync errors. - # But this is not necessary in the test-connector - if options.perform_send_again? - @perform_send_again = options.perform_send_again - else - @perform_send_again = true - - # A Master should sync with everyone! TODO: really? - for now its safer this way! - if @role is "master" - @syncMethod = "syncAll" - - # is set to true when this is synced with all other connections - @is_synced = false - # Peerjs Connections: key: conn-id, value: object - @connections = {} - # List of functions that shall process incoming data - @receive_handlers ?= [] - - # whether this instance is bound to any y instance - @connections = {} - @current_sync_target = null - @sent_hb_to_all_users = false - @is_initialized = true - - onUserEvent: (f)-> - @connections_listeners ?= [] - @connections_listeners.push f - - isRoleMaster: -> - @role is "master" - - isRoleSlave: -> - @role is "slave" - - findNewSyncTarget: ()-> - @current_sync_target = null - if @syncMethod is "syncAll" - for user, c of @connections - if not c.is_synced - @performSync user - break - if not @current_sync_target? - @setStateSynced() - null - - userLeft: (user)-> - delete @connections[user] - @findNewSyncTarget() - if @connections_listeners? - for f in @connections_listeners - f { - action: "userLeft" - user: user - } - - - userJoined: (user, role)-> - if not role? - throw new Error "Internal: You must specify the role of the joined user! E.g. userJoined('uid:3939','slave')" - # a user joined the room - @connections[user] ?= {} - @connections[user].is_synced = false - - if (not @is_synced) or @syncMethod is "syncAll" - if @syncMethod is "syncAll" - @performSync user - else if role is "master" - # TODO: What if there are two masters? Prevent sending everything two times! - @performSyncWithMaster user - - if @connections_listeners? - for f in @connections_listeners - f { - action: "userJoined" - user: user - role: role - } - - # - # Execute a function _when_ we are connected. If not connected, wait until connected. - # @param f {Function} Will be executed on the Connector context. - # - whenSynced: (args)-> - if args.constructor is Function - args = [args] - if @is_synced - args[0].apply this, args[1..] - else - @compute_when_synced ?= [] - @compute_when_synced.push args - - # - # Execute an function when a message is received. - # @param f {Function} Will be executed on the PeerJs-Connector context. f will be called with (sender_id, broadcast {true|false}, message). - # - onReceive: (f)-> - @receive_handlers.push f - - # - # perform a sync with a specific user. - # - performSync: (user)-> - if not @current_sync_target? - @current_sync_target = user - @send user, - sync_step: "getHB" - send_again: "true" - data: @getStateVector() - if not @sent_hb_to_all_users - @sent_hb_to_all_users = true - - hb = @getHB([]).hb - _hb = [] - for o in hb - _hb.push o - if _hb.length > 10 - @broadcast - sync_step: "applyHB_" - data: _hb - _hb = [] - @broadcast - sync_step: "applyHB" - data: _hb - - # - # When a master node joined the room, perform this sync with him. It will ask the master for the HB, - # and will broadcast his own HB - # - performSyncWithMaster: (user)-> - @current_sync_target = user - @send user, - sync_step: "getHB" - send_again: "true" - data: @getStateVector() - hb = @getHB([]).hb - _hb = [] - for o in hb - _hb.push o - if _hb.length > 10 - @broadcast - sync_step: "applyHB_" - data: _hb - _hb = [] - @broadcast - sync_step: "applyHB" - data: _hb - - # - # You are sure that all clients are synced, call this function. - # - setStateSynced: ()-> - if not @is_synced - @is_synced = true - if @compute_when_synced? - for el in @compute_when_synced - f = el[0] - args = el[1..] - f.apply(args) - delete @compute_when_synced - null - - # executed when the a state_vector is received. listener will be called only once! - whenReceivedStateVector: (f)-> - @when_received_state_vector_listeners ?= [] - @when_received_state_vector_listeners.push f - - - # - # You received a raw message, and you know that it is intended for to Yjs. Then call this function. - # - receiveMessage: (sender, res)-> - if not res.sync_step? - for f in @receive_handlers - f sender, res - else - if sender is @user_id - return - if res.sync_step is "getHB" - # call listeners - if @when_received_state_vector_listeners? - for f in @when_received_state_vector_listeners - f.call this, res.data - delete @when_received_state_vector_listeners - - data = @getHB(res.data) - hb = data.hb - _hb = [] - # always broadcast, when not synced. - # This reduces errors, when the clients goes offline prematurely. - # When this client only syncs to one other clients, but looses connectors, - # before syncing to the other clients, the online clients have different states. - # Since we do not want to perform regular syncs, this is a good alternative - if @is_synced - sendApplyHB = (m)=> - @send sender, m - else - sendApplyHB = (m)=> - @broadcast m - - for o in hb - _hb.push o - if _hb.length > 10 - sendApplyHB - sync_step: "applyHB_" - data: _hb - _hb = [] - - sendApplyHB - sync_step : "applyHB" - data: _hb - - if res.send_again? and @perform_send_again - send_again = do (sv = data.state_vector)=> - ()=> - hb = @getHB(sv).hb - for o in hb - _hb.push o - if _hb.length > 10 - @send sender, - sync_step: "applyHB_" - data: _hb - _hb = [] - @send sender, - sync_step: "applyHB", - data: _hb - sent_again: "true" - setTimeout send_again, 3000 - else if res.sync_step is "applyHB" - @applyHB(res.data, sender is @current_sync_target) - - if (@syncMethod is "syncAll" or res.sent_again?) and (not @is_synced) and ((@current_sync_target is sender) or (not @current_sync_target?)) - @connections[sender].is_synced = true - @findNewSyncTarget() - - else if res.sync_step is "applyHB_" - @applyHB(res.data, sender is @current_sync_target) - - - # Currently, the HB encodes operations as JSON. For the moment I want to keep it - # that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want - # too much overhead. Y is very likely to get changed a lot in the future - # - # Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable) - # we encode the JSON as XML. - # - # When the HB support encoding as XML, the format should look pretty much like this. - - # does not support primitive values as array elements - # expects an ltx (less than xml) object - parseMessageFromXml: (m)-> - parse_array = (node)-> - for n in node.children - if n.getAttribute("isArray") is "true" - parse_array n - else - parse_object n - - parse_object = (node)-> - json = {} - for name, value of node.attrs - int = parseInt(value) - if isNaN(int) or (""+int) isnt value - json[name] = value - else - json[name] = int - for n in node.children - name = n.name - if n.getAttribute("isArray") is "true" - json[name] = parse_array n - else - json[name] = parse_object n - json - parse_object m - - # encode message in xml - # we use string because Strophe only accepts an "xml-string".. - # So {a:4,b:{c:5}} will look like - # <y a="4"> - # <b c="5"></b> - # </y> - # m - ltx element - # json - guess it ;) - # - encodeMessageToXml: (m, json)-> - # attributes is optional - encode_object = (m, json)-> - for name,value of json - if not value? - # nop - else if value.constructor is Object - encode_object m.c(name), value - else if value.constructor is Array - encode_array m.c(name), value - else - m.setAttribute(name,value) - m - encode_array = (m, array)-> - m.setAttribute("isArray","true") - for e in array - if e.constructor is Object - encode_object m.c("array-element"), e - else - encode_array m.c("array-element"), e - m - if json.constructor is Object - encode_object m.c("y",{xmlns:"http://y.ninja/connector-stanza"}), json - else if json.constructor is Array - encode_array m.c("y",{xmlns:"http://y.ninja/connector-stanza"}), json - else - throw new Error "I can't encode this json!" - - setIsBoundToY: ()-> - @on_bound_to_y?() - delete @when_bound_to_y - @is_bound_to_y = true - } -}; diff --git a/src/Connectors/Test.js b/src/Connectors/Test.js new file mode 100644 index 0000000000000000000000000000000000000000..0e1ac5d17fca51a72d3d960df0841873606d00b6 --- /dev/null +++ b/src/Connectors/Test.js @@ -0,0 +1,75 @@ +// returns a rendom element of o +// works on Object, and Array +function getRandom (o) { + if (o instanceof Array) { + return o[Math.floor(Math.random() * o.length)]; + } else if (o.constructor === Object) { + var keys = []; + for (var key in o) { + keys.push(key); + } + return o[getRandom(keys)]; + } +} + +var globalRoom = { + users: {}, + buffers: {}, + removeUser: function(user){ + for (var u of this.users) { + u.userLeft(user); + } + delete this.users[user]; + delete this.buffers[user]; + }, + addUser: function(connector){ + for (var u of this.users) { + u.userJoined(connector.userId); + } + this.users[connector.userId] = connector; + this.buffers[connector.userId] = []; + } +}; + +setInterval(function(){ + var bufs = []; + for (var i in globalRoom.buffers) { + if (globalRoom.buffers[i].length > 0) { + bufs.push(i); + } + } + if (bufs.length > 0) { + var userId = getRandom(bufs); + var m = globalRoom.buffers[userId]; + var user = globalRoom.users[userId]; + user.receiveMessage(m); + } +}, 10); + +var userIdCounter = 0; + +class Test extends AbstractConnector { + constructor (options) { + if(options === undefined){ + throw new Error("Options must not be undefined!"); + } + super({ + role: "master" + }); + + this.setUserId((userIdCounter++) + ""); + } + send (uid, message) { + globalRoom.buffers[uid].push(message); + } + broadcast (message) { + for (var buf of globalRoom.buffers) { + buf.push(message); + } + } + disconnect () { + globalRoom.removeUser(this.userId); + } +} + +Y.Test = Test; diff --git a/src/Connectors/WebRTC.js b/src/Connectors/WebRTC.js new file mode 100644 index 0000000000000000000000000000000000000000..709ca1e99191933978d9bab8412033391c50d28e --- /dev/null +++ b/src/Connectors/WebRTC.js @@ -0,0 +1,83 @@ + +class WebRTC extends AbstractConnector { + constructor (options) { + if(options === undefined){ + throw new Error("Options must not be undefined!"); + } + super({ + role: "slave" + }); + + var room = options.room; + + // connect per default to our server + if(options.url == null){ + options.url = "https://yatta.ninja:8888"; + } + + var swr = new SimpleWebRTC(options); //eslint-disable-line no-undef + this.swr = swr; + var self = this; + + swr.once("connectionReady", function(userId){ + // SimpleWebRTC (swr) is initialized + swr.joinRoom(room); + + swr.once("joinedRoom", function(){ + self.setUserId(userId); + var i; + // notify the connector class about all the users that already + // joined the session + for(i in self.swr.webrtc.peers){ + self.userJoined(self.swr.webrtc.peers[i].id, "master"); + } + swr.on("channelMessage", function(peer, room_, message){ + // The client received a message + // Check if the connector is already initialized, + // only then forward the message to the connector class + if(message.type != null ){ + self.receiveMessage(peer.id, message.payload); + } + }); + }); + + swr.on("createdPeer", function(peer){ + // a new peer/client joined the session. + // Notify the connector class, if the connector + // is already initialized + self.userJoined(peer.id, "master"); + }); + + swr.on("peerStreamRemoved", function(peer){ + // a client left the session. + // Notify the connector class, if the connector + // is already initialized + self.userLeft(peer.id); + }); + }); + } + send (uid, message) { + var self = this; + // we have to make sure that the message is sent under all circumstances + var send = function(){ + // check if the clients still exists + var peer = self.swr.webrtc.getPeers(uid)[0]; + var success; + if(peer){ + // success is true, if the message is successfully sent + success = peer.sendDirectly("simplewebrtc", "yjs", message); + } + if(!success){ + // resend the message if it didn't work + setTimeout(send, 500); + } + }; + // try to send the message + send(); + } + broadcast (message) { + this.swr.sendDirectlyToAll("simplewebrtc", "yjs", message); + } +} + +Y.WebRTC = WebRTC; diff --git a/src/OperationStore.js b/src/OperationStore.js index 072947cb0cf92e179e8ad25c304deff6a967bd8c..7e351f8716c1075cfd3d44a7c23661c67881f9a8 100644 --- a/src/OperationStore.js +++ b/src/OperationStore.js @@ -120,7 +120,7 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars } // notify parent listeners, if possible var listeners = this.parentListeners[op.parent]; - if ( this.parentListenersRequestPending + if ( this.parentListenersRequestPending || ( listeners == null ) || ( listeners.length === 0 )) { return; @@ -141,7 +141,7 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars for (var parent_id in activatedOperations){ var parent = yield* this.getOperation(parent_id); Struct[parent.type].notifyObservers(activatedOperations[parent_id]); - } + } }) } diff --git a/src/Operations.js b/src/Operations.js index 6205e46fe1b39ea954aaa4e5175851d6e91e7c3e..1e807f64061483ad4b2e74a1de3ef57eb3adf547 100644 --- a/src/Operations.js +++ b/src/Operations.js @@ -1,7 +1,7 @@ /* @flow */ // Op is anything that we could get from the OperationStore. -struct Op = Object; +type Op = Object; var Struct = { Operation: { //eslint-disable-line no-unused-vars @@ -17,10 +17,11 @@ var Struct = { content : any, left : Struct.Insert, right : Struct.Insert, - parent : Struct.List) : Struct.Insert { + parent : Struct.List) : Insert { op.left = left ? left.id : null; op.origin = op.left; op.right = right ? right.id : null; + op.parent = parent.id; op.struct = "Insert"; yield* Struct.Operation.create.call(this, op, user); @@ -127,14 +128,26 @@ var Struct = { }, execute: function* (op) { // nop - } - ref: function* (op, pos) : Struct.Insert | undefined{ + }, + ref: function* (op : Op, pos : number) : Insert { var o = op.start; while ( pos !== 0 || o == null) { - o = (yield* this.getOperation(op.start)).right; + o = (yield* this.getOperation(o)).right; + pos--; } return (o == null) ? null : yield* this.getOperation(o); - } + }, + map: function* (o : Op, f : Function) : Array<any> { + o = o.start; + var res = []; + while ( pos !== 0 || o == null) { + var operation = yield* this.getOperation(o); + res.push(f(operation.content)); + o = operation.right; + pos--; + } + return res; + }, insert: function* (op, pos : number, contents : Array<any>) { var o = yield* Struct.List.ref.call(this, op, pos); var o_end = yield* this.getOperation(o.right); diff --git a/src/Types.js b/src/Types/List.js similarity index 54% rename from src/Types.js rename to src/Types/List.js index e42d10b3b374608626c6d0164b33051b3319a290..8a12de421d8cbfce486e1cbabc10a140d9f5ebff 100644 --- a/src/Types.js +++ b/src/Types/List.js @@ -7,8 +7,12 @@ this._model = _model; } *val (pos) { - var o = yield* this.Struct.List.ref(pos); - return o ? o.content : null; + if (pos != null) { + var o = yield* this.Struct.List.ref(this._model, pos); + return o ? o.content : null; + } else { + return yield* this.Struct.List.map(this._model, function(c){return c; }); + } } *insert (pos, contents) { yield* this.Struct.List.insert(pos, contents); @@ -17,9 +21,7 @@ Y.List = function* YList(){ var model = yield* this.Struct.List.create(); - return new Y.List.Create(model); - } - + return new List(model); + }; Y.List.Create = List; - Y.List = List; })(); diff --git a/src/y.js b/src/y.js index 723865eeefe12d6bd63e2a9c5debe20e88698e50..089d01a21d49a9d0583e4393bcfedc69ae33ef56 100644 --- a/src/y.js +++ b/src/y.js @@ -1,6 +1,6 @@ /* @flow */ -function Y (opts) { +function Y (opts) { //eslint-disable-line no-unused-vars var connector = opts.connector; - Y.Connectors[connector.name] + Y.Connectors[connector.name](); }