ketting 7.0.1 → 7.3.0
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.
- package/LICENSE +1 -1
- package/browser/ketting.min.js +1 -1
- package/browser/ketting.min.js.map +1 -1
- package/dist/client.d.ts +24 -4
- package/dist/client.js +96 -29
- package/dist/client.js.map +1 -1
- package/dist/http/error.js +1 -1
- package/dist/http/error.js.map +1 -1
- package/dist/http/fetch-polyfill.js.map +1 -1
- package/dist/http/fetcher.js +3 -3
- package/dist/http/fetcher.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/link.js +2 -2
- package/dist/link.js.map +1 -1
- package/dist/middlewares/accept-header.js +1 -1
- package/dist/middlewares/accept-header.js.map +1 -1
- package/dist/middlewares/cache.js +30 -25
- package/dist/middlewares/cache.js.map +1 -1
- package/dist/middlewares/warning.js +2 -1
- package/dist/middlewares/warning.js.map +1 -1
- package/dist/resource.d.ts +1 -1
- package/dist/resource.js +6 -6
- package/dist/resource.js.map +1 -1
- package/dist/src/client.d.ts +24 -4
- package/dist/src/index.d.ts +3 -1
- package/dist/src/resource.d.ts +1 -1
- package/dist/state/base-state.js +6 -4
- package/dist/state/base-state.js.map +1 -1
- package/dist/state/binary.js +1 -1
- package/dist/state/binary.js.map +1 -1
- package/dist/state/collection-json.js +1 -1
- package/dist/state/collection-json.js.map +1 -1
- package/dist/state/hal.js +13 -9
- package/dist/state/hal.js.map +1 -1
- package/dist/state/head.js +1 -1
- package/dist/state/head.js.map +1 -1
- package/dist/state/html.js +3 -3
- package/dist/state/html.js.map +1 -1
- package/dist/state/jsonapi.js +1 -1
- package/dist/state/jsonapi.js.map +1 -1
- package/dist/state/siren.js +3 -3
- package/dist/state/siren.js.map +1 -1
- package/dist/state/text.js +1 -1
- package/dist/state/text.js.map +1 -1
- package/dist/util/html.js +2 -2
- package/dist/util/html.js.map +1 -1
- package/dist/util/html.web.js +2 -2
- package/dist/util/html.web.js.map +1 -1
- package/dist/util/uri-template.js +1 -1
- package/dist/util/uri-template.js.map +1 -1
- package/dist/util/uri.js +3 -3
- package/dist/util/uri.js.map +1 -1
- package/dist/util/uri.web.js +1 -1
- package/dist/util/uri.web.js.map +1 -1
- package/package.json +11 -10
- package/src/action.ts +2 -2
- package/src/client.ts +119 -34
- package/src/field.ts +9 -9
- package/src/http/error.ts +7 -7
- package/src/http/fetch-polyfill.ts +4 -4
- package/src/http/fetcher.ts +3 -3
- package/src/index.ts +3 -5
- package/src/link.ts +10 -10
- package/src/middlewares/accept-header.ts +1 -1
- package/src/middlewares/cache.ts +31 -24
- package/src/middlewares/warning.ts +1 -0
- package/src/resource.ts +8 -8
- package/src/state/base-state.ts +5 -3
- package/src/state/collection-json.ts +28 -28
- package/src/state/hal.ts +12 -7
- package/src/state/interface.ts +2 -2
- package/src/state/jsonapi.ts +9 -9
- package/src/state/siren.ts +23 -23
- package/src/types.ts +1 -1
- package/src/util/html.ts +10 -10
- package/src/util/html.web.ts +7 -7
- package/src/util/uri.ts +1 -1
- package/src/util/uri.web.ts +2 -2
- package/changelog.md +0 -894
package/src/client.ts
CHANGED
|
@@ -38,16 +38,16 @@ export default class Client {
|
|
|
38
38
|
* headers. Higher means higher priority.
|
|
39
39
|
*/
|
|
40
40
|
contentTypeMap: {
|
|
41
|
-
[mimeType: string]: [StateFactory<any>, string]
|
|
41
|
+
[mimeType: string]: [StateFactory<any>, string];
|
|
42
42
|
} = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
'application/prs.hal-forms+json': [halStateFactory, '1.0'],
|
|
44
|
+
'application/hal+json': [halStateFactory, '0.9'],
|
|
45
|
+
'application/vnd.api+json': [jsonApiStateFactory, '0.8'],
|
|
46
|
+
'application/vnd.siren+json': [sirenStateFactory, '0.8'],
|
|
47
|
+
'application/vnd.collection+json': [cjStateFactory, '0.8'],
|
|
48
|
+
'application/json': [halStateFactory, '0.7'],
|
|
49
|
+
'text/html': [htmlStateFactory, '0.6'],
|
|
50
|
+
};
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* The cache for 'State' objects
|
|
@@ -141,6 +141,83 @@ export default class Client {
|
|
|
141
141
|
clearCache() {
|
|
142
142
|
|
|
143
143
|
this.cache.clear();
|
|
144
|
+
this.cacheDependencies = new Map();
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Caches a State object
|
|
150
|
+
*
|
|
151
|
+
* This function will also emit 'update' events to resources, and store all
|
|
152
|
+
* embedded states.
|
|
153
|
+
*/
|
|
154
|
+
cacheState(state: State) {
|
|
155
|
+
|
|
156
|
+
this.cache.store(state);
|
|
157
|
+
const resource = this.resources.get(state.uri);
|
|
158
|
+
if (resource) {
|
|
159
|
+
// We have a resource for this object, notify it as well.
|
|
160
|
+
resource.emit('update', state);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for(const embeddedState of state.getEmbedded()) {
|
|
164
|
+
// Recursion. MADNESS
|
|
165
|
+
this.cacheState(embeddedState);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* cacheDependencies contains all cache relationships between
|
|
172
|
+
* resources.
|
|
173
|
+
*
|
|
174
|
+
* This lets a user (for example) let a resource automatically
|
|
175
|
+
* expire, if another one expires.
|
|
176
|
+
*
|
|
177
|
+
* A server can populate this list using the `inv-by' link.
|
|
178
|
+
*/
|
|
179
|
+
public cacheDependencies: Map<string, Set<string>> = new Map();
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Helper function for clearing the cache for a resource.
|
|
183
|
+
*
|
|
184
|
+
* This function will also emit the 'stale' event for resources that have
|
|
185
|
+
* subscribers, and handle any dependent resource caches.
|
|
186
|
+
*
|
|
187
|
+
* If any resources are specified in deletedUris, those will not
|
|
188
|
+
* receive 'stale' events, but 'delete' events instead.
|
|
189
|
+
*/
|
|
190
|
+
clearResourceCache(staleUris: string[], deletedUris: string[]) {
|
|
191
|
+
|
|
192
|
+
let stale = new Set<string>();
|
|
193
|
+
const deleted = new Set<string>();
|
|
194
|
+
for(const uri of staleUris) {
|
|
195
|
+
stale.add(resolve(this.bookmarkUri, uri));
|
|
196
|
+
}
|
|
197
|
+
for(const uri of deletedUris) {
|
|
198
|
+
stale.add(resolve(this.bookmarkUri, uri));
|
|
199
|
+
deleted.add(resolve(this.bookmarkUri, uri));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
stale = expandCacheDependencies(
|
|
203
|
+
new Set([...stale, ...deleted]),
|
|
204
|
+
this.cacheDependencies
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
for(const uri of stale) {
|
|
208
|
+
this.cache.delete(uri);
|
|
209
|
+
|
|
210
|
+
const resource = this.resources.get(uri);
|
|
211
|
+
if (resource) {
|
|
212
|
+
if (deleted.has(uri)) {
|
|
213
|
+
resource.emit('delete');
|
|
214
|
+
} else {
|
|
215
|
+
resource.emit('stale');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
}
|
|
144
221
|
|
|
145
222
|
}
|
|
146
223
|
|
|
@@ -151,48 +228,56 @@ export default class Client {
|
|
|
151
228
|
|
|
152
229
|
const contentType = parseContentType(response.headers.get('Content-Type')!);
|
|
153
230
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (!contentType) {
|
|
231
|
+
if (!contentType || response.status === 204) {
|
|
157
232
|
return binaryStateFactory(this, uri, response);
|
|
158
233
|
}
|
|
159
234
|
|
|
160
235
|
if (contentType in this.contentTypeMap) {
|
|
161
|
-
|
|
236
|
+
return this.contentTypeMap[contentType][0](this, uri, response);
|
|
162
237
|
} else if (contentType.startsWith('text/')) {
|
|
163
238
|
// Default to TextState for any format starting with text/
|
|
164
|
-
|
|
239
|
+
return textStateFactory(this, uri, response);
|
|
165
240
|
} else if (contentType.match(/^application\/[A-Za-z-.]+\+json/)) {
|
|
166
241
|
// Default to HalState for any format containing a pattern like application/*+json
|
|
167
|
-
|
|
242
|
+
return halStateFactory(this, uri, response);
|
|
168
243
|
} else {
|
|
169
|
-
|
|
244
|
+
return binaryStateFactory(this, uri, response);
|
|
170
245
|
}
|
|
171
246
|
|
|
172
|
-
return state;
|
|
173
|
-
|
|
174
247
|
}
|
|
175
248
|
|
|
176
|
-
/**
|
|
177
|
-
* Caches a State object
|
|
178
|
-
*
|
|
179
|
-
* This function will also emit 'update' events to resources, and store all
|
|
180
|
-
* embedded states.
|
|
181
|
-
*/
|
|
182
|
-
cacheState(state: State) {
|
|
183
249
|
|
|
184
|
-
|
|
185
|
-
const resource = this.resources.get(state.uri);
|
|
186
|
-
if (resource) {
|
|
187
|
-
// We have a resource for this object, notify it as well.
|
|
188
|
-
resource.emit('update', state);
|
|
189
|
-
}
|
|
250
|
+
}
|
|
190
251
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Find all dependencies for a given resource.
|
|
256
|
+
*
|
|
257
|
+
* For example, if
|
|
258
|
+
* * if resourceA depends on resourceB
|
|
259
|
+
* * and resourceB depends on resourceC
|
|
260
|
+
*
|
|
261
|
+
* Then if 'resourceC' expires, so should 'resourceA' and 'resourceB'.
|
|
262
|
+
*
|
|
263
|
+
* This function helps us find these dependencies recursively and guarding
|
|
264
|
+
* against recursive loops.
|
|
265
|
+
*/
|
|
266
|
+
function expandCacheDependencies(uris: Set<string>, dependencies: Map<string, Set<string>>, output?: Set<string>): Set<string> {
|
|
267
|
+
|
|
268
|
+
if (!output) output = new Set();
|
|
269
|
+
|
|
270
|
+
for(const uri of uris) {
|
|
271
|
+
|
|
272
|
+
if (!output.has(uri)) {
|
|
273
|
+
output.add(uri);
|
|
274
|
+
if (dependencies.has(uri)) {
|
|
275
|
+
expandCacheDependencies(dependencies.get(uri)!, dependencies, output);
|
|
276
|
+
}
|
|
194
277
|
}
|
|
195
278
|
|
|
196
279
|
}
|
|
197
280
|
|
|
281
|
+
return output;
|
|
282
|
+
|
|
198
283
|
}
|
package/src/field.ts
CHANGED
|
@@ -30,7 +30,7 @@ interface BaseField<T> {
|
|
|
30
30
|
*
|
|
31
31
|
* This is similar to the HTML5 "type" attribute on forms.
|
|
32
32
|
*/
|
|
33
|
-
type: string
|
|
33
|
+
type: string;
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* The current (pre-filed) value on the form.
|
|
@@ -81,15 +81,15 @@ interface BooleanField extends BaseField<boolean> {
|
|
|
81
81
|
* special features.
|
|
82
82
|
*/
|
|
83
83
|
interface BasicStringField extends BaseField<string> {
|
|
84
|
-
type: 'color' | 'email' | 'password' | 'search' | 'tel' | 'url'
|
|
84
|
+
type: 'color' | 'email' | 'password' | 'search' | 'tel' | 'url';
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
interface RangeStringField extends RangeField<string> {
|
|
88
|
-
type: 'date' | 'month' | 'time' | 'week'
|
|
88
|
+
type: 'date' | 'month' | 'time' | 'week';
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
interface DateTimeField extends RangeField<Date> {
|
|
92
|
-
type: 'datetime' | 'datetime-local'
|
|
92
|
+
type: 'datetime' | 'datetime-local';
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
interface HiddenField extends BaseField<string | number | null | boolean> {
|
|
@@ -115,7 +115,7 @@ export type OptionsDataSource = {
|
|
|
115
115
|
*
|
|
116
116
|
* If specified as a plain array, use array values as labels and values.
|
|
117
117
|
*/
|
|
118
|
-
options: Record<string, string> | string[]
|
|
118
|
+
options: Record<string, string> | string[];
|
|
119
119
|
} | {
|
|
120
120
|
/**
|
|
121
121
|
* If dataSource is specified, we'll grab the list of options from a
|
|
@@ -147,7 +147,7 @@ export type OptionsDataSource = {
|
|
|
147
147
|
* This is ignored if it doesn't apply for the media type.
|
|
148
148
|
*/
|
|
149
149
|
valueField?: string;
|
|
150
|
-
}
|
|
150
|
+
};
|
|
151
151
|
} | {
|
|
152
152
|
/**
|
|
153
153
|
* If 'linkSource' is specified, we assume that the value will be a URI.
|
|
@@ -158,7 +158,7 @@ export type OptionsDataSource = {
|
|
|
158
158
|
linkSource: {
|
|
159
159
|
href: string;
|
|
160
160
|
rel: string;
|
|
161
|
-
}
|
|
161
|
+
};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
/**
|
|
@@ -181,10 +181,10 @@ type SelectFieldMulti = BaseField<string> & {
|
|
|
181
181
|
|
|
182
182
|
|
|
183
183
|
interface TextField extends BaseField<string> {
|
|
184
|
-
type: 'text'
|
|
184
|
+
type: 'text';
|
|
185
185
|
minLength?: number;
|
|
186
186
|
maxLength?: number;
|
|
187
|
-
pattern?: RegExp
|
|
187
|
+
pattern?: RegExp;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
interface TextAreaField extends BaseField<string> {
|
package/src/http/error.ts
CHANGED
|
@@ -28,12 +28,12 @@ export class HttpError extends Error {
|
|
|
28
28
|
export class Problem extends HttpError {
|
|
29
29
|
|
|
30
30
|
body: {
|
|
31
|
-
type: string
|
|
32
|
-
title?: string
|
|
33
|
-
status: number
|
|
34
|
-
detail?: string
|
|
35
|
-
instance?: string
|
|
36
|
-
[x: string]: any
|
|
31
|
+
type: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
status: number;
|
|
34
|
+
detail?: string;
|
|
35
|
+
instance?: string;
|
|
36
|
+
[x: string]: any;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
constructor(response: Response, problemBody: Record<string, any>) {
|
|
@@ -65,7 +65,7 @@ export class Problem extends HttpError {
|
|
|
65
65
|
export default async function problemFactory(response: Response): Promise<HttpError | Problem> {
|
|
66
66
|
|
|
67
67
|
const contentType = response.headers.get('Content-Type');
|
|
68
|
-
if (contentType
|
|
68
|
+
if (contentType?.match(/^application\/problem\+json/i)) {
|
|
69
69
|
const problemBody = await response.json();
|
|
70
70
|
return new Problem(response, problemBody);
|
|
71
71
|
} else {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from 'node-fetch';
|
|
7
7
|
|
|
8
8
|
// Registering Fetch as a glboal polyfill
|
|
9
|
-
(
|
|
10
|
-
(
|
|
11
|
-
(
|
|
12
|
-
(
|
|
9
|
+
(global as any).fetch = nodeFetch;
|
|
10
|
+
(global as any).Request = Request;
|
|
11
|
+
(global as any).Headers = Headers;
|
|
12
|
+
(global as any).Response = Response;
|
package/src/http/fetcher.ts
CHANGED
|
@@ -15,7 +15,7 @@ export class Fetcher {
|
|
|
15
15
|
|
|
16
16
|
middlewares: [RegExp, FetchMiddleware][] = [];
|
|
17
17
|
|
|
18
|
-
advertiseKetting: boolean = true
|
|
18
|
+
advertiseKetting: boolean = true;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* A wrapper for MDN fetch()
|
|
@@ -49,10 +49,10 @@ export class Fetcher {
|
|
|
49
49
|
*/
|
|
50
50
|
getMiddlewaresByOrigin(origin: string): FetchMiddleware[] {
|
|
51
51
|
|
|
52
|
-
return this.middlewares.filter( ([regex
|
|
52
|
+
return this.middlewares.filter( ([regex]) => {
|
|
53
53
|
return regex.test(origin);
|
|
54
54
|
|
|
55
|
-
}).map( ([
|
|
55
|
+
}).map( ([, middleware]) => {
|
|
56
56
|
return middleware;
|
|
57
57
|
});
|
|
58
58
|
|
package/src/index.ts
CHANGED
|
@@ -15,11 +15,9 @@ export {
|
|
|
15
15
|
isState,
|
|
16
16
|
} from './state';
|
|
17
17
|
|
|
18
|
-
export {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
ShortCache,
|
|
22
|
-
} from './cache';
|
|
18
|
+
export { ForeverCache } from './cache/forever';
|
|
19
|
+
export { ShortCache } from './cache/short';
|
|
20
|
+
export { NeverCache } from './cache/never';
|
|
23
21
|
|
|
24
22
|
export { default as basicAuth } from './http/basic-auth';
|
|
25
23
|
export { default as bearerAuth } from './http/bearer-auth';
|
package/src/link.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type Link = {
|
|
|
5
5
|
/**
|
|
6
6
|
* Target URI
|
|
7
7
|
*/
|
|
8
|
-
href: string
|
|
8
|
+
href: string;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Context URI.
|
|
@@ -17,17 +17,17 @@ export type Link = {
|
|
|
17
17
|
/**
|
|
18
18
|
* Relation type
|
|
19
19
|
*/
|
|
20
|
-
rel: string
|
|
20
|
+
rel: string;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Link title
|
|
24
24
|
*/
|
|
25
|
-
title?: string
|
|
25
|
+
title?: string;
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Content type hint of the target resource
|
|
29
29
|
*/
|
|
30
|
-
type?: string
|
|
30
|
+
type?: string;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Anchor.
|
|
@@ -35,22 +35,22 @@ export type Link = {
|
|
|
35
35
|
* This describes where the link is linked from, from for example
|
|
36
36
|
* a fragment in the current document
|
|
37
37
|
*/
|
|
38
|
-
anchor?: string
|
|
38
|
+
anchor?: string;
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Language of the target resource
|
|
42
42
|
*/
|
|
43
|
-
hreflang?: string
|
|
43
|
+
hreflang?: string;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* HTML5 media attribute
|
|
47
47
|
*/
|
|
48
|
-
media?: string
|
|
48
|
+
media?: string;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* If templated is set to true, the href is a templated URI.
|
|
52
52
|
*/
|
|
53
|
-
templated?: boolean
|
|
53
|
+
templated?: boolean;
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Link hints, as defined in draft-nottingham-link-hint
|
|
@@ -67,7 +67,7 @@ type NewLink = Omit<Link, 'context'>;
|
|
|
67
67
|
*/
|
|
68
68
|
export class Links {
|
|
69
69
|
|
|
70
|
-
private store: Map<string, Link[]
|
|
70
|
+
private store: Map<string, Link[]>;
|
|
71
71
|
|
|
72
72
|
constructor(public defaultContext: string, links?: Link[] | Links) {
|
|
73
73
|
|
|
@@ -221,5 +221,5 @@ export class LinkNotFound extends Error {}
|
|
|
221
221
|
* A key->value map of variables to place in a templated link
|
|
222
222
|
*/
|
|
223
223
|
export type LinkVariables = {
|
|
224
|
-
[key: string]: string | number
|
|
224
|
+
[key: string]: string | number;
|
|
225
225
|
};
|
|
@@ -13,7 +13,7 @@ export default function(client: Client): FetchMiddleware {
|
|
|
13
13
|
|
|
14
14
|
if (!request.headers.has('Accept')) {
|
|
15
15
|
const acceptHeader = Object.entries(client.contentTypeMap).map(
|
|
16
|
-
([contentType, [
|
|
16
|
+
([contentType, [, q]]) => contentType + ';q=' + q
|
|
17
17
|
).join(', ');
|
|
18
18
|
request.headers.set('Accept', acceptHeader);
|
|
19
19
|
}
|
package/src/middlewares/cache.ts
CHANGED
|
@@ -29,6 +29,22 @@ export default function(client: Client): FetchMiddleware {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const response = await next(request);
|
|
32
|
+
|
|
33
|
+
// If the response had a Link: rel=inv-by header, it means that when the
|
|
34
|
+
// target uri's cache expires, the uri of this resource should also
|
|
35
|
+
// expire.
|
|
36
|
+
if (response.headers.has('Link')) {
|
|
37
|
+
for (const httpLink of LinkHeader.parse(response.headers.get('Link')!).rel('inv-by')) {
|
|
38
|
+
const uri = resolve(request.url, httpLink.uri);
|
|
39
|
+
if (client.cacheDependencies.has(uri)) {
|
|
40
|
+
client.cacheDependencies.get(uri)!.add(request.url);
|
|
41
|
+
} else {
|
|
42
|
+
client.cacheDependencies.set(uri, new Set([request.url]));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
32
48
|
if (isSafeMethod(request.method)) {
|
|
33
49
|
return response;
|
|
34
50
|
}
|
|
@@ -39,10 +55,13 @@ export default function(client: Client): FetchMiddleware {
|
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
// We just processed an unsafe method, lets notify all subsystems.
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
const stale = [];
|
|
59
|
+
const deleted = [];
|
|
60
|
+
|
|
61
|
+
if (request.method === 'DELETE') {
|
|
62
|
+
deleted.push(request.url);
|
|
63
|
+
} else if (!noStaleEvent) {
|
|
64
|
+
stale.push(request.url);
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
// If the response had a Link: rel=invalidate header, we want to
|
|
@@ -50,17 +69,22 @@ export default function(client: Client): FetchMiddleware {
|
|
|
50
69
|
if (response.headers.has('Link')) {
|
|
51
70
|
for (const httpLink of LinkHeader.parse(response.headers.get('Link')!).rel('invalidates')) {
|
|
52
71
|
const uri = resolve(request.url, httpLink.uri);
|
|
53
|
-
|
|
72
|
+
stale.push(uri);
|
|
54
73
|
}
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
// Location headers should also expire
|
|
58
77
|
if (response.headers.has('Location')) {
|
|
59
|
-
|
|
78
|
+
stale.push(
|
|
60
79
|
resolve(request.url, response.headers.get('Location')!)
|
|
61
80
|
);
|
|
62
81
|
}
|
|
63
|
-
|
|
82
|
+
|
|
83
|
+
client.clearResourceCache(stale, deleted);
|
|
84
|
+
|
|
85
|
+
// If the response had a 'Content-Location' header, it means that the
|
|
86
|
+
// response body is the _new_ state for the url in the content-location
|
|
87
|
+
// header, so we store it!
|
|
64
88
|
if (response.headers.has('Content-Location')) {
|
|
65
89
|
const cl = resolve(request.url, response.headers.get('Content-Location')!);
|
|
66
90
|
const clState = await client.getStateForResponse(
|
|
@@ -70,23 +94,6 @@ export default function(client: Client): FetchMiddleware {
|
|
|
70
94
|
client.cacheState(clState);
|
|
71
95
|
}
|
|
72
96
|
|
|
73
|
-
for (const uri of expireUris) {
|
|
74
|
-
client.cache.delete(request.url);
|
|
75
|
-
|
|
76
|
-
const resource = client.resources.get(uri);
|
|
77
|
-
if (resource) {
|
|
78
|
-
// We have a resource for this object, notify it as well.
|
|
79
|
-
resource.emit('stale');
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (request.method === 'DELETE') {
|
|
83
|
-
client.cache.delete(request.url);
|
|
84
|
-
const resource = client.resources.get(request.url);
|
|
85
|
-
if (resource) {
|
|
86
|
-
resource.emit('delete');
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
97
|
return response;
|
|
91
98
|
|
|
92
99
|
};
|
package/src/resource.ts
CHANGED
|
@@ -197,7 +197,7 @@ export class Resource<T = any> extends EventEmitter {
|
|
|
197
197
|
switch (response.status) {
|
|
198
198
|
case 201:
|
|
199
199
|
if (response.headers.has('location')) {
|
|
200
|
-
return this.go(
|
|
200
|
+
return this.go(response.headers.get('location')!);
|
|
201
201
|
}
|
|
202
202
|
throw new Error('Could not follow after a 201 request, because the server did not reply with a Location header. If you sent a Location header, check if your service is returning "Access-Control-Expose-Headers: Location".');
|
|
203
203
|
case 204 :
|
|
@@ -216,7 +216,7 @@ export class Resource<T = any> extends EventEmitter {
|
|
|
216
216
|
*
|
|
217
217
|
* If the server responds with 200 Status code this will return a State object
|
|
218
218
|
*/
|
|
219
|
-
async patch(options: PatchRequestOptions): Promise<
|
|
219
|
+
async patch(options: PatchRequestOptions): Promise<undefined | State<T>> {
|
|
220
220
|
|
|
221
221
|
const response = await this.fetchOrThrow(
|
|
222
222
|
optionsToRequestInit('PATCH', options)
|
|
@@ -307,7 +307,7 @@ export class Resource<T = any> extends EventEmitter {
|
|
|
307
307
|
*/
|
|
308
308
|
clearCache(): void {
|
|
309
309
|
|
|
310
|
-
this.client.
|
|
310
|
+
this.client.clearResourceCache([this.uri],[]);
|
|
311
311
|
|
|
312
312
|
}
|
|
313
313
|
|
|
@@ -385,7 +385,7 @@ export declare interface Resource<T = any> {
|
|
|
385
385
|
* It will also trigger when calling 'PUT' with a full state object,
|
|
386
386
|
* and when updateCache() was used.
|
|
387
387
|
*/
|
|
388
|
-
on(event: 'update', listener: (state: State) => void) : this
|
|
388
|
+
on(event: 'update', listener: (state: State) => void) : this;
|
|
389
389
|
|
|
390
390
|
/**
|
|
391
391
|
* Subscribe to the 'stale' event.
|
|
@@ -408,7 +408,7 @@ export declare interface Resource<T = any> {
|
|
|
408
408
|
* Subscribe to the 'update' event and unsubscribe after it was
|
|
409
409
|
* emitted the first time.
|
|
410
410
|
*/
|
|
411
|
-
once(event: 'update', listener: (state: State) => void) : this
|
|
411
|
+
once(event: 'update', listener: (state: State) => void) : this;
|
|
412
412
|
|
|
413
413
|
/**
|
|
414
414
|
* Subscribe to the 'stale' event and unsubscribe after it was
|
|
@@ -425,7 +425,7 @@ export declare interface Resource<T = any> {
|
|
|
425
425
|
/**
|
|
426
426
|
* Unsubscribe from the 'update' event
|
|
427
427
|
*/
|
|
428
|
-
off(event: 'update', listener: (state: State) => void) : this
|
|
428
|
+
off(event: 'update', listener: (state: State) => void) : this;
|
|
429
429
|
|
|
430
430
|
/**
|
|
431
431
|
* Unsubscribe from the 'stale' event
|
|
@@ -440,7 +440,7 @@ export declare interface Resource<T = any> {
|
|
|
440
440
|
/**
|
|
441
441
|
* Emit an 'update' event.
|
|
442
442
|
*/
|
|
443
|
-
emit(event: 'update', state: State) : boolean
|
|
443
|
+
emit(event: 'update', state: State) : boolean;
|
|
444
444
|
|
|
445
445
|
/**
|
|
446
446
|
* Emit a 'stale' event.
|
|
@@ -457,7 +457,7 @@ export declare interface Resource<T = any> {
|
|
|
457
457
|
export default Resource;
|
|
458
458
|
|
|
459
459
|
type StrictRequestInit = RequestInit & {
|
|
460
|
-
headers: Headers
|
|
460
|
+
headers: Headers;
|
|
461
461
|
};
|
|
462
462
|
|
|
463
463
|
/**
|
package/src/state/base-state.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { entityHeaderNames } from '../http/util';
|
|
|
9
9
|
|
|
10
10
|
type HeadStateInit = {
|
|
11
11
|
|
|
12
|
-
client: Client
|
|
12
|
+
client: Client;
|
|
13
13
|
uri: string;
|
|
14
14
|
links: Links;
|
|
15
15
|
|
|
@@ -20,7 +20,7 @@ type HeadStateInit = {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
type StateInit<T> = {
|
|
23
|
-
client: Client
|
|
23
|
+
client: Client;
|
|
24
24
|
uri: string;
|
|
25
25
|
data: T;
|
|
26
26
|
headers: Headers;
|
|
@@ -84,6 +84,7 @@ export class BaseHeadState implements HeadState {
|
|
|
84
84
|
href = resolve(link);
|
|
85
85
|
}
|
|
86
86
|
if (link.hints?.status === 'deprecated') {
|
|
87
|
+
/* eslint-disable-next-line no-console */
|
|
87
88
|
console.warn(`[ketting] The ${link.rel} link on ${this.uri} is marked deprecated.`, link);
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -102,6 +103,7 @@ export class BaseHeadState implements HeadState {
|
|
|
102
103
|
return this.links.getMany(rel).map( link => {
|
|
103
104
|
|
|
104
105
|
if (link.hints?.status === 'deprecated') {
|
|
106
|
+
/* eslint-disable-next-line no-console */
|
|
105
107
|
console.warn(`[ketting] The ${link.rel} link on ${this.uri} is marked deprecated.`, link);
|
|
106
108
|
}
|
|
107
109
|
const href = resolve(link);
|
|
@@ -171,7 +173,7 @@ export class BaseState<T> extends BaseHeadState implements State<T> {
|
|
|
171
173
|
}
|
|
172
174
|
for(const action of this.actionInfo) {
|
|
173
175
|
if (action.name === name) {
|
|
174
|
-
return new SimpleAction(this.client,
|
|
176
|
+
return new SimpleAction(this.client, action);
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
throw new ActionNotFound('This State defines no action');
|