@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 +1 -1
- package/src/core/services/actions-handler/actions-handler-service.js +4 -3
- package/src/core/services/actions-handler/actions-handler.js +4 -6
- package/src/core/services/loan-token-poller.js +0 -8
- package/src/ia-book-actions.js +22 -30
- package/test/core/services/loan-token-poller.test.js +1 -43
- package/test/ia-book-actions.test.js +71 -75
package/package.json
CHANGED
|
@@ -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
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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
|
|
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
|
-
//
|
|
215
|
-
//
|
|
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
|
});
|
package/src/ia-book-actions.js
CHANGED
|
@@ -767,10 +767,10 @@ export default class IABookActions extends LitElement {
|
|
|
767
767
|
// triggered this renewal.
|
|
768
768
|
this.skipNextInitialTokenCall = true;
|
|
769
769
|
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
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(
|
|
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('
|
|
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.
|
|
1644
|
-
|
|
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 () => {
|