@internetarchive/ia-book-actions 1.4.4-alpha12 → 1.4.4-alpha14
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/assets/data/info.js +10 -5
- package/src/core/services/actions-handler/actions-handler-service.js +17 -0
- package/src/core/services/actions-handler/actions-handler.js +64 -35
- package/src/core/services/loan-analytics.js +0 -11
- package/src/core/services/loan-renew-helper.js +2 -2
- package/src/core/services/loan-token-poller.js +34 -16
- package/src/ia-book-actions.js +266 -247
- package/test/core/services/loan-token-poller.test.js +72 -6
- package/test/ia-book-actions.test.js +420 -26
package/package.json
CHANGED
package/src/assets/data/info.js
CHANGED
|
@@ -4,9 +4,14 @@ export const infoIcon = html`<svg
|
|
|
4
4
|
height="20"
|
|
5
5
|
width="20"
|
|
6
6
|
viewBox="0 0 100 100"
|
|
7
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
>
|
|
9
|
+
<g fill="none" fill-rule="evenodd">
|
|
10
|
+
<path d="m0 0h100v100h-100z" fill="#fff" />
|
|
11
|
+
<path
|
|
12
|
+
d="m49.8315487 0h.1702245c6.7356878 0 13.1853038 1.31117332 19.3488483 3.93351997 6.1635444 2.62234664 11.4854233 6.15778963 15.9656369 10.60632903 4.4802135 4.4485394 8.0478347 9.7522946 10.7028636 15.9112655 2.655029 6.1589709 3.980878 12.6038012 3.9789971 19.3344909.0567419 6.6716279-1.1702933 13.0585776-3.6811042 19.1608491-2.510811 6.1022715-6.106803 11.5206067-10.7879759 16.2550055-9.7027949 9.7522946-21.4884754 14.6851412-35.3570414 14.79854h-.1702244c-6.7333236 0-13.1829397-1.3111733-19.3488483-3.93352-6.1659087-2.6223466-11.4877876-6.1577896-15.9656369-10.606329s-8.04547055-9.7522946-10.7028637-15.9112655c-2.65739314-6.1589709-3.9844243-12.6038012-3.98254337-19.3344909-.05674149-6.6716279 1.17029325-13.0585776 3.68110421-19.1608491 2.51081095-6.1022715 6.10680292-11.5206067 10.78797586-16.2550055 9.7027949-9.75229456 21.4884754-14.68514123 35.3570414-14.79854zm12.6566146 26.4757998c1.6745578-1.6828001 2.5118367-3.6747334 2.5118367-5.9757998 0-2.4126333-.8095238-4.4324583-2.4285714-6.059475s-3.6289796-2.440525-6.0297959-2.440525c-2.4008164 0-4.4107483.8135083-6.029796 2.440525-1.6745578 1.6270167-2.5118367 3.6468417-2.5118367 6.059475 0 2.1871753.8372789 4.1791086 2.5118367 5.9757998 1.6745579 1.6828001 3.6844898 2.5242002 6.029796 2.5242002 2.3453061 0 4.3274829-.8414001 5.9465306-2.5242002zm-12.1370589 52.7776981-1.2815282-.9486968c0-.4588935.398855-1.8938272 1.196565-4.3048011l12.7338588-39-23.0745876 3.6164609.2548896 3.873251c0-.1141289.4554971-.1997256 1.3664914-.2567901.9109942 0 1.623741.1723823 2.1382404.5171468.5121392.2306356.7965299.6039323.8531721 1.1198902 0 .8607225-.6549247 3.2134431-1.9647739 7.0581619l-8.1175252 24.1061729c-1.0242785 3.2716963-1.5080967 5.5388203-1.4514546 6.8013717.0566421 1.6643804.8826732 2.9839963 2.4780932 3.9588477 1.2532071.803658 2.8769482 1.205487 4.8712231 1.205487h.5982825c1.7653464-.0570645 3.8445846-.3875629 6.2377146-.9914952 2.3931299-.6039323 4.3590839-1.3933242 5.8978617-2.3681756 1.3098493-.803658 2.3919499-1.5359853 3.2463021-2.1969821.8543521-.6609968 1.3959925-1.1341564 1.6249211-1.4194788l.3433929-.3459533-2.8214861-3.3596708-2.9914125 2.0650205c-.79771.4588935-1.5104568.7454047-2.1382404.8595337z"
|
|
13
|
+
fill="#000"
|
|
14
|
+
fill-rule="nonzero"
|
|
15
|
+
/>
|
|
10
16
|
</g>
|
|
11
|
-
</svg>
|
|
12
|
-
`;
|
|
17
|
+
</svg> `;
|
|
@@ -95,5 +95,22 @@ export default async function ActionsHandlerService(options) {
|
|
|
95
95
|
window?.Sentry?.captureException(
|
|
96
96
|
`${sentryLogs.actionsHandlerService} - Error: ${error}`
|
|
97
97
|
);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Report it, don't just swallow it. A rejected fetch (offline, dropped
|
|
101
|
+
* connection) or a non-JSON body (a 405 returning HTML, which QA has
|
|
102
|
+
* been seeing) lands here, and calling neither `success` nor `error`
|
|
103
|
+
* leaves every caller waiting on a callback that never comes.
|
|
104
|
+
*
|
|
105
|
+
* For renew_loan that's not merely a missing modal: IABookActions
|
|
106
|
+
* clears its loanRenewInProgress guard from these callbacks, so a
|
|
107
|
+
* silent failure latched it on for good and every later renewal
|
|
108
|
+
* attempt became a no-op. Backgrounding a tab on a flaky mobile
|
|
109
|
+
* connection is exactly when this fires.
|
|
110
|
+
*/
|
|
111
|
+
log(`[IABookActions] ✗ ${option.action} threw`, error);
|
|
112
|
+
option?.error({
|
|
113
|
+
error: `Could not reach the lending service. Please check your connection and try again. (${error})`,
|
|
114
|
+
});
|
|
98
115
|
}
|
|
99
116
|
}
|
|
@@ -6,7 +6,11 @@ import log from '../log.js';
|
|
|
6
6
|
|
|
7
7
|
import ActionsHandlerService from './actions-handler-service.js';
|
|
8
8
|
import LoanAnanlytics from '../loan-analytics.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
analyticsCategories,
|
|
11
|
+
analyticsActions,
|
|
12
|
+
analyticsLabels,
|
|
13
|
+
} from '../../config/analytics-event-and-category.js';
|
|
10
14
|
import * as Cookies from '../doc-cookies.js';
|
|
11
15
|
|
|
12
16
|
/**
|
|
@@ -172,32 +176,53 @@ export default class ActionsHandler extends LitElement {
|
|
|
172
176
|
action,
|
|
173
177
|
identifier: this.identifier,
|
|
174
178
|
success: async data => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
179
|
+
// Anything thrown in here is an unhandled rejection that dispatches
|
|
180
|
+
// NOTHING — and IABookActions only clears loanRenewInProgress when
|
|
181
|
+
// one of these two events arrives, so a throw here latches it on
|
|
182
|
+
// permanently (dead countdown, loan stuck "active"). Always exit
|
|
183
|
+
// through exactly one of the two dispatches below.
|
|
184
|
+
try {
|
|
185
|
+
log('RENEW_LOAN --- ', data, this.identifier);
|
|
186
|
+
const activeLoan = data.loan ? data.loan : undefined;
|
|
187
|
+
// Optional chaining: `data.loan` is genuinely absent on some
|
|
188
|
+
// failures, and reading `.renewal` off undefined threw here.
|
|
189
|
+
const isRenewal = activeLoan?.renewal;
|
|
190
|
+
|
|
191
|
+
if (activeLoan && isRenewal) {
|
|
192
|
+
// Await — loanAutoRenewed listeners read this same cache key
|
|
193
|
+
// immediately; dispatching before the write lands was a race.
|
|
194
|
+
await this.setBrowseTimeSession();
|
|
195
|
+
|
|
196
|
+
// Only record the renew analytics event once the renewal has
|
|
197
|
+
// actually succeeded — not merely attempted.
|
|
198
|
+
await this.loanAnanlytics?.storeLoanStatsCount(
|
|
199
|
+
this.identifier,
|
|
200
|
+
'autorenew'
|
|
201
|
+
);
|
|
202
|
+
const analyticsLabel =
|
|
203
|
+
renewType === 'auto'
|
|
204
|
+
? analyticsLabels.browseAutoRenew
|
|
205
|
+
: analyticsLabels.browseManualRenew;
|
|
206
|
+
this.loanAnanlytics?.sendEvent(
|
|
207
|
+
analyticsCategories.browse,
|
|
208
|
+
analyticsActions.browseRenew,
|
|
209
|
+
analyticsLabel,
|
|
210
|
+
this.identifier
|
|
211
|
+
);
|
|
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.
|
|
218
|
+
this.dispatchEvent(
|
|
219
|
+
new CustomEvent('loanAutoRenewed', {
|
|
220
|
+
detail: { action, data: { ...data, loan: activeLoan } },
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
201
226
|
log('RENEW_LOAN ERROR --- ', {
|
|
202
227
|
action,
|
|
203
228
|
isRenewal,
|
|
@@ -213,14 +238,17 @@ export default class ActionsHandler extends LitElement {
|
|
|
213
238
|
error: true,
|
|
214
239
|
message: 'Loan renewal failed: no loan active.',
|
|
215
240
|
});
|
|
241
|
+
} catch (error) {
|
|
242
|
+
log('RENEW_LOAN THREW --- ', error);
|
|
243
|
+
window?.Sentry?.captureException(
|
|
244
|
+
`${sentryLogs.bookRenewFailed} - Exception: ${error}`
|
|
245
|
+
);
|
|
246
|
+
this.dispatchActionError(action, {
|
|
247
|
+
data,
|
|
248
|
+
error: true,
|
|
249
|
+
message: `Loan renewal failed: ${error}`,
|
|
250
|
+
});
|
|
216
251
|
}
|
|
217
|
-
|
|
218
|
-
// dispatch outcome of loan renewal
|
|
219
|
-
this.dispatchEvent(
|
|
220
|
-
new CustomEvent('loanAutoRenewed', {
|
|
221
|
-
detail: { action, data: { ...data, loan: activeLoan } },
|
|
222
|
-
})
|
|
223
|
-
);
|
|
224
252
|
},
|
|
225
253
|
error: data => {
|
|
226
254
|
this.dispatchActionError(action, data);
|
|
@@ -428,7 +456,8 @@ export default class ActionsHandler extends LitElement {
|
|
|
428
456
|
* @returns {void}
|
|
429
457
|
*/
|
|
430
458
|
setStickyAdminAccess(value) {
|
|
431
|
-
const domain =
|
|
459
|
+
const domain =
|
|
460
|
+
window.location.hostname === 'localhost' ? 'localhost' : '.archive.org';
|
|
432
461
|
const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days from now
|
|
433
462
|
Cookies.setItem('sticky-admin-access', value, expires, '/', domain);
|
|
434
463
|
}
|
|
@@ -159,17 +159,6 @@ export default class LoanAnanlytics {
|
|
|
159
159
|
* @memberof LoanAnanlytics
|
|
160
160
|
*/
|
|
161
161
|
sendEvent(eventCategory, eventAction, label, extraParams) {
|
|
162
|
-
// log(
|
|
163
|
-
// 'eventCategory:-',
|
|
164
|
-
// eventCategory,
|
|
165
|
-
// '||\teventAction:-',
|
|
166
|
-
// eventAction,
|
|
167
|
-
// '||\tlabel:-',
|
|
168
|
-
// label,
|
|
169
|
-
// '||\textraParams:-',
|
|
170
|
-
// extraParams,
|
|
171
|
-
// );
|
|
172
|
-
|
|
173
162
|
window?.archive_analytics?.send_event_no_sampling(
|
|
174
163
|
eventCategory,
|
|
175
164
|
eventAction,
|
|
@@ -26,7 +26,7 @@ export class LoanRenewHelper {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
handleLoanRenew() {
|
|
30
30
|
try {
|
|
31
31
|
if (this.hasPageChanged) {
|
|
32
32
|
return this.pageChanged(); // user clicked on page
|
|
@@ -99,7 +99,7 @@ export class LoanRenewHelper {
|
|
|
99
99
|
} else if (pageChangedTime >= pageChangeTimeFrame) {
|
|
100
100
|
this.result = {
|
|
101
101
|
texts: '',
|
|
102
|
-
renewNow: true,
|
|
102
|
+
renewNow: true, // viewed in last time frame
|
|
103
103
|
renewType: 'auto',
|
|
104
104
|
};
|
|
105
105
|
}
|
|
@@ -28,8 +28,16 @@ export class LoanTokenPoller {
|
|
|
28
28
|
* the loan is genuinely valid. Retry a few times with backoff instead
|
|
29
29
|
* of giving up on the first attempt.
|
|
30
30
|
*/
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
31
|
+
this.maxTokenRetries = 3;
|
|
32
|
+
this.tokenRetryDelay = 1000; // ms, multiplied by attempt number
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handle for a pending retry, so teardown can cancel it. It's a bare
|
|
36
|
+
* setTimeout rather than an IALendingIntervals entry, so without this
|
|
37
|
+
* a retry scheduled just before the loan is returned would still fire
|
|
38
|
+
* its create_token afterwards.
|
|
39
|
+
*/
|
|
40
|
+
this.retryTimeout = undefined;
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
43
|
* loan analytics instance
|
|
@@ -42,6 +50,8 @@ export class LoanTokenPoller {
|
|
|
42
50
|
|
|
43
51
|
disconnectedCallback() {
|
|
44
52
|
window?.IALendingIntervals?.clearTokenPoller();
|
|
53
|
+
clearTimeout(this.retryTimeout);
|
|
54
|
+
this.retryTimeout = undefined;
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
async bookAccessed() {
|
|
@@ -123,19 +133,24 @@ export class LoanTokenPoller {
|
|
|
123
133
|
error: data?.error,
|
|
124
134
|
});
|
|
125
135
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
) {
|
|
131
|
-
const delay = this.
|
|
132
|
-
log(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
// The renewal write can be mid-propagation for an interval refresh too,
|
|
137
|
+
// not just the initial call — a routine tick landing moments after a
|
|
138
|
+
// renewal hits the same race. Retry both; isInitial only decides how a
|
|
139
|
+
// final failure is reported, not whether it's worth retrying.
|
|
140
|
+
if (isStaleLoanReadError && retryCount < this.maxTokenRetries) {
|
|
141
|
+
const delay = this.tokenRetryDelay * (retryCount + 1);
|
|
142
|
+
log(
|
|
143
|
+
'[LoanTokenPoller] retrying create_token after stale-loan-read error',
|
|
144
|
+
{
|
|
145
|
+
identifier: this.identifier,
|
|
146
|
+
nextRetryCount: retryCount + 1,
|
|
147
|
+
delay,
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
clearTimeout(this.retryTimeout);
|
|
151
|
+
this.retryTimeout = setTimeout(() => {
|
|
152
|
+
this.retryTimeout = undefined;
|
|
153
|
+
this.handleLoanTokenPoller(isInitial, retryCount + 1);
|
|
139
154
|
}, delay);
|
|
140
155
|
return;
|
|
141
156
|
}
|
|
@@ -146,7 +161,10 @@ export class LoanTokenPoller {
|
|
|
146
161
|
retryCount,
|
|
147
162
|
});
|
|
148
163
|
|
|
149
|
-
|
|
164
|
+
// isInitial rides along so the consumer can distinguish "the book won't
|
|
165
|
+
// open at all" from "a mid-read refresh blipped" — see
|
|
166
|
+
// IABookActions.handleLendingActionError.
|
|
167
|
+
this.errorCallback({ detail: { action, data, isInitial } });
|
|
150
168
|
|
|
151
169
|
// send error to Sentry
|
|
152
170
|
window?.Sentry?.captureMessage(
|