noxt-server 0.1.13 → 0.1.15
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.
- package/noxt-server.js +36 -11
- package/package.json +9 -7
- package/units/bundler.js +46 -0
- package/units/compression.js +33 -0
- package/units/config.js +12 -7
- package/units/env.js +3 -3
- package/units/express.js +86 -48
- package/units/fetch-cache-fs.js +172 -45
- package/units/fetch-cache.js +1 -1
- package/units/fetch-node.js +0 -13
- package/units/fetch.js +31 -15
- package/units/hooks.js +21 -8
- package/units/image-resizer.js +278 -0
- package/units/logger.js +59 -8
- package/units/noxt-dev.js +4 -2
- package/units/noxt-plugin.js +86 -96
- package/units/noxt-router-dev.js +15 -5
- package/units/noxt-router.js +2 -3
- package/units/noxt.js +3 -1
- package/units/plugin.js +1 -1
- package/units/reload.js +101 -12
- package/units/static-dev.js +42 -0
- package/units/static.js +14 -10
- package/units/utils.js +31 -9
- package/units.txt +815 -0
package/noxt-server.js
CHANGED
|
@@ -5,31 +5,56 @@ import MLM from 'mlm-core';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = dirname(__filename);
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function resolveModule(name) {
|
|
9
|
+
if (!name.includes('/')) {
|
|
10
|
+
return `${__dirname}/units/${name}.js`;
|
|
11
|
+
}
|
|
12
|
+
if (name.startsWith('app/')) {
|
|
13
|
+
return `${process.cwd()}/units/${name.slice(4)}.js`;
|
|
14
|
+
}
|
|
15
|
+
if (name.startsWith('contrib/')) {
|
|
16
|
+
const m = name.match(/^contrib\/([^/]+)(?:\/(.*))?$/);
|
|
17
|
+
if (!m) throw new Error(`Invalid contrib module name: ${name}`);
|
|
18
|
+
if (!m[2]) return `noxt-contrib-${m[1]}`;
|
|
19
|
+
return `noxt-contrib-${m[1]}/${m[2]}`;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Invalid module name: ${name}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function startServer({ config, recipe, import: _import}) {
|
|
9
25
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
resolveModule
|
|
14
|
-
name => name.startsWith('app/')
|
|
15
|
-
? `${process.cwd()}/units/${name.slice(4)}.js`
|
|
16
|
-
: `${__dirname}/units/${name}.js`
|
|
26
|
+
const beforeBoot = process.hrtime.bigint();
|
|
27
|
+
const mlmInstance = await MLM({
|
|
28
|
+
import: _import ?? (p => import(p)),
|
|
29
|
+
resolveModule
|
|
17
30
|
});
|
|
18
31
|
try {
|
|
19
|
-
|
|
32
|
+
const afterBoot = process.hrtime.bigint();
|
|
20
33
|
const report = await mlmInstance.analyze(recipe ?? 'noxt-dev');
|
|
21
34
|
if (!report.success) {
|
|
22
35
|
console.log('Bad recipe: ' + recipe + '\n' + report.order.join(', '));
|
|
23
36
|
console.log(report.errors.join('\n'));
|
|
24
37
|
process.exit(1);
|
|
25
38
|
}
|
|
26
|
-
|
|
39
|
+
const afterAnalyze = process.hrtime.bigint();
|
|
27
40
|
await mlmInstance.install(recipe ?? 'noxt-dev');
|
|
41
|
+
const afterInstall = process.hrtime.bigint();
|
|
28
42
|
const mlm = mlmInstance.context;
|
|
29
43
|
await mlm.services.config.merge(config);
|
|
30
|
-
|
|
44
|
+
const afterConfig = process.hrtime.bigint();
|
|
31
45
|
await mlmInstance.start();
|
|
46
|
+
const afterStart = process.hrtime.bigint();
|
|
47
|
+
|
|
48
|
+
console.log(
|
|
49
|
+
'[noxt-server] Total time', Number((afterStart - beforeBoot) / 1_000_000n), 'ms',
|
|
50
|
+
'| Boot', Number((afterBoot - beforeBoot) / 1_000_000n), 'ms',
|
|
51
|
+
'| Load', Number((afterAnalyze - afterBoot) / 1_000_000n), 'ms',
|
|
52
|
+
'| Install', Number((afterInstall - afterAnalyze) / 1_000_000n), 'ms',
|
|
53
|
+
'| Config', Number((afterConfig - afterInstall) / 1_000_000n), 'ms',
|
|
54
|
+
'| Start', Number((afterStart - afterConfig) / 1_000_000n), 'ms',
|
|
55
|
+
);
|
|
32
56
|
|
|
57
|
+
mlmInstance.repl();
|
|
33
58
|
} catch (e) {
|
|
34
59
|
console.log(await mlmInstance.analyze(recipe ?? 'noxt-dev'));
|
|
35
60
|
console.error(e);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "noxt-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Server for noxt-js-middleware with CLI and config support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "noxt-server.js",
|
|
@@ -23,13 +23,15 @@
|
|
|
23
23
|
"author": "Zoran Obradović [https://github.com/zocky]",
|
|
24
24
|
"license": "GPL-3.0-or-later",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"noxt-js-middleware": "^1.0.
|
|
27
|
-
"mlm-core": "^1.0.
|
|
28
|
-
"express": "^5.
|
|
26
|
+
"noxt-js-middleware": "^1.0.7",
|
|
27
|
+
"mlm-core": "^1.0.7",
|
|
28
|
+
"express": "^5.1.0",
|
|
29
29
|
"cookie-parser": "^1.4.6",
|
|
30
|
-
"js-yaml": "^4.1.0"
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
"js-yaml": "^4.1.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"compression": "^1.8.1",
|
|
34
|
+
"sharp": "^0.34.5"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
37
|
"noxt-js-middleware": "../noxt-js-middleware",
|
package/units/bundler.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'bundler',
|
|
3
|
+
version: '1.0.0',
|
|
4
|
+
description: 'Bundle js and css from component exports',
|
|
5
|
+
requires: ['noxt-plugin'],
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default mlm => {
|
|
9
|
+
const styles = {};
|
|
10
|
+
const scripts = {};
|
|
11
|
+
return {
|
|
12
|
+
'config.bundler': {
|
|
13
|
+
is: {
|
|
14
|
+
style: 'string|false',
|
|
15
|
+
script: 'string|false',
|
|
16
|
+
},
|
|
17
|
+
default: {
|
|
18
|
+
style: '_style',
|
|
19
|
+
script: '_script',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
'services.bundler': () => ({ styles, scripts }),
|
|
23
|
+
'registerComponent.bundler': async ({ name, component, module }) => {
|
|
24
|
+
styles[component.name] = module.style ? `/* ${name} */ \n ${module.style}\n` : '';
|
|
25
|
+
scripts[component.name] = module.script ? `/* ${name} */ \n ${module.script}\n` : '';
|
|
26
|
+
},
|
|
27
|
+
'middleware.bundler': () => {
|
|
28
|
+
const router = mlm.services.express.Router();
|
|
29
|
+
if (mlm.config.bundler.style) {
|
|
30
|
+
router.get(`/${mlm.config.bundler.style}`, (req, res) => {
|
|
31
|
+
res.logGroup = 'bundler';
|
|
32
|
+
res.set('Content-Type', 'text/css');
|
|
33
|
+
res.send( Object.values(styles).join('') );
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (mlm.config.bundler.script) {
|
|
37
|
+
router.get(`/${mlm.config.bundler.script}`, (req, res) => {
|
|
38
|
+
res.logGroup = 'bundler';
|
|
39
|
+
res.set('Content-Type', 'text/javascript');
|
|
40
|
+
res.send( Object.values(scripts).join('') );
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return router
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// units/compression.js
|
|
2
|
+
export const info = {
|
|
3
|
+
name: 'compression',
|
|
4
|
+
version: '1.0.0',
|
|
5
|
+
description: 'Response compression',
|
|
6
|
+
requires: ['express'],
|
|
7
|
+
npm: {
|
|
8
|
+
'compression': '^1.8.0'
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default mlm => ({
|
|
14
|
+
'config.compression': {
|
|
15
|
+
is: {
|
|
16
|
+
enabled: 'boolean',
|
|
17
|
+
level: 'number', // 0-9, default 6
|
|
18
|
+
threshold: 'number', // bytes, default 1024
|
|
19
|
+
},
|
|
20
|
+
default: {
|
|
21
|
+
enabled: true,
|
|
22
|
+
level: 6,
|
|
23
|
+
threshold: 1024, // only compress responses > 1kb
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
'middleware.compression': async () => {
|
|
28
|
+
const { enabled, level, threshold } = mlm.config.compression;
|
|
29
|
+
if (!enabled) return null;
|
|
30
|
+
const compression = (await mlm.import('compression')).default;
|
|
31
|
+
return compression({ level, threshold });
|
|
32
|
+
},
|
|
33
|
+
});
|
package/units/config.js
CHANGED
|
@@ -5,6 +5,7 @@ export const info = {
|
|
|
5
5
|
requires: ['utils','services'],
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
const config = {};
|
|
8
9
|
const config_is = {};
|
|
9
10
|
const config_defaults = {};
|
|
10
11
|
const config_defs = {}
|
|
@@ -54,7 +55,11 @@ function isPlainObject(value) {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export default mlm => ({
|
|
57
|
-
'define.config':
|
|
58
|
+
'define.config': {
|
|
59
|
+
get: () => config,
|
|
60
|
+
enumerable: true,
|
|
61
|
+
configurable: false
|
|
62
|
+
},
|
|
58
63
|
'register.config': (defs, unit) => {
|
|
59
64
|
for (const key in defs) {
|
|
60
65
|
const def = defs[key];
|
|
@@ -78,8 +83,8 @@ export default mlm => ({
|
|
|
78
83
|
},
|
|
79
84
|
'utils.merge_deep': merge_deep,
|
|
80
85
|
'services.config': () => new class ConfigService {
|
|
81
|
-
process(
|
|
82
|
-
const merged = merge_deep({}, config_defaults,
|
|
86
|
+
process(userConfig) {
|
|
87
|
+
const merged = merge_deep({}, config_defaults, userConfig);
|
|
83
88
|
const ret = {};
|
|
84
89
|
for (const key in config_defs) {
|
|
85
90
|
const { is: type, normalize } = config_defs[key];
|
|
@@ -94,10 +99,10 @@ export default mlm => ({
|
|
|
94
99
|
}
|
|
95
100
|
return ret;
|
|
96
101
|
}
|
|
97
|
-
merge(
|
|
98
|
-
|
|
99
|
-
Object.assign(
|
|
100
|
-
|
|
102
|
+
merge(userConfig) {
|
|
103
|
+
// console.log('initial',config_defaults,userConfig)
|
|
104
|
+
Object.assign(config, this.process(userConfig));
|
|
105
|
+
// console.log(userConfig,'final',config)
|
|
101
106
|
}
|
|
102
107
|
get_defs() {
|
|
103
108
|
return {
|
package/units/env.js
CHANGED
|
@@ -3,12 +3,12 @@ export const info = {
|
|
|
3
3
|
version: '1.0.0',
|
|
4
4
|
description: 'Environment',
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
export default mlm => ({
|
|
8
8
|
'define.DEV': () => process.env.NODE_ENV !== 'production',
|
|
9
9
|
'define.PROD': () => process.env.NODE_ENV === 'production',
|
|
10
10
|
'onBeforeLoad': () => {
|
|
11
|
-
|
|
11
|
+
process.env.NODE_ENV ||= 'development'
|
|
12
12
|
}
|
|
13
13
|
})
|
|
14
|
-
|
|
14
|
+
|
package/units/express.js
CHANGED
|
@@ -8,54 +8,92 @@ export const info = {
|
|
|
8
8
|
},
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
let c = conf[key];
|
|
21
|
-
if (mlm.is.function(c)) {
|
|
22
|
-
c = { create: c };
|
|
11
|
+
export default async mlm => {
|
|
12
|
+
let app;
|
|
13
|
+
const middlewareNames = new Set();
|
|
14
|
+
const middlewares = [];
|
|
15
|
+
const { default: express } = await mlm.import('express');
|
|
16
|
+
return {
|
|
17
|
+
'services.express': () => new class {
|
|
18
|
+
get app () {
|
|
19
|
+
return app;
|
|
23
20
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
create: 'function'
|
|
27
|
-
}, c, 'middleware');
|
|
28
|
-
mlm.assert.is.function(c.create, 'middleware');
|
|
29
|
-
middlewares.push({ ...c, unit: unit.name });
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
'config.port': {
|
|
33
|
-
is: v => Number.isInteger(v) && v > 0 && v < 65536,
|
|
34
|
-
default: 3000
|
|
35
|
-
},
|
|
36
|
-
'config.host': {
|
|
37
|
-
is: 'string',
|
|
38
|
-
default: 'localhost'
|
|
39
|
-
},
|
|
40
|
-
'middleware.json': async (app) => {
|
|
41
|
-
const { default: express } = await mlm.import('express');
|
|
42
|
-
return express.json();
|
|
43
|
-
},
|
|
44
|
-
'middleware.urlencoded': async (app) => {
|
|
45
|
-
const { default: express } = await mlm.import('express');
|
|
46
|
-
return express.urlencoded({ extended: true });
|
|
47
|
-
},
|
|
48
|
-
async onStart() {
|
|
49
|
-
const { default: express } = await mlm.import('express');
|
|
50
|
-
app = express();
|
|
51
|
-
for (const middleware of middlewares) {
|
|
52
|
-
const mw = await middleware.create(app);
|
|
53
|
-
if (middleware.path) {
|
|
54
|
-
app.use(middleware.path, mw);
|
|
55
|
-
} else {
|
|
56
|
-
app.use(mw);
|
|
21
|
+
get Router() {
|
|
22
|
+
return express.Router;
|
|
57
23
|
}
|
|
24
|
+
get express () {
|
|
25
|
+
return express;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
'register.middleware': (conf, unit) => {
|
|
29
|
+
for (const key in conf) {
|
|
30
|
+
mlm.assert.not(key in middlewareNames, 'Duplicate middleware ' + key);
|
|
31
|
+
middlewareNames.add(key);
|
|
32
|
+
let c = conf[key];
|
|
33
|
+
if (mlm.is.function(c)) {
|
|
34
|
+
c = { create: c };
|
|
35
|
+
}
|
|
36
|
+
mlm.assert.is({
|
|
37
|
+
path: 'string|none',
|
|
38
|
+
create: 'function'
|
|
39
|
+
}, c, 'middleware');
|
|
40
|
+
mlm.assert.is.function(c.create, 'middleware');
|
|
41
|
+
middlewares.push({ ...c, unit: unit.name });
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
'config.port': {
|
|
45
|
+
is: v => Number.isInteger(v) && v > 0 && v < 65536,
|
|
46
|
+
default: 3000
|
|
47
|
+
},
|
|
48
|
+
'config.host': {
|
|
49
|
+
is: 'string',
|
|
50
|
+
default: 'localhost'
|
|
51
|
+
},
|
|
52
|
+
'middleware.json': async (app) => {
|
|
53
|
+
return express.json();
|
|
54
|
+
},
|
|
55
|
+
'middleware.urlencoded': async (app) => {
|
|
56
|
+
return express.urlencoded({ extended: true });
|
|
57
|
+
},
|
|
58
|
+
async onStart() {
|
|
59
|
+
app = express();
|
|
60
|
+
for (const middleware of middlewares) {
|
|
61
|
+
const mw = await middleware.create(app);
|
|
62
|
+
if (!mw) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (middleware.path) {
|
|
66
|
+
app.use(middleware.path, mw);
|
|
67
|
+
} else {
|
|
68
|
+
app.use(mw);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
app.use('/.well-known', (req, res, next) => {
|
|
72
|
+
res.logGroup = 'well-known';
|
|
73
|
+
res.status(404).send('Not found');
|
|
74
|
+
res.end();
|
|
75
|
+
});
|
|
76
|
+
app.use((req, res, next) => {
|
|
77
|
+
res.logGroup = '404';
|
|
78
|
+
res.status(404).send('Not found');
|
|
79
|
+
})
|
|
80
|
+
app.use((error, req, res, next,) => {
|
|
81
|
+
mlm.log('error', error);
|
|
82
|
+
res.status(500).send(error.stack);
|
|
83
|
+
})
|
|
84
|
+
const server = app.listen(mlm.config.port, mlm.config.host, (error) => {
|
|
85
|
+
if (error) {
|
|
86
|
+
mlm.throw(error);
|
|
87
|
+
}
|
|
88
|
+
mlm.log(`Listening on ${mlm.config.host}:${mlm.config.port}`);
|
|
89
|
+
});
|
|
90
|
+
server.on('close', () => {
|
|
91
|
+
mlm.log('Server closed');
|
|
92
|
+
});
|
|
93
|
+
app.on('error', (error) => {
|
|
94
|
+
mlm.error(error);
|
|
95
|
+
app.throw(error);
|
|
96
|
+
});
|
|
58
97
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})
|
|
98
|
+
}
|
|
99
|
+
}
|
package/units/fetch-cache-fs.js
CHANGED
|
@@ -1,54 +1,181 @@
|
|
|
1
1
|
export const info = {
|
|
2
2
|
name: 'fetch-cache-fs',
|
|
3
3
|
version: '1.0.0',
|
|
4
|
-
description: 'Cached fetch with filesystem cache',
|
|
5
|
-
requires: ['noxt-plugin'],
|
|
6
4
|
provides: ['#fetch'],
|
|
7
|
-
|
|
8
|
-
'node-fetch-cache': '^1.0.0',
|
|
9
|
-
},
|
|
5
|
+
description: 'Cached fetch with stale-while-revalidate',
|
|
10
6
|
}
|
|
11
7
|
|
|
12
|
-
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
import { readFile, writeFile, mkdir, stat } from 'fs/promises';
|
|
10
|
+
import { join } from 'path';
|
|
13
11
|
|
|
14
|
-
export default mlm =>
|
|
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
|
-
|
|
12
|
+
export default mlm => {
|
|
13
|
+
const inflight = new Map();
|
|
14
|
+
const refreshing = new Map();
|
|
15
|
+
|
|
16
|
+
async function cachedFetch(url, options = {}) {
|
|
17
|
+
const forceFresh = options.cache === 'no-cache';
|
|
18
|
+
const isGet = !options.method || options.method === 'GET';
|
|
19
|
+
|
|
20
|
+
if (!isGet) return fetchWithRetry(url, options);
|
|
21
|
+
|
|
22
|
+
const cacheKey = createHash('sha256').update(url).digest('hex');
|
|
23
|
+
const cachePath = join(mlm.config.fetch.cacheDir, cacheKey + '.json');
|
|
24
|
+
|
|
25
|
+
if (inflight.has(cacheKey)) {
|
|
26
|
+
return inflight.get(cacheKey);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!forceFresh) {
|
|
30
|
+
const cached = await readCache(cachePath);
|
|
31
|
+
if (cached) {
|
|
32
|
+
const age = Date.now() - cached.cachedAt;
|
|
33
|
+
|
|
34
|
+
if (age <= mlm.config.fetch.stale) {
|
|
35
|
+
return toResponse(cached);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (age <= mlm.config.fetch.ttl) {
|
|
39
|
+
refreshInBackground(url, cachePath, options, cacheKey);
|
|
40
|
+
return toResponse(cached);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const promise = (async () => {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetchWithRetry(url, options);
|
|
48
|
+
|
|
49
|
+
if (res.ok) {
|
|
50
|
+
const body = await res.text();
|
|
51
|
+
await writeCache(cachePath, res, body);
|
|
52
|
+
return new Response(body, { status: res.status, headers: res.headers });
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Fetch failed completely - fall through to stale cache check
|
|
56
|
+
mlm.log('fetch failed, checking for stale cache:', error.message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fallback to stale cache on error or non-ok response
|
|
60
|
+
const cached = await readCache(cachePath);
|
|
61
|
+
if (cached && Date.now() - cached.cachedAt <= mlm.config.fetch.ttl) {
|
|
62
|
+
mlm.log('returning stale cache for', url);
|
|
63
|
+
return toResponse(cached);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// No cache available, throw or return error response
|
|
67
|
+
throw new Error(`Failed to fetch ${url} and no cache available`);
|
|
68
|
+
})();
|
|
69
|
+
|
|
70
|
+
inflight.set(cacheKey, promise);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
return await promise;
|
|
74
|
+
} finally {
|
|
75
|
+
inflight.delete(cacheKey);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function refreshInBackground(url, cachePath, options, cacheKey) {
|
|
80
|
+
if (refreshing.has(cacheKey)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const promise = (async () => {
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetchWithRetry(url, options);
|
|
87
|
+
if (res.ok) {
|
|
88
|
+
const body = await res.text();
|
|
89
|
+
await writeCache(cachePath, res, body);
|
|
90
|
+
}
|
|
91
|
+
} catch {}
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
refreshing.set(cacheKey, promise);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await promise;
|
|
98
|
+
} finally {
|
|
99
|
+
refreshing.delete(cacheKey);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function fetchWithRetry(url, options) {
|
|
104
|
+
let lastError;
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i <= mlm.config.fetch.retry; i++) {
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timeout = setTimeout(() => controller.abort(), mlm.config.fetch.timeout);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
mlm.log('fetching', url);
|
|
112
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
return res;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
mlm.log('failed',url,e.message)
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
lastError = e;
|
|
119
|
+
if (i < mlm.config.fetch.retry) {
|
|
120
|
+
await new Promise(r => setTimeout(r, 100 * (i + 1)));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw lastError;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function readCache(path) {
|
|
129
|
+
try {
|
|
130
|
+
const stats = await stat(path);
|
|
131
|
+
const data = JSON.parse(await readFile(path, 'utf8'));
|
|
132
|
+
data.cachedAt = stats.mtimeMs;
|
|
133
|
+
return data;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function writeCache(path, res, body) {
|
|
140
|
+
await writeFile(path, JSON.stringify({
|
|
141
|
+
url: res.url,
|
|
142
|
+
status: res.status,
|
|
143
|
+
headers: Object.fromEntries(res.headers),
|
|
144
|
+
body,
|
|
145
|
+
}));
|
|
42
146
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
res.ejectFromCache();
|
|
50
|
-
res = await fetch(url, options);
|
|
147
|
+
|
|
148
|
+
function toResponse(cached) {
|
|
149
|
+
return new Response(cached.body, {
|
|
150
|
+
status: cached.status,
|
|
151
|
+
headers: new Headers(cached.headers),
|
|
152
|
+
});
|
|
51
153
|
}
|
|
52
|
-
|
|
53
|
-
return
|
|
54
|
-
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
'config.fetch': {
|
|
157
|
+
is: {
|
|
158
|
+
cacheDir: 'string',
|
|
159
|
+
ttl: 'integer',
|
|
160
|
+
stale: 'integer',
|
|
161
|
+
retry: 'positiveInteger',
|
|
162
|
+
timeout: 'positiveInteger'
|
|
163
|
+
},
|
|
164
|
+
default: {
|
|
165
|
+
cacheDir: '.cache/fetch',
|
|
166
|
+
ttl: 24 * 60 * 60 * 1000,
|
|
167
|
+
stale: 60 * 60 * 1000,
|
|
168
|
+
retry: 2,
|
|
169
|
+
timeout: 5000
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
'services.fetch': () => ({
|
|
174
|
+
fetch: cachedFetch,
|
|
175
|
+
}),
|
|
176
|
+
|
|
177
|
+
async onStart() {
|
|
178
|
+
await mkdir(mlm.config.fetch.cacheDir, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
};
|
package/units/fetch-cache.js
CHANGED
package/units/fetch-node.js
CHANGED
|
@@ -11,19 +11,6 @@ export default mlm => ({
|
|
|
11
11
|
requires: ['noxt-plugin'],
|
|
12
12
|
provides: ['#fetch'],
|
|
13
13
|
description: 'Native fetch',
|
|
14
|
-
'config.fetch': {
|
|
15
|
-
is: {
|
|
16
|
-
ttl: 'integer|none',
|
|
17
|
-
retry: 'positiveInteger|none',
|
|
18
|
-
timeout: 'positiveInteger|none'
|
|
19
|
-
},
|
|
20
|
-
default: {
|
|
21
|
-
ttl: 60 * 60 * 1000,
|
|
22
|
-
retry: 2,
|
|
23
|
-
timeout: 5000
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
|
|
27
14
|
'serverContext.fetch': () => fetchOrThrow,
|
|
28
15
|
});
|
|
29
16
|
|