0xweb
Version:
Contract package manager and other web3 tools
274 lines (244 loc) • 7.85 kB
text/typescript
import memd from 'memd';
import { $require } from '@dequanto/utils/$require';
import { File, FileSafe } from 'atma-io';
import { class_Dfr, type Constructor } from 'atma-utils';
import { JsonConvert } from 'class-json';
import { $ref } from '@dequanto/utils/$ref';
export class JsonStoreFs<T> {
public errored: Error = null;
private listeners = [] as ({version: number, promise: class_Dfr})[];
private version = 0;
private data: T;
private pending: T;
private busy = false;
private watcherFn: (path?: string) => any
private watching = false;
private transport: ITransport;
public lock = new class_Dfr;
constructor (
public path: string
, public Type?: Constructor
, public mapFn?: (x: T) => any
, public format?: boolean
, public $default?: T
, public serializeFn?: (x: T) => any
, public persistence: 'file' | 'localStorage' = 'file'
) {
this.lock.resolve();
switch (persistence) {
case 'localStorage':
this.transport = new LocalStorageTransport(this.path);
break;
case 'file':
default:
this.transport = new FileTransport(this.path);
break;
}
}
public watch (cb: typeof this.watcherFn) {
$require.Null(this.watcherFn, `Already watching`);
this.watcherFn = cb;
}
public unwatch () {
this.transport.unwatch(this.watcherFn);
this.watcherFn = null;
}
public cleanCache () {
this.data = null;
// Should we do this? clear pending promise
this.transport.cleanCache();
memd.fn.clearMemoized(this.readInner);
File.clearCache(this.path);
}
public write (arr: T) {
this.data = arr;
let dfr = new class_Dfr;
this.listeners.push({
version: ++this.version,
promise: dfr
});
if (this.busy === true) {
this.pending = arr;
return dfr;
}
this.busy = true;
this.lock.defer();
this.writeInner(arr);
return dfr;
}
public async read (): Promise<T> {
if (this.data != null) {
return Promise.resolve(this.data);
}
try {
let data = await this.readInner();
return this.data = data;
} catch (error) {
error.message = `${this.path}: ${error.message}`;
throw error;
}
}
.deco.memoize({ perInstance: true })
private async readInner () {
let exists = await this.transport.exists();
if (exists === false) {
return this.$default;
}
let str = await this.transport.readAsync();
if (str == null) {
return this.$default;
}
let data = this.decode(str);
if (this.watcherFn != null && this.watching === false) {
this.transport.watch(this.watcherFn);
this.watching = true;
}
return data;
}
private async writeInner (data: T) {
let v = this.version;
let str = this.encode(data);
try {
await this.transport.writeAsync(str);
this.lock.resolve();
this.callWriteListeners(v, null);
} catch (error) {
console.error(`JsonStoreFs.WriteInner> ${this.path}`, error);
this.errored = error;
this.callWriteListeners(v, error);
} finally {
if (this.pending == null) {
this.busy = false;
return;
}
let next = this.pending;
this.pending = null;
this.writeInner(next);
}
}
private callWriteListeners (v: number, error = null) {
for (let i = 0; i < this.listeners.length; i++) {
let x = this.listeners[i];
if (x.version <= v) {
try {
if (error != null) {
x.promise.reject(error);
} else {
x.promise.resolve();
}
} finally {
this.listeners.splice(i, 1);
i--;
}
}
}
}
private decode (mix: string | any) {
let isCsv = this.path.endsWith('.csv');
if (isCsv) {
}
let data = typeof mix ==='string'
? JSON.parse(mix)
: mix;
let { Type, mapFn } = this;
if (Type) {
data = Array.isArray(data)
? data.map(x => JsonConvert.toJSON(x, { Type }))
: JsonConvert.toJSON(data, { Type });
}
if (mapFn) {
data = Array.isArray(data)
? data.map(mapFn)
: mapFn(data);
}
return data;
}
private encode (data: any) {
let { Type, format, serializeFn } = this;
if (Type) {
data = Array.isArray(data)
? data.map(x => JsonConvert.toJSON(x, { Type }))
: JsonConvert.toJSON(data, { Type });
}
if (serializeFn) {
data = Array.isArray(data)
? data.map(serializeFn)
: serializeFn(data);
}
return JSON.stringify(data, null, format ? ' ' : null)
}
}
interface ITransport {
writeAsync(str: string): Promise<void>
readAsync(): Promise<string>
watch (watcherFn: (path?: string) => any)
unwatch (watcherFn: (path?: string) => any)
cleanCache()
exists (): Promise<boolean>
}
class FileTransport implements ITransport {
private file: InstanceType<typeof File>;
constructor (public path: string) {
const FileCtor = FileSafe ?? File
this.file = new FileCtor(this.path, {
cached: false,
processSafe: true,
threadSafe: true,
});
}
async exists(): Promise<boolean> {
return File.existsAsync (this.path);
}
async writeAsync(str: string) {
await this.file.writeAsync(str, { skipHooks: true });
}
async readAsync(): Promise<string> {
let str = await this.file.readAsync <string> ({
skipHooks: true,
encoding: 'utf8',
cached: false
});
return str;
}
async watch (watcherFn: (path?: string) => any) {
File.watch(this.path, watcherFn);
}
async unwatch (watcherFn: (path?: string) => any) {
File.unwatch(this.path, watcherFn);
}
async cleanCache () {
(this.file as any).pending = null;
this.file.content = null;
}
}
class LocalStorageTransport implements ITransport {
private global = $ref.getGlobal();
private listener: (event) => void;
constructor (public path: string) {
$require.notNull(this.global.localStorage, `LocalStorage is not available`);
}
async writeAsync(str: string) {
this.global.localStorage.setItem(this.path, str);
}
async readAsync(): Promise<string> {
return this.global.localStorage.getItem(this.path);
}
async exists (): Promise<boolean> {
// To prevent double load with "getItem", assume the existence of the item, later in readAsync the NULL will be handled.
return true;
}
async watch (watcherFn: (path?: string) => any) {
this.listener ??= (event) => {
if (event.key === this.path) {
watcherFn(this.path);
}
};
this.global.addEventListener('storage', this.listener, false);
}
async unwatch (watcherFn: (path?: string) => any) {
this.global.removeEventListener('storage', this.listener, false);
}
async cleanCache () {
// no cached
}
}