*Add IPFS caching with IndexedDb
This commit is contained in:
parent
ac5cf77f0f
commit
8a6ec42fa6
|
@ -41,6 +41,7 @@
|
||||||
"@lumeweb/kernel-dht-client": "https://github.com/LumeWeb/kernel-dht-client.git",
|
"@lumeweb/kernel-dht-client": "https://github.com/LumeWeb/kernel-dht-client.git",
|
||||||
"@lumeweb/kernel-ipfs-client": "https://github.com/LumeWeb/kernel-ipfs-client.git",
|
"@lumeweb/kernel-ipfs-client": "https://github.com/LumeWeb/kernel-ipfs-client.git",
|
||||||
"@lumeweb/tld-enum": "https://github.com/LumeWeb/list-of-top-level-domains.git",
|
"@lumeweb/tld-enum": "https://github.com/LumeWeb/list-of-top-level-domains.git",
|
||||||
|
"dexie": "^3.2.2",
|
||||||
"ejs": "^3.1.8",
|
"ejs": "^3.1.8",
|
||||||
"is-ipfs": "^6.0.2",
|
"is-ipfs": "^6.0.2",
|
||||||
"libkernel": "https://github.com/LumeWeb/libextension.git",
|
"libkernel": "https://github.com/LumeWeb/libextension.git",
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
CONTENT_MODE_CHUNKED,
|
CONTENT_MODE_CHUNKED,
|
||||||
contentModes,
|
contentModes,
|
||||||
} from "../mimes.js";
|
} from "../mimes.js";
|
||||||
|
import Dexie from "dexie";
|
||||||
|
|
||||||
const INDEX_HTML_FILES = ["index.html", "index.htm", "index.shtml"];
|
const INDEX_HTML_FILES = ["index.html", "index.htm", "index.shtml"];
|
||||||
|
|
||||||
|
@ -86,6 +87,14 @@ interface StatFileSubfile {
|
||||||
size: number;
|
size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cacheDb = new Dexie("LumeWebIFSCache");
|
||||||
|
|
||||||
|
cacheDb.version(1).stores({
|
||||||
|
items: `url,contentType,data,timestamp`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const MAX_CACHE_SIZE = 1024 * 1024 * 1024 * 50;
|
||||||
|
|
||||||
export default class IpfsProvider extends BaseProvider {
|
export default class IpfsProvider extends BaseProvider {
|
||||||
async shouldHandleRequest(
|
async shouldHandleRequest(
|
||||||
details: OnBeforeRequestDetailsType
|
details: OnBeforeRequestDetailsType
|
||||||
|
@ -127,37 +136,46 @@ export default class IpfsProvider extends BaseProvider {
|
||||||
let resp: StatFileResponse | null = null;
|
let resp: StatFileResponse | null = null;
|
||||||
let fetchMethod: typeof fetchIpfs | typeof fetchIpns;
|
let fetchMethod: typeof fetchIpfs | typeof fetchIpns;
|
||||||
let err;
|
let err;
|
||||||
|
let contentType: string;
|
||||||
if (urlObj.protocol == "https") {
|
if (urlObj.protocol == "https") {
|
||||||
urlObj.protocol = "http";
|
urlObj.protocol = "http";
|
||||||
return { redirectUrl: urlObj.toString() };
|
return { redirectUrl: urlObj.toString() };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
let cachedPage: { contentType: string; data: Blob } | null = null;
|
||||||
if (ipfsPath(hash)) {
|
|
||||||
hash = hash.replace("/ipfs/", "");
|
|
||||||
resp = await statIpfs(hash.replace("/ipfs/", ""), urlPath);
|
|
||||||
fetchMethod = fetchIpfs;
|
|
||||||
} else if (ipnsPath(hash)) {
|
|
||||||
hash = hash.replace("/ipns/", "");
|
|
||||||
resp = await statIpns(hash.replace("/ipns/", ""), urlPath);
|
|
||||||
fetchMethod = fetchIpns;
|
|
||||||
} else {
|
|
||||||
err = "invalid content";
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
err = (e as Error).message;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contentType = resp?.contentType;
|
try {
|
||||||
let contentLength = resp?.size;
|
// @ts-ignore
|
||||||
|
cachedPage = await cacheDb.items.where("url").equals(details.url).first();
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (!cachedPage) {
|
||||||
|
try {
|
||||||
|
if (ipfsPath(hash)) {
|
||||||
|
hash = hash.replace("/ipfs/", "");
|
||||||
|
resp = await statIpfs(hash.replace("/ipfs/", ""), urlPath);
|
||||||
|
fetchMethod = fetchIpfs;
|
||||||
|
} else if (ipnsPath(hash)) {
|
||||||
|
hash = hash.replace("/ipns/", "");
|
||||||
|
resp = await statIpns(hash.replace("/ipns/", ""), urlPath);
|
||||||
|
fetchMethod = fetchIpns;
|
||||||
|
} else {
|
||||||
|
err = "invalid content";
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
err = (e as Error).message;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType = resp?.contentType as string;
|
||||||
|
if (contentType?.includes(";")) {
|
||||||
|
contentType = contentType?.split(";").shift() as string;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contentType = cachedPage.contentType;
|
||||||
|
}
|
||||||
|
|
||||||
let status = "200";
|
let status = "200";
|
||||||
|
|
||||||
if (contentType?.includes(";")) {
|
|
||||||
contentType = contentType?.split(";").shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resp) {
|
if (resp) {
|
||||||
if (!resp.exists) {
|
if (!resp.exists) {
|
||||||
err = "404";
|
err = "404";
|
||||||
|
@ -188,7 +206,6 @@ export default class IpfsProvider extends BaseProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setData(details, "status", status);
|
this.setData(details, "status", status);
|
||||||
this.setData(details, "contentLength", contentLength);
|
|
||||||
|
|
||||||
let filterPromiseResolve: any;
|
let filterPromiseResolve: any;
|
||||||
let filterPromise = new Promise((resolve) => {
|
let filterPromise = new Promise((resolve) => {
|
||||||
|
@ -203,9 +220,13 @@ export default class IpfsProvider extends BaseProvider {
|
||||||
filterPromiseResolve();
|
filterPromiseResolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
const buffer: Uint8Array[] = [];
|
let buffer: Uint8Array[] = [];
|
||||||
|
let cacheBuffer: Uint8Array[] | Uint8Array = [];
|
||||||
|
|
||||||
const receiveUpdate = (chunk: Uint8Array) => {
|
const receiveUpdate = (chunk: Uint8Array) => {
|
||||||
|
if (!chunk.length && !chunk.byteLength) {
|
||||||
|
return filterPromise;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
Object.keys(contentModes).includes(contentType as string) &&
|
Object.keys(contentModes).includes(contentType as string) &&
|
||||||
[CONTENT_MODE_CHUNKED, CONTENT_MODE_BUFFERED].includes(
|
[CONTENT_MODE_CHUNKED, CONTENT_MODE_BUFFERED].includes(
|
||||||
|
@ -213,12 +234,20 @@ export default class IpfsProvider extends BaseProvider {
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
buffer.push(chunk);
|
buffer.push(chunk);
|
||||||
return;
|
resp = resp as StatFileResponse;
|
||||||
|
cacheBuffer = cacheBuffer as Uint8Array[];
|
||||||
|
if (!cachedPage && resp.size <= MAX_CACHE_SIZE) {
|
||||||
|
cacheBuffer.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
filterPromise.then(() => {
|
return filterPromise.then(() => {
|
||||||
streamPromise = streamPromise.then(() => {
|
streamPromise = streamPromise.then(() => {
|
||||||
filter.write(chunk);
|
filter.write(chunk);
|
||||||
|
cacheBuffer = cacheBuffer as Uint8Array[];
|
||||||
|
cacheBuffer.push(chunk);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -238,62 +267,92 @@ export default class IpfsProvider extends BaseProvider {
|
||||||
urlPath += `/${indexFiles[0].name}`;
|
urlPath += `/${indexFiles[0].name}`;
|
||||||
} else {
|
} else {
|
||||||
const renderedDirectory = DIRECTORY_TEMPLATE(resp?.files);
|
const renderedDirectory = DIRECTORY_TEMPLATE(resp?.files);
|
||||||
contentLength = renderedDirectory.length;
|
|
||||||
receiveUpdate(new TextEncoder().encode(renderedDirectory));
|
receiveUpdate(new TextEncoder().encode(renderedDirectory));
|
||||||
filterPromise
|
filterPromise
|
||||||
.then(() => streamPromise)
|
.then(() => streamPromise)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
filter.close();
|
filter.close();
|
||||||
});
|
});
|
||||||
this.setData(details, "contentLength", contentLength);
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBuffer = () => {
|
||||||
|
if (buffer.length) {
|
||||||
|
let mode = contentModes[contentType as string];
|
||||||
|
buffer = buffer.map((item: Uint8Array | ArrayBuffer) => {
|
||||||
|
if (item instanceof ArrayBuffer) {
|
||||||
|
return new Uint8Array(item);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
if (mode === CONTENT_MODE_BUFFERED) {
|
||||||
|
let data: string | Uint8Array = Uint8Array.from(
|
||||||
|
buffer.reduce(
|
||||||
|
(previousValue: Uint8Array, currentValue: Uint8Array) => {
|
||||||
|
return Uint8Array.from([...previousValue, ...currentValue]);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
/* if (contentType === "text/html") {
|
||||||
|
data = new TextDecoder("utf-8", { fatal: true }).decode(data);
|
||||||
|
let htmlDoc = new DOMParser().parseFromString(
|
||||||
|
data as string,
|
||||||
|
contentType
|
||||||
|
);
|
||||||
|
let found = htmlDoc.documentElement.querySelectorAll(
|
||||||
|
'meta[http-equiv="Content-Security-Policy"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (found.length) {
|
||||||
|
found.forEach((item) => item.remove());
|
||||||
|
data = htmlDoc.documentElement.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = new TextEncoder().encode(data);
|
||||||
|
}*/
|
||||||
|
filter.write(data);
|
||||||
|
} else if (mode == CONTENT_MODE_CHUNKED) {
|
||||||
|
buffer.forEach((data) => filter.write(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (cachedPage) {
|
||||||
|
// @ts-ignore
|
||||||
|
cachedPage.data.arrayBuffer().then((data: ArrayBuffer) => {
|
||||||
|
// @ts-ignore
|
||||||
|
receiveUpdate(data)?.then(() => {
|
||||||
|
handleBuffer();
|
||||||
|
filterPromise.then(() => filter.close());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
fetchMethod?.(hash, urlPath, receiveUpdate)
|
fetchMethod?.(hash, urlPath, receiveUpdate)
|
||||||
.then(() => streamPromise)
|
.then(() => streamPromise)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (buffer.length) {
|
handleBuffer();
|
||||||
let mode = contentModes[contentType as string];
|
resp = resp as StatFileResponse;
|
||||||
if (mode === CONTENT_MODE_BUFFERED) {
|
if (resp.size <= MAX_CACHE_SIZE) {
|
||||||
let data: string | Uint8Array = Uint8Array.from(
|
cacheBuffer = Uint8Array.from(
|
||||||
buffer.reduce(
|
(cacheBuffer as Uint8Array[]).reduce(
|
||||||
(previousValue: Uint8Array, currentValue: Uint8Array) => {
|
(previousValue: Uint8Array, currentValue: Uint8Array) => {
|
||||||
return Uint8Array.from([...previousValue, ...currentValue]);
|
return Uint8Array.from([...previousValue, ...currentValue]);
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (contentType === "application/javascript") {
|
|
||||||
data = new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
||||||
data = data.replace(
|
|
||||||
/\/\/#\s*sourceMappingURL=([^\.]+)\.js.map/,
|
|
||||||
""
|
|
||||||
);
|
|
||||||
data = new TextEncoder().encode(data);
|
|
||||||
}
|
|
||||||
/* if (contentType === "text/html") {
|
|
||||||
data = new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
||||||
let htmlDoc = new DOMParser().parseFromString(
|
|
||||||
data as string,
|
|
||||||
contentType
|
|
||||||
);
|
|
||||||
let found = htmlDoc.documentElement.querySelectorAll(
|
|
||||||
'meta[http-equiv="Content-Security-Policy"]'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (found.length) {
|
|
||||||
found.forEach((item) => item.remove());
|
|
||||||
data = htmlDoc.documentElement.outerHTML;
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
data = new TextEncoder().encode(data);
|
// @ts-ignore
|
||||||
}*/
|
return cacheDb.items.put({
|
||||||
filter.write(data);
|
url: details.url,
|
||||||
} else if (mode == CONTENT_MODE_CHUNKED) {
|
contentType,
|
||||||
buffer.forEach((data) => filter.write(data));
|
data: new Blob([cacheBuffer.buffer], { type: contentType }),
|
||||||
}
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
filter.close();
|
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error("page error", urlPath, e.message);
|
console.error("page error", urlPath, e.message);
|
||||||
|
|
|
@ -598,6 +598,11 @@ define-properties@^1.1.3, define-properties@^1.1.4:
|
||||||
has-property-descriptors "^1.0.0"
|
has-property-descriptors "^1.0.0"
|
||||||
object-keys "^1.1.1"
|
object-keys "^1.1.1"
|
||||||
|
|
||||||
|
dexie@^3.2.2:
|
||||||
|
version "3.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.2.tgz#fa6f2a3c0d6ed0766f8d97a03720056f88fe0e01"
|
||||||
|
integrity sha512-q5dC3HPmir2DERlX+toCBbHQXW5MsyrFqPFcovkH9N2S/UW/H3H5AWAB6iEOExeraAu+j+zRDG+zg/D7YhH0qg==
|
||||||
|
|
||||||
dir-glob@^3.0.1:
|
dir-glob@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||||
|
|
Reference in New Issue