@vida-global/core 2.4.1 → 2.4.3
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.
|
@@ -12,6 +12,7 @@ class AppDependencies {
|
|
|
12
12
|
setDependency(name, dependency) {
|
|
13
13
|
this.#dependencies[name] = dependency;
|
|
14
14
|
this.#defineAccessor(name);
|
|
15
|
+
this.#defineSingletonAccessor(name);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
|
|
@@ -26,6 +27,17 @@ class AppDependencies {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
#defineSingletonAccessor(name) {
|
|
31
|
+
if (Object.getOwnPropertyDescriptor(this, name)) return;
|
|
32
|
+
if (name in this) return;
|
|
33
|
+
|
|
34
|
+
Object.defineProperty(this, name, {
|
|
35
|
+
configurable: false,
|
|
36
|
+
get: () => this.get(name)
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
29
41
|
// Registering after `installOn` still defines the accessor, so wiring order does not matter.
|
|
30
42
|
// An existing property is left alone: a controller that defines the name itself wins.
|
|
31
43
|
#defineAccessor(name) {
|
|
@@ -248,13 +248,53 @@ const InstanceMethods = {
|
|
|
248
248
|
if (this.rendered) return;
|
|
249
249
|
if (this.isStreaming) return;
|
|
250
250
|
|
|
251
|
+
this._response.setHeader('Accept-Ranges', 'bytes');
|
|
252
|
+
const range = Buffer.isBuffer(body) ? this._requestedByteRange(body.length) : null;
|
|
253
|
+
if (range?.unsatisfiable) {
|
|
254
|
+
this._response.status(416);
|
|
255
|
+
this._response.setHeader('Content-Range', `bytes */${body.length}`);
|
|
256
|
+
this._send('');
|
|
257
|
+
this.markRendered();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
251
261
|
this._response.setHeader('Content-Type', contentType);
|
|
252
262
|
if (filename) this._response.attachment(filename);
|
|
263
|
+
if (range) {
|
|
264
|
+
this._response.status(206);
|
|
265
|
+
this._response.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${body.length}`);
|
|
266
|
+
body = body.subarray(range.start, range.end + 1);
|
|
267
|
+
}
|
|
253
268
|
this._send(body);
|
|
254
269
|
this.markRendered();
|
|
255
270
|
},
|
|
256
271
|
|
|
257
272
|
|
|
273
|
+
// Single-range only: a multi-range request gets the whole body, which media players handle
|
|
274
|
+
// better than a multipart/byteranges reply.
|
|
275
|
+
_requestedByteRange(totalBytes) {
|
|
276
|
+
const header = this._request.headers?.range;
|
|
277
|
+
if (typeof header !== 'string') return null;
|
|
278
|
+
|
|
279
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
280
|
+
if (!match || (!match[1] && !match[2])) return null;
|
|
281
|
+
|
|
282
|
+
let start, end;
|
|
283
|
+
if (!match[1]) {
|
|
284
|
+
const suffix = Number(match[2]);
|
|
285
|
+
if (suffix === 0) return { unsatisfiable: true };
|
|
286
|
+
start = Math.max(0, totalBytes - suffix);
|
|
287
|
+
end = totalBytes - 1;
|
|
288
|
+
} else {
|
|
289
|
+
start = Number(match[1]);
|
|
290
|
+
end = match[2] ? Math.min(Number(match[2]), totalBytes - 1) : totalBytes - 1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (start >= totalBytes || start > end) return { unsatisfiable: true };
|
|
294
|
+
return { start, end };
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
|
|
258
298
|
renderFile(filePath, { contentType, cacheControl, maxAge } = {}) {
|
|
259
299
|
if (!filePath) throw new Error('renderFile requires a filePath');
|
|
260
300
|
const resolvedType = contentType || guessContentType(filePath);
|
package/package.json
CHANGED
|
@@ -46,6 +46,44 @@ describe('AppDependencies', () => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
describe('singleton accessors', () => {
|
|
50
|
+
it ('exposes a registered name as a property on the singleton', () => {
|
|
51
|
+
const helper = { name: 'helper' };
|
|
52
|
+
|
|
53
|
+
AppDependencies.setDependency('singletonHelper', helper);
|
|
54
|
+
|
|
55
|
+
expect(AppDependencies.singletonHelper).toBe(helper);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
it ('reads through, so a later replacement is picked up', () => {
|
|
60
|
+
AppDependencies.setDependency('replacedHelper', 'first');
|
|
61
|
+
|
|
62
|
+
AppDependencies.setDependency('replacedHelper', 'second');
|
|
63
|
+
|
|
64
|
+
expect(AppDependencies.replacedHelper).toBe('second');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
it ('reads null after a reset', () => {
|
|
69
|
+
AppDependencies.setDependency('resetHelper', 'something');
|
|
70
|
+
|
|
71
|
+
AppDependencies.reset();
|
|
72
|
+
|
|
73
|
+
expect(AppDependencies.resetHelper).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
// `get`, `reset`, and friends must survive a hostile dependency name.
|
|
78
|
+
it ('does not overwrite the singleton API', () => {
|
|
79
|
+
AppDependencies.setDependency('get', 'not a function');
|
|
80
|
+
|
|
81
|
+
expect(typeof AppDependencies.get).toBe('function');
|
|
82
|
+
expect(AppDependencies.get('get')).toBe('not a function');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
|
|
49
87
|
describe('#installOn', () => {
|
|
50
88
|
it ('exposes what was registered before the install as a getter', () => {
|
|
51
89
|
AppDependencies.setDependency('myHelper', 'early');
|
|
@@ -758,11 +758,12 @@ describe('VidaServerController', () => {
|
|
|
758
758
|
|
|
759
759
|
|
|
760
760
|
beforeEach(() => {
|
|
761
|
-
request = {};
|
|
761
|
+
request = { headers: {} };
|
|
762
762
|
response = {
|
|
763
763
|
setHeader: jest.fn(),
|
|
764
764
|
attachment: jest.fn(),
|
|
765
765
|
send: jest.fn(),
|
|
766
|
+
status: jest.fn(),
|
|
766
767
|
};
|
|
767
768
|
controller = new FooController(request, response);
|
|
768
769
|
});
|
|
@@ -774,6 +775,52 @@ describe('VidaServerController', () => {
|
|
|
774
775
|
controller.renderBinary(body, { contentType: 'audio/wav' });
|
|
775
776
|
|
|
776
777
|
expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'audio/wav');
|
|
778
|
+
expect(response.setHeader).toHaveBeenCalledWith('Accept-Ranges', 'bytes');
|
|
779
|
+
expect(response.status).not.toHaveBeenCalled();
|
|
780
|
+
expect(response.send).toHaveBeenCalledWith(body);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
it.each([
|
|
785
|
+
['a bounded range', 'bytes=2-5', 'bytes 2-5/10', '2345'],
|
|
786
|
+
['an open-ended range', 'bytes=7-', 'bytes 7-9/10', '789'],
|
|
787
|
+
['a suffix range', 'bytes=-3', 'bytes 7-9/10', '789'],
|
|
788
|
+
['an overshooting end', 'bytes=8-99', 'bytes 8-9/10', '89'],
|
|
789
|
+
])('serves %s as 206 Partial Content', (_label, range, contentRange, slice) => {
|
|
790
|
+
request.headers.range = range;
|
|
791
|
+
|
|
792
|
+
controller.renderBinary(Buffer.from('0123456789'), { contentType: 'audio/wav' });
|
|
793
|
+
|
|
794
|
+
expect(response.status).toHaveBeenCalledWith(206);
|
|
795
|
+
expect(response.setHeader).toHaveBeenCalledWith('Content-Range', contentRange);
|
|
796
|
+
expect(response.send).toHaveBeenCalledWith(Buffer.from(slice));
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
it ('answers an unsatisfiable range with 416 and the total size', () => {
|
|
801
|
+
request.headers.range = 'bytes=10-';
|
|
802
|
+
|
|
803
|
+
controller.renderBinary(Buffer.from('0123456789'), { contentType: 'audio/wav' });
|
|
804
|
+
|
|
805
|
+
expect(response.status).toHaveBeenCalledWith(416);
|
|
806
|
+
expect(response.setHeader).toHaveBeenCalledWith('Content-Range', 'bytes */10');
|
|
807
|
+
expect(response.send).toHaveBeenCalledWith('');
|
|
808
|
+
expect(controller.rendered).toBe(true);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
it.each([
|
|
813
|
+
['a multi-range request', 'bytes=0-1,5-9'],
|
|
814
|
+
['an empty range', 'bytes=-'],
|
|
815
|
+
['a non-byte unit', 'items=0-5'],
|
|
816
|
+
['garbage', 'garbage'],
|
|
817
|
+
])('serves the whole body for %s', (_label, range) => {
|
|
818
|
+
request.headers.range = range;
|
|
819
|
+
const body = Buffer.from('0123456789');
|
|
820
|
+
|
|
821
|
+
controller.renderBinary(body, { contentType: 'audio/wav' });
|
|
822
|
+
|
|
823
|
+
expect(response.status).not.toHaveBeenCalled();
|
|
777
824
|
expect(response.send).toHaveBeenCalledWith(body);
|
|
778
825
|
});
|
|
779
826
|
|