UNPKG

7.86 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const fetcher_1 = require("./http/fetcher");
4const resource_1 = require("./resource");
5const state_1 = require("./state");
6const util_1 = require("./http/util");
7const uri_1 = require("./util/uri");
8const cache_1 = require("./cache");
9const LinkHeader = require("http-link-header");
10class Client {
11 constructor(bookmarkUri) {
12 /**
13 * Supported content types
14 *
15 * Each content-type has a 'factory' that turns a HTTP response
16 * into a State object.
17 *
18 * The last value in the array is the 'q=' value, used in Accept
19 * headers. Higher means higher priority.
20 */
21 this.contentTypeMap = {
22 'application/prs.hal-forms+json': [state_1.halStateFactory, '1.0'],
23 'application/hal+json': [state_1.halStateFactory, '0.9'],
24 'application/vnd.api+json': [state_1.jsonApiStateFactory, '0.8'],
25 'application/vnd.siren+json': [state_1.sirenStateFactory, '0.8'],
26 'application/vnd.collection+json': [state_1.cjStateFactory, '0.8'],
27 'application/json': [state_1.halStateFactory, '0.7'],
28 'text/html': [state_1.htmlStateFactory, '0.6'],
29 };
30 this.acceptHeader = (request, next) => {
31 if (!request.headers.has('Accept')) {
32 const acceptHeader = Object.entries(this.contentTypeMap).map(([contentType, [stateFactory, q]]) => contentType + ';q=' + q).join(', ');
33 request.headers.set('Accept', acceptHeader);
34 }
35 return next(request);
36 };
37 this.cacheExpireHandler = async (request, next) => {
38 /**
39 * Prevent a 'stale' event from being emitted, but only for the main
40 * uri
41 */
42 let noStaleEvent = false;
43 if (request.headers.has('X-KETTING-NO-STALE')) {
44 noStaleEvent = true;
45 request.headers.delete('X-KETTING-NO-STALE');
46 }
47 const response = await next(request);
48 if (util_1.isSafeMethod(request.method)) {
49 return response;
50 }
51 if (!response.ok) {
52 // There was an error, no cache changes
53 return response;
54 }
55 // We just processed an unsafe method, lets notify all subsystems.
56 const expireUris = [];
57 if (!noStaleEvent && request.method !== 'DELETE') {
58 // Sorry for the double negative
59 expireUris.push(request.url);
60 }
61 // If the response had a Link: rel=invalidate header, we want to
62 // expire those too.
63 if (response.headers.has('Link')) {
64 for (const httpLink of LinkHeader.parse(response.headers.get('Link')).rel('invalidates')) {
65 const uri = uri_1.resolve(request.url, httpLink.uri);
66 expireUris.push(uri);
67 }
68 }
69 // Location headers should also expire
70 if (response.headers.has('Location')) {
71 expireUris.push(uri_1.resolve(request.url, response.headers.get('Location')));
72 }
73 // Content-Location headers should also expire
74 if (response.headers.has('Content-Location')) {
75 expireUris.push(uri_1.resolve(request.url, response.headers.get('Content-Location')));
76 }
77 for (const uri of expireUris) {
78 this.cache.delete(request.url);
79 const resource = this.resources.get(uri);
80 if (resource) {
81 // We have a resource for this object, notify it as well.
82 resource.emit('stale');
83 }
84 }
85 if (request.method === 'DELETE') {
86 this.cache.delete(request.url);
87 const resource = this.resources.get(request.url);
88 if (resource) {
89 resource.emit('delete');
90 }
91 }
92 return response;
93 };
94 this.bookmarkUri = bookmarkUri;
95 this.fetcher = new fetcher_1.Fetcher();
96 this.fetcher.use(this.cacheExpireHandler);
97 this.fetcher.use(this.acceptHeader);
98 this.cache = new cache_1.ForeverCache();
99 this.resources = new Map();
100 }
101 /**
102 * Follows a relationship, based on its reltype. For example, this might be
103 * 'alternate', 'item', 'edit' or a custom url-based one.
104 *
105 * This function can also follow templated uris. You can specify uri
106 * variables in the optional variables argument.
107 */
108 follow(rel, variables) {
109 return this.go().follow(rel, variables);
110 }
111 /**
112 * Returns a resource by its uri.
113 *
114 * This function doesn't do any HTTP requests. The uri is optional. If it's
115 * not specified, it will return the bookmark resource.
116 *
117 * If a relative uri is passed, it will be resolved based on the bookmark
118 * uri.
119 *
120 * @example
121 * const res = ketting.go('https://example.org/);
122 * @example
123 * const res = ketting.go<Author>('/users/1');
124 * @example
125 * const res = ketting.go(); // bookmark
126 */
127 go(uri) {
128 let absoluteUri;
129 if (uri !== undefined) {
130 absoluteUri = uri_1.resolve(this.bookmarkUri, uri);
131 }
132 else {
133 absoluteUri = this.bookmarkUri;
134 }
135 if (!this.resources.has(absoluteUri)) {
136 const resource = new resource_1.default(this, absoluteUri);
137 this.resources.set(absoluteUri, resource);
138 return resource;
139 }
140 return this.resources.get(absoluteUri);
141 }
142 /**
143 * Adds a fetch middleware, which will be executed for
144 * each fetch() call.
145 *
146 * If 'origin' is specified, fetch middlewares can be executed
147 * only if the host/origin matches.
148 */
149 use(middleware, origin = '*') {
150 this.fetcher.use(middleware, origin);
151 }
152 /**
153 * Clears the entire state cache
154 */
155 clearCache() {
156 this.cache.clear();
157 }
158 /**
159 * Transforms a fetch Response to a State object.
160 */
161 async getStateForResponse(uri, response) {
162 const contentType = util_1.parseContentType(response.headers.get('Content-Type'));
163 let state;
164 if (!contentType) {
165 return state_1.binaryStateFactory(uri, response);
166 }
167 if (contentType in this.contentTypeMap) {
168 state = await this.contentTypeMap[contentType][0](uri, response);
169 }
170 else if (contentType.startsWith('text/')) {
171 // Default to TextState for any format starting with text/
172 state = await state_1.textStateFactory(uri, response);
173 }
174 else if (contentType.match(/^application\/[A-Za-z-.]+\+json/)) {
175 // Default to HalState for any format containing a pattern like application/*+json
176 state = await state_1.halStateFactory(uri, response);
177 }
178 else {
179 state = await state_1.binaryStateFactory(uri, response);
180 }
181 state.client = this;
182 return state;
183 }
184 /**
185 * Caches a State object
186 *
187 * This function will also emit 'update' events to resources, and store all
188 * embedded states.
189 */
190 cacheState(state) {
191 this.cache.store(state);
192 const resource = this.resources.get(state.uri);
193 if (resource) {
194 // We have a resource for this object, notify it as well.
195 resource.emit('update', state);
196 }
197 for (const embeddedState of state.getEmbedded()) {
198 // Recursion. MADNESS
199 this.cacheState(embeddedState);
200 }
201 }
202}
203exports.default = Client;
204//# sourceMappingURL=client.js.map
\No newline at end of file