incyclist-services 1.8.5 → 1.8.6

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toRouteImportError = exports.mapErrorToImportCode = exports.RouteImportError = void 0;
3
+ exports.toRouteImportError = exports.mapErrorToImportCode = exports.DuplicateRouteError = exports.RouteImportError = void 0;
4
4
  class RouteImportError extends Error {
5
5
  code;
6
6
  missingExt;
@@ -18,6 +18,15 @@ class RouteImportError extends Error {
18
18
  }
19
19
  }
20
20
  exports.RouteImportError = RouteImportError;
21
+ class DuplicateRouteError extends Error {
22
+ duplicateOf;
23
+ constructor(duplicateOf) {
24
+ super(`Duplicate of ${duplicateOf}`);
25
+ this.name = 'DuplicateRouteError';
26
+ this.duplicateOf = duplicateOf;
27
+ }
28
+ }
29
+ exports.DuplicateRouteError = DuplicateRouteError;
21
30
  const mapErrorToImportCode = (err, scope) => {
22
31
  if (err instanceof RouteImportError)
23
32
  return err.code;
@@ -95,7 +95,6 @@ let RouteLibraryScannerService = (() => {
95
95
  __runInitializers(_classThis, _classExtraInitializers);
96
96
  }
97
97
  isCancelled = (__runInitializers(this, _instanceExtraInitializers), false);
98
- scanResult = [];
99
98
  importProps;
100
99
  currentScope;
101
100
  constructor() {
@@ -110,7 +109,6 @@ let RouteLibraryScannerService = (() => {
110
109
  }
111
110
  done() {
112
111
  this.importProps = undefined;
113
- this.scanResult = [];
114
112
  }
115
113
  getDisplayProps() {
116
114
  const importProps = this.importProps ?? { phase: 'landing', routes: [], hasICloudDownloadFailures: false };
@@ -156,7 +154,8 @@ let RouteLibraryScannerService = (() => {
156
154
  this.isCancelled = false;
157
155
  if (!this.importProps)
158
156
  this.prepare();
159
- this.importProps.phase = 'ingesting';
157
+ const session = this.importProps;
158
+ session.phase = 'ingesting';
160
159
  const list = this.getRouteList();
161
160
  list.pauseListUpdates();
162
161
  this._importFiles(files ?? [], observer)
@@ -169,15 +168,16 @@ let RouteLibraryScannerService = (() => {
169
168
  list.emitLists('updated', { source: 'system' });
170
169
  });
171
170
  observer.on('ingest-progress', (progress) => {
172
- if (this.importProps)
173
- this.importProps.ingestProgress = progress;
171
+ if (this.importProps !== session)
172
+ return;
173
+ session.ingestProgress = progress;
174
174
  });
175
175
  observer.on('ingest-complete', (status) => {
176
- if (!this.importProps)
176
+ if (this.importProps !== session)
177
177
  return;
178
178
  const { imported, skipped, errors, failedRoutes } = status;
179
- this.importProps.phase = 'complete';
180
- this.importProps.completionSummary = { imported, skipped, errors, failedRoutes };
179
+ session.phase = 'complete';
180
+ session.completionSummary = { imported, skipped, errors, failedRoutes };
181
181
  });
182
182
  return observer;
183
183
  }
@@ -203,9 +203,9 @@ let RouteLibraryScannerService = (() => {
203
203
  this.prepare();
204
204
  this.logEvent({ message: 'import route library', folder: folderInfo.displayName });
205
205
  this.isCancelled = false;
206
- this.scanResult = [];
207
- this.importProps.phase = 'scanning';
208
- this.importProps.scanProgress = { scannedFolders: 0, failedFolders: 0 };
206
+ const session = this.importProps;
207
+ session.phase = 'scanning';
208
+ session.scanProgress = { scannedFolders: 0, failedFolders: 0 };
209
209
  const observer = new types_1.Observer();
210
210
  this._scan(folderInfo, observer).catch(err => {
211
211
  this.logError(err, 'scan', { folder: folderInfo.displayName });
@@ -213,12 +213,16 @@ let RouteLibraryScannerService = (() => {
213
213
  });
214
214
  observer
215
215
  .on('scan-progress', (progress) => {
216
- this.importProps.scanProgress = progress;
216
+ if (this.importProps !== session)
217
+ return;
218
+ session.scanProgress = progress;
217
219
  })
218
220
  .on('scan-complete', () => {
219
- const { scannedFolders: cntScanned, failedFolders: cntFailed } = this.importProps.scanProgress;
221
+ if (this.importProps !== session)
222
+ return;
223
+ const { scannedFolders: cntScanned, failedFolders: cntFailed } = session.scanProgress;
220
224
  this.logEvent({ message: 'import route library: scan success', folder: folderInfo.displayName, cntScanned, cntFailed });
221
- this.importProps.phase = 'parsing';
225
+ session.phase = 'parsing';
222
226
  });
223
227
  return observer;
224
228
  }
@@ -226,35 +230,42 @@ let RouteLibraryScannerService = (() => {
226
230
  const observer = new types_1.Observer();
227
231
  if (!this.importProps)
228
232
  this.prepare();
229
- this.logEvent({ message: 'import route library: parse start', folder: scannedRoutes?.[0].folderName });
230
- this._parse(scannedRoutes, observer).catch(err => {
231
- this.logError(err, 'parse');
232
- observer.emit('error', err.message);
233
- });
233
+ const session = this.importProps;
234
+ this.logEvent({ message: 'import route library: parse start', folder: scannedRoutes?.[0]?.folderName });
234
235
  observer.on('parse-progress', (progress) => {
236
+ if (this.importProps !== session)
237
+ return;
235
238
  const { parsed, total } = progress;
236
- this.importProps.parseProgress = { parsed, total };
239
+ session.parseProgress = { parsed, total };
237
240
  });
238
241
  observer.on('parse-result', (route) => {
239
- const idx = this.importProps.routes.findIndex(r => r.id === route.controlFileUri);
242
+ if (this.importProps !== session)
243
+ return;
244
+ const idx = session.routes.findIndex(r => r.fileUri === route.controlFileUri);
240
245
  if (idx !== -1) {
241
- const observer = this.importProps.routes[idx].observer;
242
- const displayProps = this.buildRouteDisplayItem(route, observer);
243
- this.importProps.routes[idx] = displayProps;
246
+ const observer = session.routes[idx].observer;
247
+ const displayProps = this.buildRouteDisplayItem(route, session, observer);
248
+ session.routes[idx] = displayProps;
244
249
  if (observer) {
245
250
  observer.emit('updated', displayProps);
246
251
  }
247
252
  }
248
253
  });
249
254
  observer.on('parse-complete', () => {
255
+ if (this.importProps !== session)
256
+ return;
250
257
  const parseSumamry = {
251
- cntTotal: this.importProps.routes.length,
252
- cntSuccess: this.importProps.routes.filter(r => !r.errorReason).length,
253
- cntError: this.importProps.routes.filter(r => r.errorReason != null).length,
254
- cntExisting: this.importProps.routes.filter(r => r.alreadyImported).length,
258
+ cntTotal: session.routes.length,
259
+ cntSuccess: session.routes.filter(r => !r.errorReason).length,
260
+ cntError: session.routes.filter(r => r.errorReason != null).length,
261
+ cntExisting: session.routes.filter(r => r.alreadyImported).length,
255
262
  };
256
- this.logEvent({ message: 'import route library: parse completed', folder: scannedRoutes?.[0].folderName, parseSumamry });
257
- this.importProps.phase = 'selecting';
263
+ this.logEvent({ message: 'import route library: parse completed', folder: scannedRoutes?.[0]?.folderName, parseSumamry });
264
+ session.phase = 'selecting';
265
+ });
266
+ this._parse(scannedRoutes, observer, session).catch(err => {
267
+ this.logError(err, 'parse');
268
+ observer.emit('error', err.message);
258
269
  });
259
270
  return observer;
260
271
  }
@@ -262,9 +273,10 @@ let RouteLibraryScannerService = (() => {
262
273
  const observer = new types_1.Observer();
263
274
  if (!this.importProps)
264
275
  this.prepare();
276
+ const session = this.importProps;
265
277
  const list = this.getRouteList();
266
278
  list.pauseListUpdates();
267
- this.importProps.phase = 'ingesting';
279
+ session.phase = 'ingesting';
268
280
  this._ingest(routes, observer)
269
281
  .catch(err => {
270
282
  this.logError(err, 'ingest');
@@ -275,13 +287,17 @@ let RouteLibraryScannerService = (() => {
275
287
  list.emitLists('updated', { source: 'system' });
276
288
  });
277
289
  observer.on('ingest-progress', (progress) => {
290
+ if (this.importProps !== session)
291
+ return;
278
292
  const { current, total, currentName } = progress;
279
- this.importProps.ingestProgress = { current, total, currentName };
293
+ session.ingestProgress = { current, total, currentName };
280
294
  });
281
295
  observer.on('ingest-complete', (status) => {
282
- this.importProps.phase = 'complete';
296
+ if (this.importProps !== session)
297
+ return;
298
+ session.phase = 'complete';
283
299
  const { imported, skipped, errors, failedRoutes } = status;
284
- this.importProps.completionSummary = { imported, skipped, errors, failedRoutes };
300
+ session.completionSummary = { imported, skipped, errors, failedRoutes };
285
301
  });
286
302
  return observer;
287
303
  }
@@ -403,8 +419,8 @@ let RouteLibraryScannerService = (() => {
403
419
  await this.scanFolder(folderInfo.uri, folderInfo.displayName, ctx);
404
420
  await this.upsertImportHistory(folderInfo, ctx.discoveredCount.value);
405
421
  const { scannedFolders, failedFolders } = ctx.progress;
406
- this.logEvent({ message: 'video scan result', scannedFolders, failedFolders, files: this.scanResult.length });
407
- observer.emit('scan-complete', this.scanResult);
422
+ this.logEvent({ message: 'video scan result', scannedFolders, failedFolders, files: ctx.results.length });
423
+ observer.emit('scan-complete', ctx.results);
408
424
  }
409
425
  createScanContext(observer, parsers, recursive) {
410
426
  return {
@@ -412,7 +428,8 @@ let RouteLibraryScannerService = (() => {
412
428
  parsers,
413
429
  recursive,
414
430
  progress: { scannedFolders: 0, failedFolders: 0 },
415
- discoveredCount: { value: 0 }
431
+ discoveredCount: { value: 0 },
432
+ results: []
416
433
  };
417
434
  }
418
435
  async scanFolder(uri, folderName, ctx) {
@@ -449,7 +466,7 @@ let RouteLibraryScannerService = (() => {
449
466
  const routeAnnouncement = await this.buildDiscoveredRoute(file, files, uri, folderName, ctx.parsers);
450
467
  ctx.discoveredCount.value++;
451
468
  ctx.observer.emit('scan-result', routeAnnouncement);
452
- this.scanResult.push(routeAnnouncement);
469
+ ctx.results.push(routeAnnouncement);
453
470
  }
454
471
  }
455
472
  }
@@ -600,29 +617,30 @@ let RouteLibraryScannerService = (() => {
600
617
  scanError: skipReason
601
618
  };
602
619
  }
603
- async _parse(scannedRoutes, observer) {
620
+ async _parse(scannedRoutes, observer, session) {
604
621
  const service = this.getRouteList();
605
622
  const targets = scannedRoutes.filter(r => !r.scanError);
606
623
  const total = targets.length;
607
624
  targets.forEach(target => {
608
625
  const file = this.buildFileInfo(target.controlFileUri, target.format);
609
- this.importProps.routes.push({
626
+ session.routes.push({
610
627
  format: target.format,
611
628
  parseState: 'waiting',
612
629
  importable: false,
613
630
  label: file.base,
614
631
  folder: target.folderName,
615
632
  id: target.controlFileUri,
633
+ fileUri: target.controlFileUri,
616
634
  alreadyImported: false,
617
635
  observer: new types_1.Observer()
618
636
  });
619
637
  });
620
638
  observer.emit('parse-start');
621
- for (let i = 0; i < targets.length; i++) {
622
- if (this.isCancelled)
623
- continue;
639
+ await targets.reduce(async (previous, target, i) => {
640
+ await previous;
641
+ if (this.isCancelled || this.importProps !== session)
642
+ return;
624
643
  const parsed = i + 1;
625
- const target = targets[i];
626
644
  observer.emit('parse-progress', { current: parsed, parsed, total, currentFolder: target.folderName });
627
645
  let fileName = target.controlFileUri;
628
646
  try {
@@ -631,15 +649,15 @@ let RouteLibraryScannerService = (() => {
631
649
  fileName = info.base;
632
650
  }
633
651
  catch { }
634
- await this._parseTarget(target, service, observer);
652
+ await this._parseTarget(target, service, observer, session);
635
653
  this.logEvent({ message: 'parsing route done', fileName });
636
- }
654
+ }, Promise.resolve());
637
655
  observer.emit('parse-complete');
638
656
  }
639
- async _parseTarget(target, service, observer) {
657
+ async _parseTarget(target, service, observer, session) {
640
658
  let result;
641
659
  const file = this.buildFileInfo(target.controlFileUri, target.format);
642
- const importProps = this.importProps.routes.find(r => r.id === target.controlFileUri) ?? {};
660
+ const importProps = session.routes.find(r => r.fileUri === target.controlFileUri) ?? {};
643
661
  const scope = (0, externalFiles_1.useExternalFileService)().beginScope({ isCancelled: () => this.isCancelled });
644
662
  this.currentScope = scope;
645
663
  try {
@@ -658,10 +676,10 @@ let RouteLibraryScannerService = (() => {
658
676
  }
659
677
  importProps.parseState = 'parsed';
660
678
  const route = new route_1.Route(result.data, result.details);
661
- const existing = this.importProps.routes.find(ri => ri.id === route.description.id);
679
+ const existing = session.routes.find(ri => ri.id === route.description.id);
662
680
  if (existing) {
663
681
  result = undefined;
664
- throw new Error(`Duplicate of ${existing.label}`);
682
+ throw new errors_1.DuplicateRouteError(existing.label);
665
683
  }
666
684
  if (result.data.hasVideo) {
667
685
  this.validateVideoUrl(route, target.folderUri, target.files);
@@ -681,20 +699,31 @@ let RouteLibraryScannerService = (() => {
681
699
  }
682
700
  catch (err) {
683
701
  importProps.parseState = 'parsed';
684
- const parsed = {
685
- alreadyImported: false,
686
- route: result ? new route_1.Route(result.data, result.details) : undefined,
687
- folderUri: target.folderUri,
688
- folderName: target.folderName,
689
- controlFileUri: target.controlFileUri,
690
- format: target.format,
691
- parseError: scope.lastFailure ? this.getReadFailureMessage(scope) : (err?.message ?? String(err)),
692
- parseErrorCode: (0, errors_1.mapErrorToImportCode)(err, scope)
693
- };
694
- this.logEvent({ message: 'could not parse route file', file: file.base, reason: err.message, stack: err.stack });
702
+ const parsed = this.buildParseFailure(target, err, scope, result);
703
+ this.logParseFailure(file.base, err);
695
704
  observer.emit('parse-result', parsed);
696
705
  }
697
706
  }
707
+ buildParseFailure(target, err, scope, result) {
708
+ const duplicateOf = err instanceof errors_1.DuplicateRouteError ? err.duplicateOf : undefined;
709
+ return {
710
+ alreadyImported: false,
711
+ route: result ? new route_1.Route(result.data, result.details) : undefined,
712
+ folderUri: target.folderUri,
713
+ folderName: target.folderName,
714
+ controlFileUri: target.controlFileUri,
715
+ format: target.format,
716
+ parseError: scope.lastFailure ? this.getReadFailureMessage(scope) : (err?.message ?? String(err)),
717
+ parseErrorCode: duplicateOf ? undefined : (0, errors_1.mapErrorToImportCode)(err, scope),
718
+ duplicateOf
719
+ };
720
+ }
721
+ logParseFailure(fileName, err) {
722
+ if (err instanceof errors_1.DuplicateRouteError)
723
+ this.logEvent({ message: 'route is a duplicate in this import', file: fileName, duplicateOf: err.duplicateOf });
724
+ else
725
+ this.logEvent({ message: 'could not parse route file', file: fileName, reason: err?.message, stack: err?.stack });
726
+ }
698
727
  getReadFailureMessage(scope) {
699
728
  const name = this.getFileName(scope.failedFile);
700
729
  switch (scope.lastFailure?.reason) {
@@ -910,10 +939,10 @@ let RouteLibraryScannerService = (() => {
910
939
  delimiter: uri?.startsWith('content://') ? '%2F' : '/'
911
940
  };
912
941
  }
913
- getImportProps(parsed) {
914
- return this.importProps.routes.find(r => r.id === parsed.controlFileUri);
942
+ getImportProps(parsed, session) {
943
+ return session.routes.find(r => r.fileUri === parsed.controlFileUri);
915
944
  }
916
- buildRouteDisplayItem(parsed, observer) {
945
+ buildRouteDisplayItem(parsed, session, observer) {
917
946
  const { route, alreadyImported, parseError, format, controlFileUri, folderName } = parsed;
918
947
  const descr = route?.description ?? {};
919
948
  const [C, U] = this.getUnitConversionShortcuts();
@@ -923,9 +952,11 @@ let RouteLibraryScannerService = (() => {
923
952
  };
924
953
  const path = this.getBindings().path;
925
954
  const info = path.parse(controlFileUri);
926
- const importProps = this.getImportProps(parsed);
955
+ const importProps = this.getImportProps(parsed, session);
927
956
  return {
928
957
  id: route?.description?.id ?? info?.base,
958
+ fileUri: controlFileUri,
959
+ duplicateOf: parsed.duplicateOf,
929
960
  distance,
930
961
  label: route?.title ?? info?.base,
931
962
  folder: folderName,
@@ -215,7 +215,7 @@ let RouteListService = (() => {
215
215
  }
216
216
  if (this.initialized && !hasLists)
217
217
  emitLoadedEvent();
218
- this.observer.on('stats-update', this.onRouteStatsUpdate.bind(this));
218
+ this.subscribeStats();
219
219
  this.emit('opened', this.observer, this.stats === undefined);
220
220
  const unitsObserver = this.getUserSettings().requestNotifyOnChange('routeList', 'preferences.units');
221
221
  unitsObserver.on('changed', this.redrawCards.bind(this));
@@ -336,7 +336,7 @@ let RouteListService = (() => {
336
336
  this.setListTop('tiles', 0);
337
337
  }
338
338
  const res = this.searchRepo(filters);
339
- this.observer.on('stats-update', this.onRouteStatsUpdate.bind(this));
339
+ this.subscribeStats();
340
340
  this.emit('opened', this.observer, this.stats === undefined);
341
341
  this.getAppState().setPersistedState('page', 'search');
342
342
  return res;
@@ -1132,8 +1132,10 @@ let RouteListService = (() => {
1132
1132
  async performSync(service) {
1133
1133
  return new Promise(done => {
1134
1134
  const observer = this.getRouteSyncFactory().sync(service);
1135
- if (!observer)
1135
+ if (!observer) {
1136
1136
  done();
1137
+ return;
1138
+ }
1137
1139
  this.observer.emit('sync-start');
1138
1140
  this.syncInfo.observer = observer;
1139
1141
  const onAdded = this.onSyncRouteAdded.bind(this);
@@ -1620,6 +1622,11 @@ let RouteListService = (() => {
1620
1622
  this.emitLists('updated');
1621
1623
  });
1622
1624
  }
1625
+ onStatsUpdateHandler = (stats) => this.onRouteStatsUpdate(stats);
1626
+ subscribeStats() {
1627
+ this.observer.off('stats-update', this.onStatsUpdateHandler);
1628
+ this.observer.on('stats-update', this.onStatsUpdateHandler);
1629
+ }
1623
1630
  onRouteStatsUpdate(stats) {
1624
1631
  this.stats = stats;
1625
1632
  for (const stat of stats) {
@@ -14,6 +14,14 @@ export class RouteImportError extends Error {
14
14
  return failure;
15
15
  }
16
16
  }
17
+ export class DuplicateRouteError extends Error {
18
+ duplicateOf;
19
+ constructor(duplicateOf) {
20
+ super(`Duplicate of ${duplicateOf}`);
21
+ this.name = 'DuplicateRouteError';
22
+ this.duplicateOf = duplicateOf;
23
+ }
24
+ }
17
25
  export const mapErrorToImportCode = (err, scope) => {
18
26
  if (err instanceof RouteImportError)
19
27
  return err.code;
@@ -41,7 +41,7 @@ import { Observer } from '../../base/types';
41
41
  import { RouteParser, useParsers } from '../base/parsers';
42
42
  import { useRouteList } from '../list/service';
43
43
  import { waitNextTick } from '../../utils';
44
- import { RouteImportError, mapErrorToImportCode, toRouteImportError } from './errors';
44
+ import { DuplicateRouteError, RouteImportError, mapErrorToImportCode, toRouteImportError } from './errors';
45
45
  import { useRoutesDbLoader } from '../list/loaders/db';
46
46
  import { Route } from '../base/model/route';
47
47
  import { sleep } from '../../utils/sleep';
@@ -92,7 +92,6 @@ let RouteLibraryScannerService = (() => {
92
92
  __runInitializers(_classThis, _classExtraInitializers);
93
93
  }
94
94
  isCancelled = (__runInitializers(this, _instanceExtraInitializers), false);
95
- scanResult = [];
96
95
  importProps;
97
96
  currentScope;
98
97
  constructor() {
@@ -107,7 +106,6 @@ let RouteLibraryScannerService = (() => {
107
106
  }
108
107
  done() {
109
108
  this.importProps = undefined;
110
- this.scanResult = [];
111
109
  }
112
110
  getDisplayProps() {
113
111
  const importProps = this.importProps ?? { phase: 'landing', routes: [], hasICloudDownloadFailures: false };
@@ -153,7 +151,8 @@ let RouteLibraryScannerService = (() => {
153
151
  this.isCancelled = false;
154
152
  if (!this.importProps)
155
153
  this.prepare();
156
- this.importProps.phase = 'ingesting';
154
+ const session = this.importProps;
155
+ session.phase = 'ingesting';
157
156
  const list = this.getRouteList();
158
157
  list.pauseListUpdates();
159
158
  this._importFiles(files ?? [], observer)
@@ -166,15 +165,16 @@ let RouteLibraryScannerService = (() => {
166
165
  list.emitLists('updated', { source: 'system' });
167
166
  });
168
167
  observer.on('ingest-progress', (progress) => {
169
- if (this.importProps)
170
- this.importProps.ingestProgress = progress;
168
+ if (this.importProps !== session)
169
+ return;
170
+ session.ingestProgress = progress;
171
171
  });
172
172
  observer.on('ingest-complete', (status) => {
173
- if (!this.importProps)
173
+ if (this.importProps !== session)
174
174
  return;
175
175
  const { imported, skipped, errors, failedRoutes } = status;
176
- this.importProps.phase = 'complete';
177
- this.importProps.completionSummary = { imported, skipped, errors, failedRoutes };
176
+ session.phase = 'complete';
177
+ session.completionSummary = { imported, skipped, errors, failedRoutes };
178
178
  });
179
179
  return observer;
180
180
  }
@@ -200,9 +200,9 @@ let RouteLibraryScannerService = (() => {
200
200
  this.prepare();
201
201
  this.logEvent({ message: 'import route library', folder: folderInfo.displayName });
202
202
  this.isCancelled = false;
203
- this.scanResult = [];
204
- this.importProps.phase = 'scanning';
205
- this.importProps.scanProgress = { scannedFolders: 0, failedFolders: 0 };
203
+ const session = this.importProps;
204
+ session.phase = 'scanning';
205
+ session.scanProgress = { scannedFolders: 0, failedFolders: 0 };
206
206
  const observer = new Observer();
207
207
  this._scan(folderInfo, observer).catch(err => {
208
208
  this.logError(err, 'scan', { folder: folderInfo.displayName });
@@ -210,12 +210,16 @@ let RouteLibraryScannerService = (() => {
210
210
  });
211
211
  observer
212
212
  .on('scan-progress', (progress) => {
213
- this.importProps.scanProgress = progress;
213
+ if (this.importProps !== session)
214
+ return;
215
+ session.scanProgress = progress;
214
216
  })
215
217
  .on('scan-complete', () => {
216
- const { scannedFolders: cntScanned, failedFolders: cntFailed } = this.importProps.scanProgress;
218
+ if (this.importProps !== session)
219
+ return;
220
+ const { scannedFolders: cntScanned, failedFolders: cntFailed } = session.scanProgress;
217
221
  this.logEvent({ message: 'import route library: scan success', folder: folderInfo.displayName, cntScanned, cntFailed });
218
- this.importProps.phase = 'parsing';
222
+ session.phase = 'parsing';
219
223
  });
220
224
  return observer;
221
225
  }
@@ -223,35 +227,42 @@ let RouteLibraryScannerService = (() => {
223
227
  const observer = new Observer();
224
228
  if (!this.importProps)
225
229
  this.prepare();
226
- this.logEvent({ message: 'import route library: parse start', folder: scannedRoutes?.[0].folderName });
227
- this._parse(scannedRoutes, observer).catch(err => {
228
- this.logError(err, 'parse');
229
- observer.emit('error', err.message);
230
- });
230
+ const session = this.importProps;
231
+ this.logEvent({ message: 'import route library: parse start', folder: scannedRoutes?.[0]?.folderName });
231
232
  observer.on('parse-progress', (progress) => {
233
+ if (this.importProps !== session)
234
+ return;
232
235
  const { parsed, total } = progress;
233
- this.importProps.parseProgress = { parsed, total };
236
+ session.parseProgress = { parsed, total };
234
237
  });
235
238
  observer.on('parse-result', (route) => {
236
- const idx = this.importProps.routes.findIndex(r => r.id === route.controlFileUri);
239
+ if (this.importProps !== session)
240
+ return;
241
+ const idx = session.routes.findIndex(r => r.fileUri === route.controlFileUri);
237
242
  if (idx !== -1) {
238
- const observer = this.importProps.routes[idx].observer;
239
- const displayProps = this.buildRouteDisplayItem(route, observer);
240
- this.importProps.routes[idx] = displayProps;
243
+ const observer = session.routes[idx].observer;
244
+ const displayProps = this.buildRouteDisplayItem(route, session, observer);
245
+ session.routes[idx] = displayProps;
241
246
  if (observer) {
242
247
  observer.emit('updated', displayProps);
243
248
  }
244
249
  }
245
250
  });
246
251
  observer.on('parse-complete', () => {
252
+ if (this.importProps !== session)
253
+ return;
247
254
  const parseSumamry = {
248
- cntTotal: this.importProps.routes.length,
249
- cntSuccess: this.importProps.routes.filter(r => !r.errorReason).length,
250
- cntError: this.importProps.routes.filter(r => r.errorReason != null).length,
251
- cntExisting: this.importProps.routes.filter(r => r.alreadyImported).length,
255
+ cntTotal: session.routes.length,
256
+ cntSuccess: session.routes.filter(r => !r.errorReason).length,
257
+ cntError: session.routes.filter(r => r.errorReason != null).length,
258
+ cntExisting: session.routes.filter(r => r.alreadyImported).length,
252
259
  };
253
- this.logEvent({ message: 'import route library: parse completed', folder: scannedRoutes?.[0].folderName, parseSumamry });
254
- this.importProps.phase = 'selecting';
260
+ this.logEvent({ message: 'import route library: parse completed', folder: scannedRoutes?.[0]?.folderName, parseSumamry });
261
+ session.phase = 'selecting';
262
+ });
263
+ this._parse(scannedRoutes, observer, session).catch(err => {
264
+ this.logError(err, 'parse');
265
+ observer.emit('error', err.message);
255
266
  });
256
267
  return observer;
257
268
  }
@@ -259,9 +270,10 @@ let RouteLibraryScannerService = (() => {
259
270
  const observer = new Observer();
260
271
  if (!this.importProps)
261
272
  this.prepare();
273
+ const session = this.importProps;
262
274
  const list = this.getRouteList();
263
275
  list.pauseListUpdates();
264
- this.importProps.phase = 'ingesting';
276
+ session.phase = 'ingesting';
265
277
  this._ingest(routes, observer)
266
278
  .catch(err => {
267
279
  this.logError(err, 'ingest');
@@ -272,13 +284,17 @@ let RouteLibraryScannerService = (() => {
272
284
  list.emitLists('updated', { source: 'system' });
273
285
  });
274
286
  observer.on('ingest-progress', (progress) => {
287
+ if (this.importProps !== session)
288
+ return;
275
289
  const { current, total, currentName } = progress;
276
- this.importProps.ingestProgress = { current, total, currentName };
290
+ session.ingestProgress = { current, total, currentName };
277
291
  });
278
292
  observer.on('ingest-complete', (status) => {
279
- this.importProps.phase = 'complete';
293
+ if (this.importProps !== session)
294
+ return;
295
+ session.phase = 'complete';
280
296
  const { imported, skipped, errors, failedRoutes } = status;
281
- this.importProps.completionSummary = { imported, skipped, errors, failedRoutes };
297
+ session.completionSummary = { imported, skipped, errors, failedRoutes };
282
298
  });
283
299
  return observer;
284
300
  }
@@ -400,8 +416,8 @@ let RouteLibraryScannerService = (() => {
400
416
  await this.scanFolder(folderInfo.uri, folderInfo.displayName, ctx);
401
417
  await this.upsertImportHistory(folderInfo, ctx.discoveredCount.value);
402
418
  const { scannedFolders, failedFolders } = ctx.progress;
403
- this.logEvent({ message: 'video scan result', scannedFolders, failedFolders, files: this.scanResult.length });
404
- observer.emit('scan-complete', this.scanResult);
419
+ this.logEvent({ message: 'video scan result', scannedFolders, failedFolders, files: ctx.results.length });
420
+ observer.emit('scan-complete', ctx.results);
405
421
  }
406
422
  createScanContext(observer, parsers, recursive) {
407
423
  return {
@@ -409,7 +425,8 @@ let RouteLibraryScannerService = (() => {
409
425
  parsers,
410
426
  recursive,
411
427
  progress: { scannedFolders: 0, failedFolders: 0 },
412
- discoveredCount: { value: 0 }
428
+ discoveredCount: { value: 0 },
429
+ results: []
413
430
  };
414
431
  }
415
432
  async scanFolder(uri, folderName, ctx) {
@@ -446,7 +463,7 @@ let RouteLibraryScannerService = (() => {
446
463
  const routeAnnouncement = await this.buildDiscoveredRoute(file, files, uri, folderName, ctx.parsers);
447
464
  ctx.discoveredCount.value++;
448
465
  ctx.observer.emit('scan-result', routeAnnouncement);
449
- this.scanResult.push(routeAnnouncement);
466
+ ctx.results.push(routeAnnouncement);
450
467
  }
451
468
  }
452
469
  }
@@ -597,29 +614,30 @@ let RouteLibraryScannerService = (() => {
597
614
  scanError: skipReason
598
615
  };
599
616
  }
600
- async _parse(scannedRoutes, observer) {
617
+ async _parse(scannedRoutes, observer, session) {
601
618
  const service = this.getRouteList();
602
619
  const targets = scannedRoutes.filter(r => !r.scanError);
603
620
  const total = targets.length;
604
621
  targets.forEach(target => {
605
622
  const file = this.buildFileInfo(target.controlFileUri, target.format);
606
- this.importProps.routes.push({
623
+ session.routes.push({
607
624
  format: target.format,
608
625
  parseState: 'waiting',
609
626
  importable: false,
610
627
  label: file.base,
611
628
  folder: target.folderName,
612
629
  id: target.controlFileUri,
630
+ fileUri: target.controlFileUri,
613
631
  alreadyImported: false,
614
632
  observer: new Observer()
615
633
  });
616
634
  });
617
635
  observer.emit('parse-start');
618
- for (let i = 0; i < targets.length; i++) {
619
- if (this.isCancelled)
620
- continue;
636
+ await targets.reduce(async (previous, target, i) => {
637
+ await previous;
638
+ if (this.isCancelled || this.importProps !== session)
639
+ return;
621
640
  const parsed = i + 1;
622
- const target = targets[i];
623
641
  observer.emit('parse-progress', { current: parsed, parsed, total, currentFolder: target.folderName });
624
642
  let fileName = target.controlFileUri;
625
643
  try {
@@ -628,15 +646,15 @@ let RouteLibraryScannerService = (() => {
628
646
  fileName = info.base;
629
647
  }
630
648
  catch { }
631
- await this._parseTarget(target, service, observer);
649
+ await this._parseTarget(target, service, observer, session);
632
650
  this.logEvent({ message: 'parsing route done', fileName });
633
- }
651
+ }, Promise.resolve());
634
652
  observer.emit('parse-complete');
635
653
  }
636
- async _parseTarget(target, service, observer) {
654
+ async _parseTarget(target, service, observer, session) {
637
655
  let result;
638
656
  const file = this.buildFileInfo(target.controlFileUri, target.format);
639
- const importProps = this.importProps.routes.find(r => r.id === target.controlFileUri) ?? {};
657
+ const importProps = session.routes.find(r => r.fileUri === target.controlFileUri) ?? {};
640
658
  const scope = useExternalFileService().beginScope({ isCancelled: () => this.isCancelled });
641
659
  this.currentScope = scope;
642
660
  try {
@@ -655,10 +673,10 @@ let RouteLibraryScannerService = (() => {
655
673
  }
656
674
  importProps.parseState = 'parsed';
657
675
  const route = new Route(result.data, result.details);
658
- const existing = this.importProps.routes.find(ri => ri.id === route.description.id);
676
+ const existing = session.routes.find(ri => ri.id === route.description.id);
659
677
  if (existing) {
660
678
  result = undefined;
661
- throw new Error(`Duplicate of ${existing.label}`);
679
+ throw new DuplicateRouteError(existing.label);
662
680
  }
663
681
  if (result.data.hasVideo) {
664
682
  this.validateVideoUrl(route, target.folderUri, target.files);
@@ -678,20 +696,31 @@ let RouteLibraryScannerService = (() => {
678
696
  }
679
697
  catch (err) {
680
698
  importProps.parseState = 'parsed';
681
- const parsed = {
682
- alreadyImported: false,
683
- route: result ? new Route(result.data, result.details) : undefined,
684
- folderUri: target.folderUri,
685
- folderName: target.folderName,
686
- controlFileUri: target.controlFileUri,
687
- format: target.format,
688
- parseError: scope.lastFailure ? this.getReadFailureMessage(scope) : (err?.message ?? String(err)),
689
- parseErrorCode: mapErrorToImportCode(err, scope)
690
- };
691
- this.logEvent({ message: 'could not parse route file', file: file.base, reason: err.message, stack: err.stack });
699
+ const parsed = this.buildParseFailure(target, err, scope, result);
700
+ this.logParseFailure(file.base, err);
692
701
  observer.emit('parse-result', parsed);
693
702
  }
694
703
  }
704
+ buildParseFailure(target, err, scope, result) {
705
+ const duplicateOf = err instanceof DuplicateRouteError ? err.duplicateOf : undefined;
706
+ return {
707
+ alreadyImported: false,
708
+ route: result ? new Route(result.data, result.details) : undefined,
709
+ folderUri: target.folderUri,
710
+ folderName: target.folderName,
711
+ controlFileUri: target.controlFileUri,
712
+ format: target.format,
713
+ parseError: scope.lastFailure ? this.getReadFailureMessage(scope) : (err?.message ?? String(err)),
714
+ parseErrorCode: duplicateOf ? undefined : mapErrorToImportCode(err, scope),
715
+ duplicateOf
716
+ };
717
+ }
718
+ logParseFailure(fileName, err) {
719
+ if (err instanceof DuplicateRouteError)
720
+ this.logEvent({ message: 'route is a duplicate in this import', file: fileName, duplicateOf: err.duplicateOf });
721
+ else
722
+ this.logEvent({ message: 'could not parse route file', file: fileName, reason: err?.message, stack: err?.stack });
723
+ }
695
724
  getReadFailureMessage(scope) {
696
725
  const name = this.getFileName(scope.failedFile);
697
726
  switch (scope.lastFailure?.reason) {
@@ -907,10 +936,10 @@ let RouteLibraryScannerService = (() => {
907
936
  delimiter: uri?.startsWith('content://') ? '%2F' : '/'
908
937
  };
909
938
  }
910
- getImportProps(parsed) {
911
- return this.importProps.routes.find(r => r.id === parsed.controlFileUri);
939
+ getImportProps(parsed, session) {
940
+ return session.routes.find(r => r.fileUri === parsed.controlFileUri);
912
941
  }
913
- buildRouteDisplayItem(parsed, observer) {
942
+ buildRouteDisplayItem(parsed, session, observer) {
914
943
  const { route, alreadyImported, parseError, format, controlFileUri, folderName } = parsed;
915
944
  const descr = route?.description ?? {};
916
945
  const [C, U] = this.getUnitConversionShortcuts();
@@ -920,9 +949,11 @@ let RouteLibraryScannerService = (() => {
920
949
  };
921
950
  const path = this.getBindings().path;
922
951
  const info = path.parse(controlFileUri);
923
- const importProps = this.getImportProps(parsed);
952
+ const importProps = this.getImportProps(parsed, session);
924
953
  return {
925
954
  id: route?.description?.id ?? info?.base,
955
+ fileUri: controlFileUri,
956
+ duplicateOf: parsed.duplicateOf,
926
957
  distance,
927
958
  label: route?.title ?? info?.base,
928
959
  folder: folderName,
@@ -209,7 +209,7 @@ let RouteListService = (() => {
209
209
  }
210
210
  if (this.initialized && !hasLists)
211
211
  emitLoadedEvent();
212
- this.observer.on('stats-update', this.onRouteStatsUpdate.bind(this));
212
+ this.subscribeStats();
213
213
  this.emit('opened', this.observer, this.stats === undefined);
214
214
  const unitsObserver = this.getUserSettings().requestNotifyOnChange('routeList', 'preferences.units');
215
215
  unitsObserver.on('changed', this.redrawCards.bind(this));
@@ -330,7 +330,7 @@ let RouteListService = (() => {
330
330
  this.setListTop('tiles', 0);
331
331
  }
332
332
  const res = this.searchRepo(filters);
333
- this.observer.on('stats-update', this.onRouteStatsUpdate.bind(this));
333
+ this.subscribeStats();
334
334
  this.emit('opened', this.observer, this.stats === undefined);
335
335
  this.getAppState().setPersistedState('page', 'search');
336
336
  return res;
@@ -1126,8 +1126,10 @@ let RouteListService = (() => {
1126
1126
  async performSync(service) {
1127
1127
  return new Promise(done => {
1128
1128
  const observer = this.getRouteSyncFactory().sync(service);
1129
- if (!observer)
1129
+ if (!observer) {
1130
1130
  done();
1131
+ return;
1132
+ }
1131
1133
  this.observer.emit('sync-start');
1132
1134
  this.syncInfo.observer = observer;
1133
1135
  const onAdded = this.onSyncRouteAdded.bind(this);
@@ -1614,6 +1616,11 @@ let RouteListService = (() => {
1614
1616
  this.emitLists('updated');
1615
1617
  });
1616
1618
  }
1619
+ onStatsUpdateHandler = (stats) => this.onRouteStatsUpdate(stats);
1620
+ subscribeStats() {
1621
+ this.observer.off('stats-update', this.onStatsUpdateHandler);
1622
+ this.observer.on('stats-update', this.onStatsUpdateHandler);
1623
+ }
1617
1624
  onRouteStatsUpdate(stats) {
1618
1625
  this.stats = stats;
1619
1626
  for (const stat of stats) {
@@ -8,5 +8,9 @@ export declare class RouteImportError extends Error {
8
8
  });
9
9
  toFailure(): RouteImportFailure;
10
10
  }
11
+ export declare class DuplicateRouteError extends Error {
12
+ readonly duplicateOf: string;
13
+ constructor(duplicateOf: string);
14
+ }
11
15
  export declare const mapErrorToImportCode: (err: unknown, scope?: ExternalFileScope) => RouteImportErrorCode;
12
16
  export declare const toRouteImportError: (err: unknown, scope?: ExternalFileScope, message?: string) => RouteImportError;
@@ -7,7 +7,6 @@ import { FolderInfo, ImportDisplayProps, ParsedRoute, ScannedRoute } from './typ
7
7
  import { Route } from '../base/model/route';
8
8
  export declare class RouteLibraryScannerService extends IncyclistService {
9
9
  private isCancelled;
10
- private scanResult;
11
10
  private importProps;
12
11
  private currentScope;
13
12
  constructor();
@@ -51,6 +50,8 @@ export declare class RouteLibraryScannerService extends IncyclistService {
51
50
  private buildDiscoveredRoute;
52
51
  private _parse;
53
52
  private _parseTarget;
53
+ private buildParseFailure;
54
+ private logParseFailure;
54
55
  private getReadFailureMessage;
55
56
  private getFileName;
56
57
  private validateVideoUrl;
@@ -32,12 +32,14 @@ export interface ParsedRoute {
32
32
  alreadyImported: boolean;
33
33
  parseError?: string;
34
34
  parseErrorCode?: RouteImportErrorCode;
35
+ duplicateOf?: string;
35
36
  format: RouteFormat;
36
37
  observer?: IObserver;
37
38
  }
38
39
  export type ParseState = 'waiting' | 'parsing' | 'parsed';
39
40
  export interface RouteDisplayItem {
40
41
  id: string;
42
+ fileUri?: string;
41
43
  label: string;
42
44
  folder?: string;
43
45
  distance?: FormattedNumber;
@@ -47,6 +49,7 @@ export interface RouteDisplayItem {
47
49
  importable: boolean;
48
50
  errorReason?: string;
49
51
  errorCode?: RouteImportErrorCode;
52
+ duplicateOf?: string;
50
53
  observer: IObserver;
51
54
  }
52
55
  export interface ImportDisplayProps {
@@ -102,6 +105,7 @@ export interface ScanContext {
102
105
  discoveredCount: {
103
106
  value: number;
104
107
  };
108
+ results: ScannedRoute[];
105
109
  recursive: boolean;
106
110
  }
107
111
  export interface ImportedLibrary {
@@ -212,6 +212,8 @@ export declare class RouteListService extends IncyclistService implements IRoute
212
212
  private sortCardsBySuggestion;
213
213
  protected isSameCardSet(a: Array<RouteCard>, b: Array<RouteCard>): boolean;
214
214
  protected handleConfigChanges(): void;
215
+ protected onStatsUpdateHandler: (stats: ActiveRideCount[]) => void;
216
+ protected subscribeStats(): void;
215
217
  protected onRouteStatsUpdate(stats: ActiveRideCount[]): void;
216
218
  protected isMobile(): boolean;
217
219
  protected getUserSettings(): import("../../settings").UserSettingsService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incyclist-services",
3
- "version": "1.8.5",
3
+ "version": "1.8.6",
4
4
  "peerDependencies": {
5
5
  "gd-eventlog": "^0.1.27"
6
6
  },