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() { 
405 								// if (!entity.__destroying) {
406 									if (onFailure) {
407 										onFailure();
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 						});
466 						if (!asynchronous) {
467 							return entity;
468 						}
469 					},
470 
471 					findAll: function(options) {
472 						options = options || {};
473 						var asynchronous = options.asynchronous == undefined || options.asynchronous;
474 						
475 						if (options.onSuccess == undefined) {
476 							asynchronous = false;
477 							options.onSuccess = Prototype.emptyFunction;
478 						}
479 						 
480 						var idField = Object.keys(options.parameters).detect(function(parameter) {
481 							var val = options.parameters[parameter];
482 							return val && val.temporary && val.temporary();
483 						});
484 
485 						var urlToUse = options.url || this.url();
486 						var parameters = new rio.Parameters(options.parameters || {}, options.nonAjaxParameters);
487 						// Assume that there are no entities yet (since a param is unreified)
488 						// New entities will be added to the collectionEntity as they are created or broadcast
489 						// If this poses a problem, we can always schedule a find for after reification and then reconcile the CE's
490 						if (idField) {
491 							var collectionEntity = rio.CollectionEntity.create({
492 								model: model,
493 								values: [],
494 								condition: parameters.conditionFunction()
495 							});
496 							this.putCollectionEntity(urlToUse + "#" + Object.toJSON(parameters.ajaxParameters()), collectionEntity);
497 							
498 							Object.values(model._identityCache).each(function(entity) {
499 								collectionEntity.add(entity);
500 							});
501 							
502 							options.onSuccess(collectionEntity);
503 							return collectionEntity;
504 						}
505 						
506 						
507 						if (this._collectionEntities[urlToUse + "#" + Object.toJSON(parameters.ajaxParameters())]) {
508 							var found = this._collectionEntities[urlToUse + "#" + Object.toJSON(parameters.ajaxParameters())];
509 							found.prepare();
510 							options.onSuccess(found);
511 							if (!asynchronous) {
512 								return found;
513 							} else {
514 								return;
515 							}
516 						}
517 
518 						var results;
519 						rio.Model._findAllRequests.push(
520 							new Ajax.Request(urlToUse, { 
521 								asynchronous: asynchronous,
522 								method: 'get',
523 								evalJSON: true,
524 								parameters: {conditions: Object.toJSON(parameters.ajaxParameters())},
525 								onSuccess: function(response) {
526 									results = this._collectionEntityFromJson(response.responseJSON, parameters, urlToUse);
527 									results.prepare();
528 									options.onSuccess(results);
529 								}.bind(this)
530 							})
531 						);
532 						if (!asynchronous) {
533 							return results;
534 						}
535 					},
536 					
537 					_hasManyAssociations: {},
538 					hasMany: function(hasManyName) {
539 						var options = {};
540 						if (!Object.isString(hasManyName)) {
541 							options = hasManyName[1] || {};
542 							hasManyName = hasManyName[0];
543 						}
544 						options.className = hasManyName.singularize().classize();
545 						options.foreignKey = this.NAME.toLowerCase() + "Id";
546 						options.parameters = options.parameters || {};
547 
548 						this._hasManyAssociations[hasManyName] = options;
549 						
550 						this.attrAccessor(hasManyName);
551 						this.clientOnlyAttr(hasManyName);
552 						
553 						var getName = ("get-" + hasManyName).camelize();
554 
555 						this.prototype[getName] = this.prototype[getName].wrap(function(proceed) {
556 							if (this["_" + hasManyName] == undefined) {
557 								var parameters = Object.clone(options.parameters);
558 								parameters[options.foreignKey] = this.getId();
559 								rio.models[options.className].findAll({
560 									asynchronous: false,
561 									parameters: parameters,
562 									onSuccess: function(entities) {
563 										this["_" + hasManyName] = entities;
564 									}.bind(this)
565 								});
566 							}
567 							return proceed.apply(this, $A(arguments).slice(1));
568 						});
569 					},
570 					
571 					belongsTo: function(args) {
572 						var options = {};
573 						var associationName;
574 						if (!Object.isString(args)) {
575 							associationName = args[0];
576 							options = args[1] || {};
577 						} else {
578 							associationName = args;
579 						}
580 						this.attrAccessor(associationName);
581 						this.clientOnlyAttr(associationName);
582 						
583 						var className = options.className || associationName.classize();
584 						var foreignKey = options.foreignKey || associationName + "Id";
585 						var getName = ("get-" + associationName).camelize();
586 						
587 						this.prototype[getName] = this.prototype[getName].wrap(function(proceed) {
588 							if (this["_" + associationName] == undefined) {
589 								var setAssociation = function() {
590 									var associationId = this["_" + foreignKey];
591 									var foundValue = rio.models[className].find(associationId, {
592 										asynchronous: false
593 									});
594 									this[("set-" + associationName).camelize()](foundValue);
595 								}.bind(this);
596 								setAssociation();
597 								this[foreignKey].bind(setAssociation, true);
598 							}
599 							return proceed.apply(this, $A(arguments).slice(1));
600 						});
601 					},
602 					
603 					_parametersFromJsonParameters: function(params) {
604 						return new rio.Parameters(
605 							Object.keys(params).inject({}, function(acc, p) {
606 								acc[p.camelize()] = params[p];
607 								return acc;
608 							})
609 						);
610 					},
611 					
612 					_processIncludedCollectionEntities: function(json) {
613 						for (var i=json.length; i--;) {
614 							var include = json[i];
615 							params = this._parametersFromJsonParameters(include.parameters);
616 							var url = rio.models[include.className].url();
617 							if (this._collectionEntities[url + "#" + Object.toJSON(params.ajaxParameters())] == undefined) {
618 								rio.models[include.className]._collectionEntityFromJson(include.json, params, url);
619 							}
620 						}
621 					},
622 					
623 					_filterAndProcessJsonWhileAccumulatingCollectionEntities: function(inJson) {
624 						if (inJson._set) {
625 							var ceFunction = function() {
626 								model._processIncludedCollectionEntities(inJson._set.include);
627 							};
628 							return [rio.Model.filterJson(inJson._set.self), ceFunction];
629 						} else {
630 							return [rio.Model.filterJson(inJson), Prototype.emptyFunction];
631 						}
632 					},
633 					
634 					_filterAndProcessJson: function(inJson) {
635 						var results = model._filterAndProcessJsonWhileAccumulatingCollectionEntities(inJson);
636 						results[1]();
637 						return results[0];
638 					},
639 
640 					_collectionEntityFromJson: function(json, parameters, url) {
641 						// In case multiple identical queries were made simultaneously
642 						if (this._collectionEntities[url + "#" + Object.toJSON(parameters.ajaxParameters())]) {
643 							return this._collectionEntities[url + "#" + Object.toJSON(parameters.ajaxParameters())];
644 						}
645 
646 						var results = json.map(function(result) {
647 							// This may cause bugs.  It should map the results of _filterAndProcessJsonWhileAccumulatingCollectionEntities
648 							// and execute them after them map to prevent reification collisions
649 							var modelJson = model._filterAndProcessJson(result);
650 							var fromCache = this.getFromCache(model.id(modelJson.id));
651 							if (fromCache) {
652 								return fromCache;
653 							} else {
654 								var lazyNew = function() {
655 									var fromCache = this.getFromCache(model.id(modelJson.id));
656 									if (fromCache) { return fromCache; }
657 									return new model(modelJson);
658 								}.bind(this);
659 								lazyNew.__lazyNew = true;
660 								return lazyNew;
661 							}
662 						}.bind(this));
663 						
664 						var collectionEntity = rio.CollectionEntity.create({
665 							model: model,
666 							values: results,
667 							condition: parameters.conditionFunction()
668 						});
669 						collectionEntity.prepare = function() {
670 							collectionEntity.prepare = Prototype.emptyFunction;
671 							// prevent the initialization from double adding the entities
672 							var oldAdd = this.add;
673 							try {
674 								var i;
675 								this.add = Prototype.emptyFunction;
676 								var timesToLoop = this.length;
677 								var toRemove = [];
678 								for (i=0; i<timesToLoop; i++) {
679 									if (this[i].__lazyNew) {
680 										var modelInstance = this[i]();
681 										if (!this.include(modelInstance)) {
682 											this[i] = modelInstance;
683 										} else {
684 											toRemove.push(i);
685 										}
686 									}
687 								}
688 								for (i=toRemove.length; i--;) {
689 									this.splice(toRemove[i], 1);
690 								}
691 							} finally {
692 								this.add = oldAdd;
693 							}
694 						};
695 						(function() {
696 							// Ultimately we shouldn't need to do this
697 							// This is required if when parsing eager loaded json collections
698 							// contain individual entities that need to be added to other collection entities
699 							// and the entity representing the eager collection is never loaded, hence prepared.
700 							collectionEntity.prepare();
701 						}.defer());
702 						this.putCollectionEntity(url + "#" + Object.toJSON(parameters.ajaxParameters()), collectionEntity);
703 
704 						return collectionEntity;
705 					},
706 					
707 					_clientOnlyAttrs: [],
708 					
709 					clientOnlyAttr: function(attrName) {
710 						this._clientOnlyAttrs.push(attrName);
711 					},
712 					
713 					_identityCache: {},
714 					
715 					getFromCache: function(id) {
716 						if (id == undefined) { return; }
717 						return this._identityCache[id.cacheKey ? id.cacheKey() : id] || this._identityCache[model.id(id) && model.id(id).cacheKey()];
718 					},
719 					
720 					putInCache: function(id, value) {
721 						var cacheKey = id.cacheKey ? id.cacheKey() : id;
722 						if (this._identityCache[cacheKey] == value) {
723 							return; 
724 						}
725 						this._identityCache[cacheKey] = value;
726 						this.addToCollectionEntities(value);
727 					},
728 					
729 					removeFromCache: function(id) {
730 						var cacheKey = id.cacheKey ? id.cacheKey() : id;
731 						delete this._identityCache[cacheKey];
732 					},
733 					
734 					_collectionEntities: {},
735 					putCollectionEntity: function(key, value) {
736 						this._collectionEntities[key] = value;
737 					},
738 					
739 					addToCollectionEntities: function(value) {
740 						var ces = this._collectionEntities;
741 						for (var key in ces) {
742 							ces[key].add(value);
743 						}
744 					},
745 					
746 					updateInCollectionEntites: function(value) {
747 						var ces = this._collectionEntities;
748 						for (var key in ces) {
749 							ces[key].update(value);
750 						}
751 					},
752 					
753 					removeFromCollectionEntities: function(value) {
754 						Object.values(this._collectionEntities).each(function(collectionEntity) {
755 							collectionEntity.remove(value);
756 						});
757 					}
758 
759 				});
760 			},
761 			
762 			hasChannel: function() {
763 				return false;
764 			},
765 
766 			channel: function() {
767 				model.addMethods({
768 					broadcast: function() {
769 						var args = $A(arguments);
770 						var methodName = args.shift();
771 						var body = {
772 							id: this.getId().toString(),
773 							method: methodName,
774 							args: args
775 						};
776 
777 						new Ajax.Request("/push/broadcast", {
778 							asynchronous: true,
779 							method: "get",
780 							evalJSON: false,
781 							evalJS: false,
782 							parameters: {
783 								channel: this.channelName(),
784 								message: Object.toJSON(body)
785 							}
786 						});
787 					},
788 					
789 					channelName: function() {
790 						return model + "." + this.getId();
791 					}
792 				});
793 				
794 				Object.extend(model, {
795 					hasChannel: function() { return true; },
796 					
797 					receiveBroadcast: function(options) {
798 						var instance = model.getFromCache(model.id(options.id));
799 						if (instance) {
800 							instance[options.method].apply(instance, options.args);
801 						}
802 					}
803 				});
804 			}
805 		});
806 		
807 		if (args.length > 0 && args.last() != undefined && !args.last().ATTR) {
808 			var initializers = args.last();
809 			if (Object.isString(initializers.resource)) {
810 				model.resource(initializers.resource);
811 			}
812 			if (initializers.channel) {
813 				model.channel();
814 			}
815 			
816 			if (initializers.hasMany) {
817 				initializers.hasMany.each(function(h) {
818 					model.hasMany(h);
819 				});
820 			}
821 
822 			if (initializers.belongsTo) {
823 				initializers.belongsTo.each(function(h) {
824 					model.belongsTo(h);
825 				});
826 			}
827 			
828 			(initializers.clientOnlyAttrs || []).each(function(clientOnlyAttr) {
829 				model.clientOnlyAttr(clientOnlyAttr);
830 			});
831 			
832 			if (initializers.undoEnabled) {
833 				model.undoEnabled = true;
834 			}
835 			
836 			rio.Model.extend(model, initializers.methods || {});
837 		}
838 		
839 		return model;
840 	},
841 
842 	_findAllRequests: [],
843 	afterActiveQueries: function(fcn) {
844 		var count = 0;
845 		this._findAllRequests.each(function(far) {
846 			if (far._complete) { return; }
847 			count++;
848 			far.options.onComplete = (far.options.onComplete || Prototype.emptyFunction).wrap(function(proceed) {
849 				var ret = proceed(arguments);
850 				count--;
851 				if (count == 0) { fcn(); }
852 				return ret;
853 			});
854 		});
855 		if (count == 0) { fcn(); }
856 	},
857 	
858 	filterJson: function(json) {
859 		if (json.attributes) { return json.attributes; }
860 		return (rio.environment.includeRootInJson) ? Object.values(json)[0] : json;
861 	},
862 	
863 	remoteCreate: function(options) {
864 		if (options.transactionKey == rio.environment.transactionKey) { return; }
865 		try {
866 			var results = rio.Model.doRemoteCreate(options);
867 			if (results.undoFunction) {
868 				rio.Undo.registerUndo(results.undoFunction);
869 			}
870 		} catch(e) { 
871 			rio.error(e, "Remote create error!");
872 		}
873 	},
874 	
875 	remoteUpdate: function(options) {
876 		if (options.transactionKey == rio.environment.transactionKey) { return; }
877 		try {
878 			var results = rio.Model.doRemoteUpdate(options);
879 			if (results.undoFunction) {
880 				rio.Undo.registerUndo(results.undoFunction);
881 			}
882 		} catch(e) { 
883 			rio.error(e, "Remote update error!");
884 		}
885 	},
886 
887 	remoteDestroy: function(options) {
888 		if (options.transactionKey == rio.environment.transactionKey) { return; }
889 		try {
890 			var results = rio.Model.doRemoteDestroy(options);
891 			if (results.undoFunction) {
892 				rio.Undo.registerUndo(results.undoFunction);
893 			}
894 		} catch(e) { 
895 			rio.error(e, "Remote destroy error!");
896 		}
897 	},
898 
899 	doRemoteCreate: function(options) {
900 		var model = rio.models[options.name];
901 		var fromCache = model.getFromCache(model.id(options.id));
902 		if (!fromCache) {
903 			fromCache = new model(model._filterAndProcessJson(options.json));
904 		} else {
905 			return {};
906 		}
907 
908 		return {
909 			undoFunction: model.undoEnabled ? function() {
910 				model.getFromCache(fromCache.getId()).destroy();
911 			} : null
912 		};
913 	},
914 	
915 	doRemoteUpdate: function(options) {
916 		var model = rio.models[options.name];
917 		var instance = model.getFromCache(model.id(options.id));
918 		if (!instance) {
919 			return {};
920 		}
921 
922 		var json = model._filterAndProcessJson(options.json);
923 	
924 		var results = {};
925 	
926 		if (model.undoEnabled) {
927 			var oldState = instance.attributeState();
928 			var processUndo = function() {
929 				model.getFromCache(instance.getId()).updateAttributes(oldState);
930 			};
931 			results.undoFunction = processUndo;
932 		}
933 
934 		var attributes = Object.keys(json).without("id").inject({}, function(acc, k) {
935 			acc[k.camelize()] = json[k];
936 			return acc;
937 		});
938 		instance.updateAttributes(attributes, { skipSave: true });
939 		rio.models[options.name].updateInCollectionEntites(instance);
940 
941 		instance._lastSavedState = instance.attributeState();
942 		
943 		return results;
944 	},
945 	
946 	doRemoteDestroy: function(options) {
947 		var model = rio.models[options.name];
948 		var fromCache = model.getFromCache(model.id(options.id));
949 		var results = {};
950 
951 		if (fromCache) {
952 			if (model.undoEnabled) {
953 				var oldState = fromCache.attributeState();
954 				var processUndo = function() {
955 					var instance = new model(Object.extend({id: fromCache.getId()}, oldState));
956 					instance._lastSavedState = undefined;
957 					instance.save();
958 				};
959 				results.undoFunction = processUndo;
960 			}
961 			
962 			fromCache.removeFromCaches();
963 			
964 			fromCache.fire("destroy");
965 		}
966 		
967 		return results;
968 	},
969 	
970 	remoteTransaction: function(transactionData) {
971 		if (transactionData.transactionKey == rio.environment.transactionKey) { return; }
972 		try {
973 			// TODO: Two potential bugs here
974 			//
975 			// 1) Need to execute this in a rio.Attr.transaction
976 			// 2) Need to get back the collection entity process functions from _filterAndProcessJsonWhileAccumulatingCollectionEntities
977 			//    and run them at the end.  Otherwise we might have reification collisions.
978 			var resultSets = transactionData.transaction.map(function(t) {
979 				switch(t.action) {
980 				case "create":
981 					return rio.Model.doRemoteCreate(t);
982 				case "update":
983 					return rio.Model.doRemoteUpdate(t);
984 				case "destroy":
985 					return rio.Model.doRemoteDestroy(t);
986 				}
987 			});
988 			
989 			var undos = resultSets.map(function(r) { return r.undoFunction; }).compact();
990 			
991 			if (!undos.empty()) {
992 				rio.Undo.registerUndo(function() {
993 					undos.each(function(undo) {
994 						undo();
995 					});
996 				});
997 			}
998 		} catch(e) { 
999 			rio.error(e, "Remote transaction error!");
1000 		}
1001 	},
1002 	
1003 	extend: function(model, extension) {
1004 		
1005 		extension.__initialize = extension.initialize || Prototype.emptyFunction;
1006 		extension.initialize = function(options) {
1007 			this.__model = model;
1008 
1009 			if (options.id == undefined && model.url) {
1010 				options.id = model.id();
1011 				this._id = options.id;
1012 			}
1013 			
1014 			if (options.id && options.id.constructor != rio.Id && model.id) {
1015 				options.id = model.id(options.id, true);
1016 				this.setId(options.id);
1017 			}
1018 
1019 			(this.__initialize.bind(this))(options);
1020 			
1021 			// Add the hasManyAssocitation#create methods
1022 			Object.keys(model._hasManyAssociations).each(function(hasManyName) {
1023 				var options = model._hasManyAssociations[hasManyName];
1024 				this[hasManyName].create = function(createOptions) {
1025 					// In the case that the entity is still new, we should preload the association
1026 					// to make sure no entries are missed
1027 					// TODO: before we can wrap in isNew, we need to make the spec fixture proxy include created entities
1028 					// if (this.isNew()) {
1029 						this[("get-" + hasManyName).camelize()]();
1030 					// }
1031 					var parameters = Object.clone(options.parameters);
1032 					parameters[options.foreignKey] = this.getId();
1033 					return rio.models[options.className].create(Object.extend(createOptions || {}, parameters));
1034 				}.bind(this);
1035 			}.bind(this));
1036 			
1037 			if (model.hasChannel()) {
1038 				if (rio.push) {
1039 					var addChannel = function() {
1040 						rio.push.addChannel(this.channelName());
1041 					}.bind(this);
1042 					if (options.id && options.id.temporary()) {
1043 						options.id.doAfterReification(addChannel);
1044 					} else {
1045 						addChannel();
1046 					}
1047 				} else {
1048 					rio.warn("Attempted to add a channel without an available push server");
1049 				}
1050 			}
1051 
1052 			options = options || {};
1053 			if (options.id && model.url) {
1054 				model.putInCache(options.id, this);
1055 			}
1056 			if (options.id && options.id.temporary && !options.id.temporary()) {
1057 				this._lastSavedState = this.attributeState();
1058 			}
1059 		};
1060 		
1061 		rio.Attr.extend(model, extension);
1062 	},
1063 	
1064 	toString: function() {
1065 		return "Model";
1066 	}
1067 };