From 9f72850590bb897ad755a7ee94df1c254d3d5bd2 Mon Sep 17 00:00:00 2001 From: Roj Date: Mon, 1 Jan 2024 22:18:24 +0300 Subject: [PATCH] 1.0.3 --- package.json | 4 +- src/index.ts | 71 +++++++++++++++++++++++++-------- src/utilities.ts | 101 ++++++++++++++++++++++++++++++++++------------- 3 files changed, 130 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 24239b3..30c6786 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mtkruto/storage-local-storage", - "version": "1.0.2", + "version": "1.0.3", "description": "", "main": "dist/index.js", "license": "MIT", @@ -9,7 +9,7 @@ }, "devDependencies": { "typescript": "^5.2.2", - "@mtkruto/node": "^0.1.107", + "@mtkruto/node": "^0.1.138", "@types/node-localstorage": "^1.3.2", "node-localstorage": "^3.0.5", "rimraf": "^5.0.5" diff --git a/src/index.ts b/src/index.ts index 4bb5c6e..0d9448d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,46 +1,79 @@ -import { Storage, StorageKeyPart } from "@mtkruto/node"; +import { GetManyFilter, Storage, StorageKeyPart } from "@mtkruto/node"; import { LocalStorage } from "node-localstorage"; -import { fromString, toString } from "./utilities"; +import { fromString, isInRange, toString } from "./utilities"; export class StorageLocalStorage extends Storage implements Storage { - constructor( - private readonly prefix: string, - private readonly localStorage = new LocalStorage(".mtkruto"), - ) { + readonly #prefix: string; + readonly #localStorage: LocalStorage; + + constructor(prefix: string, localStorage?: LocalStorage) { if (prefix.length <= 0) { throw new Error("Empty prefix"); } else if (!/^[0-9a-zA-Z]+$/.test(prefix)) { throw new Error("Unallowed prefix"); } super(); + this.#prefix = prefix; + this.#localStorage = localStorage ?? new LocalStorage(".mtkruto"); + } + + get prefix() { + return this.#prefix; + } + + branch(id: string) { + return new StorageLocalStorage(this.prefix + "S__" + id); + } + + initialize() { } - init() { + get supportsFiles() { + return false; } - get(key_: readonly StorageKeyPart[]) { + get(key_: readonly StorageKeyPart[]) { const key = this.prefix + toString(key_); - const value = this.localStorage.getItem(key); + const value = this.#localStorage.getItem(key); if (value != null) { - return fromString(value); + return fromString(value); } else { return null; } } - *getMany(prefix: readonly StorageKeyPart[]) { - for (let [key, value] of Object.entries(this.localStorage)) { + *getMany( + filter: GetManyFilter, + params?: { limit?: number; reverse?: boolean }, + ) { + let entries = Object.entries(localStorage).sort(([a], [b]) => + a.localeCompare(b) + ); + if (params?.reverse) { + entries.reverse(); + } + if (params?.limit !== undefined) { + entries = entries.slice(0, params.limit <= 0 ? 1 : params.limit); + } + entries: for (let [key, value] of entries) { if (key.startsWith(this.prefix)) { key = key.slice(this.prefix.length); } const parts = fromString(key); if (Array.isArray(parts)) { - for (const [i, p] of prefix.entries()) { - if (toString(p) != toString(parts[i])) { + if ("prefix" in filter) { + for (const [i, p] of filter.prefix.entries()) { + if (toString(p) != toString(parts[i])) { + continue entries; + } + } + } else { + if (!isInRange(parts, filter.start, filter.end)) { continue; } - yield [parts, fromString(value)] as [readonly StorageKeyPart[], T]; } + + yield [parts, fromString(value)] as [readonly StorageKeyPart[], T]; } } } @@ -48,9 +81,13 @@ export class StorageLocalStorage extends Storage implements Storage { set(key_: readonly StorageKeyPart[], value: unknown) { const key = this.prefix + toString(key_); if (value != null) { - this.localStorage.setItem(key, toString(value)); + this.#localStorage.setItem(key, toString(value)); } else { - this.localStorage.removeItem(key); + this.#localStorage.removeItem(key); } } + + incr(key: readonly StorageKeyPart[], by: number) { + this.set(key, (this.get(key) || 0) + by); + } } diff --git a/src/utilities.ts b/src/utilities.ts index 4426567..19bdbad 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -1,3 +1,5 @@ +import { StorageKeyPart } from "@mtkruto/node"; + function UNREACHABLE(): never { throw new Error("Unreachable"); } @@ -11,45 +13,90 @@ export enum ValueType { Uint8Array, Array, } + export function toString(value: unknown): string { if (typeof value === "boolean") { - return JSON.stringify([ValueType.Boolean, value]); + return `${ValueType.Boolean}${Number(value)}`; } else if (typeof value === "number") { - return JSON.stringify([ValueType.Number, value]); + return `${ValueType.Number}${value}`; } else if (typeof value === "string") { - return JSON.stringify([ValueType.String, value]); + return `${ValueType.String}${value}`; } else if (typeof value == "bigint") { - return JSON.stringify([ValueType.BigInt, String(value)]); + return `${ValueType.BigInt}${value}`; } else if (value instanceof Date) { - return JSON.stringify([ValueType.Date, value.getTime()]); + return `${ValueType.Date}${value.getTime()}`; } else if (value instanceof Uint8Array) { - return JSON.stringify([ - ValueType.Uint8Array, - Buffer.from(value).toString("base64"), - ]); + return `${ValueType.Uint8Array}${Buffer.from(value).toString("base64")}`; } else if (Array.isArray(value)) { - return JSON.stringify([ValueType.Array, value.map(toString)]); + const items = value.map((v) => { + if (typeof v === "string" || v instanceof Uint8Array || Array.isArray(v)) { + const s = toString(v).slice(1); + return String(typeof v === "string" ? ValueType.String : v instanceof Uint8Array ? ValueType.Uint8Array : ValueType.Array) + toString(s.length).slice(1) + "\n" + s; + } else { + return toString(v); + } + }); + return `${ValueType.Array}${items.join("\n")}`; } else { UNREACHABLE(); } } -export function fromString(string: string) { - const [type, value] = JSON.parse(string); - if ( - type == ValueType.Boolean || type == ValueType.Number || - type == ValueType.String - ) { - return value as T; - } else if (type == ValueType.BigInt) { - return BigInt(value) as T; - } else if (type == ValueType.Date) { - return new Date(value) as T; - } else if (type == ValueType.Uint8Array) { - return Buffer.from(value, "base64"); - } else if (type == ValueType.Array) { - return value.map(fromString); - } else { - UNREACHABLE(); +export function fromString(string: string): T { + const [type, value] = [Number(string[0]) as ValueType, string.slice(1)]; + switch (type) { + case ValueType.Boolean: + return Boolean(Number(value)) as T; + case ValueType.Number: + return Number(value) as T; + case ValueType.String: + return value as T; + case ValueType.BigInt: + return BigInt(value) as T; + case ValueType.Date: + return new Date(Number(value)) as T; + case ValueType.Uint8Array: + return Buffer.from(value, 'base64') as T; + case ValueType.Array: { + const arr = []; + for (let i = 0; i < value.length; ++i) { + const type = Number(value[i]) as ValueType; + let value_ = ""; + while (value[++i] != "\n") { + value_ += value[i]; + if (i == value.length - 1) { + break; + } + } + + switch (type) { + case ValueType.String: + case ValueType.Uint8Array: + case ValueType.Array: { + const len = Number(value_); + ++i; + value_ = value.slice(i, i + Number(value_)); + i += len; + } + } + arr.push(fromString(`${type}${value_}`)); + } + return arr as T; + } + } +} + +export function isInRange(key: StorageKeyPart[], start: readonly StorageKeyPart[], end: readonly StorageKeyPart[]) { + for (const [i, part] of key.entries()) { + const left = start[i]; + const right = end[i]; + if (left === undefined || right === undefined) { + return false; + } + if (part >= left && part <= right) { + continue; + } + return false; } + return true; }