import { isNode } from "typesafecss"; import { DelayedStorage } from "./DelayedStorage"; import { FileStorage, getFileStorage, getFileStorageNested } from "./FileFolderAPI"; import { IStorage, IStorageRaw, IStorageSync } from "./IStorage"; import { JSONStorage } from "./JSONStorage"; import { StorageSync } from "./StorageObservable"; import { TransactionStorage } from "./TransactionStorage"; import { PendingStorage } from "./PendingStorage"; import { isDefined } from "../misc/types"; import { PrivateFileSystemStorage } from "./PrivateFileSystemStorage"; import { isInChromeExtension, isInChromeExtensionBackground } from "../misc/environment"; import { CBORStorage } from "./CBORStorage"; const FORCE_NO_PROMPT_KEY = "DiskCollection_forceNoPrompt"; export class DiskCollection implements IStorageSync { // Globally forces every DiskCollection into noPrompt mode (browser only), overriding per-collection config. public static getForceNoPrompt(): boolean { if (isNode()) return false; return !!localStorage.getItem(FORCE_NO_PROMPT_KEY); } public static setForceNoPrompt(forceNoPrompt: boolean) { if (isNode()) return; if (forceNoPrompt) { localStorage.setItem(FORCE_NO_PROMPT_KEY, "1"); } else { localStorage.removeItem(FORCE_NO_PROMPT_KEY); } } constructor( private collectionName: string, private config?: { writeDelay?: number; cbor?: boolean; noPrompt?: boolean; // Poll disk and reload when another process/tab writes to this collection. Off by default. resyncFromDisk?: boolean; freeze?: "shallow" | "deep"; // May mutate newValue in order to change what will be written beforeWrite?: (update: { newValue: T; key: string; collection: DiskCollection }) => void; } ) { } public transactionStorage: TransactionStorage | undefined; async initStorage(): Promise> { // If a Chrome extension, just return null. if (isInChromeExtensionBackground()) return null as any; let curCollection: IStorageRaw; if (!isNode() && (this.config?.noPrompt || DiskCollection.getForceNoPrompt())) { curCollection = await new PrivateFileSystemStorage(`collections/${this.collectionName}`); } else { let fileStorage = await getFileStorage(); let collections = await fileStorage.folder.getStorage("collections"); curCollection = await collections.folder.getStorage(this.collectionName); } let baseStorage = new TransactionStorage(curCollection, this.collectionName, this.config?.writeDelay, this.config?.resyncFromDisk); this.transactionStorage = baseStorage; return this.config?.cbor ? new CBORStorage(baseStorage) : new JSONStorage(baseStorage); } public baseStorage = this.initStorage(); private synced = new StorageSync( new PendingStorage(`Collection (${this.collectionName})`, new DelayedStorage(this.baseStorage) ), { freeze: this.config?.freeze, beforeWrite: this.config?.beforeWrite && ((update) => this.config!.beforeWrite!({ ...update, collection: this })), } ); public get(key: string): T | undefined { return this.synced.get(key); } public async getPromise(key: string): Promise { let base = await this.baseStorage; return base.get(key); } public set(key: string, value: T): void { this.synced.set(key, value); } public remove(key: string): void { this.synced.remove(key); } public getKeys(): string[] { return this.synced.getKeys(); } public getKeysPromise(): Promise { return this.synced.getKeysPromise(); } public getEntries(): [string, T][] { let keys = this.getKeys(); return keys.map(key => [key, this.get(key)]).filter(([_, value]) => isDefined(value)) as [string, T][]; } public getValues(): T[] { let keys = this.getKeys(); return keys.map(key => this.get(key)).filter(isDefined); } public async getValuesPromise(): Promise { let keys = await this.getKeysPromise(); let values: T[] = []; for (let key of keys) { let value = await this.getPromise(key); if (isDefined(value)) { values.push(value); } } return values; } public getInfo(key: string) { return this.synced.getInfo(key); } public async reset() { await this.synced.reset(); } } export class DiskCollectionPromise implements IStorage { constructor( private collectionName: string, private writeDelay?: number, ) { } async initStorage(): Promise> { let fileStorage = await getFileStorage(); let collections = await fileStorage.folder.getStorage("collections"); let curCollection = await collections.folder.getStorage(this.collectionName); let baseStorage = new TransactionStorage(curCollection, this.collectionName, this.writeDelay); return new JSONStorage(baseStorage); } private synced = ( new PendingStorage(`Collection (${this.collectionName})`, new DelayedStorage(this.initStorage()) ) ); public async get(key: string): Promise { return await this.synced.get(key); } public async set(key: string, value: T): Promise { await this.synced.set(key, value); } public async remove(key: string): Promise { await this.synced.remove(key); } public async getKeys(): Promise { return await this.synced.getKeys(); } public async getInfo(key: string) { return await this.synced.getInfo(key); } public async reset() { await this.synced.reset(); } } export class DiskCollectionRaw implements IStorage { constructor(private collectionName: string) { } async initStorage(): Promise> { let fileStorage = await getFileStorage(); let collections = await fileStorage.folder.getStorage("collections"); let baseStorage = await collections.folder.getStorage(this.collectionName); return baseStorage; } private synced = ( new PendingStorage(`Collection (${this.collectionName})`, new DelayedStorage(this.initStorage()) ) ); public async get(key: string): Promise { return await this.synced.get(key); } public async set(key: string, value: Buffer): Promise { await this.synced.set(key, value); } public async remove(key: string): Promise { await this.synced.remove(key); } public async getKeys(): Promise { return await this.synced.getKeys(); } public async getInfo(key: string) { return await this.synced.getInfo(key); } public async reset() { await this.synced.reset(); } } export class DiskCollectionRawSynced { constructor(private collectionName: string) { } async initStorage(): Promise> { let fileStorage = await getFileStorage(); let collections = await fileStorage.folder.getStorage("collections"); let baseStorage = await collections.folder.getStorage(this.collectionName); return baseStorage; } private synced = new StorageSync( new PendingStorage(`Collection (${this.collectionName})`, new DelayedStorage(this.initStorage()) ) ); public get(key: string): Buffer | undefined { return this.synced.get(key); } public async getPromise(key: string): Promise { return await this.synced.getPromise(key); } public set(key: string, value: Buffer) { this.synced.set(key, value); } public async getKeys(): Promise { return await this.synced.getKeysPromise(); } public async getInfo(key: string) { return await this.synced.getInfo(key); } public async reset() { await this.synced.reset(); } } export class DiskCollectionRawBrowser { constructor(private collectionName: string) { } async initStorage(): Promise> { return await new PrivateFileSystemStorage(`collections/${this.collectionName}`); } private synced = new StorageSync( new PendingStorage(`Collection (${this.collectionName})`, new DelayedStorage(this.initStorage()) ) ); public get(key: string): Buffer | undefined { return this.synced.get(key); } public async getPromise(key: string): Promise { return await this.synced.getPromise(key); } public set(key: string, value: Buffer) { this.synced.set(key, value); } public async getKeys(): Promise { return await this.synced.getKeysPromise(); } public async getInfo(key: string) { return await this.synced.getInfo(key); } public async reset() { await this.synced.reset(); } } export function newFileStorageBufferSyncer(folder = "") { let base = new PendingStorage(`FileStorageBufferSyncer`, new DelayedStorage(getFileStorageNested(folder)) ); return new StorageSync(base); } export function newFileStorageJSONSyncer(folder = "") { let base = new PendingStorage(`FileStorageJSONSyncer`, new DelayedStorage(getFileStorageNested(folder)) ); return new StorageSync(new JSONStorage(base)); }