@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.
- package/README.md +586 -527
- package/dist/component/component.d.ts +13 -5
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/html.d.ts +40 -3
- package/dist/component/html.d.ts.map +1 -1
- package/dist/component/index.d.ts +4 -2
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/library.d.ts +34 -0
- package/dist/component/library.d.ts.map +1 -0
- package/dist/component/types.d.ts +132 -13
- package/dist/component/types.d.ts.map +1 -1
- package/dist/component-BEQgt5hl.js +600 -0
- package/dist/component-BEQgt5hl.js.map +1 -0
- package/dist/component.es.mjs +7 -184
- package/dist/config-DRmZZno3.js +40 -0
- package/dist/config-DRmZZno3.js.map +1 -0
- package/dist/core-BGQJVw0-.js +35 -0
- package/dist/core-BGQJVw0-.js.map +1 -0
- package/dist/core-CCEabVHl.js +648 -0
- package/dist/core-CCEabVHl.js.map +1 -0
- package/dist/core.es.mjs +45 -1261
- package/dist/effect-AFRW_Plg.js +84 -0
- package/dist/effect-AFRW_Plg.js.map +1 -0
- package/dist/full.d.ts +8 -8
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +101 -91
- package/dist/full.iife.js +173 -3
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +173 -3
- package/dist/full.umd.js.map +1 -1
- package/dist/index.es.mjs +147 -139
- package/dist/motion/transition.d.ts +1 -1
- package/dist/motion/transition.d.ts.map +1 -1
- package/dist/motion/types.d.ts +11 -1
- package/dist/motion/types.d.ts.map +1 -1
- package/dist/motion-D9TcHxOF.js +415 -0
- package/dist/motion-D9TcHxOF.js.map +1 -0
- package/dist/motion.es.mjs +25 -361
- package/dist/object-qGpWr6-J.js +38 -0
- package/dist/object-qGpWr6-J.js.map +1 -0
- package/dist/platform/announcer.d.ts +59 -0
- package/dist/platform/announcer.d.ts.map +1 -0
- package/dist/platform/config.d.ts +92 -0
- package/dist/platform/config.d.ts.map +1 -0
- package/dist/platform/cookies.d.ts +45 -0
- package/dist/platform/cookies.d.ts.map +1 -0
- package/dist/platform/index.d.ts +8 -0
- package/dist/platform/index.d.ts.map +1 -1
- package/dist/platform/meta.d.ts +62 -0
- package/dist/platform/meta.d.ts.map +1 -0
- package/dist/platform-Dr9b6fsq.js +362 -0
- package/dist/platform-Dr9b6fsq.js.map +1 -0
- package/dist/platform.es.mjs +11 -248
- package/dist/reactive/async-data.d.ts +114 -0
- package/dist/reactive/async-data.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/signal.d.ts +2 -0
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive-DSkct0dO.js +254 -0
- package/dist/reactive-DSkct0dO.js.map +1 -0
- package/dist/reactive.es.mjs +18 -32
- package/dist/router-CbDhl8rS.js +188 -0
- package/dist/router-CbDhl8rS.js.map +1 -0
- package/dist/router.es.mjs +11 -200
- package/dist/sanitize-Bs2dkMby.js +313 -0
- package/dist/sanitize-Bs2dkMby.js.map +1 -0
- package/dist/security/constants.d.ts.map +1 -1
- package/dist/security/index.d.ts +4 -2
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/sanitize.d.ts +4 -1
- package/dist/security/sanitize.d.ts.map +1 -1
- package/dist/security/trusted-html.d.ts +53 -0
- package/dist/security/trusted-html.d.ts.map +1 -0
- package/dist/security.es.mjs +11 -56
- package/dist/store/define-store.d.ts +1 -1
- package/dist/store/define-store.d.ts.map +1 -1
- package/dist/store/mapping.d.ts +1 -1
- package/dist/store/mapping.d.ts.map +1 -1
- package/dist/store/persisted.d.ts +1 -1
- package/dist/store/persisted.d.ts.map +1 -1
- package/dist/store/types.d.ts +2 -2
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/watch.d.ts +1 -1
- package/dist/store/watch.d.ts.map +1 -1
- package/dist/store-BwDvI45q.js +263 -0
- package/dist/store-BwDvI45q.js.map +1 -0
- package/dist/store.es.mjs +12 -25
- package/dist/storybook/index.d.ts +37 -0
- package/dist/storybook/index.d.ts.map +1 -0
- package/dist/storybook.es.mjs +151 -0
- package/dist/storybook.es.mjs.map +1 -0
- package/dist/untrack-B0rVscTc.js +7 -0
- package/dist/untrack-B0rVscTc.js.map +1 -0
- package/dist/view-C70lA3vf.js +397 -0
- package/dist/view-C70lA3vf.js.map +1 -0
- package/dist/view.es.mjs +11 -430
- package/package.json +141 -132
- package/src/component/component.ts +524 -289
- package/src/component/html.ts +153 -53
- package/src/component/index.ts +50 -40
- package/src/component/library.ts +518 -0
- package/src/component/types.ts +256 -85
- package/src/core/collection.ts +628 -628
- package/src/core/element.ts +774 -774
- package/src/core/index.ts +48 -48
- package/src/core/utils/function.ts +151 -151
- package/src/full.ts +229 -187
- package/src/motion/animate.ts +113 -113
- package/src/motion/flip.ts +176 -176
- package/src/motion/scroll.ts +57 -57
- package/src/motion/spring.ts +150 -150
- package/src/motion/timeline.ts +246 -246
- package/src/motion/transition.ts +97 -51
- package/src/motion/types.ts +11 -1
- package/src/platform/announcer.ts +208 -0
- package/src/platform/config.ts +163 -0
- package/src/platform/cookies.ts +165 -0
- package/src/platform/index.ts +21 -0
- package/src/platform/meta.ts +168 -0
- package/src/platform/storage.ts +215 -215
- package/src/reactive/async-data.ts +486 -0
- package/src/reactive/core.ts +114 -114
- package/src/reactive/effect.ts +54 -54
- package/src/reactive/index.ts +15 -1
- package/src/reactive/internals.ts +122 -122
- package/src/reactive/signal.ts +9 -0
- package/src/security/constants.ts +3 -1
- package/src/security/index.ts +17 -10
- package/src/security/sanitize-core.ts +364 -364
- package/src/security/sanitize.ts +70 -66
- package/src/security/trusted-html.ts +71 -0
- package/src/store/define-store.ts +49 -48
- package/src/store/mapping.ts +74 -73
- package/src/store/persisted.ts +62 -61
- package/src/store/types.ts +92 -94
- package/src/store/watch.ts +53 -52
- package/src/storybook/index.ts +479 -0
- package/src/view/evaluate.ts +290 -290
- package/dist/batch-x7b2eZST.js +0 -13
- package/dist/batch-x7b2eZST.js.map +0 -1
- package/dist/component.es.mjs.map +0 -1
- package/dist/core-BhpuvPhy.js +0 -170
- package/dist/core-BhpuvPhy.js.map +0 -1
- package/dist/core.es.mjs.map +0 -1
- package/dist/full.es.mjs.map +0 -1
- package/dist/index.es.mjs.map +0 -1
- package/dist/motion.es.mjs.map +0 -1
- package/dist/persisted-DHoi3uEs.js +0 -278
- package/dist/persisted-DHoi3uEs.js.map +0 -1
- package/dist/platform.es.mjs.map +0 -1
- package/dist/reactive.es.mjs.map +0 -1
- package/dist/router.es.mjs.map +0 -1
- package/dist/sanitize-Cxvxa-DX.js +0 -283
- package/dist/sanitize-Cxvxa-DX.js.map +0 -1
- package/dist/security.es.mjs.map +0 -1
- package/dist/store.es.mjs.map +0 -1
- package/dist/type-guards-BdKlYYlS.js +0 -32
- package/dist/type-guards-BdKlYYlS.js.map +0 -1
- package/dist/untrack-DNnnqdlR.js +0 -6
- package/dist/untrack-DNnnqdlR.js.map +0 -1
- package/dist/view.es.mjs.map +0 -1
- package/dist/watch-DXXv3iAI.js +0 -58
- 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
|
+
};
|
package/src/platform/storage.ts
CHANGED
|
@@ -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
|
+
};
|