@internetarchive/ia-book-actions 1.4.4-alpha16 → 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-alpha16",
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 } },
@@ -970,7 +970,12 @@ export default class IABookActions extends LitElement {
970
970
  this.disableActionGroup = false;
971
971
 
972
972
  const action = event?.detail?.action;
973
- 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;
974
979
  const isInitial = event?.detail?.isInitial === true;
975
980
 
976
981
  log('[IABookActions] handleLendingActionError', {
@@ -983,20 +988,14 @@ export default class IABookActions extends LitElement {
983
988
  });
984
989
 
985
990
  if (action === 'create_token') {
986
- // Only the INITIAL token matters enough to interrupt the patron — a
987
- // routine interval refresh stays silent; the pages already loaded
988
- // are still served, and the next scheduled poll tries again on its
989
- // own without any special retry.
990
- if (!isInitial) {
991
- log(
992
- '[IABookActions] create_token failed on interval refresh — staying silent'
993
- );
994
- return;
995
- }
996
-
997
- // The book never opened, and we don't retry — reset the bar to
998
- // Borrow and the timer to 0 rather than leaving a red "Return now"
999
- // 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.
1000
999
  window?.IALendingIntervals?.clearAll();
1001
1000
  this.tokenPoller?.disconnectedCallback();
1002
1001
  this.lendingStatus = {
@@ -1007,8 +1006,9 @@ export default class IABookActions extends LitElement {
1007
1006
  };
1008
1007
 
1009
1008
  // showErrorModal has dedicated create_token messaging (refresh
1010
- // button + support email).
1011
- 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);
1012
1012
  } else if (action === 'renew_loan') {
1013
1013
  window?.IALendingIntervals?.clearAll();
1014
1014
  this.loanRenewInProgress = false;
@@ -1076,7 +1076,7 @@ export default class IABookActions extends LitElement {
1076
1076
  .identifier}"
1077
1077
  >info@archive.org</a
1078
1078
  ><br /><br />
1079
- <code>errorLog: ${errorMsg}</code>`;
1079
+ ${errorMsg ? html`<code>errorLog: ${errorMsg}</code>` : nothing}`;
1080
1080
  }
1081
1081
 
1082
1082
  await this.modal?.showModal({
@@ -1130,6 +1130,10 @@ export default class IABookActions extends LitElement {
1130
1130
  justify-content: center;
1131
1131
  flex-wrap: wrap;
1132
1132
  }
1133
+
1134
+ #action-bar-modal {
1135
+ --modalWidth: 36rem;
1136
+ }
1133
1137
  `;
1134
1138
  }
1135
1139
  }
@@ -1170,75 +1170,75 @@ describe('handleLendingActionError - loanRenewInProgress reset', () => {
1170
1170
  });
1171
1171
  });
1172
1172
 
1173
- describe('handleLendingActionError - create_token failures must not stop the countdown (WEBDEV-8322 follow-up)', () => {
1174
- it('does not clear the reading countdown on a create_token failure', async () => {
1175
- const el = await fixture(
1176
- container({
1177
- userid: '@user1',
1178
- identifier: 'foobar',
1179
- lendingStatus: {
1180
- user_has_browsed: true,
1181
- browsingExpired: false,
1182
- 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 },
1183
1202
  },
1184
- })
1185
- );
1186
- await el.updateComplete;
1187
- expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1203
+ });
1188
1204
 
1189
- el.handleLendingActionError({
1190
- detail: {
1191
- action: 'create_token',
1192
- data: { error: 'loan token not found. please try again later.' },
1193
- },
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);
1194
1210
  });
1195
1211
 
1196
- // The countdown must keep running — a token refresh hiccup says
1197
- // nothing about how much time is left on the loan itself.
1198
- expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1199
- });
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);
1200
1227
 
1201
- it('shows the error modal and resets to Borrow/0 on a failed INITIAL create_token (no retry)', async () => {
1202
- // For the INITIAL token, BookReader never got page images — the book
1203
- // never opened. We don't retry, so the bar must reflect that honestly
1204
- // (Borrow, 0 left) instead of staying red with a stale countdown for a
1205
- // session that isn't actually accessible.
1206
- const el = await fixture(
1207
- container({
1208
- userid: '@user1',
1209
- identifier: 'foobar',
1210
- lendingStatus: {
1211
- user_has_browsed: true,
1212
- browsingExpired: false,
1213
- secondsLeftOnLoan: 100,
1228
+ el.handleLendingActionError({
1229
+ detail: {
1230
+ action: 'create_token',
1231
+ isInitial,
1232
+ data: { error: 'loan token not found. please try again later.' },
1214
1233
  },
1215
- })
1216
- );
1217
- await el.updateComplete;
1218
-
1219
- const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1220
- const errorMsg = 'loan token not found. please try again later.';
1234
+ });
1221
1235
 
1222
- el.handleLendingActionError({
1223
- detail: {
1224
- action: 'create_token',
1225
- isInitial: true,
1226
- data: { error: errorMsg },
1227
- },
1236
+ expect(window.IALendingIntervals.timerCountdown).to.equal(0);
1237
+ expect(window.IALendingIntervals.browseExpireTimeout).to.equal(0);
1228
1238
  });
1239
+ }
1229
1240
 
1230
- expect(showErrorModalSpy.calledOnceWith(errorMsg, 'create_token')).to.be
1231
- .true;
1232
- expect(el.lendingStatus.user_has_browsed).to.be.false;
1233
- expect(el.lendingStatus.available_to_browse).to.be.true;
1234
- expect(el.lendingStatus.secondsLeftOnLoan).to.equal(0);
1235
- });
1236
-
1237
- it('stops the countdown/expiry timers on a failed INITIAL create_token — nothing to read yet', async () => {
1238
- // Unlike the interval-refresh case above (pages already loaded, keep
1239
- // ticking), an initial create_token failure means BookReader never got
1240
- // page images and we're resetting the bar to Borrow — both timers
1241
- // should stop rather than keep counting down a dead session.
1241
+ it('shows the modal even without a specific error message', async () => {
1242
1242
  const el = await fixture(
1243
1243
  container({
1244
1244
  userid: '@user1',
@@ -1251,19 +1251,14 @@ describe('handleLendingActionError - create_token failures must not stop the cou
1251
1251
  })
1252
1252
  );
1253
1253
  await el.updateComplete;
1254
- expect(window.IALendingIntervals.timerCountdown).to.not.equal(0);
1255
- expect(window.IALendingIntervals.browseExpireTimeout).to.not.equal(0);
1254
+
1255
+ const showErrorModalSpy = Sinon.spy(el, 'showErrorModal');
1256
1256
 
1257
1257
  el.handleLendingActionError({
1258
- detail: {
1259
- action: 'create_token',
1260
- isInitial: true,
1261
- data: { error: 'loan token not found. please try again later.' },
1262
- },
1258
+ detail: { action: 'create_token', isInitial: false, data: {} },
1263
1259
  });
1264
1260
 
1265
- expect(window.IALendingIntervals.timerCountdown).to.equal(0);
1266
- expect(window.IALendingIntervals.browseExpireTimeout).to.equal(0);
1261
+ expect(showErrorModalSpy.calledOnce).to.be.true;
1267
1262
  });
1268
1263
 
1269
1264
  it('still clears everything on a renew_loan failure', async () => {
@@ -1608,7 +1603,10 @@ describe('WEBDEV-8322 review fixes', () => {
1608
1603
  expect(el.loanRenewInProgress).to.be.true;
1609
1604
  });
1610
1605
 
1611
- 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).
1612
1610
  const el = await fixture(
1613
1611
  container({
1614
1612
  userid: '@user1',
@@ -1634,9 +1632,8 @@ describe('WEBDEV-8322 review fixes', () => {
1634
1632
  });
1635
1633
  await el.updateComplete;
1636
1634
 
1637
- expect(spy.called).to.be.false;
1638
- // the patron is still reading — don't knock them out of the loan
1639
- 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;
1640
1637
  });
1641
1638
 
1642
1639
  it('tears down the previous poller before starting a new one', async () => {