Subversion Repositories Applications.papyrus

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
2150 mathias 1
if(!dojo._hasResource["dojox.off.sync"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
2
dojo._hasResource["dojox.off.sync"] = true;
3
dojo.provide("dojox.off.sync");
4
 
5
dojo.require("dojox.storage.GearsStorageProvider");
6
dojo.require("dojox.off._common");
7
dojo.require("dojox.off.files");
8
 
9
// Author: Brad Neuberg, bkn3@columbia.edu, http://codinginparadise.org
10
 
11
// summary:
12
//		Exposes syncing functionality to offline applications
13
dojo.mixin(dojox.off.sync, {
14
	// isSyncing: boolean
15
	//		Whether we are in the middle of a syncing session.
16
	isSyncing: false,
17
 
18
	// cancelled: boolean
19
	//		Whether we were cancelled during our last sync request or not. If
20
	//		we are cancelled, then successful will be false.
21
	cancelled: false,
22
 
23
	// successful: boolean
24
	//		Whether the last sync was successful or not.  If false, an error
25
	//		occurred.
26
	successful: true,
27
 
28
	// details: String[]
29
	//		Details on the sync. If the sync was successful, this will carry
30
	//		any conflict or merging messages that might be available; if the
31
	//		sync was unsuccessful, this will have an error message.  For both
32
	//		of these, this should be an array of Strings, where each string
33
	//		carries details on the sync.
34
	//	Example:
35
	//		dojox.off.sync.details = ["The document 'foobar' had conflicts - yours one",
36
	//						"The document 'hello world' was automatically merged"];
37
	details: [],
38
 
39
	// error: boolean
40
	//		Whether an error occurred during the syncing process.
41
	error: false,
42
 
43
	// actions: dojox.off.sync.ActionLog
44
	//		Our ActionLog that we store offline actions into for later
45
	//		replaying when we go online
46
	actions: null,
47
 
48
	// autoSync: boolean
49
	//		For advanced usage; most developers can ignore this.
50
	//		Whether we do automatically sync on page load or when we go online.
51
	//		If true we do, if false syncing must be manually initiated.
52
	//		Defaults to true.
53
	autoSync: true,
54
 
55
	// summary:
56
	//	An event handler that is called during the syncing process with
57
	//	the state of syncing. It is important that you connect to this
58
	//	method and respond to certain sync events, especially the
59
	//	"download" event.
60
	// description:
61
	//	This event handler is called during the syncing process. You can
62
	//	do a dojo.connect to receive sync feedback:
63
	//
64
	//		dojo.connect(dojox.off.sync, "onSync", someFunc);
65
	//
66
	//	You will receive one argument, which is the type of the event
67
	//	and which can have the following values.
68
	//
69
	//	The most common two types that you need to care about are "download"
70
	//	and "finished", especially if you are using the default
71
	//	Dojo Offline UI widget that does the hard work of informing
72
	//	the user through the UI about what is occuring during syncing.
73
	//
74
	//	If you receive the "download" event, you should make a network call
75
	//	to retrieve and store your data somehow for offline access. The
76
	//	"finished" event indicates that syncing is done. An example:
77
	//
78
	//		dojo.connect(dojox.off.sync, "onSync", function(type){
79
	//			if(type == "download"){
80
	//				// make a network call to download some data
81
	//				// for use offline
82
	//				dojo.xhrGet({
83
	//					url: 		"downloadData.php",
84
	//					handleAs:	"javascript",
85
	//					error:		function(err){
86
	//						dojox.off.sync.finishedDownloading(false, "Can't download data");
87
	//					},
88
	//					load:		function(data){
89
	//						// store our data
90
	//						dojox.storage.put("myData", data);
91
	//
92
	//						// indicate we are finished downloading
93
	//						dojox.off.sync.finishedDownloading(true);
94
	//					}
95
	//				});
96
	//			}else if(type == "finished"){
97
	//				// update UI somehow to indicate we are finished,
98
	//				// such as using the download data to change the
99
	//				// available data
100
	//			}
101
	//		})
102
	//
103
	//	Here is the full list of event types if you want to do deep
104
	//	customization, such as updating your UI to display the progress
105
	//	of syncing (note that the default Dojo Offline UI widget does
106
	//	this for you if you choose to pull that in). Most of these
107
	//	are only appropriate for advanced usage and can be safely
108
	//	ignored:
109
	//
110
	//		* "start"
111
	//				syncing has started
112
	//		* "refreshFiles"
113
	//				syncing will begin refreshing
114
	//				our offline file cache
115
	//		* "upload"
116
	//				syncing will begin uploading
117
	//				any local data changes we have on the client.
118
	//				This event is fired before we fire
119
	//				the dojox.off.sync.actions.onReplay event for
120
	//				each action to replay; use it to completely
121
	//				over-ride the replaying behavior and prevent
122
	//				it entirely, perhaps rolling your own sync
123
	//				protocol if needed.
124
	//		* "download"
125
	//				syncing will begin downloading any new data that is
126
	//				needed into persistent storage. Applications are required to
127
	//				implement this themselves, storing the required data into
128
	//				persistent local storage using Dojo Storage.
129
	//		* "finished"
130
	//				syncing is finished; this
131
	//				will be called whether an error ocurred or not; check
132
	//				dojox.off.sync.successful and dojox.off.sync.error for sync details
133
	//		* "cancel"
134
	//				Fired when canceling has been initiated; canceling will be
135
	//				attempted, followed by the sync event "finished".
136
	onSync: function(/* String */ type){},
137
 
138
	synchronize: function(){ /* void */
139
		// summary: Starts synchronizing
140
 
141
		//dojo.debug("synchronize");
142
		if(this.isSyncing || dojox.off.goingOnline || (!dojox.off.isOnline)){
143
			return;
144
		}
145
 
146
		this.isSyncing = true;
147
		this.successful = false;
148
		this.details = [];
149
		this.cancelled = false;
150
 
151
		this.start();
152
	},
153
 
154
	cancel: function(){ /* void */
155
		// summary:
156
		//	Attempts to cancel this sync session
157
 
158
		if(!this.isSyncing){ return; }
159
 
160
		this.cancelled = true;
161
		if(dojox.off.files.refreshing){
162
			dojox.off.files.abortRefresh();
163
		}
164
 
165
		this.onSync("cancel");
166
	},
167
 
168
	finishedDownloading: function(successful /* boolean? */,
169
									errorMessage /* String? */){
170
		// summary:
171
		//		Applications call this method from their
172
		//		after getting a "download" event in
173
		//		dojox.off.sync.onSync to signal that
174
		//		they are finished downloading any data
175
		//		that should be available offline
176
		// successful: boolean?
177
		//		Whether our downloading was successful or not.
178
		//		If not present, defaults to true.
179
		// errorMessage: String?
180
		//		If unsuccessful, a message explaining why
181
		if(typeof successful == "undefined"){
182
			successful = true;
183
		}
184
 
185
		if(!successful){
186
			this.successful = false;
187
			this.details.push(errorMessage);
188
			this.error = true;
189
		}
190
 
191
		this.finished();
192
	},
193
 
194
	start: function(){ /* void */
195
		// summary:
196
		//	For advanced usage; most developers can ignore this.
197
		//	Called at the start of the syncing process. Advanced
198
		//	developers can over-ride this method to use their
199
		//	own sync mechanism to start syncing.
200
 
201
		if(this.cancelled){
202
			this.finished();
203
			return;
204
		}
205
		this.onSync("start");
206
		this.refreshFiles();
207
	},
208
 
209
	refreshFiles: function(){ /* void */
210
		// summary:
211
		//	For advanced usage; most developers can ignore this.
212
		//	Called when we are going to refresh our list
213
		//	of offline files during syncing. Advanced developers
214
		//	can over-ride this method to do some advanced magic related to
215
		//	refreshing files.
216
 
217
		//dojo.debug("refreshFiles");
218
		if(this.cancelled){
219
			this.finished();
220
			return;
221
		}
222
 
223
		this.onSync("refreshFiles");
224
 
225
		dojox.off.files.refresh(dojo.hitch(this, function(error, errorMessages){
226
			if(error){
227
				this.error = true;
228
				this.successful = false;
229
				for(var i = 0; i < errorMessages.length; i++){
230
					this.details.push(errorMessages[i]);
231
				}
232
 
233
				// even if we get an error while syncing files,
234
				// keep syncing so we can upload and download
235
				// data
236
			}
237
 
238
			this.upload();
239
		}));
240
	},
241
 
242
	upload: function(){ /* void */
243
		// summary:
244
		//	For advanced usage; most developers can ignore this.
245
		//	Called when syncing wants to upload data. Advanced
246
		//	developers can over-ride this method to completely
247
		//	throw away the Action Log and replaying system
248
		//	and roll their own advanced sync mechanism if needed.
249
 
250
		if(this.cancelled){
251
			this.finished();
252
			return;
253
		}
254
 
255
		this.onSync("upload");
256
 
257
		// when we are done uploading start downloading
258
		dojo.connect(this.actions, "onReplayFinished", this, this.download);
259
 
260
		// replay the actions log
261
		this.actions.replay();
262
	},
263
 
264
	download: function(){ /* void */
265
		// summary:
266
		//	For advanced usage; most developers can ignore this.
267
		//	Called when syncing wants to download data. Advanced
268
		//	developers can over-ride this method to use their
269
		//	own sync mechanism.
270
 
271
		if(this.cancelled){
272
			this.finished();
273
			return;
274
		}
275
 
276
		// apps should respond to the "download"
277
		// event to download their data; when done
278
		// they must call dojox.off.sync.finishedDownloading()
279
		this.onSync("download");
280
	},
281
 
282
	finished: function(){ /* void */
283
		// summary:
284
		//	For advanced usage; most developers can ignore this.
285
		//	Called when syncing is finished. Advanced
286
		//	developers can over-ride this method to clean
287
		//	up after finishing their own sync
288
		//	mechanism they might have rolled.
289
		this.isSyncing = false;
290
 
291
		this.successful = (!this.cancelled && !this.error);
292
 
293
		this.onSync("finished");
294
	},
295
 
296
	_save: function(callback){
297
		this.actions._save(function(){
298
			callback();
299
		});
300
	},
301
 
302
	_load: function(callback){
303
		this.actions._load(function(){
304
			callback();
305
		});
306
	}
307
});
308
 
309
 
310
// summary:
311
//		A class that records actions taken by a user when they are offline,
312
//		suitable for replaying when the network reappears.
313
// description:
314
//		The basic idea behind this method is to record user actions that would
315
//		normally have to contact a server into an action log when we are
316
//		offline, so that later when we are online we can simply replay this log
317
//		in the order user actions happened so that they can be executed against
318
//		the server, causing synchronization to happen.
319
//
320
//		When we replay, for each of the actions that were added, we call a
321
//		method named onReplay that applications should connect to and
322
//		which will be called over and over for each of our actions --
323
//		applications should take the offline action
324
//		information and use it to talk to a server to have this action
325
//		actually happen online, 'syncing' themselves with the server.
326
//
327
//		For example, if the action was "update" with the item that was updated, we
328
//		might call some RESTian server API that exists for updating an item in
329
//		our application.  The server could either then do sophisticated merging
330
//		and conflict resolution on the server side, for example, allowing you
331
//		to pop up a custom merge UI, or could do automatic merging or nothing
332
//		of the sort. When you are finished with this particular action, your
333
//		application is then required to call continueReplay() on the actionLog object
334
//		passed to onReplay() to continue replaying the action log, or haltReplay()
335
//		with the reason for halting to completely stop the syncing/replaying
336
//		process.
337
//
338
//		For example, imagine that we have a web application that allows us to add
339
//		contacts. If we are offline, and we update a contact, we would add an action;
340
//		imagine that the user has to click an Update button after changing the values
341
//		for a given contact:
342
//
343
//		dojox.off.whenOffline(dojo.byId("updateButton"), "onclick", function(evt){
344
//			// get the updated customer values
345
//			var customer = getCustomerValues();
346
//
347
//			// we are offline -- just record this action
348
//			var action = {name: "update", customer: customer};
349
//			dojox.off.sync.actions.add(action)
350
//
351
//			// persist this customer data into local storage as well
352
//			dojox.storage.put(customer.name, customer);
353
//		})
354
//
355
//		Then, when we go back online, the dojox.off.sync.actions.onReplay event
356
//		will fire over and over, once for each action that was recorded while offline:
357
//
358
//		dojo.connect(dojox.off.sync.actions, "onReplay", function(action, actionLog){
359
//			// called once for each action we added while offline, in the order
360
//			// they were added
361
//			if(action.name == "update"){
362
//				var customer = action.customer;
363
//
364
//				// call some network service to update this customer
365
//				dojo.xhrPost({
366
//					url: "updateCustomer.php",
367
//					content: {customer: dojo.toJson(customer)},
368
//					error: function(err){
369
//						actionLog.haltReplay(err);
370
//					},
371
//					load: function(data){
372
//						actionLog.continueReplay();
373
//					}
374
//				})
375
//			}
376
//		})
377
//
378
//		Note that the actions log is always automatically persisted locally while using it, so
379
//		that if the user closes the browser or it crashes the actions will safely be stored
380
//		for later replaying.
381
dojo.declare("dojox.off.sync.ActionLog", null, {
382
		// entries: Array
383
		//		An array of our action entries, where each one is simply a custom
384
		//		object literal that were passed to add() when this action entry
385
		//		was added.
386
		entries: [],
387
 
388
		// reasonHalted: String
389
		//		If we halted, the reason why
390
		reasonHalted: null,
391
 
392
		// isReplaying: boolean
393
		//		If true, we are in the middle of replaying a command log; if false,
394
		//		then we are not
395
		isReplaying: false,
396
 
397
		// autoSave: boolean
398
		//		Whether we automatically save the action log after each call to
399
		//		add(); defaults to true. For applications that are rapidly adding
400
		//		many action log entries in a short period of time, it can be
401
		//		useful to set this to false and simply call save() yourself when
402
		//		you are ready to persist your command log -- otherwise performance
403
		//		could be slow as the default action is to attempt to persist the
404
		//		actions log constantly with calls to add().
405
		autoSave: true,
406
 
407
		add: function(action /* Object */){ /* void */
408
			// summary:
409
			//	Adds an action to our action log
410
			// description:
411
			//	This method will add an action to our
412
			//	action log, later to be replayed when we
413
			//	go from offline to online. 'action'
414
			//	will be available when this action is
415
			//	replayed and will be passed to onReplay.
416
			//
417
			//	Example usage:
418
			//
419
			//	dojox.off.sync.log.add({actionName: "create", itemType: "document",
420
			//					  {title: "Message", content: "Hello World"}});
421
			//
422
			//	The object literal is simply a custom object appropriate
423
			//	for our application -- it can be anything that preserves the state
424
			//	of a user action that will be executed when we go back online
425
			//	and replay this log. In the above example,
426
			//	"create" is the name of this action; "documents" is the
427
			//	type of item this command is operating on, such as documents, contacts,
428
			//	tasks, etc.; and the final argument is the document that was created.
429
 
430
			if(this.isReplaying){
431
				throw "Programming error: you can not call "
432
						+ "dojox.off.sync.actions.add() while "
433
						+ "we are replaying an action log";
434
			}
435
 
436
			this.entries.push(action);
437
 
438
			// save our updated state into persistent
439
			// storage
440
			if(this.autoSave){
441
				this._save();
442
			}
443
		},
444
 
445
		onReplay: function(action /* Object */,
446
							actionLog /* dojox.off.sync.ActionLog */){ /* void */
447
			// summary:
448
			//	Called when we replay our log, for each of our action
449
			//	entries.
450
			// action: Object
451
			//	A custom object literal representing an action for this
452
			//	application, such as
453
			//	{actionName: "create", item: {title: "message", content: "hello world"}}
454
			// actionLog: dojox.off.sync.ActionLog
455
			//	A reference to the dojox.off.sync.actions log so that developers
456
			//	can easily call actionLog.continueReplay() or actionLog.haltReplay().
457
			// description:
458
			//	This callback should be connected to by applications so that
459
			//	they can sync themselves when we go back online:
460
			//
461
			//		dojo.connect(dojox.off.sync.actions, "onReplay", function(action, actionLog){
462
			//				// do something
463
			//		})
464
			//
465
			//	When we replay our action log, this callback is called for each
466
			//	of our action entries in the order they were added. The
467
			//	'action' entry that was passed to add() for this action will
468
			//	also be passed in to onReplay, so that applications can use this information
469
			//	to do their syncing, such as contacting a server web-service
470
			//	to create a new item, for example.
471
			//
472
			//	Inside the method you connected to onReplay, you should either call
473
			//	actionLog.haltReplay(reason) if an error occurred and you would like to halt
474
			//	action replaying or actionLog.continueReplay() to have the action log
475
			//	continue replaying its log and proceed to the next action;
476
			//	the reason you must call these is the action you execute inside of
477
			//	onAction will probably be asynchronous, since it will be talking on
478
			//	the network, and you should call one of these two methods based on
479
			//	the result of your network call.
480
		},
481
 
482
		length: function(){ /* Number */
483
			// summary:
484
			//	Returns the length of this
485
			//	action log
486
			return this.entries.length;
487
		},
488
 
489
		haltReplay: function(reason /* String */){ /* void */
490
			// summary: Halts replaying this command log.
491
			// reason: String
492
			//		The reason we halted.
493
			// description:
494
			//		This method is called as we are replaying an action log; it
495
			//		can be called from dojox.off.sync.actions.onReplay, for
496
			//		example, for an application to indicate an error occurred
497
			//		while replaying this action, halting further processing of
498
			//		the action log. Note that any action log entries that
499
			//		were processed before have their effects retained (i.e.
500
			//		they are not rolled back), while the action entry that was
501
			//		halted stays in our list of actions to later be replayed.
502
			if(!this.isReplaying){
503
				return;
504
			}
505
 
506
			if(reason){
507
				this.reasonHalted = reason.toString();
508
			}
509
 
510
			// save the state of our action log, then
511
			// tell anyone who is interested that we are
512
			// done when we are finished saving
513
			if(this.autoSave){
514
				var self = this;
515
				this._save(function(){
516
					self.isReplaying = false;
517
					self.onReplayFinished();
518
				});
519
			}else{
520
				this.isReplaying = false;
521
				this.onReplayFinished();
522
			}
523
		},
524
 
525
		continueReplay: function(){ /* void */
526
			// summary:
527
			//		Indicates that we should continue processing out list of
528
			//		actions.
529
			// description:
530
			//		This method is called by applications that have overridden
531
			//		dojox.off.sync.actions.onReplay() to continue replaying our
532
			//		action log after the application has finished handling the
533
			//		current action.
534
			if(!this.isReplaying){
535
				return;
536
			}
537
 
538
			// shift off the old action we just ran
539
			this.entries.shift();
540
 
541
			// are we done?
542
			if(!this.entries.length){
543
				// save the state of our action log, then
544
				// tell anyone who is interested that we are
545
				// done when we are finished saving
546
				if(this.autoSave){
547
					var self = this;
548
					this._save(function(){
549
						self.isReplaying = false;
550
						self.onReplayFinished();
551
					});
552
					return;
553
				}else{
554
					this.isReplaying = false;
555
					this.onReplayFinished();
556
					return;
557
				}
558
			}
559
 
560
			// get the next action
561
			var nextAction = this.entries[0];
562
			this.onReplay(nextAction, this);
563
		},
564
 
565
		clear: function(){ /* void */
566
			// summary:
567
			//	Completely clears this action log of its entries
568
 
569
			if(this.isReplaying){
570
				return;
571
			}
572
 
573
			this.entries = [];
574
 
575
			// save our updated state into persistent
576
			// storage
577
			if(this.autoSave){
578
				this._save();
579
			}
580
		},
581
 
582
		replay: function(){ /* void */
583
			// summary:
584
			//	For advanced usage; most developers can ignore this.
585
			//	Replays all of the commands that have been
586
			//	cached in this command log when we go back online;
587
			//	onCommand will be called for each command we have
588
 
589
			if(this.isReplaying){
590
				return;
591
			}
592
 
593
			this.reasonHalted = null;
594
 
595
			if(!this.entries.length){
596
				this.onReplayFinished();
597
				return;
598
			}
599
 
600
			this.isReplaying = true;
601
 
602
			var nextAction = this.entries[0];
603
			this.onReplay(nextAction, this);
604
		},
605
 
606
		// onReplayFinished: Function
607
		//	For advanced usage; most developers can ignore this.
608
		//	Called when we are finished replaying our commands;
609
		//	called if we have successfully exhausted all of our
610
		//	commands, or if an error occurred during replaying.
611
		//	The default implementation simply continues the
612
		//	synchronization process. Connect to this to register
613
		//	for the event:
614
		//
615
		//		dojo.connect(dojox.off.sync.actions, "onReplayFinished",
616
		//					someFunc)
617
		onReplayFinished: function(){
618
		},
619
 
620
		toString: function(){
621
			var results = "";
622
			results += "[";
623
 
624
			for(var i = 0; i < this.entries.length; i++){
625
				results += "{";
626
				for(var j in this.entries[i]){
627
					results += j + ": \"" + this.entries[i][j] + "\"";
628
					results += ", ";
629
				}
630
				results += "}, ";
631
			}
632
 
633
			results += "]";
634
 
635
			return results;
636
		},
637
 
638
		_save: function(callback){
639
			if(!callback){
640
				callback = function(){};
641
			}
642
 
643
			try{
644
				var self = this;
645
				var resultsHandler = function(status, key, message){
646
					//console.debug("resultsHandler, status="+status+", key="+key+", message="+message);
647
					if(status == dojox.storage.FAILED){
648
						dojox.off.onFrameworkEvent("save",
649
											{status: dojox.storage.FAILED,
650
											isCoreSave: true,
651
											key: key,
652
											value: message,
653
											namespace: dojox.off.STORAGE_NAMESPACE});
654
						callback();
655
					}else if(status == dojox.storage.SUCCESS){
656
						callback();
657
					}
658
				};
659
 
660
				dojox.storage.put("actionlog", this.entries, resultsHandler,
661
									dojox.off.STORAGE_NAMESPACE);
662
			}catch(exp){
663
				console.debug("dojox.off.sync._save: " + exp.message||exp);
664
				dojox.off.onFrameworkEvent("save",
665
							{status: dojox.storage.FAILED,
666
							isCoreSave: true,
667
							key: "actionlog",
668
							value: this.entries,
669
							namespace: dojox.off.STORAGE_NAMESPACE});
670
				callback();
671
			}
672
		},
673
 
674
		_load: function(callback){
675
			var entries = dojox.storage.get("actionlog", dojox.off.STORAGE_NAMESPACE);
676
 
677
			if(!entries){
678
				entries = [];
679
			}
680
 
681
			this.entries = entries;
682
 
683
			callback();
684
		}
685
	}
686
);
687
 
688
dojox.off.sync.actions = new dojox.off.sync.ActionLog();
689
 
690
}