Support for dynamic page loading for Multi-WACZs (#399)

Support querying pages dynamically via pages endpoint:
- If loaded pages < total pages, load additional pages via /pages
endpoint if canQueryPages is set to true on item
- If query provided, do `/pages?search=...` to search for pages. Use
pagination of 25 pages for now.

WACZ hash:
- if provided, store waczhash per page, and use for thumbnail, favicon
and top-level replay loading.

Embed: better support for force reloading, call delete with reload
directly in embed

Depends on webrecorder/wabac.js#220
This commit is contained in:
Ilya Kreymer
2025-02-24 14:35:03 -08:00
committed by GitHub
parent 94cbf5e43d
commit 6ef8bf4973
9 changed files with 203 additions and 66 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "replaywebpage", "name": "replaywebpage",
"productName": "ReplayWeb.page", "productName": "ReplayWeb.page",
"version": "2.2.5", "version": "2.3.0",
"description": "Serverless Web Archive Replay", "description": "Serverless Web Archive Replay",
"repository": "https://github.com/webrecorder/replayweb.page", "repository": "https://github.com/webrecorder/replayweb.page",
"homepage": "https://replayweb.page/", "homepage": "https://replayweb.page/",
@@ -18,7 +18,7 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-free": "^5.15.4",
"@shoelace-style/shoelace": "~2.15.1", "@shoelace-style/shoelace": "~2.15.1",
"@webrecorder/wabac": "^2.20.8", "@webrecorder/wabac": "^2.21.0",
"bulma": "^0.9.3", "bulma": "^0.9.3",
"electron-log": "^4.4.1", "electron-log": "^4.4.1",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
+23 -9
View File
@@ -7,9 +7,9 @@ import {
} from "lit"; } from "lit";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { wrapCss, updateFaviconLinks } from "./misc"; import { wrapCss, updateFaviconLinks, apiPrefix } from "./misc";
import { SWManager } from "./swmanager"; import { SWManager } from "./swmanager";
import { property } from "lit/decorators.js"; import { property, query } from "lit/decorators.js";
import type { FavIconEventDetail } from "./types"; import type { FavIconEventDetail } from "./types";
import type { EmbedReplayData, EmbedReplayEvent } from "./item"; import type { EmbedReplayData, EmbedReplayEvent } from "./item";
@@ -82,6 +82,9 @@ class Embed extends LitElement {
@property({ type: Boolean }) useRuffle = false; @property({ type: Boolean }) useRuffle = false;
@query("iframe")
private readonly iframe?: HTMLIFrameElement | null;
replayfile = defaultReplayFile; replayfile = defaultReplayFile;
mainElementName = "replay-app-main"; mainElementName = "replay-app-main";
appName = "ReplayWeb.page"; appName = "ReplayWeb.page";
@@ -126,17 +129,28 @@ class Embed extends LitElement {
} }
} }
fullReload() { async fullReload() {
const iframe = this.renderRoot.querySelector("iframe"); if (!this.iframe?.contentWindow) {
if (iframe?.contentWindow) { return false;
iframe.contentWindow.postMessage({ type: "fullReload" });
} }
const deleteURL = apiPrefix + "/c/" + this.coll + "?reload=1";
const resp = await this.iframe.contentWindow.fetch(deleteURL, {
method: "DELETE",
});
if (resp.status !== 200) {
return false;
}
this.iframe.contentWindow.location.reload();
return true;
} }
handleMessage(event: IframeMessage) { handleMessage(event: IframeMessage) {
const iframe = this.renderRoot.querySelector("iframe"); if (this.iframe && event.source === this.iframe.contentWindow) {
if (iframe && event.source === iframe.contentWindow) {
switch (event.data.type) { switch (event.data.type) {
case "urlchange": case "urlchange":
if (this.deepLink) { if (this.deepLink) {
+2 -10
View File
@@ -85,6 +85,7 @@ export type EmbedReplayData = {
ts?: string; ts?: string;
title?: string; title?: string;
query?: string; query?: string;
waczhash?: string;
}; };
export type EmbedReplayEvent = EmbedReplayData & { export type EmbedReplayEvent = EmbedReplayData & {
@@ -229,16 +230,6 @@ class Item extends LitElement {
this.observer.observe(this); this.observer.observe(this);
} }
if (this.embed) {
window.addEventListener("message", (event: MessageEvent) => {
if (event.source === window.parent) {
if (event.data.type === "fullReload") {
void this.deleteFully(true);
}
}
});
}
} }
async runUpdateLoop() { async runUpdateLoop() {
@@ -976,6 +967,7 @@ class Item extends LitElement {
sourceUrl="${this.sourceUrl || ""}" sourceUrl="${this.sourceUrl || ""}"
url="${this.tabData.url || ""}" url="${this.tabData.url || ""}"
ts="${this.tabData.ts || ""}" ts="${this.tabData.ts || ""}"
waczhash="${this.tabData.waczhash || ""}"
@coll-tab-nav="${this.onItemTabNav}" @coll-tab-nav="${this.onItemTabNav}"
id="replay" id="replay"
@replay-loading="${this.onReplayLoading}" @replay-loading="${this.onReplayLoading}"
+12 -8
View File
@@ -5,7 +5,7 @@ import { property, state } from "lit/decorators.js";
import "keyword-mark-element/lib/keyword-mark.js"; import "keyword-mark-element/lib/keyword-mark.js";
import { getReplayLink } from "./pageutils"; import { getPageDateTS, getReplayLink } from "./pageutils";
import { wrapCss } from "./misc"; import { wrapCss } from "./misc";
import type { URLResource } from "./types"; import type { URLResource } from "./types";
@@ -192,7 +192,7 @@ class PageEntry extends LitElement {
render() { render() {
const p = this.page!; const p = this.page!;
const date = this.page!.date; const { date } = getPageDateTS(p);
const hasSize = typeof p.size === "number"; const hasSize = typeof p.size === "number";
@@ -245,6 +245,7 @@ class PageEntry extends LitElement {
"pages", "pages",
this.page!.url, this.page!.url,
this.page!.timestamp!, this.page!.timestamp!,
this.page!.waczhash,
)}" )}"
> >
<p class="is-size-6 has-text-weight-bold has-text-link text"> <p class="is-size-6 has-text-weight-bold has-text-link text">
@@ -298,6 +299,12 @@ class PageEntry extends LitElement {
`; `;
} }
private getReplayPrefix() {
const waczhash = this.page!.waczhash ? `:${this.page!.waczhash}/` : "";
const ts = this.page!.timestamp || "";
return this.replayPrefix + "/" + waczhash + ts + "id_";
}
private renderPageIcon() { private renderPageIcon() {
if (!this.thumbnailValid) { if (!this.thumbnailValid) {
return this.renderFavicon(); return this.renderFavicon();
@@ -305,9 +312,7 @@ class PageEntry extends LitElement {
return html`<img return html`<img
class="thumbnail" class="thumbnail"
@error=${() => (this.thumbnailValid = false)} @error=${() => (this.thumbnailValid = false)}
src=${`${this.replayPrefix}/${this.page!.timestamp}id_/urn:thumbnail:${ src=${`${this.getReplayPrefix()}/urn:thumbnail:${this.page!.url}`}
this.page!.url
}`}
loading="lazy" loading="lazy"
/>`; />`;
} }
@@ -319,9 +324,7 @@ class PageEntry extends LitElement {
return html`<img return html`<img
class="favicon" class="favicon"
@error=${() => (this.iconValid = false)} @error=${() => (this.iconValid = false)}
src=${`${this.replayPrefix}/${this.page!.timestamp}id_/${ src=${`${this.getReplayPrefix()}/${this.page!.favIconUrl}`}
this.page!.favIconUrl
}`}
loading="lazy" loading="lazy"
/>`; />`;
} }
@@ -377,6 +380,7 @@ class PageEntry extends LitElement {
const data = { const data = {
url: this.page!.url, url: this.page!.url,
ts: this.page!.timestamp, ts: this.page!.timestamp,
waczhash: this.page!.waczhash,
}; };
this.sendChangeEvent(data, reload); this.sendChangeEvent(data, reload);
return false; return false;
+126 -16
View File
@@ -20,6 +20,8 @@ import type { PageEntry } from "./pageentry";
import type { Id, Index } from "flexsearch"; import type { Id, Index } from "flexsearch";
import type { ItemType, URLResource } from "./types"; import type { ItemType, URLResource } from "./types";
const DYNAMIC_PAGE_SIZE = 25;
// =========================================================================== // ===========================================================================
class Pages extends LitElement { class Pages extends LitElement {
@property({ type: Array }) @property({ type: Array })
@@ -103,6 +105,18 @@ class Pages extends LitElement {
@property({ type: String }) @property({ type: String })
defaultKey = ""; defaultKey = "";
@property({ type: Boolean })
dynamicPagesQuery = false;
@property({ type: Number })
totalPages = 0;
@property({ type: Number })
dynamicPageCount = 1;
@property({ type: Boolean })
skipScrollMore = false;
private _ival: number | undefined; private _ival: number | undefined;
static get sortKeys() { static get sortKeys() {
@@ -214,10 +228,25 @@ class Pages extends LitElement {
); );
} else if (this.showAllPages && this.hasExtraPages) { } else if (this.showAllPages && this.hasExtraPages) {
this.filteredPages = [...this.textPages!]; this.filteredPages = [...this.textPages!];
} else { } else if (!this.dynamicPagesQuery) {
this.filteredPages = [...this.collInfo!.pages]; this.filteredPages = [...this.collInfo!.pages];
} }
this.totalPages = this.filteredPages.length;
if (this.dynamicPagesQuery) {
if (!this.query) {
const seedPages = this.collInfo!.pages.filter((x) => x.isSeed);
this.filteredPages =
this.showAllPages || !seedPages.length
? [...this.collInfo!.pages]
: seedPages;
this.hasExtraPages = seedPages.length !== this.collInfo!.pages.length;
}
this.dynamicPageCount = 1;
await this.addDynamicPages();
}
if (this.currList !== 0) { if (this.currList !== 0) {
await this.filterCurated(); await this.filterCurated();
} }
@@ -238,6 +267,80 @@ class Pages extends LitElement {
this.sendChangeEvent(data); this.sendChangeEvent(data);
} }
async addDynamicPages() {
const search = new URLSearchParams();
if (this.query) {
search.set("search", this.query);
}
search.set("page", this.dynamicPageCount + "");
search.set("pageSize", DYNAMIC_PAGE_SIZE + "");
const resp = await fetch(
`${this.collInfo!.apiPrefix}/pages?${search.toString()}`,
);
const json = await resp.json();
if (!json.pages) {
return;
}
const knownPages = new Set();
this.filteredPages.forEach((x) => knownPages.add(x.id));
const newPages = [];
for (const {
id,
url,
title,
mime,
status,
ts,
favIconUrl,
waczhash,
isSeed,
} of json.pages) {
if (knownPages.has(id)) {
continue;
}
if (!this.showAllPages && this.hasExtraPages && !this.query && !isSeed) {
continue;
}
let tsActual;
if (typeof ts === "string") {
tsActual = new Date(ts).getTime();
} else {
tsActual = ts;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newPage: any = {
id,
url,
title,
mime,
status,
ts: tsActual,
favIconUrl,
waczhash,
};
newPages.push(newPage);
}
if (newPages.length) {
this.filteredPages = [...this.filteredPages, ...newPages];
}
if (json.total) {
this.totalPages = json.total;
} else {
this.totalPages = this.filteredPages.length;
}
}
async filterCurated() { async filterCurated() {
const resp = await fetch( const resp = await fetch(
`${this.collInfo!.apiPrefix}/curated/${this.currList}`, `${this.collInfo!.apiPrefix}/curated/${this.currList}`,
@@ -263,11 +366,17 @@ class Pages extends LitElement {
this.flex = flex; this.flex = flex;
this.textPages = pages; this.textPages = pages;
if (!this.dynamicPagesQuery) {
this.hasExtraPages = Boolean( this.hasExtraPages = Boolean(
this.textPages && this.textPages &&
this.collInfo?.pages && this.collInfo?.pages &&
this.textPages.length > this.collInfo.pages.length, this.textPages.length > this.collInfo.pages.length,
); );
}
if (this.collInfo) {
this.dynamicPagesQuery = this.collInfo.canQueryPages || false;
}
return Promise.all( return Promise.all(
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'page' implicitly has an 'any' type. | TS7006 - Parameter 'index' implicitly has an 'any' type. // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'page' implicitly has an 'any' type. | TS7006 - Parameter 'index' implicitly has an 'any' type.
@@ -893,7 +1002,7 @@ class Pages extends LitElement {
.sortDesc="${this.sortDesc}" .sortDesc="${this.sortDesc}"
.sortKeys="${Pages.sortKeys}" .sortKeys="${Pages.sortKeys}"
.data="${this.filteredPages}" .data="${this.filteredPages}"
pageResults="100" .pageResults="${this.dynamicPagesQuery ? this.totalPages : 100}"
@sort-changed="${this.onSortChanged}" @sort-changed="${this.onSortChanged}"
class="${this.filteredPages.length ? "" : "is-hidden"}" class="${this.filteredPages.length ? "" : "is-hidden"}"
> >
@@ -1064,6 +1173,8 @@ class Pages extends LitElement {
this.sortedPages = event.detail.sortedData; this.sortedPages = event.detail.sortedData;
this.sortKey = event.detail.sortKey; this.sortKey = event.detail.sortKey;
this.sortDesc = event.detail.sortDesc; this.sortDesc = event.detail.sortDesc;
void this.updateComplete.then(() => (this.skipScrollMore = false));
} }
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type. // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type.
@@ -1184,9 +1295,7 @@ class Pages extends LitElement {
} }
formatResults() { formatResults() {
// Default behavior: Count all available pages const length = this.totalPages;
if (!this.query) {
const length = this.filteredPages.length;
if (length === this.sortedPages.length) { if (length === this.sortedPages.length) {
return `${length} Page${length !== 1 ? "s" : ""}`; return `${length} Page${length !== 1 ? "s" : ""}`;
} else { } else {
@@ -1194,14 +1303,6 @@ class Pages extends LitElement {
} }
} }
// ... unless they were filtered
if (this.sortedPages.length === 1) {
return "1 Page";
} else {
return `${this.sortedPages.length} Pages`;
}
}
getNoResultsMessage() { getNoResultsMessage() {
if (!this.collInfo?.pages.length) { if (!this.collInfo?.pages.length) {
return html`<span class="fix-text-wrapping" return html`<span class="fix-text-wrapping"
@@ -1229,14 +1330,23 @@ class Pages extends LitElement {
} }
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type. // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type.
onScroll(event) { async onScroll(event) {
const element = event.currentTarget; const element = event.currentTarget;
const diff = const diff =
element.scrollHeight - element.scrollTop - element.clientHeight; element.scrollHeight - element.scrollTop - element.clientHeight;
if (diff < 40) { if (diff < 40 && !this.skipScrollMore) {
this.skipScrollMore = true;
if (
this.dynamicPagesQuery &&
this.filteredPages.length < this.totalPages
) {
this.dynamicPageCount += 1;
await this.addDynamicPages();
}
const sorter = this.renderRoot.querySelector<Sorter>("wr-sorter"); const sorter = this.renderRoot.querySelector<Sorter>("wr-sorter");
if (sorter) { if (sorter) {
sorter.getMore(); sorter.getMore(DYNAMIC_PAGE_SIZE);
} }
} }
} }
+9 -1
View File
@@ -69,11 +69,19 @@ function getTS(iso: string) {
} }
// =========================================================================== // ===========================================================================
function getReplayLink(view: string, url: string, ts: string) { function getReplayLink(
view: string,
url: string,
ts: string,
waczhash?: string,
) {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set("view", view); params.set("view", view);
params.set("url", url); params.set("url", url);
params.set("ts", ts); params.set("ts", ts);
if (waczhash) {
params.set("waczhash", waczhash);
}
return "#" + params.toString(); return "#" + params.toString();
} }
+6 -1
View File
@@ -20,6 +20,9 @@ class Replay extends LitElement {
@property({ type: String }) @property({ type: String })
ts = ""; ts = "";
@property({ type: String })
waczhash = "";
// actual replay url // actual replay url
@property({ type: String }) @property({ type: String })
replayUrl = ""; replayUrl = "";
@@ -97,7 +100,9 @@ class Replay extends LitElement {
doSetIframeUrl() { doSetIframeUrl() {
this.iframeUrl = this.iframeUrl =
this.url && this.collInfo this.url && this.collInfo
? `${this.collInfo.replayPrefix}/${this.ts || ""}mp_/${this.url}` ? `${this.collInfo.replayPrefix}/${
this.waczhash ? `:${this.waczhash}/` : ""
}${this.ts || ""}mp_/${this.url}`
: ""; : "";
} }
+6 -2
View File
@@ -11,9 +11,12 @@ export type URLResource = {
size: number; size: number;
favIconUrl: string; favIconUrl: string;
text?: string; text?: string;
waczhash?: string;
}; };
export type Page = URLResource; export type Page = URLResource & {
isSeed?: boolean;
};
export type ItemType = { export type ItemType = {
filename?: string; filename?: string;
@@ -43,12 +46,13 @@ export type ItemType = {
software?: string; software?: string;
}; };
onDemand?: boolean; onDemand?: boolean;
pages: URLResource[]; pages: Page[];
curatedPages: (URLResource & { list: string })[]; curatedPages: (URLResource & { list: string })[];
lists: URLResource[]; lists: URLResource[];
ctime?: string; ctime?: string;
totalSize?: unknown; totalSize?: unknown;
size?: number | string; size?: number | string;
canQueryPages?: boolean;
}; };
export type FavIconEventDetail = { export type FavIconEventDetail = {
+9 -9
View File
@@ -1028,16 +1028,16 @@
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e"
integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==
"@webrecorder/wabac@^2.20.8": "@webrecorder/wabac@^2.21.0":
version "2.20.8" version "2.21.0"
resolved "https://registry.yarnpkg.com/@webrecorder/wabac/-/wabac-2.20.8.tgz#37dd14f4cf821f36ecdf5719d5f334bb3445290e" resolved "https://registry.yarnpkg.com/@webrecorder/wabac/-/wabac-2.21.0.tgz#cc3483f15cb3f1502fdef89e1c9a4ee5709bcaec"
integrity sha512-P6FxC17ZgPfwfp3HbdI9hkzMnqD5I3kBF0cMnue6m8wBVMCzcVGp9x43wT+pcq0zOv6ATGObxcmjJKCsDUus7A== integrity sha512-28G3ci/m/qJXnLDoUP/Kt09PiTuORBZH0bw/JlKhPCeLL4IdYsODeXaImVwvTT12R9tV7Zp37w26jhkMfcur0w==
dependencies: dependencies:
"@peculiar/asn1-ecc" "^2.3.4" "@peculiar/asn1-ecc" "^2.3.4"
"@peculiar/asn1-schema" "^2.3.3" "@peculiar/asn1-schema" "^2.3.3"
"@peculiar/x509" "^1.9.2" "@peculiar/x509" "^1.9.2"
"@types/js-levenshtein" "^1.1.3" "@types/js-levenshtein" "^1.1.3"
"@webrecorder/wombat" "^3.8.7" "@webrecorder/wombat" "^3.8.8"
acorn "^8.10.0" acorn "^8.10.0"
auto-js-ipfs "^2.1.1" auto-js-ipfs "^2.1.1"
base64-js "^1.5.1" base64-js "^1.5.1"
@@ -1058,10 +1058,10 @@
stream-browserify "^3.0.0" stream-browserify "^3.0.0"
warcio "^2.4.3" warcio "^2.4.3"
"@webrecorder/wombat@^3.8.7": "@webrecorder/wombat@^3.8.8":
version "3.8.7" version "3.8.8"
resolved "https://registry.yarnpkg.com/@webrecorder/wombat/-/wombat-3.8.7.tgz#51c7465c589e0020be064121127c7c10a38ec21f" resolved "https://registry.yarnpkg.com/@webrecorder/wombat/-/wombat-3.8.8.tgz#aab4dd8eea6d6cb17bfefb7ee1802e7b45b11ed7"
integrity sha512-bW5V7cBweTkTazOIN8oZZGwHLevsGNv1luY3t0RYdEZhs5BDpTmUHN33zEbrXDOiPUlY3N3I8+73VA+PuxihoQ== integrity sha512-XkJOZAyHrdXNkAVoISQEh/NHzaBMekQZfWqes/k2vYkW6v9DmZ0wjP7Kf6MHCuajKX8uSH+caB2tv1kJgvnv3Q==
dependencies: dependencies:
warcio "^2.4.0" warcio "^2.4.0"