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

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-alpha18",
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
  });
@@ -55,10 +55,6 @@ export default class IABookActions extends LitElement {
55
55
  type: Function,
56
56
  attribute: false,
57
57
  },
58
- reloadPageImages: {
59
- type: Function,
60
- attribute: false,
61
- },
62
58
  barType: { type: String },
63
59
  sharedObserver: { attribute: false },
64
60
  disableActionGroup: { type: Boolean },
@@ -81,7 +77,6 @@ export default class IABookActions extends LitElement {
81
77
  this.width = 0;
82
78
  this.bwbPurchaseUrl = '';
83
79
  this.lendingBarPostInit = () => {};
84
- this.reloadPageImages = () => {};
85
80
  this.barType = 'action'; // 'title'|'action'
86
81
  this.sharedObserver = undefined;
87
82
  this.disableActionGroup = false;
@@ -767,12 +762,6 @@ export default class IABookActions extends LitElement {
767
762
  // triggered this renewal.
768
763
  this.skipNextInitialTokenCall = true;
769
764
 
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).
774
- this.reloadPageImages();
775
-
776
765
  if (this.recoveringFromLoanExpiry) {
777
766
  // Only now is it safe to re-initialize BookReader — not for a
778
767
  // routine top-up, where that would be disruptive. Call it
@@ -946,13 +935,6 @@ export default class IABookActions extends LitElement {
946
935
  const errorCallback = eventObj => {
947
936
  this.handleLendingActionError(eventObj);
948
937
  };
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
938
 
957
939
  // Tear down any previous poller first, so only one is ever controlling
958
940
  // window.IALendingIntervals.tokenPoller at a time.
@@ -964,7 +946,6 @@ export default class IABookActions extends LitElement {
964
946
  errorCallback,
965
947
  pollerDelay: this.tokenDelay, // in seconds
966
948
  skipInitialCall,
967
- onTokenRefreshed,
968
949
  });
969
950
  }
970
951
 
@@ -989,7 +970,12 @@ export default class IABookActions extends LitElement {
989
970
  this.disableActionGroup = false;
990
971
 
991
972
  const action = event?.detail?.action;
992
- const errorMsg = event?.detail?.data?.error;
973
+ // handleLoanRenewNow's failure path sets `.error` to a boolean flag and
974
+ // puts the real text in `.message` — only take `.error` when it's
975
+ // actually a string, or the modal renders the literal word "true".
976
+ const rawErrorMsg = event?.detail?.data?.error;
977
+ const errorMsg =
978
+ typeof rawErrorMsg === 'string' ? rawErrorMsg : undefined;
993
979
  const isInitial = event?.detail?.isInitial === true;
994
980
 
995
981
  log('[IABookActions] handleLendingActionError', {
@@ -1002,20 +988,14 @@ export default class IABookActions extends LitElement {
1002
988
  });
1003
989
 
1004
990
  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.
991
+ // Any create_token failure — initial or a routine interval refresh —
992
+ // means access can no longer be confirmed (e.g. a stale loan-record
993
+ // cache on some other node can make this request fail even though
994
+ // the loan is fine, or the loan may genuinely have just been
995
+ // returned via BookReaderImages.php's revokeAccess()). Don't retry
996
+ // and don't stay silent either way: reset the bar to Borrow and the
997
+ // timer to 0 rather than leaving a stale countdown for a session
998
+ // that isn't accessible.
1019
999
  window?.IALendingIntervals?.clearAll();
1020
1000
  this.tokenPoller?.disconnectedCallback();
1021
1001
  this.lendingStatus = {
@@ -1026,8 +1006,9 @@ export default class IABookActions extends LitElement {
1026
1006
  };
1027
1007
 
1028
1008
  // showErrorModal has dedicated create_token messaging (refresh
1029
- // button + support email).
1030
- if (errorMsg) this.showErrorModal(errorMsg, action);
1009
+ // button + support email) and shows regardless of whether a
1010
+ // specific error string came back.
1011
+ this.showErrorModal(errorMsg, action);
1031
1012
  } else if (action === 'renew_loan') {
1032
1013
  window?.IALendingIntervals?.clearAll();
1033
1014
  this.loanRenewInProgress = false;
@@ -1095,7 +1076,7 @@ export default class IABookActions extends LitElement {
1095
1076
  .identifier}"
1096
1077
  >info@archive.org</a
1097
1078
  ><br /><br />
1098
- <code>errorLog: ${errorMsg}</code>`;
1079
+ ${errorMsg ? html`<code>errorLog: ${errorMsg}</code>` : nothing}`;
1099
1080
  }
1100
1081
 
1101
1082
  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
- });
@@ -958,7 +958,6 @@ describe('autoRenewExpiredLoan', () => {
958
958
  );
959
959
  await el.updateComplete;
960
960
  el.postInitComplete = true;
961
- el.reloadPageImages = Sinon.spy();
962
961
 
963
962
  await el.localCache.set({
964
963
  key: 'foobar-loanTime',
@@ -986,9 +985,6 @@ describe('autoRenewExpiredLoan', () => {
986
985
  await el.updateComplete;
987
986
 
988
987
  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.
991
- expect(el.reloadPageImages.called).to.be.true;
992
988
  });
993
989
 
994
990
  it('calls lendingBarPostInit directly on a recovery renewal and skips the initial create_token', async () => {
@@ -1010,7 +1006,6 @@ describe('autoRenewExpiredLoan', () => {
1010
1006
  );
1011
1007
  await el.updateComplete;
1012
1008
  el.lendingBarPostInit = Sinon.spy();
1013
- el.reloadPageImages = Sinon.spy();
1014
1009
 
1015
1010
  await el.localCache.set({
1016
1011
  key: 'foobar-loanTime',
@@ -1035,7 +1030,6 @@ describe('autoRenewExpiredLoan', () => {
1035
1030
  // create_token success callback.
1036
1031
  expect(el.lendingBarPostInit.calledOnce).to.be.true;
1037
1032
  expect(el.postInitComplete).to.be.true;
1038
- expect(el.reloadPageImages.called).to.be.true;
1039
1033
 
1040
1034
  await el.updateComplete;
1041
1035
  await aTimeout(150); // let the 100ms token-poller-restart check run
@@ -1176,75 +1170,75 @@ describe('handleLendingActionError - loanRenewInProgress reset', () => {
1176
1170
  });
1177
1171
  });
1178
1172
 
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,
1173
+ describe('handleLendingActionError - every create_token failure resets the bar (WEBDEV-8322 follow-up)', () => {
1174
+ // A create_token failure can't be told apart from "the loan was actually
1175
+ // returned" (e.g. a stale loan-record cache on another node, or a genuine
1176
+ // revokeAccess() in BookReaderImages.php) from here, so initial and
1177
+ // routine interval failures are treated identically: no retry, reset to
1178
+ // Borrow/0, show the modal every time.
1179
+ for (const isInitial of [true, false]) {
1180
+ it(`shows the error modal and resets to Borrow/0 on a failed create_token (isInitial=${isInitial})`, 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,
1189
+ },
1190
+ })
1191
+ );
1192
+ await el.updateComplete;
1193
+
1194
+ const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1195
+ const errorMsg = 'loan token not found. please try again later.';
1196
+
1197
+ el.handleLendingActionError({
1198
+ detail: {
1199
+ action: 'create_token',
1200
+ isInitial,
1201
+ data: { error: errorMsg },
1189
1202
  },
1190
- })
1191
- );
1192
- await el.updateComplete;
1193
- expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1203
+ });
1194
1204
 
1195
- el.handleLendingActionError({
1196
- detail: {
1197
- action: 'create_token',
1198
- data: { error: 'loan token not found. please try again later.' },
1199
- },
1205
+ expect(showErrorModalSpy.calledOnceWith(errorMsg, 'create_token')).to.be
1206
+ .true;
1207
+ expect(el.lendingStatus.user_has_browsed).to.be.false;
1208
+ expect(el.lendingStatus.available_to_browse).to.be.true;
1209
+ expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
1200
1210
  });
1201
1211
 
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
- });
1212
+ it(`stops the countdown/expiry timers on a failed create_token (isInitial=${isInitial})`, async () => {
1213
+ const el = await fixture(
1214
+ container({
1215
+ userid: '@user1',
1216
+ identifier: 'foobar',
1217
+ lendingStatus: {
1218
+ user_has_browsed: true,
1219
+ browsingExpired: false,
1220
+ secondsLeftOnLoan: 100,
1221
+ },
1222
+ })
1223
+ );
1224
+ await el.updateComplete;
1225
+ expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1226
+ expect(window.IALendingIntervals.browseExpireTimeout).to.not.equal(0);
1206
1227
 
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,
1228
+ el.handleLendingActionError({
1229
+ detail: {
1230
+ action: 'create_token',
1231
+ isInitial,
1232
+ data: { error: 'loan token not found. please try again later.' },
1220
1233
  },
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.';
1234
+ });
1227
1235
 
1228
- el.handleLendingActionError({
1229
- detail: {
1230
- action: 'create_token',
1231
- isInitial: true,
1232
- data: { error: errorMsg },
1233
- },
1236
+ expect(window.IALendingIntervals.timerCountdown).to.equal(0);
1237
+ expect(window.IALendingIntervals.browseExpireTimeout).to.equal(0);
1234
1238
  });
1239
+ }
1235
1240
 
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.
1241
+ it('shows the modal even without a specific error message', async () => {
1248
1242
  const el = await fixture(
1249
1243
  container({
1250
1244
  userid: '@user1',
@@ -1257,19 +1251,14 @@ describe('handleLendingActionError - create_token failures must not stop the cou
1257
1251
  })
1258
1252
  );
1259
1253
  await el.updateComplete;
1260
- expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1261
- expect(window.IALendingIntervals.browseExpireTimeout).to.not.equal(0);
1254
+
1255
+ const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1262
1256
 
1263
1257
  el.handleLendingActionError({
1264
- detail: {
1265
- action: 'create_token',
1266
- isInitial: true,
1267
- data: { error: 'loan token not found. please try again later.' },
1268
- },
1258
+ detail: { action: 'create_token', isInitial: false, data: {} },
1269
1259
  });
1270
1260
 
1271
- expect(window.IALendingIntervals.timerCountdown).to.equal(0);
1272
- expect(window.IALendingIntervals.browseExpireTimeout).to.equal(0);
1261
+ expect(showErrorModalSpy.calledOnce).to.be.true;
1273
1262
  });
1274
1263
 
1275
1264
  it('still clears everything on a renew_loan failure', async () => {
@@ -1614,7 +1603,10 @@ describe('WEBDEV-8322 review fixes', () => {
1614
1603
  expect(el.loanRenewInProgress).to.be.true;
1615
1604
  });
1616
1605
 
1617
- it('stays silent when an interval create_token refresh fails', async () => {
1606
+ it('resets the bar and shows the modal on an interval create_token refresh failure too', async () => {
1607
+ // A routine refresh failing is treated the same as the initial one --
1608
+ // there's no reliable way to tell "harmless blip" from "the loan is
1609
+ // actually gone" from here (see handleLendingActionError).
1618
1610
  const el = await fixture(
1619
1611
  container({
1620
1612
  userid: '@user1',
@@ -1640,9 +1632,8 @@ describe('WEBDEV-8322 review fixes', () => {
1640
1632
  });
1641
1633
  await el.updateComplete;
1642
1634
 
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;
1635
+ expect(spy.calledOnce).to.be.true;
1636
+ expect(el.lendingStatus.user_has_browsed).to.be.false;
1646
1637
  });
1647
1638
 
1648
1639
  it('tears down the previous poller before starting a new one', async () => {