@getforma/core 0.9.0 → 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.
Files changed (58) hide show
  1. package/README.md +20 -1
  2. package/dist/chunk-DCTOXHPF.cjs +399 -0
  3. package/dist/chunk-DCTOXHPF.cjs.map +1 -0
  4. package/dist/chunk-OUVOAYIO.js +359 -0
  5. package/dist/chunk-OUVOAYIO.js.map +1 -0
  6. package/dist/{chunk-YMIMKO4W.cjs → chunk-V732ZBCU.cjs} +119 -511
  7. package/dist/chunk-V732ZBCU.cjs.map +1 -0
  8. package/dist/{chunk-N522P3G6.js → chunk-VTPFK5TJ.js} +89 -442
  9. package/dist/chunk-VTPFK5TJ.js.map +1 -0
  10. package/dist/forma-runtime-csp.js +1 -1
  11. package/dist/forma-runtime.js +1 -1
  12. package/dist/formajs-runtime-hardened.global.js +1 -1
  13. package/dist/formajs-runtime-hardened.global.js.map +1 -1
  14. package/dist/formajs-runtime.global.js +1 -1
  15. package/dist/formajs-runtime.global.js.map +1 -1
  16. package/dist/formajs.global.js +1 -1
  17. package/dist/formajs.global.js.map +1 -1
  18. package/dist/http.cjs +225 -0
  19. package/dist/http.cjs.map +1 -0
  20. package/dist/http.d.cts +108 -0
  21. package/dist/http.d.ts +108 -0
  22. package/dist/http.js +220 -0
  23. package/dist/http.js.map +1 -0
  24. package/dist/index.cjs +71 -607
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +4 -458
  27. package/dist/index.d.ts +4 -458
  28. package/dist/index.js +7 -523
  29. package/dist/index.js.map +1 -1
  30. package/dist/resource-Cd0cGOxS.d.ts +62 -0
  31. package/dist/resource-DK98lW5e.d.cts +62 -0
  32. package/dist/runtime-hardened.cjs +3 -111
  33. package/dist/runtime-hardened.cjs.map +1 -1
  34. package/dist/runtime-hardened.js +4 -112
  35. package/dist/runtime-hardened.js.map +1 -1
  36. package/dist/runtime.cjs +23 -22
  37. package/dist/runtime.cjs.map +1 -1
  38. package/dist/runtime.js +2 -1
  39. package/dist/runtime.js.map +1 -1
  40. package/dist/server.cjs +179 -0
  41. package/dist/server.cjs.map +1 -0
  42. package/dist/server.d.cts +217 -0
  43. package/dist/server.d.ts +217 -0
  44. package/dist/server.js +166 -0
  45. package/dist/server.js.map +1 -0
  46. package/dist/{signal-B4_wQJHs.d.cts → signal-YlS1kgfh.d.cts} +1 -1
  47. package/dist/{signal-B4_wQJHs.d.ts → signal-YlS1kgfh.d.ts} +1 -1
  48. package/dist/storage.cjs +151 -0
  49. package/dist/storage.cjs.map +1 -0
  50. package/dist/storage.d.cts +77 -0
  51. package/dist/storage.d.ts +77 -0
  52. package/dist/storage.js +147 -0
  53. package/dist/storage.js.map +1 -0
  54. package/dist/tc39-compat.d.cts +1 -1
  55. package/dist/tc39-compat.d.ts +1 -1
  56. package/package.json +31 -1
  57. package/dist/chunk-N522P3G6.js.map +0 -1
  58. package/dist/chunk-YMIMKO4W.cjs.map +0 -1
package/dist/server.js ADDED
@@ -0,0 +1,166 @@
1
+ import { batch } from './chunk-OUVOAYIO.js';
2
+ import { createSignal } from './chunk-OZCHIVAZ.js';
3
+
4
+ // src/server/action.ts
5
+ function createAction(serverFn, options) {
6
+ const [pending, setPending] = createSignal(false);
7
+ const [error, setError] = createSignal(void 0);
8
+ const action = async (...args) => {
9
+ setPending(true);
10
+ setError(void 0);
11
+ if (options?.optimistic) {
12
+ try {
13
+ batch(() => options.optimistic(...args));
14
+ } catch {
15
+ }
16
+ }
17
+ try {
18
+ const result = await serverFn(...args);
19
+ if (options?.onSuccess) {
20
+ batch(() => options.onSuccess(result, ...args));
21
+ }
22
+ if (options?.invalidates) {
23
+ for (const resource of options.invalidates) {
24
+ resource.refetch();
25
+ }
26
+ }
27
+ setPending(false);
28
+ return result;
29
+ } catch (err) {
30
+ if (options?.onError) {
31
+ try {
32
+ batch(() => options.onError(err, ...args));
33
+ } catch {
34
+ }
35
+ }
36
+ setError(err);
37
+ setPending(false);
38
+ throw err;
39
+ }
40
+ };
41
+ const typedAction = action;
42
+ typedAction.pending = pending;
43
+ typedAction.error = error;
44
+ typedAction.clearError = () => setError(void 0);
45
+ return typedAction;
46
+ }
47
+
48
+ // src/server/mutation.ts
49
+ var resourceRegistry = /* @__PURE__ */ new Map();
50
+ function registerResource(key, resource) {
51
+ resourceRegistry.set(key, resource);
52
+ }
53
+ function unregisterResource(key) {
54
+ resourceRegistry.delete(key);
55
+ }
56
+ function applyRevalidation(revalidateData) {
57
+ for (const [key, freshData] of Object.entries(revalidateData)) {
58
+ const resource = resourceRegistry.get(key);
59
+ if (resource) {
60
+ resource.mutate(freshData);
61
+ }
62
+ }
63
+ }
64
+ function enableAutoRevalidation() {
65
+ if (typeof window === "undefined") {
66
+ return () => {
67
+ };
68
+ }
69
+ const handler = (event) => {
70
+ const detail = event.detail;
71
+ if (detail && typeof detail === "object") {
72
+ applyRevalidation(detail);
73
+ }
74
+ };
75
+ window.addEventListener("forma:revalidate", handler);
76
+ return () => window.removeEventListener("forma:revalidate", handler);
77
+ }
78
+ function withRevalidation(data, revalidate) {
79
+ return { data, __revalidate: revalidate };
80
+ }
81
+
82
+ // src/server/rpc-client.ts
83
+ function $$serverFunction(endpoint) {
84
+ const rpcFn = async (...args) => {
85
+ const response = await fetch(endpoint, {
86
+ method: "POST",
87
+ headers: {
88
+ "Content-Type": "application/json",
89
+ "X-Forma-RPC": "1"
90
+ },
91
+ body: JSON.stringify({ args })
92
+ });
93
+ if (!response.ok) {
94
+ const errorText = await response.text();
95
+ throw new Error(`Server function failed (${response.status}): ${errorText}`);
96
+ }
97
+ const result = await response.json();
98
+ if (result && typeof result === "object" && "__revalidate" in result) {
99
+ if (typeof window !== "undefined") {
100
+ window.dispatchEvent(new CustomEvent("forma:revalidate", {
101
+ detail: result.__revalidate
102
+ }));
103
+ }
104
+ return result.data;
105
+ }
106
+ return result;
107
+ };
108
+ return rpcFn;
109
+ }
110
+
111
+ // src/server/rpc-handler.ts
112
+ var registry = /* @__PURE__ */ new Map();
113
+ function registerServerFunction(endpoint, fn) {
114
+ registry.set(endpoint, fn);
115
+ }
116
+ function getServerFunction(endpoint) {
117
+ return registry.get(endpoint);
118
+ }
119
+ function getRegisteredEndpoints() {
120
+ return [...registry.keys()];
121
+ }
122
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
123
+ async function handleRPC(endpoint, body, revalidateData) {
124
+ const endpointName = endpoint.split("/").pop() ?? "";
125
+ if (FORBIDDEN_KEYS.has(endpointName)) {
126
+ return { error: "Forbidden endpoint name" };
127
+ }
128
+ const fn = registry.get(endpoint);
129
+ if (!fn) {
130
+ return { error: `Unknown server function: ${endpoint}` };
131
+ }
132
+ try {
133
+ const result = await fn(...body.args);
134
+ if (revalidateData) {
135
+ return { data: result, __revalidate: revalidateData };
136
+ }
137
+ return { data: result };
138
+ } catch (err) {
139
+ const isDev = typeof process !== "undefined" && process.env?.NODE_ENV === "development";
140
+ const message = isDev && err instanceof Error ? err.message : "Internal server error";
141
+ return { error: message };
142
+ }
143
+ }
144
+ function createRPCMiddleware() {
145
+ return async (req, res) => {
146
+ if (req.method !== "POST") {
147
+ res.status(405).json({ error: "Method not allowed" });
148
+ return;
149
+ }
150
+ const body = req.body;
151
+ if (!body || !Array.isArray(body.args)) {
152
+ res.status(400).json({ error: "Invalid RPC request: missing args array" });
153
+ return;
154
+ }
155
+ const result = await handleRPC(req.url, body);
156
+ if (result.error) {
157
+ res.status(500).json(result);
158
+ } else {
159
+ res.json(result);
160
+ }
161
+ };
162
+ }
163
+
164
+ export { $$serverFunction, applyRevalidation, createAction, createRPCMiddleware, enableAutoRevalidation, getRegisteredEndpoints, getServerFunction, handleRPC, registerResource, registerServerFunction, unregisterResource, withRevalidation };
165
+ //# sourceMappingURL=server.js.map
166
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/action.ts","../src/server/mutation.ts","../src/server/rpc-client.ts","../src/server/rpc-handler.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["/**\n * FormaJS Server - Action\n *\n * Creates an action that wraps a server function with optimistic UI support.\n * The action immediately applies an optimistic update, then reconciles when\n * the server responds (or rolls back on error).\n */\n\nimport { createSignal, batch } from '../reactive/index.js';\nimport type { Resource } from '../reactive/resource.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ActionOptions<Args extends unknown[], Result> {\n /**\n * Apply an optimistic update immediately when the action is called.\n * This runs BEFORE the server function.\n * Store whatever you need to roll back in the return value or via closures.\n */\n optimistic?: (...args: Args) => void;\n\n /**\n * Called when the server function resolves successfully.\n * Use this to apply the server result to local state.\n */\n onSuccess?: (result: Result, ...args: Args) => void;\n\n /**\n * Called when the server function rejects.\n * Use this to roll back the optimistic update.\n */\n onError?: (error: unknown, ...args: Args) => void;\n\n /**\n * Resources to refetch after the action completes successfully.\n * Used with single-flight mutations: if the server response includes\n * revalidation data, these resources are updated without a refetch.\n */\n invalidates?: Resource<unknown>[];\n}\n\nexport interface Action<Args extends unknown[], Result> {\n /** Execute the action. */\n (...args: Args): Promise<Result>;\n /** Whether the action is currently in-flight. */\n pending: () => boolean;\n /** The last error from the action (or undefined). */\n error: () => unknown;\n /** Clear the error state. */\n clearError: () => void;\n}\n\n/**\n * Create an action that wraps a server function with optimistic UI.\n *\n * ```ts\n * const addTodo = createAction(\n * serverCreateTodo,\n * {\n * optimistic: (text) => {\n * // Immediately add to local list\n * setTodos(prev => [...prev, { text, done: false, id: 'temp' }]);\n * },\n * onSuccess: (result) => {\n * // Replace temp item with server result\n * setTodos(prev => prev.map(t => t.id === 'temp' ? result : t));\n * },\n * onError: (err, text) => {\n * // Remove the optimistic item\n * setTodos(prev => prev.filter(t => t.id !== 'temp'));\n * },\n * invalidates: [todosResource],\n * },\n * );\n *\n * // Use:\n * await addTodo('Buy milk');\n * ```\n */\nexport function createAction<Args extends unknown[], Result>(\n serverFn: (...args: Args) => Promise<Result>,\n options?: ActionOptions<Args, Result>,\n): Action<Args, Result> {\n const [pending, setPending] = createSignal(false);\n const [error, setError] = createSignal<unknown>(undefined);\n\n const action = async (...args: Args): Promise<Result> => {\n setPending(true);\n setError(undefined);\n\n // Apply optimistic update immediately\n if (options?.optimistic) {\n try {\n batch(() => options.optimistic!(...args));\n } catch {\n // Swallow errors in optimistic callback\n }\n }\n\n try {\n const result = await serverFn(...args);\n\n // Apply success handler\n if (options?.onSuccess) {\n batch(() => options.onSuccess!(result, ...args));\n }\n\n // Revalidate dependent resources\n if (options?.invalidates) {\n for (const resource of options.invalidates) {\n resource.refetch();\n }\n }\n\n setPending(false);\n return result;\n } catch (err) {\n // Apply error/rollback handler\n if (options?.onError) {\n try {\n batch(() => options.onError!(err, ...args));\n } catch {\n // Swallow errors in rollback\n }\n }\n\n setError(err);\n setPending(false);\n throw err;\n }\n };\n\n // Attach signal accessors to the action function\n const typedAction = action as Action<Args, Result>;\n typedAction.pending = pending;\n typedAction.error = error;\n typedAction.clearError = () => setError(undefined);\n\n return typedAction;\n}\n","/**\n * FormaJS Server - Mutation\n *\n * Single-flight mutation pattern: the server response carries both the\n * mutation result AND fresh data for dependent resources in one round trip.\n *\n * Without single-flight:\n * Client -> Server: createTodo(\"Buy milk\")\n * Server -> Client: { id: 1, text: \"Buy milk\" }\n * Client -> Server: GET /api/todos (refetch to update list)\n * Server -> Client: [all todos]\n *\n * With single-flight:\n * Client -> Server: createTodo(\"Buy milk\")\n * Server -> Client: { data: {...}, __revalidate: { \"/api/todos\": [all todos] } }\n * (No second request needed!)\n */\n\nimport type { Resource } from '../reactive/resource.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MutationResponse<T> {\n /** The mutation result. */\n data: T;\n /**\n * Fresh data for dependent resources, keyed by resource identifier.\n * When present, the client updates these resources directly instead of refetching.\n */\n __revalidate?: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Resource registry for revalidation\n// ---------------------------------------------------------------------------\n\nconst resourceRegistry = new Map<string, Resource<unknown>>();\n\n/**\n * Register a resource with a key so it can be revalidated by single-flight mutations.\n *\n * ```ts\n * const todos = createResource(\n * () => true,\n * () => fetch('/api/todos').then(r => r.json()),\n * );\n * registerResource('/api/todos', todos);\n * ```\n */\nexport function registerResource(key: string, resource: Resource<unknown>): void {\n resourceRegistry.set(key, resource);\n}\n\n/**\n * Unregister a resource (call on cleanup/unmount).\n */\nexport function unregisterResource(key: string): void {\n resourceRegistry.delete(key);\n}\n\n/**\n * Apply revalidation data from a single-flight mutation response.\n * For each key in the revalidate map, find the matching resource\n * and mutate it directly with the fresh data (skipping a refetch).\n */\nexport function applyRevalidation(revalidateData: Record<string, unknown>): void {\n for (const [key, freshData] of Object.entries(revalidateData)) {\n const resource = resourceRegistry.get(key);\n if (resource) {\n resource.mutate(freshData);\n }\n }\n}\n\n/**\n * Listen for revalidation events dispatched by $$serverFunction.\n * Call this once during app initialization to enable automatic\n * single-flight mutation handling.\n *\n * ```ts\n * // In your app entry:\n * import { enableAutoRevalidation } from 'forma/server/mutation';\n * enableAutoRevalidation();\n * ```\n */\nexport function enableAutoRevalidation(): () => void {\n if (typeof window === 'undefined') {\n // SSR/Node.js — no-op\n return () => {};\n }\n\n const handler = (event: Event) => {\n const detail = (event as CustomEvent).detail;\n if (detail && typeof detail === 'object') {\n applyRevalidation(detail as Record<string, unknown>);\n }\n };\n\n window.addEventListener('forma:revalidate', handler);\n\n // Return cleanup function\n return () => window.removeEventListener('forma:revalidate', handler);\n}\n\n/**\n * Wrap a server function response to include revalidation data.\n * Use this on the server side to enable single-flight mutations.\n *\n * ```ts\n * // Server-side:\n * async function createTodo(text: string) {\n * \"use server\";\n * const newTodo = await db.insert('todos', { text, done: false });\n * const allTodos = await db.query('todos');\n * return withRevalidation(newTodo, {\n * '/api/todos': allTodos,\n * });\n * }\n * ```\n */\nexport function withRevalidation<T>(data: T, revalidate: Record<string, unknown>): MutationResponse<T> {\n return { data, __revalidate: revalidate };\n}\n","/**\n * FormaJS Server - RPC Client\n *\n * Provides the client-side stub function that replaces \"use server\" function\n * bodies after compilation. Each call becomes a fetch POST to the server endpoint.\n */\n\n/**\n * Create an RPC stub function for a server function.\n * This replaces the original function body on the client side.\n *\n * @param endpoint - The RPC endpoint path (e.g. \"/rpc/createTodo_a1b2c3\")\n * @returns An async function that sends args to the server and returns the result\n */\nexport function $$serverFunction<T extends (...args: unknown[]) => Promise<unknown>>(\n endpoint: string,\n): T {\n const rpcFn = async (...args: unknown[]): Promise<unknown> => {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Forma-RPC': '1',\n },\n body: JSON.stringify({ args }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Server function failed (${response.status}): ${errorText}`);\n }\n\n const result = await response.json();\n\n // If the response includes revalidation data (single-flight mutations),\n // return it alongside the result\n if (result && typeof result === 'object' && '__revalidate' in result) {\n // Dispatch a custom event so createAction can pick up the revalidation data\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent('forma:revalidate', {\n detail: result.__revalidate,\n }));\n }\n return result.data;\n }\n\n return result;\n };\n\n return rpcFn as T;\n}\n","/**\n * FormaJS Server - RPC Handler\n *\n * Server-side registry and request handler for \"use server\" functions.\n * Framework-agnostic — works with any Node.js HTTP server, Express, Hono, etc.\n */\n\nexport type ServerFunction = (...args: unknown[]) => Promise<unknown>;\n\n/** Registry of server functions by endpoint path. */\nconst registry = new Map<string, ServerFunction>();\n\n/**\n * Register a server function at a specific endpoint.\n * Called by the server-side compiled output.\n */\nexport function registerServerFunction(endpoint: string, fn: ServerFunction): void {\n registry.set(endpoint, fn);\n}\n\n/**\n * Get a registered server function by endpoint.\n */\nexport function getServerFunction(endpoint: string): ServerFunction | undefined {\n return registry.get(endpoint);\n}\n\n/**\n * Get all registered server function endpoints.\n */\nexport function getRegisteredEndpoints(): string[] {\n return [...registry.keys()];\n}\n\nexport interface RPCRequest {\n args: unknown[];\n}\n\nexport interface RPCResponse {\n data?: unknown;\n error?: string;\n __revalidate?: Record<string, unknown>;\n}\n\n/**\n * Handle an incoming RPC request.\n * Call this from your HTTP server's request handler.\n *\n * Usage with any HTTP framework:\n * ```ts\n * import { handleRPC } from 'forma/server/rpc-handler';\n *\n * // Express example:\n * app.post('/rpc/:endpoint', async (req, res) => {\n * const result = await handleRPC(req.path, req.body);\n * res.json(result);\n * });\n *\n * // Hono example:\n * app.post('/rpc/*', async (c) => {\n * const body = await c.req.json();\n * const result = await handleRPC(c.req.path, body);\n * return c.json(result);\n * });\n * ```\n *\n * @param endpoint - The full endpoint path (e.g. \"/rpc/createTodo_a1b2c3\")\n * @param body - The parsed request body containing { args: [...] }\n * @param revalidateData - Optional revalidation data to include in the response\n * (for single-flight mutations)\n */\n/** \"Crash Barrier\": block prototype pollution attacks via endpoint names. */\nconst FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nexport async function handleRPC(\n endpoint: string,\n body: RPCRequest,\n revalidateData?: Record<string, unknown>,\n): Promise<RPCResponse> {\n // Security: prevent prototype pollution via crafted endpoint names\n const endpointName = endpoint.split('/').pop() ?? '';\n if (FORBIDDEN_KEYS.has(endpointName)) {\n return { error: 'Forbidden endpoint name' };\n }\n\n const fn = registry.get(endpoint);\n if (!fn) {\n return { error: `Unknown server function: ${endpoint}` };\n }\n\n try {\n const result = await fn(...body.args);\n\n if (revalidateData) {\n return { data: result, __revalidate: revalidateData };\n }\n\n return { data: result };\n } catch (err) {\n // Don't leak internal error details to clients\n const isDev = typeof process !== 'undefined'\n && process.env?.NODE_ENV === 'development';\n const message = isDev && err instanceof Error\n ? err.message\n : 'Internal server error';\n return { error: message };\n }\n}\n\n/**\n * Create a middleware-style handler for use with Express-like frameworks.\n *\n * ```ts\n * import { createRPCMiddleware } from 'forma/server/rpc-handler';\n * app.use('/rpc', createRPCMiddleware());\n * ```\n */\nexport function createRPCMiddleware() {\n return async (req: { url: string; method: string; body?: unknown }, res: { json: (data: unknown) => void; status: (code: number) => { json: (data: unknown) => void } }) => {\n if (req.method !== 'POST') {\n res.status(405).json({ error: 'Method not allowed' });\n return;\n }\n\n const body = req.body as RPCRequest;\n if (!body || !Array.isArray(body.args)) {\n res.status(400).json({ error: 'Invalid RPC request: missing args array' });\n return;\n }\n\n const result = await handleRPC(req.url, body);\n if (result.error) {\n res.status(500).json(result);\n } else {\n res.json(result);\n }\n };\n}\n"]}
@@ -50,4 +50,4 @@ interface SignalOptions<T> {
50
50
  */
51
51
  declare function createSignal<T>(initialValue: T, options?: SignalOptions<T>): [get: SignalGetter<T>, set: SignalSetter<T>];
52
52
 
53
- export { type SignalGetter as S, type SignalOptions as a, createSignal as c };
53
+ export { type SignalGetter as S, type SignalOptions as a, type SignalSetter as b, createSignal as c };
@@ -50,4 +50,4 @@ interface SignalOptions<T> {
50
50
  */
51
51
  declare function createSignal<T>(initialValue: T, options?: SignalOptions<T>): [get: SignalGetter<T>, set: SignalSetter<T>];
52
52
 
53
- export { type SignalGetter as S, type SignalOptions as a, createSignal as c };
53
+ export { type SignalGetter as S, type SignalOptions as a, type SignalSetter as b, createSignal as c };
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ // src/storage/local.ts
4
+ function createLocalStorage(key, options) {
5
+ const serialize = options?.serialize ?? JSON.stringify;
6
+ const deserialize = options?.deserialize ?? JSON.parse;
7
+ const validate = options?.validate;
8
+ return {
9
+ key,
10
+ get() {
11
+ try {
12
+ const raw = localStorage.getItem(key);
13
+ if (raw === null) return null;
14
+ const value = deserialize(raw);
15
+ if (validate && !validate(value)) return null;
16
+ return value;
17
+ } catch {
18
+ return null;
19
+ }
20
+ },
21
+ set(value) {
22
+ try {
23
+ const serialized = serialize(value);
24
+ localStorage.setItem(key, serialized);
25
+ } catch {
26
+ }
27
+ },
28
+ remove() {
29
+ localStorage.removeItem(key);
30
+ }
31
+ };
32
+ }
33
+
34
+ // src/storage/session.ts
35
+ function createSessionStorage(key, options) {
36
+ const serialize = options?.serialize ?? JSON.stringify;
37
+ const deserialize = options?.deserialize ?? JSON.parse;
38
+ const validate = options?.validate;
39
+ return {
40
+ key,
41
+ get() {
42
+ try {
43
+ const raw = sessionStorage.getItem(key);
44
+ if (raw === null) return null;
45
+ const value = deserialize(raw);
46
+ if (validate && !validate(value)) return null;
47
+ return value;
48
+ } catch {
49
+ return null;
50
+ }
51
+ },
52
+ set(value) {
53
+ try {
54
+ const serialized = serialize(value);
55
+ sessionStorage.setItem(key, serialized);
56
+ } catch {
57
+ }
58
+ },
59
+ remove() {
60
+ sessionStorage.removeItem(key);
61
+ }
62
+ };
63
+ }
64
+
65
+ // src/storage/indexed.ts
66
+ var dbCache = /* @__PURE__ */ new Map();
67
+ function openDB(dbName, storeName) {
68
+ const cacheKey = `${dbName}::${storeName}`;
69
+ const cached = dbCache.get(cacheKey);
70
+ if (cached) return cached;
71
+ const promise = new Promise((resolve, reject) => {
72
+ const probe = indexedDB.open(dbName);
73
+ probe.onerror = () => reject(probe.error);
74
+ probe.onsuccess = () => {
75
+ const db = probe.result;
76
+ if (db.objectStoreNames.contains(storeName)) {
77
+ resolve(db);
78
+ return;
79
+ }
80
+ const nextVersion = db.version + 1;
81
+ db.close();
82
+ const upgrade = indexedDB.open(dbName, nextVersion);
83
+ upgrade.onerror = () => reject(upgrade.error);
84
+ upgrade.onupgradeneeded = () => {
85
+ const upgradedDB = upgrade.result;
86
+ if (!upgradedDB.objectStoreNames.contains(storeName)) {
87
+ upgradedDB.createObjectStore(storeName);
88
+ }
89
+ };
90
+ upgrade.onsuccess = () => resolve(upgrade.result);
91
+ };
92
+ probe.onupgradeneeded = () => {
93
+ const db = probe.result;
94
+ if (!db.objectStoreNames.contains(storeName)) {
95
+ db.createObjectStore(storeName);
96
+ }
97
+ };
98
+ });
99
+ dbCache.set(cacheKey, promise);
100
+ promise.catch(() => {
101
+ dbCache.delete(cacheKey);
102
+ });
103
+ return promise;
104
+ }
105
+ function createIndexedDB(dbName, storeName = "default") {
106
+ function withStore(mode, fn) {
107
+ return openDB(dbName, storeName).then(
108
+ (db) => new Promise((resolve, reject) => {
109
+ const tx = db.transaction(storeName, mode);
110
+ const store = tx.objectStore(storeName);
111
+ const request = fn(store);
112
+ request.onsuccess = () => resolve(request.result);
113
+ request.onerror = () => reject(request.error);
114
+ })
115
+ );
116
+ }
117
+ return {
118
+ get(key) {
119
+ return withStore("readonly", (store) => store.get(key));
120
+ },
121
+ set(key, value) {
122
+ return withStore("readwrite", (store) => store.put(value, key)).then(
123
+ () => void 0
124
+ );
125
+ },
126
+ delete(key) {
127
+ return withStore("readwrite", (store) => store.delete(key)).then(
128
+ () => void 0
129
+ );
130
+ },
131
+ getAll() {
132
+ return withStore("readonly", (store) => store.getAll());
133
+ },
134
+ keys() {
135
+ return withStore("readonly", (store) => store.getAllKeys()).then(
136
+ (keys) => keys.map(String)
137
+ );
138
+ },
139
+ clear() {
140
+ return withStore("readwrite", (store) => store.clear()).then(
141
+ () => void 0
142
+ );
143
+ }
144
+ };
145
+ }
146
+
147
+ exports.createIndexedDB = createIndexedDB;
148
+ exports.createLocalStorage = createLocalStorage;
149
+ exports.createSessionStorage = createSessionStorage;
150
+ //# sourceMappingURL=storage.cjs.map
151
+ //# sourceMappingURL=storage.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/storage/local.ts","../src/storage/session.ts","../src/storage/indexed.ts"],"names":[],"mappings":"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","file":"storage.cjs","sourcesContent":["/**\n * Forma Storage - Local\n *\n * Typed localStorage wrapper with graceful error handling.\n * Zero dependencies — native browser APIs only.\n */\n\nimport { TypedStorage, StorageOptions } from './types.js';\n\n/**\n * Create a typed localStorage wrapper for the given key.\n *\n * ```ts\n * const store = createLocalStorage<{ name: string }>('user');\n * store.set({ name: 'Alice' });\n * store.get(); // { name: 'Alice' }\n * store.remove();\n * ```\n */\nexport function createLocalStorage<T>(\n key: string,\n options?: StorageOptions<T>,\n): TypedStorage<T> {\n const serialize = options?.serialize ?? JSON.stringify;\n const deserialize = options?.deserialize ?? JSON.parse;\n const validate = options?.validate;\n\n return {\n key,\n\n get(): T | null {\n try {\n const raw = localStorage.getItem(key);\n if (raw === null) return null;\n const value = deserialize(raw);\n if (validate && !validate(value)) return null;\n return value as T;\n } catch {\n return null;\n }\n },\n\n set(value: T): void {\n try {\n const serialized = serialize(value);\n localStorage.setItem(key, serialized);\n } catch { /* QuotaExceededError or storage disabled — silently ignore */ }\n },\n\n remove(): void {\n localStorage.removeItem(key);\n },\n };\n}\n","/**\n * Forma Storage - Session\n *\n * Typed sessionStorage wrapper with graceful error handling.\n * Zero dependencies — native browser APIs only.\n */\n\nimport { TypedStorage, StorageOptions } from './types.js';\n\n/**\n * Create a typed sessionStorage wrapper for the given key.\n *\n * ```ts\n * const store = createSessionStorage<{ token: string }>('auth');\n * store.set({ token: 'abc123' });\n * store.get(); // { token: 'abc123' }\n * store.remove();\n * ```\n */\nexport function createSessionStorage<T>(\n key: string,\n options?: StorageOptions<T>,\n): TypedStorage<T> {\n const serialize = options?.serialize ?? JSON.stringify;\n const deserialize = options?.deserialize ?? JSON.parse;\n const validate = options?.validate;\n\n return {\n key,\n\n get(): T | null {\n try {\n const raw = sessionStorage.getItem(key);\n if (raw === null) return null;\n const value = deserialize(raw);\n if (validate && !validate(value)) return null;\n return value as T;\n } catch {\n return null;\n }\n },\n\n set(value: T): void {\n try {\n const serialized = serialize(value);\n sessionStorage.setItem(key, serialized);\n } catch { /* QuotaExceededError or storage disabled — silently ignore */ }\n },\n\n remove(): void {\n sessionStorage.removeItem(key);\n },\n };\n}\n","/**\n * Forma Storage - IndexedDB\n *\n * Simplified IndexedDB wrapper with lazy connection and promise-based API.\n * Zero dependencies — native browser APIs only.\n */\n\nexport interface IDBStore<T> {\n get(key: string): Promise<T | undefined>;\n set(key: string, value: T): Promise<void>;\n delete(key: string): Promise<void>;\n getAll(): Promise<T[]>;\n keys(): Promise<string[]>;\n clear(): Promise<void>;\n}\n\n/** Cached database connections keyed by \"dbName::storeName\". */\nconst dbCache = new Map<string, Promise<IDBDatabase>>();\n\n/**\n * Open (or retrieve a cached) IndexedDB database, ensuring the\n * requested object store exists.\n */\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n const cacheKey = `${dbName}::${storeName}`;\n const cached = dbCache.get(cacheKey);\n if (cached) return cached;\n\n const promise = new Promise<IDBDatabase>((resolve, reject) => {\n // First, try opening at the current (or default) version.\n const probe = indexedDB.open(dbName);\n\n probe.onerror = () => reject(probe.error);\n\n probe.onsuccess = () => {\n const db = probe.result;\n if (db.objectStoreNames.contains(storeName)) {\n resolve(db);\n return;\n }\n // Store doesn't exist yet — close and reopen with a version bump.\n const nextVersion = db.version + 1;\n db.close();\n\n const upgrade = indexedDB.open(dbName, nextVersion);\n upgrade.onerror = () => reject(upgrade.error);\n upgrade.onupgradeneeded = () => {\n const upgradedDB = upgrade.result;\n if (!upgradedDB.objectStoreNames.contains(storeName)) {\n upgradedDB.createObjectStore(storeName);\n }\n };\n upgrade.onsuccess = () => resolve(upgrade.result);\n };\n\n probe.onupgradeneeded = () => {\n const db = probe.result;\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName);\n }\n };\n });\n\n dbCache.set(cacheKey, promise);\n\n // If the open fails, remove from cache so the next call retries.\n promise.catch(() => {\n dbCache.delete(cacheKey);\n });\n\n return promise;\n}\n\n/**\n * Create a simplified IndexedDB store.\n *\n * ```ts\n * const store = createIndexedDB<User>('myApp', 'users');\n * await store.set('u1', { name: 'Alice' });\n * const user = await store.get('u1'); // { name: 'Alice' }\n * ```\n */\nexport function createIndexedDB<T>(\n dbName: string,\n storeName: string = 'default',\n): IDBStore<T> {\n /** Helper: run a single read/write transaction and return the request result. */\n function withStore<R>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<R>,\n ): Promise<R> {\n return openDB(dbName, storeName).then(\n (db) =>\n new Promise<R>((resolve, reject) => {\n const tx = db.transaction(storeName, mode);\n const store = tx.objectStore(storeName);\n const request = fn(store);\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n }),\n );\n }\n\n return {\n get(key: string): Promise<T | undefined> {\n return withStore('readonly', (store) => store.get(key)) as Promise<T | undefined>;\n },\n\n set(key: string, value: T): Promise<void> {\n return withStore('readwrite', (store) => store.put(value, key)).then(\n () => undefined,\n );\n },\n\n delete(key: string): Promise<void> {\n return withStore('readwrite', (store) => store.delete(key)).then(\n () => undefined,\n );\n },\n\n getAll(): Promise<T[]> {\n return withStore('readonly', (store) => store.getAll()) as Promise<T[]>;\n },\n\n keys(): Promise<string[]> {\n return withStore('readonly', (store) => store.getAllKeys()).then(\n (keys) => keys.map(String),\n );\n },\n\n clear(): Promise<void> {\n return withStore('readwrite', (store) => store.clear()).then(\n () => undefined,\n );\n },\n };\n}\n"]}
@@ -0,0 +1,77 @@
1
+ interface TypedStorage<T> {
2
+ get(): T | null;
3
+ set(value: T): void;
4
+ remove(): void;
5
+ key: string;
6
+ }
7
+ interface StorageOptions<T> {
8
+ serialize?: (v: T) => string;
9
+ deserialize?: (s: string) => T;
10
+ /** Optional validator — return true if the deserialized value is valid. */
11
+ validate?: (v: unknown) => v is T;
12
+ }
13
+
14
+ /**
15
+ * Forma Storage - Local
16
+ *
17
+ * Typed localStorage wrapper with graceful error handling.
18
+ * Zero dependencies — native browser APIs only.
19
+ */
20
+
21
+ /**
22
+ * Create a typed localStorage wrapper for the given key.
23
+ *
24
+ * ```ts
25
+ * const store = createLocalStorage<{ name: string }>('user');
26
+ * store.set({ name: 'Alice' });
27
+ * store.get(); // { name: 'Alice' }
28
+ * store.remove();
29
+ * ```
30
+ */
31
+ declare function createLocalStorage<T>(key: string, options?: StorageOptions<T>): TypedStorage<T>;
32
+
33
+ /**
34
+ * Forma Storage - Session
35
+ *
36
+ * Typed sessionStorage wrapper with graceful error handling.
37
+ * Zero dependencies — native browser APIs only.
38
+ */
39
+
40
+ /**
41
+ * Create a typed sessionStorage wrapper for the given key.
42
+ *
43
+ * ```ts
44
+ * const store = createSessionStorage<{ token: string }>('auth');
45
+ * store.set({ token: 'abc123' });
46
+ * store.get(); // { token: 'abc123' }
47
+ * store.remove();
48
+ * ```
49
+ */
50
+ declare function createSessionStorage<T>(key: string, options?: StorageOptions<T>): TypedStorage<T>;
51
+
52
+ /**
53
+ * Forma Storage - IndexedDB
54
+ *
55
+ * Simplified IndexedDB wrapper with lazy connection and promise-based API.
56
+ * Zero dependencies — native browser APIs only.
57
+ */
58
+ interface IDBStore<T> {
59
+ get(key: string): Promise<T | undefined>;
60
+ set(key: string, value: T): Promise<void>;
61
+ delete(key: string): Promise<void>;
62
+ getAll(): Promise<T[]>;
63
+ keys(): Promise<string[]>;
64
+ clear(): Promise<void>;
65
+ }
66
+ /**
67
+ * Create a simplified IndexedDB store.
68
+ *
69
+ * ```ts
70
+ * const store = createIndexedDB<User>('myApp', 'users');
71
+ * await store.set('u1', { name: 'Alice' });
72
+ * const user = await store.get('u1'); // { name: 'Alice' }
73
+ * ```
74
+ */
75
+ declare function createIndexedDB<T>(dbName: string, storeName?: string): IDBStore<T>;
76
+
77
+ export { type IDBStore, type StorageOptions, type TypedStorage, createIndexedDB, createLocalStorage, createSessionStorage };
@@ -0,0 +1,77 @@
1
+ interface TypedStorage<T> {
2
+ get(): T | null;
3
+ set(value: T): void;
4
+ remove(): void;
5
+ key: string;
6
+ }
7
+ interface StorageOptions<T> {
8
+ serialize?: (v: T) => string;
9
+ deserialize?: (s: string) => T;
10
+ /** Optional validator — return true if the deserialized value is valid. */
11
+ validate?: (v: unknown) => v is T;
12
+ }
13
+
14
+ /**
15
+ * Forma Storage - Local
16
+ *
17
+ * Typed localStorage wrapper with graceful error handling.
18
+ * Zero dependencies — native browser APIs only.
19
+ */
20
+
21
+ /**
22
+ * Create a typed localStorage wrapper for the given key.
23
+ *
24
+ * ```ts
25
+ * const store = createLocalStorage<{ name: string }>('user');
26
+ * store.set({ name: 'Alice' });
27
+ * store.get(); // { name: 'Alice' }
28
+ * store.remove();
29
+ * ```
30
+ */
31
+ declare function createLocalStorage<T>(key: string, options?: StorageOptions<T>): TypedStorage<T>;
32
+
33
+ /**
34
+ * Forma Storage - Session
35
+ *
36
+ * Typed sessionStorage wrapper with graceful error handling.
37
+ * Zero dependencies — native browser APIs only.
38
+ */
39
+
40
+ /**
41
+ * Create a typed sessionStorage wrapper for the given key.
42
+ *
43
+ * ```ts
44
+ * const store = createSessionStorage<{ token: string }>('auth');
45
+ * store.set({ token: 'abc123' });
46
+ * store.get(); // { token: 'abc123' }
47
+ * store.remove();
48
+ * ```
49
+ */
50
+ declare function createSessionStorage<T>(key: string, options?: StorageOptions<T>): TypedStorage<T>;
51
+
52
+ /**
53
+ * Forma Storage - IndexedDB
54
+ *
55
+ * Simplified IndexedDB wrapper with lazy connection and promise-based API.
56
+ * Zero dependencies — native browser APIs only.
57
+ */
58
+ interface IDBStore<T> {
59
+ get(key: string): Promise<T | undefined>;
60
+ set(key: string, value: T): Promise<void>;
61
+ delete(key: string): Promise<void>;
62
+ getAll(): Promise<T[]>;
63
+ keys(): Promise<string[]>;
64
+ clear(): Promise<void>;
65
+ }
66
+ /**
67
+ * Create a simplified IndexedDB store.
68
+ *
69
+ * ```ts
70
+ * const store = createIndexedDB<User>('myApp', 'users');
71
+ * await store.set('u1', { name: 'Alice' });
72
+ * const user = await store.get('u1'); // { name: 'Alice' }
73
+ * ```
74
+ */
75
+ declare function createIndexedDB<T>(dbName: string, storeName?: string): IDBStore<T>;
76
+
77
+ export { type IDBStore, type StorageOptions, type TypedStorage, createIndexedDB, createLocalStorage, createSessionStorage };