Skip to content

Commit 6700cb1

Browse files
committed
Added support for canceling requests
1 parent ecaa110 commit 6700cb1

14 files changed

Lines changed: 71 additions & 36 deletions

endpoints/Endpoint.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,13 @@ export class Endpoint {
102102
* Sends an HTTP request to this endpoint's URI.
103103
* Handles various cross-cutting concerns regarding a response message such as discovering links and handling errors.
104104
* @param method The HTTP method to use.
105+
* @param signal Used to cancel the request.
105106
* @param headers The HTTP headers to set.
106107
* @param body The body to send.
107108
* @throws {@link HttpError}
108109
*/
109-
protected async send(method: HttpMethod, headers?: HeadersInit, body?: BodyInit): Promise<Response> {
110-
const response = await this.httpClient.send(this.uri, method, headers, body);
110+
protected async send(method: HttpMethod, signal?: AbortSignal, headers?: HeadersInit, body?: BodyInit): Promise<Response> {
111+
const response = await this.httpClient.send(this.uri, method, signal, headers, body);
111112
await this.handle(response);
112113
return response;
113114
}

endpoints/EntryEndpoint.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ export class EntryEndpoint extends Endpoint {
3636

3737
/**
3838
* Fetches meta data such as links from the server.
39+
* @param signal Used to cancel the request.
3940
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
4041
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
4142
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
4243
* @throws {@link HttpError}: Other non-success status code
4344
*/
44-
async readMeta() { await this.send(HttpMethod.Get); }
45+
async readMeta(signal?: AbortSignal) {
46+
await this.send(HttpMethod.Get, signal);
47+
}
4548
}

endpoints/generic/ETagEndpointBase.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,18 @@ export class ETagEndpointBase extends Endpoint implements CachingEndpoint {
2222

2323
/**
2424
* Performs an {@link HttpMethod.Put} request on the {@link uri} and caches the response if the server sends an {@link HttpHeader.ETag}.
25+
* @param signal Used to cancel the request.
2526
* @throws {@link BadRequestError}: {@link HttpStatusCode.BadRequest}
2627
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
2728
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
2829
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
2930
* @throws {@link HttpError}: Other non-success status code
3031
*/
31-
protected async getContent(): Promise<string> {
32+
protected async getContent(signal?: AbortSignal): Promise<string> {
3233
const headers = new Headers();
3334
if (this.responseCache?.eTag) headers.append(HttpHeader.IfNoneMatch, this.responseCache.eTag);
3435

35-
const response = await this.httpClient.send(this.uri, HttpMethod.Get, new Headers(headers));
36+
const response = await this.httpClient.send(this.uri, HttpMethod.Get, signal, new Headers(headers));
3637
if (response.status !== HttpStatusCode.NotModified || !this.responseCache?.content) {
3738
await this.handle(response);
3839
this.responseCache = await ResponseCache.from(response);
@@ -43,36 +44,38 @@ export class ETagEndpointBase extends Endpoint implements CachingEndpoint {
4344

4445
/**
4546
* Performs an {@link HttpMethod.Put} request on the {@link uri}. Sets {@link HttpHeader.IfMatch} if there is a cached {@link HttpHeader.ETag} to detect lost updates.
47+
* @param signal Used to cancel the request.
4648
* @throws {@link ConcurrencyError}: The entity has changed since it was last retrieved with {@link getContent}. Your changes were rejected to prevent a lost update.
4749
* @throws {@link BadRequestError}: {@link HttpStatusCode.BadRequest}
4850
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
4951
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
5052
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
5153
* @throws {@link HttpError}: Other non-success status code
5254
*/
53-
protected async putContent(content: any): Promise<Response> {
55+
protected async putContent(content: any, signal?: AbortSignal): Promise<Response> {
5456
const headers = new Headers();
5557
headers.append(HttpHeader.ContentType, this.serializer.supportedMediaTypes[0])
5658
if (this.responseCache?.eTag) headers.append(HttpHeader.IfMatch, this.responseCache.eTag);
5759

5860
this.responseCache = undefined;
59-
return this.send(HttpMethod.Put, headers, this.serializer.serialize(content));
61+
return this.send(HttpMethod.Put, signal, headers, this.serializer.serialize(content));
6062
}
6163

6264
/**
6365
* Performs an {@link HttpMethod.Delete} request on the {@link uri}. Sets {@link HttpHeader.IfMatch} if there is a cached {@link HttpHeader.ETag} to detect lost updates.
66+
* @param signal Used to cancel the request.
6467
* @throws {@link ConcurrencyError}: The entity has changed since it was last retrieved with {@link getContent}. Your changes were rejected to prevent a lost update.
6568
* @throws {@link BadRequestError}: {@link HttpStatusCode.BadRequest}
6669
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
6770
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
6871
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
6972
* @throws {@link HttpError}: Other non-success status code
7073
*/
71-
protected async deleteContent(): Promise<Response> {
74+
protected async deleteContent(signal?: AbortSignal): Promise<Response> {
7275
const headers = new Headers();
7376
if (this.responseCache?.eTag) headers.append(HttpHeader.IfMatch, this.responseCache.eTag);
7477

7578
this.responseCache = undefined;
76-
return this.send(HttpMethod.Delete, headers);
79+
return this.send(HttpMethod.Delete, signal, headers);
7780
}
7881
}

endpoints/generic/ElementEndpoint.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ export class ElementEndpoint<TEntity> extends ETagEndpointBase {
2828

2929
/**
3030
* Returns the `TEntity`.
31+
* @param signal Used to cancel the request.
3132
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
3233
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
3334
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
3435
* @throws {@link HttpError}: Other non-success status code
3536
*/
36-
async read() { return this.serializer.deserialize<TEntity>(await this.getContent()); }
37+
async read(signal?: AbortSignal) {
38+
return this.serializer.deserialize<TEntity>(await this.getContent(signal));
39+
}
3740

3841
/**
3942
* Determines whether the element currently exists.
@@ -86,6 +89,7 @@ export class ElementEndpoint<TEntity> extends ETagEndpointBase {
8689
/**
8790
* Modifies an existing `TEntity` by merging changes on the server-side.
8891
* @param entity The `TEntity` data to merge with the existing element.
92+
* @param signal Used to cancel the request.
8993
* @returns The `TEntity` as returned by the server, possibly with additional fields set. undefined if the server does not respond with a result entity.
9094
* @throws {@link ConcurrencyError}: The entity has changed since it was last retrieved with {@link read}. Your changes were rejected to prevent a lost update.
9195
* @throws {@link BadRequestError}: {@link HttpStatusCode.BadRequest}
@@ -94,9 +98,9 @@ export class ElementEndpoint<TEntity> extends ETagEndpointBase {
9498
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
9599
* @throws {@link HttpError}: Other non-success status code
96100
*/
97-
async merge(entity: TEntity): Promise<(TEntity | undefined)> {
101+
async merge(entity: TEntity, signal?: AbortSignal): Promise<(TEntity | undefined)> {
98102
this.responseCache = undefined;
99-
const response = await this.send(HttpMethod.Patch, {
103+
const response = await this.send(HttpMethod.Patch, signal, {
100104
[HttpHeader.ContentType]: this.serializer.supportedMediaTypes[0]
101105
}, this.serializer.serialize(entity));
102106

endpoints/generic/GenericCollectionEndpoint.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,13 @@ export class GenericCollectionEndpoint<TEntity, TElementEndpoint extends Element
4949

5050
/**
5151
* Returns all `TEntity`s in the collection.
52+
* @param signal Used to cancel the request.
5253
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
5354
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
5455
* @throws {@link ConflictError}: {@link HttpStatusCode.Conflict}
5556
* @throws {@link HttpError}: Other non-success status code
5657
*/
57-
async readAll() {
58+
async readAll(signal?: AbortSignal) {
5859
return this.serializer.deserialize<TEntity[]>(await this.getContent(signal));
5960
}
6061

@@ -68,15 +69,16 @@ export class GenericCollectionEndpoint<TEntity, TElementEndpoint extends Element
6869
/**
6970
* Adds a `TEntity` as a new element to the collection.
7071
* @param entity The new `TEntity`.
72+
* @param signal Used to cancel the request.
7173
* @returns The `TEntity` as returned by the server, possibly with additional fields set. undefined if the server does not respond with a result entity.
7274
* @throws {@link BadRequestError}: {@link HttpStatusCode.BadRequest}
7375
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
7476
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
7577
* @throws {@link ConflictError}: {@link HttpStatusCode.Conflict}
7678
* @throws {@link HttpError}: Other non-success status code
7779
*/
78-
async create(entity: TEntity): Promise<TElementEndpoint> {
79-
const response = await this.send(HttpMethod.Post, {
80+
async create(entity: TEntity, signal?: AbortSignal): Promise<TElementEndpoint> {
81+
const response = await this.send(HttpMethod.Post, signal, {
8082
[HttpHeader.ContentType]: this.serializer.supportedMediaTypes[0]
8183
}, this.serializer.serialize(entity));
8284

@@ -98,14 +100,15 @@ export class GenericCollectionEndpoint<TEntity, TElementEndpoint extends Element
98100
/**
99101
* Adds (or updates) multiple `TEntity`s as elements in the collection.
100102
* @param entities The `TEntity`s to create or modify.
103+
* @param signal Used to cancel the request.
101104
* @throws {@link BadRequestError}: {@link HttpStatusCode.BadRequest}
102105
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
103106
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
104107
* @throws {@link ConflictError}: {@link HttpStatusCode.Conflict}
105108
* @throws {@link HttpError}: Other non-success status code
106109
*/
107-
async createAll(entities: TEntity[]) {
108-
await this.send(HttpMethod.Patch, {
110+
async createAll(entities: TEntity[], signal?: AbortSignal) {
111+
await this.send(HttpMethod.Patch, signal, {
109112
[HttpHeader.ContentType]: this.serializer.supportedMediaTypes[0]
110113
}, this.serializer.serialize(entities));
111114
}

endpoints/raw/BlobEndpoint.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ export class BlobEndpoint extends Endpoint {
1313

1414
/**
1515
* Queries the server about capabilities of the endpoint without performing any action.
16+
* @param signal Used to cancel the request.
1617
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
1718
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
1819
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
1920
* @throws {@link HttpError}: Other non-success status code
2021
*/
21-
async probe() { await this.send(HttpMethod.Options); }
22+
async probe(signal?: AbortSignal) {
23+
await this.send(HttpMethod.Options, signal);
24+
}
2225

2326
/**
2427
* Shows whether the server has indicated that {@link download} is currently allowed.
@@ -29,14 +32,15 @@ export class BlobEndpoint extends Endpoint {
2932

3033
/**
3134
* Downloads the blob's content.
35+
* @param signal Used to cancel the request.
3236
* @throws {@link BadRequestError}: {@link HttpStatusCode.BadRequest}
3337
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
3438
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
3539
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
3640
* @throws {@link HttpError}: Other non-success status code
3741
*/
38-
async download(): Promise<Blob> {
39-
const response = await this.send(HttpMethod.Get);
42+
async download(signal?: AbortSignal): Promise<Blob> {
43+
const response = await this.send(HttpMethod.Get, signal);
4044
return response.blob();
4145
}
4246

@@ -50,12 +54,15 @@ export class BlobEndpoint extends Endpoint {
5054
/**
5155
* Uploads data as the blob's content.
5256
* @param blob The blob to read the upload data from.
57+
* @param signal Used to cancel the request.
5358
* @throws {@link BadRequestError}: {@link HttpStatusCode.BadRequest}
5459
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
5560
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
5661
* @throws {@link HttpError}: Other non-success status code
5762
*/
58-
async upload(blob: Blob) { await this.send(HttpMethod.Put, { [HttpHeader.ContentType]: blob.type }, blob); }
63+
async upload(blob: Blob, signal?: AbortSignal) {
64+
await this.send(HttpMethod.Put, signal, { [HttpHeader.ContentType]: blob.type }, blob);
65+
}
5966

6067
/**
6168
* Shows whether the server has indicated that {@link delete} is currently allowed.
@@ -66,11 +73,14 @@ export class BlobEndpoint extends Endpoint {
6673

6774
/**
6875
* Deletes the blob from the server.
76+
* @param signal Used to cancel the request.
6977
* @throws {@link BadRequestError}: {@link HttpStatusCode.BadRequest}
7078
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
7179
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
7280
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
7381
* @throws {@link HttpError}: Other non-success status code
7482
*/
75-
async delete() { await this.send(HttpMethod.Delete); }
83+
async delete(signal?: AbortSignal) {
84+
await this.send(HttpMethod.Delete, signal);
85+
}
7686
}

endpoints/raw/UploadEndpoint.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,20 @@ export class UploadEndpoint extends Endpoint {
1717
* Uploads data to the endpoint.
1818
* @param blob The blob to read the upload data from.
1919
* @param fileName The name of the uploaded file.
20+
* @param signal Used to cancel the request.
2021
* @throws {@link BadRequestError}: {@link HttpStatusCode.BadRequest}
2122
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
2223
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
2324
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
2425
* @throws {@link HttpError}: Other non-success status code
2526
*/
26-
async upload(blob: Blob, fileName?: string) {
27+
async upload(blob: Blob, fileName?: string, signal?: AbortSignal) {
2728
if (this.formField) {
2829
const formData = new FormData();
2930
formData.set(this.formField, blob, fileName);
30-
await this.send(HttpMethod.Post, { [HttpHeader.ContentType]: "multipart/form-data" }, formData);
31+
await this.send(HttpMethod.Post, signal, { [HttpHeader.ContentType]: "multipart/form-data" }, formData);
3132
} else {
32-
await this.send(HttpMethod.Post, { [HttpHeader.ContentType]: blob.type }, blob);
33+
await this.send(HttpMethod.Post, signal, { [HttpHeader.ContentType]: blob.type }, blob);
3334
}
3435
}
3536
}

endpoints/rpc/ActionEndpoint.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ export class ActionEndpoint extends RpcEndpointBase {
1717

1818
/**
1919
* Invokes the action.
20+
* @param signal Used to cancel the request.
2021
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
2122
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
2223
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
2324
* @throws {@link HttpError}: Other non-success status code
2425
*/
25-
async invoke() { await this.send(HttpMethod.Post); }
26+
async invoke(signal?: AbortSignal) {
27+
await this.send(HttpMethod.Post, signal);
28+
}
2629
}

endpoints/rpc/ConsumerEndpoint.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ export class ConsumerEndpoint<TEntity> extends RpcEndpointBase {
1919
/**
2020
* Sends the entity to the consumer.
2121
* @param entity The `TEntity` to post as input.
22+
* @param signal Used to cancel the request.
2223
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
2324
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
2425
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
2526
* @throws {@link HttpError}: Other non-success status code
2627
*/
27-
async invoke(entity: TEntity) {
28-
await this.send(HttpMethod.Post, {
28+
async invoke(entity: TEntity, signal?: AbortSignal) {
29+
await this.send(HttpMethod.Post, signal, {
2930
[HttpHeader.ContentType]: this.serializer.supportedMediaTypes[0]
3031
}, this.serializer.serialize(entity));
3132
}

endpoints/rpc/FunctionEndpoint.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ export class FunctionEndpoint<TEntity, TResult> extends RpcEndpointBase {
2020
/**
2121
* Invokes the function.
2222
* @param entity The `TEntity` to post as input.
23+
* @param signal Used to cancel the request.
2324
* @returns The `TResult` returned by the server.
2425
* @throws {@link AuthenticationError}: {@link HttpStatusCode.Unauthorized}
2526
* @throws {@link AuthorizationError}: {@link HttpStatusCode.Forbidden}
2627
* @throws {@link NotFoundError}: {@link HttpStatusCode.NotFound} or {@link HttpStatusCode.Gone}
2728
* @throws {@link HttpError}: Other non-success status code
2829
*/
29-
async invoke(entity: TEntity): Promise<TResult> {
30-
const response = await this.send(HttpMethod.Post, {
30+
async invoke(entity: TEntity, signal?: AbortSignal): Promise<TResult> {
31+
const response = await this.send(HttpMethod.Post, signal, {
3132
[HttpHeader.ContentType]: this.serializer.supportedMediaTypes[0]
3233
}, this.serializer.serialize(entity));
3334

0 commit comments

Comments
 (0)