diff options
Diffstat (limited to 'src/api/server')
-rw-r--r-- | src/api/server/LocalApi.js | 20 | ||||
-rw-r--r-- | src/api/server/ServerApi.js | 288 |
2 files changed, 190 insertions, 118 deletions
diff --git a/src/api/server/LocalApi.js b/src/api/server/LocalApi.js index 4b1f03f22..2d5bd8b80 100644 --- a/src/api/server/LocalApi.js +++ b/src/api/server/LocalApi.js | |||
@@ -2,7 +2,7 @@ import { ipcRenderer } from 'electron'; | |||
2 | import { session } from '@electron/remote'; | 2 | import { session } from '@electron/remote'; |
3 | import du from 'du'; | 3 | import du from 'du'; |
4 | 4 | ||
5 | import { getServicePartitionsDirectory } from '../../helpers/service-helpers.js'; | 5 | import { getServicePartitionsDirectory } from '../../helpers/service-helpers'; |
6 | 6 | ||
7 | const debug = require('debug')('Ferdi:LocalApi'); | 7 | const debug = require('debug')('Ferdi:LocalApi'); |
8 | 8 | ||
@@ -41,11 +41,23 @@ export default class LocalApi { | |||
41 | } | 41 | } |
42 | 42 | ||
43 | async clearCache(serviceId = null) { | 43 | async clearCache(serviceId = null) { |
44 | const s = serviceId ? session.fromPartition(`persist:service-${serviceId}`) : session.defaultSession; | 44 | const s = serviceId |
45 | ? session.fromPartition(`persist:service-${serviceId}`) | ||
46 | : session.defaultSession; | ||
45 | 47 | ||
46 | debug('LocalApi::clearCache resolves', (serviceId || 'clearAppCache')); | 48 | debug('LocalApi::clearCache resolves', serviceId || 'clearAppCache'); |
47 | await s.clearStorageData({ | 49 | await s.clearStorageData({ |
48 | storages: ['appcache', 'cookies', 'filesystem', 'indexdb', 'localstorage', 'shadercache', 'websql', 'serviceworkers', 'cachestorage'], | 50 | storages: [ |
51 | 'appcache', | ||
52 | 'cookies', | ||
53 | 'filesystem', | ||
54 | 'indexdb', | ||
55 | 'localstorage', | ||
56 | 'shadercache', | ||
57 | 'websql', | ||
58 | 'serviceworkers', | ||
59 | 'cachestorage', | ||
60 | ], | ||
49 | quotas: ['temporary', 'persistent', 'syncable'], | 61 | quotas: ['temporary', 'persistent', 'syncable'], |
50 | }); | 62 | }); |
51 | return s.clearCache(); | 63 | return s.clearCache(); |
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js index 78a98e544..bc1d665b1 100644 --- a/src/api/server/ServerApi.js +++ b/src/api/server/ServerApi.js | |||
@@ -23,16 +23,11 @@ import { | |||
23 | loadRecipeConfig, | 23 | loadRecipeConfig, |
24 | } from '../../helpers/recipe-helpers'; | 24 | } from '../../helpers/recipe-helpers'; |
25 | 25 | ||
26 | import { | 26 | import { removeServicePartitionDirectory } from '../../helpers/service-helpers'; |
27 | removeServicePartitionDirectory, | ||
28 | } from '../../helpers/service-helpers.js'; | ||
29 | 27 | ||
30 | const debug = require('debug')('Ferdi:ServerApi'); | 28 | const debug = require('debug')('Ferdi:ServerApi'); |
31 | 29 | ||
32 | module.paths.unshift( | 30 | module.paths.unshift(getDevRecipeDirectory(), getRecipeDirectory()); |
33 | getDevRecipeDirectory(), | ||
34 | getRecipeDirectory(), | ||
35 | ); | ||
36 | 31 | ||
37 | const { default: fetch } = remoteRequire('electron-fetch'); | 32 | const { default: fetch } = remoteRequire('electron-fetch'); |
38 | 33 | ||
@@ -43,12 +38,16 @@ export default class ServerApi { | |||
43 | 38 | ||
44 | // User | 39 | // User |
45 | async login(email, passwordHash) { | 40 | async login(email, passwordHash) { |
46 | const request = await sendAuthRequest(`${apiBase()}/auth/login`, { | 41 | const request = await sendAuthRequest( |
47 | method: 'POST', | 42 | `${apiBase()}/auth/login`, |
48 | headers: { | 43 | { |
49 | Authorization: `Basic ${window.btoa(`${email}:${passwordHash}`)}`, | 44 | method: 'POST', |
45 | headers: { | ||
46 | Authorization: `Basic ${window.btoa(`${email}:${passwordHash}`)}`, | ||
47 | }, | ||
50 | }, | 48 | }, |
51 | }, false); | 49 | false, |
50 | ); | ||
52 | if (!request.ok) { | 51 | if (!request.ok) { |
53 | throw request; | 52 | throw request; |
54 | } | 53 | } |
@@ -59,10 +58,14 @@ export default class ServerApi { | |||
59 | } | 58 | } |
60 | 59 | ||
61 | async signup(data) { | 60 | async signup(data) { |
62 | const request = await sendAuthRequest(`${apiBase()}/auth/signup`, { | 61 | const request = await sendAuthRequest( |
63 | method: 'POST', | 62 | `${apiBase()}/auth/signup`, |
64 | body: JSON.stringify(data), | 63 | { |
65 | }, false); | 64 | method: 'POST', |
65 | body: JSON.stringify(data), | ||
66 | }, | ||
67 | false, | ||
68 | ); | ||
66 | if (!request.ok) { | 69 | if (!request.ok) { |
67 | throw request; | 70 | throw request; |
68 | } | 71 | } |
@@ -86,12 +89,16 @@ export default class ServerApi { | |||
86 | } | 89 | } |
87 | 90 | ||
88 | async retrievePassword(email) { | 91 | async retrievePassword(email) { |
89 | const request = await sendAuthRequest(`${apiBase()}/auth/password`, { | 92 | const request = await sendAuthRequest( |
90 | method: 'POST', | 93 | `${apiBase()}/auth/password`, |
91 | body: JSON.stringify({ | 94 | { |
92 | email, | 95 | method: 'POST', |
93 | }), | 96 | body: JSON.stringify({ |
94 | }, false); | 97 | email, |
98 | }), | ||
99 | }, | ||
100 | false, | ||
101 | ); | ||
95 | if (!request.ok) { | 102 | if (!request.ok) { |
96 | throw request; | 103 | throw request; |
97 | } | 104 | } |
@@ -128,7 +135,9 @@ export default class ServerApi { | |||
128 | } | 135 | } |
129 | const updatedData = await request.json(); | 136 | const updatedData = await request.json(); |
130 | 137 | ||
131 | const user = Object.assign(updatedData, { data: new UserModel(updatedData.data) }); | 138 | const user = Object.assign(updatedData, { |
139 | data: new UserModel(updatedData.data), | ||
140 | }); | ||
132 | debug('ServerApi::updateUserInfo resolves', user); | 141 | debug('ServerApi::updateUserInfo resolves', user); |
133 | return user; | 142 | return user; |
134 | } | 143 | } |
@@ -159,7 +168,7 @@ export default class ServerApi { | |||
159 | const data = await request.json(); | 168 | const data = await request.json(); |
160 | 169 | ||
161 | let services = await this._mapServiceModels(data); | 170 | let services = await this._mapServiceModels(data); |
162 | services = services.filter(service => service !== null); | 171 | services = services.filter((service) => service !== null); |
163 | debug('ServerApi::getServices resolves', services); | 172 | debug('ServerApi::getServices resolves', services); |
164 | return services; | 173 | return services; |
165 | } | 174 | } |
@@ -175,12 +184,17 @@ export default class ServerApi { | |||
175 | const serviceData = await request.json(); | 184 | const serviceData = await request.json(); |
176 | 185 | ||
177 | if (data.iconFile) { | 186 | if (data.iconFile) { |
178 | const iconData = await this.uploadServiceIcon(serviceData.data.id, data.iconFile); | 187 | const iconData = await this.uploadServiceIcon( |
188 | serviceData.data.id, | ||
189 | data.iconFile, | ||
190 | ); | ||
179 | 191 | ||
180 | serviceData.data = iconData; | 192 | serviceData.data = iconData; |
181 | } | 193 | } |
182 | 194 | ||
183 | const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) }); | 195 | const service = Object.assign(serviceData, { |
196 | data: await this._prepareServiceModel(serviceData.data), | ||
197 | }); | ||
184 | 198 | ||
185 | debug('ServerApi::createService resolves', service); | 199 | debug('ServerApi::createService resolves', service); |
186 | return service; | 200 | return service; |
@@ -204,7 +218,9 @@ export default class ServerApi { | |||
204 | 218 | ||
205 | const serviceData = await request.json(); | 219 | const serviceData = await request.json(); |
206 | 220 | ||
207 | const service = Object.assign(serviceData, { data: await this._prepareServiceModel(serviceData.data) }); | 221 | const service = Object.assign(serviceData, { |
222 | data: await this._prepareServiceModel(serviceData.data), | ||
223 | }); | ||
208 | 224 | ||
209 | debug('ServerApi::updateService resolves', service); | 225 | debug('ServerApi::updateService resolves', service); |
210 | return service; | 226 | return service; |
@@ -221,7 +237,10 @@ export default class ServerApi { | |||
221 | 237 | ||
222 | delete requestData.headers['Content-Type']; | 238 | delete requestData.headers['Content-Type']; |
223 | 239 | ||
224 | const request = await window.fetch(`${apiBase()}/service/${serviceId}`, requestData); | 240 | const request = await window.fetch( |
241 | `${apiBase()}/service/${serviceId}`, | ||
242 | requestData, | ||
243 | ); | ||
225 | 244 | ||
226 | if (!request.ok) { | 245 | if (!request.ok) { |
227 | throw request; | 246 | throw request; |
@@ -292,18 +311,21 @@ export default class ServerApi { | |||
292 | // Recipes | 311 | // Recipes |
293 | async getInstalledRecipes() { | 312 | async getInstalledRecipes() { |
294 | const recipesDirectory = getRecipeDirectory(); | 313 | const recipesDirectory = getRecipeDirectory(); |
295 | const paths = fs.readdirSync(recipesDirectory) | 314 | const paths = fs |
296 | .filter(file => ( | 315 | .readdirSync(recipesDirectory) |
297 | fs.statSync(path.join(recipesDirectory, file)).isDirectory() | 316 | .filter( |
298 | && file !== 'temp' | 317 | (file) => fs.statSync(path.join(recipesDirectory, file)).isDirectory() |
299 | && file !== 'dev' | 318 | && file !== 'temp' |
300 | )); | 319 | && file !== 'dev', |
301 | 320 | ); | |
302 | this.recipes = paths.map((id) => { | 321 | |
303 | // eslint-disable-next-line | 322 | this.recipes = paths |
304 | const Recipe = require(id)(RecipeModel); | 323 | .map((id) => { |
305 | return new Recipe(loadRecipeConfig(id)); | 324 | // eslint-disable-next-line |
306 | }).filter(recipe => recipe.id); | 325 | const Recipe = require(id)(RecipeModel); |
326 | return new Recipe(loadRecipeConfig(id)); | ||
327 | }) | ||
328 | .filter((recipe) => recipe.id); | ||
307 | 329 | ||
308 | this.recipes = this.recipes.concat(this._getDevRecipes()); | 330 | this.recipes = this.recipes.concat(this._getDevRecipes()); |
309 | 331 | ||
@@ -373,7 +395,9 @@ export default class ServerApi { | |||
373 | console.log('[ServerApi::getRecipePackage] Using internal recipe file'); | 395 | console.log('[ServerApi::getRecipePackage] Using internal recipe file'); |
374 | archivePath = internalRecipeFile; | 396 | archivePath = internalRecipeFile; |
375 | } else { | 397 | } else { |
376 | console.log('[ServerApi::getRecipePackage] Downloading recipe from server'); | 398 | console.log( |
399 | '[ServerApi::getRecipePackage] Downloading recipe from server', | ||
400 | ); | ||
377 | archivePath = tempArchivePath; | 401 | archivePath = tempArchivePath; |
378 | 402 | ||
379 | const packageUrl = `${apiBase()}/recipes/download/${recipeId}`; | 403 | const packageUrl = `${apiBase()}/recipes/download/${recipeId}`; |
@@ -393,12 +417,14 @@ export default class ServerApi { | |||
393 | preservePaths: true, | 417 | preservePaths: true, |
394 | unlink: true, | 418 | unlink: true, |
395 | preserveOwner: false, | 419 | preserveOwner: false, |
396 | onwarn: x => console.log('warn', recipeId, x), | 420 | onwarn: (x) => console.log('warn', recipeId, x), |
397 | }); | 421 | }); |
398 | 422 | ||
399 | await sleep(10); | 423 | await sleep(10); |
400 | 424 | ||
401 | const { id } = fs.readJsonSync(path.join(recipeTempDirectory, 'package.json')); | 425 | const { id } = fs.readJsonSync( |
426 | path.join(recipeTempDirectory, 'package.json'), | ||
427 | ); | ||
402 | const recipeDirectory = path.join(recipesDirectory, id); | 428 | const recipeDirectory = path.join(recipesDirectory, id); |
403 | fs.copySync(recipeTempDirectory, recipeDirectory); | 429 | fs.copySync(recipeTempDirectory, recipeDirectory); |
404 | fs.remove(recipeTempDirectory); | 430 | fs.remove(recipeTempDirectory); |
@@ -414,7 +440,9 @@ export default class ServerApi { | |||
414 | 440 | ||
415 | // News | 441 | // News |
416 | async getLatestNews() { | 442 | async getLatestNews() { |
417 | const url = `${apiBase(true)}/news?platform=${osPlatform}&arch=${osArch}&version=${app.getVersion()}`; | 443 | const url = `${apiBase( |
444 | true, | ||
445 | )}/news?platform=${osPlatform}&arch=${osArch}&version=${app.getVersion()}`; | ||
418 | const request = await sendAuthRequest(url); | 446 | const request = await sendAuthRequest(url); |
419 | if (!request.ok) throw request; | 447 | if (!request.ok) throw request; |
420 | const data = await request.json(); | 448 | const data = await request.json(); |
@@ -435,9 +463,13 @@ export default class ServerApi { | |||
435 | throw new Error('Server not loaded'); | 463 | throw new Error('Server not loaded'); |
436 | } | 464 | } |
437 | 465 | ||
438 | const request = await sendAuthRequest(`${apiBase(false)}/health`, { | 466 | const request = await sendAuthRequest( |
439 | method: 'GET', | 467 | `${apiBase(false)}/health`, |
440 | }, false); | 468 | { |
469 | method: 'GET', | ||
470 | }, | ||
471 | false, | ||
472 | ); | ||
441 | if (!request.ok) { | 473 | if (!request.ok) { |
442 | throw request; | 474 | throw request; |
443 | } | 475 | } |
@@ -445,23 +477,31 @@ export default class ServerApi { | |||
445 | } | 477 | } |
446 | 478 | ||
447 | async getLegacyServices() { | 479 | async getLegacyServices() { |
448 | const file = path.join(app.getPath('userData'), 'settings', 'services.json'); | 480 | const file = path.join( |
481 | app.getPath('userData'), | ||
482 | 'settings', | ||
483 | 'services.json', | ||
484 | ); | ||
449 | 485 | ||
450 | try { | 486 | try { |
451 | const config = fs.readJsonSync(file); | 487 | const config = fs.readJsonSync(file); |
452 | 488 | ||
453 | if (Object.prototype.hasOwnProperty.call(config, 'services')) { | 489 | if (Object.prototype.hasOwnProperty.call(config, 'services')) { |
454 | const services = await Promise.all(config.services.map(async (s) => { | 490 | const services = await Promise.all( |
455 | const service = s; | 491 | config.services.map(async (s) => { |
456 | const request = await sendAuthRequest(`${apiBase()}/recipes/${s.service}`); | 492 | const service = s; |
457 | 493 | const request = await sendAuthRequest( | |
458 | if (request.status === 200) { | 494 | `${apiBase()}/recipes/${s.service}`, |
459 | const data = await request.json(); | 495 | ); |
460 | service.recipe = new RecipePreviewModel(data); | 496 | |
461 | } | 497 | if (request.status === 200) { |
462 | 498 | const data = await request.json(); | |
463 | return service; | 499 | service.recipe = new RecipePreviewModel(data); |
464 | })); | 500 | } |
501 | |||
502 | return service; | ||
503 | }), | ||
504 | ); | ||
465 | 505 | ||
466 | debug('ServerApi::getLegacyServices resolves', services); | 506 | debug('ServerApi::getLegacyServices resolves', services); |
467 | return services; | 507 | return services; |
@@ -475,17 +515,19 @@ export default class ServerApi { | |||
475 | 515 | ||
476 | // Helper | 516 | // Helper |
477 | async _mapServiceModels(services) { | 517 | async _mapServiceModels(services) { |
478 | const recipes = services.map(s => s.recipeId); | 518 | const recipes = services.map((s) => s.recipeId); |
479 | await this._bulkRecipeCheck(recipes); | 519 | await this._bulkRecipeCheck(recipes); |
480 | /* eslint-disable no-return-await */ | 520 | /* eslint-disable no-return-await */ |
481 | return Promise.all(services.map(async service => await this._prepareServiceModel(service))); | 521 | return Promise.all( |
522 | services.map(async (service) => await this._prepareServiceModel(service)), | ||
523 | ); | ||
482 | /* eslint-enable no-return-await */ | 524 | /* eslint-enable no-return-await */ |
483 | } | 525 | } |
484 | 526 | ||
485 | async _prepareServiceModel(service) { | 527 | async _prepareServiceModel(service) { |
486 | let recipe; | 528 | let recipe; |
487 | try { | 529 | try { |
488 | recipe = this.recipes.find(r => r.id === service.recipeId); | 530 | recipe = this.recipes.find((r) => r.id === service.recipeId); |
489 | 531 | ||
490 | if (!recipe) { | 532 | if (!recipe) { |
491 | console.warn(`Recipe ${service.recipeId} not loaded`); | 533 | console.warn(`Recipe ${service.recipeId} not loaded`); |
@@ -501,21 +543,25 @@ export default class ServerApi { | |||
501 | 543 | ||
502 | async _bulkRecipeCheck(unfilteredRecipes) { | 544 | async _bulkRecipeCheck(unfilteredRecipes) { |
503 | // Filter recipe duplicates as we don't need to download 3 Slack recipes | 545 | // Filter recipe duplicates as we don't need to download 3 Slack recipes |
504 | const recipes = unfilteredRecipes.filter((elem, pos, arr) => arr.indexOf(elem) === pos); | 546 | const recipes = unfilteredRecipes.filter( |
547 | (elem, pos, arr) => arr.indexOf(elem) === pos, | ||
548 | ); | ||
505 | 549 | ||
506 | return Promise.all(recipes | 550 | return Promise.all( |
507 | .map(async (recipeId) => { | 551 | recipes.map(async (recipeId) => { |
508 | let recipe = this.recipes.find(r => r.id === recipeId); | 552 | let recipe = this.recipes.find((r) => r.id === recipeId); |
509 | 553 | ||
510 | if (!recipe) { | 554 | if (!recipe) { |
511 | console.warn(`Recipe '${recipeId}' not installed, trying to fetch from server`); | 555 | console.warn( |
556 | `Recipe '${recipeId}' not installed, trying to fetch from server`, | ||
557 | ); | ||
512 | 558 | ||
513 | await this.getRecipePackage(recipeId); | 559 | await this.getRecipePackage(recipeId); |
514 | 560 | ||
515 | debug('Rerun ServerAPI::getInstalledRecipes'); | 561 | debug('Rerun ServerAPI::getInstalledRecipes'); |
516 | await this.getInstalledRecipes(); | 562 | await this.getInstalledRecipes(); |
517 | 563 | ||
518 | recipe = this.recipes.find(r => r.id === recipeId); | 564 | recipe = this.recipes.find((r) => r.id === recipeId); |
519 | 565 | ||
520 | if (!recipe) { | 566 | if (!recipe) { |
521 | console.warn(`Could not load recipe ${recipeId}`); | 567 | console.warn(`Could not load recipe ${recipeId}`); |
@@ -524,69 +570,83 @@ export default class ServerApi { | |||
524 | } | 570 | } |
525 | 571 | ||
526 | return recipe; | 572 | return recipe; |
527 | })).catch(err => console.error('Can\'t load recipe', err)); | 573 | }), |
574 | ).catch((err) => console.error("Can't load recipe", err)); | ||
528 | } | 575 | } |
529 | 576 | ||
530 | _mapRecipePreviewModel(recipes) { | 577 | _mapRecipePreviewModel(recipes) { |
531 | return recipes.map((recipe) => { | 578 | return recipes |
532 | try { | 579 | .map((recipe) => { |
533 | return new RecipePreviewModel(recipe); | 580 | try { |
534 | } catch (e) { | 581 | return new RecipePreviewModel(recipe); |
535 | console.error(e); | 582 | } catch (e) { |
536 | return null; | 583 | console.error(e); |
537 | } | 584 | return null; |
538 | }).filter(recipe => recipe !== null); | 585 | } |
586 | }) | ||
587 | .filter((recipe) => recipe !== null); | ||
539 | } | 588 | } |
540 | 589 | ||
541 | _mapNewsModels(news) { | 590 | _mapNewsModels(news) { |
542 | return news.map((newsItem) => { | 591 | return news |
543 | try { | 592 | .map((newsItem) => { |
544 | return new NewsModel(newsItem); | 593 | try { |
545 | } catch (e) { | 594 | return new NewsModel(newsItem); |
546 | console.error(e); | 595 | } catch (e) { |
547 | return null; | 596 | console.error(e); |
548 | } | 597 | return null; |
549 | }).filter(newsItem => newsItem !== null); | 598 | } |
599 | }) | ||
600 | .filter((newsItem) => newsItem !== null); | ||
550 | } | 601 | } |
551 | 602 | ||
552 | _mapOrderModels(orders) { | 603 | _mapOrderModels(orders) { |
553 | return orders.map((orderItem) => { | 604 | return orders |
554 | try { | 605 | .map((orderItem) => { |
555 | return new OrderModel(orderItem); | 606 | try { |
556 | } catch (e) { | 607 | return new OrderModel(orderItem); |
557 | console.error(e); | 608 | } catch (e) { |
558 | return null; | 609 | console.error(e); |
559 | } | 610 | return null; |
560 | }).filter(orderItem => orderItem !== null); | 611 | } |
612 | }) | ||
613 | .filter((orderItem) => orderItem !== null); | ||
561 | } | 614 | } |
562 | 615 | ||
563 | _getDevRecipes() { | 616 | _getDevRecipes() { |
564 | const recipesDirectory = getDevRecipeDirectory(); | 617 | const recipesDirectory = getDevRecipeDirectory(); |
565 | try { | 618 | try { |
566 | const paths = fs.readdirSync(recipesDirectory) | 619 | const paths = fs |
567 | .filter(file => fs.statSync(path.join(recipesDirectory, file)).isDirectory() && file !== 'temp'); | 620 | .readdirSync(recipesDirectory) |
568 | 621 | .filter( | |
569 | const recipes = paths.map((id) => { | 622 | (file) => fs.statSync(path.join(recipesDirectory, file)).isDirectory() |
570 | let Recipe; | 623 | && file !== 'temp', |
571 | try { | 624 | ); |
572 | // eslint-disable-next-line | 625 | |
573 | Recipe = require(id)(RecipeModel); | 626 | const recipes = paths |
574 | return new Recipe(loadRecipeConfig(id)); | 627 | .map((id) => { |
575 | } catch (err) { | 628 | let Recipe; |
576 | console.error(err); | 629 | try { |
577 | } | 630 | // eslint-disable-next-line |
631 | Recipe = require(id)(RecipeModel); | ||
632 | return new Recipe(loadRecipeConfig(id)); | ||
633 | } catch (err) { | ||
634 | console.error(err); | ||
635 | } | ||
578 | 636 | ||
579 | return false; | 637 | return false; |
580 | }).filter(recipe => recipe.id).map((data) => { | 638 | }) |
581 | const recipe = data; | 639 | .filter((recipe) => recipe.id) |
640 | .map((data) => { | ||
641 | const recipe = data; | ||
582 | 642 | ||
583 | recipe.icons = { | 643 | recipe.icons = { |
584 | svg: `${recipe.path}/icon.svg`, | 644 | svg: `${recipe.path}/icon.svg`, |
585 | }; | 645 | }; |
586 | recipe.local = true; | 646 | recipe.local = true; |
587 | 647 | ||
588 | return data; | 648 | return data; |
589 | }); | 649 | }); |
590 | 650 | ||
591 | return recipes; | 651 | return recipes; |
592 | } catch (err) { | 652 | } catch (err) { |