@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 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
+ }
@@ -0,0 +1,3 @@
1
+ export * from './errors/NetworkError.js';
2
+ export * from './errors/HttpError.js';
3
+ export * from './http/createHttpClient.js';
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
+ }
@@ -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
+ }
@@ -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
+ }