From 3153be8cdb0a1fa8b7cf6977424655446f842ea8 Mon Sep 17 00:00:00 2001 From: Evert Pot Date: Sun, 3 Nov 2019 22:14:02 -0500 Subject: [PATCH 1/4] Prefer-push is opt-in and more reliable --- src/follower.ts | 13 +++++++++++++ src/resource.ts | 47 +++++++++++++++++++++++------------------------ 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/follower.ts b/src/follower.ts index b0179e42..45895de1 100644 --- a/src/follower.ts +++ b/src/follower.ts @@ -7,12 +7,18 @@ import { LinkVariables } from './types'; abstract class Follower implements PromiseLike { protected prefetchEnabled: boolean; + protected preferPushEnabled: boolean; preFetch(): this { this.prefetchEnabled = true; return this; } + preferPush(): this { + this.preferPushEnabled = true; + return this; + } + abstract then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): PromiseLike; abstract catch(onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): PromiseLike; @@ -103,6 +109,9 @@ export class FollowerOne extends Follower> { private async fetchLinkedResource(): Promise> { const resource = await this.resource; + if (this.preferPushEnabled) { + resource.addNextRefreshHeader('Prefer-Push', this.rel); + } const link = await resource.link(this.rel); let href; @@ -172,6 +181,10 @@ export class FollowerMany extends Follower>> { private async fetchLinkedResources(): Promise>> { const resource = await this.resource; + if (this.preferPushEnabled) { + resource.addNextRefreshHeader('Prefer-Push', this.rel); + } + const links = await resource.links(this.rel); let href; diff --git a/src/resource.ts b/src/resource.ts index d27079ce..bd0ec3f1 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -43,18 +43,11 @@ export default class Resource> { * be used to set this value. * * It's also possible for resources to get a mimetype through a link. - * - * If the mimetype was "null" when doing the request, the chosen mimetype - * will come from the first item in Client.resourceTypes */ contentType: string | null; private inFlightRefresh: Promise | null = null; - /** - * A list of rels that should be added to a Prefer-Push header. - */ - private preferPushRels: Set; constructor(client: Ketting, uri: string, contentType: string | null = null) { @@ -62,7 +55,7 @@ export default class Resource> { this.uri = uri; this.repr = null; this.contentType = contentType; - this.preferPushRels = new Set(); + this.nextRefreshHeaders = {}; } @@ -181,14 +174,10 @@ export default class Resource> { if (!this.inFlightRefresh) { const headers: { [name: string]: string } = { - Accept: this.contentType ? this.contentType : this.client.representorHelper.getAcceptHeader() + Accept: this.contentType ? this.contentType : this.client.representorHelper.getAcceptHeader(), + ...this.nextRefreshHeaders, }; - if (this.preferPushRels.size > 0) { - headers['Prefer-Push'] = Array.from(this.preferPushRels).join(' '); - headers.Prefer = 'transclude="' + Array.from(this.preferPushRels).join(';') + '"'; - } - this.inFlightRefresh = this.fetchAndThrow({ method: 'GET' , headers @@ -205,6 +194,7 @@ export default class Resource> { await this.inFlightRefresh; } finally { this.inFlightRefresh = null; + this.nextRefreshHeaders = {}; } } else { @@ -247,11 +237,6 @@ export default class Resource> { async links(rel?: string): Promise { const r = await this.representation(); - - // After we got a representation, it no longer makes sense to remember - // the rels we want to add to Prefer-Push. - this.preferPushRels = new Set(); - return r.getLinks(rel); } @@ -269,11 +254,6 @@ export default class Resource> { async link(rel: string): Promise { const r = await this.representation(); - - // After we got a representation, it no longer makes sense to remember - // the rels we want to add to Prefer-Push. - this.preferPushRels = new Set(); - return r.getLink(rel); } @@ -437,4 +417,23 @@ export default class Resource> { } + + /** + * A set of HTTP headers that will be sent along with the next call to Refresh() + */ + private nextRefreshHeaders: { [name: string]: string }; + + /** + * When a HTTP header gets added here, it will automatically get sent along + * to the next call to refresh(). + * + * This means that the next time a GET request is done, these headers will be + * added. This list gets cleared after the GET request. + */ + addNextRefreshHeader(name: string, value: string): void { + + this.nextRefreshHeaders[name] = value; + + } + } From c605f9715de4e2e013adfd5d4ae51e499897dae5 Mon Sep 17 00:00:00 2001 From: Evert Pot Date: Sun, 3 Nov 2019 22:47:34 -0500 Subject: [PATCH 2/4] Simplify refreshing --- src/resource.ts | 78 +++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 44 deletions(-) diff --git a/src/resource.ts b/src/resource.ts index bd0ec3f1..089d4733 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -46,8 +46,7 @@ export default class Resource> { */ contentType: string | null; - private inFlightRefresh: Promise | null = null; - + private inFlightRefresh: Promise | null = null; constructor(client: Ketting, uri: string, contentType: string | null = null) { @@ -166,65 +165,56 @@ export default class Resource> { */ async refresh(): Promise { - let response: Response; - let body: string; + if (this.inFlightRefresh) { + return this.inFlightRefresh; + } - // If 2 systems request a 'refresh' at the exact same time, this mechanism - // will coalesc them into one. - if (!this.inFlightRefresh) { + const refreshFunc = async (): Promise => { const headers: { [name: string]: string } = { Accept: this.contentType ? this.contentType : this.client.representorHelper.getAcceptHeader(), ...this.nextRefreshHeaders, }; - this.inFlightRefresh = this.fetchAndThrow({ + const response = await this.fetchAndThrow({ method: 'GET' , headers - }).then( result1 => { - response = result1; - return response.text(); - }) - .then( result2 => { - body = result2; - return [response, body]; }); - try { - await this.inFlightRefresh; - } finally { - this.inFlightRefresh = null; - this.nextRefreshHeaders = {}; - } + this.nextRefreshHeaders = {}; + this.inFlightRefresh = null; - } else { - // Something else asked for refresh, so we piggypack on it. - [response, body] = await this.inFlightRefresh; + const body = await response.text(); - } + this.repr = this.client.representorHelper.createFromResponse( + this.uri, + response!, + body!, + ) as any as Representator; + + if (!this.contentType) { + this.contentType = this.repr.contentType; + } - this.repr = this.client.representorHelper.createFromResponse( - this.uri, - response!, - body!, - ) as any as Representator; + for (const [subUri, subBody] of Object.entries(this.repr.getEmbedded())) { + const subResource = this.go(subUri); + subResource.repr = this.client.representorHelper.create( + subUri, + this.repr.contentType, + null, + new Map(), + ); + subResource.repr.setBody(subBody); + } - if (!this.contentType) { - this.contentType = this.repr.contentType; - } + return this.repr.getBody(); - for (const [subUri, subBody] of Object.entries(this.repr.getEmbedded())) { - const subResource = this.go(subUri); - subResource.repr = this.client.representorHelper.create( - subUri, - this.repr.contentType, - null, - new Map(), - ); - subResource.repr.setBody(subBody); - } + }; + + const refreshResult = refreshFunc(); + this.inFlightRefresh = refreshResult; - return this.repr.getBody(); + return refreshResult; } From 6e3b8452ee7b18d8abbfed456893e252e1b1716b Mon Sep 17 00:00:00 2001 From: Evert Pot Date: Mon, 4 Nov 2019 10:01:55 -0500 Subject: [PATCH 3/4] Don't need those ! anymore --- src/resource.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resource.ts b/src/resource.ts index 089d4733..692bcc84 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -188,8 +188,8 @@ export default class Resource> { this.repr = this.client.representorHelper.createFromResponse( this.uri, - response!, - body!, + response, + body, ) as any as Representator; if (!this.contentType) { From 69ef93bbc3ea1c1be442d6983fd35fee2f4ab997 Mon Sep 17 00:00:00 2001 From: Evert Pot Date: Mon, 4 Nov 2019 10:02:25 -0500 Subject: [PATCH 4/4] refresh doesn't need to be async anymore --- src/resource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resource.ts b/src/resource.ts index 692bcc84..b5163f72 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -163,7 +163,7 @@ export default class Resource> { * This function will return the a parsed JSON object, like the get * function does. */ - async refresh(): Promise { + refresh(): Promise { if (this.inFlightRefresh) { return this.inFlightRefresh;