@readium/navigator 2.6.1 → 2.7.0-alpha.1

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.
Files changed (51) hide show
  1. package/LICENSE +28 -0
  2. package/dist/index.js +4795 -3156
  3. package/dist/index.umd.cjs +42 -40
  4. package/package.json +10 -10
  5. package/src/divina/DivinaNavigator.ts +662 -0
  6. package/src/divina/DivinaPageManager.ts +322 -0
  7. package/src/divina/DivinaPagedPresenter.ts +508 -0
  8. package/src/divina/DivinaPeripherals.ts +630 -0
  9. package/src/divina/DivinaScrolledPresenter.ts +516 -0
  10. package/src/divina/DivinaSpreader.ts +202 -0
  11. package/src/divina/DivinaVariantSelector.ts +77 -0
  12. package/src/divina/index.ts +8 -0
  13. package/src/divina/preferences/DivinaDefaults.ts +37 -0
  14. package/src/divina/preferences/DivinaPreferences.ts +77 -0
  15. package/src/divina/preferences/DivinaPreferencesEditor.ts +89 -0
  16. package/src/divina/preferences/DivinaSettings.ts +38 -0
  17. package/src/divina/preferences/index.ts +4 -0
  18. package/src/divina/protection/DivinaNavigatorProtector.ts +44 -0
  19. package/src/epub/EpubNavigator.ts +59 -3
  20. package/src/epub/frame/FrameBlobBuilder.ts +85 -32
  21. package/src/epub/frame/FrameManager.ts +3 -3
  22. package/src/epub/frame/FramePoolManager.ts +78 -34
  23. package/src/epub/fxl/FXLFrameManager.ts +69 -26
  24. package/src/epub/fxl/FXLFramePoolManager.ts +88 -50
  25. package/src/epub/fxl/FXLPeripherals.ts +22 -1
  26. package/src/index.ts +1 -0
  27. package/src/peripherals/KeyboardConditionBridge.ts +15 -5
  28. package/src/peripherals/KeyboardPeripherals.ts +13 -7
  29. package/src/webpub/WebPubFrameManager.ts +3 -3
  30. package/types/src/divina/DivinaNavigator.d.ts +113 -0
  31. package/types/src/divina/DivinaPageManager.d.ts +66 -0
  32. package/types/src/divina/DivinaPagedPresenter.d.ts +122 -0
  33. package/types/src/divina/DivinaPeripherals.d.ts +121 -0
  34. package/types/src/divina/DivinaScrolledPresenter.d.ts +125 -0
  35. package/types/src/divina/DivinaSpreader.d.ts +65 -0
  36. package/types/src/divina/DivinaVariantSelector.d.ts +30 -0
  37. package/types/src/divina/index.d.ts +8 -0
  38. package/types/src/divina/preferences/DivinaDefaults.d.ts +18 -0
  39. package/types/src/divina/preferences/DivinaPreferences.d.ts +33 -0
  40. package/types/src/divina/preferences/DivinaPreferencesEditor.d.ts +20 -0
  41. package/types/src/divina/preferences/DivinaSettings.d.ts +26 -0
  42. package/types/src/divina/preferences/index.d.ts +4 -0
  43. package/types/src/divina/protection/DivinaNavigatorProtector.d.ts +14 -0
  44. package/types/src/epub/EpubNavigator.d.ts +3 -1
  45. package/types/src/epub/frame/FrameBlobBuilder.d.ts +6 -3
  46. package/types/src/epub/frame/FramePoolManager.d.ts +1 -0
  47. package/types/src/epub/fxl/FXLFrameManager.d.ts +2 -0
  48. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +6 -2
  49. package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
  50. package/types/src/index.d.ts +1 -0
  51. package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -0
@@ -6,6 +6,7 @@ import { CommsEventKey, ContextMenuEvent, DecorationActivatedEvent, FXLModules,
6
6
  import { Decoration, DecorationActivationEvent, DecorationObserver, DecorableNavigator, DecoratorConfig, decorationsEqual, resolveDecorationForWire, BUILTIN_DECORATION_TYPES } from "../decorations/index.ts";
7
7
  import * as path from "path-browserify";
8
8
  import { FXLFrameManager } from "./fxl/FXLFrameManager.ts";
9
+ import { isTypedOMSupported } from "./fxl/FXLPeripherals.ts";
9
10
  import { FrameManager } from "./frame/FrameManager.ts";
10
11
  import { IEpubPreferences, EpubPreferences } from "./preferences/EpubPreferences.ts";
11
12
  import { IEpubDefaults, EpubDefaults } from "./preferences/EpubDefaults.ts";
@@ -71,6 +72,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
71
72
  private readonly container: HTMLElement;
72
73
  private readonly listeners: EpubNavigatorListeners;
73
74
  private framePool!: FramePoolManager | FXLFramePoolManager;
75
+ private _destroyed = false;
74
76
  private positions!: Locator[];
75
77
  private currentLocation!: Locator;
76
78
  private lastLocationInView: Locator | undefined;
@@ -249,11 +251,14 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
249
251
  });
250
252
  }
251
253
 
254
+ if (this._destroyed) return;
255
+
252
256
  if(this._layout === Layout.fixed) {
253
257
  this.framePool = new FXLFramePoolManager(
254
258
  this.container,
255
259
  this.positions,
256
260
  this.pub,
261
+ this._layout,
257
262
  this._injector,
258
263
  this._contentProtection,
259
264
  this._keyboardPeripherals
@@ -263,6 +268,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
263
268
  }
264
269
  } else {
265
270
  await this.updateCSS(false);
271
+ if (this._destroyed) return;
266
272
  const cssProperties = this.compileCSSProperties(this._css);
267
273
  this.framePool = new FramePoolManager(
268
274
  this.container,
@@ -276,9 +282,17 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
276
282
 
277
283
  if(this.currentLocation === undefined)
278
284
  this.currentLocation = this.positions[0];
285
+ else
286
+ this.currentLocation = this.completeLocator(this.currentLocation);
279
287
 
280
288
  await this.resizeHandler();
281
- await this.apply();
289
+ if (this._destroyed) return;
290
+ return new Promise(async res => {
291
+ if (this._destroyed) return res(false);
292
+ await this.go(this.currentLocation, false, (s) => {
293
+ res(s);
294
+ });
295
+ });
282
296
  }
283
297
 
284
298
  public get settings(): Readonly<EpubSettings> {
@@ -383,7 +397,12 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
383
397
  const parentEl = this.container.parentElement || document.documentElement;
384
398
 
385
399
  if (this._layout === Layout.fixed) {
386
- this.container.style.width = `${ getContentWidth(parentEl) - this._settings.constraint }px`;
400
+ const width = getContentWidth(parentEl) - this._settings.constraint;
401
+ if (isTypedOMSupported()) {
402
+ this.container.attributeStyleMap.set("width", CSS.px(width));
403
+ } else {
404
+ this.container.style.width = `${ width }px`;
405
+ }
387
406
  if (!this.framePool) return;
388
407
  (this.framePool as FXLFramePoolManager).resizeHandler();
389
408
  } else {
@@ -779,6 +798,8 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
779
798
  // End of Decoration
780
799
 
781
800
  public async destroy() {
801
+ // Flag synchronously so an in-flight load() bails before attaching frames.
802
+ this._destroyed = true;
782
803
  if (this._suspiciousActivityListener) {
783
804
  window.removeEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
784
805
  }
@@ -1078,7 +1099,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
1078
1099
  return;
1079
1100
  }
1080
1101
 
1081
- const progression = locator?.locations?.progression;
1102
+ const progression = locator.locations?.progression;
1082
1103
  const hasProgression = progression && progression > 0;
1083
1104
  if(hasProgression)
1084
1105
  done = await new Promise<boolean>((res, _) => {
@@ -1089,7 +1110,42 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
1089
1110
  cb(done);
1090
1111
  }
1091
1112
 
1113
+ private completeLocator(locator: Locator): Locator {
1114
+ if(!locator.href) {
1115
+ let fellback = false;
1116
+ if(typeof locator.locations.position === "number") {
1117
+ const match = this.positions.find(p => p.locations.position === locator.locations.position);
1118
+ if (match) {
1119
+ locator = match.copyWithLocations(locator.locations);
1120
+ fellback = true;
1121
+ }
1122
+ }
1123
+ if(!fellback && typeof locator.locations?.totalProgression === "number") {
1124
+ // If locator has no href, but it does have a totalProgression,
1125
+ // we can attempt to find the right resource from the positions list.
1126
+ // This is here to help with conversion from OPDS locators which only
1127
+ // require the total progression in the publication.
1128
+ const targetProgression = locator.locations.totalProgression;
1129
+ let closestIdx = 0;
1130
+ let closestDist = Infinity;
1131
+ for (let i = 0; i < this.positions.length; i++) {
1132
+ const pos = this.positions[i];
1133
+ // Use totalProgression if available, otherwise estimate from index
1134
+ const posProg = pos.locations.totalProgression ?? (i / this.positions.length);
1135
+ const dist = Math.abs(posProg - targetProgression);
1136
+ if (dist < closestDist) {
1137
+ closestDist = dist;
1138
+ closestIdx = i;
1139
+ }
1140
+ }
1141
+ locator = this.positions[closestIdx].copyWithLocations(locator.locations);
1142
+ }
1143
+ }
1144
+ return locator;
1145
+ }
1146
+
1092
1147
  public go(locator: Locator, _: boolean, cb: (ok: boolean) => void): void {
1148
+ locator = this.completeLocator(locator);
1093
1149
  const href = locator.href.split("#")[0];
1094
1150
  let link = this.pub.readingOrder.findWithHref(href);
1095
1151
  if(!link) {
@@ -1,4 +1,4 @@
1
- import { Link, MediaType, Publication, ReadingProgression } from "@readium/shared";
1
+ import { Link, MediaType, Publication, ReadingProgression, Resource } from "@readium/shared";
2
2
  import { Injector } from "../../injection/Injector.ts";
3
3
  import { getScriptMode } from "../../helpers/scriptMode.ts";
4
4
 
@@ -20,73 +20,121 @@ const csp = (domains: string[]) => {
20
20
  ].join("; ");
21
21
  };
22
22
 
23
- export default class FrameBlobBuider {
24
- private readonly item: Link;
25
- private readonly burl: string;
26
- private readonly pub: Publication;
23
+ export default class FrameBlobBuilder {
27
24
  private readonly cssProperties?: { [key: string]: string };
28
25
  private readonly injector: Injector | null = null;
29
26
 
27
+ private currentUrl?: string;
28
+ private currentResource?: Resource;
29
+
30
30
  constructor(
31
- pub: Publication,
32
- baseURL: string,
33
- item: Link,
31
+ private readonly pub: Publication,
32
+ private readonly baseURL: string,
33
+ private readonly item: Link,
34
34
  options: {
35
35
  cssProperties?: { [key: string]: string };
36
36
  injector?: Injector | null;
37
37
  }
38
38
  ) {
39
- this.pub = pub;
40
39
  this.item = item;
41
- this.burl = item.toURL(baseURL) || "";
42
40
  this.cssProperties = options.cssProperties;
43
41
  this.injector = options.injector ?? null;
44
42
  }
45
43
 
44
+ public reset() {
45
+ this.currentUrl && URL.revokeObjectURL(this.currentUrl);
46
+ this.currentUrl = undefined;
47
+ this.currentResource?.close();
48
+ this.currentResource = undefined;
49
+ }
50
+
46
51
  public async build(fxl = false): Promise<string> {
47
- if(!this.item.mediaType.isHTML) {
48
- if(this.item.mediaType.isBitmap || this.item.mediaType.equals(MediaType.SVG)) {
49
- return this.buildImageFrame();
52
+ if(this.currentUrl) return this.currentUrl;
53
+
54
+ this.currentResource = this.pub.get(this.item);
55
+ const link = await this.currentResource.link();
56
+ if(!this.currentResource) {
57
+ // Reset has occured in the meantime
58
+ return "about:blank";
59
+ }
60
+ if(!link.mediaType.isHTML) {
61
+ if(link.mediaType.isBitmap || link.mediaType.equals(MediaType.SVG)) {
62
+ const blobUrl = await this.buildImageFrame();
63
+ this.currentUrl = blobUrl;
64
+ return blobUrl;
50
65
  } else
51
- throw Error("Unsupported frame mediatype " + this.item.mediaType.string);
66
+ throw Error("Unsupported frame mediatype " + link.mediaType.string);
52
67
  } else {
53
- return await this.buildHtmlFrame(fxl);
68
+ const blobUrl = await this.buildHtmlFrame(fxl);
69
+ this.currentUrl = blobUrl;
70
+ return blobUrl;
54
71
  }
55
72
  }
56
73
 
57
74
  private async buildHtmlFrame(fxl = false): Promise<string> {
58
- // Load the HTML resource
59
- const txt = await this.pub.get(this.item).readAsString();
60
- if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
75
+ if(!this.currentResource) throw new Error("No resource loaded");
61
76
 
62
- const doc = new DOMParser().parseFromString(
63
- txt,
64
- this.item.mediaType.string as DOMParserSupportedType
65
- );
77
+ // Load the HTML resource
78
+ const link = await this.currentResource.link();
79
+ const doc = await this.currentResource.readAsXML() as HTMLDocument;
80
+ if(!doc) throw new Error(`Failed reading item ${link.href}`);
66
81
 
67
82
  const perror = doc.querySelector("parsererror");
68
83
  if (perror) {
69
84
  const details = perror.querySelector("div");
70
- throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
85
+ throw new Error(`Failed parsing item ${link.href}: ${details?.textContent || perror.textContent}`);
71
86
  }
72
87
 
73
88
  // Apply resource injections if injection service is provided
74
89
  if (this.injector) {
75
- await this.injector.injectForDocument(doc, this.item);
90
+ await this.injector.injectForDocument(doc, link);
76
91
  }
77
92
 
78
- return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, fxl, this.cssProperties);
93
+ return this.finalizeDOM(doc, this.pub.baseURL, link.toURL(this.baseURL) || "", link.mediaType, fxl, this.cssProperties);
79
94
  }
80
95
 
81
- private buildImageFrame(): string {
82
- // Rudimentary image display
83
- const doc = document.implementation.createHTMLDocument(this.item.title || this.item.href);
96
+ private async buildImageFrame(): Promise<string> {
97
+ if(!this.currentResource) throw new Error("No resource loaded");
98
+ const link = await this.currentResource.link();
99
+ const burl = link.toURL(this.baseURL) || ""
100
+
101
+ // Rudimentary image display in an HTML doc
102
+ const doc = document.implementation.createHTMLDocument(link.title || link.href);
103
+
104
+ // Add viewport if available
105
+ if((link?.height || 0) > 0 && (link?.width || 0) > 0) {
106
+ const viewportMeta = doc.createElement("meta");
107
+ viewportMeta.name = "viewport";
108
+ viewportMeta.content = `width=${link.width}, height=${link.height}`;
109
+ viewportMeta.dataset.readium = "true";
110
+ doc.head.appendChild(viewportMeta);
111
+ }
112
+
84
113
  const simg = document.createElement("img");
85
- simg.src = this.burl || "";
86
- simg.alt = this.item.title || "";
87
- simg.decoding = "async";
114
+ simg.src = burl || "";
115
+ simg.alt = link.title || "";
116
+ await simg.decode(); // Reduce repaints
88
117
  doc.body.appendChild(simg);
89
- return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, true);
118
+
119
+ // Apply resource injections if injection service is provided
120
+ if (this.injector) {
121
+ await this.injector.injectForDocument(doc, new Link({
122
+ // Temporary solution to address injector only expecting (X)HTML
123
+ // documents for injection, which we are technically providing
124
+ href: "readium-image-frame.xhtml",
125
+ type: MediaType.XHTML.string
126
+ }));
127
+ }
128
+
129
+ // Add image style
130
+ const sstyle = doc.createElement("style");
131
+ sstyle.dataset.readium = "true";
132
+ sstyle.textContent = `
133
+ html, body { width: 100%; height: 100%; margin: 0; padding: 0; font-size: 0; }
134
+ img { margin: 0; padding: 0; border: 0; }`;
135
+ doc.head.appendChild(sstyle);
136
+
137
+ return this.finalizeDOM(doc, this.pub.baseURL, burl, link.mediaType, true);
90
138
  }
91
139
 
92
140
  private setProperties(cssProperties: { [key: string]: string }, doc: Document) {
@@ -102,6 +150,10 @@ export default class FrameBlobBuider {
102
150
  // Get allowed domains from injector if it exists
103
151
  const allowedDomains = this.injector?.getAllowedDomains?.() || [];
104
152
 
153
+ // Remove query from root if present, as CSP doesn't allow them
154
+ root = root?.split("?")[0];
155
+
156
+
105
157
  // Always include the root domain if provided
106
158
  const domains = [...new Set([
107
159
  ...(root ? [root] : []),
@@ -124,6 +176,7 @@ export default class FrameBlobBuider {
124
176
  // loaded in parallel, greatly increasing overall speed.
125
177
  doc.body.querySelectorAll("img").forEach((img) => {
126
178
  img.setAttribute("fetchpriority", "high");
179
+ img.setAttribute("referrerpolicy", "origin");
127
180
  });
128
181
 
129
182
  // We need to ensure that lang is set on the root element
@@ -27,7 +27,7 @@ export class FrameManager {
27
27
  this.frame.sandbox.value = "allow-same-origin allow-scripts";
28
28
  this.frame.classList.add("readium-navigator-iframe");
29
29
  this.frame.style.visibility = "hidden";
30
- this.frame.style.setProperty("aria-hidden", "true");
30
+ this.frame.ariaHidden = "true";
31
31
  this.frame.style.opacity = "0";
32
32
  this.frame.style.position = "absolute";
33
33
  this.frame.style.pointerEvents = "none";
@@ -111,7 +111,7 @@ export class FrameManager {
111
111
  async hide(): Promise<void> {
112
112
  if(this.destroyed) return;
113
113
  this.frame.style.visibility = "hidden";
114
- this.frame.style.setProperty("aria-hidden", "true");
114
+ this.frame.ariaHidden = "true";
115
115
  this.frame.style.opacity = "0";
116
116
  this.frame.style.pointerEvents = "none";
117
117
  this.hidden = true;
@@ -143,9 +143,9 @@ export class FrameManager {
143
143
 
144
144
  const remove = () => {
145
145
  this.frame.style.removeProperty("visibility");
146
- this.frame.style.removeProperty("aria-hidden");
147
146
  this.frame.style.removeProperty("opacity");
148
147
  this.frame.style.removeProperty("pointer-events");
148
+ this.frame.ariaHidden = null;
149
149
  this.hidden = false;
150
150
 
151
151
  if (sML.UA.WebKit) {
@@ -1,12 +1,12 @@
1
1
  import { ModuleName } from "@readium/navigator-html-injectables";
2
2
  import { Locator, Publication } from "@readium/shared";
3
- import FrameBlobBuider from "./FrameBlobBuilder.ts";
3
+ import FrameBlobBuilder from "./FrameBlobBuilder.ts";
4
4
  import { FrameManager } from "./FrameManager.ts";
5
5
  import { Injector } from "../../injection/Injector.ts";
6
6
  import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator.ts";
7
7
 
8
- const UPPER_BOUNDARY = 5;
9
- const LOWER_BOUNDARY = 3;
8
+ const UPPER_BOUNDARY = 10;
9
+ const LOWER_BOUNDARY = 5;
10
10
 
11
11
  export class FramePoolManager {
12
12
  private readonly container: HTMLElement;
@@ -14,13 +14,14 @@ export class FramePoolManager {
14
14
  private _currentFrame: FrameManager | undefined;
15
15
  private currentCssProperties: { [key: string]: string } | undefined;
16
16
  private readonly pool: Map<string, FrameManager> = new Map();
17
- private readonly blobs: Map<string, string> = new Map();
18
- private readonly inprogress: Map<string, Promise<void>> = new Map();
17
+ private readonly blobs: Map<string, FrameBlobBuilder> = new Map();
18
+ private readonly inprogress: Map<string, Promise<unknown>> = new Map();
19
19
  private pendingUpdates: Map<string, { inPool: boolean }> = new Map();
20
20
  private currentBaseURL: string | undefined;
21
21
  private readonly injector: Injector | null = null;
22
22
  private readonly contentProtectionConfig: IContentProtectionConfig;
23
23
  private readonly keyboardPeripheralsConfig: IKeyboardPeripheralsConfig;
24
+ private updateSequence = 0;
24
25
 
25
26
  constructor(
26
27
  container: HTMLElement,
@@ -42,7 +43,7 @@ export class FramePoolManager {
42
43
  // Wait for all in-progress loads to complete
43
44
  let iit = this.inprogress.values();
44
45
  let inp = iit.next();
45
- const inprogressPromises: Promise<void>[] = [];
46
+ const inprogressPromises: Promise<unknown>[] = [];
46
47
  while(inp.value) {
47
48
  inprogressPromises.push(inp.value);
48
49
  inp = iit.next();
@@ -62,10 +63,8 @@ export class FramePoolManager {
62
63
  this.pool.clear();
63
64
 
64
65
  // Revoke all blobs
65
- this.blobs.forEach(v => {
66
- this.injector?.releaseBlobUrl?.(v);
67
- URL.revokeObjectURL(v);
68
- });
66
+ this.blobs.forEach(v => v.reset());
67
+ this.blobs.clear();
69
68
 
70
69
  // Clean up injector if it exists
71
70
  this.injector?.dispose();
@@ -77,6 +76,7 @@ export class FramePoolManager {
77
76
  }
78
77
 
79
78
  async update(pub: Publication, locator: Locator, modules: ModuleName[], force=false) {
79
+ const updateSequence = ++this.updateSequence;
80
80
  let i = this.positions.findIndex(l => l.locations.position === locator.locations.position);
81
81
  if(i < 0) throw Error(`Locator not found in position list: ${locator.locations.position} > ${this.positions.reduce<number>((acc, l) => l.locations.position || 0 > acc ? l.locations.position || 0 : acc, 0) }`);
82
82
  const newHref = this.positions[i].href;
@@ -106,15 +106,18 @@ export class FramePoolManager {
106
106
  this.pool.delete(href);
107
107
  if(this.pendingUpdates.has(href))
108
108
  this.pendingUpdates.set(href, { inPool: false });
109
+ // Note that we don't reset the blob here, unlike in the FXL pool.
110
+ // This is because FXL tends to have a ton more blobs. Maybe we'll adjust
111
+ // this at a later point with a much larger boundary for resets to deal
112
+ // with extremely long/large reflowable publications.
113
+ // Reflowable publication resources also tend to be much larger documents,
114
+ // so they're more expensive to preprocess with the FrameBlobBuilder.
109
115
  });
110
116
 
111
117
  // Check if base URL of publication has changed
112
118
  if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
113
119
  // Revoke all blobs
114
- this.blobs.forEach(v => {
115
- this.injector?.releaseBlobUrl?.(v);
116
- URL.revokeObjectURL(v);
117
- });
120
+ this.blobs.forEach(v => v.reset());
118
121
  this.blobs.clear();
119
122
  }
120
123
  this.currentBaseURL = pub.baseURL;
@@ -127,18 +130,14 @@ export class FramePoolManager {
127
130
  // when navigating backwards, where paginated will go the
128
131
  // start of the resource instead of the end due to the
129
132
  // corrupted width ColumnSnapper (injectables) gets on init
130
- this.blobs.forEach(v => {
131
- this.injector?.releaseBlobUrl?.(v);
132
- URL.revokeObjectURL(v);
133
- });
133
+ this.blobs.forEach(v => v.reset());
134
134
  this.blobs.clear();
135
135
  this.pendingUpdates.clear();
136
136
  }
137
137
  if(this.pendingUpdates.has(href) && this.pendingUpdates.get(href)?.inPool === false) {
138
- const url = this.blobs.get(href);
139
- if(url) {
140
- this.injector?.releaseBlobUrl?.(url);
141
- URL.revokeObjectURL(url);
138
+ const v = this.blobs.get(href);
139
+ if(v) {
140
+ v.reset();
142
141
  this.blobs.delete(href);
143
142
  this.pendingUpdates.delete(href);
144
143
  }
@@ -157,7 +156,7 @@ export class FramePoolManager {
157
156
  const itm = pub.readingOrder.findWithHref(href);
158
157
  if(!itm) return; // TODO throw?
159
158
  if(!this.blobs.has(href)) {
160
- const blobBuilder = new FrameBlobBuider(
159
+ this.blobs.set(href, new FrameBlobBuilder(
161
160
  pub,
162
161
  this.currentBaseURL || "",
163
162
  itm,
@@ -165,36 +164,81 @@ export class FramePoolManager {
165
164
  cssProperties: this.currentCssProperties,
166
165
  injector: this.injector
167
166
  }
168
- );
169
- const blobURL = await blobBuilder.build();
170
- this.blobs.set(href, blobURL);
167
+ ));
171
168
  }
172
169
 
173
170
  // Create <iframe>
174
- const fm = new FrameManager(this.blobs.get(href)!, this.contentProtectionConfig, this.keyboardPeripheralsConfig);
171
+ const fm = new FrameManager(await this.blobs.get(href)!.build(), this.contentProtectionConfig, this.keyboardPeripheralsConfig);
175
172
  if(href !== newHref) await fm.hide(); // Avoid unecessary hide
176
173
  this.container.appendChild(fm.iframe);
177
174
  await fm.load(modules);
178
175
  this.pool.set(href, fm);
176
+ return fm;
179
177
  }
178
+
179
+ // Target frame is awaited
180
180
  try {
181
- await Promise.all(creation.map(href => creator(href)));
181
+ await Promise.all(creation.filter(href => href === newHref).map(href => creator(href)));
182
182
  } catch (error) {
183
183
  reject(error);
184
+ return;
185
+ }
186
+
187
+ if (updateSequence !== this.updateSequence) {
188
+ resolve();
189
+ return;
184
190
  }
185
191
 
192
+ // Remaining frames can resolve later
193
+ Promise.all(creation.map(async href => {
194
+ const c = creator(href);
195
+ this.inprogress.set(href, c);
196
+ await c;
197
+ this.inprogress.delete(href);
198
+ }));
199
+
186
200
  // Update current frame
187
- const newFrame = this.pool.get(newHref)!;
201
+ const newFrame = this.pool.get(newHref);
202
+ if (!newFrame) {
203
+ resolve();
204
+ return;
205
+ }
206
+
188
207
  if(newFrame?.source !== this._currentFrame?.source || force) {
189
208
  await this._currentFrame?.hide(); // Hide current frame. It's possible it no longer even exists in the DOM at this point
190
- if(newFrame) // If user is speeding through the publication, this can get destroyed
191
- await newFrame.load(modules); // In order to ensure modules match the latest configuration
209
+
210
+ // Resolve if there is a concurrent update that has started since this one began
211
+ if (updateSequence !== this.updateSequence) {
212
+ resolve();
213
+ return;
214
+ }
215
+
216
+ // If user is speeding through the publication, this can get destroyed
217
+ const currentFrame = this.pool.get(newHref);
218
+ if (!currentFrame) {
219
+ resolve();
220
+ return;
221
+ }
222
+
223
+ await currentFrame.load(modules); // In order to ensure modules match the latest configuration
224
+
225
+ // Resolve if there is a concurrent update that has started since this one began
226
+ if (updateSequence !== this.updateSequence) {
227
+ resolve();
228
+ return;
229
+ }
230
+
231
+ // Check if it was destroyed, again
232
+ const latestFrame = this.pool.get(newHref);
233
+ if (!latestFrame) {
234
+ resolve();
235
+ return;
236
+ }
192
237
 
193
238
  // Update progression if necessary and show the new frame
194
- if(newFrame) // If user is speeding through the publication, this can get destroyed
195
- await newFrame.show(locator.locations.progression); // Show/activate new frame
239
+ await latestFrame.show(locator.locations.progression); // Show/activate new frame
196
240
 
197
- this._currentFrame = newFrame;
241
+ this._currentFrame = latestFrame;
198
242
 
199
243
  // Safari retains focus on the hidden iframe; transfer it to the new
200
244
  // frame only if no meaningful element in the parent document owns focus.