@internetarchive/ia-book-actions 1.4.4-alpha15 → 1.4.4-alpha16
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
|
@@ -117,8 +117,5 @@ 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({
|
|
121
|
-
error: `Could not reach the lending service. Please check your connection and try again. (${error})`,
|
|
122
|
-
});
|
|
123
120
|
}
|
|
124
121
|
}
|
|
@@ -10,12 +10,39 @@ import log from './log.js';
|
|
|
10
10
|
* ActionsHandlerService is a function being used to execute
|
|
11
11
|
*/
|
|
12
12
|
export class LoanTokenPoller {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} options
|
|
15
|
+
* @param {string} options.identifier
|
|
16
|
+
* @param {string} options.borrowType
|
|
17
|
+
* @param {Function} options.successCallback - called after the initial
|
|
18
|
+
* loan token is created.
|
|
19
|
+
* @param {Function} options.errorCallback - called on any create_token
|
|
20
|
+
* failure (initial or a routine interval tick).
|
|
21
|
+
* @param {number} options.pollerDelay - interval between routine
|
|
22
|
+
* create_token checks, in seconds.
|
|
23
|
+
* @param {boolean} [options.skipInitialCall] - skip the immediate
|
|
24
|
+
* create_token call, only start the recurring interval. Used after a
|
|
25
|
+
* loan-expiry recovery renewal, where BookLoanService::
|
|
26
|
+
* attempt_to_renew_loan() already minted a valid access token as part
|
|
27
|
+
* of the renew_loan response itself — an immediate confirming
|
|
28
|
+
* create_token call is redundant.
|
|
29
|
+
*/
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
const {
|
|
32
|
+
identifier,
|
|
33
|
+
borrowType,
|
|
34
|
+
successCallback,
|
|
35
|
+
errorCallback,
|
|
36
|
+
pollerDelay,
|
|
37
|
+
skipInitialCall = false,
|
|
38
|
+
} = options;
|
|
39
|
+
|
|
40
|
+
this.identifier = identifier;
|
|
15
41
|
this.borrowType = borrowType;
|
|
16
42
|
this.successCallback = successCallback; // callback function to be called after loan token is created
|
|
17
43
|
this.errorCallback = errorCallback; // callback function to be called after loan token is created
|
|
18
44
|
this.pollerDelay = pollerDelay; // value in seconds
|
|
45
|
+
this.skipInitialCall = skipInitialCall === true;
|
|
19
46
|
|
|
20
47
|
this.loanTokenInterval = undefined;
|
|
21
48
|
|
|
@@ -34,8 +61,14 @@ export class LoanTokenPoller {
|
|
|
34
61
|
|
|
35
62
|
async bookAccessed() {
|
|
36
63
|
if (this.borrowType) {
|
|
37
|
-
|
|
38
|
-
|
|
64
|
+
if (this.skipInitialCall) {
|
|
65
|
+
log('[LoanTokenPoller] skipping initial create_token — already minted by the renewal response', {
|
|
66
|
+
identifier: this.identifier,
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
// Do an initial token, then set an interval
|
|
70
|
+
this.handleLoanTokenPoller(true);
|
|
71
|
+
}
|
|
39
72
|
|
|
40
73
|
// if this.borrowType = adminBorrowed,
|
|
41
74
|
// - we don't want to fetch token on interval
|
package/src/ia-book-actions.js
CHANGED
|
@@ -104,6 +104,15 @@ export default class IABookActions extends LitElement {
|
|
|
104
104
|
*/
|
|
105
105
|
this.recoveringFromLoanExpiry = false;
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* True right after a confirmed renewal (any kind), until the token
|
|
109
|
+
* poller restarts. renew_loan's own response already carries a valid
|
|
110
|
+
* access token, so that restart should skip its normal immediate
|
|
111
|
+
* create_token call — see handleLoanAutoRenewed/
|
|
112
|
+
* setupLendingToolbarActions.
|
|
113
|
+
*/
|
|
114
|
+
this.skipNextInitialTokenCall = false;
|
|
115
|
+
|
|
107
116
|
this.warningModalOpen = false;
|
|
108
117
|
|
|
109
118
|
/** Once dismissed, don't re-show the warning modal every tick — only
|
|
@@ -318,8 +327,14 @@ export default class IABookActions extends LitElement {
|
|
|
318
327
|
!this.loanRenewInProgress &&
|
|
319
328
|
!window.IALendingIntervals.tokenPoller
|
|
320
329
|
) {
|
|
330
|
+
// Any confirmed renewal's response already minted a valid access
|
|
331
|
+
// token (see handleLoanAutoRenewed) — the poller only needs to
|
|
332
|
+
// start its recurring check, not fire an immediate confirming
|
|
333
|
+
// create_token call too.
|
|
334
|
+
const skipInitialCall = this.skipNextInitialTokenCall;
|
|
335
|
+
this.skipNextInitialTokenCall = false;
|
|
321
336
|
this.recoveringFromLoanExpiry = false;
|
|
322
|
-
this.startLoanTokenPoller();
|
|
337
|
+
this.startLoanTokenPoller(skipInitialCall);
|
|
323
338
|
}
|
|
324
339
|
}, 100);
|
|
325
340
|
}
|
|
@@ -740,10 +755,22 @@ export default class IABookActions extends LitElement {
|
|
|
740
755
|
ajaxResponse: detail?.data,
|
|
741
756
|
});
|
|
742
757
|
|
|
758
|
+
// BookLoanService::attempt_to_renew_loan() mints a valid access
|
|
759
|
+
// token as part of EVERY renew_loan response, not just a recovery
|
|
760
|
+
// from expiry — so the poller restart below never needs to fire a
|
|
761
|
+
// redundant confirming create_token call, regardless of which path
|
|
762
|
+
// triggered this renewal.
|
|
763
|
+
this.skipNextInitialTokenCall = true;
|
|
764
|
+
|
|
743
765
|
if (this.recoveringFromLoanExpiry) {
|
|
744
766
|
// Only now is it safe to re-initialize BookReader — not for a
|
|
745
|
-
// routine top-up, where that would be disruptive.
|
|
746
|
-
|
|
767
|
+
// routine top-up, where that would be disruptive. Call it
|
|
768
|
+
// directly rather than waiting on a confirming create_token
|
|
769
|
+
// success: BookLoanService::attempt_to_renew_loan() already
|
|
770
|
+
// minted a valid access token as part of this renew_loan
|
|
771
|
+
// response, so there's nothing left to confirm.
|
|
772
|
+
this.lendingBarPostInit();
|
|
773
|
+
this.postInitComplete = true;
|
|
747
774
|
}
|
|
748
775
|
|
|
749
776
|
const currStatus = {
|
|
@@ -895,9 +922,10 @@ export default class IABookActions extends LitElement {
|
|
|
895
922
|
|
|
896
923
|
/**
|
|
897
924
|
* enable access of borrowed/browsed books
|
|
925
|
+
* @param {boolean} [skipInitialCall] - see LoanTokenPoller's constructor doc
|
|
898
926
|
* @see LoanTokenPoller
|
|
899
927
|
*/
|
|
900
|
-
startLoanTokenPoller() {
|
|
928
|
+
startLoanTokenPoller(skipInitialCall = false) {
|
|
901
929
|
const successCallback = () => {
|
|
902
930
|
if (!this.postInitComplete) {
|
|
903
931
|
this.lendingBarPostInit();
|
|
@@ -911,13 +939,14 @@ export default class IABookActions extends LitElement {
|
|
|
911
939
|
// Tear down any previous poller first, so only one is ever controlling
|
|
912
940
|
// window.IALendingIntervals.tokenPoller at a time.
|
|
913
941
|
this.tokenPoller?.disconnectedCallback();
|
|
914
|
-
this.tokenPoller = new LoanTokenPoller(
|
|
915
|
-
this.identifier,
|
|
916
|
-
this.borrowType,
|
|
942
|
+
this.tokenPoller = new LoanTokenPoller({
|
|
943
|
+
identifier: this.identifier,
|
|
944
|
+
borrowType: this.borrowType,
|
|
917
945
|
successCallback,
|
|
918
946
|
errorCallback,
|
|
919
|
-
this.tokenDelay // in seconds
|
|
920
|
-
|
|
947
|
+
pollerDelay: this.tokenDelay, // in seconds
|
|
948
|
+
skipInitialCall,
|
|
949
|
+
});
|
|
921
950
|
}
|
|
922
951
|
|
|
923
952
|
/*
|
|
@@ -17,18 +17,17 @@ afterEach(() => {
|
|
|
17
17
|
|
|
18
18
|
describe('Get Loan Token', () => {
|
|
19
19
|
it('get loan token for browsed books', async () => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
() => {
|
|
20
|
+
const tokenPoller = new LoanTokenPoller({
|
|
21
|
+
identifier: 'identifier1',
|
|
22
|
+
borrowType: 'browsed',
|
|
23
|
+
successCallback: () => {
|
|
25
24
|
console.log('success callback is executed!');
|
|
26
25
|
},
|
|
27
|
-
() => {
|
|
26
|
+
errorCallback: () => {
|
|
28
27
|
console.log('error callback is executed!');
|
|
29
28
|
},
|
|
30
|
-
2000 // 2 minutes'
|
|
31
|
-
);
|
|
29
|
+
pollerDelay: 2000, // 2 minutes'
|
|
30
|
+
});
|
|
32
31
|
const successCallbackSpy = Sinon.stub(tokenPoller, 'successCallback');
|
|
33
32
|
successCallbackSpy();
|
|
34
33
|
expect(successCallbackSpy.callCount).to.equal(1);
|
|
@@ -39,13 +38,13 @@ describe('Get Loan Token', () => {
|
|
|
39
38
|
});
|
|
40
39
|
|
|
41
40
|
it('get loan token for admin borrowed books', async () => {
|
|
42
|
-
const tokenPoller = new LoanTokenPoller(
|
|
43
|
-
'identifier1',
|
|
44
|
-
'adminBorrowed',
|
|
45
|
-
() => {},
|
|
46
|
-
() => {},
|
|
47
|
-
2000 // 2 minutes'
|
|
48
|
-
);
|
|
41
|
+
const tokenPoller = new LoanTokenPoller({
|
|
42
|
+
identifier: 'identifier1',
|
|
43
|
+
borrowType: 'adminBorrowed',
|
|
44
|
+
successCallback: () => {},
|
|
45
|
+
errorCallback: () => {},
|
|
46
|
+
pollerDelay: 2000, // 2 minutes'
|
|
47
|
+
});
|
|
49
48
|
|
|
50
49
|
const successCallbackSpy = Sinon.stub(tokenPoller, 'successCallback');
|
|
51
50
|
successCallbackSpy();
|
|
@@ -61,13 +60,13 @@ describe('handleTokenError (no retry)', () => {
|
|
|
61
60
|
// A create_token failure is treated as terminal on the first attempt —
|
|
62
61
|
// no retry, whether it's the "stale loan read" race or anything else.
|
|
63
62
|
const makeTokenPoller = () =>
|
|
64
|
-
new LoanTokenPoller(
|
|
65
|
-
'identifier1',
|
|
66
|
-
'browsed',
|
|
67
|
-
() => {},
|
|
68
|
-
() => {},
|
|
69
|
-
2000
|
|
70
|
-
);
|
|
63
|
+
new LoanTokenPoller({
|
|
64
|
+
identifier: 'identifier1',
|
|
65
|
+
borrowType: 'browsed',
|
|
66
|
+
successCallback: () => {},
|
|
67
|
+
errorCallback: () => {},
|
|
68
|
+
pollerDelay: 2000,
|
|
69
|
+
});
|
|
71
70
|
|
|
72
71
|
it('reports the error immediately, without retrying, on the initial call', () => {
|
|
73
72
|
const tokenPoller = makeTokenPoller();
|
|
@@ -100,7 +99,13 @@ describe('handleTokenError (no retry)', () => {
|
|
|
100
99
|
|
|
101
100
|
it('passes isInitial through to the error callback', () => {
|
|
102
101
|
const errorCallback = Sinon.spy();
|
|
103
|
-
const poller = new LoanTokenPoller(
|
|
102
|
+
const poller = new LoanTokenPoller({
|
|
103
|
+
identifier: 'foo',
|
|
104
|
+
borrowType: 'browsed',
|
|
105
|
+
successCallback: () => {},
|
|
106
|
+
errorCallback,
|
|
107
|
+
pollerDelay: 120,
|
|
108
|
+
});
|
|
104
109
|
poller.disconnectedCallback();
|
|
105
110
|
|
|
106
111
|
poller.handleTokenError({ error: 'something else went wrong' }, true);
|
|
@@ -109,3 +114,41 @@ describe('handleTokenError (no retry)', () => {
|
|
|
109
114
|
expect(errorCallback.firstCall.args[0].detail.isInitial).to.be.true;
|
|
110
115
|
});
|
|
111
116
|
});
|
|
117
|
+
|
|
118
|
+
describe('skipInitialCall option', () => {
|
|
119
|
+
it('starts the recurring interval but fires no immediate create_token call', () => {
|
|
120
|
+
const poller = new LoanTokenPoller({
|
|
121
|
+
identifier: 'foo',
|
|
122
|
+
borrowType: 'browsed',
|
|
123
|
+
successCallback: () => {},
|
|
124
|
+
errorCallback: () => {},
|
|
125
|
+
pollerDelay: 120,
|
|
126
|
+
skipInitialCall: true,
|
|
127
|
+
});
|
|
128
|
+
const handleLoanTokenPollerSpy = Sinon.spy(poller, 'handleLoanTokenPoller');
|
|
129
|
+
|
|
130
|
+
// bookAccessed() already ran in the constructor above (before the spy
|
|
131
|
+
// was attached) -- re-run it to observe whether it calls out.
|
|
132
|
+
poller.bookAccessed();
|
|
133
|
+
|
|
134
|
+
expect(handleLoanTokenPollerSpy.called).to.be.false;
|
|
135
|
+
expect(window.IALendingIntervals.tokenPoller).to.not.equal(0);
|
|
136
|
+
poller.disconnectedCallback();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('fires the immediate call as usual when skipInitialCall is not set', () => {
|
|
140
|
+
const poller = new LoanTokenPoller({
|
|
141
|
+
identifier: 'foo',
|
|
142
|
+
borrowType: 'browsed',
|
|
143
|
+
successCallback: () => {},
|
|
144
|
+
errorCallback: () => {},
|
|
145
|
+
pollerDelay: 120,
|
|
146
|
+
});
|
|
147
|
+
const handleLoanTokenPollerSpy = Sinon.spy(poller, 'handleLoanTokenPoller');
|
|
148
|
+
|
|
149
|
+
poller.bookAccessed();
|
|
150
|
+
|
|
151
|
+
expect(handleLoanTokenPollerSpy.calledWith(true)).to.be.true;
|
|
152
|
+
poller.disconnectedCallback();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -479,8 +479,7 @@ describe('Browsing expired status', () => {
|
|
|
479
479
|
});
|
|
480
480
|
});
|
|
481
481
|
|
|
482
|
-
describe('Auto renew one hour loan',
|
|
483
|
-
await aTimeout(5000);
|
|
482
|
+
describe('Auto renew one hour loan', () => {
|
|
484
483
|
it('Book is browsing and renewed it now', async () => {
|
|
485
484
|
const loanTime = 88;
|
|
486
485
|
const el = await fixture(
|
|
@@ -553,8 +552,7 @@ describe('Auto renew one hour loan', async () => {
|
|
|
553
552
|
});
|
|
554
553
|
});
|
|
555
554
|
|
|
556
|
-
describe('Visibility change API for document',
|
|
557
|
-
await aTimeout(5000);
|
|
555
|
+
describe('Visibility change API for document', () => {
|
|
558
556
|
|
|
559
557
|
beforeEach(() => {
|
|
560
558
|
Object.defineProperty(document, 'hidden', {
|
|
@@ -578,6 +576,10 @@ describe('Visibility change API for document', async () => {
|
|
|
578
576
|
lendingStatus: {
|
|
579
577
|
user_has_browsed: true,
|
|
580
578
|
browsingExpired: false,
|
|
579
|
+
// Without this, startBrowseTimer() computes setTimeout(fn,
|
|
580
|
+
// undefined * 1000) -> NaN, which fires almost immediately and
|
|
581
|
+
// expires the loan before this test ever dispatches its event.
|
|
582
|
+
secondsLeftOnLoan: 3600,
|
|
581
583
|
},
|
|
582
584
|
})
|
|
583
585
|
);
|
|
@@ -605,6 +607,7 @@ describe('Visibility change API for document', async () => {
|
|
|
605
607
|
lendingStatus: {
|
|
606
608
|
user_has_browsed: true,
|
|
607
609
|
browsingExpired: false,
|
|
610
|
+
secondsLeftOnLoan: 3600,
|
|
608
611
|
},
|
|
609
612
|
})
|
|
610
613
|
);
|
|
@@ -940,6 +943,100 @@ describe('autoRenewExpiredLoan', () => {
|
|
|
940
943
|
expect(el.lendingBarPostInit.called).to.be.false;
|
|
941
944
|
});
|
|
942
945
|
|
|
946
|
+
it('also skips the initial create_token after a routine (non-expiry) renewal', async () => {
|
|
947
|
+
// BookLoanService::attempt_to_renew_loan() mints the access token as
|
|
948
|
+
// part of EVERY renew_loan response, not just a recovery from expiry.
|
|
949
|
+
// A routine top-up also has its poller stopped mid-renewal (see
|
|
950
|
+
// updated()'s clearAll() on loanRenewResult.renewNow), so it would
|
|
951
|
+
// otherwise fire this same redundant call once the poller restarts.
|
|
952
|
+
const el = await fixture(
|
|
953
|
+
container({
|
|
954
|
+
userid: '@user1',
|
|
955
|
+
identifier: 'foobar',
|
|
956
|
+
lendingStatus: { user_has_browsed: true, browsingExpired: false },
|
|
957
|
+
})
|
|
958
|
+
);
|
|
959
|
+
await el.updateComplete;
|
|
960
|
+
el.postInitComplete = true;
|
|
961
|
+
|
|
962
|
+
await el.localCache.set({
|
|
963
|
+
key: 'foobar-loanTime',
|
|
964
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
965
|
+
ttl: 3600,
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
const startTokenPollerSpy = Sinon.spy(el, 'startLoanTokenPoller');
|
|
969
|
+
|
|
970
|
+
// Triggers updated()'s clearAll(), stopping the running poller, same
|
|
971
|
+
// as a real routine top-up would.
|
|
972
|
+
el.loanRenewResult = { texts: '', renewNow: true, renewType: 'auto' };
|
|
973
|
+
await el.updateComplete;
|
|
974
|
+
|
|
975
|
+
const collapsibleActionGroupEl = el.shadowRoot.querySelector(
|
|
976
|
+
'collapsible-action-group'
|
|
977
|
+
);
|
|
978
|
+
collapsibleActionGroupEl.dispatchEvent(
|
|
979
|
+
new CustomEvent('loanAutoRenewed', {
|
|
980
|
+
detail: { data: { loan: { identifier: 'foobar', renewal: true } } },
|
|
981
|
+
})
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
await aTimeout(200);
|
|
985
|
+
await el.updateComplete;
|
|
986
|
+
|
|
987
|
+
expect(startTokenPollerSpy.calledOnceWith(true)).to.be.true;
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it('calls lendingBarPostInit directly on a recovery renewal and skips the initial create_token', async () => {
|
|
991
|
+
// BookLoanService::attempt_to_renew_loan() now mints the access token
|
|
992
|
+
// as part of the renew_loan response itself (using the just-written
|
|
993
|
+
// loan record already in memory, not a separate read) -- so recovering
|
|
994
|
+
// from a genuine expiry no longer needs to wait on a confirming
|
|
995
|
+
// create_token success before re-initializing BookReader, and the
|
|
996
|
+
// poller it starts afterward shouldn't fire a redundant immediate call.
|
|
997
|
+
const el = await fixture(
|
|
998
|
+
container({
|
|
999
|
+
userid: '@user1',
|
|
1000
|
+
identifier: 'foobar',
|
|
1001
|
+
lendingStatus: {
|
|
1002
|
+
user_has_browsed: true,
|
|
1003
|
+
browsingExpired: true,
|
|
1004
|
+
},
|
|
1005
|
+
})
|
|
1006
|
+
);
|
|
1007
|
+
await el.updateComplete;
|
|
1008
|
+
el.lendingBarPostInit = Sinon.spy();
|
|
1009
|
+
|
|
1010
|
+
await el.localCache.set({
|
|
1011
|
+
key: 'foobar-loanTime',
|
|
1012
|
+
value: new Date(new Date().getTime() + 3600 * 1000),
|
|
1013
|
+
ttl: 3600,
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
const startTokenPollerSpy = Sinon.spy(el, 'startLoanTokenPoller');
|
|
1017
|
+
|
|
1018
|
+
el.autoRenewExpiredLoan();
|
|
1019
|
+
await el.updateComplete;
|
|
1020
|
+
await aTimeout(50); // let autoRenewExpiredLoan's setTimeout(0) set renewNow
|
|
1021
|
+
|
|
1022
|
+
await el.handleLoanAutoRenewed({
|
|
1023
|
+
detail: {
|
|
1024
|
+
action: 'renew_loan',
|
|
1025
|
+
data: { success: true, loan: { identifier: 'foobar', renewal: true } },
|
|
1026
|
+
},
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// Called synchronously from handleLoanAutoRenewed, not via a
|
|
1030
|
+
// create_token success callback.
|
|
1031
|
+
expect(el.lendingBarPostInit.calledOnce).to.be.true;
|
|
1032
|
+
expect(el.postInitComplete).to.be.true;
|
|
1033
|
+
|
|
1034
|
+
await el.updateComplete;
|
|
1035
|
+
await aTimeout(150); // let the 100ms token-poller-restart check run
|
|
1036
|
+
|
|
1037
|
+
expect(startTokenPollerSpy.calledOnceWith(true)).to.be.true;
|
|
1038
|
+
});
|
|
1039
|
+
|
|
943
1040
|
it('clears loanRenewInProgress and shows unavailable modal on renew_loan failure', async () => {
|
|
944
1041
|
const el = await fixture(
|
|
945
1042
|
container({
|