@biruframework/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/auth/index.js +93 -0
- package/base.js +78 -0
- package/index.js +10 -0
- package/network/errors/HttpError.js +15 -0
- package/network/errors/NetworkError.js +12 -0
- package/network/http/createHttpClient.js +114 -0
- package/network/index.js +3 -0
- package/package.json +9 -0
- package/routes/index.js +38 -0
- package/state/index.js +90 -0
- package/storage/index.js +90 -0
package/auth/index.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { AppError } from '../base.js';
|
|
2
|
+
import { createJsonStorage } from '../storage/index.js';
|
|
3
|
+
|
|
4
|
+
export class AuthError extends AppError {
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} message
|
|
7
|
+
* @param {{ cause?: unknown, meta?: Record<string, unknown> }=} options
|
|
8
|
+
*/
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super('AUTH_ERROR', message, options);
|
|
11
|
+
this.name = 'AuthError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{
|
|
17
|
+
* accessToken: string,
|
|
18
|
+
* refreshToken?: string,
|
|
19
|
+
* expiresAt?: string,
|
|
20
|
+
* user?: Record<string, unknown>,
|
|
21
|
+
* }} AuthSession
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {{ username: string, password: string }} LoginCredentials
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {{ login: string, logout?: string, me?: string }} AuthEndpoints
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {{
|
|
34
|
+
* httpClient: { request?: Function, get?: Function, post: Function },
|
|
35
|
+
* storage: { getItem: Function, setItem: Function, removeItem: Function, clear: Function },
|
|
36
|
+
* endpoints: AuthEndpoints,
|
|
37
|
+
* sessionStorageKey?: string,
|
|
38
|
+
* }} CreateAuthServiceOptions
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {CreateAuthServiceOptions} options
|
|
43
|
+
*/
|
|
44
|
+
export function createAuthService(options) {
|
|
45
|
+
const sessionKey = options.sessionStorageKey ?? 'auth.session';
|
|
46
|
+
const sessionStorage = createJsonStorage(options.storage, sessionKey);
|
|
47
|
+
|
|
48
|
+
/** @returns {Promise<AuthSession | null>} */
|
|
49
|
+
async function getSession() {
|
|
50
|
+
return await sessionStorage.get();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @param {AuthSession | null} session */
|
|
54
|
+
async function setSession(session) {
|
|
55
|
+
if (session == null) {
|
|
56
|
+
await sessionStorage.remove();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
await sessionStorage.set(session);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @param {LoginCredentials} credentials */
|
|
63
|
+
async function login(credentials) {
|
|
64
|
+
try {
|
|
65
|
+
const response = await options.httpClient.post(options.endpoints.login, {
|
|
66
|
+
body: credentials,
|
|
67
|
+
});
|
|
68
|
+
const session = response.data;
|
|
69
|
+
if (!session?.accessToken) throw new AuthError('Login response missing accessToken');
|
|
70
|
+
await setSession(session);
|
|
71
|
+
return session;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e instanceof AppError) throw e;
|
|
74
|
+
throw new AuthError('Login failed', { cause: e });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function logout() {
|
|
79
|
+
try {
|
|
80
|
+
const currentSession = await getSession();
|
|
81
|
+
await setSession(null);
|
|
82
|
+
if (!options.endpoints.logout) return;
|
|
83
|
+
if (!currentSession?.accessToken) return;
|
|
84
|
+
await options.httpClient.post(options.endpoints.logout, {
|
|
85
|
+
headers: { Authorization: `Bearer ${currentSession.accessToken}` },
|
|
86
|
+
});
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { getSession, setSession, login, logout };
|
|
93
|
+
}
|
package/base.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template T
|
|
3
|
+
* @typedef {Record<string, T>} Dictionary
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @typedef {null | undefined} Nil */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @template T
|
|
10
|
+
* @typedef {{ ok: true, value: T }} Ok
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @template {Error} [E=Error]
|
|
15
|
+
* @typedef {{ ok: false, error: E }} Err
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @template T
|
|
20
|
+
* @template {Error} [E=Error]
|
|
21
|
+
* @typedef {Ok<T> | Err<E>} Result
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @template T
|
|
26
|
+
* @param {T} value
|
|
27
|
+
* @returns {Ok<T>}
|
|
28
|
+
*/
|
|
29
|
+
export function ok(value) {
|
|
30
|
+
return { ok: true, value };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @template {Error} E
|
|
35
|
+
* @param {E} error
|
|
36
|
+
* @returns {Err<E>}
|
|
37
|
+
*/
|
|
38
|
+
export function err(error) {
|
|
39
|
+
return { ok: false, error };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class AppError extends Error {
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} code
|
|
45
|
+
* @param {string} message
|
|
46
|
+
* @param {{ cause?: unknown, meta?: Dictionary<unknown> }=} options
|
|
47
|
+
*/
|
|
48
|
+
constructor(code, message, options) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'AppError';
|
|
51
|
+
this.code = code;
|
|
52
|
+
this.cause = options?.cause;
|
|
53
|
+
this.meta = options?.meta;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {{
|
|
59
|
+
* debug: (message: string, meta?: Dictionary<unknown>) => void,
|
|
60
|
+
* info: (message: string, meta?: Dictionary<unknown>) => void,
|
|
61
|
+
* warn: (message: string, meta?: Dictionary<unknown>) => void,
|
|
62
|
+
* error: (message: string, meta?: Dictionary<unknown>) => void,
|
|
63
|
+
* }} Logger
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string=} prefix
|
|
68
|
+
* @returns {Logger}
|
|
69
|
+
*/
|
|
70
|
+
export function createConsoleLogger(prefix = 'app') {
|
|
71
|
+
const p = `[${prefix}]`;
|
|
72
|
+
return {
|
|
73
|
+
debug: (m, meta) => console.debug(p, m, meta ?? ''),
|
|
74
|
+
info: (m, meta) => console.info(p, m, meta ?? ''),
|
|
75
|
+
warn: (m, meta) => console.warn(p, m, meta ?? ''),
|
|
76
|
+
error: (m, meta) => console.error(p, m, meta ?? ''),
|
|
77
|
+
};
|
|
78
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @biruframework/core - Otak template: logic platform-agnostic (auth, network, storage, state, routes)
|
|
3
|
+
* Gunakan untuk React Native maupun Web.
|
|
4
|
+
*/
|
|
5
|
+
export * from './base.js';
|
|
6
|
+
export * from './network/index.js';
|
|
7
|
+
export * from './storage/index.js';
|
|
8
|
+
export * from './auth/index.js';
|
|
9
|
+
export * from './state/index.js';
|
|
10
|
+
export * from './routes/index.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AppError } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
export class HttpError extends AppError {
|
|
4
|
+
/**
|
|
5
|
+
* @param {number} status
|
|
6
|
+
* @param {string} message
|
|
7
|
+
* @param {unknown} data
|
|
8
|
+
*/
|
|
9
|
+
constructor(status, message, data) {
|
|
10
|
+
super('HTTP_ERROR', message, { meta: { status } });
|
|
11
|
+
this.name = 'HttpError';
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.data = data;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AppError } from '../../base.js';
|
|
2
|
+
|
|
3
|
+
export class NetworkError extends AppError {
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} message
|
|
6
|
+
* @param {{ cause?: unknown, meta?: Record<string, unknown> }=} options
|
|
7
|
+
*/
|
|
8
|
+
constructor(message, options) {
|
|
9
|
+
super('NETWORK_ERROR', message, options);
|
|
10
|
+
this.name = 'NetworkError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { AppError } from '../../base.js';
|
|
2
|
+
import { HttpError } from '../errors/HttpError.js';
|
|
3
|
+
import { NetworkError } from '../errors/NetworkError.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} url
|
|
7
|
+
* @param {Record<string, string | number | boolean | null | undefined>=} query
|
|
8
|
+
*/
|
|
9
|
+
function buildUrl(url, query) {
|
|
10
|
+
if (!query) return url;
|
|
11
|
+
const u = new URL(url);
|
|
12
|
+
for (const [k, v] of Object.entries(query)) {
|
|
13
|
+
if (v === null || v === undefined) continue;
|
|
14
|
+
u.searchParams.set(k, String(v));
|
|
15
|
+
}
|
|
16
|
+
return u.toString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {Response} res
|
|
21
|
+
* @returns {Promise<unknown>}
|
|
22
|
+
*/
|
|
23
|
+
async function readBody(res) {
|
|
24
|
+
const ct = res.headers.get('content-type') ?? '';
|
|
25
|
+
if (ct.includes('application/json')) return await res.json();
|
|
26
|
+
return await res.text();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {{
|
|
31
|
+
* method: 'GET'|'POST'|'PUT'|'PATCH'|'DELETE',
|
|
32
|
+
* url: string,
|
|
33
|
+
* headers?: Record<string, string>,
|
|
34
|
+
* query?: Record<string, string | number | boolean | null | undefined>,
|
|
35
|
+
* body?: unknown,
|
|
36
|
+
* timeoutMs?: number,
|
|
37
|
+
* }} HttpRequest
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @template T
|
|
42
|
+
* @typedef {{ ok: boolean, status: number, headers: Headers, data: T, raw: Response }} HttpResponse
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {{
|
|
47
|
+
* request: <T = unknown>(req: HttpRequest) => Promise<HttpResponse<T>>,
|
|
48
|
+
* get: <T = unknown>(url: string, req?: Omit<HttpRequest, 'method' | 'url'>) => Promise<HttpResponse<T>>,
|
|
49
|
+
* post: <T = unknown>(url: string, req?: Omit<HttpRequest, 'method' | 'url'>) => Promise<HttpResponse<T>>,
|
|
50
|
+
* }} HttpClient
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {{
|
|
55
|
+
* defaultHeaders?: Record<string, string>,
|
|
56
|
+
* fetchImpl?: typeof fetch,
|
|
57
|
+
* }} CreateHttpClientOptions
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {CreateHttpClientOptions=} options
|
|
62
|
+
* @returns {HttpClient}
|
|
63
|
+
*/
|
|
64
|
+
export function createHttpClient(options = {}) {
|
|
65
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
66
|
+
const defaultHeaders = options.defaultHeaders ?? {};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @template T
|
|
70
|
+
* @param {HttpRequest} req
|
|
71
|
+
* @returns {Promise<HttpResponse<T>>}
|
|
72
|
+
*/
|
|
73
|
+
async function request(req) {
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timeout = req.timeoutMs
|
|
76
|
+
? setTimeout(() => controller.abort('timeout'), req.timeoutMs)
|
|
77
|
+
: undefined;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const headers = {
|
|
81
|
+
Accept: 'application/json',
|
|
82
|
+
...defaultHeaders,
|
|
83
|
+
...(req.headers ?? {}),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const hasBody = req.body !== undefined;
|
|
87
|
+
const body =
|
|
88
|
+
hasBody && typeof req.body === 'string' ? req.body : hasBody ? JSON.stringify(req.body) : undefined;
|
|
89
|
+
if (hasBody && typeof req.body !== 'string') headers['Content-Type'] ??= 'application/json';
|
|
90
|
+
|
|
91
|
+
const res = await fetchImpl(buildUrl(req.url, req.query), {
|
|
92
|
+
method: req.method,
|
|
93
|
+
headers,
|
|
94
|
+
body,
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const data = /** @type {any} */ (await readBody(res));
|
|
99
|
+
if (!res.ok) throw new HttpError(res.status, `Request failed with status ${res.status}`, data);
|
|
100
|
+
return { ok: true, status: res.status, headers: res.headers, data, raw: res };
|
|
101
|
+
} catch (e) {
|
|
102
|
+
if (e instanceof AppError) throw e;
|
|
103
|
+
throw new NetworkError('Network request failed', { cause: e });
|
|
104
|
+
} finally {
|
|
105
|
+
if (timeout) clearTimeout(timeout);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
request,
|
|
111
|
+
get: (url, req) => request({ ...(req ?? {}), method: 'GET', url }),
|
|
112
|
+
post: (url, req) => request({ ...(req ?? {}), method: 'POST', url }),
|
|
113
|
+
};
|
|
114
|
+
}
|
package/network/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@biruframework/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Template brain: auth, network, storage, state, routes (platform-agnostic)",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"exports": { ".": "./index.js" },
|
|
7
|
+
"keywords": ["biruframework", "core", "auth", "network", "storage"],
|
|
8
|
+
"license": "ISC"
|
|
9
|
+
}
|
package/routes/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{
|
|
3
|
+
* name: string,
|
|
4
|
+
* path?: string,
|
|
5
|
+
* meta?: Record<string, unknown>,
|
|
6
|
+
* }} RouteConfig
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {RouteConfig} config
|
|
11
|
+
* @returns {RouteConfig}
|
|
12
|
+
*/
|
|
13
|
+
export function defineRoute(config) {
|
|
14
|
+
return config;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @template {Record<string, RouteConfig>} T
|
|
19
|
+
* @param {T} routes
|
|
20
|
+
* @returns {T}
|
|
21
|
+
*/
|
|
22
|
+
export function defineRoutes(routes) {
|
|
23
|
+
return routes;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} template
|
|
28
|
+
* @param {Record<string, string | number | boolean | null | undefined>} params
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function buildPath(template, params) {
|
|
32
|
+
let path = template;
|
|
33
|
+
for (const [key, value] of Object.entries(params)) {
|
|
34
|
+
const safeValue = value == null ? '' : encodeURIComponent(String(value));
|
|
35
|
+
path = path.replace(new RegExp(`:${key}\\b`, 'g'), safeValue);
|
|
36
|
+
}
|
|
37
|
+
return path;
|
|
38
|
+
}
|
package/state/index.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { AppError } from '../base.js';
|
|
2
|
+
|
|
3
|
+
export class StateError extends AppError {
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} message
|
|
6
|
+
* @param {{ cause?: unknown, meta?: Record<string, unknown> }=} options
|
|
7
|
+
*/
|
|
8
|
+
constructor(message, options) {
|
|
9
|
+
super('STATE_ERROR', message, options);
|
|
10
|
+
this.name = 'StateError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @typedef {() => void} Unsubscribe */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @template TState
|
|
18
|
+
* @typedef {(nextState: TState, prevState: TState) => void} StoreListener
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @template TState
|
|
23
|
+
* @typedef {{
|
|
24
|
+
* getState: () => TState,
|
|
25
|
+
* setState: (nextState: TState) => void,
|
|
26
|
+
* update: (updater: (current: TState) => TState) => void,
|
|
27
|
+
* subscribe: (listener: StoreListener<TState>) => Unsubscribe,
|
|
28
|
+
* }} Store
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @template TState
|
|
33
|
+
* @param {TState} initialState
|
|
34
|
+
* @returns {Store<TState>}
|
|
35
|
+
*/
|
|
36
|
+
export function createStore(initialState) {
|
|
37
|
+
let state = initialState;
|
|
38
|
+
const listeners = new Set();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {TState} nextState
|
|
42
|
+
* @param {TState} prevState
|
|
43
|
+
*/
|
|
44
|
+
function notify(nextState, prevState) {
|
|
45
|
+
for (const listener of listeners) listener(nextState, prevState);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
getState: () => state,
|
|
50
|
+
setState: (nextState) => {
|
|
51
|
+
const prevState = state;
|
|
52
|
+
state = nextState;
|
|
53
|
+
notify(state, prevState);
|
|
54
|
+
},
|
|
55
|
+
update: (updater) => {
|
|
56
|
+
const prevState = state;
|
|
57
|
+
state = updater(state);
|
|
58
|
+
notify(state, prevState);
|
|
59
|
+
},
|
|
60
|
+
subscribe: (listener) => {
|
|
61
|
+
listeners.add(listener);
|
|
62
|
+
return () => listeners.delete(listener);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @template TValue
|
|
69
|
+
* @typedef {{
|
|
70
|
+
* get: () => TValue,
|
|
71
|
+
* set: (value: TValue) => void,
|
|
72
|
+
* update: (updater: (current: TValue) => TValue) => void,
|
|
73
|
+
* subscribe: (listener: StoreListener<TValue>) => Unsubscribe,
|
|
74
|
+
* }} Atom
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @template TValue
|
|
79
|
+
* @param {TValue} initialValue
|
|
80
|
+
* @returns {Atom<TValue>}
|
|
81
|
+
*/
|
|
82
|
+
export function createAtom(initialValue) {
|
|
83
|
+
const store = createStore(initialValue);
|
|
84
|
+
return {
|
|
85
|
+
get: store.getState,
|
|
86
|
+
set: store.setState,
|
|
87
|
+
update: store.update,
|
|
88
|
+
subscribe: store.subscribe,
|
|
89
|
+
};
|
|
90
|
+
}
|
package/storage/index.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { AppError } from '../base.js';
|
|
2
|
+
|
|
3
|
+
export class StorageError extends AppError {
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} message
|
|
6
|
+
* @param {{ cause?: unknown }=} options
|
|
7
|
+
*/
|
|
8
|
+
constructor(message, options) {
|
|
9
|
+
super('STORAGE_ERROR', message, { cause: options?.cause });
|
|
10
|
+
this.name = 'StorageError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{
|
|
16
|
+
* getItem: (key: string) => Promise<string | null>,
|
|
17
|
+
* setItem: (key: string, value: string) => Promise<void>,
|
|
18
|
+
* removeItem: (key: string) => Promise<void>,
|
|
19
|
+
* clear: () => Promise<void>,
|
|
20
|
+
* }} KeyValueStorage
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export class MemoryStorage {
|
|
24
|
+
map = new Map();
|
|
25
|
+
|
|
26
|
+
/** @param {string} key */
|
|
27
|
+
async getItem(key) {
|
|
28
|
+
return this.map.has(key) ? (this.map.get(key) ?? null) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} key
|
|
33
|
+
* @param {string} value
|
|
34
|
+
*/
|
|
35
|
+
async setItem(key, value) {
|
|
36
|
+
this.map.set(key, value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {string} key */
|
|
40
|
+
async removeItem(key) {
|
|
41
|
+
this.map.delete(key);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async clear() {
|
|
45
|
+
this.map.clear();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @template T
|
|
51
|
+
* @typedef {{
|
|
52
|
+
* get: () => Promise<T | null>,
|
|
53
|
+
* set: (value: T) => Promise<void>,
|
|
54
|
+
* remove: () => Promise<void>,
|
|
55
|
+
* }} JsonStorage
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @template T
|
|
60
|
+
* @param {KeyValueStorage} storage
|
|
61
|
+
* @param {string} key
|
|
62
|
+
* @returns {JsonStorage<T>}
|
|
63
|
+
*/
|
|
64
|
+
export function createJsonStorage(storage, key) {
|
|
65
|
+
return {
|
|
66
|
+
get: async () => {
|
|
67
|
+
try {
|
|
68
|
+
const raw = await storage.getItem(key);
|
|
69
|
+
if (raw == null) return null;
|
|
70
|
+
return /** @type {T} */ (JSON.parse(raw));
|
|
71
|
+
} catch (e) {
|
|
72
|
+
throw new StorageError(`Failed to read key "${key}"`, { cause: e });
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
set: async (value) => {
|
|
76
|
+
try {
|
|
77
|
+
await storage.setItem(key, JSON.stringify(value));
|
|
78
|
+
} catch (e) {
|
|
79
|
+
throw new StorageError(`Failed to write key "${key}"`, { cause: e });
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
remove: async () => {
|
|
83
|
+
try {
|
|
84
|
+
await storage.removeItem(key);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
throw new StorageError(`Failed to remove key "${key}"`, { cause: e });
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|