import { mapMap , UserError } from "@adpt/utils" ; import * as fs from "fs-extra" ; import { JsonDB } from "node-json-db" ; import * as path from "path" ; import { fileURLToPath , URL } from "url" ; import { lock } from "../utils/lockfile" ; import { HistoryStore } from "./history" ; import { createLocalHistoryStore } from "./local_history" ; import { $serverLock , AdaptServer , DeleteOptions , GetOptions , ServerLock , ServerOptions , ServerPathExists , SetOptions , } from "./server" ; import { Locker , ServerBase } from "./server_base" ; export interface LocalServerOptions extends ServerOptions { init ? : boolean ; } export interface FileLock extends ServerLock { release : ( ) => Promise < void > ; } export class FileLocker implements Locker < FileLock > { constructor ( public filename ) { } async lock ( ) { return { release : await lock ( this . filename , "Adapt server lock" ) , [ $serverLock ] : true , } ; } async unlock ( l ) { await l . release ( ) ; } } export const dbFilename = "adapt_local.json" ; const defaultOptions = { init : false , } ; const currentVersion = 0 ; const openDbs = new Map < string , JsonDB > ( ) ; export class LocalServer extends ServerBase < FileLock > implements AdaptServer { static urlMatch = /^file:/ ; private db : JsonDB ; private rootDir : string ; private filename : string ; private options : LocalServerOptions ; private historyStores : Map < string , HistoryStore > ; private url : string ; constructor ( url , options ) { const pathname = fileURLToPath ( url ) ; const rootDir = path . resolve ( pathname ) ; const filename = path . join ( rootDir , dbFilename ) ; super ( new FileLocker ( filename ) ) ; this . rootDir = rootDir ; this . filename = filename ; this . historyStores = new Map < string , HistoryStore > ( ) ; this . options = { ... defaultOptions , ... options } ; this . url = url . href ; } async init ( ) { const alreadyOpen = openDbs . get ( this . filename ) ; if ( alreadyOpen !== undefined ) { this . db = alreadyOpen ; return ; } let rootStat ; try { rootStat = await fs . stat ( this . rootDir ) ; } catch ( err ) { if ( err . code !== "ENOENT" ) throw err ; } if ( rootStat && ! rootStat . isDirectory ( ) ) { return throwServerUrlError ( this . url , `'${ this . rootDir }' is not a directory` ) ; } if ( this . options . init === true && rootStat === undefined ) { await fs . ensureDir ( this . rootDir ) ; } const exists = await fs . pathExists ( this . filename ) ; if ( exists === false && this . options . init === false ) { return throwServerUrlError ( this . url , `'${ dbFilename }' does not exist` ) ; } this . db = new JsonDB ( this . filename , true , true ) ; if ( exists ) { let ver = null ; try { ver = this . db . getData ( "/adaptLocalServerVersion" ) ; } catch ( err ) { } if ( ver !== currentVersion ) { return throwServerUrlError ( this . url , `'${ dbFilename }' is not a valid local server file (ver=${ ver })` ) ; } } else { this . db . push ( "/adaptLocalServerVersion" , currentVersion ) ; } openDbs . set ( this . filename , this . db ) ; } async destroy ( ) { const promises = mapMap ( this . historyStores , ( _ , s ) => s . destroy ( ) ) ; await Promise . all ( promises ) ; } async set ( dataPath , val , options = { } ) { await this . withLock ( options , async ( ) => { this . db . reload ( ) ; if ( options . mustCreate ) { try { this . db . getData ( dataPath ) ; throw new ServerPathExists ( dataPath ) ; } catch ( err ) { if ( err . name !== "DataError" ) throw err ; } } this . db . push ( dataPath , val ) ; } ) ; } async get ( dataPath , options = { } ) { return this . withLock ( options , ( ) => { this . db . reload ( ) ; return this . db . getData ( dataPath ) ; } ) ; } async delete ( dataPath , options = { } ) { await this . withLock ( options , async ( ) => { this . db . reload ( ) ; this . db . delete ( dataPath ) ; } ) ; } async historyStore ( dataPath , init ) { let store = this . historyStores . get ( dataPath ) ; if ( store ) return store ; store = await createLocalHistoryStore ( this . db , dataPath , path . join ( this . rootDir , dataPath ) , init ) ; const origDestroy = store . destroy ; store . destroy = async ( ) => { this . historyStores . delete ( dataPath ) ; await origDestroy . call ( store ) ; } ; this . historyStores . set ( dataPath , store ) ; return store ; } } function throwServerUrlError ( url , message ) { throw new UserError ( `Invalid Adapt Server URL '${ url }': ${ message }` ) ; }