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
@@ -1,7 +1,8 @@
1
- var fileCache = alchemy.shared('files.fileCache'),
2
- libstream = alchemy.use('stream'),
1
+ const FILECACHE = alchemy.getCache('served_files'),
2
+ RX_TEXT = /svg|xml|javascript|text/i;
3
+
4
+ var libstream = alchemy.use('stream'),
3
5
  libpath = alchemy.use('path'),
4
- libmime = alchemy.use('mime'),
5
6
  libua = alchemy.use('useragent'),
6
7
  zlib = alchemy.use('zlib'),
7
8
  BODY = Symbol('body'),
@@ -345,7 +346,7 @@ Conduit.setMethod(function setRequestFiles(files) {
345
346
  *
346
347
  * @author Jelle De Loecker <jelle@elevenways.be>
347
348
  * @since 1.2.0
348
- * @version 1.2.0
349
+ * @version 1.3.0
349
350
  *
350
351
  * @param {Conduit} conduit
351
352
  * @param {Array} files
@@ -370,7 +371,7 @@ function _setRequestFiles(conduit, files, target) {
370
371
 
371
372
  _setRequestFiles(conduit, entry, context);
372
373
  } else {
373
- target[key] = Classes.Alchemy.Inode.File.from(entry);
374
+ target[key] = Classes.Alchemy.Inode.File.fromUntrusted(entry);
374
375
  }
375
376
  }
376
377
  }
@@ -980,7 +981,7 @@ Conduit.setMethod(function getRouteByName(name) {
980
981
  *
981
982
  * @author Jelle De Loecker <jelle@develry.be>
982
983
  * @since 0.2.0
983
- * @version 1.1.7
984
+ * @version 1.3.0
984
985
  *
985
986
  * @param {Route} after_route Only check routes after this one
986
987
  *
@@ -1009,7 +1010,7 @@ Conduit.setMethod(async function parseRoute(after_route) {
1009
1010
 
1010
1011
  if (temp) {
1011
1012
  this.route = temp.route;
1012
- this.params = temp.parameters;
1013
+ this.setRouteParameters(temp.parameters);
1013
1014
  this.route_string_parameters = temp.original_parameters;
1014
1015
  this.path_definition = temp.definition;
1015
1016
  } else {
@@ -1048,11 +1049,11 @@ Conduit.setMethod(async function parseRoute(after_route) {
1048
1049
  }
1049
1050
 
1050
1051
  if (temp) {
1051
- this.params = temp.parameters || {};
1052
+ this.setRouteParameters(temp.parameters);
1052
1053
  this.route_string_parameters = temp.original_parameters || {};
1053
1054
  this.path_definition = temp.definition;
1054
1055
  } else {
1055
- this.params = {};
1056
+ this.setRouteParameters();
1056
1057
  }
1057
1058
  }
1058
1059
  });
@@ -1230,19 +1231,67 @@ Conduit.setMethod(function callHandler() {
1230
1231
  this.route.callHandler(this);
1231
1232
  });
1232
1233
 
1234
+ /**
1235
+ * Put this request in a queue
1236
+ *
1237
+ * @author Jelle De Loecker <jelle@elevenways.be>
1238
+ * @since 1.3.1
1239
+ * @version 1.3.1
1240
+ *
1241
+ * @param {Object} options Options or url
1242
+ */
1243
+ Conduit.setMethod(function postponeAndQueue(options) {
1244
+
1245
+ if (!options) {
1246
+ options = {};
1247
+ }
1248
+
1249
+ const postponement = this.postponeRequest({
1250
+ put_in_queue : true,
1251
+ });
1252
+
1253
+ return postponement;
1254
+ });
1255
+
1256
+ /**
1257
+ * Postpone the response and the request
1258
+ *
1259
+ * This does not stop the current request from processing.
1260
+ *
1261
+ * @author Jelle De Loecker <jelle@elevenways.be>
1262
+ * @since 1.3.1
1263
+ * @version 1.3.1
1264
+ *
1265
+ * @param {Number|Object} options Options or time to wait
1266
+ *
1267
+ * @return {Alchemy.Conduit.Postponement}
1268
+ */
1269
+ Conduit.setMethod(function postponeRequest(options) {
1270
+
1271
+ let postponement = this.postponeResponse(options);
1272
+
1273
+ this.afterOnce('get-postponed-response', () => {
1274
+ this.callMiddleware();
1275
+ });
1276
+
1277
+ return postponement;
1278
+ });
1279
+
1233
1280
  /**
1234
1281
  * End the current request with a 202 status
1235
- * and tell the client to look at another url later
1282
+ * and tell the client to look at another url later.
1236
1283
  *
1237
- * @author Jelle De Loecker <jelle@develry.be>
1284
+ * This does not stop the current request from processing.
1285
+ *
1286
+ * @author Jelle De Loecker <jelle@elevenways.be>
1238
1287
  * @since 1.1.0
1239
- * @version 1.2.5
1288
+ * @version 1.3.1
1240
1289
  *
1241
- * @param {String|Object} options Options or url
1290
+ * @param {Number|Object} options Options or time to wait
1291
+ *
1292
+ * @return {Alchemy.Conduit.Postponement}
1242
1293
  */
1243
- Conduit.setMethod(function postpone(options) {
1244
-
1245
- let session = this.getSession();
1294
+ Conduit.setMethod(function postponeResponse(options) {
1246
1295
 
1247
1296
  if (typeof options == 'number') {
1248
1297
  options = {
@@ -1252,55 +1301,100 @@ Conduit.setMethod(function postpone(options) {
1252
1301
  options = {};
1253
1302
  }
1254
1303
 
1255
- // Make sure the scene id exists
1256
- this.createScene();
1304
+ return this._postpone(options);
1305
+ });
1257
1306
 
1258
- // Already set the cookies
1259
- if (this.new_cookie_header.length) {
1260
- this.response.setHeader('set-cookie', this.new_cookie_header);
1261
- }
1307
+ /**
1308
+ * Handle the postponement
1309
+ *
1310
+ * @author Jelle De Loecker <jelle@elevenways.be>
1311
+ * @since 1.3.1
1312
+ * @version 1.3.1
1313
+ *
1314
+ * @param {Object} options
1315
+ *
1316
+ * @return {Alchemy.Conduit.Postponement}
1317
+ */
1318
+ Conduit.setMethod(function _postpone(options) {
1262
1319
 
1263
- let postponed_id = session.postpone(this),
1264
- url = '/alchemy/postponed/' + postponed_id;
1320
+ let session = this.getSession();
1265
1321
 
1266
- // Set the location header where the client should look at later
1267
- this.response.setHeader('Location', url);
1268
- this.response.setHeader('Content-Type', 'text/html');
1322
+ let postponement = session.getExistingPostponement(this);
1269
1323
 
1270
- if (options.expected_duration) {
1271
- this.response.setHeader('Expected-Duration', Number(options.expected_duration / 1000).toFixed(2));
1324
+ if (postponement) {
1325
+ return postponement.showPostponementMessage(this);
1272
1326
  }
1273
1327
 
1274
- // Write the headers & 202 status
1275
- this.response.writeHead(202);
1328
+ let response = this.response;
1276
1329
 
1277
- // End the response
1278
- this.response.end('The response has been postponed, you can find it at <a href="' + url + '">' + url + '</a>');
1330
+ this.postponed_response = response;
1331
+
1332
+ // Make sure the scene id exists
1333
+ this.createScene();
1334
+
1335
+ postponement = session.postpone(this, options);
1336
+
1337
+ if (options.put_in_queue) {
1338
+ postponement.putInQueue();
1339
+ }
1340
+
1341
+ if (options.show_postponement_message !== false) {
1342
+ postponement.showPostponementMessage();
1343
+ }
1279
1344
 
1280
1345
  // Nullify the response
1281
1346
  this.response = null;
1282
1347
 
1283
1348
  // Set the original url
1284
- this.overrideResponseUrl(this.url)
1349
+ this.overrideResponseUrl(this.url);
1350
+
1351
+ // Return the postponement
1352
+ return postponement;
1285
1353
  });
1286
1354
 
1287
1355
  /**
1288
1356
  * Set the response url
1289
1357
  *
1358
+ * @deprecated Use {@link #setResponseUrl} instead
1359
+ *
1290
1360
  * @author Jelle De Loecker <jelle@develry.be>
1291
1361
  * @since 1.2.5
1292
- * @version 1.2.5
1362
+ * @version 1.3.0
1293
1363
  *
1294
1364
  * @param {String|RURL} url
1295
1365
  */
1296
1366
  Conduit.setMethod(function overrideResponseUrl(url) {
1367
+ return this.setResponseUrl(url);
1368
+ });
1297
1369
 
1298
- if (typeof url != 'string') {
1299
- url = String(url);
1370
+ /**
1371
+ * Set the response url
1372
+ *
1373
+ * @author Jelle De Loecker <jelle@elevenways.be>
1374
+ * @since 1.3.0
1375
+ * @version 1.3.0
1376
+ *
1377
+ * @param {String|RURL|Boolean} new_url
1378
+ */
1379
+ Conduit.setMethod(function setResponseUrl(new_url) {
1380
+
1381
+ if (new_url == null) {
1382
+ return;
1300
1383
  }
1301
1384
 
1302
- this.setHeader('x-history-url', url);
1303
- this.expose('redirected_to', url);
1385
+ if (!new_url) {
1386
+ this.renderer.history = false;
1387
+ return;
1388
+ } else {
1389
+ this.renderer.history = true;
1390
+ }
1391
+
1392
+ if (typeof new_url != 'string') {
1393
+ new_url = String(new_url);
1394
+ }
1395
+
1396
+ this.setHeader('x-history-url', new_url);
1397
+ this.expose('redirected_to', new_url);
1304
1398
  });
1305
1399
 
1306
1400
  /**
@@ -1409,15 +1503,17 @@ Conduit.setMethod(function redirect(status, options) {
1409
1503
  /**
1410
1504
  * Respond with an error
1411
1505
  *
1412
- * @author Jelle De Loecker <jelle@develry.be>
1506
+ * @author Jelle De Loecker <jelle@elevenways.be>
1413
1507
  * @since 0.2.0
1414
- * @version 1.1.0
1508
+ * @version 1.3.1
1415
1509
  *
1416
1510
  * @param {Nulber} status Response statuscode
1417
1511
  * @param {Error} message Optional error to send
1418
- * @param {Boolean} printError Print the error, defaults to true
1512
+ * @param {Boolean} print_error Print the error, defaults to true
1419
1513
  */
1420
- Conduit.setMethod(function error(status, message, printError) {
1514
+ Conduit.setMethod(function error(status, message, print_error) {
1515
+
1516
+ let print_dev = false;
1421
1517
 
1422
1518
  if (status instanceof Classes.Alchemy.Error.HTTP) {
1423
1519
  message = status;
@@ -1439,14 +1535,33 @@ Conduit.setMethod(function error(status, message, printError) {
1439
1535
  message = error;
1440
1536
  }
1441
1537
 
1442
- let subject = 'Error found on ' + this.original_path + '';
1538
+ let is_400 = (status >= 400 && status <= 500);
1443
1539
 
1444
- if (printError === false) {
1445
- log.error(subject + ':\n' + message);
1446
- } else if (message instanceof Error) {
1447
- alchemy.printLog('error', [subject, String(message), message], {err: message, level: -2});
1448
- } else {
1449
- log.error(subject + ':\n' + message);
1540
+ if (alchemy.settings.environment == 'dev') {
1541
+ print_dev = true;
1542
+ }
1543
+
1544
+ if (print_error == null) {
1545
+ if (is_400) {
1546
+ print_error = false;
1547
+ } else {
1548
+ print_error = true;
1549
+ }
1550
+ }
1551
+
1552
+ if (print_dev) {
1553
+ let subject = 'Error found on ' + this.original_path + '';
1554
+ log.error(subject + ':\n' + message, this);
1555
+ } else if (print_error) {
1556
+ let subject = 'Error found on ' + this.original_path + '';
1557
+
1558
+ if (is_400) {
1559
+ log.error(subject + ':\n' + message);
1560
+ } else if (message instanceof Error) {
1561
+ alchemy.printLog('error', [subject, String(message), message], {err: message, level: -2});
1562
+ } else {
1563
+ log.error(subject + ':\n' + message);
1564
+ }
1450
1565
  }
1451
1566
 
1452
1567
  // Make sure the client doesn't expect compression
@@ -1470,7 +1585,12 @@ Conduit.setMethod(function error(status, message, printError) {
1470
1585
  } else {
1471
1586
  this.set('status', status);
1472
1587
  this.set('message', message);
1473
- this.render(['error/' + status, 'error/unknown']);
1588
+
1589
+ if (alchemy.isTooBusyForRequests()) {
1590
+ this._end(`Error ${status}:\n${message}`);
1591
+ } else {
1592
+ this.render(['error/' + status, 'error/unknown']);
1593
+ }
1474
1594
  }
1475
1595
  } else {
1476
1596
  // Requests for images or scripts just get a non-expensive string response
@@ -1583,9 +1703,9 @@ Conduit.setMethod(function notModified() {
1583
1703
  /**
1584
1704
  * Respond with text. Objects get JSON-dry encoded
1585
1705
  *
1586
- * @author Jelle De Loecker <jelle@develry.be>
1706
+ * @author Jelle De Loecker <jelle@elevenways.be>
1587
1707
  * @since 0.2.0
1588
- * @version 1.1.0
1708
+ * @version 1.3.0
1589
1709
  *
1590
1710
  * @param {String|Object} message
1591
1711
  */
@@ -1659,7 +1779,7 @@ Conduit.setMethod(function end(message) {
1659
1779
 
1660
1780
  // Compress the output if the client accepts it,
1661
1781
  // but only if the file is at least 150 bytes
1662
- if (alchemy.settings.compression && message.length > 150 && this.accepts('gzip')) {
1782
+ if (alchemy.settings.compression !== false && message.length > 150 && this.accepts('gzip')) {
1663
1783
 
1664
1784
  // Set the decompressed content-length for use in progress bars
1665
1785
  this.setHeader('x-decompressed-content-length', Buffer.byteLength(message));
@@ -1683,9 +1803,9 @@ Conduit.setMethod(function end(message) {
1683
1803
  /**
1684
1804
  * Call the actual end method
1685
1805
  *
1686
- * @author Jelle De Loecker <jelle@develry.be>
1806
+ * @author Jelle De Loecker <jelle@elevenways.be>
1687
1807
  * @since 0.2.0
1688
- * @version 1.1.0
1808
+ * @version 1.3.1
1689
1809
  */
1690
1810
  Conduit.setMethod(function _end(message, encoding = 'utf-8') {
1691
1811
 
@@ -1701,6 +1821,8 @@ Conduit.setMethod(function _end(message, encoding = 'utf-8') {
1701
1821
 
1702
1822
  this._end_arguments = args;
1703
1823
 
1824
+ this.emit('after-postponed-end', args);
1825
+
1704
1826
  return;
1705
1827
  }
1706
1828
 
@@ -1865,295 +1987,237 @@ function bufferToStream(buffer) {
1865
1987
  }
1866
1988
 
1867
1989
  /**
1868
- * Send a file to the browser.
1869
- * Uses cache-control by default.
1990
+ * Send a file to the browser
1870
1991
  *
1871
- * @author Jelle De Loecker <jelle@develry.be>
1992
+ * @author Jelle De Loecker <jelle@elevenways.be>
1872
1993
  * @since 0.2.0
1873
- * @version 1.1.0
1994
+ * @version 1.3.0
1874
1995
  *
1875
1996
  * @param {String} path The path on the server to send to the browser
1876
1997
  * @param {Object} options Options, including headers
1877
1998
  */
1878
- Conduit.setMethod(function serveFile(path, options) {
1999
+ Conduit.setTypedMethod([Types.String, Types.Object.optional()], function serveFile(path, options = {}) {
1879
2000
 
1880
- var that = this,
1881
- tasks = [],
1882
- stats,
1883
- isStream;
1884
-
1885
- // Create an options object if it doesn't exist yet
1886
- if (options == null) {
1887
- options = {};
1888
- }
2001
+ let file = FILECACHE.get(path);
1889
2002
 
1890
- // Error handling function
1891
- if (!options.onError) {
1892
- options.onError = function onError(err) {
1893
- that.notFound(err);
1894
- };
2003
+ if (!file) {
2004
+ file = new Classes.Alchemy.Inode.File(path);
1895
2005
  }
1896
2006
 
1897
- // See if we have a stats object
1898
- if (Object.isObject(path)) {
1899
-
1900
- if (Buffer.isBuffer(path)) {
1901
- path = bufferToStream(path);
1902
- }
2007
+ return this.serveFile(file, options);
2008
+ });
1903
2009
 
1904
- if (path.readable) {
1905
- isStream = true;
1906
- stats = {
1907
- mimetype: 'application/octet-stream'
1908
- };
1909
- } else {
1910
- stats = path;
1911
- }
1912
- } else {
1913
- stats = fileCache[path];
2010
+ /**
2011
+ * Send a file to the browser
2012
+ *
2013
+ * @author Jelle De Loecker <jelle@elevenways.be>
2014
+ * @since 0.2.0
2015
+ * @version 1.3.0
2016
+ *
2017
+ * @param {Alchemy.Inode.File} file The file to serve
2018
+ * @param {Object} options Options, including headers
2019
+ */
2020
+ Conduit.setTypedMethod([Types.Alchemy.Inode.File, Types.Object.optional()], async function serveFile(file, options = {}) {
1914
2021
 
1915
- if (stats == null) {
1916
- stats = {
1917
- path: path
1918
- };
1919
- }
2022
+ if (file.path && !FILECACHE.has(file.path)) {
2023
+ FILECACHE.set(file.path, file);
2024
+ }
2025
+
2026
+ if (alchemy.settings.cache && (!options.cache_time && options.cache_time !== false)) {
2027
+ let stats = await file.getStats();
2028
+ options.cache_time = stats.mtime;
1920
2029
  }
1921
2030
 
1922
- // Don't check for file information when it's a stream
1923
- if (!isStream) {
1924
-
1925
- if (!stats.path) {
1926
- return options.onError(new Error('No file to serve'));
1927
- }
1928
-
1929
- // Make sure the stats object is in the cache
1930
- if (fileCache[stats.path] == null) {
1931
- fileCache[stats.path] = stats;
1932
- }
1933
-
1934
- // Get file stats if it isn't available yet
1935
- if (stats.mtime == null) {
1936
- tasks.push(function getFileStats(next) {
1937
-
1938
- fs.stat(stats.path, function gotStats(err, fileStats) {
1939
-
1940
- if (err) {
1941
- stats.err = err;
1942
- stats.mtime = new Date();
1943
- } else {
1944
- Object.assign(stats, fileStats);
1945
- }
1946
-
1947
- next();
1948
- });
1949
- });
1950
- }
1951
-
1952
- // Get the mimetype if it isn't available yet
1953
- if (!options.mimetype && stats.mimetype == null) {
1954
- tasks.push(function getMimetype(next) {
1955
-
1956
- // Don't use libmime if it isn't loaded,
1957
- // that could be the case on NW.js
1958
- if (!libmime) {
1959
- return next();
1960
- }
1961
-
1962
- // Lookup the mimetype by the extension alone
1963
- stats.mimetype = libmime.getType(stats.path);
1964
-
1965
- // Return the result if a valid mimetype was found
1966
- if (stats.mimetype !== 'application/octet-stream') {
1967
- return next();
1968
- }
2031
+ if (!options.mimetype) {
2032
+ options.mimetype = await file.getMimetype();
2033
+ }
1969
2034
 
1970
- // If no mimetype was found,
1971
- // see if we can find it using the original path (for resized images)
1972
- if (options.original_path) {
1973
- stats.mimetype = libmime.getType(options.original_path);
2035
+ if (!options.filename) {
2036
+ options.filename = file.name;
2037
+ }
2038
+
2039
+ return this.serveFile(file.createReadStream(), options);
2040
+ });
1974
2041
 
1975
- if (stats.mimetype !== 'application/octet-stream') {
1976
- return next();
1977
- }
1978
- }
2042
+ /**
2043
+ * Send a buffer to the client
2044
+ *
2045
+ * @author Jelle De Loecker <jelle@elevenways.be>
2046
+ * @since 1.3.0
2047
+ * @version 1.3.0
2048
+ *
2049
+ * @param {Stream} buffer The buffer to send
2050
+ * @param {Object} options Options, including headers
2051
+ */
2052
+ Conduit.setTypedMethod([Buffer, Types.Object.optional()], function serveFile(buffer, options = {}) {
2053
+ return this.serveFile(bufferToStream(buffer), options);
2054
+ });
1979
2055
 
1980
- // "magic" currently doesn't work in nw.js
1981
- if (Blast.isNW) {
1982
- return next();
1983
- }
2056
+ /**
2057
+ * Send a stream to the client
2058
+ *
2059
+ * @author Jelle De Loecker <jelle@elevenways.be>
2060
+ * @since 1.3.0
2061
+ * @version 1.3.0
2062
+ *
2063
+ * @param {Stream} stream The stream to send
2064
+ * @param {Object} options Options, including headers
2065
+ */
2066
+ Conduit.setTypedMethod([Types.Stream, Types.Object.optional()], function serveFile(stream, options = {}) {
1984
2067
 
1985
- // Don't try to use magic if it's not loaded
1986
- if (!getMagic()) {
1987
- return next();
1988
- }
2068
+ let is_text = false;
1989
2069
 
1990
- // Look inside the data (using "magic") for a better mimetype
1991
- magic.detectFile(stats.path, function detectedMimetype(err, result) {
2070
+ if (options.mimetype && RX_TEXT.test(options.mimetype)) {
2071
+ options.mimetype += '; charset=utf-8';
2072
+ is_text = true;
2073
+ }
1992
2074
 
1993
- if (!err) {
1994
- stats.mimetype = result;
1995
- }
2075
+ if (alchemy.settings.cache === false) {
2076
+ options.cache_time = null;
2077
+ }
1996
2078
 
1997
- next();
1998
- });
1999
- });
2000
- }
2079
+ if (options.compress == null) {
2080
+ options.compress = is_text;
2001
2081
  }
2002
2082
 
2003
- Function.parallel(tasks, function gotFileInfo(err) {
2083
+ if (options.compress && (alchemy.settings.compression === false || !this.accepts('gzip'))) {
2084
+ options.compress = false;
2085
+ }
2004
2086
 
2005
- var disposition,
2006
- outStream,
2007
- mimetype,
2008
- headers,
2009
- isText,
2010
- since,
2011
- key;
2087
+ if (options.cache_time && !alchemy.settings.cache) {
2088
+ options.cache_time = false;
2089
+ }
2012
2090
 
2013
- if (err) {
2014
- return that.error(err);
2015
- }
2091
+ if (!options.onError) {
2092
+ options.onError = err => this.notFound(err);
2093
+ }
2016
2094
 
2017
- if (stats.err) {
2018
- return options.onError(stats.err);
2019
- }
2095
+ return this._sendStream(stream, options);
2096
+ });
2020
2097
 
2021
- if (!isStream && !stats.path) {
2022
- return options.onError(new Error('File not found'));
2023
- }
2098
+ /**
2099
+ * Send a stream to the client
2100
+ *
2101
+ * @author Jelle De Loecker <jelle@elevenways.be>
2102
+ * @since 1.3.0
2103
+ * @version 1.3.0
2104
+ *
2105
+ * @param {Stream} stream The stream to send
2106
+ * @param {Object} options Options, including headers
2107
+ */
2108
+ Conduit.setMethod(function _sendStream(stream, options) {
2024
2109
 
2025
- // Check the if-modified-since header if it's supplied
2026
- if (alchemy.settings.cache !== false && that.headers['if-modified-since'] != null) {
2110
+ if (options.cache_time) {
2111
+ let modified_since = this.headers['if-modified-since'];
2027
2112
 
2028
- // Turn the string into a date
2029
- since = new Date(that.headers['if-modified-since']);
2113
+ if (modified_since != null) {
2114
+ let since = new Date(modified_since);
2030
2115
 
2031
2116
  // If the file's modifytime is smaller or equal to the since time,
2032
2117
  // don't serve the contents!
2033
- if (stats.mtime <= since) {
2034
- return that.notModified();
2035
- }
2036
- }
2037
-
2038
- mimetype = stats.mimetype;
2039
-
2040
- // If we get a general mimetype, and an alternative is provided, use that one
2041
- if (!mimetype || mimetype === 'application/octet-stream') {
2042
- if (options.mimetype != null) {
2043
- mimetype = options.mimetype;
2118
+ if (options.cache_time <= since) {
2119
+ return this.notModified();
2044
2120
  }
2045
2121
  }
2046
2122
 
2047
- isText = /svg|xml|javascript|text/.test(mimetype);
2048
-
2049
- // Serve text files as utf-8
2050
- if (isText) {
2051
- mimetype += '; charset=utf-8';
2052
- }
2053
-
2054
- that.setHeader('content-type', mimetype);
2055
-
2056
- // Setting the disposition makes the browser download the file
2057
- // This is on by default, but can be disabled
2058
- if (options.disposition == 'inline') {
2059
- disposition = 'inline';
2060
-
2061
- if (options.filename) {
2062
- disposition += '; filename=' + JSON.stringify(options.filename)
2063
- }
2064
-
2065
- that.setHeader('content-disposition', disposition);
2066
- } else if (options.disposition !== false) {
2067
- if (options.filename) {
2068
- disposition = 'attachment; filename=' + JSON.stringify(options.filename);
2069
- } else {
2070
- disposition = 'attachment';
2071
- }
2123
+ // Allow the browser to cache this for 60 minutes,
2124
+ // after which it has to revalidate the content
2125
+ // by seeing if it has been modified
2126
+ this.setHeader('cache-control', 'public, max-age=3600, must-revalidate');
2127
+ this.setHeader('last-modified', options.cache_time.toGMTString());
2128
+ } else {
2129
+ this.setHeader('cache-control', 'no-cache');
2130
+ }
2072
2131
 
2073
- that.setHeader('content-disposition', disposition);
2074
- }
2132
+ let disposition,
2133
+ key;
2075
2134
 
2076
- if (stats.mtime && alchemy.settings.cache) {
2077
- // Allow the browser to cache this for 60 minutes,
2078
- // after which it has to revalidate the content
2079
- // by seeing if it has been modified
2080
- that.setHeader('cache-control', 'public, max-age=3600, must-revalidate');
2081
- that.setHeader('last-modified', stats.mtime.toGMTString());
2082
- } else if (!alchemy.settings.cache) {
2083
- that.setHeader('cache-control', 'no-cache');
2084
- }
2135
+ if (options.mimetype) {
2136
+ this.setHeader('content-type', options.mimetype);
2137
+ }
2085
2138
 
2086
- for (key in options.headers) {
2087
- that.setHeader(key, options.headers[key]);
2088
- }
2139
+ // Setting the disposition makes the browser download the file
2140
+ // This is on by default, but can be disabled
2141
+ if (options.disposition == 'inline') {
2142
+ disposition = 'inline';
2089
2143
 
2090
- // End now if it's just a HEAD request
2091
- if (that.method == 'head') {
2092
- return that.end();
2144
+ if (options.filename) {
2145
+ disposition += '; filename=' + JSON.stringify(options.filename)
2093
2146
  }
2094
2147
 
2095
- if (isStream) {
2096
- outStream = path;
2148
+ this.setHeader('content-disposition', disposition);
2149
+ } else if (options.disposition !== false) {
2150
+ if (options.filename) {
2151
+ disposition = 'attachment; filename=' + JSON.stringify(options.filename);
2097
2152
  } else {
2098
- outStream = fs.createReadStream(path, {bufferSize: 64*1024});
2099
-
2100
- // Listen for file errors
2101
- outStream.on('error', options.onError);
2153
+ disposition = 'attachment';
2102
2154
  }
2103
2155
 
2104
- // Compress text responses
2105
- if (isText && alchemy.settings.compression && that.accepts('gzip')) {
2156
+ this.setHeader('content-disposition', disposition);
2157
+ }
2158
+
2159
+ // Set all the headers
2160
+ for (key in options.headers) {
2161
+ this.setHeader(key, options.headers[key]);
2162
+ }
2106
2163
 
2107
- // Set the gzip header
2108
- that.setHeader('content-encoding', 'gzip');
2109
- that.setHeader('vary', 'accept-encoding');
2164
+ // Don't send anything if it's a HEAD request
2165
+ if (this.method == 'head') {
2166
+ return this.end();
2167
+ }
2110
2168
 
2111
- // Create the gzip stream
2112
- outStream = outStream.pipe(zlib.createGzip());
2113
- }
2169
+ const response = this.response;
2170
+ let out_stream = stream;
2114
2171
 
2115
- // If we received a stream as parameter...
2116
- if (isStream) {
2117
- that.response.on('end', cleanup);
2118
- that.response.on('finish', cleanup);
2119
- that.response.on('error', cleanup);
2120
- that.response.on('close', cleanup);
2121
- }
2172
+ if (options.compress) {
2173
+ // Set the gzip header
2174
+ this.setHeader('content-encoding', 'gzip');
2175
+ this.setHeader('vary', 'accept-encoding');
2122
2176
 
2123
- function cleanup() {
2177
+ // Create the gzip stream
2178
+ out_stream = out_stream.pipe(zlib.createGzip());
2179
+ }
2124
2180
 
2181
+ if (options.cleanup_stream) {
2182
+ const cleanup = function cleanupOriginalStream() {
2125
2183
  // Remove all pipes
2126
- outStream.unpipe();
2184
+ stream.unpipe();
2127
2185
 
2128
- if (outStream.destroy) {
2129
- outStream.destroy();
2130
- } else if (outStream.end) {
2131
- outStream.end();
2186
+ if (stream.destroy) {
2187
+ stream.destroy();
2188
+ } else if (stream.end) {
2189
+ stream.end();
2132
2190
  }
2133
- }
2191
+ };
2134
2192
 
2135
- // Send the headers
2136
- for (key in that.response_headers) {
2137
- that.response.setHeader(key, that.response_headers[key]);
2138
- }
2193
+ response.on('end', cleanup);
2194
+ response.on('finish', cleanup);
2195
+ response.on('error', cleanup);
2196
+ response.on('close', cleanup);
2197
+ }
2139
2198
 
2140
- if (that.new_cookie_header.length) {
2141
- that.response.setHeader('set-cookie', that.new_cookie_header);
2142
- }
2199
+ // Set the response headers
2200
+ for (key in this.response_headers) {
2201
+ response.setHeader(key, this.response_headers[key]);
2202
+ }
2203
+
2204
+ if (this.new_cookie_header.length) {
2205
+ response.setHeader('set-cookie', this.new_cookie_header);
2206
+ }
2143
2207
 
2144
- that.response.statusCode = 200;
2208
+ // If we got this far, the file has been found!
2209
+ response.statusCode = 200;
2145
2210
 
2146
- // Stream the file to the client
2147
- outStream.pipe(that.response);
2148
- });
2211
+ // Actually stream the contents to the client
2212
+ out_stream.pipe(response);
2149
2213
  });
2150
2214
 
2151
2215
  /**
2152
2216
  * Create a session
2153
2217
  *
2154
- * @author Jelle De Loecker <jelle@develry.be>
2218
+ * @author Jelle De Loecker <jelle@elevenways.be>
2155
2219
  * @since 0.2.0
2156
- * @version 1.1.0
2220
+ * @version 1.3.1
2157
2221
  *
2158
2222
  * @param {Boolean} create Create a session if none exist
2159
2223
  *
@@ -2161,16 +2225,16 @@ Conduit.setMethod(function serveFile(path, options) {
2161
2225
  */
2162
2226
  Conduit.setMethod(function getSession(allow_create = true) {
2163
2227
 
2164
- var cookie_name,
2165
- fingerprint,
2166
- session_id,
2167
- session;
2168
-
2169
2228
  // Only do this once per request
2170
2229
  if (this.sessionData != null) {
2171
2230
  return this.sessionData;
2172
2231
  }
2173
2232
 
2233
+ let cookie_name,
2234
+ fingerprint,
2235
+ session_id,
2236
+ session;
2237
+
2174
2238
  // Set the name of the cookie (could change in the future)
2175
2239
  cookie_name = alchemy.settings.session_key || 'alchemy_sid';
2176
2240
 
@@ -2307,6 +2371,24 @@ Conduit.setMethod(function routeParam(name) {
2307
2371
  return this.params[name];
2308
2372
  });
2309
2373
 
2374
+ /**
2375
+ * Set route parameters
2376
+ *
2377
+ * @author Jelle De Loecker <jelle@elevenways.be>
2378
+ * @since 1.3.0
2379
+ * @version 1.3.0
2380
+ */
2381
+ Conduit.setMethod(function setRouteParameters(data) {
2382
+
2383
+ if (!this.params) {
2384
+ this.params = {};
2385
+ }
2386
+
2387
+ if (data) {
2388
+ Object.assign(this.params, data);
2389
+ }
2390
+ });
2391
+
2310
2392
  /**
2311
2393
  * Get/set a cookie
2312
2394
  *
@@ -2370,7 +2452,7 @@ Conduit.setMethod(function cookie(name, value, options) {
2370
2452
  *
2371
2453
  * @author Jelle De Loecker <jelle@develry.be>
2372
2454
  * @since 0.2.0
2373
- * @version 1.1.0
2455
+ * @version 1.3.0
2374
2456
  *
2375
2457
  * @param {String} name
2376
2458
  * @param {Mixed} value
@@ -2382,7 +2464,7 @@ Conduit.setMethod(function setHeader(name, value) {
2382
2464
  }
2383
2465
 
2384
2466
  if (this.websocket) {
2385
- throw new Error("Can't set a header on a websocket connection");
2467
+ throw new Error("Can't set header `" + name + "` on a websocket connection");
2386
2468
  }
2387
2469
 
2388
2470
  this.response_headers[name] = value;
@@ -2511,6 +2593,72 @@ Conduit.setMethod(function supports(feature) {
2511
2593
  return null;
2512
2594
  });
2513
2595
 
2596
+ /**
2597
+ * Should this request be delayed because the server is too busy?
2598
+ * This performs an early check, the controller might also check this later.
2599
+ * This is mainly so we can prevent Server-Side-Renders from happening
2600
+ * when the system is already overloaded.
2601
+ *
2602
+ * @author Jelle De Loecker <jelle@elevenways.be>
2603
+ * @since 1.3.1
2604
+ * @version 1.3.1
2605
+ *
2606
+ * @return {Boolean}
2607
+ */
2608
+ Conduit.setMethod(function shouldBePostponed() {
2609
+
2610
+ if (!alchemy.settings.postpone_requests_on_overload) {
2611
+ return false;
2612
+ }
2613
+
2614
+ const route = this.route;
2615
+
2616
+ // If no route is found, it can be allowed.
2617
+ // Most of the time this means it's a middleware-request
2618
+ // (like stylesheets, images, scripts, ...)
2619
+ // But also 404s (This can be delayed later by the controller)
2620
+ if (route == null) {
2621
+ return false;
2622
+ }
2623
+
2624
+ // Some routes can't even be postponed
2625
+ if (!route.can_be_postponed) {
2626
+ return false;
2627
+ }
2628
+
2629
+ // If alchemy isn't even too busy, it's obviously allowed
2630
+ if (!alchemy.isTooBusyForRequests()) {
2631
+ return false;
2632
+ }
2633
+
2634
+ // Some users have the permission to skip postponements
2635
+ if (this.hasPermission('alchemy.queue.skip')) {
2636
+ return false;
2637
+ }
2638
+
2639
+ let session = this.getSession(false);
2640
+
2641
+ if (session) {
2642
+
2643
+ // Do not postpone clients that have already been in the queue
2644
+ if (session.hasAlreadyQueued()) {
2645
+ return false;
2646
+ }
2647
+
2648
+ // Do not postpone clients that have already rendered something
2649
+ if (session.render_counter > 0 && session.is_active) {
2650
+ return false;
2651
+ }
2652
+ }
2653
+
2654
+ // AJAX requests are allowed a bit more
2655
+ if (this.ajax) {
2656
+ return alchemy.isTooBusyForAjax();
2657
+ }
2658
+
2659
+ return true;
2660
+ });
2661
+
2514
2662
  /**
2515
2663
  * Broadcast data to every connected user
2516
2664
  *