Skip to content

Commit 772770f

Browse files
authored
Merge pull request #4 from script-development/feat/fs-storage
feat(storage): add fs-storage package
2 parents a9c1741 + 80b7302 commit 772770f

10 files changed

Lines changed: 1015 additions & 16 deletions

File tree

package-lock.json

Lines changed: 513 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/storage/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# @script-development/fs-storage
2+
3+
## 0.1.0
4+
5+
### Minor Changes
6+
7+
- aafcb7a: Add fs-storage package — framework-agnostic localStorage service factory with prefix namespacing, JSON serialization, and QuotaExceededError handling.

packages/storage/package.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "@script-development/fs-storage",
3+
"version": "0.1.0",
4+
"description": "Framework-agnostic localStorage service factory with prefix namespacing",
5+
"license": "UNLICENSED",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/script-development/fs-packages.git",
9+
"directory": "packages/storage"
10+
},
11+
"files": [
12+
"dist"
13+
],
14+
"type": "module",
15+
"main": "./dist/index.cjs",
16+
"module": "./dist/index.mjs",
17+
"types": "./dist/index.d.mts",
18+
"exports": {
19+
".": {
20+
"import": {
21+
"types": "./dist/index.d.mts",
22+
"default": "./dist/index.mjs"
23+
},
24+
"require": {
25+
"types": "./dist/index.d.cts",
26+
"default": "./dist/index.cjs"
27+
}
28+
}
29+
},
30+
"publishConfig": {
31+
"access": "public",
32+
"registry": "https://registry.npmjs.org"
33+
},
34+
"scripts": {
35+
"build": "tsdown",
36+
"typecheck": "tsc --noEmit",
37+
"lint:pkg": "publint && attw --pack",
38+
"test": "vitest run",
39+
"test:coverage": "vitest run --coverage"
40+
},
41+
"devDependencies": {
42+
"jsdom": "^29.0.1"
43+
}
44+
}

packages/storage/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { createStorageService } from "./storage";
2+
export type { StorageService, Get } from "./types";

packages/storage/src/storage.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { StorageService, Get } from "./types";
2+
3+
export const createStorageService = (prefix: string): StorageService => {
4+
const prefixKey = (key: string): string => `${prefix}:${key}`;
5+
6+
const put = (key: string, value: unknown): void => {
7+
const valueToStore = typeof value === "string" ? value : JSON.stringify(value);
8+
9+
try {
10+
localStorage.setItem(prefixKey(key), valueToStore);
11+
} catch (error) {
12+
if (error instanceof DOMException && error.name === "QuotaExceededError") {
13+
console.error("localStorage quota exceeded");
14+
return;
15+
}
16+
console.error(error);
17+
}
18+
};
19+
20+
const get: Get = <T>(key: string, defaultValue?: T): T | undefined => {
21+
let storedValue: string | null;
22+
try {
23+
storedValue = localStorage.getItem(prefixKey(key));
24+
} catch (error) {
25+
console.error(error);
26+
return defaultValue;
27+
}
28+
29+
if (storedValue === null) return defaultValue;
30+
31+
if (typeof defaultValue === "string") return storedValue as T;
32+
33+
try {
34+
return JSON.parse(storedValue) as T;
35+
} catch {
36+
return storedValue as unknown as T;
37+
}
38+
};
39+
40+
const remove = (key: string): void => {
41+
try {
42+
localStorage.removeItem(prefixKey(key));
43+
} catch (error) {
44+
console.error(error);
45+
}
46+
};
47+
48+
const clear = (): void => {
49+
const keysToRemove: string[] = [];
50+
for (let i = 0; i < localStorage.length; i++) {
51+
const key = localStorage.key(i);
52+
if (key?.startsWith(`${prefix}:`)) {
53+
keysToRemove.push(key);
54+
}
55+
}
56+
for (const key of keysToRemove) {
57+
try {
58+
localStorage.removeItem(key);
59+
} catch (error) {
60+
console.error(error);
61+
}
62+
}
63+
};
64+
65+
return { put, get, remove, clear };
66+
};

packages/storage/src/types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Retrieve a value from storage.
3+
*
4+
* @remarks
5+
* Parsing behavior depends on the default value type:
6+
* - No default or non-string default: Attempts JSON.parse, falls back to raw string on failure
7+
* - String default: Returns the raw stored string without JSON parsing
8+
*
9+
* This prevents unintended conversions (e.g., stored "5e3" becoming number 5000).
10+
*
11+
* @example
12+
* ```ts
13+
* storage.put('key', '5e3');
14+
* storage.get('key'); // Returns number 5000 (JSON parsed)
15+
* storage.get('key', ''); // Returns string "5e3" (raw)
16+
* ```
17+
*/
18+
export interface Get {
19+
<T>(key: string): T | undefined;
20+
<T>(key: string, defaultValue: T): T;
21+
}
22+
23+
export interface StorageService {
24+
put: (key: string, value: unknown) => void;
25+
get: Get;
26+
remove: (key: string) => void;
27+
clear: () => void;
28+
}

0 commit comments

Comments
 (0)