alchemymvc 1.2.8 → 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.
Files changed (45) hide show
  1. package/lib/app/behaviour/sluggable_behaviour.js +4 -2
  2. package/lib/app/conduit/http_conduit.js +7 -2
  3. package/lib/app/conduit/loopback_conduit.js +2 -2
  4. package/lib/app/conduit/socket_conduit.js +20 -5
  5. package/lib/app/controller/alchemy_info_controller.js +4 -8
  6. package/lib/app/helper/backed_map.js +2 -2
  7. package/lib/app/helper/router_helper.js +98 -24
  8. package/lib/app/helper_controller/controller.js +45 -30
  9. package/lib/app/helper_datasource/00-nosql_datasource.js +44 -10
  10. package/lib/app/helper_field/enum_field.js +4 -4
  11. package/lib/app/helper_field/schema_field.js +50 -36
  12. package/lib/app/helper_model/document.js +81 -46
  13. package/lib/app/helper_model/field_set.js +11 -0
  14. package/lib/app/helper_model/model.js +107 -53
  15. package/lib/app/helper_validator/00_validator.js +38 -6
  16. package/lib/app/helper_validator/not_empty_validator.js +1 -3
  17. package/lib/app/routes.js +7 -1
  18. package/lib/bootstrap.js +1 -0
  19. package/lib/class/conduit.js +438 -290
  20. package/lib/class/controller.js +18 -15
  21. package/lib/class/datasource.js +19 -8
  22. package/lib/class/document.js +3 -3
  23. package/lib/class/field.js +34 -3
  24. package/lib/class/inode.js +27 -0
  25. package/lib/class/inode_file.js +204 -4
  26. package/lib/class/migration.js +2 -1
  27. package/lib/class/model.js +16 -5
  28. package/lib/class/path_definition.js +76 -120
  29. package/lib/class/path_param_definition.js +202 -0
  30. package/lib/class/postponement.js +573 -0
  31. package/lib/class/route.js +193 -33
  32. package/lib/class/router.js +22 -4
  33. package/lib/class/schema.js +47 -11
  34. package/lib/class/schema_client.js +65 -35
  35. package/lib/class/session.js +138 -12
  36. package/lib/class/sitemap.js +341 -0
  37. package/lib/core/base.js +13 -3
  38. package/lib/core/client_alchemy.js +78 -7
  39. package/lib/core/client_base.js +16 -10
  40. package/lib/core/middleware.js +56 -45
  41. package/lib/init/alchemy.js +124 -11
  42. package/lib/init/constants.js +11 -0
  43. package/lib/init/functions.js +163 -86
  44. package/lib/stages.js +18 -3
  45. package/package.json +6 -6
@@ -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
+ });