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

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-alpha12",
10
+ "version": "1.4.4-alpha14",
11
11
  "main": "index.js",
12
12
  "module": "index.js",
13
13
  "publishConfig": {
@@ -4,9 +4,14 @@ export const infoIcon = html`<svg
4
4
  height="20"
5
5
  width="20"
6
6
  viewBox="0 0 100 100"
7
- xmlns="http://www.w3.org/2000/svg">
8
- <g fill="none" fill-rule="evenodd"><path d="m0 0h100v100h-100z" fill="#fff"/>
9
- <path 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" fill="#000" fill-rule="nonzero"/>
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
+ />
10
16
  </g>
11
- </svg>
12
- `;
17
+ </svg> `;
@@ -95,5 +95,22 @@ export default async function ActionsHandlerService(options) {
95
95
  window?.Sentry?.captureException(
96
96
  `${sentryLogs.actionsHandlerService} - Error: ${error}`
97
97
  );
98
+
99
+ /**
100
+ * Report it, don't just swallow it. A rejected fetch (offline, dropped
101
+ * connection) or a non-JSON body (a 405 returning HTML, which QA has
102
+ * been seeing) lands here, and calling neither `success` nor `error`
103
+ * leaves every caller waiting on a callback that never comes.
104
+ *
105
+ * For renew_loan that's not merely a missing modal: IABookActions
106
+ * clears its loanRenewInProgress guard from these callbacks, so a
107
+ * silent failure latched it on for good and every later renewal
108
+ * attempt became a no-op. Backgrounding a tab on a flaky mobile
109
+ * connection is exactly when this fires.
110
+ */
111
+ log(`[IABookActions] ✗ ${option.action} threw`, error);
112
+ option?.error({
113
+ error: `Could not reach the lending service. Please check your connection and try again. (${error})`,
114
+ });
98
115
  }
99
116
  }
@@ -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,53 @@ 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 reading `.renewal` off undefined threw 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
+ // This used to fire on the failure path too, so a
215
+ // `{loan: {renewal: false}}` response reported an error AND a
216
+ // success: the error modal went up while handleLoanAutoRenewed
217
+ // simultaneously put a fresh countdown back on the bar.
218
+ this.dispatchEvent(
219
+ new CustomEvent('loanAutoRenewed', {
220
+ detail: { action, data: { ...data, loan: activeLoan } },
221
+ })
222
+ );
223
+ return;
224
+ }
225
+
201
226
  log('RENEW_LOAN ERROR --- ', {
202
227
  action,
203
228
  isRenewal,
@@ -213,14 +238,17 @@ export default class ActionsHandler extends LitElement {
213
238
  error: true,
214
239
  message: 'Loan renewal failed: no loan active.',
215
240
  });
241
+ } catch (error) {
242
+ log('RENEW_LOAN THREW --- ', error);
243
+ window?.Sentry?.captureException(
244
+ `${sentryLogs.bookRenewFailed} - Exception: ${error}`
245
+ );
246
+ this.dispatchActionError(action, {
247
+ data,
248
+ error: true,
249
+ message: `Loan renewal failed: ${error}`,
250
+ });
216
251
  }
217
-
218
- // dispatch outcome of loan renewal
219
- this.dispatchEvent(
220
- new CustomEvent('loanAutoRenewed', {
221
- detail: { action, data: { ...data, loan: activeLoan } },
222
- })
223
- );
224
252
  },
225
253
  error: data => {
226
254
  this.dispatchActionError(action, data);
@@ -428,7 +456,8 @@ export default class ActionsHandler extends LitElement {
428
456
  * @returns {void}
429
457
  */
430
458
  setStickyAdminAccess(value) {
431
- const domain = window.location.hostname === 'localhost' ? 'localhost' : '.archive.org';
459
+ const domain =
460
+ window.location.hostname === 'localhost' ? 'localhost' : '.archive.org';
432
461
  const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days from now
433
462
  Cookies.setItem('sticky-admin-access', value, expires, '/', domain);
434
463
  }
@@ -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
  }
@@ -28,8 +28,16 @@ export class LoanTokenPoller {
28
28
  * the loan is genuinely valid. Retry a few times with backoff instead
29
29
  * of giving up on the first attempt.
30
30
  */
31
- this.maxInitialTokenRetries = 3;
32
- this.initialTokenRetryDelay = 1000; // ms, multiplied by attempt number
31
+ this.maxTokenRetries = 3;
32
+ this.tokenRetryDelay = 1000; // ms, multiplied by attempt number
33
+
34
+ /**
35
+ * Handle for a pending retry, so teardown can cancel it. It's a bare
36
+ * setTimeout rather than an IALendingIntervals entry, so without this
37
+ * a retry scheduled just before the loan is returned would still fire
38
+ * its create_token afterwards.
39
+ */
40
+ this.retryTimeout = undefined;
33
41
 
34
42
  /**
35
43
  * loan analytics instance
@@ -42,6 +50,8 @@ export class LoanTokenPoller {
42
50
 
43
51
  disconnectedCallback() {
44
52
  window?.IALendingIntervals?.clearTokenPoller();
53
+ clearTimeout(this.retryTimeout);
54
+ this.retryTimeout = undefined;
45
55
  }
46
56
 
47
57
  async bookAccessed() {
@@ -123,19 +133,24 @@ export class LoanTokenPoller {
123
133
  error: data?.error,
124
134
  });
125
135
 
126
- if (
127
- isInitial &&
128
- isStaleLoanReadError &&
129
- retryCount < this.maxInitialTokenRetries
130
- ) {
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
- });
137
- setTimeout(() => {
138
- this.handleLoanTokenPoller(true, retryCount + 1);
136
+ // The renewal write can be mid-propagation for an interval refresh too,
137
+ // not just the initial call — a routine tick landing moments after a
138
+ // renewal hits the same race. Retry both; isInitial only decides how a
139
+ // final failure is reported, not whether it's worth retrying.
140
+ if (isStaleLoanReadError && retryCount < this.maxTokenRetries) {
141
+ const delay = this.tokenRetryDelay * (retryCount + 1);
142
+ log(
143
+ '[LoanTokenPoller] retrying create_token after stale-loan-read error',
144
+ {
145
+ identifier: this.identifier,
146
+ nextRetryCount: retryCount + 1,
147
+ delay,
148
+ }
149
+ );
150
+ clearTimeout(this.retryTimeout);
151
+ this.retryTimeout = setTimeout(() => {
152
+ this.retryTimeout = undefined;
153
+ this.handleLoanTokenPoller(isInitial, retryCount + 1);
139
154
  }, delay);
140
155
  return;
141
156
  }
@@ -146,7 +161,10 @@ export class LoanTokenPoller {
146
161
  retryCount,
147
162
  });
148
163
 
149
- this.errorCallback({ detail: { action, data } });
164
+ // isInitial rides along so the consumer can distinguish "the book won't
165
+ // open at all" from "a mid-read refresh blipped" — see
166
+ // IABookActions.handleLendingActionError.
167
+ this.errorCallback({ detail: { action, data, isInitial } });
150
168
 
151
169
  // send error to Sentry
152
170
  window?.Sentry?.captureMessage(