@internetarchive/ia-book-actions 1.4.4-alpha17 → 1.4.4-alpha19

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-alpha17",
10
+ "version": "1.4.4-alpha19",
11
11
  "main": "index.js",
12
12
  "module": "index.js",
13
13
  "publishConfig": {
@@ -106,9 +106,9 @@ export default async function ActionsHandlerService(options) {
106
106
 
107
107
  /**
108
108
  * Report it, don't just swallow it. A rejected fetch (offline, dropped
109
- * connection) or a non-JSON body (a 405 returning HTML, which QA has
110
- * been seeing) lands here, and calling neither `success` nor `error`
111
- * leaves every caller waiting on a callback that never comes.
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
112
  *
113
113
  * For renew_loan that's not merely a missing modal: IABookActions
114
114
  * clears its loanRenewInProgress guard from these callbacks, so a
@@ -117,5 +117,6 @@ export default async function ActionsHandlerService(options) {
117
117
  * connection is exactly when this fires.
118
118
  */
119
119
  log(`[IABookActions] ✗ ${option.action} threw`, error);
120
+ option?.error?.({ error: String(error) });
120
121
  }
121
122
  }
@@ -185,7 +185,7 @@ export default class ActionsHandler extends LitElement {
185
185
  log('RENEW_LOAN --- ', data, this.identifier);
186
186
  const activeLoan = data.loan ? data.loan : undefined;
187
187
  // Optional chaining: `data.loan` is genuinely absent on some
188
- // failures, and reading `.renewal` off undefined threw here.
188
+ // failures, and `activeLoan` can be undefined here.
189
189
  const isRenewal = activeLoan?.renewal;
190
190
 
191
191
  if (activeLoan && isRenewal) {
@@ -210,11 +210,9 @@ export default class ActionsHandler extends LitElement {
210
210
  this.identifier
211
211
  );
212
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.
213
+ // Dispatch the success outcome ONLY for a confirmed renewal —
214
+ // a `{loan: {renewal: false}}` response goes through
215
+ // dispatchActionError below instead, never both.
218
216
  this.dispatchEvent(
219
217
  new CustomEvent('loanAutoRenewed', {
220
218
  detail: { action, data: { ...data, loan: activeLoan } },
@@ -26,11 +26,6 @@ export class LoanTokenPoller {
26
26
  * attempt_to_renew_loan() already minted a valid access token as part
27
27
  * of the renew_loan response itself — an immediate confirming
28
28
  * create_token call is redundant.
29
- * @param {Function} [options.onTokenRefreshed] - called after EVERY
30
- * successful create_token, initial or routine — unlike successCallback,
31
- * which only fires for the initial one. Lets the consumer retry any
32
- * page image that failed during a prior access gap, now that access is
33
- * confirmed good again.
34
29
  */
35
30
  constructor(options = {}) {
36
31
  const {
@@ -40,7 +35,6 @@ export class LoanTokenPoller {
40
35
  errorCallback,
41
36
  pollerDelay,
42
37
  skipInitialCall = false,
43
- onTokenRefreshed,
44
38
  } = options;
45
39
 
46
40
  this.identifier = identifier;
@@ -49,7 +43,6 @@ export class LoanTokenPoller {
49
43
  this.errorCallback = errorCallback; // callback function to be called after loan token is created
50
44
  this.pollerDelay = pollerDelay; // value in seconds
51
45
  this.skipInitialCall = skipInitialCall === true;
52
- this.onTokenRefreshed = onTokenRefreshed;
53
46
 
54
47
  this.loanTokenInterval = undefined;
55
48
 
@@ -118,7 +111,6 @@ export class LoanTokenPoller {
118
111
  identifier: this.identifier,
119
112
  isInitial,
120
113
  });
121
- this.onTokenRefreshed?.();
122
114
  if (isInitial) this.successCallback();
123
115
  },
124
116
  });
@@ -767,10 +767,10 @@ export default class IABookActions extends LitElement {
767
767
  // triggered this renewal.
768
768
  this.skipNextInitialTokenCall = true;
769
769
 
770
- // Retry any page image that failed during the gap this renewal just
771
- // closed, right away rather than waiting for the next routine
772
- // create_token tick (up to tokenDelay seconds away, now that the
773
- // initial one above is skipped).
770
+ // A page image requested in the gap between expiry and this renewal
771
+ // landing can come back broken — browsers don't retry a failed <img>
772
+ // on their own. Retry once, right when access is confirmed good
773
+ // again, rather than leaving it broken until the patron reloads.
774
774
  this.reloadPageImages();
775
775
 
776
776
  if (this.recoveringFromLoanExpiry) {
@@ -946,13 +946,6 @@ export default class IABookActions extends LitElement {
946
946
  const errorCallback = eventObj => {
947
947
  this.handleLendingActionError(eventObj);
948
948
  };
949
- // BookReader has no self-healing for a page <img> that failed while
950
- // access was briefly invalid — retry any of those now that this
951
- // create_token confirms access is good again, on every success, not
952
- // just the initial one.
953
- const onTokenRefreshed = () => {
954
- this.reloadPageImages();
955
- };
956
949
 
957
950
  // Tear down any previous poller first, so only one is ever controlling
958
951
  // window.IALendingIntervals.tokenPoller at a time.
@@ -964,7 +957,6 @@ export default class IABookActions extends LitElement {
964
957
  errorCallback,
965
958
  pollerDelay: this.tokenDelay, // in seconds
966
959
  skipInitialCall,
967
- onTokenRefreshed,
968
960
  });
969
961
  }
970
962
 
@@ -989,7 +981,12 @@ export default class IABookActions extends LitElement {
989
981
  this.disableActionGroup = false;
990
982
 
991
983
  const action = event?.detail?.action;
992
- const errorMsg = event?.detail?.data?.error;
984
+ // handleLoanRenewNow's failure path sets `.error` to a boolean flag and
985
+ // puts the real text in `.message` — only take `.error` when it's
986
+ // actually a string, or the modal renders the literal word "true".
987
+ const rawErrorMsg = event?.detail?.data?.error;
988
+ const errorMsg =
989
+ typeof rawErrorMsg === 'string' ? rawErrorMsg : undefined;
993
990
  const isInitial = event?.detail?.isInitial === true;
994
991
 
995
992
  log('[IABookActions] handleLendingActionError', {
@@ -1002,20 +999,14 @@ export default class IABookActions extends LitElement {
1002
999
  });
1003
1000
 
1004
1001
  if (action === 'create_token') {
1005
- // Only the INITIAL token matters enough to interrupt the patron — a
1006
- // routine interval refresh stays silent; the pages already loaded
1007
- // are still served, and the next scheduled poll tries again on its
1008
- // own without any special retry.
1009
- if (!isInitial) {
1010
- log(
1011
- '[IABookActions] create_token failed on interval refresh — staying silent'
1012
- );
1013
- return;
1014
- }
1015
-
1016
- // The book never opened, and we don't retry — reset the bar to
1017
- // Borrow and the timer to 0 rather than leaving a red "Return now"
1018
- // with a stale countdown for a session that isn't accessible.
1002
+ // Any create_token failure — initial or a routine interval refresh —
1003
+ // means access can no longer be confirmed (e.g. a stale loan-record
1004
+ // cache on some other node can make this request fail even though
1005
+ // the loan is fine, or the loan may genuinely have just been
1006
+ // returned via BookReaderImages.php's revokeAccess()). Don't retry
1007
+ // and don't stay silent either way: reset the bar to Borrow and the
1008
+ // timer to 0 rather than leaving a stale countdown for a session
1009
+ // that isn't accessible.
1019
1010
  window?.IALendingIntervals?.clearAll();
1020
1011
  this.tokenPoller?.disconnectedCallback();
1021
1012
  this.lendingStatus = {
@@ -1026,8 +1017,9 @@ export default class IABookActions extends LitElement {
1026
1017
  };
1027
1018
 
1028
1019
  // showErrorModal has dedicated create_token messaging (refresh
1029
- // button + support email).
1030
- if (errorMsg) this.showErrorModal(errorMsg, action);
1020
+ // button + support email) and shows regardless of whether a
1021
+ // specific error string came back.
1022
+ this.showErrorModal(errorMsg, action);
1031
1023
  } else if (action === 'renew_loan') {
1032
1024
  window?.IALendingIntervals?.clearAll();
1033
1025
  this.loanRenewInProgress = false;
@@ -1095,7 +1087,7 @@ export default class IABookActions extends LitElement {
1095
1087
  .identifier}"
1096
1088
  >info@archive.org</a
1097
1089
  ><br /><br />
1098
- <code>errorLog: ${errorMsg}</code>`;
1090
+ ${errorMsg ? html`<code>errorLog: ${errorMsg}</code>` : nothing}`;
1099
1091
  }
1100
1092
 
1101
1093
  await this.modal?.showModal({
@@ -1,4 +1,4 @@
1
- import { expect, aTimeout } from '@open-wc/testing';
1
+ import { expect } from '@open-wc/testing';
2
2
  import Sinon from 'sinon';
3
3
 
4
4
  import { LoanTokenPoller } from '../../../src/core/services/loan-token-poller.js';
@@ -152,45 +152,3 @@ describe('skipInitialCall option', () => {
152
152
  poller.disconnectedCallback();
153
153
  });
154
154
  });
155
-
156
- describe('onTokenRefreshed option', () => {
157
- // BookReader has no self-healing for a page <img> that failed while
158
- // access was briefly invalid, so the consumer needs a signal on EVERY
159
- // create_token success (not just the initial one) to know it's safe to
160
- // retry those images.
161
- it('fires on the initial create_token success', async () => {
162
- const onTokenRefreshed = Sinon.spy();
163
- const poller = new LoanTokenPoller({
164
- identifier: 'foo',
165
- borrowType: 'browsed',
166
- successCallback: () => {},
167
- errorCallback: () => {},
168
- pollerDelay: 120,
169
- onTokenRefreshed,
170
- });
171
-
172
- await aTimeout(50);
173
-
174
- expect(onTokenRefreshed.called).to.be.true;
175
- poller.disconnectedCallback();
176
- });
177
-
178
- it('fires on a routine (non-initial) create_token success too', async () => {
179
- const onTokenRefreshed = Sinon.spy();
180
- const poller = new LoanTokenPoller({
181
- identifier: 'foo',
182
- borrowType: 'browsed',
183
- successCallback: () => {},
184
- errorCallback: () => {},
185
- pollerDelay: 120,
186
- onTokenRefreshed,
187
- });
188
- onTokenRefreshed.resetHistory();
189
-
190
- await poller.handleLoanTokenPoller(false);
191
- await aTimeout(50);
192
-
193
- expect(onTokenRefreshed.called).to.be.true;
194
- poller.disconnectedCallback();
195
- });
196
- });
@@ -986,8 +986,7 @@ describe('autoRenewExpiredLoan', () => {
986
986
  await el.updateComplete;
987
987
 
988
988
  expect(startTokenPollerSpy.calledOnceWith(true)).to.be.true;
989
- // Retried right away rather than waiting for the (now-skipped) initial
990
- // create_token or the next routine tick.
989
+ // Retries any page image broken during the gap this renewal just closed.
991
990
  expect(el.reloadPageImages.called).to.be.true;
992
991
  });
993
992
 
@@ -1176,75 +1175,75 @@ describe('handleLendingActionError - loanRenewInProgress reset', () => {
1176
1175
  });
1177
1176
  });
1178
1177
 
1179
- describe('handleLendingActionError - create_token failures must not stop the countdown (WEBDEV-8322 follow-up)', () => {
1180
- it('does not clear the reading countdown on a create_token failure', async () => {
1181
- const el = await fixture(
1182
- container({
1183
- userid: '@user1',
1184
- identifier: 'foobar',
1185
- lendingStatus: {
1186
- user_has_browsed: true,
1187
- browsingExpired: false,
1188
- secondsLeftOnLoan: 100,
1178
+ describe('handleLendingActionError - every create_token failure resets the bar (WEBDEV-8322 follow-up)', () => {
1179
+ // A create_token failure can't be told apart from "the loan was actually
1180
+ // returned" (e.g. a stale loan-record cache on another node, or a genuine
1181
+ // revokeAccess() in BookReaderImages.php) from here, so initial and
1182
+ // routine interval failures are treated identically: no retry, reset to
1183
+ // Borrow/0, show the modal every time.
1184
+ for (const isInitial of [true, false]) {
1185
+ it(`shows the error modal and resets to Borrow/0 on a failed create_token (isInitial=${isInitial})`, async () => {
1186
+ const el = await fixture(
1187
+ container({
1188
+ userid: '@user1',
1189
+ identifier: 'foobar',
1190
+ lendingStatus: {
1191
+ user_has_browsed: true,
1192
+ browsingExpired: false,
1193
+ secondsLeftOnLoan: 100,
1194
+ },
1195
+ })
1196
+ );
1197
+ await el.updateComplete;
1198
+
1199
+ const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1200
+ const errorMsg = 'loan token not found. please try again later.';
1201
+
1202
+ el.handleLendingActionError({
1203
+ detail: {
1204
+ action: 'create_token',
1205
+ isInitial,
1206
+ data: { error: errorMsg },
1189
1207
  },
1190
- })
1191
- );
1192
- await el.updateComplete;
1193
- expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1208
+ });
1194
1209
 
1195
- el.handleLendingActionError({
1196
- detail: {
1197
- action: 'create_token',
1198
- data: { error: 'loan token not found. please try again later.' },
1199
- },
1210
+ expect(showErrorModalSpy.calledOnceWith(errorMsg, 'create_token')).to.be
1211
+ .true;
1212
+ expect(el.lendingStatus.user_has_browsed).to.be.false;
1213
+ expect(el.lendingStatus.available_to_browse).to.be.true;
1214
+ expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
1200
1215
  });
1201
1216
 
1202
- // The countdown must keep running — a token refresh hiccup says
1203
- // nothing about how much time is left on the loan itself.
1204
- expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1205
- });
1217
+ it(`stops the countdown/expiry timers on a failed create_token (isInitial=${isInitial})`, async () => {
1218
+ const el = await fixture(
1219
+ container({
1220
+ userid: '@user1',
1221
+ identifier: 'foobar',
1222
+ lendingStatus: {
1223
+ user_has_browsed: true,
1224
+ browsingExpired: false,
1225
+ secondsLeftOnLoan: 100,
1226
+ },
1227
+ })
1228
+ );
1229
+ await el.updateComplete;
1230
+ expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1231
+ expect(window.IALendingIntervals.browseExpireTimeout).to.not.equal(0);
1206
1232
 
1207
- it('shows the error modal and resets to Borrow/0 on a failed INITIAL create_token (no retry)', async () => {
1208
- // For the INITIAL token, BookReader never got page images — the book
1209
- // never opened. We don't retry, so the bar must reflect that honestly
1210
- // (Borrow, 0 left) instead of staying red with a stale countdown for a
1211
- // session that isn't actually accessible.
1212
- const el = await fixture(
1213
- container({
1214
- userid: '@user1',
1215
- identifier: 'foobar',
1216
- lendingStatus: {
1217
- user_has_browsed: true,
1218
- browsingExpired: false,
1219
- secondsLeftOnLoan: 100,
1233
+ el.handleLendingActionError({
1234
+ detail: {
1235
+ action: 'create_token',
1236
+ isInitial,
1237
+ data: { error: 'loan token not found. please try again later.' },
1220
1238
  },
1221
- })
1222
- );
1223
- await el.updateComplete;
1224
-
1225
- const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1226
- const errorMsg = 'loan token not found. please try again later.';
1239
+ });
1227
1240
 
1228
- el.handleLendingActionError({
1229
- detail: {
1230
- action: 'create_token',
1231
- isInitial: true,
1232
- data: { error: errorMsg },
1233
- },
1241
+ expect(window.IALendingIntervals.timerCountdown).to.equal(0);
1242
+ expect(window.IALendingIntervals.browseExpireTimeout).to.equal(0);
1234
1243
  });
1244
+ }
1235
1245
 
1236
- expect(showErrorModalSpy.calledOnceWith(errorMsg, 'create_token')).to.be
1237
- .true;
1238
- expect(el.lendingStatus.user_has_browsed).to.be.false;
1239
- expect(el.lendingStatus.available_to_browse).to.be.true;
1240
- expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
1241
- });
1242
-
1243
- it('stops the countdown/expiry timers on a failed INITIAL create_token — nothing to read yet', async () => {
1244
- // Unlike the interval-refresh case above (pages already loaded, keep
1245
- // ticking), an initial create_token failure means BookReader never got
1246
- // page images and we're resetting the bar to Borrow — both timers
1247
- // should stop rather than keep counting down a dead session.
1246
+ it('shows the modal even without a specific error message', async () => {
1248
1247
  const el = await fixture(
1249
1248
  container({
1250
1249
  userid: '@user1',
@@ -1257,19 +1256,14 @@ describe('handleLendingActionError - create_token failures must not stop the cou
1257
1256
  })
1258
1257
  );
1259
1258
  await el.updateComplete;
1260
- expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1261
- expect(window.IALendingIntervals.browseExpireTimeout).to.not.equal(0);
1259
+
1260
+ const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1262
1261
 
1263
1262
  el.handleLendingActionError({
1264
- detail: {
1265
- action: 'create_token',
1266
- isInitial: true,
1267
- data: { error: 'loan token not found. please try again later.' },
1268
- },
1263
+ detail: { action: 'create_token', isInitial: false, data: {} },
1269
1264
  });
1270
1265
 
1271
- expect(window.IALendingIntervals.timerCountdown).to.equal(0);
1272
- expect(window.IALendingIntervals.browseExpireTimeout).to.equal(0);
1266
+ expect(showErrorModalSpy.calledOnce).to.be.true;
1273
1267
  });
1274
1268
 
1275
1269
  it('still clears everything on a renew_loan failure', async () => {
@@ -1614,7 +1608,10 @@ describe('WEBDEV-8322 review fixes', () => {
1614
1608
  expect(el.loanRenewInProgress).to.be.true;
1615
1609
  });
1616
1610
 
1617
- it('stays silent when an interval create_token refresh fails', async () => {
1611
+ it('resets the bar and shows the modal on an interval create_token refresh failure too', async () => {
1612
+ // A routine refresh failing is treated the same as the initial one --
1613
+ // there's no reliable way to tell "harmless blip" from "the loan is
1614
+ // actually gone" from here (see handleLendingActionError).
1618
1615
  const el = await fixture(
1619
1616
  container({
1620
1617
  userid: '@user1',
@@ -1640,9 +1637,8 @@ describe('WEBDEV-8322 review fixes', () => {
1640
1637
  });
1641
1638
  await el.updateComplete;
1642
1639
 
1643
- expect(spy.called).to.be.false;
1644
- // the patron is still reading — don't knock them out of the loan
1645
- expect(el.lendingStatus.user_has_browsed).to.be.true;
1640
+ expect(spy.calledOnce).to.be.true;
1641
+ expect(el.lendingStatus.user_has_browsed).to.be.false;
1646
1642
  });
1647
1643
 
1648
1644
  it('tears down the previous poller before starting a new one', async () => {