diff --git a/web/src/lib/storage/index.ts b/web/src/lib/storage/index.ts new file mode 100644 index 00000000..2ddeb15e --- /dev/null +++ b/web/src/lib/storage/index.ts @@ -0,0 +1,15 @@ +import type { AbstractStorage } from "./storage"; +import { MemoryStorage } from "./memory"; +import { OPFSStorage } from "./opfs"; + +export function init(expectedSize?: number): Promise { + if (OPFSStorage.isAvailable()) { + return OPFSStorage.init(); + } + + if (MemoryStorage.isAvailable()) { + return MemoryStorage.init(expectedSize || 0); + } + + throw "no storage method is available"; +} diff --git a/web/src/lib/storage/memory.ts b/web/src/lib/storage/memory.ts new file mode 100644 index 00000000..f238b1a1 --- /dev/null +++ b/web/src/lib/storage/memory.ts @@ -0,0 +1,91 @@ +import { AbstractStorage } from "./storage"; + +export class MemoryStorage extends AbstractStorage { + #chunkSize: number; + #actualSize: number = 0; + #chunks: Uint8Array[] = []; + + constructor(chunkSize: number) { + super(); + this.#chunkSize = chunkSize; + } + + static async init(expectedSize: number) { + const MB = 1024 * 1024; + const chunkSize = Math.min(512 * MB, expectedSize); + + const storage = new this(chunkSize); + + // since we expect the output file to be roughly the same size + // as inputs, preallocate its size for the output + for ( + let toAllocate = expectedSize; + toAllocate > 0; + toAllocate -= chunkSize + ) { + storage.#chunks.push(new Uint8Array(chunkSize)); + } + + return storage; + } + + async res() { + // if we didn't need as much space as we allocated for some reason, + // shrink the buffers so that we don't inflate the file with zeroes + const outputView: Uint8Array[] = []; + + for (let i = 0; i < this.#chunks.length; ++i) { + outputView.push( + this.#chunks[i].subarray( + 0, + Math.min(this.#chunkSize, this.#actualSize), + ), + ); + + this.#actualSize -= this.#chunkSize; + if (this.#actualSize <= 0) { + break; + } + } + + return new Blob(outputView); + } + + #expand(size: number) { + while (size > this.#chunkSize * this.#chunks.length) { + this.#chunks.push(new Uint8Array(this.#chunkSize)); + } + } + + async write(data: Uint8Array | Int8Array, pos: number) { + const writeEnd = pos + data.length; + this.#expand(writeEnd); + + const chunkIndex = pos / this.#chunkSize | 0; + const offset = pos - (this.#chunkSize * chunkIndex); + + if (offset + data.length > this.#chunkSize) { + this.#chunks[chunkIndex].set( + data.subarray(0, this.#chunkSize - offset), + offset, + ); + this.#chunks[chunkIndex + 1].set( + data.subarray(this.#chunkSize - offset), + 0, + ); + } else { + this.#chunks[chunkIndex].set(data, offset); + } + + this.#actualSize = Math.max(writeEnd, this.#actualSize); + return data.length; + } + + async destroy() { + this.#chunks = []; + } + + static isAvailable() { + return true; + } +} diff --git a/web/src/lib/storage/storage.ts b/web/src/lib/storage/storage.ts index 8ab173ca..4df68633 100644 --- a/web/src/lib/storage/storage.ts +++ b/web/src/lib/storage/storage.ts @@ -1,5 +1,5 @@ export abstract class AbstractStorage { - static init(): Promise { + static init(_expected_size: number): Promise { throw "init() call on abstract implementation"; }