@bquery/bquery 1.4.0 → 1.6.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 (164) hide show
  1. package/README.md +586 -527
  2. package/dist/component/component.d.ts +13 -5
  3. package/dist/component/component.d.ts.map +1 -1
  4. package/dist/component/html.d.ts +40 -3
  5. package/dist/component/html.d.ts.map +1 -1
  6. package/dist/component/index.d.ts +4 -2
  7. package/dist/component/index.d.ts.map +1 -1
  8. package/dist/component/library.d.ts +34 -0
  9. package/dist/component/library.d.ts.map +1 -0
  10. package/dist/component/types.d.ts +132 -13
  11. package/dist/component/types.d.ts.map +1 -1
  12. package/dist/component-BEQgt5hl.js +600 -0
  13. package/dist/component-BEQgt5hl.js.map +1 -0
  14. package/dist/component.es.mjs +7 -184
  15. package/dist/config-DRmZZno3.js +40 -0
  16. package/dist/config-DRmZZno3.js.map +1 -0
  17. package/dist/core-BGQJVw0-.js +35 -0
  18. package/dist/core-BGQJVw0-.js.map +1 -0
  19. package/dist/core-CCEabVHl.js +648 -0
  20. package/dist/core-CCEabVHl.js.map +1 -0
  21. package/dist/core.es.mjs +45 -1261
  22. package/dist/effect-AFRW_Plg.js +84 -0
  23. package/dist/effect-AFRW_Plg.js.map +1 -0
  24. package/dist/full.d.ts +8 -8
  25. package/dist/full.d.ts.map +1 -1
  26. package/dist/full.es.mjs +101 -91
  27. package/dist/full.iife.js +173 -3
  28. package/dist/full.iife.js.map +1 -1
  29. package/dist/full.umd.js +173 -3
  30. package/dist/full.umd.js.map +1 -1
  31. package/dist/index.es.mjs +147 -139
  32. package/dist/motion/transition.d.ts +1 -1
  33. package/dist/motion/transition.d.ts.map +1 -1
  34. package/dist/motion/types.d.ts +11 -1
  35. package/dist/motion/types.d.ts.map +1 -1
  36. package/dist/motion-D9TcHxOF.js +415 -0
  37. package/dist/motion-D9TcHxOF.js.map +1 -0
  38. package/dist/motion.es.mjs +25 -361
  39. package/dist/object-qGpWr6-J.js +38 -0
  40. package/dist/object-qGpWr6-J.js.map +1 -0
  41. package/dist/platform/announcer.d.ts +59 -0
  42. package/dist/platform/announcer.d.ts.map +1 -0
  43. package/dist/platform/config.d.ts +92 -0
  44. package/dist/platform/config.d.ts.map +1 -0
  45. package/dist/platform/cookies.d.ts +45 -0
  46. package/dist/platform/cookies.d.ts.map +1 -0
  47. package/dist/platform/index.d.ts +8 -0
  48. package/dist/platform/index.d.ts.map +1 -1
  49. package/dist/platform/meta.d.ts +62 -0
  50. package/dist/platform/meta.d.ts.map +1 -0
  51. package/dist/platform-Dr9b6fsq.js +362 -0
  52. package/dist/platform-Dr9b6fsq.js.map +1 -0
  53. package/dist/platform.es.mjs +11 -248
  54. package/dist/reactive/async-data.d.ts +114 -0
  55. package/dist/reactive/async-data.d.ts.map +1 -0
  56. package/dist/reactive/index.d.ts +2 -2
  57. package/dist/reactive/index.d.ts.map +1 -1
  58. package/dist/reactive/signal.d.ts +2 -0
  59. package/dist/reactive/signal.d.ts.map +1 -1
  60. package/dist/reactive-DSkct0dO.js +254 -0
  61. package/dist/reactive-DSkct0dO.js.map +1 -0
  62. package/dist/reactive.es.mjs +18 -32
  63. package/dist/router-CbDhl8rS.js +188 -0
  64. package/dist/router-CbDhl8rS.js.map +1 -0
  65. package/dist/router.es.mjs +11 -200
  66. package/dist/sanitize-Bs2dkMby.js +313 -0
  67. package/dist/sanitize-Bs2dkMby.js.map +1 -0
  68. package/dist/security/constants.d.ts.map +1 -1
  69. package/dist/security/index.d.ts +4 -2
  70. package/dist/security/index.d.ts.map +1 -1
  71. package/dist/security/sanitize.d.ts +4 -1
  72. package/dist/security/sanitize.d.ts.map +1 -1
  73. package/dist/security/trusted-html.d.ts +53 -0
  74. package/dist/security/trusted-html.d.ts.map +1 -0
  75. package/dist/security.es.mjs +11 -56
  76. package/dist/store/define-store.d.ts +1 -1
  77. package/dist/store/define-store.d.ts.map +1 -1
  78. package/dist/store/mapping.d.ts +1 -1
  79. package/dist/store/mapping.d.ts.map +1 -1
  80. package/dist/store/persisted.d.ts +1 -1
  81. package/dist/store/persisted.d.ts.map +1 -1
  82. package/dist/store/types.d.ts +2 -2
  83. package/dist/store/types.d.ts.map +1 -1
  84. package/dist/store/watch.d.ts +1 -1
  85. package/dist/store/watch.d.ts.map +1 -1
  86. package/dist/store-BwDvI45q.js +263 -0
  87. package/dist/store-BwDvI45q.js.map +1 -0
  88. package/dist/store.es.mjs +12 -25
  89. package/dist/storybook/index.d.ts +37 -0
  90. package/dist/storybook/index.d.ts.map +1 -0
  91. package/dist/storybook.es.mjs +151 -0
  92. package/dist/storybook.es.mjs.map +1 -0
  93. package/dist/untrack-B0rVscTc.js +7 -0
  94. package/dist/untrack-B0rVscTc.js.map +1 -0
  95. package/dist/view-C70lA3vf.js +397 -0
  96. package/dist/view-C70lA3vf.js.map +1 -0
  97. package/dist/view.es.mjs +11 -430
  98. package/package.json +141 -132
  99. package/src/component/component.ts +524 -289
  100. package/src/component/html.ts +153 -53
  101. package/src/component/index.ts +50 -40
  102. package/src/component/library.ts +518 -0
  103. package/src/component/types.ts +256 -85
  104. package/src/core/collection.ts +628 -628
  105. package/src/core/element.ts +774 -774
  106. package/src/core/index.ts +48 -48
  107. package/src/core/utils/function.ts +151 -151
  108. package/src/full.ts +229 -187
  109. package/src/motion/animate.ts +113 -113
  110. package/src/motion/flip.ts +176 -176
  111. package/src/motion/scroll.ts +57 -57
  112. package/src/motion/spring.ts +150 -150
  113. package/src/motion/timeline.ts +246 -246
  114. package/src/motion/transition.ts +97 -51
  115. package/src/motion/types.ts +11 -1
  116. package/src/platform/announcer.ts +208 -0
  117. package/src/platform/config.ts +163 -0
  118. package/src/platform/cookies.ts +165 -0
  119. package/src/platform/index.ts +21 -0
  120. package/src/platform/meta.ts +168 -0
  121. package/src/platform/storage.ts +215 -215
  122. package/src/reactive/async-data.ts +486 -0
  123. package/src/reactive/core.ts +114 -114
  124. package/src/reactive/effect.ts +54 -54
  125. package/src/reactive/index.ts +15 -1
  126. package/src/reactive/internals.ts +122 -122
  127. package/src/reactive/signal.ts +9 -0
  128. package/src/security/constants.ts +3 -1
  129. package/src/security/index.ts +17 -10
  130. package/src/security/sanitize-core.ts +364 -364
  131. package/src/security/sanitize.ts +70 -66
  132. package/src/security/trusted-html.ts +71 -0
  133. package/src/store/define-store.ts +49 -48
  134. package/src/store/mapping.ts +74 -73
  135. package/src/store/persisted.ts +62 -61
  136. package/src/store/types.ts +92 -94
  137. package/src/store/watch.ts +53 -52
  138. package/src/storybook/index.ts +479 -0
  139. package/src/view/evaluate.ts +290 -290
  140. package/dist/batch-x7b2eZST.js +0 -13
  141. package/dist/batch-x7b2eZST.js.map +0 -1
  142. package/dist/component.es.mjs.map +0 -1
  143. package/dist/core-BhpuvPhy.js +0 -170
  144. package/dist/core-BhpuvPhy.js.map +0 -1
  145. package/dist/core.es.mjs.map +0 -1
  146. package/dist/full.es.mjs.map +0 -1
  147. package/dist/index.es.mjs.map +0 -1
  148. package/dist/motion.es.mjs.map +0 -1
  149. package/dist/persisted-DHoi3uEs.js +0 -278
  150. package/dist/persisted-DHoi3uEs.js.map +0 -1
  151. package/dist/platform.es.mjs.map +0 -1
  152. package/dist/reactive.es.mjs.map +0 -1
  153. package/dist/router.es.mjs.map +0 -1
  154. package/dist/sanitize-Cxvxa-DX.js +0 -283
  155. package/dist/sanitize-Cxvxa-DX.js.map +0 -1
  156. package/dist/security.es.mjs.map +0 -1
  157. package/dist/store.es.mjs.map +0 -1
  158. package/dist/type-guards-BdKlYYlS.js +0 -32
  159. package/dist/type-guards-BdKlYYlS.js.map +0 -1
  160. package/dist/untrack-DNnnqdlR.js +0 -6
  161. package/dist/untrack-DNnnqdlR.js.map +0 -1
  162. package/dist/view.es.mjs.map +0 -1
  163. package/dist/watch-DXXv3iAI.js +0 -58
  164. package/dist/watch-DXXv3iAI.js.map +0 -1
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Document title and meta helpers.
3
+ *
4
+ * @module bquery/platform
5
+ */
6
+
7
+ import { getBqueryConfig } from './config';
8
+
9
+ /** Meta tag definition. */
10
+ export interface PageMetaTag {
11
+ /** Standard meta name attribute. */
12
+ name?: string;
13
+ /** Open Graph / custom property attribute. */
14
+ property?: string;
15
+ /** http-equiv attribute. */
16
+ httpEquiv?: string;
17
+ /** Meta tag content. */
18
+ content: string;
19
+ }
20
+
21
+ /** Link tag definition. */
22
+ export interface PageLinkTag {
23
+ /** Link relation. */
24
+ rel: string;
25
+ /** Link URL. */
26
+ href: string;
27
+ /** Optional type attribute. */
28
+ type?: string;
29
+ /** Optional media query. */
30
+ media?: string;
31
+ /** Optional crossOrigin attribute. */
32
+ crossOrigin?: 'anonymous' | 'use-credentials';
33
+ }
34
+
35
+ /** Page metadata definition. */
36
+ export interface PageMetaDefinition {
37
+ /** Document title. */
38
+ title?: string;
39
+ /** Convenience shortcut for the description meta tag. */
40
+ description?: string;
41
+ /** Additional meta tags. */
42
+ meta?: PageMetaTag[];
43
+ /** Additional link tags. */
44
+ link?: PageLinkTag[];
45
+ /** Attributes applied to the html element. */
46
+ htmlAttributes?: Record<string, string>;
47
+ /** Attributes applied to the body element. */
48
+ bodyAttributes?: Record<string, string>;
49
+ }
50
+
51
+ /** Cleanup function returned by definePageMeta(). */
52
+ export type PageMetaCleanup = () => void;
53
+
54
+ const setAttributes = (target: HTMLElement, attributes: Record<string, string>): (() => void) => {
55
+ const previousValues = new Map<string, string | null>();
56
+
57
+ for (const [name, value] of Object.entries(attributes)) {
58
+ previousValues.set(name, target.getAttribute(name));
59
+ target.setAttribute(name, value);
60
+ }
61
+
62
+ return () => {
63
+ for (const [name, value] of previousValues.entries()) {
64
+ if (value == null) {
65
+ target.removeAttribute(name);
66
+ } else {
67
+ target.setAttribute(name, value);
68
+ }
69
+ }
70
+ };
71
+ };
72
+
73
+ const createElement = <T extends 'meta' | 'link'>(
74
+ tagName: T,
75
+ attributes: Record<string, string | undefined>
76
+ ): HTMLElementTagNameMap[T] => {
77
+ const element = document.createElement(tagName);
78
+ element.setAttribute('data-bquery-page-meta', 'true');
79
+
80
+ for (const [name, value] of Object.entries(attributes)) {
81
+ if (value !== undefined) {
82
+ element.setAttribute(name, value);
83
+ }
84
+ }
85
+
86
+ return element;
87
+ };
88
+
89
+ /**
90
+ * Apply document metadata for the current page.
91
+ *
92
+ * @param definition - Title, meta tags, link tags, and document attributes
93
+ * @returns Cleanup function that restores the previous document state
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const cleanup = definePageMeta({
98
+ * title: 'Dashboard',
99
+ * description: 'Overview of your account',
100
+ * });
101
+ * ```
102
+ */
103
+ export const definePageMeta = (definition: PageMetaDefinition): PageMetaCleanup => {
104
+ if (typeof document === 'undefined') {
105
+ return () => {};
106
+ }
107
+
108
+ const config = getBqueryConfig().pageMeta;
109
+ const title = definition.title
110
+ ? config?.titleTemplate
111
+ ? config.titleTemplate(definition.title)
112
+ : definition.title
113
+ : undefined;
114
+
115
+ const inserted: HTMLElement[] = [];
116
+ const restoreFns: Array<() => void> = [];
117
+ const previousTitle = document.title;
118
+
119
+ if (title !== undefined) {
120
+ document.title = title;
121
+ }
122
+
123
+ const metaEntries = [...(definition.meta ?? [])];
124
+ if (definition.description) {
125
+ metaEntries.unshift({ name: 'description', content: definition.description });
126
+ }
127
+
128
+ for (const entry of metaEntries) {
129
+ const meta = createElement('meta', {
130
+ name: entry.name,
131
+ property: entry.property,
132
+ 'http-equiv': entry.httpEquiv,
133
+ content: entry.content,
134
+ });
135
+ document.head.appendChild(meta);
136
+ inserted.push(meta);
137
+ }
138
+
139
+ for (const entry of definition.link ?? []) {
140
+ const link = createElement('link', {
141
+ rel: entry.rel,
142
+ href: entry.href,
143
+ type: entry.type,
144
+ media: entry.media,
145
+ crossorigin: entry.crossOrigin,
146
+ });
147
+ document.head.appendChild(link);
148
+ inserted.push(link);
149
+ }
150
+
151
+ if (definition.htmlAttributes) {
152
+ restoreFns.push(setAttributes(document.documentElement, definition.htmlAttributes));
153
+ }
154
+
155
+ if (definition.bodyAttributes && document.body) {
156
+ restoreFns.push(setAttributes(document.body, definition.bodyAttributes));
157
+ }
158
+
159
+ return () => {
160
+ document.title = previousTitle;
161
+ for (const restore of restoreFns.reverse()) {
162
+ restore();
163
+ }
164
+ for (const element of inserted) {
165
+ element.remove();
166
+ }
167
+ };
168
+ };
@@ -1,215 +1,215 @@
1
- /**
2
- * Unified storage adapters for web platform storage APIs.
3
- * Provides a consistent, promise-based interface with predictable errors.
4
- */
5
-
6
- /**
7
- * Common interface for all storage adapters.
8
- * All methods return promises for a unified async API.
9
- */
10
- export interface StorageAdapter {
11
- /**
12
- * Retrieve a value by key.
13
- * @param key - The storage key
14
- * @returns The stored value or null if not found
15
- */
16
- get<T>(key: string): Promise<T | null>;
17
-
18
- /**
19
- * Store a value by key.
20
- * @param key - The storage key
21
- * @param value - The value to store
22
- */
23
- set<T>(key: string, value: T): Promise<void>;
24
-
25
- /**
26
- * Remove a value by key.
27
- * @param key - The storage key
28
- */
29
- remove(key: string): Promise<void>;
30
-
31
- /**
32
- * Clear all stored values.
33
- */
34
- clear(): Promise<void>;
35
-
36
- /**
37
- * Get all storage keys.
38
- * @returns Array of all keys
39
- */
40
- keys(): Promise<string[]>;
41
- }
42
-
43
- /**
44
- * Abstract base class for web storage adapters (localStorage/sessionStorage).
45
- * Implements DRY principle by sharing common logic.
46
- */
47
- abstract class WebStorageAdapter implements StorageAdapter {
48
- constructor(protected readonly storage: Storage) {}
49
-
50
- async get<T>(key: string): Promise<T | null> {
51
- const raw = this.storage.getItem(key);
52
- if (raw === null) return null;
53
- try {
54
- return JSON.parse(raw) as T;
55
- } catch {
56
- return raw as unknown as T;
57
- }
58
- }
59
-
60
- async set<T>(key: string, value: T): Promise<void> {
61
- const serialized = typeof value === 'string' ? value : JSON.stringify(value);
62
- this.storage.setItem(key, serialized);
63
- }
64
-
65
- async remove(key: string): Promise<void> {
66
- this.storage.removeItem(key);
67
- }
68
-
69
- async clear(): Promise<void> {
70
- this.storage.clear();
71
- }
72
-
73
- async keys(): Promise<string[]> {
74
- const result: string[] = [];
75
- for (let i = 0; i < this.storage.length; i++) {
76
- const key = this.storage.key(i);
77
- if (key !== null) {
78
- result.push(key);
79
- }
80
- }
81
- return result;
82
- }
83
- }
84
-
85
- /**
86
- * localStorage adapter with async interface.
87
- */
88
- class LocalStorageAdapter extends WebStorageAdapter {
89
- constructor() {
90
- super(localStorage);
91
- }
92
- }
93
-
94
- /**
95
- * sessionStorage adapter with async interface.
96
- */
97
- class SessionStorageAdapter extends WebStorageAdapter {
98
- constructor() {
99
- super(sessionStorage);
100
- }
101
- }
102
-
103
- /**
104
- * IndexedDB configuration options.
105
- */
106
- export interface IndexedDBOptions {
107
- /** Database name */
108
- name: string;
109
- /** Object store name */
110
- store: string;
111
- /** Database version (optional) */
112
- version?: number;
113
- }
114
-
115
- /**
116
- * IndexedDB key-value adapter.
117
- * Wraps IndexedDB with a simple key-value interface.
118
- */
119
- class IndexedDBAdapter implements StorageAdapter {
120
- private dbPromise: Promise<IDBDatabase> | null = null;
121
-
122
- constructor(private readonly options: IndexedDBOptions) {}
123
-
124
- /**
125
- * Opens or creates the IndexedDB database.
126
- */
127
- private openDB(): Promise<IDBDatabase> {
128
- if (this.dbPromise) return this.dbPromise;
129
-
130
- this.dbPromise = new Promise((resolve, reject) => {
131
- const request = indexedDB.open(this.options.name, this.options.version ?? 1);
132
-
133
- request.onupgradeneeded = () => {
134
- const db = request.result;
135
- if (!db.objectStoreNames.contains(this.options.store)) {
136
- db.createObjectStore(this.options.store);
137
- }
138
- };
139
-
140
- request.onsuccess = () => resolve(request.result);
141
- request.onerror = () => reject(request.error);
142
- });
143
-
144
- return this.dbPromise;
145
- }
146
-
147
- /**
148
- * Executes a transaction on the object store.
149
- */
150
- private async withStore<T>(
151
- mode: IDBTransactionMode,
152
- operation: (store: IDBObjectStore) => IDBRequest<T>
153
- ): Promise<T> {
154
- const db = await this.openDB();
155
- return new Promise((resolve, reject) => {
156
- const tx = db.transaction(this.options.store, mode);
157
- const store = tx.objectStore(this.options.store);
158
- const request = operation(store);
159
- request.onsuccess = () => resolve(request.result);
160
- request.onerror = () => reject(request.error);
161
- });
162
- }
163
-
164
- async get<T>(key: string): Promise<T | null> {
165
- const result = await this.withStore<T | undefined>('readonly', (store) => store.get(key));
166
- return result ?? null;
167
- }
168
-
169
- async set<T>(key: string, value: T): Promise<void> {
170
- await this.withStore('readwrite', (store) => store.put(value, key));
171
- }
172
-
173
- async remove(key: string): Promise<void> {
174
- await this.withStore('readwrite', (store) => store.delete(key));
175
- }
176
-
177
- async clear(): Promise<void> {
178
- await this.withStore('readwrite', (store) => store.clear());
179
- }
180
-
181
- async keys(): Promise<string[]> {
182
- const result = await this.withStore<IDBValidKey[]>('readonly', (store) => store.getAllKeys());
183
- return result.map((key) => String(key));
184
- }
185
- }
186
-
187
- /**
188
- * Storage factory providing access to different storage adapters.
189
- */
190
- export const storage = {
191
- /**
192
- * Create a localStorage adapter.
193
- * @returns StorageAdapter wrapping localStorage
194
- */
195
- local(): StorageAdapter {
196
- return new LocalStorageAdapter();
197
- },
198
-
199
- /**
200
- * Create a sessionStorage adapter.
201
- * @returns StorageAdapter wrapping sessionStorage
202
- */
203
- session(): StorageAdapter {
204
- return new SessionStorageAdapter();
205
- },
206
-
207
- /**
208
- * Create an IndexedDB adapter with key-value interface.
209
- * @param options - Database and store configuration
210
- * @returns StorageAdapter wrapping IndexedDB
211
- */
212
- indexedDB(options: IndexedDBOptions): StorageAdapter {
213
- return new IndexedDBAdapter(options);
214
- },
215
- };
1
+ /**
2
+ * Unified storage adapters for web platform storage APIs.
3
+ * Provides a consistent, promise-based interface with predictable errors.
4
+ */
5
+
6
+ /**
7
+ * Common interface for all storage adapters.
8
+ * All methods return promises for a unified async API.
9
+ */
10
+ export interface StorageAdapter {
11
+ /**
12
+ * Retrieve a value by key.
13
+ * @param key - The storage key
14
+ * @returns The stored value or null if not found
15
+ */
16
+ get<T>(key: string): Promise<T | null>;
17
+
18
+ /**
19
+ * Store a value by key.
20
+ * @param key - The storage key
21
+ * @param value - The value to store
22
+ */
23
+ set<T>(key: string, value: T): Promise<void>;
24
+
25
+ /**
26
+ * Remove a value by key.
27
+ * @param key - The storage key
28
+ */
29
+ remove(key: string): Promise<void>;
30
+
31
+ /**
32
+ * Clear all stored values.
33
+ */
34
+ clear(): Promise<void>;
35
+
36
+ /**
37
+ * Get all storage keys.
38
+ * @returns Array of all keys
39
+ */
40
+ keys(): Promise<string[]>;
41
+ }
42
+
43
+ /**
44
+ * Abstract base class for web storage adapters (localStorage/sessionStorage).
45
+ * Implements DRY principle by sharing common logic.
46
+ */
47
+ abstract class WebStorageAdapter implements StorageAdapter {
48
+ constructor(protected readonly storage: Storage) {}
49
+
50
+ async get<T>(key: string): Promise<T | null> {
51
+ const raw = this.storage.getItem(key);
52
+ if (raw === null) return null;
53
+ try {
54
+ return JSON.parse(raw) as T;
55
+ } catch {
56
+ return raw as unknown as T;
57
+ }
58
+ }
59
+
60
+ async set<T>(key: string, value: T): Promise<void> {
61
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
62
+ this.storage.setItem(key, serialized);
63
+ }
64
+
65
+ async remove(key: string): Promise<void> {
66
+ this.storage.removeItem(key);
67
+ }
68
+
69
+ async clear(): Promise<void> {
70
+ this.storage.clear();
71
+ }
72
+
73
+ async keys(): Promise<string[]> {
74
+ const result: string[] = [];
75
+ for (let i = 0; i < this.storage.length; i++) {
76
+ const key = this.storage.key(i);
77
+ if (key !== null) {
78
+ result.push(key);
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * localStorage adapter with async interface.
87
+ */
88
+ class LocalStorageAdapter extends WebStorageAdapter {
89
+ constructor() {
90
+ super(localStorage);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * sessionStorage adapter with async interface.
96
+ */
97
+ class SessionStorageAdapter extends WebStorageAdapter {
98
+ constructor() {
99
+ super(sessionStorage);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * IndexedDB configuration options.
105
+ */
106
+ export interface IndexedDBOptions {
107
+ /** Database name */
108
+ name: string;
109
+ /** Object store name */
110
+ store: string;
111
+ /** Database version (optional) */
112
+ version?: number;
113
+ }
114
+
115
+ /**
116
+ * IndexedDB key-value adapter.
117
+ * Wraps IndexedDB with a simple key-value interface.
118
+ */
119
+ class IndexedDBAdapter implements StorageAdapter {
120
+ private dbPromise: Promise<IDBDatabase> | null = null;
121
+
122
+ constructor(private readonly options: IndexedDBOptions) {}
123
+
124
+ /**
125
+ * Opens or creates the IndexedDB database.
126
+ */
127
+ private openDB(): Promise<IDBDatabase> {
128
+ if (this.dbPromise) return this.dbPromise;
129
+
130
+ this.dbPromise = new Promise((resolve, reject) => {
131
+ const request = indexedDB.open(this.options.name, this.options.version ?? 1);
132
+
133
+ request.onupgradeneeded = () => {
134
+ const db = request.result;
135
+ if (!db.objectStoreNames.contains(this.options.store)) {
136
+ db.createObjectStore(this.options.store);
137
+ }
138
+ };
139
+
140
+ request.onsuccess = () => resolve(request.result);
141
+ request.onerror = () => reject(request.error);
142
+ });
143
+
144
+ return this.dbPromise;
145
+ }
146
+
147
+ /**
148
+ * Executes a transaction on the object store.
149
+ */
150
+ private async withStore<T>(
151
+ mode: IDBTransactionMode,
152
+ operation: (store: IDBObjectStore) => IDBRequest<T>
153
+ ): Promise<T> {
154
+ const db = await this.openDB();
155
+ return new Promise((resolve, reject) => {
156
+ const tx = db.transaction(this.options.store, mode);
157
+ const store = tx.objectStore(this.options.store);
158
+ const request = operation(store);
159
+ request.onsuccess = () => resolve(request.result);
160
+ request.onerror = () => reject(request.error);
161
+ });
162
+ }
163
+
164
+ async get<T>(key: string): Promise<T | null> {
165
+ const result = await this.withStore<T | undefined>('readonly', (store) => store.get(key));
166
+ return result ?? null;
167
+ }
168
+
169
+ async set<T>(key: string, value: T): Promise<void> {
170
+ await this.withStore('readwrite', (store) => store.put(value, key));
171
+ }
172
+
173
+ async remove(key: string): Promise<void> {
174
+ await this.withStore('readwrite', (store) => store.delete(key));
175
+ }
176
+
177
+ async clear(): Promise<void> {
178
+ await this.withStore('readwrite', (store) => store.clear());
179
+ }
180
+
181
+ async keys(): Promise<string[]> {
182
+ const result = await this.withStore<IDBValidKey[]>('readonly', (store) => store.getAllKeys());
183
+ return result.map((key) => String(key));
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Storage factory providing access to different storage adapters.
189
+ */
190
+ export const storage = {
191
+ /**
192
+ * Create a localStorage adapter.
193
+ * @returns StorageAdapter wrapping localStorage
194
+ */
195
+ local(): StorageAdapter {
196
+ return new LocalStorageAdapter();
197
+ },
198
+
199
+ /**
200
+ * Create a sessionStorage adapter.
201
+ * @returns StorageAdapter wrapping sessionStorage
202
+ */
203
+ session(): StorageAdapter {
204
+ return new SessionStorageAdapter();
205
+ },
206
+
207
+ /**
208
+ * Create an IndexedDB adapter with key-value interface.
209
+ * @param options - Database and store configuration
210
+ * @returns StorageAdapter wrapping IndexedDB
211
+ */
212
+ indexedDB(options: IndexedDBOptions): StorageAdapter {
213
+ return new IndexedDBAdapter(options);
214
+ },
215
+ };