@internetarchive/ia-book-actions 1.4.4-alpha10 → 1.4.4-alpha12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "author": "Internet Archive",
9
9
  "license": "AGPL-3.0-only",
10
- "version": "1.4.4-alpha10",
10
+ "version": "1.4.4-alpha12",
11
11
  "main": "index.js",
12
12
  "module": "index.js",
13
13
  "publishConfig": {
@@ -372,6 +372,12 @@ export default class ActionsHandler extends LitElement {
372
372
  new Date().getTime() + this.loanTotalTime * 1000
373
373
  );
374
374
 
375
+ log('[ActionsHandler] setBrowseTimeSession: resetting loanTime', {
376
+ identifier: this.identifier,
377
+ expireDate,
378
+ loanTotalTime: this.loanTotalTime,
379
+ });
380
+
375
381
  // set a value
376
382
  await this.localCache.set({
377
383
  key: `${this.identifier}-loanTime`,
@@ -381,12 +387,20 @@ export default class ActionsHandler extends LitElement {
381
387
 
382
388
  // delete pageChangedTime when book is auto renew at nth minute
383
389
  await this.localCache.delete(`${this.identifier}-pageChangedTime`);
390
+
391
+ log('[ActionsHandler] setBrowseTimeSession: loanTime reset complete', {
392
+ identifier: this.identifier,
393
+ });
384
394
  } catch (error) {
385
- log(error);
395
+ log('[ActionsHandler] setBrowseTimeSession failed', error);
386
396
  }
387
397
  }
388
398
 
389
399
  deleteLoanCookies() {
400
+ log('[ActionsHandler] deleteLoanCookies: expiring loan cookies', {
401
+ identifier: this.identifier,
402
+ });
403
+
390
404
  const date = new Date();
391
405
  date.setTime(date.getTime() - 24 * 60 * 60 * 1000); // one day ago
392
406
 
@@ -159,16 +159,16 @@ export default class LoanAnanlytics {
159
159
  * @memberof LoanAnanlytics
160
160
  */
161
161
  sendEvent(eventCategory, eventAction, label, extraParams) {
162
- log(
163
- 'eventCategory:-',
164
- eventCategory,
165
- '||\teventAction:-',
166
- eventAction,
167
- '||\tlabel:-',
168
- label,
169
- '||\textraParams:-',
170
- extraParams,
171
- );
162
+ // log(
163
+ // 'eventCategory:-',
164
+ // eventCategory,
165
+ // '||\teventAction:-',
166
+ // eventAction,
167
+ // '||\tlabel:-',
168
+ // label,
169
+ // '||\textraParams:-',
170
+ // extraParams,
171
+ // );
172
172
 
173
173
  window?.archive_analytics?.send_event_no_sampling(
174
174
  eventCategory,
@@ -2,6 +2,7 @@
2
2
  import ActionsHandlerService from './actions-handler/actions-handler-service.js';
3
3
  import LoanAnanlytics from './loan-analytics.js';
4
4
  import { sentryLogs } from '../config/sentry-events.js';
5
+ import log from './log.js';
5
6
 
6
7
  /**
7
8
  * This class is used to create loan token for borrowed books
@@ -77,11 +78,21 @@ export class LoanTokenPoller {
77
78
  */
78
79
  async handleLoanTokenPoller(isInitial = false, retryCount = 0) {
79
80
  const action = 'create_token';
81
+ log('[LoanTokenPoller] create_token requested', {
82
+ identifier: this.identifier,
83
+ isInitial,
84
+ retryCount,
85
+ });
80
86
  ActionsHandlerService({
81
87
  identifier: this.identifier,
82
88
  action,
83
89
  error: data => this.handleTokenError(data, isInitial, retryCount),
84
90
  success: () => {
91
+ log('[LoanTokenPoller] create_token succeeded', {
92
+ identifier: this.identifier,
93
+ isInitial,
94
+ retryCount,
95
+ });
85
96
  if (isInitial) this.successCallback();
86
97
  },
87
98
  });
@@ -104,18 +115,37 @@ export class LoanTokenPoller {
104
115
  typeof data?.error === 'string' &&
105
116
  /do not currently have this book borrowed/i.test(data.error);
106
117
 
118
+ log('[LoanTokenPoller] create_token failed', {
119
+ identifier: this.identifier,
120
+ isInitial,
121
+ retryCount,
122
+ isStaleLoanReadError,
123
+ error: data?.error,
124
+ });
125
+
107
126
  if (
108
127
  isInitial &&
109
128
  isStaleLoanReadError &&
110
129
  retryCount < this.maxInitialTokenRetries
111
130
  ) {
112
131
  const delay = this.initialTokenRetryDelay * (retryCount + 1);
132
+ log('[LoanTokenPoller] retrying create_token after stale-loan-read error', {
133
+ identifier: this.identifier,
134
+ nextRetryCount: retryCount + 1,
135
+ delay,
136
+ });
113
137
  setTimeout(() => {
114
138
  this.handleLoanTokenPoller(true, retryCount + 1);
115
139
  }, delay);
116
140
  return;
117
141
  }
118
142
 
143
+ log('[LoanTokenPoller] giving up on create_token, reporting error', {
144
+ identifier: this.identifier,
145
+ isInitial,
146
+ retryCount,
147
+ });
148
+
119
149
  this.errorCallback({ detail: { action, data } });
120
150
 
121
151
  // send error to Sentry
@@ -61,6 +61,7 @@ export default class IABookActions extends LitElement {
61
61
  modal: { Object },
62
62
  tokenDelay: { type: Number },
63
63
  timerExecutionSeconds: { type: Number },
64
+ userActionGracePeriod: { type: Number },
64
65
  localCache: { type: Object },
65
66
  loanRenewTimeConfig: { type: Object },
66
67
  loanRenewResult: { type: Object },
@@ -83,6 +84,14 @@ export default class IABookActions extends LitElement {
83
84
  this.tokenDelay = 120; // in seconds
84
85
  this.timerExecutionSeconds = 30;
85
86
 
87
+ /**
88
+ * How long (ms) after bindLoanRenewEvents() runs to ignore
89
+ * BookReader:userAction events — BookReader fires that event on its
90
+ * own init-time jump to the last-read page, not just on genuine
91
+ * interaction. @see bindLoanRenewEvents
92
+ */
93
+ this.userActionGracePeriod = 2000;
94
+
86
95
  // private props
87
96
  this.postInitComplete = false;
88
97
  this.primaryActions = [];
@@ -283,7 +292,14 @@ export default class IABookActions extends LitElement {
283
292
 
284
293
  this.borrowType = actions.borrowType ? actions.borrowType : null;
285
294
 
286
- if (this.borrowType === 'browsed') {
295
+ // Don't (re)start the countdown while a renewal is in flight — the
296
+ // optimistic browsingExpired flip in autoRenewExpiredLoan() (which
297
+ // triggers this same setupLendingToolbarActions() call) happens before
298
+ // renewal is confirmed, so secondsLeftOnLoan could still be a stale
299
+ // value from before the renewal. Wait for handleLoanAutoRenewed() to
300
+ // confirm the real value and clear loanRenewInProgress before showing
301
+ // a countdown again.
302
+ if (this.borrowType === 'browsed' && !this.loanRenewInProgress) {
287
303
  // start timer for loan-renew
288
304
  await this.startTimerCountdown();
289
305
 
@@ -327,10 +343,19 @@ export default class IABookActions extends LitElement {
327
343
  * 2. IABookActions:loanRenew - dispatched from timer-countdown component
328
344
  */
329
345
  bindLoanRenewEvents() {
346
+ this.userActionReadyAt = Date.now() + this.userActionGracePeriod;
347
+
330
348
  /**
331
349
  * dispatched this event from bookreader page changed
332
350
  */
333
351
  window.addEventListener('BookReader:userAction', () => {
352
+ if (Date.now() < this.userActionReadyAt) {
353
+ log(
354
+ '[IABookActions] BookReader:userAction ignored — within startup grace period'
355
+ );
356
+ return;
357
+ }
358
+
334
359
  // Capture before autoRenewExpiredLoan() runs — it synchronously flips
335
360
  // lendingStatus.browsingExpired to false as an optimistic UI update,
336
361
  // so checking the live property afterward would always see it as
@@ -440,7 +465,15 @@ export default class IABookActions extends LitElement {
440
465
  * On error: handleLendingActionError() shows showLoanUnavailableModal().
441
466
  */
442
467
  autoRenewExpiredLoan() {
443
- if (this.loanRenewInProgress) return;
468
+ if (this.loanRenewInProgress) {
469
+ log('[IABookActions] autoRenewExpiredLoan: skipped, renewal already in progress', {
470
+ identifier: this.identifier,
471
+ });
472
+ return;
473
+ }
474
+ log('[IABookActions] autoRenewExpiredLoan: starting silent renewal', {
475
+ identifier: this.identifier,
476
+ });
444
477
  this.loanRenewInProgress = true;
445
478
  this.recoveringFromLoanExpiry = true;
446
479
 
@@ -448,11 +481,17 @@ export default class IABookActions extends LitElement {
448
481
 
449
482
  // Optimistically flip browsingExpired back to false so the action bar
450
483
  // stays red (patronIsReadingAction) while the renew_loan request is in
451
- // flight, instead of showing the blue "Borrow" state.
484
+ // flight, instead of showing the blue "Borrow" state. Deliberately NOT
485
+ // resetting secondsLeftOnLoan here — the renewal hasn't been confirmed
486
+ // yet (it can take several seconds, or genuinely fail), so showing a
487
+ // full hour before we know the outcome would be misleading. The real
488
+ // value is set once handleLoanAutoRenewed() confirms success;
489
+ // loanRenewInProgress (already true above) is the signal consumers
490
+ // should use to show a "renewing" state instead of trusting the
491
+ // countdown during this window.
452
492
  this.lendingStatus = {
453
493
  ...this.lendingStatus,
454
494
  browsingExpired: false,
455
- secondsLeftOnLoan: this.loanRenewTimeConfig.loanTotalTime,
456
495
  };
457
496
 
458
497
  // Defer renewNow until after lendingStatus timer setup completes.
@@ -505,19 +544,14 @@ export default class IABookActions extends LitElement {
505
544
  headerColor: '#194880',
506
545
  showCloseButton: false,
507
546
  closeOnBackdropClick: false,
508
- message: html`<span
509
- style="display:inline-flex;align-items:center;flex-wrap:nowrap;gap:4px;"
510
- >
511
- <span>${this.loanRenewHelper?.getMessageTexts(
512
- warningTexts,
513
- secondsLeft
514
- )}</span>
547
+ message: html`<span>
548
+ ${this.loanRenewHelper?.getMessageTexts(warningTexts, secondsLeft)}
515
549
  <a
516
550
  href="https://help.archive.org/help/borrowing-from-the-lending-library"
517
551
  target="_blank"
518
552
  title="Get more info on borrowing from The Lending Library"
519
553
  data-event-click-tracking="BookReader|BrowsableMoreInfo"
520
- style="display:inline-flex;flex-shrink:0;line-height:0;"
554
+ style="display:inline-block;vertical-align:middle;line-height:0;margin-left:4px;"
521
555
  >
522
556
  ${infoIcon}
523
557
  </a>
@@ -552,7 +586,10 @@ export default class IABookActions extends LitElement {
552
586
  * Execute when loan is expired
553
587
  */
554
588
  async browseHasExpired() {
555
- log('[IABookActions] browseHasExpired');
589
+ log('[IABookActions] browseHasExpired', {
590
+ identifier: this.identifier,
591
+ loanRenewInProgress: this.loanRenewInProgress,
592
+ });
556
593
  window?.IALendingIntervals?.clearAll();
557
594
 
558
595
  const currStatus = {
@@ -565,6 +602,9 @@ export default class IABookActions extends LitElement {
565
602
  // remove respected key:value for loan-renew
566
603
  await this.localCache.delete(`${this.identifier}-loanTime`);
567
604
  await this.localCache.delete(`${this.identifier}-pageChangedTime`);
605
+ log('[IABookActions] browseHasExpired: cleared loanTime/pageChangedTime cache', {
606
+ identifier: this.identifier,
607
+ });
568
608
 
569
609
  // show message after browsed book is expired.
570
610
  this.loanRenewResult.renewNow = false;
@@ -791,9 +831,8 @@ export default class IABookActions extends LitElement {
791
831
  }
792
832
 
793
833
  log('[IABookActions] timer', {
794
- secondsLeft,
795
- loanRenewAtLast: this.loanRenewTimeConfig.loanRenewAtLast,
796
- pageChangedInLast: this.loanRenewTimeConfig.pageChangedInLast,
834
+ whatShouldLeft: resyncd.whatShouldLeft,
835
+ whatIsleft: secondsLeft,
797
836
  });
798
837
 
799
838
  /**
@@ -952,6 +991,14 @@ export default class IABookActions extends LitElement {
952
991
  const action = event?.detail?.action;
953
992
  const errorMsg = event?.detail?.data?.error;
954
993
 
994
+ log('[IABookActions] handleLendingActionError', {
995
+ identifier: this.identifier,
996
+ action,
997
+ errorMsg,
998
+ loanRenewInProgress: this.loanRenewInProgress,
999
+ recoveringFromLoanExpiry: this.recoveringFromLoanExpiry,
1000
+ });
1001
+
955
1002
  if (action === 'create_token') {
956
1003
  // A create_token hiccup only affects BookReader's page-image access
957
1004
  // token — it says nothing about how much time is left on the loan
@@ -89,6 +89,8 @@ describe('<ia-book-actions>', () => {
89
89
  identifier: 'foobar',
90
90
  })
91
91
  );
92
+ // Not testing the BookReader-init grace period here — bypass it.
93
+ el.userActionReadyAt = 0;
92
94
 
93
95
  const spy = Sinon.spy(el, 'autoLoanRenewChecker');
94
96
  el.lendingStatus = { ...el.lendingStatus, browsingExpired: false };
@@ -111,6 +113,8 @@ describe('<ia-book-actions>', () => {
111
113
  lendingStatus: { user_has_browsed: true, browsingExpired: true },
112
114
  })
113
115
  );
116
+ // Not testing the BookReader-init grace period here — bypass it.
117
+ el.userActionReadyAt = 0;
114
118
 
115
119
  const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
116
120
  el.borrowType = 'browsed';
@@ -121,6 +125,34 @@ describe('<ia-book-actions>', () => {
121
125
 
122
126
  expect(spy.calledOnce).to.be.true;
123
127
  });
128
+
129
+ it('ignores BookReader:userAction fired within the startup grace period (WEBDEV-8322 follow-up)', async () => {
130
+ // Regression: BookReader.jumpToIndex() fires its own 'userAction'
131
+ // event any time it runs — including BookReader's own init-time jump
132
+ // to the reader's last-read page (updateFromParams(), called from
133
+ // init()), which has nothing to do with the patron doing anything.
134
+ // Without this grace period, a plain page refresh on an already-
135
+ // expired loan would silently auto-renew before the patron did
136
+ // anything at all.
137
+ const el = await fixture(
138
+ container({
139
+ userid: '@user1',
140
+ identifier: 'foobar',
141
+ lendingStatus: { user_has_browsed: true, browsingExpired: true },
142
+ })
143
+ );
144
+ el.borrowType = 'browsed';
145
+ await el.updateComplete;
146
+
147
+ const spy = Sinon.spy(el, 'autoRenewExpiredLoan');
148
+
149
+ // Fired immediately after bindLoanRenewEvents() ran (in firstUpdated())
150
+ // — well within the default grace period.
151
+ window.dispatchEvent(new Event('BookReader:userAction'));
152
+ await el.updateComplete;
153
+
154
+ expect(spy.called).to.be.false;
155
+ });
124
156
  });
125
157
 
126
158
  describe('Primary Actions data', () => {
@@ -595,6 +627,36 @@ describe('autoRenewExpiredLoan', () => {
595
627
  expect(el.primaryColor).to.equal('danger');
596
628
  });
597
629
 
630
+ it('does not (re)start the countdown during the optimistic pre-confirmation window (WEBDEV-8322 follow-up)', async () => {
631
+ // Regression: the optimistic browsingExpired flip above triggers
632
+ // setupLendingToolbarActions() before renew_loan is even dispatched,
633
+ // let alone confirmed. Without gating on loanRenewInProgress, that
634
+ // would (re)start the timer-countdown interval using whatever stale
635
+ // secondsLeftOnLoan was left over from before the loan expired,
636
+ // showing a wrong/flickering value until handleLoanAutoRenewed()
637
+ // corrects it moments later.
638
+ const el = await fixture(
639
+ container({
640
+ userid: '@user1',
641
+ identifier: 'foobar',
642
+ lendingStatus: {
643
+ user_has_browsed: true,
644
+ available_to_browse: true,
645
+ browsingExpired: true,
646
+ },
647
+ })
648
+ );
649
+ await el.updateComplete;
650
+
651
+ const startTimerCountdownSpy = Sinon.spy(el, 'startTimerCountdown');
652
+
653
+ el.autoRenewExpiredLoan();
654
+ await el.updateComplete;
655
+
656
+ expect(el.loanRenewInProgress).to.be.true;
657
+ expect(startTimerCountdownSpy.called).to.be.false;
658
+ });
659
+
598
660
  it('sets loanRenewInProgress and triggers renewNow after timeout', async () => {
599
661
  const el = await fixture(
600
662
  container({
@@ -882,6 +944,8 @@ describe('BookReader:userAction race regression (WEBDEV-8322)', () => {
882
944
  );
883
945
  await el.updateComplete;
884
946
  expect(el.borrowType).to.equal('browsed');
947
+ // Not testing the BookReader-init grace period here — bypass it.
948
+ el.userActionReadyAt = 0;
885
949
 
886
950
  el.lendingStatus = { ...baseStatus, browsingExpired: true };
887
951
  await el.updateComplete;
@@ -923,6 +987,8 @@ describe('BookReader:userAction race regression (WEBDEV-8322)', () => {
923
987
  );
924
988
  await el.updateComplete;
925
989
  expect(el.borrowType).to.equal('browsed');
990
+ // Not testing the BookReader-init grace period here — bypass it.
991
+ el.userActionReadyAt = 0;
926
992
 
927
993
  el.loanRenewInProgress = true;
928
994