1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const fetcher_1 = require("./http/fetcher");
|
4 | const resource_1 = require("./resource");
|
5 | const state_1 = require("./state");
|
6 | const util_1 = require("./http/util");
|
7 | const uri_1 = require("./util/uri");
|
8 | const cache_1 = require("./cache");
|
9 | const LinkHeader = require("http-link-header");
|
10 | class Client {
|
11 | constructor(bookmarkUri) {
|
12 | |
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
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 |
|
40 |
|
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 |
|
53 | return response;
|
54 | }
|
55 |
|
56 | const expireUris = [];
|
57 | if (!noStaleEvent && request.method !== 'DELETE') {
|
58 |
|
59 | expireUris.push(request.url);
|
60 | }
|
61 |
|
62 |
|
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 |
|
70 | if (response.headers.has('Location')) {
|
71 | expireUris.push(uri_1.resolve(request.url, response.headers.get('Location')));
|
72 | }
|
73 |
|
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 |
|
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 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 | follow(rel, variables) {
|
109 | return this.go().follow(rel, variables);
|
110 | }
|
111 | |
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
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 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 | use(middleware, origin = '*') {
|
150 | this.fetcher.use(middleware, origin);
|
151 | }
|
152 | |
153 |
|
154 |
|
155 | clearCache() {
|
156 | this.cache.clear();
|
157 | }
|
158 | |
159 |
|
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 |
|
172 | state = await state_1.textStateFactory(uri, response);
|
173 | }
|
174 | else if (contentType.match(/^application\/[A-Za-z-.]+\+json/)) {
|
175 |
|
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 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 | cacheState(state) {
|
191 | this.cache.store(state);
|
192 | const resource = this.resources.get(state.uri);
|
193 | if (resource) {
|
194 |
|
195 | resource.emit('update', state);
|
196 | }
|
197 | for (const embeddedState of state.getEmbedded()) {
|
198 |
|
199 | this.cacheState(embeddedState);
|
200 | }
|
201 | }
|
202 | }
|
203 | exports.default = Client;
|
204 |
|
\ | No newline at end of file |