diff --git a/exec/load/json-db.js b/exec/load/json-db.js index 6ed994251f197a2d141edc367a1332b80f4a6812..cd90e7678b165a6e898ec0e3f342fc304be524fd 100644 --- a/exec/load/json-db.js +++ b/exec/load/json-db.js @@ -1,4 +1,3 @@ -load("synchronet-json.js"); /* JSON database - for Synchronet 3.15a+ (2011) @@ -11,17 +10,16 @@ load("synchronet-json.js"); methods: - - Repository.query(client,query); - - Repository.cycle(Date.now()); - - Repository.load(); - - Repository.save(Date.now()); - - Repository.lock(client,record,child_name,lock_type); - - Repository.read(client,record,child_name); - - Repository.write(client,record,child_name,data); - - Repository.create(client,record,child_name,data); - - Repository.delete(client,record,child_name); - - Repository.subscribe(client,record,child_name); - - Repository.unsubscribe(client,record,child_name); + - Database.query(client,query); + - Database.cycle(Date.now()); + - Database.load(); + - Database.save(Date.now()); + - Database.lock(client,record,child_name,lock_type); + - Database.read(client,record,child_name); + - Database.write(client,record,child_name,data); + - Database.delete(client,record,child_name); + - Database.subscribe(client,record,child_name); + - Database.unsubscribe(client,record,child_name); optional arguments: @@ -29,11 +27,13 @@ load("synchronet-json.js"); */ -if(!this.Repository) { +if(!this.Database) { -Repository = new (function() { +Database = new (function() { + + this.VERSION = "$Revision$".split(' ')[1]; - var settings={ + this.settings={ /* record lock constants, incremental (do not change the order or value) */ LOCK_UNLOCK:-1, @@ -57,8 +57,8 @@ Repository = new (function() { ERROR_DUPLICATE_LOCK:5 }; - /**************************** repository objects *****************************/ - /* locking object generated by Repository.lock() method + /**************************** database objects *****************************/ + /* locking object generated by Database.lock() method contains the type of lock requested, and the time at which the request came in TODO: possibly "expire" unfulfilled lock requests after a certain period */ function Lock(lock_type,timestamp) { @@ -70,29 +70,7 @@ Repository = new (function() { function Error(error_num,error_desc) { this.operation="ERROR"; this.error_num=error_num; - switch(error_num) { - case settings.ERROR_INVALID_REQUEST: - this.error_desc="Invalid record request"; - break; - case settings.ERROR_OBJECT_NOT_FOUND: - this.error_desc="Record not found"; - break; - case settings.ERROR_NOT_LOCKED: - this.error_desc="Record not locked"; - break; - case settings.ERROR_LOCKED_WRITE: - this.error_desc="Record locked for writing"; - break; - case settings.ERROR_LOCKED_READ: - this.error_desc="Record locked for reading"; - break; - case settings.ERROR_DUPLICATE_LOCK: - this.error_desc="Duplicate record lock request"; - break; - default: - this.error_desc="Unknown error"; - break; - } + this.error_desc=error_desc; } /* shadow properties generated by composite_sketch() @@ -107,13 +85,15 @@ Repository = new (function() { contains the requested object, its shadow object, the prevailing lock state of that object (affected by parents/children), and a list of subscribers associated with that object or its parents */ - function Record(data,shadow,info) { + function Record(data,shadow,parent_name,child_name,info) { this.data=data; this.shadow=shadow; + this.parent_name=parent_name; + this.child_name=child_name; this.info=info; } - /* request object generated by Repository.queue() method + /* request object generated by Database.queue() method contains the requested object parent, the specific child property requested, data (in the case of a PUT operation ) */ function Request(client,operation,parent_name,child_name,data) { @@ -131,15 +111,13 @@ Repository = new (function() { it would also allow for a much simpler mechanism for removing games you no longer wish to host (simply delete the db file and edit your service inis) */ - /*************************** repository data ******************************/ + /*************************** database data ******************************/ /* database storage file */ - if(file_exists(argv[0])) { + if(argv[0]) this.file=new File(argv[0]); - } - else { - this.file=new File(system.data_dir + "db.json"); - } - + else + this.file=undefined; + /* master database object */ this.data={}; @@ -149,48 +127,54 @@ Repository = new (function() { /* queued array of data requests (get/put/create/delete) */ this.queue=[]; - /*************************** repository methods ****************************/ + /*************************** database methods ****************************/ /* subscribe a client to an object */ - this.subscribe = function(client,record,child_name) { - record.shadow[child_name]._subscribers[client.id]=client; + this.subscribe = function(client,record) { + record.shadow[record.child_name]._subscribers[client.id]=client; /* TODO: track duplicate subscribers (deny?) and expire existing subscribers after certain amount of time, maybe */ + return true; }; /* unsubscribe a client from an object */ - this.unsubscribe = function(client,record,child_name) { - delete record.shadow[child_name]._subscribers[client.id]; + this.unsubscribe = function(client,record) { + delete record.shadow[record.child_name]._subscribers[client.id]; /* TODO: validate existing subscription before attempting to delete */ + return true; }; /* The point of a read lock is *only* to prevent someone from getting a write lock... just a simple counter is usually enough. */ - this.lock = function(client,record,child_name,lock_type) { - + this.lock = function(client,record,lock_type) { switch(lock_type) { /* if the client wants to read... */ - case settings.LOCK_READ: + case this.settings.LOCK_READ: + /* if this is a duplicate lock attempt */ + if(record.info.lock[client.id]) { + this.error(client,this.settings.ERROR_DUPLICATE_LOCK); + return true; + } switch(record.info.lock_type) { - case settings.LOCK_READ: + case this.settings.LOCK_READ: /* if there are any pending write locks, deny */ if(record.info.lock_pending) { return false; } /* otherwise, we can read lock */ else { - record.shadow[child_name]._lock[client.id] = new Lock( + record.shadow[record.child_name]._lock[client.id] = new Lock( lock_type, Date.now() ); return true; } /* we cant lock a record that is already locked for reading */ - case settings.LOCK_WRITE: + case this.settings.LOCK_WRITE: return false; /* if the record isnt locked at all, we can lock */ - case settings.LOCK_NONE: - record.shadow[child_name]._lock[client.id] = new Lock( + case this.settings.LOCK_NONE: + record.shadow[record.child_name]._lock[client.id] = new Lock( lock_type, Date.now() ); @@ -198,33 +182,39 @@ Repository = new (function() { } break; /* if the client wants to write... */ - case settings.LOCK_WRITE: + case this.settings.LOCK_WRITE: + /* if this is a duplicate lock attempt */ + if(record.info.lock[client.id]) { + this.error(client,this.settings.ERROR_DUPLICATE_LOCK); + return true; + } switch(record.info.lock_type) { /* ...and the record is already locked, flag for pending write lock */ - case settings.LOCK_READ: - case settings.LOCK_WRITE: - record.shadow[child_name]._lock_pending=true; + case this.settings.LOCK_READ: + case this.settings.LOCK_WRITE: + record.shadow[record.child_name]._lock_pending=true; return false; /* ...and the record isnt locked, lock for writing and remove flag */ - case settings.LOCK_NONE: - record.shadow[child_name]._lock[client.id] = new Lock( + case this.settings.LOCK_NONE: + record.shadow[record.child_name]._lock[client.id] = new Lock( lock_type, Date.now() ); - record.shadow[child_name]._lock_pending=false; + record.shadow[record.child_name]._lock_pending=false; return true; } break; /* if the client wants to unlock, check credentials */ - case settings.LOCK_UNLOCK: + case this.settings.LOCK_UNLOCK: /* if the client has a lock on this record, release the lock */ - if(record.shadow[child_name]._lock[client.id]) { - delete record.shadow[child_name]._lock[client.id]; + if(record.shadow[record.child_name]._lock[client.id]) { + delete record.shadow[record.child_name]._lock[client.id]; return true; } /* otherwise deny */ else { - return false; + this.error(client,this.settings.ERROR_NOT_LOCKED); + return true; } } /* fallthrough? */ @@ -232,163 +222,188 @@ Repository = new (function() { }; /* server's data retrieval method (not directly called by client) */ - this.read = function(client,record,child_name) { + this.read = function(client,record) { /* if this client has this record locked, read */ - if(record.shadow[child_name]._lock[client.id]) { - client.send(record.data[child_name]); + if(record.info.lock[client.id]) { + var data = clone(record.data[record.child_name]); + send_packet(client,data,"RESPONSE"); return true; } /* if there is no lock for this client, error */ else { - client.send(new Error(settings.ERROR_NOT_LOCKED)); return false; } }; /* server's data submission method (not directly called by client) */ - this.write = function(client,record,child_name,data) { + this.write = function(client,record,data) { /* if this client has this record locked */ - if(record.shadow[child_name]._lock[client.id] && - record.shadow[child_name]._lock[client.id].type == settings.LOCK_WRITE) { - record.data=data; - record.shadow=composite_sketch(data); + if(record.info.lock[client.id] && + record.info.lock[client.id].type == this.settings.LOCK_WRITE) { + record.data[record.child_name]=data; + /* populate this object's children with shadow objects */ + composite_sketch(record.data[record.child_name],record.shadow[record.child_name]); /* send data updates to all subscribers */ send_updates(record); + this.updates=true; return true; } /* if there is no lock for this client, error */ else { - client.send(new Error(settings.ERROR_NOT_LOCKED)); return false; } }; /* remove a record from the database (requires WRITE_LOCK) */ - this.delete = function(client,record,child_name) { + this.delete = function(client,record) { /* if this client has this record locked */ - if(record.shadow[child_name]._lock[client.id] && - record.shadow[child_name]._lock[client.id].type == settings.LOCK_WRITE) { - delete record.data[child_name]; - delete record.shadow[child_name]; + if(record.shadow[record.child_name]._lock[client.id] && + record.shadow[record.child_name]._lock[client.id].type == this.settings.LOCK_WRITE) { + delete record.data[record.child_name]; + delete record.shadow[record.child_name]; /* send data updates to all subscribers */ send_updates(record); + this.updates=true; return true; } /* if there is no lock for this client, error */ else { - client.send(new Error(settings.ERROR_NOT_LOCKED)); + this.error(client,this.settings.ERROR_NOT_LOCKED); return false; } }; - /* create a new database record (requires WRITE_LOCK) - TODO: it seems this method may end up doing the exact same thing - as the put() method, do we need it? can we lock an object that - doesn't yet exist? does it matter? */ - this.create = function(client,record,child_name,data) { - - /* let parent adopt this new child */ - record.data[child_name] = data; - /* create shadow data for this object */ - record.shadow[child_name] = composite_sketch(data); - /* send data updates to all subscribers */ - send_updates(record); + /* return an object representing a record's overall subscriber and lock status */ + this.status = function(client,record) { + var sub=[]; + for(var s in record.info.subscribers) + sub.push(s); + var data={ + subscribers:sub, + lock:record.info.lock_type, + pending:record.info.lock_pending, + location:record.parent_name + "." + record.child_name + } + send_packet(client,data,"RESPONSE"); + return true; + } - /* TODO: track subscribers and distribute updates */ - return true; - }; - /* generic query handler, will process locks, reads, writes, and unlocks and put them into the appropriate queues */ this.query = function(client,query) { - /* strip the last child identifier from the string */ - var parent_name = query.object_name.substring(0,query.object_name.lastIndexOf(".")); + var parent_name = query.location.substring(0,query.location.lastIndexOf(".")); /* store the child name */ - var child_name = query.object_name.substr(query.object_name.lastIndexOf(".")+1); + var child_name = query.location.substr(query.location.lastIndexOf(".")+1); /* temporary array for queue additions */ var q=[]; - /* push this query into a queue to be processed at the next response cycle (this.cycle()) */; - q.push(new Request( - client, - query.operation, - parent_name, - child_name, - query.data - )); + + /* if an operation is requested */ + if(query.operation !== undefined) { + request = new Request(client,query.operation,parent_name,child_name,query.data); + /* push this query into a queue to be processed at the next response cycle (this.cycle()) */; + q.push(request); + } /* if there is an attached lock operation, process accordingly */ - switch(query.lock) { - /* if this is a read or write lock, put it ahead of the operation in the request queue */ - case settings.LOCK_READ: - case settings.LOCK_WRITE: - q.unshift(new Request( - client, - "LOCK", - parent_name, - child_name, - query.lock - )); - break; - /* if this is an unlock, process it after the operation in the request queue */ - case settings.LOCK_UNLOCK: - q.push(new Request( - client, - "LOCK", - parent_name, - child_name, - query.lock - )); - break; - /* if no lock has been specified, do nothing */ - case settings.LOCK_NONE: - break; + if(query.lock !== undefined) { + request = new Request(client,"LOCK",parent_name,child_name,query.lock); + /* if this is a read or write lock, put it ahead of the operation in the request queue */ + if(query.lock == this.settings.LOCK_UNLOCK) + q.unshift(request); + /* if this is an unlock, process it after the operation in the request queue */ + else if(query.lock == this.settings.LOCK_READ || query.lock == this.settings.LOCK_WRITE) + q.push(request); } /* add the temporary queue to the main queue */ this.queue=this.queue.concat(q); } + /* generate an error object and send it to client */ + this.error = function(client,error_num) { + var error_desc="Unknown error"; + switch(error_num) { + case this.settings.ERROR_INVALID_REQUEST: + error_desc="Invalid record request"; + break; + case this.settings.ERROR_OBJECT_NOT_FOUND: + error_desc="Record not found"; + break; + case this.settings.ERROR_NOT_LOCKED: + error_desc="Record not locked"; + break; + case this.settings.ERROR_LOCKED_WRITE: + error_desc="Record locked for writing"; + break; + case this.settings.ERROR_LOCKED_READ: + error_desc="Record locked for reading"; + break; + case this.settings.ERROR_DUPLICATE_LOCK: + error_desc="Duplicate record lock request"; + break; + } + var error=new Error(error_num,error_desc); + send_packet(client,error,"ERROR"); + } + /* internal periodic data storage method TODO: this should probably eventually be a background thread to prevent lag when writing a large database to file */ this.save = function(timestamp) { - /* strip _location tags from data before saving */ - dismember(this.data); - //TODO: create n backups before overwriting data file - this.file.open("w"); - // This function gets called every 30 seconds or so - // And flushes all objects to disk in case of crash - // Also, this is called on clean exit. - this.file.write(JSON.stringify(this.data)); - this.file.close(); - - settings.LAST_SAVE=timestamp; + if(!this.file) + return; + if(!timestamp) + timestamp = Date.now(); + /* if we are due for a data update, save everything to file */ + if(timestamp - this.settings.LAST_SAVE >= this.settings.AUTO_SAVE + && this.updates) { + //TODO: create n backups before overwriting data file + this.file.open("w"); + // This function gets called every 30 seconds or so + // And flushes all objects to disk in case of crash + // Also, this is called on clean exit. + this.file.write(JSON.stringify(this.data)); + this.file.close(); + this.settings.LAST_SAVE=timestamp; + } }; /* data initialization (likely happens only once) */ this.load = function() { - this.file.open("r"); - this.data=JSON.parse(this.file.readAll(settings.FILE_BUFFER).join('\n')); - this.file.close(); + if(!this.file) + return; + if(file_exists(this.file.name)) { + this.file.open("r"); + this.data = JSON.parse( + this.file.readAll(this.settings.FILE_BUFFER).join('\n') + ); + this.file.close(); + } }; /* create a copy of data object keys and give them locking properties */ - this.init=function() { + this.init = function() { + this.load(); this.shadow=composite_sketch(this.data); + /* initialize autosave timer */ - settings.LAST_SAVE = Date.now(); + this.settings.LAST_SAVE = Date.now(); + log(LOG_INFO,"JSON database initialized (v" + this.VERSION + ")"); } - /* main "loop" called by server */ - this.cycle=function(timestamp) { - - if(!timestamp) - timestamp = Date.now(); + /* release any locks or subscriptions held by a disconnected client */ + this.release = function(client_id) { + free_prisoner(client_id,this.shadow); + for(var c=0;c<this.queue.length;c++) { + if(this.queue[c].client.id == client_id) + this.queue.splice(c--,1); + } + } - /* if we are due for a data update, save everything to file */ - if(timestamp - settings.LAST_SAVE >= settings.AUTO_SAVE) - this.save(timestamp); + /* main "loop" called by server */ + this.cycle = function(timestamp) { + this.save(); /* process request queue, removing successful operations */ for(var r=0;r<this.queue.length;r++) { @@ -396,137 +411,166 @@ Repository = new (function() { var result=false; /* locate the requested record within the database */ - var record=identify_remains.call(this,request.client,request.parent_name); + var record=identify_remains.call(this,request.client,request.parent_name,request.child_name); + + if(!record) { + log(LOG_DEBUG,"db: bad request removed from queue"); + this.queue.splice(r--,1); + } switch(request.operation.toUpperCase()) { case "READ": - result=this.read(request.client,record,request.child_name); + result=this.read(request.client,record); break; case "WRITE": - result=this.write(request.client,record,request.child_name,request.data); + result=this.write(request.client,record,request.data); break; case "DELETE": - result=this.delete(request.client,record,request.child_name); + result=this.delete(request.client,record); break; case "CREATE": - result=this.create(request.client,record,request.child_name,request.data); + result=this.create(request.client,record,request.data); break; case "SUBSCRIBE": - result=this.subscribe(request.client,record,request.child_name); + result=this.subscribe(request.client,record); break; case "UNSUBSCRIBE": - result=this.unsubscribe(request.client,record,request.child_name); + result=this.unsubscribe(request.client,record); break; case "LOCK": - result=this.lock(request.client,record,request.child_name,request.data); + result=this.lock(request.client,record,request.data); + break; + case "STATUS": + result=this.status(request.client,record); break; } if(result == true) { - log(LOG_DEBUG, - "done: " + + log(LOG_DEBUG,"db: " + request.operation + " " + request.parent_name + "." + - request.child_name + request.child_name + " OK" ); this.queue.splice(r--,1); } else { - log(LOG_DEBUG, - "fail: " + + log(LOG_DEBUG,"db: " + request.operation + " " + request.parent_name + "." + - request.child_name - ); + request.child_name + " FAILED" + ); } - } + } }; - /*************************** repository functions ****************************/ + /*************************** database functions ****************************/ /* traverse object and create a shadow copy of the object structure - for record locking and subscribers, and create hash names for database objects */ - function composite_sketch(obj,name,shadow,location) { - - /* generate serialized location (maybe this can be removed )*/ - if(!location) - location=name; - else - location+="."+name; - + for record locking and subscribers, and create location names for database objects */ + function composite_sketch(obj,shadow) { /* create shadow object */ - shadow=new Shadow(); + if(!shadow) + shadow=new Shadow(); /* iterate object members */ for(var p in obj) { - shadow[p]=composite_sketch(obj[p],p,shadow[p],location); + if(typeof obj[p] == "object") + shadow[p]=composite_sketch(obj[p],shadow[p]); } - /* assign generated location id to this object */ - obj._location=location; - /* returns an object containing the passed objects property keys with their own lock, subscribers, and name properties also adds a location property to keys of original object */ + return shadow; } - /* traverse object and remove location (hash name) tags */ - function dismember(obj) { - delete obj._location; - for(var p in obj) { - dismember(obj[p]); - } - } - - /* parse an object hash name and return the object (ex: dicewarz2.games.1.players.1.tiles.0) + /* parse an object location name and return the object (ex: dicewarz2.games.1.players.1.tiles.0) an object containing the corresponding data and its shadow object */ - function identify_remains(client,object_name) { + function identify_remains(client,parent_name,child_name) { - var p=object_name.split(/\./); - /* if the data request is invalid or empty, return an empty object */ + var p=parent_name.split(/\./); + /* if the data request is invalid or empty, return an error */ if(!p) { - // Error! - client.send(new Error(settings.ERROR_INVALID_REQUEST)); + this.error(client,this.settings.ERROR_INVALID_REQUEST); return false; } var data=this.data; var shadow=this.shadow; var info={ - lock_type:settings.LOCK_NONE, - lock_pending:false + lock:[], + lock_type:this.settings.LOCK_NONE, + lock_pending:false, + subscribers:[] } /* iterate through split object name checking the keys against the database and checking the lock statuses against the shadow copy */ for each(var c in p) { + verify(data,shadow,c); + + /* keep track of current object, and store the immediate parent of the request object */ data=data[c]; shadow=shadow[c]; - - /* if this key doesnt exist in the database, return error */ - if(!data || !shadow) { - client.send(new Error(settings.ERROR_OBJECT_NOT_FOUND)); - return false; - } /* check the current object's lock and subscriber status along the way */ info = investigate(shadow,info); } + verify(data,shadow,child_name); + /* continue on through the selected shadow object's children to check for locked children */ - info = search_party(data,shadow,info); + info = search_party(data[child_name],shadow[child_name],info); /* return selected database object, shadow object, and overall lock status of the chosen tree */ - return new Record(data,shadow,info); + return new Record(data,shadow,parent_name,child_name,info); } - + + /* if the requested child object does not exist, create it */ + function verify(data,shadow,child_name) { + if(!data[child_name]) { + log(LOG_DEBUG,"creating new data: " + child_name); + data[child_name]={}; + } + if(!shadow[child_name]) { + log(LOG_DEBUG,"creating new shadow: " + child_name); + shadow[child_name]=new Shadow(); + } + } + + /* release subscriptions and locks on an object recursively */ + function free_prisoner(client_id,shadow) { + if(shadow._lock) { + if(shadow._lock[client_id]) { + log(LOG_DEBUG,"releasing lock: " + client_id); + delete shadow._lock[client_id]; + } + } + if(shadow._subscribers) { + if(shadow._subscribers[client_id]) { + log(LOG_DEBUG,"releasing subscriber: " + client_id); + delete shadow._subscribers[client_id]; + } + } + for(var s in shadow) { + if(typeof shadow[s] == "object") + free_prisoner(client_id,shadow[s]); + } + } + + /* make a copy of an object to avoid modifying the original */ + function clone(obj) { + var newobj=eval(obj.toSource()); + return newobj; + } + /* return the prevailing lock type and pending lock status for an object */ function investigate(shadow, info) { /* if we havent found a write locked record yet, keep searching */ if(info.lock_type == undefined) { for(var l in shadow._lock) { info.lock_type = shadow._lock[l].type; - break; + info.lock[l] = shadow._lock[l]; } } for(var s in shadow._subscribers) { @@ -552,12 +596,24 @@ Repository = new (function() { /* send updates of this object to all subscribers */ function send_updates(record) { for each(var c in record.info.subscribers) { - c.send(JSON.stringify(record.data)+"\r\n"); + var data = { + location:record.parent_name + "." + record.child_name, + data:record.data[record.child_name] + }; + send_packet(c,data,"UPDATE"); } } + /* send data packet to a client */ + function send_packet(client,object,func) { + var data={ + func:func, + data:object + }; + client.sendJSON(data); + } + /* constructor */ - this.load(); this.init(); })();