incyclist-services 1.7.71 → 1.7.73

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.
Files changed (36) hide show
  1. package/lib/cjs/devices/ride/service.js +7 -0
  2. package/lib/cjs/ride/display/service.js +4 -4
  3. package/lib/cjs/ride/page/service.js +18 -0
  4. package/lib/cjs/ride/route/RouteDisplayService.js +1 -1
  5. package/lib/cjs/routes/list/cards/RouteCard.js +11 -1
  6. package/lib/cjs/workouts/list/index.js +1 -0
  7. package/lib/cjs/workouts/list/pageService.js +98 -0
  8. package/lib/esm/activities/upload/trainingpeaks.js +123 -0
  9. package/lib/esm/apps/base/api/trainingpeaks/api.js +143 -0
  10. package/lib/esm/apps/base/api/trainingpeaks/index.js +1 -0
  11. package/lib/esm/apps/base/api/trainingpeaks/types.js +1 -0
  12. package/lib/esm/apps/trainingpeaks/TrainingPeaksAppConnection.js +189 -0
  13. package/lib/esm/apps/trainingpeaks/types.js +1 -0
  14. package/lib/esm/devices/ride/service.js +7 -0
  15. package/lib/esm/ride/display/service.js +4 -4
  16. package/lib/esm/ride/page/service.js +18 -0
  17. package/lib/esm/ride/route/RouteDisplayService.js +1 -1
  18. package/lib/esm/routes/list/cards/RouteCard.js +11 -1
  19. package/lib/esm/workouts/calendar/sync/trainingpeaks/provider.js +201 -0
  20. package/lib/esm/workouts/list/index.js +1 -0
  21. package/lib/esm/workouts/list/pageService.js +94 -0
  22. package/lib/types/activities/upload/trainingpeaks.d.ts +16 -0
  23. package/lib/types/apps/base/api/trainingpeaks/api.d.ts +30 -0
  24. package/lib/types/apps/base/api/trainingpeaks/index.d.ts +2 -0
  25. package/lib/types/apps/base/api/trainingpeaks/types.d.ts +55 -0
  26. package/lib/types/apps/trainingpeaks/TrainingPeaksAppConnection.d.ts +22 -0
  27. package/lib/types/apps/trainingpeaks/types.d.ts +6 -0
  28. package/lib/types/appstate/types.d.ts +1 -1
  29. package/lib/types/ride/page/service.d.ts +1 -0
  30. package/lib/types/ride/page/types.d.ts +2 -1
  31. package/lib/types/routes/list/cards/RouteCard.d.ts +1 -0
  32. package/lib/types/routes/list/cards/types.d.ts +1 -0
  33. package/lib/types/workouts/calendar/sync/trainingpeaks/provider.d.ts +22 -0
  34. package/lib/types/workouts/list/index.d.ts +1 -0
  35. package/lib/types/workouts/list/pageService.d.ts +18 -0
  36. package/package.json +2 -2
@@ -1362,8 +1362,15 @@ let DeviceRideService = (() => {
1362
1362
  canEnforceSimulator() {
1363
1363
  const selected = (0, routes_1.getRouteList)().getSelected();
1364
1364
  const personalApiKey = this.getGoogleMaps().hasPersonalApiKey();
1365
+ if (!selected?.description)
1366
+ return false;
1365
1367
  if (selected?.description?.hasVideo || personalApiKey)
1366
1368
  return true;
1369
+ if (!selected.description.hasVideo) {
1370
+ const rideView = (0, settings_1.useUserSettings)().getValue('preferences.rideView', null);
1371
+ if (rideView === 'map')
1372
+ return true;
1373
+ }
1367
1374
  return (0, settings_1.useUserSettings)().isNewUser();
1368
1375
  }
1369
1376
  logEvent(event) {
@@ -377,7 +377,7 @@ let RideDisplayService = (() => {
377
377
  return { ...props, ...childProps };
378
378
  }
379
379
  catch (err) {
380
- this.logError(err, 'getDisplayProperties');
380
+ this.logError(err, 'getDisplayProperties', { state: this.state });
381
381
  return { state: this.state };
382
382
  }
383
383
  }
@@ -425,7 +425,7 @@ let RideDisplayService = (() => {
425
425
  async stopRide(props = {}) {
426
426
  try {
427
427
  if (this.state !== 'Starting' && this.state !== 'Idle') {
428
- this.logEvent({ message: "activity stopped", activity: this.activity?.id });
428
+ this.logEvent({ message: "activity stopped", activity: this.activity?.id, lastState: this.state });
429
429
  }
430
430
  const prevState = this.state;
431
431
  this.state = prevState === 'Starting' ? 'Closing' : 'Finished';
@@ -449,7 +449,6 @@ let RideDisplayService = (() => {
449
449
  }
450
450
  if (prevState !== 'Starting')
451
451
  this.enableScreensaver();
452
- this.state = 'Idle';
453
452
  }
454
453
  catch (err) {
455
454
  this.logError(err, 'stopRide');
@@ -458,9 +457,10 @@ let RideDisplayService = (() => {
458
457
  }
459
458
  async closePrevRide() {
460
459
  try {
461
- if (this.observer || (this.state !== 'Idle')) {
460
+ if (this.observer || (this.state !== 'Idle' && this.state !== 'Finished')) {
462
461
  await this.stopRide({ noStateUpdates: true });
463
462
  }
463
+ this.state = 'Idle';
464
464
  }
465
465
  catch (err) {
466
466
  this.logError(err, 'init');
@@ -196,6 +196,12 @@ let RidePageService = (() => {
196
196
  }
197
197
  onMenuClose() {
198
198
  try {
199
+ const state = this.getRideDisplay().getState();
200
+ if (state === 'Finished' || this.menuProps.finished) {
201
+ this.moveToPreviousPage();
202
+ this.closePage();
203
+ return;
204
+ }
199
205
  this.menuProps = null;
200
206
  this.updatePageDisplay();
201
207
  }
@@ -223,6 +229,15 @@ let RidePageService = (() => {
223
229
  this.logError(err, 'onResume');
224
230
  }
225
231
  }
232
+ onFinished() {
233
+ try {
234
+ this.menuProps = { showResume: false, finished: true };
235
+ this.updatePageDisplay();
236
+ }
237
+ catch (err) {
238
+ this.logError(err, 'onResume');
239
+ }
240
+ }
226
241
  onEndRide() {
227
242
  try {
228
243
  this.getRideDisplay().stop();
@@ -336,6 +351,9 @@ let RidePageService = (() => {
336
351
  case 'Paused':
337
352
  this.menuProps = { showResume: true };
338
353
  break;
354
+ case 'Finished':
355
+ this.menuProps = { showResume: false, finished: true };
356
+ break;
339
357
  case 'Active':
340
358
  this.menuProps = null;
341
359
  break;
@@ -276,7 +276,7 @@ let RouteDisplayService = (() => {
276
276
  const mode = this.getDeviceRide().getCyclingMode();
277
277
  const isSIM = mode?.isSIM();
278
278
  const realityFactor = this.startSettings?.realityFactor ?? 100;
279
- const targetSlope = (this.position.slope ?? 0) * realityFactor / 100;
279
+ const targetSlope = (this.position?.slope ?? 0) * realityFactor / 100;
280
280
  if (props?.limits && !isSIM) {
281
281
  delete this.prevRequestSlope;
282
282
  return { ...props?.limits, slope: targetSlope };
@@ -53,6 +53,7 @@ const monitoring_1 = require("../../../monitoring");
53
53
  const geo_1 = require("../../../utils/geo");
54
54
  const i18n_1 = require("../../../i18n");
55
55
  const decorators_1 = require("../../../base/decorators");
56
+ const appstate_1 = require("../../../appstate");
56
57
  exports.DEFAULT_TITLE = 'Import Route';
57
58
  exports.DEFAULT_FILTERS = [
58
59
  { name: 'Routes', extensions: ['gpx', 'epm', 'xml', 'rlv', 'pgmf'] },
@@ -79,13 +80,16 @@ let RouteCard = (() => {
79
80
  let _instanceExtraInitializers = [];
80
81
  let _getBindings_decorators;
81
82
  let _getOnlineStatusMonitoring_decorators;
83
+ let _getAppState_decorators;
82
84
  return class RouteCard extends _classSuper {
83
85
  static {
84
86
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
85
87
  _getBindings_decorators = [decorators_1.Injectable];
86
88
  _getOnlineStatusMonitoring_decorators = [decorators_1.Injectable];
89
+ _getAppState_decorators = [decorators_1.Injectable];
87
90
  __esDecorate(this, null, _getBindings_decorators, { kind: "method", name: "getBindings", static: false, private: false, access: { has: obj => "getBindings" in obj, get: obj => obj.getBindings }, metadata: _metadata }, null, _instanceExtraInitializers);
88
91
  __esDecorate(this, null, _getOnlineStatusMonitoring_decorators, { kind: "method", name: "getOnlineStatusMonitoring", static: false, private: false, access: { has: obj => "getOnlineStatusMonitoring" in obj, get: obj => obj.getOnlineStatusMonitoring }, metadata: _metadata }, null, _instanceExtraInitializers);
92
+ __esDecorate(this, null, _getAppState_decorators, { kind: "method", name: "getAppState", static: false, private: false, access: { has: obj => "getAppState" in obj, get: obj => obj.getAppState }, metadata: _metadata }, null, _instanceExtraInitializers);
89
93
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
90
94
  }
91
95
  downloadObserver = __runInitializers(this, _instanceExtraInitializers);
@@ -370,11 +374,14 @@ let RouteCard = (() => {
370
374
  }
371
375
  let showLoopOverwrite, showNextOverwrite;
372
376
  let hasWorkout = true;
377
+ let showWorkoutOption = false;
373
378
  try {
374
379
  showLoopOverwrite = this.route?.description?.isLoop;
375
380
  const next = (0, route_1.getNextVideoId)(this.route);
376
381
  showNextOverwrite = !!next;
377
382
  hasWorkout = (0, valid_1.valid)(workouts.getSelected());
383
+ const workoutsEnabled = this.isMobile() ? this.getAppState().hasFeature('MOBILE_WORKOUTS') : true;
384
+ showWorkoutOption = workoutsEnabled && !hasWorkout;
378
385
  if (showNextOverwrite) {
379
386
  const card = (0, service_1.getRouteList)().getCard(next);
380
387
  const route = card?.getData();
@@ -392,7 +399,7 @@ let RouteCard = (() => {
392
399
  const totalElevation = { value: C(this.route.description.elevation, 'elevation', { digits: 0 }), unit: uEl };
393
400
  const xScale = { value: C(1, 'distance'), unit: uDist };
394
401
  const yScale = { value: C(1, 'elevation'), unit: uEl };
395
- return { settings: uiSettings, totalDistance, totalElevation, showLoopOverwrite, showNextOverwrite, hasWorkout, canStart, videoChecking, videoMissing,
402
+ return { settings: uiSettings, totalDistance, totalElevation, showLoopOverwrite, showNextOverwrite, hasWorkout, showWorkoutOption, canStart, videoChecking, videoMissing,
396
403
  xScale, yScale,
397
404
  updateStartPos: this.updateStartPos.bind(this),
398
405
  updateMarkers: this.updateMarkers.bind(this)
@@ -909,6 +916,9 @@ let RouteCard = (() => {
909
916
  getOnlineStatusMonitoring() {
910
917
  return (0, monitoring_1.useOnlineStatusMonitoring)();
911
918
  }
919
+ getAppState() {
920
+ return (0, appstate_1.useAppState)();
921
+ }
912
922
  getRouteDownload() {
913
923
  return (0, service_2.useRouteDownload)();
914
924
  }
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./service"), exports);
18
18
  __exportStar(require("./cards"), exports);
19
+ __exportStar(require("./pageService"), exports);
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
3
+ var useValue = arguments.length > 2;
4
+ for (var i = 0; i < initializers.length; i++) {
5
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
6
+ }
7
+ return useValue ? value : void 0;
8
+ };
9
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
10
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
11
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
12
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
13
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
14
+ var _, done = false;
15
+ for (var i = decorators.length - 1; i >= 0; i--) {
16
+ var context = {};
17
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
18
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
19
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
20
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
21
+ if (kind === "accessor") {
22
+ if (result === void 0) continue;
23
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
24
+ if (_ = accept(result.get)) descriptor.get = _;
25
+ if (_ = accept(result.set)) descriptor.set = _;
26
+ if (_ = accept(result.init)) initializers.unshift(_);
27
+ }
28
+ else if (_ = accept(result)) {
29
+ if (kind === "field") initializers.unshift(_);
30
+ else descriptor[key] = _;
31
+ }
32
+ }
33
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
34
+ done = true;
35
+ };
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.getWorkoutListPageService = exports.WorkoutListPageService = void 0;
38
+ const types_1 = require("../../base/types");
39
+ const pages_1 = require("../../base/pages");
40
+ const decorators_1 = require("../../base/decorators");
41
+ const appstate_1 = require("../../appstate");
42
+ let WorkoutListPageService = (() => {
43
+ let _classDecorators = [types_1.Singleton];
44
+ let _classDescriptor;
45
+ let _classExtraInitializers = [];
46
+ let _classThis;
47
+ let _classSuper = pages_1.IncyclistPageService;
48
+ let _instanceExtraInitializers = [];
49
+ let _getAppState_decorators;
50
+ var WorkoutListPageService = class extends _classSuper {
51
+ static { _classThis = this; }
52
+ static {
53
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
54
+ _getAppState_decorators = [decorators_1.Injectable];
55
+ __esDecorate(this, null, _getAppState_decorators, { kind: "method", name: "getAppState", static: false, private: false, access: { has: obj => "getAppState" in obj, get: obj => obj.getAppState }, metadata: _metadata }, null, _instanceExtraInitializers);
56
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
57
+ WorkoutListPageService = _classThis = _classDescriptor.value;
58
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
59
+ __runInitializers(_classThis, _classExtraInitializers);
60
+ }
61
+ constructor() {
62
+ super('WorkoutListPage');
63
+ __runInitializers(this, _instanceExtraInitializers);
64
+ }
65
+ openPage() {
66
+ try {
67
+ super.openPage();
68
+ this.logEvent({ message: 'page shown', page: 'Workouts' });
69
+ return this.getPageObserver();
70
+ }
71
+ catch (err) {
72
+ this.logError(err, 'openPage');
73
+ }
74
+ }
75
+ closePage() {
76
+ try {
77
+ this.logEvent({ message: 'page closed', page: 'Workouts' });
78
+ super.closePage();
79
+ }
80
+ catch (err) {
81
+ this.logError(err, 'closePage');
82
+ }
83
+ }
84
+ getPageDisplayProps() {
85
+ if (!this.getAppState().hasFeature('MOBILE_WORKOUTS')) {
86
+ return { pageType: 'placeholder' };
87
+ }
88
+ return { pageType: 'list' };
89
+ }
90
+ getAppState() {
91
+ return (0, appstate_1.useAppState)();
92
+ }
93
+ };
94
+ return WorkoutListPageService = _classThis;
95
+ })();
96
+ exports.WorkoutListPageService = WorkoutListPageService;
97
+ const getWorkoutListPageService = () => new WorkoutListPageService();
98
+ exports.getWorkoutListPageService = getWorkoutListPageService;
@@ -0,0 +1,123 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
8
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
+ var _, done = false;
14
+ for (var i = decorators.length - 1; i >= 0; i--) {
15
+ var context = {};
16
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
+ if (kind === "accessor") {
21
+ if (result === void 0) continue;
22
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
+ if (_ = accept(result.get)) descriptor.get = _;
24
+ if (_ = accept(result.set)) descriptor.set = _;
25
+ if (_ = accept(result.init)) initializers.unshift(_);
26
+ }
27
+ else if (_ = accept(result)) {
28
+ if (kind === "field") initializers.unshift(_);
29
+ else descriptor[key] = _;
30
+ }
31
+ }
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
35
+ import { Injectable } from '../../base/decorators/Injection';
36
+ import { IncyclistService } from '../../base/service';
37
+ import { Singleton } from '../../base/types';
38
+ import { TrainingPeaksAppConnection } from '../../apps/trainingpeaks/TrainingPeaksAppConnection';
39
+ import { ActivityUploadFactory } from './factory';
40
+ let TrainingPeaksUpload = (() => {
41
+ let _classDecorators = [Singleton];
42
+ let _classDescriptor;
43
+ let _classExtraInitializers = [];
44
+ let _classThis;
45
+ let _classSuper = IncyclistService;
46
+ let _instanceExtraInitializers = [];
47
+ let _getConnection_decorators;
48
+ var TrainingPeaksUpload = class extends _classSuper {
49
+ static { _classThis = this; }
50
+ static {
51
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
52
+ _getConnection_decorators = [Injectable];
53
+ __esDecorate(this, null, _getConnection_decorators, { kind: "method", name: "getConnection", static: false, private: false, access: { has: obj => "getConnection" in obj, get: obj => obj.getConnection }, metadata: _metadata }, null, _instanceExtraInitializers);
54
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
55
+ TrainingPeaksUpload = _classThis = _classDescriptor.value;
56
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
57
+ __runInitializers(_classThis, _classExtraInitializers);
58
+ }
59
+ isInitialized = __runInitializers(this, _instanceExtraInitializers);
60
+ connection = new TrainingPeaksAppConnection();
61
+ constructor() {
62
+ super('TrainingPeaksUpload');
63
+ this.isInitialized = false;
64
+ this.init();
65
+ }
66
+ init() {
67
+ if (this.isInitialized)
68
+ return true;
69
+ this.isInitialized = this.getConnection().init();
70
+ return this.isInitialized;
71
+ }
72
+ isConnected() {
73
+ return this.getConnection().isConnected();
74
+ }
75
+ async upload(activity, format = 'TCX') {
76
+ try {
77
+ const ok = this.ensureInitialized();
78
+ if (!ok) {
79
+ this.logEvent({ message: 'TrainingPeaks upload skipped', reason: 'not initialized' });
80
+ return false;
81
+ }
82
+ if (!this.isConnected())
83
+ return false;
84
+ const lcFormat = format.toLowerCase();
85
+ const ucFormat = format.toUpperCase();
86
+ this.logEvent({ message: 'TrainingPeaks upload', format: ucFormat });
87
+ if (!activity.links)
88
+ activity.links = {};
89
+ const fileName = activity[`${lcFormat}FileName`];
90
+ const res = await this.getApi().upload(fileName, { name: activity.title, description: '' });
91
+ this.logEvent({ message: 'TrainingPeaks upload success', activityId: res.trainingPeaksId });
92
+ activity.links.trainingpeaks = {
93
+ activity_id: String(res.trainingPeaksId),
94
+ url: this.getUrl(res.trainingPeaksId)
95
+ };
96
+ return true;
97
+ }
98
+ catch (err) {
99
+ this.logEvent({ message: 'TrainingPeaks upload failure', error: err.message });
100
+ activity.links.trainingpeaks = { error: err.message };
101
+ return false;
102
+ }
103
+ }
104
+ getUrl(activityId) {
105
+ return `https://www.trainingpeaks.com/athlete/workouts/${activityId}`;
106
+ }
107
+ ensureInitialized() {
108
+ if (!this.isInitialized)
109
+ this.init();
110
+ return this.isInitialized;
111
+ }
112
+ getConnection() {
113
+ return this.connection;
114
+ }
115
+ getApi() {
116
+ return this.getConnection().getApi();
117
+ }
118
+ };
119
+ return TrainingPeaksUpload = _classThis;
120
+ })();
121
+ export { TrainingPeaksUpload };
122
+ const factory = new ActivityUploadFactory();
123
+ factory.add('trainingpeaks', new TrainingPeaksUpload());
@@ -0,0 +1,143 @@
1
+ import { EventLogger } from 'gd-eventlog';
2
+ import { Observer } from '../../../../base/types/observer';
3
+ import { valid } from '../../../../utils/valid';
4
+ import { sleep } from '../../../../utils/sleep';
5
+ import { AppApiBase } from '../base';
6
+ const UPLOAD_POLL_INTERVAL_MS = 2000;
7
+ const UPLOAD_TIMEOUT_MS = 60000;
8
+ export class TrainingPeaksApi extends AppApiBase {
9
+ config;
10
+ logger;
11
+ observer;
12
+ constructor() {
13
+ super();
14
+ this.logger = new EventLogger('TrainingPeaksApi');
15
+ this.observer = new Observer();
16
+ }
17
+ getBaseUrl() {
18
+ return process.env.TRAININGPEAKS_API || 'https://api.sandbox.trainingpeaks.com';
19
+ }
20
+ getOauthBaseUrl() {
21
+ return process.env.TRAININGPEAKS_OAUTH_URL || 'https://oauth.sandbox.trainingpeaks.com';
22
+ }
23
+ init(config) {
24
+ this.config = config;
25
+ return this.observer;
26
+ }
27
+ update(config) {
28
+ if (!this.config)
29
+ throw new Error('update not allowed before init');
30
+ this.config = { ...this.config, ...config };
31
+ }
32
+ isAuthenticated() {
33
+ return valid(this.config?.accessToken);
34
+ }
35
+ async getLoggedInAthlete() {
36
+ await this.verifyAuthentication();
37
+ const response = await this.get('/v1/athlete/profile');
38
+ return response.data;
39
+ }
40
+ async getCalendarWorkouts(opts) {
41
+ await this.verifyAuthentication();
42
+ const oldest = opts?.oldest ?? new Date();
43
+ const newest = opts?.newest ?? new Date(oldest.valueOf() + 30 * 24 * 60 * 60 * 1000);
44
+ const fmt = (d) => d.toISOString().slice(0, 10);
45
+ const response = await this.get(`/v2/workouts/${fmt(oldest)}/${fmt(newest)}`);
46
+ return response.data;
47
+ }
48
+ async getWorkoutFile(id, format = 'zwo') {
49
+ await this.verifyAuthentication();
50
+ const response = await this.get(`/v2/workouts/wod/file/${id}?format=${format}`);
51
+ return response.data;
52
+ }
53
+ async upload(fileName, props) {
54
+ await this.verifyAuthentication();
55
+ const reqOpts = {
56
+ headers: { Authorization: `bearer ${this.config.accessToken}` }
57
+ };
58
+ const form = await this.createForm('/v3/file', { file: { type: 'file', fileName } }, reqOpts);
59
+ const response = await this.postForm(form);
60
+ const location = response.headers?.['location'] ?? '';
61
+ const trackingId = location.split('/').filter(Boolean).pop();
62
+ if (!trackingId) {
63
+ throw new Error('TrainingPeaks upload failed: no tracking ID returned');
64
+ }
65
+ return this.waitForUploadComplete(trackingId, props?.externalId);
66
+ }
67
+ async getUploadStatus(trackingId) {
68
+ await this.verifyAuthentication();
69
+ const response = await this.get(`/v3/status/${trackingId}`);
70
+ return response.data;
71
+ }
72
+ async verifyAuthentication() {
73
+ if (!this.isAuthenticated())
74
+ throw new Error('not authenticated');
75
+ await this.verifyToken();
76
+ }
77
+ async verifyToken() {
78
+ if (!this.isTokenStillValid()) {
79
+ try {
80
+ await this.refreshToken();
81
+ }
82
+ catch {
83
+ throw new Error('token refresh failed');
84
+ }
85
+ }
86
+ }
87
+ isTokenStillValid() {
88
+ if (!this.config?.expiration)
89
+ return false;
90
+ return Date.now() < this.config.expiration.valueOf();
91
+ }
92
+ async refreshToken() {
93
+ this.logger.logEvent({ message: 'TrainingPeaks refresh token start' });
94
+ try {
95
+ const response = await this.getApi().request({
96
+ method: 'post',
97
+ url: `${this.getOauthBaseUrl()}/oauth/token`,
98
+ data: new URLSearchParams({
99
+ grant_type: 'refresh_token',
100
+ refresh_token: this.config.refreshToken,
101
+ client_id: this.config.clientId,
102
+ client_secret: this.config.clientSecret,
103
+ }).toString(),
104
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
105
+ });
106
+ const data = response.data;
107
+ this.config.accessToken = data.access_token;
108
+ this.config.refreshToken = data.refresh_token;
109
+ this.config.expiration = new Date(Date.now() + data.expires_in * 1000);
110
+ this.logger.logEvent({ message: 'TrainingPeaks refresh token success' });
111
+ this.observer.emit('token.updated', this.config);
112
+ }
113
+ catch (err) {
114
+ this.logger.logEvent({ message: 'TrainingPeaks refresh token error', error: err.message });
115
+ throw err;
116
+ }
117
+ }
118
+ async waitForUploadComplete(trackingId, externalId) {
119
+ const deadline = Date.now() + UPLOAD_TIMEOUT_MS;
120
+ while (Date.now() < deadline) {
121
+ const status = await this.getUploadStatus(trackingId);
122
+ if (status.Completed) {
123
+ if (!status.WorkoutIds?.length) {
124
+ throw new Error(`TrainingPeaks upload failed: ${status.Status}`);
125
+ }
126
+ return { trainingPeaksId: status.WorkoutIds[0], externalId };
127
+ }
128
+ await sleep(UPLOAD_POLL_INTERVAL_MS);
129
+ }
130
+ throw new Error('TrainingPeaks upload timed out');
131
+ }
132
+ async get(url, config) {
133
+ const props = config ?? {};
134
+ const response = await this.getApi().request({
135
+ method: 'get',
136
+ url: this.getBaseUrl() + url,
137
+ headers: { Authorization: `bearer ${this.config.accessToken}` },
138
+ validateStatus: (status) => (status >= 200 && status < 300) || status === 403,
139
+ ...props
140
+ });
141
+ return response;
142
+ }
143
+ }
@@ -0,0 +1 @@
1
+ export { TrainingPeaksApi } from './api';
@@ -0,0 +1 @@
1
+ export {};