memory-card
Version:
ES6 Map like Async API, with Swagger API Backend Support.
483 lines (388 loc) • 11.2 kB
text/typescript
/* eslint-disable no-use-before-define */
import type { AsyncMapLike } from 'async-map-like'
import {
log,
VERSION,
} from './config.js'
import {
getStorage,
StorageBackend,
StorageBackendOptions,
} from './storage/mod.js'
import type {
MemoryCardPayload,
} from './types.js'
export const NAMESPACE_MULTIPLEX_SEPRATOR = '\r'
export const NAMESPACE_KEY_SEPRATOR = '\n'
const NAMESPACE_MULTIPLEX_SEPRATOR_REGEX = new RegExp(NAMESPACE_MULTIPLEX_SEPRATOR)
const NAMESPACE_KEY_SEPRATOR_REGEX = new RegExp(NAMESPACE_KEY_SEPRATOR)
export interface MemoryCardOptions {
name? : string,
storageOptions? : StorageBackendOptions,
// //////////
multiplex?: {
parent : MemoryCard,
name : string,
}
}
export interface MemoryCardJsonObject {
payload: MemoryCardPayload,
options: MemoryCardOptions,
}
export class MemoryCard implements AsyncMapLike<any, any> {
/**
*
*
* Static
*
*
*/
static VERSION = VERSION
static fromJSON (textOrObj: string | MemoryCardJsonObject): AsyncMapLike<any, any> {
log.verbose('MemoryCard', 'static fromJSON(...)')
let jsonObj: MemoryCardJsonObject
if (typeof textOrObj === 'string') {
jsonObj = JSON.parse(textOrObj)
} else {
jsonObj = textOrObj
}
const card = new this(jsonObj.options)
card.payload = jsonObj.payload
return card
}
protected static multiplex<T extends typeof MemoryCard> (
this: T,
memory : MemoryCard,
name : string,
): T['prototype'] {
log.verbose('MemoryCard', 'static multiplex(%s, %s)', memory, name)
// if (!memory.options) {
// throw new Error('can not multiplex a un-named MemoryCard')
// }
const subMemory = new this({
...memory.options,
multiplex: {
name,
parent: memory,
},
})
return subMemory
}
/**
*
*
* Instance
*
*
*/
name?: string
protected parent? : MemoryCard
protected payload? : MemoryCardPayload
protected storage? : StorageBackend
protected multiplexNameList : string[]
private options?: MemoryCardOptions
constructor (
options?: string | MemoryCardOptions,
) {
log.verbose('MemoryCard', 'constructor(%s)',
JSON.stringify(options),
)
if (typeof options === 'string') {
options = { name: options }
}
this.options = options
this.name = options && options.name
if (options && options.multiplex) {
this.parent = options.multiplex.parent
this.payload = this.parent.payload
this.multiplexNameList = [
...this.parent.multiplexNameList,
options.multiplex.name,
]
} else {
// payload should be undefined before load()
this.payload = undefined
this.multiplexNameList = []
}
}
toString () {
let mpString = ''
if (this.multiplexNameList.length > 0) {
mpString = this.multiplexNameList
.map(mpName => `.multiplex(${mpName})`)
.join('')
}
return `MemoryCard<${this.options?.name ?? ''}>${mpString}`
}
version (): string {
return VERSION
}
private async getStorage (): Promise<undefined | StorageBackend> {
log.verbose('MemoryCard', 'getStorage() for storage type: %s',
(this.options
&& this.options.storageOptions
&& this.options.storageOptions.type
) || 'N/A',
)
if (!this.options) {
return
}
const storage = await getStorage(
this.options.name,
this.options.storageOptions,
)
return storage
}
async load (): Promise<void> {
log.verbose('MemoryCard', 'load() from storage: %s', this.storage || 'N/A')
if (this.isMultiplex()) {
log.verbose('MemoryCard', 'load() should not be called on a multiplex MemoryCard. NOOP')
return
}
if (this.payload) {
throw new Error('memory had already loaded before.')
}
if (!this.storage) {
this.storage = await this.getStorage()
}
if (this.storage) {
this.payload = await this.storage.load()
} else {
log.verbose('MemoryCard', 'load() no storage')
this.payload = {}
}
}
async save (): Promise<void> {
log.verbose('MemoryCard', 'save()')
if (this.isMultiplex()) {
if (!this.parent) {
throw new Error('multiplex memory no parent')
}
return this.parent.save()
}
log.verbose('MemoryCard', '<%s>%s save() to %s',
this.name || '',
this.multiplexPath(),
this.storage || 'N/A',
)
if (!this.payload) {
throw new Error('no payload, please call load() first.')
}
if (!this.storage) {
log.verbose('MemoryCard', 'save() no storage, NOOP')
return
}
await this.storage.save(this.payload)
}
/**
*
* Multiplexing related functions START
*
*/
protected isMultiplexKey (key: string): boolean {
if (NAMESPACE_MULTIPLEX_SEPRATOR_REGEX.test(key)
&& NAMESPACE_KEY_SEPRATOR_REGEX.test(key)
) {
const namespace = this.multiplexNamespace()
return key.startsWith(namespace)
}
return false
}
protected multiplexNamespace (): string {
if (!this.isMultiplex()) {
throw new Error('not a multiplex memory')
}
const namespace = NAMESPACE_MULTIPLEX_SEPRATOR
+ this.multiplexNameList.join(NAMESPACE_MULTIPLEX_SEPRATOR)
return namespace
}
protected resolveKey (name: string): string {
if (this.isMultiplex()) {
const namespace = this.multiplexNamespace()
return [
namespace,
name,
].join(NAMESPACE_KEY_SEPRATOR)
} else {
return name
}
}
isMultiplex (): boolean {
return this.multiplexNameList.length > 0
}
protected multiplexPath (): string {
return this.multiplexNameList.join('/')
}
multiplex (name: string): this {
log.verbose('MemoryCard', 'multiplex(%s)', name)
// FIXME: as any ?
return (this.constructor as any).multiplex(this, name)
}
/**
*
* Multiplexing related functions END
*
*/
async destroy (): Promise<void> {
log.verbose('MemoryCard', 'destroy() storage: %s', this.storage || 'N/A')
if (this.isMultiplex()) {
throw new Error('can not destroy on a multiplexed memory')
}
await this.clear()
if (this.storage) {
await this.storage.destroy()
this.storage = undefined
}
// to prevent to use a destroied card
this.payload = undefined
}
/**
*
* ES6 Map API (Async Version)
*
* BEGIN
*
*/
/**
* size
*/
get size (): Promise<number> {
log.verbose('MemoryCard', '<%s> size', this.multiplexPath())
if (!this.payload) {
throw new Error('no payload, please call load() first.')
}
let count
if (this.isMultiplex()) {
count = Object.keys(this.payload)
.filter(key => this.isMultiplexKey(key))
.length
} else {
count = Object.keys(this.payload).length
}
return Promise.resolve(count)
}
async get<T = any> (name: string): Promise<undefined | T> {
log.verbose('MemoryCard', '<%s> get(%s)', this.multiplexPath(), name)
if (!this.payload) {
throw new Error('no payload, please call load() first.')
}
const key = this.resolveKey(name)
return this.payload[key] as any
}
async set<T = any> (name: string, data: T): Promise<this> {
log.verbose('MemoryCard', '<%s> set(%s, %s)', this.multiplexPath(), name, data)
if (!this.payload) {
throw new Error('no payload, please call load() first.')
}
const key = this.resolveKey(name)
this.payload[key] = data as any
return this
}
async * [Symbol.asyncIterator]<T = any> (): AsyncIterableIterator<[string, T]> {
log.verbose('MemoryCard', '<%s> *[Symbol.asyncIterator]()', this.multiplexPath())
yield * this.entries()
}
async * entries<T = any> (): AsyncIterableIterator<[string, T]> {
log.verbose('MemoryCard', '<%s> *entries()', this.multiplexPath())
if (!this.payload) {
throw new Error('no payload, please call load() first.')
}
for await (const relativeKey of this.keys()) {
const absoluteKey = this.resolveKey(relativeKey)
const data: T = this.payload[absoluteKey] as any
const pair: [string, T] = [relativeKey, data]
yield pair
}
}
async clear (): Promise<void> {
log.verbose('MemoryCard', '<%s> clear()', this.multiplexPath())
if (!this.payload) {
throw new Error('no payload, please call load() first.')
}
if (this.isMultiplex()) {
for (const key in this.payload) {
if (this.isMultiplexKey(key)) {
delete this.payload[key]
}
}
} else {
this.payload = {}
}
}
async delete (name: string): Promise<boolean> {
log.verbose('MemoryCard', '<%s> delete(%s)', this.multiplexPath(), name)
if (!this.payload) {
throw new Error('no payload, please call load() first.')
}
const key = this.resolveKey(name)
if (key in this.payload) {
delete this.payload[key]
return true
} else {
return false
}
}
async has (key: string): Promise<boolean> {
log.verbose('MemoryCard', '<%s> has(%s)', this.multiplexPath(), key)
if (!this.payload) {
throw new Error('no payload, please call load() first.')
}
const absoluteKey = this.resolveKey(key)
return absoluteKey in this.payload
}
async * keys (): AsyncIterableIterator<string> {
log.verbose('MemoryCard', '<%s> keys()', this.multiplexPath())
if (!this.payload) {
throw new Error('no payload, please call load() first.')
}
for (const key of Object.keys(this.payload)) {
// console.log('key', key)
if (this.isMultiplex()) {
if (this.isMultiplexKey(key)) {
const namespace = this.multiplexNamespace()
// `+1` means there's another NAMESPACE_KEY_SEPRATOR we need to trim
const mpKey = key.substr(namespace.length + 1)
yield mpKey
}
continue
}
yield key
}
}
async * values<T = any> (): AsyncIterableIterator<T> {
log.verbose('MemoryCard', '<%s> values()', this.multiplexPath())
if (!this.payload) {
throw new Error('no payload, please call load() first.')
}
for await (const relativeKey of this.keys()) {
const absoluteKey = this.resolveKey(relativeKey)
yield this.payload[absoluteKey] as any
}
}
async forEach<T> (
callbackfn: (
value: T,
key: string,
map: any,
) => void,
thisArg?: any,
): Promise<void> {
for await (const [key, value] of this.entries()) {
callbackfn.call(thisArg, value, key, this)
}
}
/**
* Huan(202108): FIXME
* I totally forget what are the following two methods are...
*/
[Symbol.iterator] () : AsyncIterableIterator<[string, number]> { return {} as any }
get [Symbol.toStringTag] () { return Promise.resolve('MemoryCardStringTag') }
/**
*
* ES6 Map API (Async Version)
*
* END
*
*/
}
export default MemoryCard