-
Notifications
You must be signed in to change notification settings - Fork 57
/
Copy pathmodel.js
executable file
·1849 lines (1611 loc) · 83 KB
/
model.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* **************************************************************
Copyright 2011 Zoovy, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
************************************************************** */
/*
Our goal is to have a very limited number of 'models' out there.
More controllers may exist than models.
More Extensions will exist than controllers.
view files will be distributed en-masse.
The model is primarily used only for sending and receiving data requests.
The model is also responsible for loading extensions (essentially a request with a fixed callback).
It then saves that data/extension into the control obj.
most code for display is in the 'view', which may be a TOXML file.
some display code may be present in the renderFormat portion of the control.
The view and the model should never speak to each other directly... other than maybe a button in the view executing this.dispatchThis().
There are two types of data being dealt with:
1. ajax request (referred to as request from here forward) for store data, such as product info, product lists, merchant info, etc.
2. extensions loaded as part of control object instantiation or on the fly as needed.
Some high level error handling occurs as part of the model, mostly for ISE's.
The rest is in the controller or extension. The model will execute callbacks.CALLBACKID.onError(d,uuid)
-> d is the data object returned by the server. one of the few cases the raw response is returned.
execute the zModel function from within the controller. ex: this = zModel();
-> this will return an object into the this namespace. ex: this.dispatchThis()
There are currently three dispatch queues. (Q or DQ) which reside in _app.q.
Each queue can be filled with several requests, then dispatched all at once.
that'll piggyback several dispatches on one request, which is considerably more efficient
than several requests.
q.passive - should only be used for requests with no callbacks or for requests that should never get cancelled by another, but are not strictly necessary.
For instance, if you wanted to retrieve product data for page
2 and 3 when the user is on page 1, you'd use passive. No action occurs as a results of getting this data.
Or if you have dispatches occur during your app init but are concerned they could get over-written by the immutable q (such as requests for site nav that you don't want nukd if the user is coming in to the app for checkout) then use passive.
the passive q requests are never aborted.
q.mutable - this is the bulk of your calls. Getting information and then doing something with it (executed from the callback). It is called mutable
because it can be muted by the this.abortQ() function
will abort all mutable requests. Use this when the users direction changes (they click page 1, then quickly click page 2 - you'd want to cancel the requests for
page 1 so their callbacks are not executed).
q.immutable - use this sparingly, for 'mission critical' requests, such as add to cart, update cart and any requests during checkout. Only 1 immutable request will fire at a time.
if any mutable requests are in process, they're cancelled. if an immutable request is in process and another request comes in (mutable or immutable),
the secondary request is delayed till the first is done.
in some cases, you may use a mutable request for an add to cart so that the user can push it and move on with their experience. That's ok.
however, it is highly recommended that you only use the immutable q for checkout.
even though multiple dispatches can occur at the same time, it's still in your interest to pack the queue and dispatch at once.
it'll be faster with fewer dispatches.
_app.globalAjax.overrideAttempts - keeps track of how many times a dispatch has attempted while another is in progress.
_app.globalAjax.lastDispatch - keeps track of when the last dispatch occurs. Not currently used for much, but could allow for some auto-passive dispatches when idle.
*/
function model(_app) {
var r = {
version : "201405",
// --------------------------- GENERAL USE FUNCTIONS --------------------------- \\
//pass in a json object and the last item id is returned.
//used in some of the fetchUUID function.
// note that this is potentially a heavy function (depending on object size) and should be used sparingly.
getLastIndex : function(obj) {
var prop, r;
for (prop in obj) {
r = prop;
}
_app.u.dump('END model.getLastIndex r = '+r);
return r;
}, //getLastIndex
//pass in a json object and how many tier1 nodes are returned. handy.
countProperties : function(obj) {
var prop;
var propCount = 0;
for (prop in obj) {
propCount++;
}
return propCount;
},//countProperties
// --------------------------- DISPATCH QUEUE FUNCTIONS --------------------------- \\
/*
addDispatchToQ
The function returns false if the dispatch was not added to q or the uuid if it was.
uuid is returned so that it can be passed into other dispatches if needed.
dispatch is a json object.
exactly what gets passed in depends on the type of request being made.
each dispatch will automatically get assigned the following, if not passed in:
-> uuid: Universal unique identifier. each request needs a unique id. best practice is to NOT pass one and to let this function generate one.
-> attempts: increments the number of attempts made on dispatch. will get set to zero if not set.
-> status: dispatches are not deleted from the queue. instead, a status is set (such as completed).
Depending on the command, multiple params may be required. However to keep this function light, only _cmd is validated.
In most cases, the following _rtag items should be set:
_rtag.callback -> a reference to an item in the control object that gets executed on successful request (response may include errors)
_rtag.datapointer -> this is where in _app.data the information returned will be saved to.
ex: datapointer:appProductGet|SKU would put product data into _app.data['appProductGet|SKU']
if no datapointer is passed, no data is returned in an accessable location for manipulation.
-> the only time the actual response is passed back to the control is if an error was present.
*/
addDispatchToQ : function(dispatch,QID) {
// _app.u.dump('BEGIN: addDispatchToQ');
// _app.u.dump(" -> QID: "+typeof QID);
// QID default needs to be before the string check (because a blank value typeof != string.
QID = (QID === undefined) ? 'mutable' : QID; //default to the mutable Q, but allow for PDQ to be passed in.
var r; // return value.
if(dispatch && !dispatch['_cmd']) {
_app.u.dump("in model.addDispatchToQ, no _cmd was set in dispatch. dispatch follows: ","warn"); dump(dispatch);
r = false;
}
// if QID was not a string, a catastropic JS error occured. could (and did) happen if call has bug in it.
else if(typeof QID != 'string') {
r = false;
_app.u.dump("Unable to add dispatch to Queue. QID passed was not a string. dispatch and QID follow:","error");
//the info below is meant to help troubleshoot where the error occured.
_app.u.dump("dispatch: "); _app.u.dump(dispatch);
_app.u.dump("QID: "); _app.u.dump(QID);
}
else {
// there was an issue w/ a serialized form object being added directly to the Q (as opposed to going through a .call) and then the form being re-submitted after a correction
// but the original dispatch was getting resent instead of the updated command. extend creates a duplicate w/ no pointers. solved the issue.
// full explanation here: https://github.com/zoovy/AnyCommerce-Development/commit/41396eb5546621c0b31e3273ecc314e686daf4bc
var tmp = $.extend(true,{},dispatch);
var uuid = this.fetchUUID() //uuid is obtained, not passed in.
tmp["_uuid"] = uuid;
tmp._tag = tmp._tag || {}; //the following line will error if tag is not an object. define as such if not already defined.
tmp['_tag']["status"] = 'queued';
_app.q[QID][uuid] = tmp;
r = uuid;
// this breaks stuff.
// this.writeLocal("response_"+uuid, JSON.stringify(tmp),'session'); //save a copy of each dispatch to sessionStorage for entymologist
}
return r;
},// addDispatchToQ
//if an error happens during request or is present in response, change items from requested back to queued and adjust attempts.
//this is to make sure nothing gets left undispatched on critical errors.
changeDispatchStatusInQ: function(QID,UUID,STATUS) {
var r = true;
if(!QID || !UUID)
r = false;
else {
STATUS = STATUS === undefined ? 'UNSET' : STATUS; //a default, mostly to help track down that this function was run without a status set.
if(_app.q[QID] && _app.q[QID][UUID]) {
_app.q[QID][UUID]._tag.status = STATUS;
}
}
return r;
},
//used during dispatchThis to make sure only queued items are dispatched.
//returns items for the Q and sets status to 'requesting'.
//returns items in reverse order, so the dispatches occur as FIFO.
//in the actual DQ, the item id is the uuid. Here that gets dropped and the object id's start at zero (more friendly format for B).
//puuid is the parent/pipe uuid. it's added to the original dispatch for error handling
filterQ : function(QID,puuid) {
// _app.u.dump("BEGIN: filterQ");
// _app.u.dump(" -> QID = "+QID);
var c = 0; //used to count how many dispatches are going into q. allows a 'break' if too many are present. is also 'index' of most recently added item in myQ.
var myQ = new Array();
//go through this backwards so that as items are removed, the changing .length is not impacting any items index that hasn't already been iterated through.
for(var index in _app.q[QID]) {
// _app.u.dump(index+"). "+_app.q[QID][index]._cmd+" status: "+_app.q[QID][index]._tag.status);
if(_app.q[QID][index]._tag && _app.q[QID][index]._tag.status == 'queued') {
_app.q[QID][index]._tag.status = "requesting";
// _app.u.dump(" -> new status: "+_app.q[QID][index]._tag.status);
if(puuid){_app.q[QID][index]._tag.pipeUUID = puuid}
myQ.push($.extend(true,{},_app.q[QID][index])); //creates a copy so that myQ can be manipulated without impacting actual Q. allows for _tag to be removed.
//the following are blanked out because they're not 'supported' vars. eventually, we should move this all into _tag so only one field has to be blanked.
delete myQ[c]['_tag']; //blank out rtag to make requests smaller. handleResponse will check if it's set and re-add it to pass into callback.
//* 201338 -> moved the c increment to below the comparison and changed from c > to c >=. that way if numRequestsPP = 1, this will work correctly.
// if(c > _app.globalAjax.numRequestsPerPipe){
if(c >= _app.globalAjax.numRequestsPerPipe){
setTimeout(function(){
_app.model.dispatchThis(QID);
},500); //will fire off the remaining items in the q shortly.
break //max of 100 dispatches at a time.
}
c++;
}
}
return myQ;
}, //filterQ
//execute this function in the app itself when a request is/may be in progrees and the user changes course.
//for instance, if the user does a search for 'sho' then corrects to 'shoe' prior to the request being completed,
//you'd want to abort the request in favor of the new command (you would not want a callback executed on 'sho', so cancel it).
//for this reason, the passive q requests should NEVER have callbacks on them.
//and tho technically you 'could' pass immutable as the QID, you should never cancel an immutable request, as these should be saved for 'add to cart' or 'checkout' requests.
abortQ : function(QID) {
_app.u.dump("SANITY -> abortq is being run on "+QID); //output this so that when the red cancelled request shows in the console, we know why.
_app.globalAjax.overrideAttempts = 0; //the q is being reset essentially. this needs to be reset to zero so attempts starts over.
var r = 0; //incremented with each cancelled request. returned.
for(var index in _app.globalAjax.requests[QID]) {
_app.globalAjax.requests[QID][index].abort();
//IMPORTANT
//this delete removes the ajax request from the requests array. keep that array only full of active requests.
delete _app.globalAjax.requests[QID][index];
r +=1;
}
// _app.globalAjax.requests.mutable = {}; //probably not needed since we're deleting these individually above. add back if needed.
return r;
},
//Allows for the abort of a request. Aborting a request will NOT trigger the error handler, as an abort is not an error.
abortRequest : function(QID,UUID) {
if(QID && UUID && _app.globalAjax.requests[QID][UUID]) {
_app.u.dump("model.abortRequest run on QID "+QID+" for UUID "+UUID);
_app.globalAjax.requests[QID][UUID].abort();
this.changeDispatchStatusInQ(QID,UUID,'aborted');
delete _app.globalAjax.requests[QID][UUID];
}
else {
_app.u.throwGMessage("In model.abortRequest, either QID ["+QID+"] or UUID ["+UUID+"] blank or _app.globalAjax.requests[QID][UUID] does not exist (request may have already completed)");
}
},
/*
sends dispatches with status of 'queued' in a single json request.
only high-level errors are handled here, such as an ISE returned from server, no connection, etc.
a successful request executes handleresponse (handleresponse executes the controller.response.success action)
note - a successful request just means that contact with the server was made. it does not mean the request itself didn't return errors.
QID = Queue ID. Defaults to the general dispatchQ but allows for the PDQ to be used.
either false (if no dispatch occurs) or the pipe uuid are returned. The pipe uuid can be used to cancel the request.
*/
dispatchThis : function(QID) {
// _app.u.dump("'BEGIN model.dispatchThis ["+QID+"]");
var r = true; //set to false if no dispatch occurs. set to pipeuuid if a dispatch occurs. this is the value returned.
QID = QID === undefined ? 'mutable' : QID; //default to the general Q, but allow for priorityQ to be passed in.
//used as the uuid on the 'parent' request (the one containing the pipelines).
//set this early so that it can be added to each request in the Q as pipeUUID for error handling.
//also used for ajax.requests[QID][UUID] which stores the ajax request itself (and is used for aborting later, if need be).
var pipeUUID = this.fetchUUID();
//by doing our filter first, we can see if there is even anything to BE dispatched before checking for conflicts.
//this decreases the likelyhood well set a timeout when not needed.
var Q = this.filterQ(QID,pipeUUID); //filters out all non-queued dispatches. may set a limit to the # of dispatches too.
// var immutableRequestInProgress = $.isEmptyObject(_app.globalAjax.requests.immutable) ? false : true; //if empty, no request is in progress.
var L = Q.length; //size of Q.
// _app.u.dump(" -> Q.length = "+Q.length); _app.u.dump(Q);
// _app.u.dump("QID = "+QID+" and L = "+L+" and aRequestIsInProgress = "+aRequestIsInProgress);
if(L == 0) {
// _app.u.dump(" -> dispatch attempted, but q referenced has no 'queued' dispatches. Do nothing.");
r = false; //nothing to dispatch.
}
else {
// _app.u.dump(" -> DQ has queued dispatches. no request in process. Move along... Move along...");
}
/*
Should only reach this point IF no PRIORITY dispatch running.
set control var so that if another request is made while this one is executing, we know whether or not this one is priority.
the var also gets used in the handleresponse functions to know which q to look in for callbacks.
don't move this. if it goes before some other checks, it'll resed the Qinuse var before it's checked.
*/
if(r) {
//only change the Qinuse var IF we are doing a dispatch. otherwise when the value is used later, it'll be pointing at the wrong Q.
//this var is used to reference whether the q in use is immutable or not. Never use this during handleResponse or anywhere else.
//it is constantly being overwritten, emptied, etc and by the time handle_response is running, another request could occur using a different Q
//and your code breaks.
//if this point is reached, we are exeuting a dispatch. Any vars used for tracking overrides, last dispatch, etc get reset.
_app.globalAjax.lastDispatch = _app.u.epochNow();
_app.globalAjax.overrideAttempts = 0;
//IMPORTANT
/*
the delete in the success AND error callbacks removes the ajax request from the requests array.
If this isn't done, attempts to see if an immutable or other request is in process will return inaccurate results.
must be run before handleResponse so that if handleresponse executes any requests as part of a callback, no conflicts arise.
can't be added to a 'complete' because the complete callback gets executed after the success or error callback.
*/
_app.globalAjax.requests[QID][pipeUUID] = $.ajax({
type: "POST",
url: _app.vars.jqurl,
// context : app,
async: true,
contentType : "text/json",
dataType:"json",
//ok to pass admin vars on non-admin session. They'll be ignored.
data: JSON.stringify({"_uuid":pipeUUID,"_session":_app.vars._session,"_cmd":"pipeline","@cmds":Q,"_clientid":_app.vars._clientid,"_domain":_app.vars.domain,"_userid":_app.vars.userid,"_deviceid":_app.vars.deviceid,"_authtoken":_app.vars.authtoken,"_version":_app.model.version})
});
_app.globalAjax.requests[QID][pipeUUID].error(function(j, textStatus, errorThrown) {
// _app.u.dump(" ------------------------ ");
_app.u.dump("UH OH! got into ajaxRequest.error. either call was aborted or something went wrong.");
// _app.u.dump(j); _app.u.dump("textStatus: "+textStatus); _app.u.dump(errorThrown);
// _app.u.dump(" ------------------------ ");
if(textStatus == 'abort') {
delete _app.globalAjax.requests[QID][pipeUUID];
for(var index in Q) {
_app.model.changeDispatchStatusInQ(QID,Q[index]['_uuid'],'abort');
}
}
else {
_app.u.dump(' -> REQUEST FAILURE! Request returned high-level errors or did not request: textStatus = '+textStatus+' errorThrown = '+errorThrown,'error');
delete _app.globalAjax.requests[QID][pipeUUID];
_app.model.handleCancellations(Q,QID);
if(typeof jQuery().hideLoading == 'function'){
// $(".loading-indicator-overlay").parent().hideLoading();
// ** 201403 -> rather than targeting a child and then going up the dom, we'll target the element that had showLoading applied to it directly.
$(".ui-showloading").hideLoading(); //kill all 'loading' gfx. otherwise, UI could become unusable.
}
// setTimeout("_app.model.dispatchThis('"+QID+"')",1000); //try again. a dispatch is only attempted three times before it errors out.
}
});
_app.globalAjax.requests[QID][pipeUUID].success(function(d) {
delete _app.globalAjax.requests[QID][pipeUUID];
_app.model.handleResponse(d,QID,Q);
}
)
r = pipeUUID; //return the pipe uuid so that a request can be cancelled if need be.
}
return r;
// _app.u.dump('//END dispatchThis');
}, //dispatchThis
//run when a request fails, most likely due to an ISE
handleCancellations : function(Q,QID) {
var uuid;
for(var index in Q) {
uuid = Q[index]['_uuid'];
_app.model.changeDispatchStatusInQ(QID,uuid,'cancelledDueToErrors');
//make sure a callback is defined.
var msgDetails = "<ul>";
msgDetails += "<li>issue: API request failure (likely an ISE)<\/li>";
msgDetails += "<li>uri: "+document.location+"<\/li>";
msgDetails += "<li>_cmd: "+Q[index]['_cmd']+"<\/li>";
msgDetails += "<li>domain: "+_app.vars.domain+"<\/li>";
msgDetails += "<li>release: "+_app.model.version+"|"+_app.vars.release+"<\/li>";
msgDetails += "<\/ul>";
this.handleErrorByUUID(uuid,QID,{'errid':666,'errtype':'ise','persistent':true,'errmsg':'The request has failed. The app may continue to operate normally.<br \/>Please try again or contact the site administrator with the following details:'+msgDetails})
}
},
// --------------------------- HANDLERESPONSE FUNCTIONS --------------------------- \\
handleErrorByUUID : function(UUID,QID,responseData) {
// _app.u.dump("BEGIN model.handleErrorByUUID ["+UUID+"]");
if(QID && UUID && responseData) {
responseData['_rtag'] = responseData['_rtag'] || this.getRequestTag(UUID); //_tag is stripped at dispatch and readded. make sure it's present.
if(responseData['_rtag']) {
var Q = _app.q[QID];
if(Q[UUID]['_tag'] && Q[UUID]['_tag']['callback']) {
var callback = Q[UUID]['_tag']['callback'];
//callback is an anonymous function. Execute it.
if(typeof callback == 'function') {
callback(responseData,UUID)
}
//callback is defined in extension or controller as object (with onSuccess and maybe onError)
else if(typeof callback == 'string') {
callback = Q[UUID]['_tag']['extension'] ? _app.ext[Q[UUID]['_tag']['extension']].callbacks[Q[UUID]['_tag']['callback']] : _app.callbacks[Q[UUID]['_tag']['callback']];
if(callback && typeof callback.onError == 'function'){
callback.onError(responseData,UUID);
}
else{
_app.u.throwMessage(responseData);
}
}
else {
//unknown type for callback.
_app.u.throwMessage(responseData);
}
}
//_rtag defined, but no callback.
else {
_app.u.dump(" -> callback not set");
_app.u.throwMessage(responseData);
}
}
//no callback is defined. throw generic messag
else {
_app.u.dump(" -> rtag not set");
_app.u.throwMessage(responseData);
}
}
else {
_app.u.dump("WARNING! required params for handleErrorByUUID not all present:");
_app.u.dump(" -> UUID: "+UUID);
_app.u.dump(" -> QID: "+QID);
_app.u.dump(" -> typeof responseData: "+typeof responseData);
}
},
/*
handleResponse and you...
some high level errors, like no cartid or invalid json or whatever get handled in handeResponse
lower level (_cmd specific) get handled inside their independent response functions or in responseHasErrors(), as they're specific to the _cmd
if no high level errors are present, execute a response function specific to the request (ex: request of addToCart executed handleResponse_addToCart).
this allows for request specific errors to get handled on an individual basis, based on request type (addToCart errors are different than appProductGet errors).
the defaultResponse also gets executed in most cases, if no errors are encountered.
the defaultResponse contains all the 'success' code, since it is uniform across commands.
QID is the dispatchQ ID (either passive, mutable or immutable. required for the handleReQ function.
*/
handleResponse : function(responseData,QID,Q) {
// _app.u.dump('BEGIN model.handleResponse.');
//if the request was not-pipelined or the 'parent' pipeline request contains errors, this would get executed.
//the handlereq function manages the error handling as well.
if(responseData && !$.isEmptyObject(responseData)) {
var uuid = responseData['_uuid'];
var QID = QID || this.whichQAmIFrom(uuid);
// _app.u.dump(" -> responseData is set. UUID: "+uuid);
//if the error is on the parent/piped request, no qid will be set.
//if an iseerr occurs, than even in a pipelined request, errid will be returned on 'parent' and no individual responses are returned.
if(responseData && (responseData['_rcmd'] == 'err' || responseData.errid)) {
_app.u.dump(' -> API Response for '+QID+' Q contained an error at the top level (on the pipe)','warn');
$('.ui-showloading').hideLoading(); //make sure all the showLoadings go away.
//brian says that an error 10 will always ONLY be for admin. 2014-03-20
if(responseData.errid == 10) {
_app.u.dump(" -> errid of 10 corresponds to an expired token. ");
_app.ext.admin.callbacks.handleLogout.onSuccess({"msg":"You were logged out because the token you were using has expired. Please log in to continue."});
}
if(Q && Q.length) {
// _app.u.dump(" -> Q.length: "+Q.length); _app.u.dump(Q);
for(var i = 0, L = Q.length; i < L; i += 1) {
this.handleErrorByUUID(Q[i]._uuid,QID,responseData);
}
}
//QID will b set if this is a NON pipelined request.
// could get here in a non-pipelined request.
else if(QID) {
// _app.u.dump(responseData);
this.handleErrorByUUID(responseData['_uuid'],QID,responseData);
}
else {
//most likely, a pipelined request that failed at a high level.
QID = this.getQIDFromPipeUUID(uuid) //will try to ascertain what QID to look into for error handling.
responseData.errmsg = "Something has gone wrong. please try again or refresh. If the error persists, please contact the site administrator. ["+responseData.errmsg+"]";
if(QID) {
var uuids = this.getUUIDsbyQIDandPipeUUID(QID,responseData['_uuid']);
// _app.u.dump(" -> uuids: "); _app.u.dump(uuids);
var L = uuids.length
if(L) {
_app.u.dump(" -> # uuids in failed pipe: "+L);
for(var i = 0; i < L; i += 1) {
this.handleErrorByUUID(_app.q[QID][uuids[i]]['_uuid'],QID,responseData);
}
}
else {_app.u.throwMessage(responseData);} //don't suspect we'd get here, but best practice error handling would be to throw some error as opposed to nothing.
}
else {
//still unable to determine Q. throw some generic error message along with response error.
_app.u.dump("ERROR! a high level error occured and the Q ID was unable to be determined.");
_app.u.throwMessage(responseData);
if(typeof jQuery().hideLoading == 'function'){
$(".loading-indicator-overlay").parent().hideLoading(); //kill all 'loading' gfx. otherwise, UI could become unusable.
}
}
}
}
//pipeline request
else if(responseData && responseData['_rcmd'] == 'pipeline') {
// _app.u.dump(' -> pipelined request. size = '+responseData['@rcmds'].length);
for (var i = 0, j = responseData['@rcmds'].length; i < j; i += 1) {
// _tag is reassociated early so that the data is available as quick as possible, including in any custom handleResponse_ functions
//has to be called before writing to local because datapointer is in _tag
responseData['@rcmds'][i]['_rtag'] = responseData['@rcmds'][i]['_rtag'] || this.getRequestTag(responseData['@rcmds'][i]['_uuid']);
this.writeToMemoryAndLocal(responseData['@rcmds'][i]);
}
//handle all the call specific handlers.
for (var i = 0, j = responseData['@rcmds'].length; i < j; i += 1) {
responseData['@rcmds'][i].ts = _app.u.epochNow() //set a timestamp on local data
if(typeof this['handleResponse_'+responseData['@rcmds'][i]['_rcmd']] == 'function') {
this['handleResponse_'+responseData['@rcmds'][i]['_rcmd']](responseData['@rcmds'][i]) //executes a function called handleResponse_X where X = _cmd, if it exists.
// _app.u.dump("CUSTOM handleresponse defined for "+responseData['_rcmd']);
}
else {
// _app.u.dump(' -> going straight to defaultAction');
this.handleResponse_defaultAction(responseData['@rcmds'][i],null);
// _app.u.dump("NO custom handleresponse defined for "+responseData['_rcmd']);
}
}
}
//a solo successful request.
//the logic for the order here is the same as in the pipelined response, where it is documented.
else {
responseData['_rtag'] = responseData['_rtag'] || this.getRequestTag(responseData['_uuid']);
this.writeToMemoryAndLocal(responseData['@rcmds'])
if(responseData['_rcmd'] && typeof this['handleResponse_'+responseData['_rcmd']] == 'function') {
// _app.u.dump("CUSTOM handleresponse defined for "+responseData['_rcmd']);
this['handleResponse_'+responseData['_rcmd']](responseData) //executes a function called handleResponse_X where X = _cmd, if it exists.
}
else {
// _app.u.dump("NO custom handleresponse defined for "+responseData['_rcmd']);
this.handleResponse_defaultAction(responseData,null);
}
}
}
else {
//if responseData isn't set, an uber-high level error occured.
_app.u.throwMessage("Uh oh! Something has gone very wrong with our _app. We apologize for any inconvenience. Please try agian. If error persists, please contact the site administrator.");
}
}, //handleResponse
//this will remove data from both local storage, session storage AND memory.
//execute this on a field prior to a call when you want to ensure memory/local is not used (fresh data).
destroy : function(key) {
// _app.u.dump(" -> destroying ["+key+"]");
if(_app.data[key]) {
delete _app.data[key];
}
_app.model.nukeLocal(key,'local');
_app.model.nukeLocal(key,'session');
},
//this will write the respose both to local and/or session storage and into _app.data
writeToMemoryAndLocal : function(responseData) {
var datapointer = false;
if(responseData['_rtag']) {datapointer = responseData['_rtag']['datapointer']}
//if datapointer is not set, data is automatically NOT saved to localstorage or memory.
//however, there is (ping) already, and could be more, cases where datapointers are set, but we don't want the data locally or in memory.
//so we have simple functions to check by command.
if(datapointer && !_app.model.responseHasErrors(responseData) && !_app.model.responseIsMissing(responseData)) {
//this is the data that will be saved into local or session.
var obj4Save = $.extend(true,{},responseData); //this makes a copy so that the responseData object itself isn't impacted.
obj4Save._rtag = null; //make sure _rtag doesn't get saved to localstorage. may contiain a jquery object, function, etc.
// *** 201401 -> The data stored in memory no longer contains the _rtag.
// the _tag data can be found w/ the original dispatch in _app.q[QID][uuid]
if(this.thisGetsSaved2Memory(responseData['_rcmd'])) {
_app.data[datapointer] = obj4Save;
}
if(this.thisGetsSaved2Local(responseData['_rcmd'])){
_app.u.dump(" -> passed check for save local: "+responseData['_rcmd']);
_app.model.writeLocal(datapointer,obj4Save,'local'); //save to localStorage, if feature is available.
}
if(this.thisGetsSaved2Session(responseData['_rcmd'])) {
_app.model.writeLocal(datapointer,obj4Save,'session'); //save to sessionStorage, if feature is available.
}
}
else {
//catch. not writing to local. Either not necessary or an error occured.
}
}, //writeToMemoryAndLocal
cmdIsAnAdminUpdate : function(cmd) {
var r = false;
if(cmd.indexOf('admin') === 0) {
//admin updates don't need to be saved to memory.
if(cmd.indexOf('Update', cmd.length - 6) >= 0) {r = true;}
else if(cmd.indexOf('Macro', cmd.length - 5) >= 0) {r = true;}
}
return r;
},
thisGetsSaved2Memory : function(cmd) {
var r = true;
if(cmd == 'adminNavcatMacro') {}
// ** 201402 -> if they don't need to be saved to memory, don't put a datapointer on them. But allow them to be added if necessary.
// else if(this.cmdIsAnAdminUpdate(cmd)) {
// r = false;
// }
else {
switch(cmd) {
case 'appPageGet': //saved into category object earlier in process. redundant here.
case 'cartSet': //changes are reflected in cart object.
// case 'ping': //ping may be necessary in memory for anycontent in conjunction w/ extending by datapointers. rss is a good example of this.
r = false
break;
}
}
return r;
},
//localStorage is reserved for data that MUST be carried between sessions. For everything else, sessionStorage is used.
thisGetsSaved2Local : function(cmd){
var r = false;
switch(cmd) {
case 'authAdminLogin':
// case 'appBuyerLogin': //necessary for buyer login to be persistant
// case 'whoAmI': //necessary for buyer login to be persistant
r = true;
}
return r;
},
//Session is a safe place to store most data, as it'll be gone once the browser is closed. Still, keep CC data out of there.
thisGetsSaved2Session : function(cmd) {
var r = true; //what is returned. is set to false if the cmd should not get saved to local storage.
if(this.cmdIsAnAdminUpdate(cmd)) {
r = false;
}
else {
switch(cmd) {
case 'adminCustomerWalletPeek': //contains cc #
case 'adminOrderCreate': //may contain cc
case 'adminOrderDetail': //may contain cc
case 'adminOrderPaymentAction': //may contain cc
case 'appBuyerLogin': //should be session specific. close/open will exec whoAmI which will put into memory if user is logged in.
case 'buyerWalletList': //conains some cc info.
case 'cartOrderCreate': //may contain cc
case 'cartPaymentQ': //may contain cc
case 'cartSet': //changes are reflected in cart object.
case 'ping':
r = false
break;
}
}
return false; //saving to session was causing a LOT of memory to be used in FF.
},
//gets called for each response in a pipelined request (or for the solo response in a non-pipelined request) in most cases. request-specific responses may opt to not run this, but most do.
handleResponse_defaultAction : function(responseData) {
// _app.u.dump('BEGIN handleResponse_defaultAction');
var callback = false; //the callback name.
var uuid = responseData['_uuid']; //referenced enough to justify saving to a var.
var datapointer = null; //a callback can be set with no datapointer.
var status = null; //status of request. will get set to 'error' or 'completed' later. set to null by defualt to track cases when not set to error or completed.
//check for missing first or hasErrors will flag it as an error.
if(_app.model.responseIsMissing(responseData)) {status = 'missing'}
else if(_app.model.responseHasErrors(responseData)) {status = 'error'}
else {} //status will get set later.
if(!$.isEmptyObject(responseData['_rtag']) && _app.u.isSet(responseData['_rtag']['callback'])) {
//callback has been defined in the call/response.
callback = responseData['_rtag']['callback']; //shortcut
// _app.u.dump(' -> callback: '+(typeof callback == 'string' ? callback : 'function'));
if(typeof callback == 'function'){} //do nothing to callback. will get executed later.
else if(responseData['_rtag']['extension'] && _app.ext[responseData['_rtag']['extension']] && _app.ext[responseData['_rtag']['extension']].callbacks && !$.isEmptyObject(_app.ext[responseData['_rtag']['extension']].callbacks[callback])) {
callback = _app.ext[responseData['_rtag']['extension']].callbacks[callback];
// _app.u.dump(' -> callback node exists in _app.ext['+responseData['_rtag']['extension']+'].callbacks');
}
else if(!$.isEmptyObject(_app.callbacks[callback])) {
callback = _app.callbacks[callback];
// _app.u.dump(' -> callback node exists in _app.callbacks');
}
else {
callback = false;
_app.u.dump('A callback defined but does not exist. The _rtag follows: ','warn'); _app.u.dump(responseData['_rtag']);
}
}
else {callback = false;} //no callback defined.
//if no datapointer is set, the response data is not saved to local storage or into the _app. (add to cart, ping, etc)
//effectively, a request occured but no data manipulation is required and/or available.
//likewise, if there's an error in the response, no point saving this locally.
if(!$.isEmptyObject(responseData['_rtag']) && _app.u.isSet(responseData['_rtag']['datapointer']) && status != 'error' && status != 'missing') {
datapointer = responseData['_rtag']['datapointer'];
//on a ping, it is possible a datapointer may be set but we DO NOT want to write the pings response over that data, so we ignore pings.
//an appPageGet request needs the requested data to extend the original page object. (in case two separate request come in for different attributes for the same category.
if(responseData['_rcmd'] == 'ping' || responseData['_rcmd'] == 'appPageGet') {
}
else {
this.writeToMemoryAndLocal(responseData);
}
}
else {
// _app.u.dump(' -> no datapointer set for uuid '+uuid);
}
//errors present and a defined action for handling those errors is defined.
if((status == 'error' || status == 'missing') && callback) {
if(typeof callback == 'function') {
callback(responseData,uuid); //if an anonymous function is passed in, it handles does it's own error handling.
}
else if(typeof callback == 'object' && typeof callback['on'+(status == 'error' ? 'Error' : 'Missing')] == 'function'){
/*
below, responseData['_rtag'] was passed instead of uuid, but that's already available as part of the first var passed in.
uuid is more useful because on a high level error, rtag isn't passed back in responseData. this way uuid can be used to look up originat _tag obj.
*/
callback['on'+(status == 'error' ? 'Error' : 'Missing')](responseData,uuid);
}
else if(typeof callback == 'object' && typeof _app.u.throwMessage === 'function') {
//callback defined but no error case defined. use default error handling.
_app.u.throwMessage(responseData);
}
else{
_app.u.dump('ERROR response for uuid '+uuid+'. callback defined but does not exist or is not valid type. callback = '+callback+' datapointer = '+datapointer)
}
}
//has errors but no error handler declared. use default
else if((status == 'error' || status == 'missing') && typeof _app.u.throwMessage === 'function') {
_app.u.throwMessage(responseData);
}
else if(_app.model.responseIsMissing(responseData) && !callback && typeof _app.u.throwMessage === 'function') {
_app.u.throwMessage(responseData);
}
//no errors. no callback.
else if(callback == false) {
status = 'completed';
// _app.u.dump(' --> no callback set in original dispatch. dq set to completed for uuid ('+uuid+')');
}
//to get here, no errors are present AND a callback is defined.
else {
status = 'completed';
if(typeof callback == 'function') {
// * 201334 -> responses contain macro-specific messaging. some or all of these may be success or fail, but the response is still considered a success.
callback(responseData._rtag,{'@RESPONSES':responseData['@RESPONSES']});
}
else if(typeof callback == 'object' && typeof callback.onSuccess == 'function') {
callback.onSuccess(responseData['_rtag'],{'@RESPONSES':responseData['@RESPONSES']}); //executes the onSuccess for the callback
}
else{
_app.u.dump(' -> successful response for uuid '+uuid+'. callback defined ('+callback+') but does not exist or is not valid type.')
}
}
var fromQ = _app.model.whichQAmIFrom(uuid);
if(fromQ && _app.q[fromQ] && _app.q[fromQ][Number(uuid)]) {
_app.q[_app.model.whichQAmIFrom(uuid)][Number(uuid)]._tag['status'] = status;
}
return status;
},
//after an order is created, the 'old' cart data gets saved into an order| for quick reference.
handleResponse_adminOrderCreate : function(responseData) {
this.handleResponse_cartOrderCreate(responseData); //share the same actions. append as needed.
},
handleResponse_authNewAccountCreate : function(responseData) {
_app.model.handleResponse_authAdminLogin(responseData); //this will have the same response as a login if successful.
},
/*
It is possible that multiple requests for page content could come in for the same page at different times.
so to ensure saving to appPageGet|.safe doesn't save over previously requested data, we extend it the ['%page'] object.
*/
handleResponse_appPageGet : function(responseData) {
if(responseData['_rtag'] && responseData['_rtag'].datapointer) {
var datapointer = responseData['_rtag'].datapointer;
if(_app.data[datapointer]) {
//already exists. extend the %page
_app.data[datapointer]['%page'] = $.extend(_app.data[datapointer]['%page'],responseData['%page']);
}
else {
_app.data[datapointer] = responseData;
}
_app.model.writeLocal(datapointer,_app.data[datapointer],'session'); //save to session storage, if feature is available.
}
_app.model.handleResponse_defaultAction(responseData);
}, //handleResponse_appPageGet
//this response is also executed by authNewAccoutnCreate
handleResponse_authAdminLogin: function(responseData) {
_app.u.dump("BEGIN model.handleResponse_authAdminLogin"); //_app.u.dump(responseData);
if(_app.model.responseHasErrors(responseData)) {} // do nothing. error handling handled in _default.
//executing this code block if an error is present will cause a JS error.
else {
_app.vars.deviceid = responseData.deviceid;
_app.vars.authtoken = responseData.authtoken;
_app.vars.userid = responseData.userid.toLowerCase();
_app.vars.username = responseData.username.toLowerCase();
_app.vars.thisSessionIsAdmin = true;
}
_app.model.handleResponse_defaultAction(responseData); //datapointer ommited because data already saved.
},
//this function gets executed upon a successful request for a new session id.
//it is also executed if appAdminAuthenticate returns exists=1 (yes, you can).
//formerly newSession
handleResponse_appCartCreate : function(responseData) {
//no error handling at this level. If a connection or some other critical error occured, this point would not have been reached.
//save session id locally to maintain session id throughout user experience.
this.addCart2Session(responseData['_cartid']);
_app.model.handleResponse_defaultAction(responseData); //datapointer ommited because data already saved.
_app.u.dump("cartID = "+responseData['_cartid']);
return responseData['_cartid'];
}, //handleResponse_appCartCreate
responseIsMissing : function(responseData) {
var r = false; //what is returned.
if(responseData['_rtag'] && responseData['_rtag'].forceMissing) {
r = true;
responseData.errid = "MVC-MISSING-000";
responseData.errtype = "missing";
responseData.errmsg = "forceMissing is true for _tag. cmd = "+responseData['_rcmd']+" and uuid = "+responseData['_uuid'];
// _app.u.dump(responseData);
}
else if(responseData['errtype'] == 'missing') {
r = true;
}
else {}
return r;
},
/*
in most cases, the errors are handled well by the API and returned either as a single message (errmsg)
or as a series of messages (_msg_X_id) where X is incremented depending on the number of errors.
*/
responseHasErrors : function(responseData) {
// _app.u.dump('BEGIN model.responseHasErrors');
//at the time of this version, some requests don't have especially good warning/error in the response.
//as response error handling is improved, this function may no longer be necessary.
var r = false; //defaults to no errors found.
if(responseData['_rtag'] && responseData['_rtag'].forceError) {
r = true;
responseData.errid = "MVC-ERROR-000";
responseData.errtype = "debug";
responseData.errmsg = "forceError is true for _tag. cmd = "+responseData['_rcmd']+" and uuid = "+responseData['_uuid'];
// _app.u.dump(responseData);
}
else {
switch(responseData['_rcmd']) {
case 'appProductGet':
case 'adminProductDetail':
//the API doesn't recognize doing a query for a sku and it not existing as being an error. handle it that way tho.
if(!responseData['%attribs'] || !responseData['%attribs']['db:id']) {
r = true;
responseData['errid'] = "MVC-M-100";
responseData['errtype'] = "missing";
responseData['errmsg'] = "could not find product "+responseData.pid+". Product may no longer exist. ";
} //db:id will not be set if invalid sku was passed.
break;
//most of the time, a successful response w/out errors is taken as a success. however, due to the nature of appCartCreate, we verify we have what we need.
case 'appCartCreate':
if(!responseData._cartid) {
r = true;
responseData['errid'] = "MVC-M-150";
responseData['errtype'] = "apperr";
responseData['errmsg'] = "appCartCreate response did not contain a _cartid.";
}
break;
case 'adminEBAYProfileDetail':
if(!responseData['%PROFILE'] || !responseData['%PROFILE'].PROFILE) {
r = true;
responseData['errid'] = "MVC-M-300";
responseData['errtype'] = "apperr";
responseData['errmsg'] = "profile came back either without %PROFILE or without %PROFILE.PROFILE.";
}
break;
case 'appNavcatDetail':
if(responseData.errid > 0 || responseData['exists'] == 0) {
r = true
responseData['errid'] = "MVC-M-200";
responseData['errtype'] = "apperr";
responseData['errmsg'] = "could not find category (may not exist)";
} //a response errid of zero 'may' mean no errors.
break;
default:
if(Number(responseData['errid']) > 0 && responseData.errtype != 'warn') {r = true;} //warnings do not constitute errors.
else if(Number(responseData['_msgs']) > 0) {
var errorTypes = new Array("youerr","fileerr","apperr","apierr","iseerr","cfgerr");
//the _msg format index starts at one, not zero.
for(var i = 1, L = Number(responseData['_msgs']); i <= L; i += 1) {
if($.inArray(responseData['_msg_'+i+'_type'],errorTypes) >= 0) {
r = true;
break; //once an error type is found, exit. one positive is enough.
}
}
}
// *** 201336 -> mostly impacts admin UI. @MSGS is another mechanism for alerts that needs to be checked.
else if(responseData['@MSGS'] && responseData['@MSGS'].length) {
var L = responseData['@MSGS'].length;
for(var i = 0; i < L; i += 1) {
if(responseData['@MSGS'][i]['!'] == 'ERROR') {
r = true;
break; //if we have an error, exit early.
}
}
}
else if(responseData['@RESPONSES'] && responseData['@RESPONSES'].length) {
for(var i = 0, L = responseData['@RESPONSES'].length; i < L; i += 1) {
if(responseData['@RESPONSES'][i]['msgtype'] == 'ERROR' || responseData['@RESPONSES'][i]['msgtype'] == 'apierr') {
r = true;
break; //if we have an error, exit early.
}
}
}
else {}
// _app.u.dump('default case for error handling');
break;
}
}
// if(r) {
// _app.u.dump(" -> responseData"); _app.u.dump(responseData);
// }
// _app.u.dump('//END responseHasErrors. has errors = '+r);
return r;
},
// --------------------------- FETCH FUNCTIONS --------------------------- \\
/*
each request must have a uuid (Unique Universal IDentifyer).
the uuid is also the item id in the dispatchQ. makes finding dispatches in Q faster/easier.
first check to see if the uuid is set in the _app. currently, this is considered a 'trusted' source and no validation is done.
then check local storage/cookie. if it IS set and the +1 integer is not set in the DQ, use it.
if local isn't set or is determined to be inaccurate (local + 1 is already set in DQ)
-> default to 999 if DQ is empty, which will start uuid's at 1000.
-> or if items are in the Q get the last entry and treat it as a number (this should only happen once in a session, in theory).
*/
fetchUUID : function() {
// _app.u.dump('BEGIN fetchUUID');
var uuid = false; //return value
var L;
if(_app.vars.uuid) {
// _app.u.dump(' -> isSet in _app. use it.');
uuid = _app.vars.uuid; //if the uuid is set in the control, trust it.
}
//in this else, the L is set to =, not == because it's setting itself to the value of the return of readLocal so readLocal doesn't have to be executed twice.
else if(L = _app.model.readLocal("uuid",'local')) {
L = Math.ceil(L * 1); //round it up (was occassionally get fractions for some odd reason) and treat as number.
// _app.u.dump(' -> isSet in local ('+L+' and typof = '+typeof L+')');
if($.isEmptyObject(_app.q.mutable[L+1]) && $.isEmptyObject(_app.q.immutable[L+1]) && $.isEmptyObject(_app.q.passive[L+1])){
uuid = L;