@thepassle/app-tools 0.9.11 → 0.9.13
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/package.json +1 -1
- package/router/index.js +20 -14
- package/router/plugins/offline.js +2 -2
- package/types/router/types.d.ts +5 -4
- package/api/README.md +0 -443
- package/dialog/README.md +0 -252
- package/env/README.md +0 -77
- package/pwa/README.md +0 -103
- package/router/README.md +0 -502
- package/state/README.md +0 -25
- package/utils/README.md +0 -139
package/package.json
CHANGED
package/router/index.js
CHANGED
|
@@ -182,23 +182,29 @@ export class Router extends EventTarget {
|
|
|
182
182
|
/** @type {Plugin[]} */
|
|
183
183
|
let plugins = this._collectPlugins(route);
|
|
184
184
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
185
|
+
let redirecting;
|
|
186
|
+
do {
|
|
187
|
+
redirecting = false;
|
|
188
|
+
for (const plugin of plugins) {
|
|
189
|
+
try {
|
|
190
|
+
const result = await plugin?.shouldNavigate?.(this.context);
|
|
191
|
+
if (result) {
|
|
192
|
+
const condition = await result.condition();
|
|
193
|
+
if (!condition) {
|
|
194
|
+
url = new URL(result.redirect, this.baseUrl);
|
|
195
|
+
route = this._matchRoute(url) || this._matchRoute(this.fallback);
|
|
196
|
+
plugins = this._collectPlugins(route);
|
|
197
|
+
log("Redirecting", { context: this.context, route: this.route });
|
|
198
|
+
redirecting = true;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
195
201
|
}
|
|
202
|
+
} catch (e) {
|
|
203
|
+
log(`Plugin "${plugin.name}" error on shouldNavigate hook`, e);
|
|
204
|
+
throw e;
|
|
196
205
|
}
|
|
197
|
-
} catch(e) {
|
|
198
|
-
log(`Plugin "${plugin.name}" error on shouldNavigate hook`, e);
|
|
199
|
-
throw e;
|
|
200
206
|
}
|
|
201
|
-
}
|
|
207
|
+
} while (redirecting);
|
|
202
208
|
|
|
203
209
|
this.route = route;
|
|
204
210
|
|
|
@@ -7,10 +7,10 @@ export function offlinePlugin(offlineRoute = '/offline') {
|
|
|
7
7
|
return {
|
|
8
8
|
name: 'offline',
|
|
9
9
|
shouldNavigate: () => ({
|
|
10
|
-
condition: () =>
|
|
10
|
+
condition: () => navigator.onLine,
|
|
11
11
|
redirect: offlineRoute,
|
|
12
12
|
})
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export const offline = offlinePlugin();
|
|
16
|
+
export const offline = offlinePlugin();
|
package/types/router/types.d.ts
CHANGED
|
@@ -3,12 +3,13 @@ export interface Config {
|
|
|
3
3
|
plugins?: Plugin[];
|
|
4
4
|
routes: RouteDefinition[];
|
|
5
5
|
}
|
|
6
|
+
export interface ShouldNavigateResult {
|
|
7
|
+
redirect: string;
|
|
8
|
+
condition: (() => boolean) | (() => Promise<boolean>);
|
|
9
|
+
}
|
|
6
10
|
export interface Plugin {
|
|
7
11
|
name: string;
|
|
8
|
-
shouldNavigate?: (context: Context) =>
|
|
9
|
-
redirect: string;
|
|
10
|
-
condition: () => boolean | (() => Promise<Boolean>);
|
|
11
|
-
};
|
|
12
|
+
shouldNavigate?: ((context: Context) => ShouldNavigateResult) | ((context: Context) => Promise<ShouldNavigateResult>);
|
|
12
13
|
beforeNavigation?: (context: Context) => void;
|
|
13
14
|
afterNavigation?: (context: Context) => void;
|
|
14
15
|
}
|
package/api/README.md
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
# Api
|
|
2
|
-
|
|
3
|
-
Small wrapper around the native [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
npm i -S @thepassle/app-tools
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
|
-
```js
|
|
14
|
-
import { Api } from '@thepassle/app-tools/api.js';
|
|
15
|
-
|
|
16
|
-
/** Using defaults: */
|
|
17
|
-
const api = new Api();
|
|
18
|
-
|
|
19
|
-
/** Or with configuration: */
|
|
20
|
-
const api = new Api({
|
|
21
|
-
baseURL: 'https://api.foo.com',
|
|
22
|
-
responseType: 'text',
|
|
23
|
-
plugins: [
|
|
24
|
-
{
|
|
25
|
-
beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
|
|
26
|
-
afterFetch: (res) => res,
|
|
27
|
-
transform: (data) => data,
|
|
28
|
-
handleError: (e) => true
|
|
29
|
-
}
|
|
30
|
-
]
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const user = await api.get('/users/1');
|
|
34
|
-
await api.post('/form/submit', { name: 'John Doe', email: 'johndoe@internet.com' });
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
await api.get('/foo');
|
|
38
|
-
} catch(e) {
|
|
39
|
-
console.log(e); // StatusError
|
|
40
|
-
e.message; // the `statusText` of the `response`
|
|
41
|
-
e.response; // access the `response`
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### Error Handling
|
|
46
|
-
|
|
47
|
-
By default `api` will throw an error if a response is not ok (`!response.ok`). If this is the case, it will throw a `StatusError`. The `StatusError` is thrown with the `response.statusText` as message, and also has the actual `response` available on it: `e.response`.
|
|
48
|
-
|
|
49
|
-
## Composable
|
|
50
|
-
|
|
51
|
-
Use plugins to customize your requests to fit your needs
|
|
52
|
-
|
|
53
|
-
### `logger`
|
|
54
|
-
|
|
55
|
-
```js
|
|
56
|
-
import { logger, loggerPlugin } from '@thepassle/app-tools/api/plugins/logger.js';
|
|
57
|
-
|
|
58
|
-
/** Logs metadata to the console */
|
|
59
|
-
api.get(url, {plugins: [logger]});
|
|
60
|
-
|
|
61
|
-
/** Or */
|
|
62
|
-
const logger = loggerPlugin({collapsed: false});
|
|
63
|
-
api.get(url, {plugins: [logger]});
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### `cache`
|
|
67
|
-
|
|
68
|
-
```js
|
|
69
|
-
import { cache, cachePlugin } from '@thepassle/app-tools/api/plugins/cache.js';
|
|
70
|
-
|
|
71
|
-
/** Caches the response for a default of 10 minutes */
|
|
72
|
-
api.get(url, {plugins: [cache]});
|
|
73
|
-
|
|
74
|
-
/** Or */
|
|
75
|
-
const cache = cachePlugin({maxAge: 1000});
|
|
76
|
-
api.get(url, {plugins: [cache]});
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### `debounce`
|
|
80
|
-
|
|
81
|
-
```js
|
|
82
|
-
import { debounce, debouncePlugin } from '@thepassle/app-tools/api/plugins/debounce.js';
|
|
83
|
-
|
|
84
|
-
/** Debounces the response for a default of 1000 ms */
|
|
85
|
-
api.get(url, {plugins: [debounce]});
|
|
86
|
-
|
|
87
|
-
/** Or */
|
|
88
|
-
const debounce = debouncePlugin({maxAge: 2000});
|
|
89
|
-
api.get(url, {plugins: [debounce]});
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**Note:** The `debounce` plugin wraps the `fetchFn` in a debouncer. `await`ing the call will cause the debounce to be awaited. E.g.:
|
|
93
|
-
|
|
94
|
-
```js
|
|
95
|
-
api.get(url, {plugins: [debounce]}).then(() => { console.log(1) });
|
|
96
|
-
api.get(url, {plugins: [debounce]}).then(() => { console.log(2) });
|
|
97
|
-
|
|
98
|
-
// Output:
|
|
99
|
-
// 2
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
But awaiting it will become:
|
|
103
|
-
```js
|
|
104
|
-
await api.get(url, {plugins: [debounce]}).then(() => { console.log(1) });
|
|
105
|
-
await api.get(url, {plugins: [debounce]}).then(() => { console.log(2) });
|
|
106
|
-
|
|
107
|
-
// Output:
|
|
108
|
-
// 1
|
|
109
|
-
// 2
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### `abort`
|
|
113
|
-
|
|
114
|
-
```js
|
|
115
|
-
import { abort } from '@thepassle/app-tools/api/plugins/abort.js';
|
|
116
|
-
|
|
117
|
-
/** Aborts previous, unfinished requests via an AbortController if requests are fired in quick succession, like spammy clicks on buttons */
|
|
118
|
-
api.get(url, {plugins: [abort]});
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### `mock`, `delay`
|
|
122
|
-
|
|
123
|
-
```js
|
|
124
|
-
import { mock } from '@thepassle/app-tools/api/plugins/mock.js';
|
|
125
|
-
import { delay, delayPlugin } from '@thepassle/app-tools/api/plugins/delay.js';
|
|
126
|
-
|
|
127
|
-
/** Easily mock requests during development using the native `Response` object */
|
|
128
|
-
api.get(url, {
|
|
129
|
-
plugins: [
|
|
130
|
-
mock(() => new Response(JSON.stringify({foo: 'bar'}))),
|
|
131
|
-
delay // defaults to 1000ms
|
|
132
|
-
]
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
/** Or */
|
|
136
|
-
const delay = delayPlugin(2000);
|
|
137
|
-
api.get(url, {plugins: [delay]});
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### `jsonPrefix`
|
|
141
|
-
|
|
142
|
-
```js
|
|
143
|
-
import { jsonPrefix, jsonPrefixPlugin } from '@thepassle/app-tools/api/plugins/jsonPrefix.js';
|
|
144
|
-
|
|
145
|
-
/** Add plugins to run on all requests */
|
|
146
|
-
const api = new Api({ plugins: [jsonPrefix] });
|
|
147
|
-
|
|
148
|
-
/** Or */
|
|
149
|
-
const jsonPrefix = jsonPrefixPlugin('<prefix>');
|
|
150
|
-
const api = new Api({ plugins: [jsonPRefix] });
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### `xsrf`
|
|
154
|
-
|
|
155
|
-
```js
|
|
156
|
-
import { xsrf, xsrfPlugin } from '@thepassle/app-tools/api/plugins/xsrf.js';
|
|
157
|
-
|
|
158
|
-
/** Add plugins to run on all requests */
|
|
159
|
-
const api = new Api({ plugins: [xsrf] });
|
|
160
|
-
|
|
161
|
-
/** Or */
|
|
162
|
-
const xsrf = xsrfPlugin({
|
|
163
|
-
xsrfCookieName: '',
|
|
164
|
-
xsrfHeaderName: ''
|
|
165
|
-
});
|
|
166
|
-
const api = new Api({ plugins: [xsrf] });
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### Other
|
|
170
|
-
|
|
171
|
-
```js
|
|
172
|
-
import { logger } from '@thepassle/app-tools/api/plugins/logger.js';
|
|
173
|
-
|
|
174
|
-
/** Add plugins to run on all requests */
|
|
175
|
-
const api = new Api({ plugins: [logoutOnUnauthorized, logger] });
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## Methods
|
|
179
|
-
|
|
180
|
-
```js
|
|
181
|
-
api.get(url, opts);
|
|
182
|
-
api.options(url, opts);
|
|
183
|
-
api.delete(url, opts);
|
|
184
|
-
api.head(url, opts);
|
|
185
|
-
api.post(url, data, opts);
|
|
186
|
-
api.put(url, data, opts);
|
|
187
|
-
api.patch(url, data, opts);
|
|
188
|
-
|
|
189
|
-
api.addPlugin({
|
|
190
|
-
beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
|
|
191
|
-
afterFetch: (res) => res,
|
|
192
|
-
transform: (data) => data,
|
|
193
|
-
handleError: (e) => true
|
|
194
|
-
});
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Big List of Options
|
|
198
|
-
|
|
199
|
-
```js
|
|
200
|
-
api.get(url, {
|
|
201
|
-
baseURL: 'https://api.foo.com',
|
|
202
|
-
responseType: 'text',
|
|
203
|
-
params: { foo: 'bar' },
|
|
204
|
-
plugins: [
|
|
205
|
-
{
|
|
206
|
-
beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
|
|
207
|
-
afterFetch: (res) => res,
|
|
208
|
-
transform: (data) => data,
|
|
209
|
-
handleError: (e) => true
|
|
210
|
-
}
|
|
211
|
-
],
|
|
212
|
-
|
|
213
|
-
// Also supports all the options of the native fetch API
|
|
214
|
-
// mode, credentials, cache, redirect, referrer, integrity, keepalive, signal, referrerPolicy, headers, method
|
|
215
|
-
});
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
## Options
|
|
219
|
-
|
|
220
|
-
### `baseURL`
|
|
221
|
-
|
|
222
|
-
BaseURL to resolve all requests from. Can be set globally when instantiating a new `Api` instance, or on a per request basis. When set on a per request basis, will override the globally set baseURL (if set)
|
|
223
|
-
|
|
224
|
-
```js
|
|
225
|
-
api.get(url, { baseURL: 'https://api.foo.com' });
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### `responseType`
|
|
229
|
-
|
|
230
|
-
Overwrite the default responseType (`'json'`)
|
|
231
|
-
|
|
232
|
-
```js
|
|
233
|
-
api.get(url, { responseType: 'text' });
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
### `params`
|
|
237
|
-
|
|
238
|
-
An object to be queryParam-ified and added to the request url
|
|
239
|
-
|
|
240
|
-
```js
|
|
241
|
-
api.get(url, { params: { foo: 'bar' } });
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
### `plugins`
|
|
245
|
-
|
|
246
|
-
An array of plugins.
|
|
247
|
-
|
|
248
|
-
```js
|
|
249
|
-
api.get(url, {
|
|
250
|
-
plugins: [
|
|
251
|
-
{
|
|
252
|
-
beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
|
|
253
|
-
afterFetch: (res) => res,
|
|
254
|
-
transform: (data) => data,
|
|
255
|
-
handleError: (e) => true
|
|
256
|
-
}
|
|
257
|
-
]
|
|
258
|
-
})
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
## Plugins
|
|
262
|
-
|
|
263
|
-
You can also use plugins. You can add plugins on a per-request basis, or you can globally add them to your `api` instance:
|
|
264
|
-
|
|
265
|
-
```js
|
|
266
|
-
const api = new Api({
|
|
267
|
-
plugins: [
|
|
268
|
-
{
|
|
269
|
-
beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
|
|
270
|
-
afterFetch: (res) => res,
|
|
271
|
-
transform: (data) => data,
|
|
272
|
-
handleError: (e) => true
|
|
273
|
-
}
|
|
274
|
-
]
|
|
275
|
-
});
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
You can also dynamically add plugins:
|
|
279
|
-
|
|
280
|
-
```js
|
|
281
|
-
api.addPlugin({
|
|
282
|
-
afterFetch: (res) => res
|
|
283
|
-
});
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
Or you can add them on a per request basis:
|
|
287
|
-
|
|
288
|
-
```js
|
|
289
|
-
api.get(url, {
|
|
290
|
-
plugins: [
|
|
291
|
-
{
|
|
292
|
-
beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
|
|
293
|
-
afterFetch: (res) => res,
|
|
294
|
-
transform: (data) => data,
|
|
295
|
-
handleError: (e) => true
|
|
296
|
-
}
|
|
297
|
-
]
|
|
298
|
-
});
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### `beforeFetch`
|
|
302
|
-
|
|
303
|
-
Run logic before the actual `fetch` call happens, or alter/modify the meta information of a request.
|
|
304
|
-
If you want to alter or modify the meta information of a request, make sure to return the value.
|
|
305
|
-
|
|
306
|
-
```js
|
|
307
|
-
api.get('/foo', {
|
|
308
|
-
plugins: [{
|
|
309
|
-
beforeFetch: (meta) => ({...meta, url: '/bar'})
|
|
310
|
-
}]
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// RESULT: url `/bar` gets called instead of `/foo`
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
If you dont want to alter or modify any meta information of the request, you dont have to return anything.
|
|
317
|
-
```js
|
|
318
|
-
{
|
|
319
|
-
beforeFetch: ({url}) => {
|
|
320
|
-
console.log(url)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### `afterFetch`
|
|
326
|
-
|
|
327
|
-
Runs immediately after the `fetch` call happened. `afterFetch` should always return a `Response`:
|
|
328
|
-
```js
|
|
329
|
-
{ afterFetch: (res) => res; }
|
|
330
|
-
{ afterFetch: (res) => new Response(JSON.stringify({foo: 'bar'}), res); }
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
### `transform`
|
|
334
|
-
|
|
335
|
-
Runs after the `Response` object has been handled according to the `responseType`, (e.g.: `res.json()`). Can be used to transform the returned data:
|
|
336
|
-
Should always return the data.
|
|
337
|
-
|
|
338
|
-
```js
|
|
339
|
-
{ transform: (data) => data }
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
### `handleError`
|
|
343
|
-
|
|
344
|
-
Whether or not an error should throw. Return `true` if an error should throw, return `false` if an error should be ignored.
|
|
345
|
-
|
|
346
|
-
```js
|
|
347
|
-
{ handleError: (e) => e.message !== 'AbortError' }
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### Plugin Examples
|
|
351
|
-
|
|
352
|
-
#### Request logger
|
|
353
|
-
|
|
354
|
-
```js
|
|
355
|
-
function requestLogger() {
|
|
356
|
-
let start;
|
|
357
|
-
return {
|
|
358
|
-
beforeFetch: () => {
|
|
359
|
-
start = Date.now();
|
|
360
|
-
},
|
|
361
|
-
afterFetch: () => {
|
|
362
|
-
console.log(`Request took ${Date.now() - start}ms`);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
api.addPlugin(requestLogger());
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
#### Automatic logout on 401 or 403
|
|
371
|
-
|
|
372
|
-
```js
|
|
373
|
-
api.get(url, {
|
|
374
|
-
plugins: [
|
|
375
|
-
{
|
|
376
|
-
beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
|
|
377
|
-
afterFetch: (res) => {
|
|
378
|
-
if(res.status === 401 || res.status === 403) {
|
|
379
|
-
logout();
|
|
380
|
-
}
|
|
381
|
-
return res;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
]
|
|
385
|
-
});
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
#### Accessing the response body in `afterFetch`
|
|
389
|
-
|
|
390
|
-
If you want to access the response body of your response in a plugin, make sure to clone the response:
|
|
391
|
-
|
|
392
|
-
```js
|
|
393
|
-
const myPlugin = {
|
|
394
|
-
afterFetch: async (originalResponse) => {
|
|
395
|
-
const clone = originalResponse.clone();
|
|
396
|
-
let data = await clone.text(); // or `.json()` etc
|
|
397
|
-
|
|
398
|
-
data = data.replaceAll('foo', 'bar');
|
|
399
|
-
|
|
400
|
-
// Always make sure to return a `Response`
|
|
401
|
-
return new Response(data, originalResponse);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
api.addPlugin(myPlugin);
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
#### Returning a new response entirely
|
|
409
|
-
|
|
410
|
-
You can also overwrite the response entirely by returning a new `Response`
|
|
411
|
-
|
|
412
|
-
```js
|
|
413
|
-
api.addPlugin({
|
|
414
|
-
afterFetch: async (res) => new Response(JSON.stringify({foo: 'bar'}), res);
|
|
415
|
-
});
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
#### Overwriting the `fetch` implementation
|
|
419
|
-
|
|
420
|
-
You can also overwrite the `fetch` implementation to use:
|
|
421
|
-
|
|
422
|
-
```js
|
|
423
|
-
api.addPlugin({
|
|
424
|
-
beforeFetch: (meta) => ({
|
|
425
|
-
...meta,
|
|
426
|
-
fetchFn: () => Promise.resolve(new Response('{}'))
|
|
427
|
-
})
|
|
428
|
-
})
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
Do note that if you use multiple plugins that overwrite the `fetchFn`, the last plugin to overwrite the `fetchFn` will win, there can only be one `fetchFn`.
|
|
432
|
-
|
|
433
|
-
#### Transforming data
|
|
434
|
-
|
|
435
|
-
```js
|
|
436
|
-
api.addPlugin({
|
|
437
|
-
// Adds a `.foo` property to all of your response data
|
|
438
|
-
transform: (data) => {
|
|
439
|
-
data.foo = 'bar';
|
|
440
|
-
return data;
|
|
441
|
-
}
|
|
442
|
-
})
|
|
443
|
-
```
|