@papyrus-sdk/ui-react-native 0.2.24 → 0.2.27

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.
@@ -139,8 +139,18 @@
139
139
  const COMIC_PREVIEW_CACHE_LIMIT = 8;
140
140
  const comicHelpers = window.PapyrusComicRuntime || {};
141
141
  const runtimeConfig = window.__PAPYRUS_RUNTIME_CONFIG__ || {};
142
+ const epubDiagnosticsEnabled =
143
+ runtimeConfig.epubDiagnostics === true ||
144
+ runtimeConfig.epubDiagnostics === 'true';
145
+ const EPUB_STAGE_TIMEOUT_MS = 10000;
142
146
  let comicDispose = null;
143
147
  let comicGeneration = 0;
148
+ let epubInteractionCleanup = null;
149
+ const epubSelectionCleanups = new Set();
150
+ let epubScrollDiagnostics = null;
151
+ // Shared generation for all document formats. The historical variable name
152
+ // is kept because the shipped runtime tests and diagnostics refer to it.
153
+ let epubLoadGeneration = 0;
144
154
 
145
155
  const sendMessage = (payload) => {
146
156
  if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
@@ -246,6 +256,223 @@
246
256
  sendMessage({ type: 'event', name, payload });
247
257
  };
248
258
 
259
+ const sendEpubDiagnostic = (name, payload) => {
260
+ if (!epubDiagnosticsEnabled) return;
261
+ sendEvent(name, {
262
+ loadId: payload.loadId,
263
+ timestamp: Date.now(),
264
+ sourceKind: payload.sourceKind,
265
+ byteLength: payload.byteLength,
266
+ currentType: currentType,
267
+ durationMs: payload.durationMs,
268
+ uri: payload.uri,
269
+ status: payload.status,
270
+ contentLength: payload.contentLength,
271
+ contentType: payload.contentType,
272
+ transferEncoding: payload.transferEncoding,
273
+ error: payload.error,
274
+ });
275
+ };
276
+
277
+ const withEpubStageTimeout = (promise, stage, payload) =>
278
+ new Promise((resolve, reject) => {
279
+ let settled = false;
280
+ const startedAt = performance.now();
281
+ const timeout = setTimeout(() => {
282
+ if (settled) return;
283
+ settled = true;
284
+ const error = new Error(`EPUB ${stage} excedeu ${EPUB_STAGE_TIMEOUT_MS}ms`);
285
+ sendEpubDiagnostic('epub.load.timeout', {
286
+ ...payload,
287
+ error: { message: error.message, stage },
288
+ durationMs: performance.now() - startedAt,
289
+ });
290
+ reject(error);
291
+ }, EPUB_STAGE_TIMEOUT_MS);
292
+
293
+ Promise.resolve(promise).then(
294
+ (value) => {
295
+ if (settled) return;
296
+ settled = true;
297
+ clearTimeout(timeout);
298
+ resolve(value);
299
+ },
300
+ (error) => {
301
+ if (settled) return;
302
+ settled = true;
303
+ clearTimeout(timeout);
304
+ reject(error);
305
+ },
306
+ );
307
+ });
308
+
309
+ const isEpubDiagnosticsEnabled = () => {
310
+ const configured = runtimeConfig.epubScrollDiagnostics;
311
+ return configured === true || configured === '1' || configured === 'true' ||
312
+ Boolean(window.__PAPYRUS_EPUB_SCROLL_DIAGNOSTICS__);
313
+ };
314
+
315
+ const createEpubScrollDiagnostics = (manager) => {
316
+ if (!isEpubDiagnosticsEnabled()) return null;
317
+
318
+ const state = {
319
+ lastScrollTop: null,
320
+ lastTouchY: null,
321
+ lastTouchX: null,
322
+ lastInputDeltaY: 0,
323
+ touchActive: false,
324
+ selectionActive: false,
325
+ gestureLock: false,
326
+ consecutiveStallSamples: 0,
327
+ firstStallTimestamp: null,
328
+ stallReported: false,
329
+ };
330
+ const container = manager && manager.container;
331
+
332
+ const getSnapshot = () => {
333
+ const scrollTop = Number(container && container.scrollTop) || 0;
334
+ const scrollHeight = Number(container && container.scrollHeight) || 0;
335
+ const clientHeight = Number(container && container.clientHeight) || 0;
336
+ let scrollEnabled = true;
337
+ try {
338
+ const overflowY = window.getComputedStyle(container).overflowY;
339
+ scrollEnabled = overflowY !== 'hidden' && overflowY !== 'clip';
340
+ } catch (_) {}
341
+ return { scrollTop, scrollHeight, clientHeight, scrollEnabled };
342
+ };
343
+
344
+ const emit = (name, payload) => {
345
+ sendEvent(name, {
346
+ timestamp: Date.now(),
347
+ documentType: currentType,
348
+ currentPage,
349
+ spineIndex: Math.max(0, currentPage - 1),
350
+ progress: pageCount > 0 ? currentPage / pageCount : null,
351
+ ...(payload || {}),
352
+ });
353
+ };
354
+
355
+ const resetStall = () => {
356
+ state.consecutiveStallSamples = 0;
357
+ state.firstStallTimestamp = null;
358
+ state.stallReported = false;
359
+ };
360
+
361
+ const recordScroll = (event) => {
362
+ const snapshot = getSnapshot();
363
+ const previousScrollTop = state.lastScrollTop === null
364
+ ? snapshot.scrollTop
365
+ : state.lastScrollTop;
366
+ const deltaY = state.lastInputDeltaY;
367
+ const inputDirection = deltaY > 4 ? 'up' : deltaY < -4 ? 'down' : 'none';
368
+ const offsetUnchanged = Math.abs(snapshot.scrollTop - previousScrollTop) <= 1;
369
+ const validStallSample = inputDirection === 'up' &&
370
+ snapshot.scrollTop > 2 &&
371
+ snapshot.scrollEnabled &&
372
+ !state.selectionActive &&
373
+ !state.gestureLock &&
374
+ offsetUnchanged;
375
+
376
+ if (validStallSample) {
377
+ state.consecutiveStallSamples += 1;
378
+ if (state.firstStallTimestamp === null) {
379
+ state.firstStallTimestamp = Date.now();
380
+ }
381
+ const durationMs = Date.now() - state.firstStallTimestamp;
382
+ if (!state.stallReported && state.consecutiveStallSamples >= 3 && durationMs >= 150) {
383
+ state.stallReported = true;
384
+ emit('epub.scroll.stall', {
385
+ ...snapshot,
386
+ previousScrollTop,
387
+ deltaY,
388
+ direction: 'up',
389
+ consecutiveSamples: state.consecutiveStallSamples,
390
+ durationMs,
391
+ selectionActive: state.selectionActive,
392
+ gestureLock: state.gestureLock,
393
+ });
394
+ }
395
+ } else if (!offsetUnchanged || inputDirection !== 'up') {
396
+ resetStall();
397
+ }
398
+
399
+ state.lastScrollTop = snapshot.scrollTop;
400
+ emit('epub.scroll', {
401
+ ...snapshot,
402
+ previousScrollTop,
403
+ deltaY,
404
+ direction: snapshot.scrollTop < previousScrollTop ? 'up' :
405
+ snapshot.scrollTop > previousScrollTop ? 'down' : 'none',
406
+ touchActive: state.touchActive,
407
+ selectionActive: state.selectionActive,
408
+ gestureLock: state.gestureLock,
409
+ });
410
+ };
411
+
412
+ return {
413
+ setSelectionActive(active) {
414
+ state.selectionActive = Boolean(active);
415
+ emit('epub.selection', { selectionActive: state.selectionActive });
416
+ },
417
+ setGestureLock(active) {
418
+ state.gestureLock = Boolean(active);
419
+ emit('epub.gesture-lock', { gestureLock: state.gestureLock });
420
+ },
421
+ touchStart(event) {
422
+ const touch = event && event.touches && event.touches[0];
423
+ state.touchActive = true;
424
+ state.lastTouchY = touch ? Number(touch.clientY) : null;
425
+ state.lastTouchX = touch ? Number(touch.clientX) : null;
426
+ state.lastInputDeltaY = 0;
427
+ resetStall();
428
+ emit('epub.touch.start', { x: state.lastTouchX, y: state.lastTouchY });
429
+ },
430
+ touchMove(event) {
431
+ const touch = event && event.touches && event.touches[0];
432
+ if (!touch) return;
433
+ const previousY = state.lastTouchY;
434
+ const previousX = state.lastTouchX;
435
+ state.lastTouchY = Number(touch.clientY);
436
+ state.lastTouchX = Number(touch.clientX);
437
+ state.lastInputDeltaY = previousY === null ? 0 : previousY - state.lastTouchY;
438
+ emit('epub.touch.move', {
439
+ x: state.lastTouchX,
440
+ y: state.lastTouchY,
441
+ deltaY: previousY === null ? 0 : previousY - state.lastTouchY,
442
+ deltaX: previousX === null ? 0 : previousX - state.lastTouchX,
443
+ });
444
+ recordScroll();
445
+ },
446
+ touchEnd(event) {
447
+ const touch = event && event.changedTouches && event.changedTouches[0];
448
+ state.touchActive = false;
449
+ emit('epub.touch.end', {
450
+ x: touch ? Number(touch.clientX) : state.lastTouchX,
451
+ y: touch ? Number(touch.clientY) : state.lastTouchY,
452
+ });
453
+ state.lastTouchY = null;
454
+ state.lastTouchX = null;
455
+ state.lastInputDeltaY = 0;
456
+ resetStall();
457
+ },
458
+ scroll: recordScroll,
459
+ relocated(location) {
460
+ emit('epub.relocated', {
461
+ href: location && location.start ? location.start.href || null : null,
462
+ cfi: location && location.start ? location.start.cfi || null : null,
463
+ });
464
+ },
465
+ settled(event) {
466
+ emit('epub.momentum.settled', {
467
+ scrollTop: Number(event && event.top) || 0,
468
+ });
469
+ },
470
+ command(name, payload) {
471
+ emit('epub.programmatic-scroll', { command: name, ...(payload || {}) });
472
+ },
473
+ };
474
+ };
475
+
249
476
  const reportError = (error, context) => {
250
477
  const message = error && error.message ? error.message : String(error);
251
478
  const stack = error && error.stack ? error.stack : null;
@@ -296,42 +523,98 @@
296
523
  };
297
524
 
298
525
  const clearViewer = () => {
526
+ if (epubInteractionCleanup) {
527
+ epubInteractionCleanup();
528
+ epubInteractionCleanup = null;
529
+ }
530
+ epubSelectionCleanups.forEach((cleanup) => cleanup());
531
+ epubSelectionCleanups.clear();
299
532
  clearComicState();
300
533
  while (viewer.firstChild) {
301
534
  viewer.removeChild(viewer.firstChild);
302
535
  }
303
536
  };
304
537
 
305
- const decodeBase64 = (value) => {
306
- const clean = value.replace(/\s/g, '');
307
- const binary = atob(clean);
308
- const len = binary.length;
309
- const bytes = new Uint8Array(len);
538
+ const decodeBase64 = (value) => {
539
+ const clean = value.replace(/\s/g, '');
540
+ const binary = atob(clean);
541
+ const len = binary.length;
542
+ const bytes = new Uint8Array(len);
310
543
  for (let i = 0; i < len; i += 1) {
311
544
  bytes[i] = binary.charCodeAt(i);
312
545
  }
313
- return bytes;
314
- };
546
+ return bytes;
547
+ };
548
+
549
+ const toExactArrayBuffer = (bytes) => {
550
+ if (
551
+ bytes.byteOffset === 0 &&
552
+ bytes.byteLength === bytes.buffer.byteLength
553
+ ) {
554
+ return bytes.buffer;
555
+ }
556
+ return bytes.buffer.slice(
557
+ bytes.byteOffset,
558
+ bytes.byteOffset + bytes.byteLength
559
+ );
560
+ };
315
561
 
316
562
  const decodeBase64ToText = (value) => {
317
563
  const bytes = decodeBase64(value);
318
564
  return new TextDecoder('utf-8').decode(bytes);
319
565
  };
320
566
 
567
+ const isMetroAssetUri = (value) => {
568
+ try {
569
+ const url = new URL(value);
570
+ return (
571
+ (url.protocol === 'http:' || url.protocol === 'https:') &&
572
+ url.pathname.startsWith('/assets/') &&
573
+ url.searchParams.has('platform') &&
574
+ url.searchParams.has('hash')
575
+ );
576
+ } catch {
577
+ return false;
578
+ }
579
+ };
580
+
321
581
  const sourceToArrayBuffer = async (source) => {
322
582
  if (source.kind === 'uri') {
323
- if (/^(?:file|content):\/\//i.test(source.uri)) {
583
+ if (/^(?:file|content|android\.resource):\/\//i.test(source.uri)) {
324
584
  return readLocalFile(source.uri);
325
585
  }
586
+ const isEpubHttpSource = isMetroAssetUri(source.uri);
587
+ if (isEpubHttpSource) {
588
+ sendEpubDiagnostic('epub.source.fetch.start', {uri: source.uri});
589
+ }
326
590
  const response = await fetch(source.uri);
591
+ if (isEpubHttpSource) {
592
+ sendEpubDiagnostic('epub.source.fetch.response', {
593
+ uri: source.uri,
594
+ status: response.status,
595
+ contentLength: response.headers.get('content-length'),
596
+ contentType: response.headers.get('content-type'),
597
+ transferEncoding: response.headers.get('transfer-encoding'),
598
+ });
599
+ }
327
600
  if (!response.ok && response.status !== 0) {
328
601
  throw new Error(`Falha ao carregar quadrinho (${response.status})`);
329
602
  }
330
- return response.arrayBuffer();
331
- }
332
- if (source.kind === 'base64') {
333
- return decodeBase64(source.data).buffer;
334
- }
603
+ if (isEpubHttpSource) {
604
+ sendEpubDiagnostic('epub.source.body.start', {uri: source.uri});
605
+ }
606
+ const body = await response.arrayBuffer();
607
+ if (isEpubHttpSource) {
608
+ sendEpubDiagnostic('epub.source.body.end', {
609
+ uri: source.uri,
610
+ byteLength: body.byteLength,
611
+ });
612
+ }
613
+ return body;
614
+ }
615
+ if (source.kind === 'base64') {
616
+ return toExactArrayBuffer(decodeBase64(source.data));
617
+ }
335
618
  throw new Error('Fonte de quadrinho inválida.');
336
619
  };
337
620
 
@@ -809,7 +1092,7 @@
809
1092
  return pages.length > 0 ? pages : [''];
810
1093
  };
811
1094
 
812
- const loadText = async (source) => {
1095
+ const loadText = async (source, isCurrentLoad = () => true) => {
813
1096
  let text = '';
814
1097
  if (source.kind === 'uri') {
815
1098
  const res = await fetch(source.uri);
@@ -820,6 +1103,9 @@
820
1103
  text = source.text || '';
821
1104
  }
822
1105
 
1106
+ if (!isCurrentLoad()) {
1107
+ throw new Error('Document load superseded by a newer load');
1108
+ }
823
1109
  clearViewer();
824
1110
  textPages = paginateText(text);
825
1111
  pageCount = textPages.length;
@@ -829,7 +1115,18 @@
829
1115
  return { pageCount };
830
1116
  };
831
1117
 
832
- const loadEpub = async (source) => {
1118
+ const loadEpub = async (source, loadId, generation) => {
1119
+ const diagnosticPayload = {
1120
+ loadId,
1121
+ sourceKind: source.kind,
1122
+ };
1123
+ const isCurrentLoad = () => generation === epubLoadGeneration;
1124
+ sendEpubDiagnostic('epub.load.start', diagnosticPayload);
1125
+
1126
+ if (epubInteractionCleanup) {
1127
+ epubInteractionCleanup();
1128
+ epubInteractionCleanup = null;
1129
+ }
833
1130
  if (rendition && typeof rendition.destroy === 'function') {
834
1131
  rendition.destroy();
835
1132
  }
@@ -839,28 +1136,56 @@
839
1136
 
840
1137
  let data = null;
841
1138
  if (source.kind === 'uri') {
842
- data = source.uri;
1139
+ data = await withEpubStageTimeout(
1140
+ sourceToArrayBuffer(source),
1141
+ 'source',
1142
+ diagnosticPayload,
1143
+ );
843
1144
  } else if (source.kind === 'base64') {
844
- data = decodeBase64(source.data);
1145
+ // epub.js expects an ArrayBuffer for binary books. Passing the decoded
1146
+ // Uint8Array directly can leave `book.ready` pending in Android WebView.
1147
+ data = toExactArrayBuffer(decodeBase64(source.data));
845
1148
  } else if (source.kind === 'text') {
846
1149
  const encoder = new TextEncoder();
847
1150
  data = encoder.encode(source.text || '').buffer;
848
1151
  }
849
1152
 
1153
+ if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
1154
+ sendEpubDiagnostic('epub.source.received', {
1155
+ ...diagnosticPayload,
1156
+ byteLength: data?.byteLength,
1157
+ });
1158
+ sendEpubDiagnostic('epub.source.decoded', {
1159
+ ...diagnosticPayload,
1160
+ byteLength: data?.byteLength,
1161
+ });
850
1162
  book = ePub(data);
851
- await book.ready;
1163
+ sendEpubDiagnostic('epub.book.created', {
1164
+ ...diagnosticPayload,
1165
+ byteLength: data?.byteLength,
1166
+ });
1167
+ sendEpubDiagnostic('epub.book.ready.start', diagnosticPayload);
1168
+ await withEpubStageTimeout(book.ready, 'book.ready', diagnosticPayload);
1169
+ if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
1170
+ sendEpubDiagnostic('epub.book.ready.end', diagnosticPayload);
852
1171
 
853
1172
  spineItems = book.spine && book.spine.items ? book.spine.items : [];
854
1173
  pageCount = spineItems.length;
855
1174
  currentPage = 1;
1175
+ sendEpubDiagnostic('epub.spine.ready', {
1176
+ ...diagnosticPayload,
1177
+ pageCount,
1178
+ });
856
1179
 
857
1180
  clearViewer();
858
1181
  rendition = book.renderTo(viewer, {
1182
+ manager: 'continuous',
859
1183
  width: '100%',
860
1184
  height: '100%',
861
- flow: 'paginated',
1185
+ flow: 'scrolled-continuous',
862
1186
  spread: 'none',
863
1187
  });
1188
+ sendEpubDiagnostic('epub.rendition.created', diagnosticPayload);
864
1189
 
865
1190
  if (rendition && rendition.hooks && rendition.hooks.content) {
866
1191
  rendition.hooks.content.register((contents) => {
@@ -868,6 +1193,41 @@
868
1193
  if (frame) {
869
1194
  frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
870
1195
  }
1196
+ const selectionDocument = contents && contents.document;
1197
+ const selectionWindow = contents && contents.window;
1198
+ if (
1199
+ selectionDocument &&
1200
+ typeof selectionDocument.addEventListener === 'function' &&
1201
+ selectionWindow &&
1202
+ typeof selectionWindow.getSelection === 'function'
1203
+ ) {
1204
+ const handleSelectionChange = () => {
1205
+ const selection = selectionWindow.getSelection();
1206
+ const text = selection ? selection.toString().trim() : '';
1207
+ epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
1208
+ };
1209
+ selectionDocument.addEventListener('selectionchange', handleSelectionChange);
1210
+ const selectionCleanup = () => {
1211
+ selectionDocument.removeEventListener('selectionchange', handleSelectionChange);
1212
+ epubSelectionCleanups.delete(selectionCleanup);
1213
+ };
1214
+ epubSelectionCleanups.add(selectionCleanup);
1215
+ if (contents && typeof contents === 'object') {
1216
+ contents.__papyrusSelectionCleanup = selectionCleanup;
1217
+ }
1218
+ }
1219
+ });
1220
+ }
1221
+
1222
+ if (
1223
+ rendition &&
1224
+ rendition.hooks &&
1225
+ rendition.hooks.unloaded &&
1226
+ typeof rendition.hooks.unloaded.register === 'function'
1227
+ ) {
1228
+ rendition.hooks.unloaded.register((view) => {
1229
+ const cleanup = view && view.contents && view.contents.__papyrusSelectionCleanup;
1230
+ if (typeof cleanup === 'function') cleanup();
871
1231
  });
872
1232
  }
873
1233
 
@@ -875,6 +1235,7 @@
875
1235
  rendition.on('selected', (cfiRange, contents) => {
876
1236
  const selection = contents && contents.window ? contents.window.getSelection() : null;
877
1237
  const text = selection ? selection.toString().trim() : '';
1238
+ epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
878
1239
  if (text) {
879
1240
  sendEvent('TEXT_SELECTED', { text, pageIndex: Math.max(0, currentPage - 1) });
880
1241
  }
@@ -882,12 +1243,177 @@
882
1243
  rendition.annotations.remove(cfiRange, 'highlight');
883
1244
  }
884
1245
  });
1246
+ rendition.on('relocated', (location) => {
1247
+ const start = location && location.start;
1248
+ let index = start && Number.isInteger(start.index) ? start.index : -1;
1249
+ if (index < 0 && start && start.href) index = getSpineIndexByHref(start.href);
1250
+ if (index < 0 || index >= spineItems.length) return;
1251
+ epubScrollDiagnostics && epubScrollDiagnostics.relocated(location);
1252
+ const nextPage = index + 1;
1253
+ if (nextPage === currentPage) return;
1254
+ currentPage = nextPage;
1255
+ sendState();
1256
+ });
885
1257
  }
886
1258
 
887
- await displayEpubPage(0);
1259
+ sendEpubDiagnostic('epub.display.start', diagnosticPayload);
1260
+ await withEpubStageTimeout(
1261
+ displayEpubPage(0),
1262
+ 'rendition.display',
1263
+ diagnosticPayload,
1264
+ );
1265
+ if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
1266
+ sendEpubDiagnostic('epub.display.end', diagnosticPayload);
1267
+ if (
1268
+ rendition &&
1269
+ rendition.manager &&
1270
+ typeof rendition.manager.on === 'function' &&
1271
+ typeof rendition.on === 'function'
1272
+ ) {
1273
+ let touchStart = null;
1274
+ let lastScrollOffset = -1;
1275
+ const manager = rendition.manager;
1276
+ epubScrollDiagnostics = createEpubScrollDiagnostics(manager);
1277
+
1278
+ // ContinuousManager schedules checks through its serial queue. Observe
1279
+ // the queue itself so diagnostics can distinguish queued work from the
1280
+ // check currently being executed without changing epub.js semantics.
1281
+ const originalCheck = typeof manager.check === 'function' ? manager.check : null;
1282
+ const queue = manager.q;
1283
+ const originalEnqueue = queue && typeof queue.enqueue === 'function'
1284
+ ? queue.enqueue
1285
+ : null;
1286
+ let checkRequestId = 0;
1287
+ if (originalCheck) {
1288
+ manager.check = function (...args) {
1289
+ const requestId = ++checkRequestId;
1290
+ const checkContext = this;
1291
+ if (epubScrollDiagnostics) {
1292
+ epubScrollDiagnostics.command('manager.check.request', {
1293
+ requestId,
1294
+ queueLength: manager.q && typeof manager.q.length === 'function'
1295
+ ? manager.q.length()
1296
+ : null,
1297
+ });
1298
+ }
1299
+ epubScrollDiagnostics && epubScrollDiagnostics.command('manager.check.start', {
1300
+ requestId,
1301
+ queueLength: queue && typeof queue.length === 'function'
1302
+ ? queue.length()
1303
+ : null,
1304
+ });
1305
+ let result;
1306
+ try {
1307
+ result = originalCheck.apply(checkContext, args);
1308
+ } catch (error) {
1309
+ epubScrollDiagnostics && epubScrollDiagnostics.command('manager.check.end', {
1310
+ requestId,
1311
+ status: 'error',
1312
+ message: error && error.message ? error.message : String(error),
1313
+ });
1314
+ throw error;
1315
+ }
1316
+ return Promise.resolve(result)
1317
+ .then(
1318
+ (result) => {
1319
+ epubScrollDiagnostics && epubScrollDiagnostics.command('manager.check.end', {
1320
+ requestId,
1321
+ status: 'ready',
1322
+ });
1323
+ return result;
1324
+ },
1325
+ (error) => {
1326
+ epubScrollDiagnostics && epubScrollDiagnostics.command('manager.check.end', {
1327
+ requestId,
1328
+ status: 'error',
1329
+ message: error && error.message ? error.message : String(error),
1330
+ });
1331
+ throw error;
1332
+ },
1333
+ );
1334
+ };
1335
+ }
1336
+ if (originalEnqueue) {
1337
+ queue.enqueue = function (...args) {
1338
+ const queueLengthBefore = typeof queue.length === 'function' ? queue.length() : null;
1339
+ const result = originalEnqueue.apply(this, args);
1340
+ epubScrollDiagnostics && epubScrollDiagnostics.command('manager.queue.enqueue', {
1341
+ queueLengthBefore,
1342
+ queueLengthAfter: typeof queue.length === 'function' ? queue.length() : null,
1343
+ running: Boolean(queue.running),
1344
+ });
1345
+ return result;
1346
+ };
1347
+ }
1348
+ const getTouchPoint = (event) => {
1349
+ const touch =
1350
+ (event && event.changedTouches && event.changedTouches[0]) ||
1351
+ (event && event.touches && event.touches[0]);
1352
+ if (!touch) return null;
1353
+ const x = Number(touch.screenX ?? touch.clientX);
1354
+ const y = Number(touch.screenY ?? touch.clientY);
1355
+ return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
1356
+ };
1357
+ const handleScroll = (event) => {
1358
+ const offsetY = Math.max(0, Number(event && event.top) || 0);
1359
+ epubScrollDiagnostics && epubScrollDiagnostics.scroll(event);
1360
+ if (Math.abs(offsetY - lastScrollOffset) < 1) return;
1361
+ lastScrollOffset = offsetY;
1362
+ sendEvent('VIEWER_SCROLL', { offsetY });
1363
+ };
1364
+ const handleTouchStart = (event) => {
1365
+ touchStart = getTouchPoint(event);
1366
+ epubScrollDiagnostics && epubScrollDiagnostics.touchStart(event);
1367
+ };
1368
+ const handleTouchMove = (event) => {
1369
+ epubScrollDiagnostics && epubScrollDiagnostics.touchMove(event);
1370
+ };
1371
+ const handleTouchEnd = (event) => {
1372
+ const end = getTouchPoint(event);
1373
+ const start = touchStart;
1374
+ touchStart = null;
1375
+ if (start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
1376
+ sendEvent('VIEWER_TAP', {});
1377
+ }
1378
+ epubScrollDiagnostics && epubScrollDiagnostics.touchEnd(event);
1379
+ };
1380
+
1381
+ manager.on('scroll', handleScroll);
1382
+ const handleScrolled = (event) => {
1383
+ epubScrollDiagnostics && epubScrollDiagnostics.settled(event);
1384
+ };
1385
+ manager.on('scrolled', handleScrolled);
1386
+ rendition.on('touchstart', handleTouchStart);
1387
+ rendition.on('touchmove', handleTouchMove);
1388
+ rendition.on('touchend', handleTouchEnd);
1389
+ epubInteractionCleanup = () => {
1390
+ epubSelectionCleanups.forEach((cleanup) => cleanup());
1391
+ epubSelectionCleanups.clear();
1392
+ epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(false);
1393
+ if (originalCheck) manager.check = originalCheck;
1394
+ if (originalEnqueue) queue.enqueue = originalEnqueue;
1395
+ if (typeof manager.off === 'function') manager.off('scroll', handleScroll);
1396
+ if (typeof manager.off === 'function') manager.off('scrolled', handleScrolled);
1397
+ if (typeof rendition.off === 'function') {
1398
+ rendition.off('touchstart', handleTouchStart);
1399
+ rendition.off('touchmove', handleTouchMove);
1400
+ rendition.off('touchend', handleTouchEnd);
1401
+ }
1402
+ epubScrollDiagnostics = null;
1403
+ };
1404
+ }
888
1405
  applyEpubZoom();
889
1406
 
890
- const outline = await buildOutline();
1407
+ const outline = await withEpubStageTimeout(
1408
+ buildOutline(),
1409
+ 'outline',
1410
+ diagnosticPayload,
1411
+ );
1412
+ if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
1413
+ sendEpubDiagnostic('epub.load.ready', {
1414
+ ...diagnosticPayload,
1415
+ pageCount,
1416
+ });
891
1417
  return { pageCount, outline };
892
1418
  };
893
1419
 
@@ -896,6 +1422,10 @@
896
1422
  const item = spineItems[pageIndex];
897
1423
  if (!item) return;
898
1424
  const target = item.href || item.idref || item.cfiBase || pageIndex;
1425
+ epubScrollDiagnostics && epubScrollDiagnostics.command('display', {
1426
+ target,
1427
+ pageIndex,
1428
+ });
899
1429
  await rendition.display(target);
900
1430
  currentPage = pageIndex + 1;
901
1431
  sendState();
@@ -1004,18 +1534,33 @@
1004
1534
 
1005
1535
  try {
1006
1536
  if (kind === 'load') {
1537
+ const generation = (epubLoadGeneration += 1);
1007
1538
  currentType = payload.type;
1008
1539
  zoom = 1.0;
1009
1540
 
1010
1541
  if (currentType === 'text') {
1011
- const result = await loadText(payload.source);
1542
+ const result = await loadText(
1543
+ payload.source,
1544
+ () => generation === epubLoadGeneration,
1545
+ );
1546
+ if (generation !== epubLoadGeneration) {
1547
+ throw new Error('Document load superseded by a newer load');
1548
+ }
1549
+ sendEvent('document.ready', {
1550
+ loadId: id,
1551
+ pageCount: result.pageCount,
1552
+ });
1012
1553
  sendState();
1013
1554
  sendResponse(id, true, result);
1014
1555
  return;
1015
1556
  }
1016
1557
 
1017
1558
  if (currentType === 'epub') {
1018
- const result = await loadEpub(payload.source);
1559
+ const result = await loadEpub(payload.source, id, generation);
1560
+ sendEvent('document.ready', {
1561
+ loadId: id,
1562
+ pageCount: result.pageCount,
1563
+ });
1019
1564
  sendState({ outline: result.outline || [] });
1020
1565
  sendResponse(id, true, result);
1021
1566
  return;
@@ -1132,7 +1677,28 @@
1132
1677
 
1133
1678
  sendResponse(id, false, null, 'Unknown command');
1134
1679
  } catch (err) {
1135
- sendResponse(id, false, null, err && err.message ? err.message : String(err));
1680
+ const errorMessage = err && err.message ? err.message : String(err);
1681
+ const isSuperseded = /superseded by a newer load/i.test(errorMessage);
1682
+ if (kind === 'load' && isSuperseded) {
1683
+ sendEvent('document.stale', { loadId: id, error: errorMessage });
1684
+ } else if (kind === 'load') {
1685
+ sendEvent('document.error', {
1686
+ loadId: id,
1687
+ error: { message: errorMessage, stack: err?.stack || null },
1688
+ });
1689
+ }
1690
+ if (kind === 'load' && payload?.type === 'epub') {
1691
+ sendEpubDiagnostic('epub.load.error', {
1692
+ loadId: id,
1693
+ sourceKind: payload.source?.kind,
1694
+ error: { message: errorMessage, stack: err?.stack || null },
1695
+ });
1696
+ sendEvent('document.error', {
1697
+ loadId: id,
1698
+ error: { message: errorMessage, stack: err?.stack || null },
1699
+ });
1700
+ }
1701
+ sendResponse(id, false, null, errorMessage);
1136
1702
  }
1137
1703
  };
1138
1704