yedra 0.20.9 → 0.20.11
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/dist/routing/app.d.ts +12 -0
- package/dist/routing/app.js +64 -22
- package/package.json +1 -1
package/dist/routing/app.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ declare class Context {
|
|
|
15
15
|
type ServeFile = {
|
|
16
16
|
data: Buffer;
|
|
17
17
|
mime: string;
|
|
18
|
+
etag: string;
|
|
19
|
+
cacheControl: string;
|
|
18
20
|
};
|
|
19
21
|
type ServeResponse = {
|
|
20
22
|
status?: number;
|
|
@@ -27,6 +29,16 @@ type ServeFallback = (req: {
|
|
|
27
29
|
type ServeConfig = {
|
|
28
30
|
dir: string;
|
|
29
31
|
fallback?: string | ServeFallback;
|
|
32
|
+
/**
|
|
33
|
+
* Files matching `pattern` are served with `Cache-Control: public,
|
|
34
|
+
* max-age=<maxAge>, immutable`. Intended for content-addressed assets
|
|
35
|
+
* (e.g. `app.abc12345.js`) whose URL changes when the content changes.
|
|
36
|
+
* Never matches the fallback file.
|
|
37
|
+
*/
|
|
38
|
+
immutable?: {
|
|
39
|
+
pattern: RegExp;
|
|
40
|
+
maxAge: number;
|
|
41
|
+
};
|
|
30
42
|
};
|
|
31
43
|
type ServeData = {
|
|
32
44
|
files: Map<string, ServeFile>;
|
package/dist/routing/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
3
|
import { createServer as createHttpServer } from 'node:http';
|
|
3
4
|
import { createServer as createHttpsServer } from 'node:https';
|
|
@@ -135,11 +136,27 @@ class BuiltApp {
|
|
|
135
136
|
const staticFile = this.serveData.files.get(req.url.pathname) ??
|
|
136
137
|
this.serveData.files.get('__fallback');
|
|
137
138
|
if (staticFile !== undefined) {
|
|
139
|
+
const ifNoneMatch = req.headers['if-none-match'];
|
|
140
|
+
const clientEtag = Array.isArray(ifNoneMatch)
|
|
141
|
+
? ifNoneMatch[0]
|
|
142
|
+
: ifNoneMatch;
|
|
143
|
+
if (clientEtag === staticFile.etag) {
|
|
144
|
+
return {
|
|
145
|
+
status: 304,
|
|
146
|
+
body: Buffer.alloc(0),
|
|
147
|
+
headers: {
|
|
148
|
+
etag: staticFile.etag,
|
|
149
|
+
'cache-control': staticFile.cacheControl,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
138
153
|
return {
|
|
139
154
|
status: 200,
|
|
140
155
|
body: staticFile.data,
|
|
141
156
|
headers: {
|
|
142
157
|
'content-type': staticFile.mime,
|
|
158
|
+
etag: staticFile.etag,
|
|
159
|
+
'cache-control': staticFile.cacheControl,
|
|
143
160
|
},
|
|
144
161
|
};
|
|
145
162
|
}
|
|
@@ -275,29 +292,45 @@ yedra_request_duration_sum{method="${method}",status="${status}"} ${data?.durati
|
|
|
275
292
|
}));
|
|
276
293
|
});
|
|
277
294
|
const wss = new WebSocketServer({ server });
|
|
278
|
-
wss.on('connection',
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
+
wss.on('connection', (ws, req) => {
|
|
296
|
+
const extractedContext = propagation.extract(context.active(), req.headers);
|
|
297
|
+
context.with(extractedContext, () => trace.getTracer('yedra').startActiveSpan('incoming_ws_connection', {
|
|
298
|
+
kind: SpanKind.SERVER,
|
|
299
|
+
}, async (span) => {
|
|
300
|
+
span.setAttribute('http.url', req.url ?? 'UNKNOWN');
|
|
301
|
+
let ended = false;
|
|
302
|
+
const end = () => {
|
|
303
|
+
if (ended) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
ended = true;
|
|
307
|
+
span.end();
|
|
308
|
+
};
|
|
309
|
+
ws.once('close', end);
|
|
310
|
+
const url = new URL(req.url, 'http://localhost');
|
|
311
|
+
const { result } = this.matchWsRoute(url.pathname);
|
|
312
|
+
if (!result) {
|
|
313
|
+
ws.close(4404);
|
|
314
|
+
end();
|
|
315
|
+
return;
|
|
295
316
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
317
|
+
try {
|
|
318
|
+
const headers = Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [
|
|
319
|
+
key,
|
|
320
|
+
Array.isArray(value) ? value.join(',') : (value ?? ''),
|
|
321
|
+
]));
|
|
322
|
+
await result.endpoint.handle(url, result.params, headers, ws);
|
|
299
323
|
}
|
|
300
|
-
|
|
324
|
+
catch (error) {
|
|
325
|
+
if (error instanceof HttpError) {
|
|
326
|
+
ws.close(4000 + error.status, error.message);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
console.error(error);
|
|
330
|
+
ws.close(1011, 'Internal Error');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}));
|
|
301
334
|
});
|
|
302
335
|
server.listen(port, () => {
|
|
303
336
|
if (this.quiet !== true) {
|
|
@@ -431,15 +464,22 @@ export class Yedra {
|
|
|
431
464
|
catch (_error) {
|
|
432
465
|
files = [];
|
|
433
466
|
}
|
|
467
|
+
const revalidate = 'public, max-age=0, must-revalidate';
|
|
434
468
|
await Promise.all(files.map(async (file) => {
|
|
435
469
|
const absolute = join(config.dir, file);
|
|
436
470
|
if (!(await stat(absolute)).isFile()) {
|
|
437
471
|
return;
|
|
438
472
|
}
|
|
439
473
|
const data = await readFile(absolute);
|
|
440
|
-
|
|
474
|
+
const urlPath = `/${file}`;
|
|
475
|
+
const cacheControl = config.immutable !== undefined && config.immutable.pattern.test(file)
|
|
476
|
+
? `public, max-age=${config.immutable.maxAge}, immutable`
|
|
477
|
+
: revalidate;
|
|
478
|
+
staticFiles.set(urlPath, {
|
|
441
479
|
data,
|
|
442
480
|
mime: mime.getType(extname(file)) ?? 'application/octet-stream',
|
|
481
|
+
etag: `"${createHash('sha1').update(data).digest('hex')}"`,
|
|
482
|
+
cacheControl,
|
|
443
483
|
});
|
|
444
484
|
}));
|
|
445
485
|
if (config.fallback) {
|
|
@@ -449,6 +489,8 @@ export class Yedra {
|
|
|
449
489
|
data,
|
|
450
490
|
mime: mime.getType(extname(config.fallback)) ??
|
|
451
491
|
'application/octet-stream',
|
|
492
|
+
etag: `"${createHash('sha1').update(data).digest('hex')}"`,
|
|
493
|
+
cacheControl: revalidate,
|
|
452
494
|
});
|
|
453
495
|
return {
|
|
454
496
|
files: staticFiles,
|