1 /**
  2 	@class
  3 	@extends rio.Attr
  4 	
  5 	Model is used to create model classes in a rio application.  It provides ActiveResource style synchronization with
  6 	a rest-based resource and automated client-server bindings with a push server.
  7 	
  8 	@author Jason Tillery
  9 	@copyright 2008-2009 Thinklink LLC
 10 */
 11 rio.Model = {
 12 	create: function() {
 13 		var args = $A(arguments);
 14 		if (args.length > 0 && args.last() != undefined && !args.last().ATTR) {
 15 			args[args.size() - 1] = Object.extend({ noExtend: true }, args.last());
 16 		}
 17 		var model = rio.Attr.create.apply(this, args);
 18 		if (model._fields.id == undefined || model.prototype.setId == undefined) {
 19 			model.attrAccessor("id");
 20 		}
 21 		model.attrEvent("destroy");
 22 		Object.extend(model, {
 23 			resource: function(url) {
 24 				model.addMethods({
 25 					isNew: function() {
 26 						return this.getId() == undefined || (this.getId().temporary != undefined && this.getId().temporary());
 27 					},
 28 					
 29 					url: function() {
 30 						return model.url() + "/" + this.getId();
 31 					},
 32 
 33 					save: function(options) {
 34 						if (this.__destroying) { return; }
 35 						
 36 						if (this.valid && !this.valid()) { return; }
 37 						// var idField = Object.keys(model._fields).detect(function(field) {
 38 						// 	var val = this["_" + field];
 39 						// 	return field != "id" && val && val.temporary && val.temporary();
 40 						// }.bind(this));
 41 						// if (idField) {
 42 						// 	this["_" + idField].doAfterReification(this.save.curry(options).bind(this));
 43 						// 	return;
 44 						// }
 45 						
 46 						// if (this._creating) {
 47 						// 	if (!this._pendingUpdates) { this._pendingUpdates = []; }
 48 						// 	this._pendingUpdates.push(this.save.curry(options).bind(this));
 49 						// 	return;
 50 						// }
 51 						var firstTimeCreating = !this._creating;
 52 						this._creating = true;
 53 						if(this.isNew() && this.beforeCreate && firstTimeCreating){ this.beforeCreate(); }
 54 						
 55 						model.addToTransaction(this, options);
 56 					},
 57 
 58 					destroy: function(options) {
 59 						if(this.__destroying) {return;}
 60 						
 61 						if (this.beforeDestroy) { this.beforeDestroy(); }
 62 						
 63 						this.__destroying = true;
 64 						
 65 						model.addToTransaction(this, Object.extend({destroy: true}, options));
 66 						this.removeFromCaches();
 67 
 68 						this.fire("destroy");
 69 					},
 70 					
 71 					parameters: function() {
 72 						var modelKey = model.url().match(/^\/?(.*)$/)[1].singularize();
 73 						var parameters = {};
 74 						
 75 						var persistentFieldNames = model.persistentFieldNames();
 76 						for (var i=persistentFieldNames.length; i--;) {
 77 							var name = persistentFieldNames[i];
 78 							var currentState = this["_" + name];
 79 							var lastState = this._lastSavedState ? this._lastSavedState[name] : undefined;
 80 
 81 							if (currentState != lastState) {
 82 								parameters[modelKey + "[" + name.underscore() + "]"] = currentState;
 83 							}
 84 						}
 85 						return parameters;
 86 					},
 87 					
 88 					afterUpdateField: function() {
 89 						model.updateInCollectionEntites(this);
 90 					},
 91 					
 92 					attributeState: function() {
 93 						var state = {};
 94 
 95 						var persistentFieldNames = model.persistentFieldNames();
 96 						for (var i=persistentFieldNames.length; i--;) {
 97 							var f = persistentFieldNames[i];
 98 							state[f] = this[("get-" + f).camelize()]();
 99 						}
100 						return state;
101 					},
102 
103 					attributeStateChange: function() {
104 						var state = {};
105 
106 						var persistentFieldNames = model.persistentFieldNames();
107 						for (var i=persistentFieldNames.length; i--;) {
108 							var f = persistentFieldNames[i];
109 							var currentState = this["_" + f];
110 							var lastState = this._lastSavedState ? this._lastSavedState[f] : undefined;
111 							if (currentState != lastState) {
112 								state[f] = currentState;
113 							}
114 						}
115 
116 						return state;
117 					},
118 					
119 					removeFromCaches: function() {
120 						model.removeFromCollectionEntities(this);
121 						model.removeFromCache(this.getId());
122 					},
123 					
124 					toString: function() {
125 						return "[rio.models.*]";
126 					}
127 				});
128 				
129 				Object.extend(model, {
130 					undoEnabled: false,
131 
132 					url: function() {
133 						return url;
134 					},
135 					
136 					_idCache: {},
137 					id: function(val, createIfNotFound) {
138 						if (val) {
139 							if (this._idCache[val]) { return this._idCache[val]; }
140 							if (createIfNotFound) {
141 								var id = new rio.Id(val);
142 								this._idCache[val] = id;
143 								return id;
144 							}
145 						} else {
146 							return new rio.Id();
147 						}
148 					},
149 
150 					reifyId: function(id, val) {
151 						id.reify(val);
152 						if (this._idCache[val]) {
153 							rio.warn("id collision while reifying - " + model + "#" + id);
154 						}
155 						this._idCache[val] = id;
156 					},
157 					
158 					persistentFieldNames: function() {
159 						return Object.keys(model._fields).reject(function(f) {
160 							return model._clientOnlyAttrs.include(f) || model.prototype[("set-" + f).camelize()] == undefined || f == "id";
161 						});
162 					},
163 					
164 					_transaction: [],
165 					addToTransaction: function(entity, options) {
166 						options = options || {};
167 						var existingTransaction = this._transaction.detect(function(t) { return t.entity == entity; });
168 						if (existingTransaction) {
169 							if (options.destroy) {
170 								if (existingTransaction.entity.isNew()) {
171 									this._transaction.splice(this._transaction.indexOf(existingTransaction), 1);
172 									// don't need to do this, now that we immediately remove on destroy
173 									// model.removeFromCollectionEntities(existingTransaction.entity);
174 								} else {
175 									existingTransaction.options = options;
176 								}
177 							} else {
178 								existingTransaction.attributeState = entity.attributeStateChange();
179 								existingTransaction.parameters = entity.parameters();
180 							}
181 							// TODO: chain onSuccess/onFailure functions
182 						} else {
183 							this._transaction.push({
184 								entity: entity, 
185 								options: options, 
186 								attributeState: entity.attributeStateChange(),
187 								parameters: entity.parameters()
188 							});
189 						}
190 						if (!this._transactionQueued) {
191 							this._transactionQueued = true;
192 							this.prepareTransaction();
193 						}
194 					},
195 
196 					/* Simply defers execution.  Can override this for testing. */
197 					prepareTransaction: function() {
198 						this.executeTransaction.bind(this, rio.Undo.isProcessingUndo(), rio.Undo.isProcessingRedo()).defer();
199 					},
200 					
201 					__transactionInProgress: false,
202 					__queuedTransactions: [],
203 					executeTransaction: function(undoTransaction, redoTransaction) {
204 						this._transactionQueued = false;
205 						
206 						var transaction = this._transaction.clone();
207 						this._transaction.clear();
208 						
209 						// Transactions can be queued here, so lets make sure that all entities _lastSavedState's are
210 						// correct before letting subsequent transaction queue up.
211 						for (var i=0, len=transaction.length; i<len; i++) {
212 							var t = transaction[i];
213 							t.oldLastSavedState = t.entity._lastSavedState;
214 							t.entity._lastSavedState = t.options.destroy ? undefined : t.entity.attributeState();
215 						}
216 						
217 						transaction.undoTransaction = undoTransaction;
218 						transaction.redoTransaction = redoTransaction;
219 						if (this.__transactionInProgress) {
220 							this.__queuedTransactions.push(transaction);
221 							return;
222 						} else {
223 							this._doExecuteTransaction(transaction);
224 						}
225 					},
226 
227 					_doExecuteTransaction: function(transaction) {
228 						if (transaction.empty()) { return; }
229 						
230 						if (model.undoEnabled) {
231 							var undos = transaction.map(function(t) {
232 								var undo = { id: t.entity.getId(), state: t.oldLastSavedState, destroy: t.options.destroy };
233 								// t.entity._lastSavedState = t.options.destroy ? undefined : t.entity.attributeState();
234 								return undo;
235 							});
236 
237 							var processUndos = function() {
238 								undos.reverse().each(function(u) {
239 									if (u.state) {
240 										if (u.destroy) {
241 											var instance = new model(Object.extend({ id: u.id }, u.state));
242 											instance._lastSavedState = undefined;
243 											instance.save();
244 										} else {
245 											model.getFromCache(u.id).updateAttributes(u.state);
246 										}
247 									} else {
248 										model.getFromCache(u.id).destroy();
249 									}
250 								});
251 							};
252 
253 							if (transaction.undoTransaction) {
254 								rio.Undo.registerRedo(processUndos);
255 							} else {
256 								rio.Undo.registerUndo(processUndos, transaction.redoTransaction);
257 							}
258 						}
259 						
260 						// // Always update the last saved states
261 						// for (var i=0, len=transaction.length; i<len; i++) {
262 						// 	var t = transaction[i];
263 						// 	
264 						// 	// LOOKS LIKE A BUG
265 						// 	// TODO: SHOULD PROBABLY BE USING THE t.attributeState instead of t.entity.attributeState()
266 						// 	t.entity._lastSavedState = t.options.destroy ? undefined : t.entity.attributeState();
267 						// }
268 						
269 						var createSuccess = function(entity, onSuccess, json) {
270 							entity._creating = false;
271 							var results = model._filterAndProcessJsonWhileAccumulatingCollectionEntities(json);
272 							var idString = results[0].id;
273 							if (entity.getId() && entity.getId().temporary && entity.getId().temporary()) {
274 								var id = parseInt(idString, 10);
275 								model.reifyId(entity.getId(), id);
276 							} else {
277 								// NOTE: An entity will not be updated with any attributes but the ID on create success
278 								entity._id = model.id(parseInt(idString, 10), true);
279 								model.putInCache(entity._id, entity);
280 							}
281 							onSuccess(entity);
282 							if (entity.afterCreate) { entity.afterCreate(); }
283 							// if (entity._pendingUpdates) {
284 							// 	entity._pendingUpdates.each(function(u) { u(); });
285 							// }
286 							return results[1];
287 						};
288 						
289 						var updateSuccess = function(entity, onSuccess) {
290 							onSuccess(entity);
291 						};
292 						
293 						var destroySuccess = function(entity, onSuccess) {
294 							// don't need to do this, now that we immediately remove on destroy
295 							// model.removeFromCollectionEntities(entity);
296 							(onSuccess || Prototype.emptyFunction)();
297 						};
298 						
299 						var url;
300 						var method;
301 						var parameters;
302 						var onSuccess;
303 						var onFailure;
304 						var onConnectionFailure;
305 
306 						if (transaction.length > 1) {
307 							url = model.url();
308 							method = "post";
309 							
310 							parameters = {};
311 							transaction.each(function(t) {
312 								var id = t.entity.getId();
313 								if (t.options.destroy) {
314 									parameters["transaction[" + id + "]"] = "delete";
315 								} else {
316 									for (var f in t.attributeState) {
317 										var val = t.attributeState[f];
318 										if (val && val.toString) { val = val.toString(); }
319 										parameters["transaction[" + id + "][" + f.underscore() + "]"] = val;
320 									}
321 								}
322 							});
323 							
324 							onSuccess = function(response) {
325 								var afterFunctions = transaction.map(function(t) {
326 									var entity = t.entity;
327 									var options = t.options;
328 
329 									options = options || {};
330 									options.onSuccess = options.onSuccess || Prototype.emptyFunction;
331 									
332 									var json = response.responseJSON.transaction[t.entity.getId()];
333 									return (
334 										options.destroy ? destroySuccess : (entity.isNew() ? createSuccess : updateSuccess)
335 									)(entity, options.onSuccess, json);
336 								});
337 								
338 								afterFunctions.compact().each(function(af) {
339 									af();
340 								});
341 							}.bind(this);
342 							
343 							onFailure = function() {
344 								var handled = true;
345 								transaction.each(function(t) {
346 									if (t.options.onFailure) {
347 										t.options.onFailure();
348 									} else {
349 										handled = false;
350 									}
351 								});
352 								if (!handled) {
353 									rio.Application.fail("Failed creating, updating, or destroying.", model + "\n" + Object.toJSON(parameters));
354 								}
355 							}.bind(this);
356 							
357 							onConnectionFailure = function() {
358 								transaction.each(function(t) {
359 									if (t.options.onConnectionFailure) {
360 										t.options.onConnectionFailure();
361 									}
362 								});
363 							};
364 						} else {
365 							var entity = transaction.first().entity;
366 							var options = transaction.first().options;
367 							
368 							options = options || {};
369 							options.onSuccess = options.onSuccess || Prototype.emptyFunction;
370 
371 							parameters = options.parameters || transaction.first().parameters;
372 							url = entity.isNew() ? model.url() : entity.url();
373 							method = options.destroy ? "delete" : (entity.isNew() ? "post" : "put");
374 							onSuccess = function(response) {
375 								var after = (
376 									options.destroy ? destroySuccess : (entity.isNew() ? createSuccess : updateSuccess)
377 								)(entity, options.onSuccess, response.responseJSON);
378 								if (after) { after(); }
379 							};
380 							onFailure = options.onFailure;
381 							onConnectionFailure = options.onConnectionFailure;
382 						}
383 						
384 						this.__transactionInProgress = true;
385 						new Ajax.Request(url, { 
386 				            asynchronous: true,
387 							method: method,
388 							evalJSON: true,
389 							parameters: $H(parameters).merge({
390 								'transaction_key': rio.environment.transactionKey,
391 								'authenticity_token': rio.Application.getToken()
392 							}).toObject(),
393 				            onSuccess: function(response) {
394 								if (response.status == 0) {
395 									if (onConnectionFailure) {
396 										onConnectionFailure();
397 									} else {
398 										rio.Application.fail("Connection Failure", model + ":\n" + Object.toJSON(parameters));
399 									}
400 								} else {
401 									onSuccess(response);
402 								}
403 							},
404 							onFailure: function(response) { 
405 								// if (!entity.__destroying) {
406 									if (onFailure) {
407 										onFailure(response);
408 									} else {
409 										rio.Application.fail("Failed creating, updating, or destroying.", model + ":\n" + Object.toJSON(parameters));
410 									}
411 								// }
412 							}.bind(this),
413 							onComplete: function(response) {
414 								this.__transactionInProgress = false;
415 								if (!this.__queuedTransactions.empty()) {
416 									this._doExecuteTransaction(this.__queuedTransactions.shift());
417 								}
418 							}.bind(this)
419 				        });
420 
421 						transaction.each(function(t) {
422 							if (t.entity.isNew()) {
423 								t.entity._creating = true;
424 							}
425 							if (t.options.destroy) {
426 								t.entity.__destroying = true;
427 							}
428 						});						
429 					},
430 
431 					create: function(options) {
432 						var obj = new model(options);
433 						obj.save(options);
434 						return obj;
435 					},
436 
437 					find: function(id, options) {
438 						if (id == undefined) { return; }
439 						var rioId = id.constructor == rio.Id ? id : model.id(id);
440 
441 						options = options || {};
442 						var asynchronous = options.asynchronous == undefined || options.asynchronous;
443 
444 						if (options.onSuccess == undefined) {
445 							asynchronous = false;
446 							options.onSuccess = Prototype.emptyFunction;
447 						}
448 						
449 						var existing = this.getFromCache(rioId);
450 						if (existing) {
451 							options.onSuccess(existing);
452 							return existing;
453 						}
454 						
455 						var entity;
456 						
457 						new Ajax.Request(this.url() + "/" + id, {
458 							asynchronous: asynchronous,
459 							method: 'get',
460 							evalJSON: true,
461 							onSuccess: function(response) {
462 								entity = new model(model._filterAndProcessJson(response.responseJSON));
463 								options.onSuccess(entity);
464 							},
465 							onFailure: options.onFailure || Prototype.emptyFunction
466 						});
467 						if (!asynchronous) {
468 							return entity;
469 						}
470 					},
471 
472 					findAll: function(options) {
473 						options = options || {};
474 						var asynchronous = options.asynchronous == undefined || options.asynchronous;
475 						
476 						if (options.onSuccess == undefined) {
477 							asynchronous = false;
478 							options.onSuccess = Prototype.emptyFunction;
479 						}
480 						 
481 						var idField = Object.keys(options.parameters).detect(function(parameter) {
482 							var val = options.parameters[parameter];
483 							return val && val.temporary && val.temporary();
484 						});
485 
486 						var urlToUse = options.url || this.url();
487 						var parameters = new rio.Parameters(options.parameters || {}, options.nonAjaxParameters);
488 						// Assume that there are no entities yet (since a param is unreified)
489 						// New entities will be added to the collectionEntity as they are created or broadcast
490 						// If this poses a problem, we can always schedule a find for after reification and then reconcile the CE's
491 						if (idField) {
492 							var collectionEntity = rio.CollectionEntity.create({
493 								model: model,
494 								values: [],
495 								condition: parameters.conditionFunction()
496 							});
497 							this.putCollectionEntity(urlToUse + "#" + Object.toJSON(parameters.ajaxParameters()), collectionEntity);
498 							
499 							Object.values(model._identityCache).each(function(entity) {
500 								collectionEntity.add(entity);
501 							});
502 							
503 							options.onSuccess(collectionEntity);
504 							return collectionEntity;
505 						}
506 						
507 						
508 						if (this._collectionEntities[urlToUse + "#" + Object.toJSON(parameters.ajaxParameters())]) {
509 							var found = this._collectionEntities[urlToUse + "#" + Object.toJSON(parameters.ajaxParameters())];
510 							found.prepare();
511 							options.onSuccess(found);
512 							if (!asynchronous) {
513 								return found;
514 							} else {
515 								return;
516 							}
517 						}
518 
519 						var results;
520 						rio.Model._findAllRequests.push(
521 							new Ajax.Request(urlToUse, { 
522 								asynchronous: asynchronous,
523 								method: 'get',
524 								evalJSON: true,
525 								parameters: {conditions: Object.toJSON(parameters.ajaxParameters())},
526 								onSuccess: function(response) {
527 									results = this._collectionEntityFromJson(response.responseJSON, parameters, urlToUse);
528 									results.prepare();
529 									options.onSuccess(results);
530 								}.bind(this)
531 							})
532 						);
533 						if (!asynchronous) {
534 							return results;
535 						}
536 					},
537 					
538 					_hasManyAssociations: {},
539 					hasMany: function(hasManyName) {
540 						var options = {};
541 						if (!Object.isString(hasManyName)) {
542 							options = hasManyName[1] || {};
543 							hasManyName = hasManyName[0];
544 						}
545 						options.className = hasManyName.singularize().classize();
546 						options.foreignKey = this.NAME.toLowerCase() + "Id";
547 						options.parameters = options.parameters || {};
548 
549 						this._hasManyAssociations[hasManyName] = options;
550 						
551 						this.attrAccessor(hasManyName);
552 						this.clientOnlyAttr(hasManyName);
553 						
554 						var getName = ("get-" + hasManyName).camelize();
555 
556 						this.prototype[getName] = this.prototype[getName].wrap(function(proceed) {
557 							if (this["_" + hasManyName] == undefined) {
558 								var parameters = Object.clone(options.parameters);
559 								parameters[options.foreignKey] = this.getId();
560 								rio.models[options.className].findAll({
561 									asynchronous: false,
562 									parameters: parameters,
563 									onSuccess: function(entities) {
564 										this["_" + hasManyName] = entities;
565 									}.bind(this)
566 								});
567 							}
568 							return proceed.apply(this, $A(arguments).slice(1));
569 						});
570 					},
571 					
572 					belongsTo: function(args) {
573 						var options = {};
574 						var associationName;
575 						if (!Object.isString(args)) {
576 							associationName = args[0];
577 							options = args[1] || {};
578 						} else {
579 							associationName = args;
580 						}
581 						this.attrAccessor(associationName);
582 						this.clientOnlyAttr(associationName);
583 						
584 						var className = options.className || associationName.classize();
585 						var foreignKey = options.foreignKey || associationName + "Id";
586 						var getName = ("get-" + associationName).camelize();
587 						
588 						this.prototype[getName] = this.prototype[getName].wrap(function(proceed) {
589 							if (this["_" + associationName] == undefined) {
590 								var setAssociation = function() {
591 									var associationId = this["_" + foreignKey];
592 									var foundValue = rio.models[className].find(associationId, {
593 										asynchronous: false
594 									});
595 									this[("set-" + associationName).camelize()](foundValue);
596 								}.bind(this);
597 								setAssociation();
598 								this[foreignKey].bind(setAssociation, true);
599 							}
600 							return proceed.apply(this, $A(arguments).slice(1));
601 						});
602 					},
603 					
604 					_parametersFromJsonParameters: function(params) {
605 						return new rio.Parameters(
606 							Object.keys(params).inject({}, function(acc, p) {
607 								acc[p.camelize()] = params[p];
608 								return acc;
609 							})
610 						);
611 					},
612 					
613 					_processIncludedCollectionEntities: function(json) {
614 						for (var i=json.length; i--;) {
615 							var include = json[i];
616 							params = this._parametersFromJsonParameters(include.parameters);
617 							var url = rio.models[include.className].url();
618 							if (this._collectionEntities[url + "#" + Object.toJSON(params.ajaxParameters())] == undefined) {
619 								rio.models[include.className]._collectionEntityFromJson(include.json, params, url);
620 							}
621 						}
622 					},
623 					
624 					_filterAndProcessJsonWhileAccumulatingCollectionEntities: function(inJson) {
625 						if (inJson._set) {
626 							var ceFunction = function() {
627 								model._processIncludedCollectionEntities(inJson._set.include);
628 							};
629 							return [rio.Model.filterJson(inJson._set.self), ceFunction];
630 						} else {
631 							return [rio.Model.filterJson(inJson), Prototype.emptyFunction];
632 						}
633 					},
634 					
635 					_filterAndProcessJson: function(inJson) {
636 						var results = model._filterAndProcessJsonWhileAccumulatingCollectionEntities(inJson);
637 						results[1]();
638 						return results[0];
639 					},
640 
641 					_collectionEntityFromJson: function(json, parameters, url) {
642 						// In case multiple identical queries were made simultaneously
643 						if (this._collectionEntities[url + "#" + Object.toJSON(parameters.ajaxParameters())]) {
644 							return this._collectionEntities[url + "#" + Object.toJSON(parameters.ajaxParameters())];
645 						}
646 
647 						var results = json.map(function(result) {
648 							// This may cause bugs.  It should map the results of _filterAndProcessJsonWhileAccumulatingCollectionEntities
649 							// and execute them after them map to prevent reification collisions
650 							var modelJson = model._filterAndProcessJson(result);
651 							var fromCache = this.getFromCache(model.id(modelJson.id));
652 							if (fromCache) {
653 								return fromCache;
654 							} else {
655 								var lazyNew = function() {
656 									var fromCache = this.getFromCache(model.id(modelJson.id));
657 									if (fromCache) { return fromCache; }
658 									return new model(modelJson);
659 								}.bind(this);
660 								lazyNew.__lazyNew = true;
661 								return lazyNew;
662 							}
663 						}.bind(this));
664 						
665 						var collectionEntity = rio.CollectionEntity.create({
666 							model: model,
667 							values: results,
668 							condition: parameters.conditionFunction()
669 						});
670 						collectionEntity.prepare = function() {
671 							collectionEntity.prepare = Prototype.emptyFunction;
672 							// prevent the initialization from double adding the entities
673 							var oldAdd = this.add;
674 							try {
675 								var i;
676 								this.add = Prototype.emptyFunction;
677 								var timesToLoop = this.length;
678 								var toRemove = [];
679 								for (i=0; i<timesToLoop; i++) {
680 									if (this[i].__lazyNew) {
681 										var modelInstance = this[i]();
682 										if (!this.include(modelInstance)) {
683 											this[i] = modelInstance;
684 										} else {
685 											toRemove.push(i);
686 										}
687 									}
688 								}
689 								for (i=toRemove.length; i--;) {
690 									this.splice(toRemove[i], 1);
691 								}
692 							} finally {
693 								this.add = oldAdd;
694 							}
695 						};
696 						(function() {
697 							// Ultimately we shouldn't need to do this
698 							// This is required if when parsing eager loaded json collections
699 							// contain individual entities that need to be added to other collection entities
700 							// and the entity representing the eager collection is never loaded, hence prepared.
701 							collectionEntity.prepare();
702 						}.defer());
703 						this.putCollectionEntity(url + "#" + Object.toJSON(parameters.ajaxParameters()), collectionEntity);
704 
705 						return collectionEntity;
706 					},
707 					
708 					_clientOnlyAttrs: [],
709 					
710 					clientOnlyAttr: function(attrName) {
711 						this._clientOnlyAttrs.push(attrName);
712 					},
713 					
714 					_identityCache: {},
715 					
716 					getFromCache: function(id) {
717 						if (id == undefined) { return; }
718 						return this._identityCache[id.cacheKey ? id.cacheKey() : id] || this._identityCache[model.id(id) && model.id(id).cacheKey()];
719 					},
720 					
721 					putInCache: function(id, value) {
722 						var cacheKey = id.cacheKey ? id.cacheKey() : id;
723 						if (this._identityCache[cacheKey] == value) {
724 							return; 
725 						}
726 						this._identityCache[cacheKey] = value;
727 						this.addToCollectionEntities(value);
728 					},
729 					
730 					removeFromCache: function(id) {
731 						var cacheKey = id.cacheKey ? id.cacheKey() : id;
732 						delete this._identityCache[cacheKey];
733 					},
734 					
735 					_collectionEntities: {},
736 					putCollectionEntity: function(key, value) {
737 						this._collectionEntities[key] = value;
738 					},
739 					
740 					addToCollectionEntities: function(value) {
741 						var ces = this._collectionEntities;
742 						for (var key in ces) {
743 							ces[key].add(value);
744 						}
745 					},
746 					
747 					updateInCollectionEntites: function(value) {
748 						var ces = this._collectionEntities;
749 						for (var key in ces) {
750 							ces[key].update(value);
751 						}
752 					},
753 					
754 					removeFromCollectionEntities: function(value) {
755 						Object.values(this._collectionEntities).each(function(collectionEntity) {
756 							collectionEntity.remove(value);
757 						});
758 					}
759 
760 				});
761 			},
762 			
763 			hasChannel: function() {
764 				return false;
765 			},
766 
767 			channel: function() {
768 				model.addMethods({
769 					broadcast: function() {
770 						var args = $A(arguments);
771 						var methodName = args.shift();
772 						var body = {
773 							id: this.getId().toString(),
774 							method: methodName,
775 							args: args
776 						};
777 
778 						new Ajax.Request("/push/broadcast", {
779 							asynchronous: true,
780 							method: "get",
781 							evalJSON: false,
782 							evalJS: false,
783 							parameters: {
784 								channel: this.channelName(),
785 								message: Object.toJSON(body)
786 							}
787 						});
788 					},
789 					
790 					channelName: function() {
791 						return model + "." + this.getId();
792 					}
793 				});
794 				
795 				Object.extend(model, {
796 					hasChannel: function() { return true; },
797 					
798 					receiveBroadcast: function(options) {
799 						var instance = model.getFromCache(model.id(options.id));
800 						if (instance) {
801 							instance[options.method].apply(instance, options.args);
802 						}
803 					}
804 				});
805 			}
806 		});
807 		
808 		if (args.length > 0 && args.last() != undefined && !args.last().ATTR) {
809 			var initializers = args.last();
810 			if (Object.isString(initializers.resource)) {
811 				model.resource(initializers.resource);
812 			}
813 			if (initializers.channel) {
814 				model.channel();
815 			}
816 			
817 			if (initializers.hasMany) {
818 				initializers.hasMany.each(function(h) {
819 					model.hasMany(h);
820 				});
821 			}
822 
823 			if (initializers.belongsTo) {
824 				initializers.belongsTo.each(function(h) {
825 					model.belongsTo(h);
826 				});
827 			}
828 			
829 			(initializers.clientOnlyAttrs || []).each(function(clientOnlyAttr) {
830 				model.clientOnlyAttr(clientOnlyAttr);
831 			});
832 			
833 			if (initializers.undoEnabled) {
834 				model.undoEnabled = true;
835 			}
836 			
837 			rio.Model.extend(model, initializers.methods || {});
838 		}
839 		
840 		return model;
841 	},
842 
843 	_findAllRequests: [],
844 	afterActiveQueries: function(fcn) {
845 		var count = 0;
846 		this._findAllRequests.each(function(far) {
847 			if (far._complete) { return; }
848 			count++;
849 			far.options.onComplete = (far.options.onComplete || Prototype.emptyFunction).wrap(function(proceed) {
850 				var ret = proceed(arguments);
851 				count--;
852 				if (count == 0) { fcn(); }
853 				return ret;
854 			});
855 		});
856 		if (count == 0) { fcn(); }
857 	},
858 	
859 	filterJson: function(json) {
860 		if (json.attributes) { return json.attributes; }
861 		return (rio.environment.includeRootInJson) ? Object.values(json)[0] : json;
862 	},
863 	
864 	remoteCreate: function(options) {
865 		if (options.transactionKey == rio.environment.transactionKey) { return; }
866 		try {
867 			var results = rio.Model.doRemoteCreate(options);
868 			if (results.undoFunction) {
869 				rio.Undo.registerUndo(results.undoFunction);
870 			}
871 		} catch(e) { 
872 			rio.error(e, "Remote create error!");
873 		}
874 	},
875 	
876 	remoteUpdate: function(options) {
877 		if (options.transactionKey == rio.environment.transactionKey) { return; }
878 		try {
879 			var results = rio.Model.doRemoteUpdate(options);
880 			if (results.undoFunction) {
881 				rio.Undo.registerUndo(results.undoFunction);
882 			}
883 		} catch(e) { 
884 			rio.error(e, "Remote update error!");
885 		}
886 	},
887 
888 	remoteDestroy: function(options) {
889 		if (options.transactionKey == rio.environment.transactionKey) { return; }
890 		try {
891 			var results = rio.Model.doRemoteDestroy(options);
892 			if (results.undoFunction) {
893 				rio.Undo.registerUndo(results.undoFunction);
894 			}
895 		} catch(e) { 
896 			rio.error(e, "Remote destroy error!");
897 		}
898 	},
899 
900 	doRemoteCreate: function(options) {
901 		var model = rio.models[options.name];
902 		var fromCache = model.getFromCache(model.id(options.id));
903 		if (!fromCache) {
904 			fromCache = new model(model._filterAndProcessJson(options.json));
905 		} else {
906 			return {};
907 		}
908 
909 		return {
910 			undoFunction: model.undoEnabled ? function() {
911 				model.getFromCache(fromCache.getId()).destroy();
912 			} : null
913 		};
914 	},
915 	
916 	doRemoteUpdate: function(options) {
917 		var model = rio.models[options.name];
918 		var instance = model.getFromCache(model.id(options.id));
919 		if (!instance) {
920 			return {};
921 		}
922 
923 		var json = model._filterAndProcessJson(options.json);
924 	
925 		var results = {};
926 	
927 		if (model.undoEnabled) {
928 			var oldState = instance.attributeState();
929 			var processUndo = function() {
930 				model.getFromCache(instance.getId()).updateAttributes(oldState);
931 			};
932 			results.undoFunction = processUndo;
933 		}
934 
935 		var attributes = Object.keys(json).without("id").inject({}, function(acc, k) {
936 			acc[k.camelize()] = json[k];
937 			return acc;
938 		});
939 		instance.updateAttributes(attributes, { skipSave: true });
940 		rio.models[options.name].updateInCollectionEntites(instance);
941 
942 		instance._lastSavedState = instance.attributeState();
943 		
944 		return results;
945 	},
946 	
947 	doRemoteDestroy: function(options) {
948 		var model = rio.models[options.name];
949 		var fromCache = model.getFromCache(model.id(options.id));
950 		var results = {};
951 
952 		if (fromCache) {
953 			if (model.undoEnabled) {
954 				var oldState = fromCache.attributeState();
955 				var processUndo = function() {
956 					var instance = new model(Object.extend({id: fromCache.getId()}, oldState));
957 					instance._lastSavedState = undefined;
958 					instance.save();
959 				};
960 				results.undoFunction = processUndo;
961 			}
962 			
963 			fromCache.removeFromCaches();
964 			
965 			fromCache.fire("destroy");
966 		}
967 		
968 		return results;
969 	},
970 	
971 	remoteTransaction: function(transactionData) {
972 		if (transactionData.transactionKey == rio.environment.transactionKey) { return; }
973 		try {
974 			// TODO: Two potential bugs here
975 			//
976 			// 1) Need to execute this in a rio.Attr.transaction
977 			// 2) Need to get back the collection entity process functions from _filterAndProcessJsonWhileAccumulatingCollectionEntities
978 			//    and run them at the end.  Otherwise we might have reification collisions.
979 			var resultSets = transactionData.transaction.map(function(t) {
980 				switch(t.action) {
981 				case "create":
982 					return rio.Model.doRemoteCreate(t);
983 				case "update":
984 					return rio.Model.doRemoteUpdate(t);
985 				case "destroy":
986 					return rio.Model.doRemoteDestroy(t);
987 				}
988 			});
989 			
990 			var undos = resultSets.map(function(r) { return r.undoFunction; }).compact();
991 			
992 			if (!undos.empty()) {
993 				rio.Undo.registerUndo(function() {
994 					undos.each(function(undo) {
995 						undo();
996 					});
997 				});
998 			}
999 		} catch(e) { 
1000 			rio.error(e, "Remote transaction error!");
1001 		}
1002 	},
1003 	
1004 	extend: function(model, extension) {
1005 		
1006 		extension.__initialize = extension.initialize || Prototype.emptyFunction;
1007 		extension.initialize = function(options) {
1008 			this.__model = model;
1009 
1010 			if (options.id == undefined && model.url) {
1011 				options.id = model.id();
1012 				this._id = options.id;
1013 			}
1014 			
1015 			if (options.id && options.id.constructor != rio.Id && model.id) {
1016 				options.id = model.id(options.id, true);
1017 				this.setId(options.id);
1018 			}
1019 
1020 			(this.__initialize.bind(this))(options);
1021 			
1022 			// Add the hasManyAssocitation#create methods
1023 			Object.keys(model._hasManyAssociations).each(function(hasManyName) {
1024 				var options = model._hasManyAssociations[hasManyName];
1025 				this[hasManyName].create = function(createOptions) {
1026 					// In the case that the entity is still new, we should preload the association
1027 					// to make sure no entries are missed
1028 					// TODO: before we can wrap in isNew, we need to make the spec fixture proxy include created entities
1029 					// if (this.isNew()) {
1030 						this[("get-" + hasManyName).camelize()]();
1031 					// }
1032 					var parameters = Object.clone(options.parameters);
1033 					parameters[options.foreignKey] = this.getId();
1034 					return rio.models[options.className].create(Object.extend(createOptions || {}, parameters));
1035 				}.bind(this);
1036 			}.bind(this));
1037 			
1038 			if (model.hasChannel()) {
1039 				if (rio.push) {
1040 					var addChannel = function() {
1041 						rio.push.addChannel(this.channelName());
1042 					}.bind(this);
1043 					if (options.id && options.id.temporary()) {
1044 						options.id.doAfterReification(addChannel);
1045 					} else {
1046 						addChannel();
1047 					}
1048 				} else {
1049 					rio.warn("Attempted to add a channel without an available push server");
1050 				}
1051 			}
1052 
1053 			options = options || {};
1054 			if (options.id && model.url) {
1055 				model.putInCache(options.id, this);
1056 			}
1057 			if (options.id && options.id.temporary && !options.id.temporary()) {
1058 				this._lastSavedState = this.attributeState();
1059 			}
1060 		};
1061 		
1062 		rio.Attr.extend(model, extension);
1063 	},
1064 	
1065 	toString: function() {
1066 		return "Model";
1067 	}
1068 };