alchemymvc 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,573 @@
1
+ const QUEUE = [];
2
+ let queue_check_id,
3
+ _total_postponement_counter = 0;
4
+
5
+ /**
6
+ * The Postponement Class represents requests that will be handled later.
7
+ *
8
+ * @author Jelle De Loecker <jelle@elevenways.be>
9
+ * @since 1.3.1
10
+ * @version 1.3.1
11
+ *
12
+ * @param {Conduit} conduit The original conduit
13
+ */
14
+ const Postponement = Function.inherits('Alchemy.Base', 'Alchemy.Conduit', function Postponement(conduit, id, options) {
15
+
16
+ // The original conduit instance
17
+ this.original_conduit = conduit;
18
+
19
+ // The original response object of the conduit
20
+ this.original_response = conduit.response;
21
+
22
+ // Get the session
23
+ this.session = conduit.getSession();
24
+
25
+ // The identifier of this postponement
26
+ this.id = id;
27
+
28
+ // The original path string
29
+ this.original_path = conduit.path;
30
+
31
+ // The URL where to get postponement info
32
+ this.url = '/alchemy/postponed/' + id;
33
+
34
+ // The last known position in the queue
35
+ this.last_queue_position = null;
36
+
37
+ // Postponement options
38
+ this.options = options || {};
39
+
40
+ // When did this postponement start
41
+ this.started = Date.now();
42
+
43
+ // When did this postponement end?
44
+ this.ended = null;
45
+
46
+ // Has this postponement been released yet?
47
+ this.released = false;
48
+
49
+ // When was the last check made from the client?
50
+ this.last_check = this.started;
51
+
52
+ // Also attach this postponement to the conduit
53
+ conduit.postponement = this;
54
+
55
+ // Keep track of the total amount of postponements ever
56
+ _total_postponement_counter++;
57
+ });
58
+
59
+ /**
60
+ * The queued postponements
61
+ *
62
+ * @author Jelle De Loecker <jelle@elevenways.be>
63
+ * @since 1.3.1
64
+ * @version 1.3.1
65
+ *
66
+ * @return {Postponement[]}
67
+ */
68
+ Postponement.setStatic('queue', QUEUE);
69
+
70
+ /**
71
+ * Get the total amount of postponements ever
72
+ *
73
+ * @author Jelle De Loecker <jelle@elevenways.be>
74
+ * @since 1.3.1
75
+ * @version 1.3.1
76
+ *
77
+ * @return {Number}
78
+ */
79
+ Postponement.setStaticProperty(function total_postponement_counter() {
80
+ return _total_postponement_counter;
81
+ });
82
+
83
+ /**
84
+ * The current queue length
85
+ *
86
+ * @author Jelle De Loecker <jelle@elevenways.be>
87
+ * @since 1.3.1
88
+ * @version 1.3.1
89
+ *
90
+ * @return {Number}
91
+ */
92
+ Postponement.queue_length = 0;
93
+
94
+ /**
95
+ * How long has this been waiting?
96
+ *
97
+ * @author Jelle De Loecker <jelle@elevenways.be>
98
+ * @since 1.3.1
99
+ * @version 1.3.1
100
+ *
101
+ * @return {Number}
102
+ */
103
+ Postponement.setProperty(function time_waited() {
104
+
105
+ if (!this.ended) {
106
+ return Date.now() - this.started;
107
+ }
108
+
109
+ return this.ended - this.started;
110
+ });
111
+
112
+ /**
113
+ * How long has this postponement been left unchecked?
114
+ *
115
+ * @author Jelle De Loecker <jelle@elevenways.be>
116
+ * @since 1.3.1
117
+ * @version 1.3.1
118
+ *
119
+ * @return {Number}
120
+ */
121
+ Postponement.setProperty(function time_unchecked() {
122
+ return Date.now() - this.last_check;
123
+ });
124
+
125
+ /**
126
+ * Has this postponement been abandoned?
127
+ *
128
+ * @author Jelle De Loecker <jelle@elevenways.be>
129
+ * @since 1.3.1
130
+ * @version 1.3.1
131
+ *
132
+ * @return {Number}
133
+ */
134
+ Postponement.setProperty(function has_been_abandoned() {
135
+
136
+ if (this.time_unchecked > 30 * 1000) {
137
+ return true;
138
+ }
139
+
140
+ return false;
141
+ });
142
+
143
+ /**
144
+ * Get the current position in the queue
145
+ *
146
+ * @author Jelle De Loecker <jelle@elevenways.be>
147
+ * @since 1.3.1
148
+ * @version 1.3.1
149
+ *
150
+ * @return {Number}
151
+ */
152
+ Postponement.setProperty(function position_in_queue() {
153
+
154
+ if (this.last_queue_position == null) {
155
+ return null;
156
+ }
157
+
158
+ let index = QUEUE.indexOf(this);
159
+
160
+ if (index == -1) {
161
+ index = null;
162
+ }
163
+
164
+ this.last_queue_position = index;
165
+
166
+ return index;
167
+ });
168
+
169
+ /**
170
+ * Schedule a check of the queue
171
+ *
172
+ * @author Jelle De Loecker <jelle@elevenways.be>
173
+ * @since 1.3.1
174
+ * @version 1.3.1
175
+ *
176
+ * @param {Conduit} conduit The new conduit
177
+ */
178
+ Postponement.setStatic(function scheduleQueueCheck() {
179
+
180
+ if (queue_check_id) {
181
+ return;
182
+ }
183
+
184
+ queue_check_id = setTimeout(() => {
185
+ queue_check_id = null;
186
+ Postponement.checkQueue();
187
+ }, 5000);
188
+ });
189
+
190
+ /**
191
+ * Check the (top of the) queue
192
+ *
193
+ * @author Jelle De Loecker <jelle@elevenways.be>
194
+ * @since 1.3.1
195
+ * @version 1.3.1
196
+ *
197
+ * @param {Conduit} conduit The new conduit
198
+ */
199
+ Postponement.setStatic(function checkQueue() {
200
+
201
+ let length = QUEUE.length;
202
+
203
+ this.queue_length = length;
204
+
205
+ if (!length) {
206
+ return;
207
+ }
208
+
209
+ let to_remove = [],
210
+ postponement,
211
+ max = length,
212
+ i;
213
+
214
+ if (max > 20) {
215
+ max = 20;
216
+ }
217
+
218
+ for (i = 0; i < max; i++) {
219
+ postponement = QUEUE[i];
220
+
221
+ if (postponement.has_been_abandoned) {
222
+ to_remove.push(postponement);
223
+ }
224
+ }
225
+
226
+ for (i = 0; i < to_remove.length; i++) {
227
+ postponement = to_remove[i];
228
+ postponement.expire();
229
+ }
230
+
231
+ Postponement.scheduleQueueCheck();
232
+ });
233
+
234
+ /**
235
+ * Handle a request
236
+ *
237
+ * @author Jelle De Loecker <jelle@elevenways.be>
238
+ * @since 1.3.1
239
+ * @version 1.3.1
240
+ *
241
+ * @param {Conduit} conduit The new conduit
242
+ */
243
+ Postponement.setMethod(function handleRequest(conduit) {
244
+
245
+ if (conduit.ajax) {
246
+ let check_queue = conduit.param('check_queue');
247
+
248
+ if (check_queue) {
249
+
250
+ let data = {
251
+ position : this.position_in_queue,
252
+ allowed : false,
253
+ location : null,
254
+ };
255
+
256
+ if (this.attemptUnlock()) {
257
+ data.allowed = true;
258
+ data.location = this.url;
259
+ }
260
+
261
+ conduit.end(data);
262
+
263
+ return;
264
+ }
265
+ }
266
+
267
+ let resumed = this.attemptResume(conduit);
268
+
269
+ if (!resumed) {
270
+ this.showPostponementMessage(conduit);
271
+ }
272
+ });
273
+
274
+ /**
275
+ * Attempt to unlock
276
+ *
277
+ * @author Jelle De Loecker <jelle@elevenways.be>
278
+ * @since 1.3.1
279
+ * @version 1.3.1
280
+ *
281
+ * @return {Boolean} True if the request is being resumed
282
+ */
283
+ Postponement.setMethod(function attemptUnlock() {
284
+
285
+ if (this.released) {
286
+ return true;
287
+ }
288
+
289
+ Postponement.scheduleQueueCheck();
290
+
291
+ if (this.position_in_queue > 5) {
292
+ return false;
293
+ }
294
+
295
+ if (alchemy.lagInMs() > 100) {
296
+ return false;
297
+ }
298
+
299
+ this.released = true;
300
+
301
+ return this.released;
302
+ });
303
+
304
+ /**
305
+ * Attempt to resume this postponement.
306
+ * If it's in a queue and it's not our turn yet, do nothing.
307
+ *
308
+ * @author Jelle De Loecker <jelle@elevenways.be>
309
+ * @since 1.3.1
310
+ * @version 1.3.1
311
+ *
312
+ * @param {Conduit} conduit The new conduit
313
+ *
314
+ * @return {Boolean} True if the request is being resumed
315
+ */
316
+ Postponement.setMethod(function attemptResume(conduit) {
317
+
318
+ if (!this.attemptUnlock()) {
319
+ return false;
320
+ }
321
+
322
+ // Let the conduit know the response is being requested now
323
+ // (Certain postponements also delay the processing of the request)
324
+ this.original_conduit.emit('get-postponed-response');
325
+
326
+ // Once we're sure the postponed end has been reached,
327
+ // actually send that to the browser
328
+ this.original_conduit.afterOnce('after-postponed-end', () => {
329
+
330
+ this.original_conduit.response = conduit.response;
331
+ this.original_conduit._end(...this.original_conduit._end_arguments);
332
+
333
+ this.remove();
334
+ });
335
+
336
+ return true;
337
+ });
338
+
339
+ /**
340
+ * Show the postponement message to the given conduit
341
+ *
342
+ * @author Jelle De Loecker <jelle@elevenways.be>
343
+ * @since 1.3.1
344
+ * @version 1.3.1
345
+ *
346
+ * @param {Conduit} conduit
347
+ */
348
+ Postponement.setMethod(function showPostponementMessage(conduit) {
349
+
350
+ if (!conduit) {
351
+ conduit = this.original_conduit;
352
+ }
353
+
354
+ let response = conduit?.response || this.original_response;
355
+
356
+ if (!response) {
357
+ throw new Error('Failed to find a response instance, unable to show postponement message');
358
+ }
359
+
360
+ response.setHeader('X-Robots-Tag', 'none');
361
+
362
+ let position_in_queue = this.position_in_queue;
363
+
364
+ // Already set the cookies
365
+ if (conduit.new_cookie_header.length) {
366
+ response.setHeader('set-cookie', conduit.new_cookie_header);
367
+ }
368
+
369
+ // Set the location header where the client should look at later
370
+ response.setHeader('Location', this.url);
371
+ response.setHeader('Content-Type', 'text/html');
372
+
373
+ if (this.options.expected_duration) {
374
+ response.setHeader('Expected-Duration', Number(this.options.expected_duration / 1000).toFixed(2));
375
+ }
376
+
377
+ // Write the headers & status
378
+ response.writeHead(this.options.status || 202);
379
+
380
+ let end_message = this.options.end_message;
381
+
382
+ // End the response if wanted
383
+ if (end_message !== false) {
384
+ if (!end_message) {
385
+ if (position_in_queue != null) {
386
+ end_message = this.getQueueHTML();
387
+ } else {
388
+ end_message = 'The response has been postponed, you can find it at <a href="' + this.url + '">' + this.url + '</a>';
389
+ }
390
+ }
391
+ } else {
392
+ end_message = '';
393
+ }
394
+
395
+ response.end(end_message);
396
+ });
397
+
398
+ /**
399
+ * Remove this postponement
400
+ *
401
+ * @author Jelle De Loecker <jelle@elevenways.be>
402
+ * @since 1.3.1
403
+ * @version 1.3.1
404
+ *
405
+ * @param {Boolean} expired True if this is due to an expired session
406
+ */
407
+ Postponement.setMethod(function remove(expired) {
408
+
409
+ if (!expired) {
410
+ const session = this.session;
411
+
412
+ this.ended = Date.now();
413
+ session.postponements.remove(this.id);
414
+
415
+ session.addFinishedQueueDuration(this.time_waited);
416
+ }
417
+
418
+ let index = this.position_in_queue;
419
+
420
+ if (index != null) {
421
+ QUEUE.splice(index, 1);
422
+ }
423
+ });
424
+
425
+ /**
426
+ * Called when the session or the postponement expires
427
+ *
428
+ * @author Jelle De Loecker <jelle@elevenways.be>
429
+ * @since 1.3.1
430
+ * @version 1.3.1
431
+ */
432
+ Postponement.setMethod(function expire() {
433
+ this.remove(true);
434
+ });
435
+
436
+ /**
437
+ * Put this postponement in a queue
438
+ *
439
+ * @author Jelle De Loecker <jelle@elevenways.be>
440
+ * @since 1.3.1
441
+ * @version 1.3.1
442
+ */
443
+ Postponement.setMethod(function putInQueue() {
444
+
445
+ if (this.last_queue_position != null) {
446
+ return;
447
+ }
448
+
449
+ let new_length = QUEUE.push(this) - 1;
450
+
451
+ this.last_queue_position = new_length - 1;
452
+
453
+ // Update the queue length
454
+ Postponement.queue_length = new_length;
455
+
456
+ return this.last_queue_position;
457
+ });
458
+
459
+ /**
460
+ * Get the HTML message for in the queue
461
+ *
462
+ * @author Jelle De Loecker <jelle@elevenways.be>
463
+ * @since 1.3.1
464
+ * @version 1.3.1
465
+ */
466
+ Postponement.setMethod(function getQueueHTML() {
467
+
468
+ let position = this.last_queue_position + 1;
469
+
470
+ let html = `<!DOCTYPE html>
471
+ <html>
472
+ <head>
473
+ <title>Please Wait...</title>
474
+ <style>
475
+ body {
476
+ background-color: #F5F5F5;
477
+ display: flex;
478
+ justify-content: center;
479
+ align-items: center;
480
+ height: 100vh;
481
+ font-family: 'Open Sans', sans-serif;
482
+ }
483
+ .main-logo {
484
+ max-width: 50vw;
485
+ max-height: 50vw;
486
+ object-fit: contain;
487
+ min-width: 150px;
488
+ max-width: 150px;
489
+ }
490
+ .container {
491
+ text-align: center;
492
+ background: #F0F8FF;
493
+ padding: 20px;
494
+ border-radius: 10px;
495
+ box-shadow: 5px 5px 10px #B0C4DE;
496
+ }
497
+ h1 {
498
+ font-size: 3em;
499
+ margin-bottom: 20px;
500
+ }
501
+ p {
502
+ font-size: 1.5em;
503
+ margin-bottom: 20px;
504
+ }
505
+ #queue {
506
+ font-size: 1.2em;
507
+ margin-bottom: 20px;
508
+ }
509
+ .loading {
510
+ display: flex;
511
+ justify-content: center;
512
+ align-items: center;
513
+ margin: 0 auto;
514
+ border: 6px solid #F5F5F5;
515
+ border-top: 6px solid #3498DB;
516
+ border-radius: 50%;
517
+ width: 60px;
518
+ height: 60px;
519
+ animation: spin 2s linear infinite;
520
+ }
521
+ @keyframes spin {
522
+ 0% { transform: rotate(0deg); }
523
+ 100% { transform: rotate(360deg); }
524
+ }
525
+ </style>
526
+ </head>
527
+ <body>
528
+ <div class="container">
529
+ `
530
+
531
+ if (alchemy.settings.main_logo) {
532
+ html += `<img src="${alchemy.settings.main_logo}" class="main-logo">\n`;
533
+ }
534
+
535
+ html += `
536
+ <h1>Server is busy</h1>
537
+ <p id="queue">You are #${ position } in the queue.</p>
538
+ <div class="loading">
539
+ <div class="spinner"></div>
540
+ </div>
541
+ </div>
542
+ <script>
543
+ function checkQueue() {
544
+ let xhr = new XMLHttpRequest();
545
+ xhr.open('GET', '${ this.url }?check_queue=1', true);
546
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
547
+ xhr.onreadystatechange = function() {
548
+ if (xhr.readyState === 4) {
549
+ let data = JSON.parse(xhr.responseText);
550
+ let element = document.getElementById("queue");
551
+
552
+ if (data.allowed && data.location) {
553
+ element.innerHTML = "Redirecting!";
554
+ window.location.href = data.location;
555
+ return;
556
+ }
557
+
558
+ let queue = data.position;
559
+ element.innerHTML = "You are #" + queue + " in the queue";
560
+
561
+ setTimeout(checkQueue, 15000);
562
+ }
563
+ };
564
+ xhr.send();
565
+ };
566
+
567
+ setTimeout(checkQueue, 5000);
568
+ </script>
569
+ </body>
570
+ </html>`;
571
+
572
+ return html;
573
+ });
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Route Class
3
3
  *
4
- * @author Jelle De Loecker <jelle@develry.be>
4
+ * @author Jelle De Loecker <jelle@elevenways.be>
5
5
  * @since 0.2.0
6
- * @version 1.3.0
6
+ * @version 1.3.1
7
7
  */
8
8
  const Route = Function.inherits('Alchemy.Base', function Route(router, paths, options) {
9
9
 
@@ -55,6 +55,9 @@ const Route = Function.inherits('Alchemy.Base', function Route(router, paths, op
55
55
  this.controller = null;
56
56
  this.action = null;
57
57
 
58
+ // All routes can be postponed by default
59
+ this.can_be_postponed = true;
60
+
58
61
  this.setPaths(paths);
59
62
  });
60
63
 
@@ -182,6 +185,19 @@ Route.setMethod(function setPermission(permission) {
182
185
  }
183
186
  });
184
187
 
188
+ /**
189
+ * Set if this route can be postponed
190
+ *
191
+ * @author Jelle De Loecker <jelle@elevenways.be>
192
+ * @since 1.3.1
193
+ * @version 1.3.1
194
+ *
195
+ * @param {Boolean} postponable
196
+ */
197
+ Route.setMethod(function setCanBePostponed(postponable) {
198
+ this.can_be_postponed = postponable;
199
+ });
200
+
185
201
  /**
186
202
  * Compile paths for this route
187
203
  *
@@ -765,9 +765,9 @@ RouterClass.setMethod(function setOption(name, value) {
765
765
  /**
766
766
  * Add a route
767
767
  *
768
- * @author Jelle De Loecker <jelle@develry.be>
768
+ * @author Jelle De Loecker <jelle@elevenways.be>
769
769
  * @since 0.2.0
770
- * @version 1.3.0
770
+ * @version 1.3.1
771
771
  *
772
772
  * @param {Object} args
773
773
  * @param {String} args.name Optional route name
@@ -828,6 +828,10 @@ RouterClass.setMethod(function add(args) {
828
828
  route.setPermission(args.permission);
829
829
  }
830
830
 
831
+ if (args.can_be_postponed != null) {
832
+ route.setCanBePostponed(args.can_be_postponed);
833
+ }
834
+
831
835
  if (args.breadcrumb) {
832
836
  breadcrumb_options = {
833
837
  route : route
@@ -514,4 +514,40 @@ Schema.setMethod(function hasMany(alias, modelName, options) {
514
514
  */
515
515
  Schema.setMethod(function hasOneChild(alias, modelName, options) {
516
516
  this.addAssociation('HasOneChild', alias, modelName, options);
517
+ });
518
+
519
+ /**
520
+ * Clone
521
+ *
522
+ * @author Jelle De Loecker <jelle@elevenways.be>
523
+ * @since 1.3.1
524
+ * @version 1.3.1
525
+ *
526
+ * @return {Object}
527
+ */
528
+ Schema.setMethod(function dryClone(wm, custom_method) {
529
+
530
+ if (!custom_method) {
531
+ custom_method = 'toShallowClone';
532
+ }
533
+
534
+ let cloned = JSON.clone(this.toDry(), custom_method, wm);
535
+
536
+ cloned = this.constructor.unDry(cloned.value);
537
+
538
+ return cloned;
539
+ });
540
+
541
+ /**
542
+ * Clone using JSON-Dry
543
+ * (Needed anyway because Deck also has a clone method)
544
+ *
545
+ * @author Jelle De Loecker <jelle@elevenways.be>
546
+ * @since 1.3.1
547
+ * @version 1.3.1
548
+ *
549
+ * @return {Object}
550
+ */
551
+ Schema.setMethod(function clone() {
552
+ return this.dryClone(new WeakMap(), 'toShallowClone');
517
553
  });