@internetarchive/ia-book-actions 1.4.4-alpha8 → 1.4.5

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/index.html CHANGED
@@ -360,7 +360,33 @@
360
360
  });
361
361
  });
362
362
 
363
-
363
+ // WEBDEV-8322 repro: fail only create_token (via ?failAction=create_token),
364
+ // leaving renew_loan (and every other action) succeeding normally —
365
+ // see actions-handler-service.js's shouldReturnError.
366
+ document.querySelectorAll('.failCreateToken input[type=checkbox]').forEach((item) => {
367
+ if (window.location.href.indexOf('?failAction=create_token') !== -1) {
368
+ item.checked = true;
369
+ }
370
+ item.addEventListener('click', (e) => {
371
+ console.log(`[Demo] fail-create_token-only mode ${e.target.checked ? 'enabled' : 'disabled'}`);
372
+
373
+ var searchParams = new URLSearchParams(window.location.search);
374
+ var separate = '';
375
+ if (e.target.checked) {
376
+ separate = '?';
377
+ searchParams.set('failAction', 'create_token');
378
+ } else {
379
+ separate = '';
380
+ searchParams.delete('failAction');
381
+ }
382
+
383
+ if (history.pushState) {
384
+ var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + separate + searchParams;
385
+ window.history.pushState({path:newurl},'',newurl);
386
+ }
387
+ });
388
+ });
389
+
364
390
  document.querySelectorAll('.userState input[type=checkbox]').forEach((item) => {
365
391
  item.addEventListener('click', (e) => {
366
392
  if (e.target.checked) {
@@ -726,6 +752,17 @@
726
752
  </span>
727
753
  </td>
728
754
  </tr>
755
+ <tr colspan='5'>
756
+ <td class="failCreateToken">
757
+ <label><input type="checkbox" name="failCreateToken" value="failCreateToken" />Fail create_token only (renew_loan still succeeds)</label>
758
+ <span style="color: #666; font-size: 11px; display: block; padding: 0 10px 10px;">
759
+ WEBDEV-8322 repro: unlike the checkbox above, this leaves renew_loan
760
+ succeeding and fails only the create_token call right after it — turn
761
+ this on, then run scenario 2 → 3 (or 2 → 4) below. Expect the error
762
+ modal, and the bar/timer resetting to Borrow/0 (no retry).
763
+ </span>
764
+ </td>
765
+ </tr>
729
766
  <tr>
730
767
  <td class="userState">
731
768
  <label><input type="checkbox" name="user" value="isAdmin" />isAdmin</label>
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-alpha8",
10
+ "version": "1.4.5",
11
11
  "main": "index.js",
12
12
  "module": "index.js",
13
13
  "publishConfig": {
@@ -0,0 +1,17 @@
1
+ import { html } from 'lit';
2
+
3
+ export const infoIcon = html`<svg
4
+ height="20"
5
+ width="20"
6
+ viewBox="0 0 100 100"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ >
9
+ <g fill="none" fill-rule="evenodd">
10
+ <path d="m0 0h100v100h-100z" fill="#fff" />
11
+ <path
12
+ d="m49.8315487 0h.1702245c6.7356878 0 13.1853038 1.31117332 19.3488483 3.93351997 6.1635444 2.62234664 11.4854233 6.15778963 15.9656369 10.60632903 4.4802135 4.4485394 8.0478347 9.7522946 10.7028636 15.9112655 2.655029 6.1589709 3.980878 12.6038012 3.9789971 19.3344909.0567419 6.6716279-1.1702933 13.0585776-3.6811042 19.1608491-2.510811 6.1022715-6.106803 11.5206067-10.7879759 16.2550055-9.7027949 9.7522946-21.4884754 14.6851412-35.3570414 14.79854h-.1702244c-6.7333236 0-13.1829397-1.3111733-19.3488483-3.93352-6.1659087-2.6223466-11.4877876-6.1577896-15.9656369-10.606329s-8.04547055-9.7522946-10.7028637-15.9112655c-2.65739314-6.1589709-3.9844243-12.6038012-3.98254337-19.3344909-.05674149-6.6716279 1.17029325-13.0585776 3.68110421-19.1608491 2.51081095-6.1022715 6.10680292-11.5206067 10.78797586-16.2550055 9.7027949-9.75229456 21.4884754-14.68514123 35.3570414-14.79854zm12.6566146 26.4757998c1.6745578-1.6828001 2.5118367-3.6747334 2.5118367-5.9757998 0-2.4126333-.8095238-4.4324583-2.4285714-6.059475s-3.6289796-2.440525-6.0297959-2.440525c-2.4008164 0-4.4107483.8135083-6.029796 2.440525-1.6745578 1.6270167-2.5118367 3.6468417-2.5118367 6.059475 0 2.1871753.8372789 4.1791086 2.5118367 5.9757998 1.6745579 1.6828001 3.6844898 2.5242002 6.029796 2.5242002 2.3453061 0 4.3274829-.8414001 5.9465306-2.5242002zm-12.1370589 52.7776981-1.2815282-.9486968c0-.4588935.398855-1.8938272 1.196565-4.3048011l12.7338588-39-23.0745876 3.6164609.2548896 3.873251c0-.1141289.4554971-.1997256 1.3664914-.2567901.9109942 0 1.623741.1723823 2.1382404.5171468.5121392.2306356.7965299.6039323.8531721 1.1198902 0 .8607225-.6549247 3.2134431-1.9647739 7.0581619l-8.1175252 24.1061729c-1.0242785 3.2716963-1.5080967 5.5388203-1.4514546 6.8013717.0566421 1.6643804.8826732 2.9839963 2.4780932 3.9588477 1.2532071.803658 2.8769482 1.205487 4.8712231 1.205487h.5982825c1.7653464-.0570645 3.8445846-.3875629 6.2377146-.9914952 2.3931299-.6039323 4.3590839-1.3933242 5.8978617-2.3681756 1.3098493-.803658 2.3919499-1.5359853 3.2463021-2.1969821.8543521-.6609968 1.3959925-1.1341564 1.6249211-1.4194788l.3433929-.3459533-2.8214861-3.3596708-2.9914125 2.0650205c-.79771.4588935-1.5104568.7454047-2.1382404.8595337z"
13
+ fill="#000"
14
+ fill-rule="nonzero"
15
+ />
16
+ </g>
17
+ </svg> `;
@@ -18,7 +18,6 @@ export default async function ActionsHandlerService(options) {
18
18
  let baseHost = '/services/loans/loan';
19
19
  const location = window?.location;
20
20
 
21
- // return error reponse when not production and has ?error=true param...
22
21
  const tokenError = 'loan token not found. please try again later.';
23
22
  const borrowError =
24
23
  'This book is not available to borrow at this time. Please try again later.';
@@ -29,9 +28,18 @@ export default async function ActionsHandlerService(options) {
29
28
  'renew_loan',
30
29
  'return_loan',
31
30
  ];
31
+ const searchParams = new URLSearchParams(location?.search);
32
+ // ?error=true fails every erroneous action (e.g. simulates the book
33
+ // already being borrowed by someone else). ?failAction=create_token (or
34
+ // any action name) fails ONLY that action, so a renew_loan can succeed
35
+ // and the immediately-following create_token can be made to fail —
36
+ // reproducing WEBDEV-8322's "renewed, but create_token failed" bug
37
+ // without needing a real book.
32
38
  const shouldReturnError =
33
- new URLSearchParams(location?.search).get('error') === 'true' &&
34
- location?.hostname !== 'archive.org';
39
+ location?.hostname !== 'archive.org' &&
40
+ erroneousActions.includes(option?.action) &&
41
+ (searchParams.get('error') === 'true' ||
42
+ searchParams.get('failAction') === option?.action);
35
43
 
36
44
  const testHostname = ['localhost', 'internetarchive.github.io'];
37
45
  let isTest = false;
@@ -51,7 +59,7 @@ export default async function ActionsHandlerService(options) {
51
59
  })
52
60
  .then(async response => {
53
61
  // intentional error on localhost
54
- if (shouldReturnError && erroneousActions.includes(option?.action)) {
62
+ if (shouldReturnError) {
55
63
  return {
56
64
  success: false,
57
65
  error: option?.action === 'create_token' ? tokenError : borrowError,
@@ -95,5 +103,20 @@ export default async function ActionsHandlerService(options) {
95
103
  window?.Sentry?.captureException(
96
104
  `${sentryLogs.actionsHandlerService} - Error: ${error}`
97
105
  );
106
+
107
+ /**
108
+ * Report it, don't just swallow it. A rejected fetch (offline, dropped
109
+ * connection) or a non-JSON body (a 405 returning HTML) lands here, and
110
+ * calling neither `success` nor `error` leaves every caller waiting on a
111
+ * callback that never comes.
112
+ *
113
+ * For renew_loan that's not merely a missing modal: IABookActions
114
+ * clears its loanRenewInProgress guard from these callbacks, so a
115
+ * silent failure latched it on for good and every later renewal
116
+ * attempt became a no-op. Backgrounding a tab on a flaky mobile
117
+ * connection is exactly when this fires.
118
+ */
119
+ log(`[IABookActions] ✗ ${option.action} threw`, error);
120
+ option?.error?.({ error: String(error) });
98
121
  }
99
122
  }
@@ -6,7 +6,11 @@ import log from '../log.js';
6
6
 
7
7
  import ActionsHandlerService from './actions-handler-service.js';
8
8
  import LoanAnanlytics from '../loan-analytics.js';
9
- import { analyticsCategories, analyticsActions, analyticsLabels } from '../../config/analytics-event-and-category.js';
9
+ import {
10
+ analyticsCategories,
11
+ analyticsActions,
12
+ analyticsLabels,
13
+ } from '../../config/analytics-event-and-category.js';
10
14
  import * as Cookies from '../doc-cookies.js';
11
15
 
12
16
  /**
@@ -172,32 +176,51 @@ export default class ActionsHandler extends LitElement {
172
176
  action,
173
177
  identifier: this.identifier,
174
178
  success: async data => {
175
- log('RENEW_LOAN --- ', data, this.identifier);
176
- const activeLoan = data.loan ? data.loan : undefined;
177
- const isRenewal = activeLoan.renewal;
178
-
179
- if (activeLoan && isRenewal) {
180
- // Await — loanAutoRenewed listeners read this same cache key
181
- // immediately; dispatching before the write lands was a race.
182
- await this.setBrowseTimeSession();
183
-
184
- // Only record the renew analytics event once the renewal has
185
- // actually succeeded — not merely attempted.
186
- await this.loanAnanlytics?.storeLoanStatsCount(
187
- this.identifier,
188
- 'autorenew'
189
- );
190
- const analyticsLabel =
191
- renewType === 'auto'
192
- ? analyticsLabels.browseAutoRenew
193
- : analyticsLabels.browseManualRenew;
194
- this.loanAnanlytics?.sendEvent(
195
- analyticsCategories.browse,
196
- analyticsActions.browseRenew,
197
- analyticsLabel,
198
- this.identifier
199
- );
200
- } else {
179
+ // Anything thrown in here is an unhandled rejection that dispatches
180
+ // NOTHING — and IABookActions only clears loanRenewInProgress when
181
+ // one of these two events arrives, so a throw here latches it on
182
+ // permanently (dead countdown, loan stuck "active"). Always exit
183
+ // through exactly one of the two dispatches below.
184
+ try {
185
+ log('RENEW_LOAN --- ', data, this.identifier);
186
+ const activeLoan = data.loan ? data.loan : undefined;
187
+ // Optional chaining: `data.loan` is genuinely absent on some
188
+ // failures, and `activeLoan` can be undefined here.
189
+ const isRenewal = activeLoan?.renewal;
190
+
191
+ if (activeLoan && isRenewal) {
192
+ // Await — loanAutoRenewed listeners read this same cache key
193
+ // immediately; dispatching before the write lands was a race.
194
+ await this.setBrowseTimeSession();
195
+
196
+ // Only record the renew analytics event once the renewal has
197
+ // actually succeeded — not merely attempted.
198
+ await this.loanAnanlytics?.storeLoanStatsCount(
199
+ this.identifier,
200
+ 'autorenew'
201
+ );
202
+ const analyticsLabel =
203
+ renewType === 'auto'
204
+ ? analyticsLabels.browseAutoRenew
205
+ : analyticsLabels.browseManualRenew;
206
+ this.loanAnanlytics?.sendEvent(
207
+ analyticsCategories.browse,
208
+ analyticsActions.browseRenew,
209
+ analyticsLabel,
210
+ this.identifier
211
+ );
212
+
213
+ // Dispatch the success outcome ONLY for a confirmed renewal —
214
+ // a `{loan: {renewal: false}}` response goes through
215
+ // dispatchActionError below instead, never both.
216
+ this.dispatchEvent(
217
+ new CustomEvent('loanAutoRenewed', {
218
+ detail: { action, data: { ...data, loan: activeLoan } },
219
+ })
220
+ );
221
+ return;
222
+ }
223
+
201
224
  log('RENEW_LOAN ERROR --- ', {
202
225
  action,
203
226
  isRenewal,
@@ -213,14 +236,17 @@ export default class ActionsHandler extends LitElement {
213
236
  error: true,
214
237
  message: 'Loan renewal failed: no loan active.',
215
238
  });
239
+ } catch (error) {
240
+ log('RENEW_LOAN THREW --- ', error);
241
+ window?.Sentry?.captureException(
242
+ `${sentryLogs.bookRenewFailed} - Exception: ${error}`
243
+ );
244
+ this.dispatchActionError(action, {
245
+ data,
246
+ error: true,
247
+ message: `Loan renewal failed: ${error}`,
248
+ });
216
249
  }
217
-
218
- // dispatch outcome of loan renewal
219
- this.dispatchEvent(
220
- new CustomEvent('loanAutoRenewed', {
221
- detail: { action, data: { ...data, loan: activeLoan } },
222
- })
223
- );
224
250
  },
225
251
  error: data => {
226
252
  this.dispatchActionError(action, data);
@@ -372,6 +398,12 @@ export default class ActionsHandler extends LitElement {
372
398
  new Date().getTime() + this.loanTotalTime * 1000
373
399
  );
374
400
 
401
+ log('[ActionsHandler] setBrowseTimeSession: resetting loanTime', {
402
+ identifier: this.identifier,
403
+ expireDate,
404
+ loanTotalTime: this.loanTotalTime,
405
+ });
406
+
375
407
  // set a value
376
408
  await this.localCache.set({
377
409
  key: `${this.identifier}-loanTime`,
@@ -381,12 +413,20 @@ export default class ActionsHandler extends LitElement {
381
413
 
382
414
  // delete pageChangedTime when book is auto renew at nth minute
383
415
  await this.localCache.delete(`${this.identifier}-pageChangedTime`);
416
+
417
+ log('[ActionsHandler] setBrowseTimeSession: loanTime reset complete', {
418
+ identifier: this.identifier,
419
+ });
384
420
  } catch (error) {
385
- log(error);
421
+ log('[ActionsHandler] setBrowseTimeSession failed', error);
386
422
  }
387
423
  }
388
424
 
389
425
  deleteLoanCookies() {
426
+ log('[ActionsHandler] deleteLoanCookies: expiring loan cookies', {
427
+ identifier: this.identifier,
428
+ });
429
+
390
430
  const date = new Date();
391
431
  date.setTime(date.getTime() - 24 * 60 * 60 * 1000); // one day ago
392
432
 
@@ -414,7 +454,8 @@ export default class ActionsHandler extends LitElement {
414
454
  * @returns {void}
415
455
  */
416
456
  setStickyAdminAccess(value) {
417
- const domain = window.location.hostname === 'localhost' ? 'localhost' : '.archive.org';
457
+ const domain =
458
+ window.location.hostname === 'localhost' ? 'localhost' : '.archive.org';
418
459
  const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days from now
419
460
  Cookies.setItem('sticky-admin-access', value, expires, '/', domain);
420
461
  }
@@ -159,17 +159,6 @@ 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
- );
172
-
173
162
  window?.archive_analytics?.send_event_no_sampling(
174
163
  eventCategory,
175
164
  eventAction,
@@ -26,7 +26,7 @@ export class LoanRenewHelper {
26
26
  };
27
27
  }
28
28
 
29
- async handleLoanRenew() {
29
+ handleLoanRenew() {
30
30
  try {
31
31
  if (this.hasPageChanged) {
32
32
  return this.pageChanged(); // user clicked on page
@@ -99,7 +99,7 @@ export class LoanRenewHelper {
99
99
  } else if (pageChangedTime >= pageChangeTimeFrame) {
100
100
  this.result = {
101
101
  texts: '',
102
- renewNow: true, // viewed in last time frame
102
+ renewNow: true, // viewed in last time frame
103
103
  renewType: 'auto',
104
104
  };
105
105
  }
@@ -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
@@ -9,12 +10,39 @@ import { sentryLogs } from '../config/sentry-events.js';
9
10
  * ActionsHandlerService is a function being used to execute
10
11
  */
11
12
  export class LoanTokenPoller {
12
- constructor(id, borrowType, successCallback, errorCallback, pollerDelay) {
13
- this.identifier = id;
13
+ /**
14
+ * @param {Object} options
15
+ * @param {string} options.identifier
16
+ * @param {string} options.borrowType
17
+ * @param {Function} options.successCallback - called after the initial
18
+ * loan token is created.
19
+ * @param {Function} options.errorCallback - called on any create_token
20
+ * failure (initial or a routine interval tick).
21
+ * @param {number} options.pollerDelay - interval between routine
22
+ * create_token checks, in seconds.
23
+ * @param {boolean} [options.skipInitialCall] - skip the immediate
24
+ * create_token call, only start the recurring interval. Used after a
25
+ * loan-expiry recovery renewal, where BookLoanService::
26
+ * attempt_to_renew_loan() already minted a valid access token as part
27
+ * of the renew_loan response itself — an immediate confirming
28
+ * create_token call is redundant.
29
+ */
30
+ constructor(options = {}) {
31
+ const {
32
+ identifier,
33
+ borrowType,
34
+ successCallback,
35
+ errorCallback,
36
+ pollerDelay,
37
+ skipInitialCall = false,
38
+ } = options;
39
+
40
+ this.identifier = identifier;
14
41
  this.borrowType = borrowType;
15
42
  this.successCallback = successCallback; // callback function to be called after loan token is created
16
43
  this.errorCallback = errorCallback; // callback function to be called after loan token is created
17
44
  this.pollerDelay = pollerDelay; // value in seconds
45
+ this.skipInitialCall = skipInitialCall === true;
18
46
 
19
47
  this.loanTokenInterval = undefined;
20
48
 
@@ -33,8 +61,14 @@ export class LoanTokenPoller {
33
61
 
34
62
  async bookAccessed() {
35
63
  if (this.borrowType) {
36
- // Do an initial token, then set an interval
37
- this.handleLoanTokenPoller(true);
64
+ if (this.skipInitialCall) {
65
+ log('[LoanTokenPoller] skipping initial create_token — already minted by the renewal response', {
66
+ identifier: this.identifier,
67
+ });
68
+ } else {
69
+ // Do an initial token, then set an interval
70
+ this.handleLoanTokenPoller(true);
71
+ }
38
72
 
39
73
  // if this.borrowType = adminBorrowed,
40
74
  // - we don't want to fetch token on interval
@@ -58,29 +92,63 @@ export class LoanTokenPoller {
58
92
  }
59
93
  }
60
94
 
95
+ /**
96
+ * @param {boolean} isInitial - the first create_token call right after
97
+ * bookAccessed()/a renewal, as opposed to a routine interval tick.
98
+ */
61
99
  async handleLoanTokenPoller(isInitial = false) {
62
100
  const action = 'create_token';
101
+ log('[LoanTokenPoller] create_token requested', {
102
+ identifier: this.identifier,
103
+ isInitial,
104
+ });
63
105
  ActionsHandlerService({
64
106
  identifier: this.identifier,
65
107
  action,
66
- error: data => {
67
- this.errorCallback({ detail: { action, data } });
68
-
69
- // send error to Sentry
70
- window?.Sentry?.captureMessage(
71
- `${sentryLogs.handleLoanTokenPoller} - Error: ${JSON.stringify(data)}`
72
- );
73
-
74
- // send LendingServiceError to GA
75
- this.loanAnalytics?.sendEvent(
76
- 'LendingServiceLoanError',
77
- action,
78
- this.identifier
79
- );
80
- },
108
+ error: data => this.handleTokenError(data, isInitial),
81
109
  success: () => {
110
+ log('[LoanTokenPoller] create_token succeeded', {
111
+ identifier: this.identifier,
112
+ isInitial,
113
+ });
82
114
  if (isInitial) this.successCallback();
83
115
  },
84
116
  });
85
117
  }
118
+
119
+ /**
120
+ * Reports a create_token failure via errorCallback — no retry, it's
121
+ * treated as terminal on the first failure. Split out from
122
+ * handleLoanTokenPoller so it's directly testable without needing to
123
+ * mock the network call.
124
+ *
125
+ * @param {Object} data - the error payload from ActionsHandlerService.
126
+ * @param {boolean} isInitial
127
+ */
128
+ handleTokenError(data, isInitial) {
129
+ const action = 'create_token';
130
+
131
+ log('[LoanTokenPoller] create_token failed', {
132
+ identifier: this.identifier,
133
+ isInitial,
134
+ error: data?.error,
135
+ });
136
+
137
+ // isInitial rides along so the consumer can distinguish "the book won't
138
+ // open at all" from "a mid-read refresh blipped" — see
139
+ // IABookActions.handleLendingActionError.
140
+ this.errorCallback({ detail: { action, data, isInitial } });
141
+
142
+ // send error to Sentry
143
+ window?.Sentry?.captureMessage(
144
+ `${sentryLogs.handleLoanTokenPoller} - Error: ${JSON.stringify(data)}`
145
+ );
146
+
147
+ // send LendingServiceError to GA
148
+ this.loanAnalytics?.sendEvent(
149
+ 'LendingServiceLoanError',
150
+ action,
151
+ this.identifier
152
+ );
153
+ }
86
154
  }