Skip to content
This repository has been archived by the owner on Nov 15, 2024. It is now read-only.

Commit

Permalink
1.0.3
Browse files Browse the repository at this point in the history
  • Loading branch information
rojvv committed Jan 1, 2024
1 parent 403d2ec commit 9f72850
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 46 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mtkruto/storage-local-storage",
"version": "1.0.2",
"version": "1.0.3",
"description": "",
"main": "dist/index.js",
"license": "MIT",
Expand All @@ -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"
Expand Down
71 changes: 54 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,93 @@
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<T>(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<T>(value);
} else {
return null;
}
}

*getMany<T>(prefix: readonly StorageKeyPart[]) {
for (let [key, value] of Object.entries(this.localStorage)) {
*getMany<T>(
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];
}
}
}

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<number>(key) || 0) + by);
}
}
101 changes: 74 additions & 27 deletions src/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { StorageKeyPart } from "@mtkruto/node";

function UNREACHABLE(): never {
throw new Error("Unreachable");
}
Expand All @@ -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<T>(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<T>(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;
}

0 comments on commit 9f72850

Please sign in to comment.