typedrest 0.5.0 → 0.6.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 (89) hide show
  1. package/README.md +41 -5
  2. package/endpoints/{CachingEndpoint.ts → CachingEndpoint.d.ts} +0 -1
  3. package/endpoints/Endpoint.d.ts +121 -0
  4. package/endpoints/Endpoint.js +1 -1
  5. package/endpoints/{EntryEndpoint.ts → EntryEndpoint.d.ts} +7 -27
  6. package/endpoints/generic/{CollectionEndpoint.ts → CollectionEndpoint.d.ts} +2 -5
  7. package/endpoints/generic/{ETagEndpointBase.ts → ETagEndpointBase.d.ts} +8 -38
  8. package/endpoints/generic/ETagEndpointBase.js +16 -4
  9. package/endpoints/generic/{ElementEndpoint.ts → ElementEndpoint.d.ts} +12 -70
  10. package/endpoints/generic/{GenericCollectionEndpoint.ts → GenericCollectionEndpoint.d.ts} +21 -106
  11. package/endpoints/generic/{IndexerEndpoint.ts → IndexerEndpoint.d.ts} +4 -11
  12. package/endpoints/generic/PartialResponse.d.ts +19 -0
  13. package/endpoints/{index.ts → index.d.ts} +1 -2
  14. package/endpoints/raw/{BlobEndpoint.ts → BlobEndpoint.d.ts} +9 -29
  15. package/endpoints/raw/{UploadEndpoint.ts → UploadEndpoint.d.ts} +4 -18
  16. package/endpoints/reactive/Delay.d.ts +7 -0
  17. package/endpoints/reactive/GenericStreamingCollectionEndpoint.d.ts +32 -0
  18. package/endpoints/reactive/{PollingEndpoint.ts → PollingEndpoint.d.ts} +7 -34
  19. package/endpoints/reactive/SseStreamingEndpoint.d.ts +50 -0
  20. package/endpoints/reactive/{StreamingCollectionEndpoint.ts → StreamingCollectionEndpoint.d.ts} +2 -5
  21. package/endpoints/reactive/{StreamingEndpoint.ts → StreamingEndpoint.d.ts} +5 -27
  22. package/endpoints/rpc/{ActionEndpoint.ts → ActionEndpoint.d.ts} +3 -10
  23. package/endpoints/rpc/{ConsumerEndpoint.ts → ConsumerEndpoint.d.ts} +3 -12
  24. package/endpoints/rpc/{FunctionEndpoint.ts → FunctionEndpoint.d.ts} +3 -14
  25. package/endpoints/rpc/{ProducerEndpoint.ts → ProducerEndpoint.d.ts} +3 -11
  26. package/endpoints/rpc/{RpcEndpointBase.ts → RpcEndpointBase.d.ts} +4 -12
  27. package/errors/DefaultErrorHandler.d.ts +21 -0
  28. package/errors/{Errors.ts → Errors.d.ts} +20 -21
  29. package/http/EntityStream.d.ts +10 -0
  30. package/http/FetchHttpClient.d.ts +15 -0
  31. package/http/FetchHttpClient.js +1 -1
  32. package/http/{HttpClient.ts → HttpClient.d.ts} +1 -3
  33. package/http/HttpContentRange.d.ts +23 -0
  34. package/http/{HttpHeader.ts → HttpHeader.d.ts} +1 -1
  35. package/http/{HttpMethod.ts → HttpMethod.d.ts} +1 -1
  36. package/http/{HttpStatusCode.ts → HttpStatusCode.d.ts} +1 -1
  37. package/http/ResponseCache.d.ts +19 -0
  38. package/http/RetryAfter.d.ts +6 -0
  39. package/http/ServerSentEvents.d.ts +50 -0
  40. package/links/AggregateLinkExtractor.d.ts +17 -0
  41. package/links/HalLinkExtractor.d.ts +13 -0
  42. package/links/HeaderLinkExtractor.d.ts +13 -0
  43. package/links/{Link.ts → Link.d.ts} +6 -2
  44. package/links/{LinkExtractor.ts → LinkExtractor.d.ts} +0 -1
  45. package/package.json +64 -14
  46. package/serializers/JsonSerializer.d.ts +18 -0
  47. package/serializers/{Serializer.ts → Serializer.d.ts} +0 -2
  48. package/endpoints/Endpoint.test.ts +0 -300
  49. package/endpoints/Endpoint.ts +0 -247
  50. package/endpoints/generic/CollectionEndpoint.test.ts +0 -215
  51. package/endpoints/generic/ElementEndpoint.test.ts +0 -262
  52. package/endpoints/generic/GenericCollectionEndpoint.test.ts +0 -123
  53. package/endpoints/generic/IndexerEndpoint.test.ts +0 -12
  54. package/endpoints/generic/PartialResponse.ts +0 -28
  55. package/endpoints/raw/BlobEndpoint.test.ts +0 -52
  56. package/endpoints/raw/UploadEndpoint.test.ts +0 -47
  57. package/endpoints/reactive/Delay.ts +0 -26
  58. package/endpoints/reactive/GenericStreamingCollectionEndpoint.ts +0 -73
  59. package/endpoints/reactive/PollingEndpoint.test.ts +0 -84
  60. package/endpoints/reactive/SseStreamingEndpoint.test.ts +0 -169
  61. package/endpoints/reactive/SseStreamingEndpoint.ts +0 -115
  62. package/endpoints/reactive/StreamingCollectionEndpoint.test.ts +0 -110
  63. package/endpoints/reactive/StreamingEndpoint.test.ts +0 -130
  64. package/endpoints/rpc/ActionEndpoint.test.ts +0 -36
  65. package/endpoints/rpc/ConsumerEndpoint.test.ts +0 -29
  66. package/endpoints/rpc/FunctionEndpoint.test.ts +0 -29
  67. package/endpoints/rpc/ProducerEndpoint.test.ts +0 -32
  68. package/errors/DefaultErrorHandler.ts +0 -65
  69. package/http/EntityStream.ts +0 -40
  70. package/http/FetchHttpClient.ts +0 -26
  71. package/http/HttpContentRange.ts +0 -41
  72. package/http/ResponseCache.ts +0 -25
  73. package/http/RetryAfter.ts +0 -19
  74. package/http/ServerSentEvents.ts +0 -145
  75. package/jest.config.js +0 -12
  76. package/links/AggregateLinkExtractor.ts +0 -28
  77. package/links/HalLinkExtractor.ts +0 -45
  78. package/links/HeaderLinkExtractor.ts +0 -56
  79. package/serializers/JsonSerializer.ts +0 -25
  80. /package/endpoints/generic/{index.ts → index.d.ts} +0 -0
  81. /package/endpoints/raw/{index.ts → index.d.ts} +0 -0
  82. /package/endpoints/reactive/{index.ts → index.d.ts} +0 -0
  83. /package/endpoints/rpc/{index.ts → index.d.ts} +0 -0
  84. /package/errors/{ErrorHandler.ts → ErrorHandler.d.ts} +0 -0
  85. /package/errors/{index.ts → index.d.ts} +0 -0
  86. /package/http/{index.ts → index.d.ts} +0 -0
  87. /package/{index.ts → index.d.ts} +0 -0
  88. /package/links/{index.ts → index.d.ts} +0 -0
  89. /package/serializers/{index.ts → index.d.ts} +0 -0
@@ -1,130 +0,0 @@
1
- /**
2
- * NOTE: The jsdom environment does not expose response bodies as streams, so these tests need the node environment.
3
- * @jest-environment node
4
- */
5
-
6
- import fetchMock from 'jest-fetch-mock';
7
- import { StreamingEndpoint } from '.';
8
- import { EntryEndpoint } from '..';
9
- import { NotFoundError } from '../../errors';
10
- import { HttpStatusCode } from '../../http';
11
-
12
- class MockEntity {
13
- constructor(public id: number, public name: string) { }
14
- }
15
-
16
- fetchMock.enableMocks();
17
- let entryEndpoint: EntryEndpoint;
18
-
19
- beforeEach(() => {
20
- fetchMock.resetMocks();
21
- entryEndpoint = new EntryEndpoint('http://localhost/');
22
- });
23
-
24
- /**
25
- * Mocks a response whose body is streamed in multiple chunks.
26
- * @param chunks The chunks to send, in order.
27
- * @param close Whether to close the connection after the last chunk.
28
- * @returns A callback reporting whether the client has cancelled the connection.
29
- */
30
- function mockStream(chunks: string[], close: boolean = true) {
31
- let cancelled = false;
32
-
33
- fetchMock.mockImplementation(async (_input, init) => {
34
- let streamController: ReadableStreamDefaultController<Uint8Array> | undefined;
35
- const body = new ReadableStream<Uint8Array>({
36
- start(controller) {
37
- streamController = controller;
38
- const encoder = new TextEncoder();
39
- for (const chunk of chunks) controller.enqueue(encoder.encode(chunk));
40
- if (close) controller.close();
41
- },
42
- cancel() { cancelled = true; }
43
- });
44
-
45
- // Emulate how aborting a real request fails the response body
46
- init?.signal?.addEventListener('abort', () => {
47
- try { streamController?.error(new Error('aborted')); } catch { }
48
- }, { once: true });
49
-
50
- return new Response(body);
51
- });
52
-
53
- return () => cancelled;
54
- }
55
-
56
- async function collect<T>(stream: AsyncIterable<T>) {
57
- const result: T[] = [];
58
- for await (const element of stream) result.push(element);
59
- return result;
60
- }
61
-
62
- test('stream', async () => {
63
- mockStream(['{"id":5,"name":"test1"}\n', '{"id":6,"name":"test2"}\n']);
64
- const endpoint = new StreamingEndpoint<MockEntity>(entryEndpoint, 'endpoint');
65
-
66
- expect(await collect(endpoint.stream())).toEqual([new MockEntity(5, 'test1'), new MockEntity(6, 'test2')]);
67
- });
68
-
69
- test('streamSplitAcrossChunks', async () => {
70
- mockStream(['{"id":5,"na', 'me":"test1"}\n{"id":6,', '"name":"test2"}\n']);
71
- const endpoint = new StreamingEndpoint<MockEntity>(entryEndpoint, 'endpoint');
72
-
73
- expect(await collect(endpoint.stream())).toEqual([new MockEntity(5, 'test1'), new MockEntity(6, 'test2')]);
74
- });
75
-
76
- test('streamWithoutTrailingSeparator', async () => {
77
- mockStream(['{"id":5,"name":"test1"}\n{"id":6,"name":"test2"}']);
78
- const endpoint = new StreamingEndpoint<MockEntity>(entryEndpoint, 'endpoint');
79
-
80
- expect(await collect(endpoint.stream())).toEqual([new MockEntity(5, 'test1'), new MockEntity(6, 'test2')]);
81
- });
82
-
83
- test('streamCustomSeparator', async () => {
84
- mockStream(['{"id":5,"name":"test1"}|{"id":6,"name":"test2"}']);
85
- const endpoint = new StreamingEndpoint<MockEntity>(entryEndpoint, 'endpoint', '|');
86
-
87
- expect(await collect(endpoint.stream())).toEqual([new MockEntity(5, 'test1'), new MockEntity(6, 'test2')]);
88
- });
89
-
90
- test('streamAsyncIterator', async () => {
91
- mockStream(['{"id":5,"name":"test1"}\n']);
92
- const endpoint = new StreamingEndpoint<MockEntity>(entryEndpoint, 'endpoint');
93
-
94
- expect(await collect(endpoint)).toEqual([new MockEntity(5, 'test1')]);
95
- });
96
-
97
- test('streamBreak', async () => {
98
- const cancelled = mockStream(['{"id":5,"name":"test1"}\n', '{"id":6,"name":"test2"}\n'], false);
99
- const endpoint = new StreamingEndpoint<MockEntity>(entryEndpoint, 'endpoint');
100
-
101
- const result: MockEntity[] = [];
102
- for await (const element of endpoint.stream()) {
103
- result.push(element);
104
- break;
105
- }
106
-
107
- expect(result).toEqual([new MockEntity(5, 'test1')]);
108
- expect(cancelled()).toBe(true);
109
- });
110
-
111
- test('streamAbort', async () => {
112
- mockStream(['{"id":5,"name":"test1"}\n', '{"id":6,"name":"test2"}\n'], false);
113
- const endpoint = new StreamingEndpoint<MockEntity>(entryEndpoint, 'endpoint');
114
- const controller = new AbortController();
115
-
116
- const result: MockEntity[] = [];
117
- for await (const element of endpoint.stream(controller.signal)) {
118
- result.push(element);
119
- if (result.length === 2) controller.abort();
120
- }
121
-
122
- expect(result).toEqual([new MockEntity(5, 'test1'), new MockEntity(6, 'test2')]);
123
- });
124
-
125
- test('streamError', async () => {
126
- fetchMock.mockResponseOnce('', { status: HttpStatusCode.NotFound });
127
- const endpoint = new StreamingEndpoint<MockEntity>(entryEndpoint, 'endpoint');
128
-
129
- await expect(collect(endpoint.stream())).rejects.toThrow(NotFoundError);
130
- });
@@ -1,36 +0,0 @@
1
- import fetchMock from 'jest-fetch-mock';
2
- import { ActionEndpoint } from '.';
3
- import { EntryEndpoint } from '..';
4
- import { HttpMethod, HttpHeader } from '../../http';
5
-
6
- fetchMock.enableMocks();
7
- let endpoint: ActionEndpoint;
8
-
9
- beforeEach(() => {
10
- fetchMock.resetMocks();
11
- endpoint = new ActionEndpoint(new EntryEndpoint('http://localhost/'), 'endpoint');
12
- });
13
-
14
- test('probe', async () => {
15
- fetchMock.mockOnceIf(
16
- req => req.method === HttpMethod.Options && req.url === 'http://localhost/endpoint',
17
- async () => {
18
- return {
19
- headers: {
20
- [HttpHeader.Allow]: HttpMethod.Post
21
- }
22
- };
23
- }
24
- );
25
- await endpoint.probe();
26
-
27
- expect(endpoint.invokeAllowed).toBe(true);
28
- });
29
-
30
- test('invoke', async () => {
31
- fetchMock.mockOnceIf(
32
- req => req.method === HttpMethod.Post && req.url === 'http://localhost/endpoint'
33
- );
34
-
35
- await endpoint.invoke();
36
- });
@@ -1,29 +0,0 @@
1
- import fetchMock from 'jest-fetch-mock';
2
- import { ConsumerEndpoint } from '.';
3
- import { EntryEndpoint } from '..';
4
- import { HttpMethod, HttpHeader } from '../../http';
5
-
6
- class MockEntity {
7
- constructor(public id: number, public name: string) { }
8
- }
9
-
10
- fetchMock.enableMocks();
11
- let endpoint: ConsumerEndpoint<MockEntity>;
12
-
13
- beforeEach(() => {
14
- fetchMock.resetMocks();
15
- endpoint = new ConsumerEndpoint(new EntryEndpoint('http://localhost/'), 'endpoint');
16
- });
17
-
18
- test('invoke', async () => {
19
- fetchMock.mockOnceIf(
20
- req => req.method === HttpMethod.Post && req.url === 'http://localhost/endpoint',
21
- async req => {
22
- expect(req.headers.get(HttpHeader.ContentType)).toBe('application/json');
23
- expect(await req.text()).toBe('{"id":1,"name":"input"}');
24
- return {};
25
- }
26
- );
27
-
28
- await endpoint.invoke(new MockEntity(1, 'input'));
29
- });
@@ -1,29 +0,0 @@
1
- import fetchMock from 'jest-fetch-mock';
2
- import { FunctionEndpoint } from '.';
3
- import { EntryEndpoint } from '..';
4
- import { HttpMethod, HttpHeader } from '../../http';
5
-
6
- fetchMock.enableMocks();
7
- let endpoint: FunctionEndpoint<{input: string}, {output: string}>;
8
-
9
- beforeEach(() => {
10
- fetchMock.resetMocks();
11
- endpoint = new FunctionEndpoint(new EntryEndpoint('http://localhost/'), 'endpoint');
12
- });
13
-
14
- test('invoke', async () => {
15
- fetchMock.mockOnceIf(
16
- req => req.method === HttpMethod.Post && req.url === 'http://localhost/endpoint',
17
- async req => {
18
- expect(req.headers.get(HttpHeader.ContentType)).toBe('application/json');
19
- expect(await req.text()).toBe('{"input":"in"}');
20
- return {
21
- headers: { [HttpHeader.ContentType]: 'application/json' },
22
- body: '{"output":"out"}'
23
- };
24
- }
25
- );
26
-
27
- const result = await endpoint.invoke({input: "in"});
28
- expect(result).toEqual({output: "out"});
29
- });
@@ -1,32 +0,0 @@
1
- import fetchMock from 'jest-fetch-mock';
2
- import { ProducerEndpoint } from '.';
3
- import { EntryEndpoint } from '..';
4
- import { HttpMethod, HttpHeader } from '../../http';
5
-
6
- class MockEntity {
7
- constructor(public id: number, public name: string) { }
8
- }
9
-
10
- fetchMock.enableMocks();
11
- let endpoint: ProducerEndpoint<MockEntity>;
12
-
13
- beforeEach(() => {
14
- fetchMock.resetMocks();
15
- endpoint = new ProducerEndpoint(new EntryEndpoint('http://localhost/'), 'endpoint');
16
- });
17
-
18
- test('invoke', async () => {
19
- fetchMock.mockOnceIf(
20
- req => req.method === HttpMethod.Post && req.url === 'http://localhost/endpoint',
21
- async req => {
22
- expect(req.headers.get(HttpHeader.Accept)).toBe('application/json');
23
- return {
24
- headers: { [HttpHeader.ContentType]: 'application/json' },
25
- body: '{"id":1,"name":"input"}'
26
- };
27
- }
28
- );
29
-
30
- const result = await endpoint.invoke();
31
- expect(result).toEqual(new MockEntity(1, 'input'));
32
- });
@@ -1,65 +0,0 @@
1
- import { ErrorHandler } from "./ErrorHandler";
2
- import { HttpError, BadRequestError, AuthenticationError, AuthorizationError, NotFoundError, TimeoutError, ConflictError, ConcurrencyError, RangeNotSatisfiableError } from "./Errors";
3
- import { HttpStatusCode, HttpHeader } from "../http";
4
-
5
- /**
6
- * Handles errors in HTTP responses by mapping status codes to common error types.
7
- */
8
- export class DefaultErrorHandler implements ErrorHandler {
9
- /**
10
- * Throws appropriate `Error`s based on HTTP status codes and response bodies.
11
- *
12
- * @throws {@link errors!BadRequestError}: {@link http!HttpStatusCode.BadRequest}
13
- * @throws {@link errors!AuthenticationError}: {@link http!HttpStatusCode.Unauthorized}
14
- * @throws {@link errors!AuthorizationError}: {@link http!HttpStatusCode.Forbidden}
15
- * @throws {@link errors!NotFoundError}: {@link http!HttpStatusCode.NotFound} or {@link http!HttpStatusCode.Gone}
16
- * @throws {@link errors!TimeoutError}: {@link http!HttpStatusCode.RequestTimeout}
17
- * @throws {@link errors!ConflictError}: {@link http!HttpStatusCode.Conflict}
18
- * @throws {@link errors!ConcurrencyError}: {@link http!HttpStatusCode.PreconditionFailed}
19
- * @throws {@link errors!RangeNotSatisfiableError}: {@link http!HttpStatusCode.RangeNotSatisfiable}
20
- * @throws {@link errors!HttpError}: Other non-success status code
21
- */
22
- async handle(response: Response) {
23
- if (response.ok) return;
24
-
25
- const contentType = response.headers.get(HttpHeader.ContentType);
26
- let jsonBody: any;
27
- if (contentType?.startsWith("application/json") || contentType?.includes("+json")) {
28
- try {
29
- jsonBody = await response.json();
30
- } catch {
31
- jsonBody = undefined;
32
- }
33
- }
34
-
35
- const errorType = DefaultErrorHandler.errorType(response.status);
36
- throw new errorType(
37
- jsonBody?.message ?? jsonBody?.details ?? `HTTP ${response.status} ${response.statusText}`,
38
- response.status,
39
- jsonBody);
40
- }
41
-
42
- private static errorType(status: HttpStatusCode): new (message: string, status: HttpStatusCode, data?: any) => Error {
43
- switch (status) {
44
- case HttpStatusCode.BadRequest:
45
- return BadRequestError;
46
- case HttpStatusCode.Unauthorized:
47
- return AuthenticationError;
48
- case HttpStatusCode.Forbidden:
49
- return AuthorizationError;
50
- case HttpStatusCode.NotFound:
51
- case HttpStatusCode.Gone:
52
- return NotFoundError;
53
- case HttpStatusCode.RequestTimeout:
54
- return TimeoutError;
55
- case HttpStatusCode.Conflict:
56
- return ConflictError;
57
- case HttpStatusCode.PreconditionFailed:
58
- return ConcurrencyError;
59
- case HttpStatusCode.RangeNotSatisfiable:
60
- return RangeNotSatisfiableError;
61
- default:
62
- return HttpError;
63
- }
64
- }
65
- }
@@ -1,40 +0,0 @@
1
- import { Serializer } from "../serializers";
2
-
3
- /**
4
- * Exposes the body of a `Response` as a stream of deserialized entities.
5
- * @typeParam TEntity The type of entity the stream provides.
6
- * @param response The response to read the body from.
7
- * @param serializer Controls the deserialization of the entities in the body.
8
- * @param separator The character sequence used to detect that a new entity starts in the stream.
9
- * @returns A stream that ends when the server closes the connection.
10
- */
11
- export async function* entityStream<TEntity>(response: Response, serializer: Serializer, separator: string = "\n"): AsyncIterableIterator<TEntity> {
12
- if (!response.body) return;
13
-
14
- const reader = response.body.getReader();
15
- const decoder = new TextDecoder();
16
- let buffer = "";
17
-
18
- try {
19
- while (true) {
20
- const { done, value } = await reader.read();
21
- if (done) break;
22
-
23
- buffer += decoder.decode(value, { stream: true });
24
-
25
- let index: number;
26
- while ((index = buffer.indexOf(separator)) !== -1) {
27
- const entity = buffer.slice(0, index);
28
- buffer = buffer.slice(index + separator.length);
29
- if (entity.trim()) yield serializer.deserialize<TEntity>(entity);
30
- }
31
- }
32
-
33
- // Tolerate a missing separator after the last entity
34
- buffer += decoder.decode();
35
- if (buffer.trim()) yield serializer.deserialize<TEntity>(buffer);
36
- } finally {
37
- // NOTE: Ignore errors here so that a failure that already ended the stream is not masked.
38
- await reader.cancel().catch(() => { });
39
- }
40
- }
@@ -1,26 +0,0 @@
1
- import { HttpMethod } from "./HttpMethod";
2
- import { HttpClient } from "./HttpClient";
3
-
4
- /**
5
- * Communicates with remote resources using JavaScript's standard Fetch API.
6
- */
7
- export class FetchHttpClient implements HttpClient {
8
- /**
9
- * @inheritDoc
10
- */
11
- defaultHeaders = new Headers();
12
-
13
- /**
14
- * @inheritDoc
15
- */
16
- send(uri: URL, method: HttpMethod, signal?: AbortSignal, headers?: HeadersInit, body?: BodyInit) {
17
- const mergedHeaders = new Headers(headers);
18
- this.defaultHeaders.forEach((value, key) => {
19
- if (!mergedHeaders.has(key)) {
20
- mergedHeaders.set(key, value);
21
- }
22
- });
23
-
24
- return fetch(uri.href, { method, signal, headers: mergedHeaders, body });
25
- }
26
- }
@@ -1,41 +0,0 @@
1
- import { HttpHeader } from "./HttpHeader";
2
-
3
- /**
4
- * Describes which subset of a set of elements a response contains.
5
- */
6
- export class HttpContentRange {
7
- /**
8
- * Creates a new content range.
9
- * @param unit The unit the range is expressed in, e.g. `elements`.
10
- * @param from The index at which the data starts.
11
- * @param to The index at which the data stops.
12
- * @param length The total number of elements available on the server; `undefined` if unknown.
13
- */
14
- constructor(
15
- public readonly unit: string,
16
- public readonly from?: number,
17
- public readonly to?: number,
18
- public readonly length?: number) {
19
- }
20
-
21
- private static readonly pattern = /^(\w+) (?:(\d+)-(\d+)|\*)\/(\d+|\*)$/;
22
-
23
- /**
24
- * Extracts the content range from a `Response`.
25
- * @returns The content range; `undefined` if the response has no (valid) {@link HttpHeader.ContentRange} header.
26
- */
27
- static from(response: Response): HttpContentRange | undefined {
28
- const header = response.headers.get(HttpHeader.ContentRange);
29
- if (!header) return undefined;
30
-
31
- const match = HttpContentRange.pattern.exec(header.trim());
32
- if (!match) return undefined;
33
-
34
- const [, unit, from, to, length] = match;
35
- return new HttpContentRange(
36
- unit,
37
- from === undefined ? undefined : parseInt(from),
38
- to === undefined ? undefined : parseInt(to),
39
- length === "*" ? undefined : parseInt(length));
40
- }
41
- }
@@ -1,25 +0,0 @@
1
- import { HttpHeader } from "./HttpHeader";
2
-
3
- /**
4
- * Caches the contents of a `Response`.
5
- */
6
- export class ResponseCache {
7
- /**
8
- * Creates a new cache.
9
- * @param content The content of the `Response`.
10
- * @param contentType The MIME type of the `content`.
11
- * @param eTag The E-Tag associated with the `content`.
12
- */
13
- constructor(public readonly content: string, public readonly contentType?: string, public readonly eTag?: string) {
14
- }
15
-
16
- /**
17
- * Creates a new cache from a `Response`.
18
- */
19
- static async from(response: Response) {
20
- return new ResponseCache(
21
- await response.text(),
22
- response.headers.get(HttpHeader.ContentType) ?? undefined,
23
- response.headers.get(HttpHeader.ETag) ?? undefined);
24
- }
25
- }
@@ -1,19 +0,0 @@
1
- import { HttpHeader } from "./HttpHeader";
2
-
3
- /**
4
- * Extracts the delay a server has asked clients to wait for before retrying a request.
5
- * @param response The response to read the {@link HttpHeader.RetryAfter} header from.
6
- * @returns The delay in milliseconds; `undefined` if the response has no (valid) header.
7
- */
8
- export function retryAfter(response: Response): number | undefined {
9
- const header = response.headers.get(HttpHeader.RetryAfter)?.trim();
10
- if (!header) return undefined;
11
-
12
- // Either a number of seconds ...
13
- if (/^\d+$/.test(header)) return parseInt(header) * 1000;
14
-
15
- // ... or an absolute point in time
16
- const date = Date.parse(header);
17
- if (Number.isNaN(date)) return undefined;
18
- return Math.max(0, date - Date.now());
19
- }
@@ -1,145 +0,0 @@
1
- /**
2
- * Yields the lines terminated by `\n`, `\r\n` or `\r` within the first `limit` characters of `buffer`.
3
- * @returns The remaining characters that are not part of a terminated line.
4
- */
5
- function* splitLines(buffer: string, limit: number): Generator<string, string> {
6
- let start = 0;
7
-
8
- for (let index = 0; index < limit; index++) {
9
- const char = buffer[index];
10
- if (char !== "\n" && char !== "\r") continue;
11
-
12
- yield buffer.slice(start, index);
13
- if (char === "\r" && buffer[index + 1] === "\n") index++;
14
- start = index + 1;
15
- }
16
-
17
- return buffer.slice(start);
18
- }
19
-
20
- /**
21
- * A single event read from a Server-Sent Events (SSE) stream.
22
- */
23
- export class ServerSentEvent {
24
- /**
25
- * Creates a new event.
26
- * @param type The event type from the `event:` field. {@link defaultEventType} if the server did not specify one.
27
- * @param data The concatenated content of the event's `data:` fields.
28
- * @param id The id of the most recent `id:` field if any.
29
- */
30
- constructor(public readonly type: string, public readonly data: string, public readonly id?: string) {
31
- }
32
- }
33
-
34
- /**
35
- * The event type used for events without an explicit `event:` field.
36
- */
37
- export const defaultEventType = "message";
38
-
39
- /**
40
- * Reads {@link ServerSentEvent}s from the body of a `Response` in the `text/event-stream` format.
41
- */
42
- export class SseReader {
43
- /**
44
- * Creates a new reader.
45
- * @param response The response to read the body from.
46
- */
47
- constructor(private readonly response: Response) {
48
- }
49
-
50
- private _lastEventId?: string;
51
-
52
- /**
53
- * The id of the most recent event that carried an `id:` field.<br>
54
- * Send this back as a {@link HttpHeader.LastEventId} header when reconnecting to let the server resume where it left off.
55
- */
56
- get lastEventId() { return this._lastEventId; }
57
-
58
- private _reconnectionInterval?: number;
59
-
60
- /**
61
- * The reconnection interval in milliseconds most recently requested by the server via a `retry:` field if any.
62
- */
63
- get reconnectionInterval() { return this._reconnectionInterval; }
64
-
65
- /**
66
- * Reads events until the stream ends.<br>
67
- * Any event that is still incomplete when the stream ends is discarded.
68
- */
69
- async *events(): AsyncIterableIterator<ServerSentEvent> {
70
- let type: string | undefined;
71
- let data = "";
72
-
73
- for await (const line of this.lines()) {
74
- if (line === "") {
75
- // A blank line dispatches the event collected so far
76
- if (data !== "") {
77
- // Every data field appended a trailing newline; the last one is not part of the payload
78
- yield new ServerSentEvent(type ?? defaultEventType, data.slice(0, -1), this._lastEventId);
79
- data = "";
80
- }
81
- type = undefined;
82
- continue;
83
- }
84
-
85
- // Lines starting with a colon are comments, e.g. the ":ping" some servers send to keep the connection alive
86
- if (line.startsWith(":")) continue;
87
-
88
- const colonIndex = line.indexOf(":");
89
- const field = (colonIndex === -1) ? line : line.slice(0, colonIndex);
90
- const value = (colonIndex === -1) ? ""
91
- : line.slice((line[colonIndex + 1] === " ") ? colonIndex + 2 : colonIndex + 1);
92
-
93
- switch (field) {
94
- case "event":
95
- type = value;
96
- break;
97
- case "data":
98
- data += value + "\n";
99
- break;
100
- case "id":
101
- if (value.indexOf("\0") === -1) this._lastEventId = value;
102
- break;
103
- case "retry":
104
- if (/^\d+$/.test(value)) this._reconnectionInterval = parseInt(value);
105
- break;
106
- }
107
- }
108
- }
109
-
110
- /**
111
- * Splits the response body into lines, accepting `\n`, `\r\n` and `\r` as terminators.
112
- */
113
- private async *lines(): AsyncIterableIterator<string> {
114
- if (!this.response.body) return;
115
-
116
- const reader = this.response.body.getReader();
117
- const decoder = new TextDecoder();
118
- let buffer = "";
119
- let stripBom = true;
120
-
121
- try {
122
- while (true) {
123
- const { done, value } = await reader.read();
124
- if (done) break;
125
-
126
- buffer += decoder.decode(value, { stream: true });
127
-
128
- if (stripBom && buffer !== "") {
129
- if (buffer.charCodeAt(0) === 0xFEFF) buffer = buffer.slice(1);
130
- stripBom = false;
131
- }
132
-
133
- // NOTE: Keep a trailing "\r" buffered; it may still be followed by a "\n" in the next chunk.
134
- buffer = yield* splitLines(buffer, buffer.endsWith("\r") ? buffer.length - 1 : buffer.length);
135
- }
136
-
137
- // NOTE: A line that is still unterminated once the stream ends is discarded.
138
- buffer += decoder.decode();
139
- yield* splitLines(buffer, buffer.length);
140
- } finally {
141
- // NOTE: Ignore errors here so that a failure that already ended the stream is not masked.
142
- await reader.cancel().catch(() => { });
143
- }
144
- }
145
- }
package/jest.config.js DELETED
@@ -1,12 +0,0 @@
1
- /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2
- module.exports = {
3
- preset: 'ts-jest',
4
- // jsdom is required for Blob() constructor and fetch()!
5
- testEnvironment: 'jsdom',
6
- testMatch: [
7
- "**/?(*.)+(spec|test).ts"
8
- ],
9
- transform: {
10
- "\\.ts$": ["ts-jest", { tsconfig: "tsconfig.test.json" }]
11
- },
12
- };
@@ -1,28 +0,0 @@
1
- import { Link } from "./Link";
2
- import { LinkExtractor } from "./LinkExtractor";
3
-
4
- /**
5
- * Combines the results of multiple {@link LinkExtractor}s.
6
- */
7
- export class AggregateLinkExtractor implements LinkExtractor {
8
- private extractors: LinkExtractor[];
9
-
10
- /**
11
- * Creates a new aggregate link extractor.
12
- * @param extractors The link extractors to aggregate.
13
- */
14
- constructor(...extractors: LinkExtractor[]) {
15
- this.extractors = extractors;
16
- }
17
-
18
- /**
19
- * @inheritDoc
20
- */
21
- async getLinks(response: Response) {
22
- let result: Link[] = [];
23
- for (const extractor of this.extractors) {
24
- result = result.concat(await extractor.getLinks(response));
25
- }
26
- return result;
27
- }
28
- }