incyclist-services 1.7.139 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/lib/cjs/activities/active-rides/service.js +1 -1
  2. package/lib/cjs/activities/page/service.js +211 -7
  3. package/lib/cjs/api/bindings/index.js +1 -0
  4. package/lib/cjs/fileaccess/externalFiles.js +39 -32
  5. package/lib/cjs/fileaccess/service.js +42 -28
  6. package/lib/cjs/fileaccess/utils.js +6 -2
  7. package/lib/cjs/index.js +1 -0
  8. package/lib/cjs/ride/page/service.js +168 -8
  9. package/lib/cjs/routes/index.js +1 -0
  10. package/lib/cjs/routes/library/service.js +118 -22
  11. package/lib/cjs/routes/list/cards/RouteCard.js +19 -2
  12. package/lib/cjs/routes/list/service.js +82 -48
  13. package/lib/cjs/routes/page/service.js +318 -12
  14. package/lib/cjs/routes/previews/store.js +9 -10
  15. package/lib/cjs/routes/video-availability/journal.js +2 -2
  16. package/lib/cjs/routes/video-availability/pageActions.js +19 -3
  17. package/lib/cjs/routes/video-availability/service.js +29 -20
  18. package/lib/esm/activities/active-rides/service.js +1 -1
  19. package/lib/esm/activities/page/service.js +211 -7
  20. package/lib/esm/api/bindings/index.js +1 -0
  21. package/lib/esm/fileaccess/externalFiles.js +39 -32
  22. package/lib/esm/fileaccess/service.js +42 -28
  23. package/lib/esm/fileaccess/utils.js +6 -2
  24. package/lib/esm/index.js +1 -0
  25. package/lib/esm/ride/page/service.js +168 -8
  26. package/lib/esm/routes/index.js +1 -0
  27. package/lib/esm/routes/library/service.js +118 -22
  28. package/lib/esm/routes/list/cards/RouteCard.js +19 -2
  29. package/lib/esm/routes/list/service.js +82 -48
  30. package/lib/esm/routes/page/service.js +318 -12
  31. package/lib/esm/routes/previews/store.js +9 -10
  32. package/lib/esm/routes/video-availability/journal.js +2 -2
  33. package/lib/esm/routes/video-availability/pageActions.js +19 -3
  34. package/lib/esm/routes/video-availability/service.js +29 -20
  35. package/lib/types/activities/active-rides/types.d.ts +1 -0
  36. package/lib/types/activities/page/service.d.ts +30 -2
  37. package/lib/types/activities/page/types.d.ts +22 -2
  38. package/lib/types/api/bindings/index.d.ts +2 -0
  39. package/lib/types/api/types.d.ts +1 -0
  40. package/lib/types/api/ui/index.d.ts +6 -1
  41. package/lib/types/fileaccess/externalFiles.d.ts +2 -0
  42. package/lib/types/fileaccess/service.d.ts +4 -0
  43. package/lib/types/index.d.ts +1 -0
  44. package/lib/types/ride/page/service.d.ts +27 -0
  45. package/lib/types/ride/page/types.d.ts +5 -0
  46. package/lib/types/routes/base/types/index.d.ts +1 -0
  47. package/lib/types/routes/index.d.ts +1 -0
  48. package/lib/types/routes/library/service.d.ts +7 -0
  49. package/lib/types/routes/library/types.d.ts +8 -0
  50. package/lib/types/routes/list/cards/RouteCard.d.ts +4 -0
  51. package/lib/types/routes/list/cards/types.d.ts +2 -0
  52. package/lib/types/routes/list/service.d.ts +2 -0
  53. package/lib/types/routes/page/service.d.ts +46 -1
  54. package/lib/types/routes/page/types.d.ts +59 -1
  55. package/lib/types/routes/types.d.ts +1 -0
  56. package/lib/types/routes/video-availability/pageActions.d.ts +2 -1
  57. package/lib/types/routes/video-availability/service.d.ts +3 -1
  58. package/lib/types/types.d.ts +1 -0
  59. package/package.json +1 -1
@@ -720,7 +720,7 @@ let ActiveRidesService = (() => {
720
720
  if (!this.current || !this.isOnline)
721
721
  return;
722
722
  const topic = `incyclist/activity/${this.session}/${this.getRouteHash()}/update`;
723
- this.getMessageQueue().sendMessage(topic, payload);
723
+ this.getMessageQueue().sendMessage(topic, { ...payload, ts: Date.now() });
724
724
  }
725
725
  async onActivityUpdateEvent(session, payload) {
726
726
  try {
@@ -42,6 +42,9 @@ const list_1 = require("../list");
42
42
  const sleep_1 = require("../../utils/sleep");
43
43
  const workouts_1 = require("../../workouts");
44
44
  const devices_1 = require("../../devices");
45
+ const service_1 = require("../../fileaccess/service");
46
+ const service_2 = require("../../routes/video-availability/service");
47
+ const pageActions_1 = require("../../routes/video-availability/pageActions");
45
48
  let ActivitiesPageService = (() => {
46
49
  let _classDecorators = [decorators_1.Singleton];
47
50
  let _classDescriptor;
@@ -52,6 +55,9 @@ let ActivitiesPageService = (() => {
52
55
  let _getActivityList_decorators;
53
56
  let _getWorkoutList_decorators;
54
57
  let _getDevicePairing_decorators;
58
+ let _getFolderAccess_decorators;
59
+ let _getAvailability_decorators;
60
+ let _getVideoActions_decorators;
55
61
  var ActivitiesPageService = class extends _classSuper {
56
62
  static { _classThis = this; }
57
63
  static {
@@ -59,9 +65,15 @@ let ActivitiesPageService = (() => {
59
65
  _getActivityList_decorators = [decorators_1.Injectable];
60
66
  _getWorkoutList_decorators = [decorators_1.Injectable];
61
67
  _getDevicePairing_decorators = [decorators_1.Injectable];
68
+ _getFolderAccess_decorators = [decorators_1.Injectable];
69
+ _getAvailability_decorators = [decorators_1.Injectable];
70
+ _getVideoActions_decorators = [decorators_1.Injectable];
62
71
  __esDecorate(this, null, _getActivityList_decorators, { kind: "method", name: "getActivityList", static: false, private: false, access: { has: obj => "getActivityList" in obj, get: obj => obj.getActivityList }, metadata: _metadata }, null, _instanceExtraInitializers);
63
72
  __esDecorate(this, null, _getWorkoutList_decorators, { kind: "method", name: "getWorkoutList", static: false, private: false, access: { has: obj => "getWorkoutList" in obj, get: obj => obj.getWorkoutList }, metadata: _metadata }, null, _instanceExtraInitializers);
64
73
  __esDecorate(this, null, _getDevicePairing_decorators, { kind: "method", name: "getDevicePairing", static: false, private: false, access: { has: obj => "getDevicePairing" in obj, get: obj => obj.getDevicePairing }, metadata: _metadata }, null, _instanceExtraInitializers);
74
+ __esDecorate(this, null, _getFolderAccess_decorators, { kind: "method", name: "getFolderAccess", static: false, private: false, access: { has: obj => "getFolderAccess" in obj, get: obj => obj.getFolderAccess }, metadata: _metadata }, null, _instanceExtraInitializers);
75
+ __esDecorate(this, null, _getAvailability_decorators, { kind: "method", name: "getAvailability", static: false, private: false, access: { has: obj => "getAvailability" in obj, get: obj => obj.getAvailability }, metadata: _metadata }, null, _instanceExtraInitializers);
76
+ __esDecorate(this, null, _getVideoActions_decorators, { kind: "method", name: "getVideoActions", static: false, private: false, access: { has: obj => "getVideoActions" in obj, get: obj => obj.getVideoActions }, metadata: _metadata }, null, _instanceExtraInitializers);
65
77
  __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
66
78
  ActivitiesPageService = _classThis = _classDescriptor.value;
67
79
  if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
@@ -71,6 +83,9 @@ let ActivitiesPageService = (() => {
71
83
  listState;
72
84
  detailActivityId;
73
85
  isOpen = false;
86
+ rideAgainRoute;
87
+ rideAgainCheck = null;
88
+ rideAgainVideoUpdateHandler = this.onRideAgainVideoUpdate.bind(this);
74
89
  constructor() {
75
90
  super('ActivitiesPage');
76
91
  }
@@ -78,6 +93,7 @@ let ActivitiesPageService = (() => {
78
93
  if (this.isOpen) {
79
94
  this.getActivityList().closeList();
80
95
  this.stopEventListener();
96
+ this.closeRideAgainCheck();
81
97
  }
82
98
  try {
83
99
  this.logEvent({ message: 'page shown', page: 'Activities' });
@@ -102,6 +118,7 @@ let ActivitiesPageService = (() => {
102
118
  this.logEvent({ message: 'page closed', page: 'Activities' });
103
119
  this.getActivityList().closeList();
104
120
  this.stopEventListener();
121
+ this.closeRideAgainCheck();
105
122
  this.isOpen = false;
106
123
  super.closePage();
107
124
  }
@@ -121,6 +138,7 @@ let ActivitiesPageService = (() => {
121
138
  resumePage() {
122
139
  try {
123
140
  this.startEventListener();
141
+ this.getAvailability().onForeground().catch(err => this.logError(err, 'resumePage'));
124
142
  return super.resumePage();
125
143
  }
126
144
  catch (err) {
@@ -131,7 +149,8 @@ let ActivitiesPageService = (() => {
131
149
  const props = {
132
150
  loading: this.listState.loading,
133
151
  activities: this.listState.activities,
134
- detailActivityId: this.detailActivityId
152
+ detailActivityId: this.detailActivityId,
153
+ rideAgainCheck: this.rideAgainCheck
135
154
  };
136
155
  return props;
137
156
  }
@@ -165,16 +184,128 @@ let ActivitiesPageService = (() => {
165
184
  return { activityId, attachedWorkout: null };
166
185
  }
167
186
  }
168
- onRideAgain(route) {
187
+ async onRideAgain(route) {
169
188
  try {
170
- const pairing = this.getDevicePairing();
171
- const readyToStart = pairing.isReadyToStart();
172
- this.logEvent({ message: 'Attempting to start a ride again', id: route?.id, title: route?.title, readyToStart });
173
- const next = readyToStart ? '/rideDeviceOK' : '/pairingStart';
174
- this.moveTo(next);
189
+ if (!route?.id || !this.getVideoActions().isSupported()) {
190
+ this.navigateToRide(route);
191
+ return 'started';
192
+ }
193
+ let status;
194
+ try {
195
+ await this.getFolderAccess().activateAll();
196
+ status = await this.getAvailability().refresh(route.id);
197
+ }
198
+ catch (err) {
199
+ this.logError(err, 'onRideAgain');
200
+ this.navigateToRide(route);
201
+ return 'started';
202
+ }
203
+ if (status.state === 'ready' || status.state === 'unknown') {
204
+ this.closeRideAgainCheck();
205
+ this.navigateToRide(route);
206
+ return 'started';
207
+ }
208
+ this.openRideAgainCheck(route);
209
+ return 'blocked';
175
210
  }
176
211
  catch (err) {
177
212
  this.logError(err, 'onRideAgain');
213
+ return 'started';
214
+ }
215
+ }
216
+ onRideAgainCheckClosed() {
217
+ try {
218
+ this.closeRideAgainCheck();
219
+ this.updatePageDisplay();
220
+ }
221
+ catch (err) {
222
+ this.logError(err, 'onRideAgainCheckClosed');
223
+ }
224
+ }
225
+ onRideAgainCheckStart() {
226
+ const route = this.rideAgainRoute;
227
+ if (!route)
228
+ return Promise.resolve('blocked');
229
+ return this.onRideAgain(route);
230
+ }
231
+ onVideoDownloadPressed(routeId) {
232
+ try {
233
+ this.getVideoActions().onVideoDownloadPressed(routeId);
234
+ }
235
+ catch (err) {
236
+ this.logError(err, 'onVideoDownloadPressed');
237
+ }
238
+ }
239
+ onVideoDownloadConfirmed(routeId, choice) {
240
+ try {
241
+ this.getVideoActions().onVideoDownloadConfirmed(routeId, choice);
242
+ }
243
+ catch (err) {
244
+ this.logError(err, 'onVideoDownloadConfirmed');
245
+ }
246
+ }
247
+ onVideoDownloadDismissed(routeId) {
248
+ try {
249
+ this.getVideoActions().onVideoDownloadDismissed(routeId);
250
+ }
251
+ catch (err) {
252
+ this.logError(err, 'onVideoDownloadDismissed');
253
+ }
254
+ }
255
+ onVideoStop(routeId) {
256
+ try {
257
+ this.getVideoActions().onVideoStop(routeId);
258
+ }
259
+ catch (err) {
260
+ this.logError(err, 'onVideoStop');
261
+ }
262
+ }
263
+ onVideoRetry(routeId) {
264
+ try {
265
+ this.getVideoActions().onVideoRetry(routeId);
266
+ }
267
+ catch (err) {
268
+ this.logError(err, 'onVideoRetry');
269
+ }
270
+ }
271
+ onVideoKeepInstead(routeId) {
272
+ try {
273
+ this.getVideoActions().onVideoKeepInstead(routeId);
274
+ }
275
+ catch (err) {
276
+ this.logError(err, 'onVideoKeepInstead');
277
+ }
278
+ }
279
+ onVideoRemovePressed(routeId) {
280
+ try {
281
+ this.getVideoActions().onVideoRemovePressed(routeId);
282
+ }
283
+ catch (err) {
284
+ this.logError(err, 'onVideoRemovePressed');
285
+ }
286
+ }
287
+ onVideoRemoveConfirmed(routeId) {
288
+ try {
289
+ this.getVideoActions().onVideoRemoveConfirmed(routeId);
290
+ }
291
+ catch (err) {
292
+ this.logError(err, 'onVideoRemoveConfirmed');
293
+ }
294
+ }
295
+ onVideoRemoveDismissed(routeId) {
296
+ try {
297
+ this.getVideoActions().onVideoRemoveDismissed(routeId);
298
+ }
299
+ catch (err) {
300
+ this.logError(err, 'onVideoRemoveDismissed');
301
+ }
302
+ }
303
+ async onConfirmAccess(routeId) {
304
+ try {
305
+ await this.getVideoActions().onConfirmAccess(routeId);
306
+ }
307
+ catch (err) {
308
+ this.logError(err, 'onConfirmAccess');
178
309
  }
179
310
  }
180
311
  onClearWorkoutSelection() {
@@ -198,6 +329,70 @@ let ActivitiesPageService = (() => {
198
329
  updatePageDisplay() {
199
330
  this.getPageObserver()?.emit('page-update');
200
331
  }
332
+ navigateToRide(route) {
333
+ const pairing = this.getDevicePairing();
334
+ const readyToStart = pairing.isReadyToStart();
335
+ this.logEvent({ message: 'Attempting to start a ride again', id: route?.id, title: route?.title, readyToStart });
336
+ const next = readyToStart ? '/rideDeviceOK' : '/pairingStart';
337
+ this.moveTo(next);
338
+ }
339
+ openRideAgainCheck(route) {
340
+ this.rideAgainRoute = route;
341
+ this.rideAgainCheck = {
342
+ routeId: route.id,
343
+ routeTitle: route.title ?? '',
344
+ video: this.getVideoActions().getDisplayProps(route.id),
345
+ downloadedWhileOpen: false
346
+ };
347
+ this.subscribeToRideAgainVideoUpdates();
348
+ this.updatePageDisplay();
349
+ }
350
+ closeRideAgainCheck() {
351
+ if (!this.rideAgainCheck && !this.rideAgainRoute)
352
+ return;
353
+ this.unsubscribeFromRideAgainVideoUpdates();
354
+ this.rideAgainRoute = undefined;
355
+ this.rideAgainCheck = null;
356
+ }
357
+ subscribeToRideAgainVideoUpdates() {
358
+ try {
359
+ this.unsubscribeFromRideAgainVideoUpdates();
360
+ this.getAvailability().on('route-video-update', this.rideAgainVideoUpdateHandler);
361
+ this.getVideoActions().on('route-video-actions-update', this.rideAgainVideoUpdateHandler);
362
+ }
363
+ catch (err) {
364
+ this.logError(err, 'subscribeToRideAgainVideoUpdates');
365
+ }
366
+ }
367
+ unsubscribeFromRideAgainVideoUpdates() {
368
+ try {
369
+ this.getAvailability().off('route-video-update', this.rideAgainVideoUpdateHandler);
370
+ this.getVideoActions().off('route-video-actions-update', this.rideAgainVideoUpdateHandler);
371
+ }
372
+ catch (err) {
373
+ this.logError(err, 'unsubscribeFromRideAgainVideoUpdates');
374
+ }
375
+ }
376
+ onRideAgainVideoUpdate(routeId) {
377
+ try {
378
+ const route = this.rideAgainRoute;
379
+ if (!route || !this.rideAgainCheck)
380
+ return;
381
+ if (routeId && routeId !== route.id)
382
+ return;
383
+ const status = this.getAvailability().getStatus(route.id);
384
+ const nowReady = status.state === 'ready' || status.state === 'unknown';
385
+ this.rideAgainCheck = {
386
+ ...this.rideAgainCheck,
387
+ video: this.getVideoActions().getDisplayProps(route.id),
388
+ downloadedWhileOpen: this.rideAgainCheck.downloadedWhileOpen || nowReady
389
+ };
390
+ this.updatePageDisplay();
391
+ }
392
+ catch (err) {
393
+ this.logError(err, 'onRideAgainVideoUpdate');
394
+ }
395
+ }
201
396
  onStateUpdate(state) {
202
397
  if (state)
203
398
  this.listState = state;
@@ -226,6 +421,15 @@ let ActivitiesPageService = (() => {
226
421
  getDevicePairing() {
227
422
  return (0, devices_1.useDevicePairing)();
228
423
  }
424
+ getFolderAccess() {
425
+ return (0, service_1.useFolderAccess)();
426
+ }
427
+ getAvailability() {
428
+ return (0, service_2.useRouteVideoAvailability)();
429
+ }
430
+ getVideoActions() {
431
+ return (0, pageActions_1.useRouteVideoPageActions)();
432
+ }
229
433
  };
230
434
  return ActivitiesPageService = _classThis;
231
435
  })();
@@ -67,6 +67,7 @@ let IncyclistBindings = (() => {
67
67
  serial;
68
68
  crypto;
69
69
  fetch;
70
+ fileAccess;
70
71
  ant;
71
72
  ble;
72
73
  wifi;
@@ -117,20 +117,9 @@ let ExternalFileService = (() => {
117
117
  async ensureLocal(path, opts) {
118
118
  const start = Date.now();
119
119
  const binding = this.getBindings()?.fileAccess;
120
- if (!path || !binding || !binding.isSupported())
121
- return { ok: true, downloaded: false, waitedMs: 0 };
122
- if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('content://'))
123
- return { ok: true, downloaded: false, waitedMs: 0 };
124
- let location;
125
- try {
126
- location = binding.classifyLocation(path);
127
- }
128
- catch (err) {
129
- this.logError(err, 'ensureLocal:classifyLocation');
130
- return { ok: true, downloaded: false, waitedMs: 0 };
131
- }
132
- if (location !== 'icloud')
133
- return { ok: true, downloaded: false, waitedMs: 0 };
120
+ const skip = this.resolveSkip(path, binding);
121
+ if (skip)
122
+ return skip;
134
123
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
135
124
  const isCancelled = opts?.isCancelled ?? (() => false);
136
125
  try {
@@ -139,7 +128,7 @@ let ExternalFileService = (() => {
139
128
  return { ok: false, reason: 'access-lost' };
140
129
  if (probe.state === 'not-found')
141
130
  return { ok: false, reason: 'not-found' };
142
- let availability = await binding.getAvailability(path);
131
+ const availability = await binding.getAvailability(path);
143
132
  if (this.isLocallyAvailable(availability))
144
133
  return { ok: true, downloaded: false, waitedMs: Date.now() - start };
145
134
  if (availability.downloadError)
@@ -149,29 +138,47 @@ let ExternalFileService = (() => {
149
138
  if (isCancelled())
150
139
  return { ok: false, reason: 'cancelled' };
151
140
  await binding.startDownload(path);
152
- let interval = INITIAL_POLL_INTERVAL_MS;
153
- for (;;) {
154
- const elapsed = Date.now() - start;
155
- if (elapsed >= timeoutMs)
156
- return { ok: false, reason: 'timeout' };
157
- if (isCancelled())
158
- return { ok: false, reason: 'cancelled' };
159
- if (this.isOffline())
160
- return { ok: false, reason: 'offline' };
161
- await (0, sleep_1.sleep)(Math.max(0, Math.min(interval, timeoutMs - elapsed)));
162
- interval = BACKOFF_POLL_INTERVAL_MS;
163
- availability = await binding.getAvailability(path);
164
- if (this.isLocallyAvailable(availability))
165
- return { ok: true, downloaded: true, waitedMs: Date.now() - start };
166
- if (availability.downloadError)
167
- return this.downloadFailedResult(availability.downloadError);
168
- }
141
+ return await this.pollUntilAvailable(binding, path, start, timeoutMs, isCancelled);
169
142
  }
170
143
  catch (err) {
171
144
  this.logError(err, 'ensureLocal');
172
145
  return { ok: false, reason: 'download-failed' };
173
146
  }
174
147
  }
148
+ resolveSkip(path, binding) {
149
+ if (!path || !binding?.isSupported())
150
+ return { ok: true, downloaded: false, waitedMs: 0 };
151
+ if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('content://'))
152
+ return { ok: true, downloaded: false, waitedMs: 0 };
153
+ let location;
154
+ try {
155
+ location = binding.classifyLocation(path);
156
+ }
157
+ catch (err) {
158
+ this.logError(err, 'ensureLocal:classifyLocation');
159
+ return { ok: true, downloaded: false, waitedMs: 0 };
160
+ }
161
+ return location !== 'icloud' ? { ok: true, downloaded: false, waitedMs: 0 } : undefined;
162
+ }
163
+ async pollUntilAvailable(binding, path, start, timeoutMs, isCancelled) {
164
+ let interval = INITIAL_POLL_INTERVAL_MS;
165
+ for (;;) {
166
+ const elapsed = Date.now() - start;
167
+ if (elapsed >= timeoutMs)
168
+ return { ok: false, reason: 'timeout' };
169
+ if (isCancelled())
170
+ return { ok: false, reason: 'cancelled' };
171
+ if (this.isOffline())
172
+ return { ok: false, reason: 'offline' };
173
+ await (0, sleep_1.sleep)(Math.max(0, Math.min(interval, timeoutMs - elapsed)));
174
+ interval = BACKOFF_POLL_INTERVAL_MS;
175
+ const availability = await binding.getAvailability(path);
176
+ if (this.isLocallyAvailable(availability))
177
+ return { ok: true, downloaded: true, waitedMs: Date.now() - start };
178
+ if (availability.downloadError)
179
+ return this.downloadFailedResult(availability.downloadError);
180
+ }
181
+ }
175
182
  isLocallyAvailable(availability) {
176
183
  return !availability.isUbiquitous || availability.downloadStatus === 'current' || availability.downloadStatus === 'downloaded';
177
184
  }
@@ -104,20 +104,7 @@ let FolderAccessService = (() => {
104
104
  const id = existing?.id ?? (0, uuid_1.v4)();
105
105
  await this.releaseScope(id);
106
106
  const location = this.classify(canonical);
107
- let activation = await this.tryActivate(id, grant);
108
- let source = 'pick';
109
- let token = grant;
110
- if (!activation.ok) {
111
- const captured = await this.tryCapture(canonical);
112
- if (captured) {
113
- const capturedActivation = await this.tryActivate(id, captured);
114
- if (capturedActivation.ok) {
115
- activation = capturedActivation;
116
- source = 'captured';
117
- token = captured;
118
- }
119
- }
120
- }
107
+ const { activation, source, token } = await this.activateWithFallback(id, canonical, grant);
121
108
  const record = {
122
109
  id,
123
110
  folder: canonical,
@@ -130,20 +117,13 @@ let FolderAccessService = (() => {
130
117
  lastError: activation.ok ? undefined : (activation.error ?? grantError ?? 'no grant')
131
118
  };
132
119
  this.upsert(record);
133
- this.activation[id] = activation.ok ? 'active' : (token ? 'failed' : 'no-grant');
134
- if (activation.ok) {
120
+ this.activation[id] = this.activationStatus(activation.ok, !!token);
121
+ if (activation.ok)
135
122
  this.resolvedPaths[id] = activation.resolvedPath ?? canonical;
136
- this.logEvent({
137
- message: source === 'captured' ? 'folder grant captured' : 'folder grant stored',
138
- location, name: this.folderName(canonical)
139
- });
140
- }
141
- else {
142
- this.logEvent({
143
- message: 'folder grant unverified',
144
- location, name: this.folderName(canonical), error: record.lastError
145
- });
146
- }
123
+ if (activation.ok)
124
+ this.logGrantActivated(source, location, canonical);
125
+ else
126
+ this.logGrantUnverified(location, canonical, record.lastError);
147
127
  await this.save(record);
148
128
  this.rebuildCoverage();
149
129
  this.emitAccessChanged(canonical);
@@ -152,6 +132,40 @@ let FolderAccessService = (() => {
152
132
  this.logError(err, 'registerGrant');
153
133
  }
154
134
  }
135
+ async activateWithFallback(id, canonical, grant) {
136
+ let activation = await this.tryActivate(id, grant);
137
+ let source = 'pick';
138
+ let token = grant;
139
+ if (!activation.ok) {
140
+ const captured = await this.tryCapture(canonical);
141
+ if (captured) {
142
+ const capturedActivation = await this.tryActivate(id, captured);
143
+ if (capturedActivation.ok) {
144
+ activation = capturedActivation;
145
+ source = 'captured';
146
+ token = captured;
147
+ }
148
+ }
149
+ }
150
+ return { activation, source, token };
151
+ }
152
+ activationStatus(ok, hasToken) {
153
+ if (ok)
154
+ return 'active';
155
+ return hasToken ? 'failed' : 'no-grant';
156
+ }
157
+ logGrantActivated(source, location, canonical) {
158
+ this.logEvent({
159
+ message: source === 'captured' ? 'folder grant captured' : 'folder grant stored',
160
+ location, name: this.folderName(canonical)
161
+ });
162
+ }
163
+ logGrantUnverified(location, canonical, lastError) {
164
+ this.logEvent({
165
+ message: 'folder grant unverified',
166
+ location, name: this.folderName(canonical), error: lastError
167
+ });
168
+ }
155
169
  isCovered(path) {
156
170
  if (!this.isSupported())
157
171
  return false;
@@ -434,7 +448,7 @@ let FolderAccessService = (() => {
434
448
  folderName(folder) {
435
449
  if (!folder)
436
450
  return undefined;
437
- return folder.split('/').filter(s => s.length > 0).at(-1);
451
+ return folder.split('/').findLast(s => s.length > 0);
438
452
  }
439
453
  logAccessState(path, location, state, transient, errno) {
440
454
  this.logEvent({ message: 'access state', location, name: this.folderName(path), state, transient, errno });
@@ -36,8 +36,12 @@ const canonicalPath = (url) => {
36
36
  if (/^\/[A-Za-z]:/.exec(path))
37
37
  path = path.substring(1);
38
38
  path = path.replace(/\/{2,}/g, SEPARATOR);
39
- if (path.length > 1)
40
- path = path.replace(/\/+$/, '');
39
+ if (path.length > 1) {
40
+ let end = path.length;
41
+ while (end > 1 && path[end - 1] === SEPARATOR)
42
+ end--;
43
+ path = path.slice(0, end);
44
+ }
41
45
  if (!path)
42
46
  return undefined;
43
47
  if (path === '/private/var')
package/lib/cjs/index.js CHANGED
@@ -24,6 +24,7 @@ __exportStar(require("./avatars"), exports);
24
24
  __exportStar(require("./base/types"), exports);
25
25
  __exportStar(require("./coaches"), exports);
26
26
  __exportStar(require("./devices"), exports);
27
+ __exportStar(require("./fileaccess"), exports);
27
28
  __exportStar(require("./i18n"), exports);
28
29
  __exportStar(require("./maps"), exports);
29
30
  __exportStar(require("./monitoring"), exports);