@adukiorg/anza 0.2.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/CHANGELOG.md +137 -0
- package/README.md +215 -0
- package/bin/anza.js +63 -0
- package/bin/create.js +150 -0
- package/importmap.json +72 -0
- package/package.json +100 -0
- package/src/core/animations/index.js +55 -0
- package/src/core/animations/play.js +111 -0
- package/src/core/animations/registry.js +54 -0
- package/src/core/animations/scroll.js +50 -0
- package/src/core/animations/tokens.js +58 -0
- package/src/core/animations/usage.md +301 -0
- package/src/core/animations/waapi.js +86 -0
- package/src/core/api/cache.js +120 -0
- package/src/core/api/caches/glob.js +24 -0
- package/src/core/api/caches/index.js +118 -0
- package/src/core/api/events/index.js +75 -0
- package/src/core/api/fetch.js +99 -0
- package/src/core/api/index.js +158 -0
- package/src/core/api/pipeline.js +98 -0
- package/src/core/api/plan.md +209 -0
- package/src/core/api/prefixes/index.js +66 -0
- package/src/core/api/retry.js +69 -0
- package/src/core/api/stream.js +127 -0
- package/src/core/api/upload.js +180 -0
- package/src/core/api/usage.md +206 -0
- package/src/core/events/bus.js +38 -0
- package/src/core/events/delegate.js +79 -0
- package/src/core/events/index.js +26 -0
- package/src/core/events/listen.js +50 -0
- package/src/core/events/missing.md +103 -0
- package/src/core/events/once.js +49 -0
- package/src/core/events/plan.md +177 -0
- package/src/core/events/types/index.js +34 -0
- package/src/core/events/usage.md +107 -0
- package/src/core/offline/bridge.js +51 -0
- package/src/core/offline/clock.js +100 -0
- package/src/core/offline/connectivity.js +116 -0
- package/src/core/offline/index.js +41 -0
- package/src/core/offline/missing.md +89 -0
- package/src/core/offline/plan.md +143 -0
- package/src/core/offline/queue.js +168 -0
- package/src/core/offline/state.js +18 -0
- package/src/core/offline/sync.js +106 -0
- package/src/core/offline/usage.md +273 -0
- package/src/core/platform/guard.js +104 -0
- package/src/core/platform/index.js +42 -0
- package/src/core/platform/missing.md +119 -0
- package/src/core/platform/platform.d.ts +88 -0
- package/src/core/platform/polyfills/anchor.js +79 -0
- package/src/core/platform/polyfills/navigation.js +142 -0
- package/src/core/platform/polyfills/popover.js +142 -0
- package/src/core/platform/polyfills/scheduler.js +194 -0
- package/src/core/platform/polyfills/shadow.js +35 -0
- package/src/core/platform/polyfills/urlpattern.js +119 -0
- package/src/core/platform/supports.js +186 -0
- package/src/core/platform/usage.md +287 -0
- package/src/core/router/cache.js +95 -0
- package/src/core/router/container.js +146 -0
- package/src/core/router/handler.js +52 -0
- package/src/core/router/history.js +120 -0
- package/src/core/router/index.js +158 -0
- package/src/core/router/intercept.js +376 -0
- package/src/core/router/match.js +145 -0
- package/src/core/router/missing.md +716 -0
- package/src/core/router/outlet.js +139 -0
- package/src/core/router/plan.md +370 -0
- package/src/core/router/sync/index.js +16 -0
- package/src/core/router/sync/tab.js +115 -0
- package/src/core/router/sync/transport.js +139 -0
- package/src/core/router/transitions.js +59 -0
- package/src/core/router/usage.md +773 -0
- package/src/core/security/crypto.js +159 -0
- package/src/core/security/index.js +49 -0
- package/src/core/security/missing.md +97 -0
- package/src/core/security/permissions.js +64 -0
- package/src/core/security/sanitize.js +100 -0
- package/src/core/security/usage.md +283 -0
- package/src/core/state/derived.js +117 -0
- package/src/core/state/index.js +23 -0
- package/src/core/state/missing.md +165 -0
- package/src/core/state/persist.js +284 -0
- package/src/core/state/store.js +308 -0
- package/src/core/state/sync.js +46 -0
- package/src/core/state/usage.md +440 -0
- package/src/core/storage/cache.js +83 -0
- package/src/core/storage/idb.js +196 -0
- package/src/core/storage/index.js +373 -0
- package/src/core/storage/lru.js +107 -0
- package/src/core/storage/missing.md +165 -0
- package/src/core/storage/opfs.js +190 -0
- package/src/core/storage/plan.md +69 -0
- package/src/core/storage/quota.js +69 -0
- package/src/core/storage/usage.md +226 -0
- package/src/core/ui/base.js +50 -0
- package/src/core/ui/define/container.js +82 -0
- package/src/core/ui/define/define.js +12 -0
- package/src/core/ui/define/element.js +390 -0
- package/src/core/ui/define/index.js +9 -0
- package/src/core/ui/define/orchestrator.js +105 -0
- package/src/core/ui/define/proxy.js +644 -0
- package/src/core/ui/define/state.js +6 -0
- package/src/core/ui/define/utils.js +134 -0
- package/src/core/ui/implementation.md +170 -0
- package/src/core/ui/index.js +41 -0
- package/src/core/ui/observe.js +117 -0
- package/src/core/ui/plan.md +510 -0
- package/src/core/ui/schedule.js +60 -0
- package/src/core/ui/template.js +37 -0
- package/src/core/ui/transitions.js +37 -0
- package/src/core/ui/ui.types.md +890 -0
- package/src/core/ui/usage.md +1124 -0
- package/src/core/ui/watch.md +346 -0
- package/src/core/workers/broadcast.js +138 -0
- package/src/core/workers/dedicated.js +153 -0
- package/src/core/workers/index.js +169 -0
- package/src/core/workers/locks.js +160 -0
- package/src/core/workers/offscreen.js +166 -0
- package/src/core/workers/plan.md +381 -0
- package/src/core/workers/pool.js +267 -0
- package/src/core/workers/shared.js +137 -0
- package/src/core/workers/usage.md +622 -0
- package/src/elements/base.js +12 -0
- package/src/elements/data/card/index.html +9 -0
- package/src/elements/data/card/index.js +19 -0
- package/src/elements/data/card/index.tags.json +1 -0
- package/src/elements/data/card/style.css +46 -0
- package/src/elements/data/chart/index.html +1 -0
- package/src/elements/data/chart/index.js +143 -0
- package/src/elements/data/chart/index.tags.json +1 -0
- package/src/elements/data/chart/style.css +13 -0
- package/src/elements/data/list/index.html +3 -0
- package/src/elements/data/list/index.js +19 -0
- package/src/elements/data/list/index.tags.json +1 -0
- package/src/elements/data/list/style.css +39 -0
- package/src/elements/data/stat/index.html +9 -0
- package/src/elements/data/stat/index.js +19 -0
- package/src/elements/data/stat/index.tags.json +1 -0
- package/src/elements/data/stat/style.css +50 -0
- package/src/elements/data/table/index.html +1 -0
- package/src/elements/data/table/index.js +16 -0
- package/src/elements/data/table/index.tags.json +1 -0
- package/src/elements/data/table/style.css +50 -0
- package/src/elements/feedback/alert/index.html +11 -0
- package/src/elements/feedback/alert/index.js +28 -0
- package/src/elements/feedback/alert/index.tags.json +1 -0
- package/src/elements/feedback/alert/style.css +75 -0
- package/src/elements/feedback/empty/index.html +13 -0
- package/src/elements/feedback/empty/index.js +34 -0
- package/src/elements/feedback/empty/index.tags.json +1 -0
- package/src/elements/feedback/empty/style.css +45 -0
- package/src/elements/feedback/progress/index.html +7 -0
- package/src/elements/feedback/progress/index.js +46 -0
- package/src/elements/feedback/progress/index.tags.json +1 -0
- package/src/elements/feedback/progress/style.css +36 -0
- package/src/elements/feedback/skeleton/index.html +1 -0
- package/src/elements/feedback/skeleton/index.js +78 -0
- package/src/elements/feedback/skeleton/index.tags.json +1 -0
- package/src/elements/feedback/skeleton/style.css +28 -0
- package/src/elements/feedback/toast/index.html +3 -0
- package/src/elements/feedback/toast/index.js +65 -0
- package/src/elements/feedback/toast/index.tags.json +1 -0
- package/src/elements/feedback/toast/style.css +36 -0
- package/src/elements/forms/checkbox/index.html +7 -0
- package/src/elements/forms/checkbox/index.js +104 -0
- package/src/elements/forms/checkbox/index.tags.json +1 -0
- package/src/elements/forms/checkbox/style.css +86 -0
- package/src/elements/forms/field/index.html +13 -0
- package/src/elements/forms/field/index.js +42 -0
- package/src/elements/forms/field/index.tags.json +1 -0
- package/src/elements/forms/field/style.css +42 -0
- package/src/elements/forms/form/index.html +3 -0
- package/src/elements/forms/form/index.js +122 -0
- package/src/elements/forms/form/index.tags.json +1 -0
- package/src/elements/forms/form/style.css +11 -0
- package/src/elements/forms/input/index.html +4 -0
- package/src/elements/forms/input/index.js +103 -0
- package/src/elements/forms/input/index.tags.json +1 -0
- package/src/elements/forms/input/style.css +39 -0
- package/src/elements/forms/radio/index.html +4 -0
- package/src/elements/forms/radio/index.js +109 -0
- package/src/elements/forms/radio/index.tags.json +1 -0
- package/src/elements/forms/radio/style.css +65 -0
- package/src/elements/forms/select/index.html +9 -0
- package/src/elements/forms/select/index.js +114 -0
- package/src/elements/forms/select/index.tags.json +1 -0
- package/src/elements/forms/select/style.css +95 -0
- package/src/elements/forms/textarea/index.html +4 -0
- package/src/elements/forms/textarea/index.js +115 -0
- package/src/elements/forms/textarea/index.tags.json +1 -0
- package/src/elements/forms/textarea/style.css +46 -0
- package/src/elements/forms/toggle/index.html +4 -0
- package/src/elements/forms/toggle/index.js +89 -0
- package/src/elements/forms/toggle/index.tags.json +1 -0
- package/src/elements/forms/toggle/style.css +63 -0
- package/src/elements/forms/upload/index.html +13 -0
- package/src/elements/forms/upload/index.js +120 -0
- package/src/elements/forms/upload/index.tags.json +1 -0
- package/src/elements/forms/upload/style.css +61 -0
- package/src/elements/index.js +71 -0
- package/src/elements/layout/app/index.html +7 -0
- package/src/elements/layout/app/index.js +16 -0
- package/src/elements/layout/app/index.tags.json +1 -0
- package/src/elements/layout/app/style.css +41 -0
- package/src/elements/layout/grid/index.html +3 -0
- package/src/elements/layout/grid/index.js +41 -0
- package/src/elements/layout/grid/index.tags.json +1 -0
- package/src/elements/layout/grid/style.css +12 -0
- package/src/elements/layout/header/index.html +8 -0
- package/src/elements/layout/header/index.js +16 -0
- package/src/elements/layout/header/index.tags.json +1 -0
- package/src/elements/layout/header/style.css +28 -0
- package/src/elements/layout/scroll/index.html +3 -0
- package/src/elements/layout/scroll/index.js +19 -0
- package/src/elements/layout/scroll/index.tags.json +1 -0
- package/src/elements/layout/scroll/style.css +24 -0
- package/src/elements/layout/sidebar/index.html +3 -0
- package/src/elements/layout/sidebar/index.js +24 -0
- package/src/elements/layout/sidebar/index.tags.json +1 -0
- package/src/elements/layout/sidebar/style.css +30 -0
- package/src/elements/layout/split/index.html +3 -0
- package/src/elements/layout/split/index.js +18 -0
- package/src/elements/layout/split/index.tags.json +1 -0
- package/src/elements/layout/split/style.css +28 -0
- package/src/elements/layout/stack/index.html +3 -0
- package/src/elements/layout/stack/index.js +31 -0
- package/src/elements/layout/stack/index.tags.json +1 -0
- package/src/elements/layout/stack/style.css +15 -0
- package/src/elements/layout/surface/index.html +3 -0
- package/src/elements/layout/surface/index.js +19 -0
- package/src/elements/layout/surface/index.tags.json +1 -0
- package/src/elements/layout/surface/style.css +29 -0
- package/src/elements/navigation/breadcrumb/index.html +5 -0
- package/src/elements/navigation/breadcrumb/index.js +16 -0
- package/src/elements/navigation/breadcrumb/index.tags.json +1 -0
- package/src/elements/navigation/breadcrumb/style.css +36 -0
- package/src/elements/navigation/nav/index.html +3 -0
- package/src/elements/navigation/nav/index.js +24 -0
- package/src/elements/navigation/nav/index.tags.json +1 -0
- package/src/elements/navigation/nav/style.css +38 -0
- package/src/elements/navigation/pagination/index.html +3 -0
- package/src/elements/navigation/pagination/index.js +94 -0
- package/src/elements/navigation/pagination/index.tags.json +1 -0
- package/src/elements/navigation/pagination/style.css +39 -0
- package/src/elements/navigation/steps/index.html +6 -0
- package/src/elements/navigation/steps/index.js +64 -0
- package/src/elements/navigation/steps/index.tags.json +1 -0
- package/src/elements/navigation/steps/style.css +78 -0
- package/src/elements/navigation/tabs/index.html +6 -0
- package/src/elements/navigation/tabs/index.js +132 -0
- package/src/elements/navigation/tabs/index.tags.json +1 -0
- package/src/elements/navigation/tabs/style.css +52 -0
- package/src/elements/overlay/dialog/index.html +5 -0
- package/src/elements/overlay/dialog/index.js +57 -0
- package/src/elements/overlay/dialog/index.tags.json +1 -0
- package/src/elements/overlay/dialog/style.css +31 -0
- package/src/elements/overlay/drawer/index.html +3 -0
- package/src/elements/overlay/drawer/index.js +56 -0
- package/src/elements/overlay/drawer/index.tags.json +1 -0
- package/src/elements/overlay/drawer/style.css +48 -0
- package/src/elements/overlay/menu/index.html +3 -0
- package/src/elements/overlay/menu/index.js +107 -0
- package/src/elements/overlay/menu/index.tags.json +1 -0
- package/src/elements/overlay/menu/style.css +43 -0
- package/src/elements/overlay/popover/index.html +3 -0
- package/src/elements/overlay/popover/index.js +44 -0
- package/src/elements/overlay/popover/index.tags.json +1 -0
- package/src/elements/overlay/popover/style.css +21 -0
- package/src/elements/overlay/sheet/index.html +8 -0
- package/src/elements/overlay/sheet/index.js +105 -0
- package/src/elements/overlay/sheet/index.tags.json +1 -0
- package/src/elements/overlay/sheet/style.css +64 -0
- package/src/elements/overlay/tooltip/index.html +6 -0
- package/src/elements/overlay/tooltip/index.js +16 -0
- package/src/elements/overlay/tooltip/index.tags.json +1 -0
- package/src/elements/overlay/tooltip/style.css +41 -0
- package/src/elements/primitives/avatar/index.html +2 -0
- package/src/elements/primitives/avatar/index.js +79 -0
- package/src/elements/primitives/avatar/index.tags.json +1 -0
- package/src/elements/primitives/avatar/style.css +36 -0
- package/src/elements/primitives/badge/index.html +3 -0
- package/src/elements/primitives/badge/index.js +20 -0
- package/src/elements/primitives/badge/index.tags.json +1 -0
- package/src/elements/primitives/badge/style.css +67 -0
- package/src/elements/primitives/button/index.html +3 -0
- package/src/elements/primitives/button/index.js +61 -0
- package/src/elements/primitives/button/index.tags.json +1 -0
- package/src/elements/primitives/button/style.css +66 -0
- package/src/elements/primitives/divider/index.html +1 -0
- package/src/elements/primitives/divider/index.js +43 -0
- package/src/elements/primitives/divider/index.tags.json +1 -0
- package/src/elements/primitives/divider/style.css +39 -0
- package/src/elements/primitives/icon/index.html +3 -0
- package/src/elements/primitives/icon/index.js +66 -0
- package/src/elements/primitives/icon/index.tags.json +1 -0
- package/src/elements/primitives/icon/style.css +20 -0
- package/src/elements/primitives/link/index.html +3 -0
- package/src/elements/primitives/link/index.js +129 -0
- package/src/elements/primitives/link/index.tags.json +1 -0
- package/src/elements/primitives/link/style.css +40 -0
- package/src/elements/primitives/spinner/index.html +1 -0
- package/src/elements/primitives/spinner/index.js +62 -0
- package/src/elements/primitives/spinner/index.tags.json +1 -0
- package/src/elements/primitives/spinner/style.css +20 -0
- package/src/elements/primitives/text/index.html +1 -0
- package/src/elements/primitives/text/index.js +79 -0
- package/src/elements/primitives/text/index.tags.json +1 -0
- package/src/elements/primitives/text/style.css +25 -0
- package/src/index.js +23 -0
- package/src/styles/base.css +66 -0
- package/src/styles/index.css +10 -0
- package/src/styles/layers.css +9 -0
- package/src/styles/reset.css +66 -0
- package/src/sw/activate.js +51 -0
- package/src/sw/expire.js +47 -0
- package/src/sw/index.js +28 -0
- package/src/sw/install.js +35 -0
- package/src/sw/push.js +58 -0
- package/src/sw/queue.js +60 -0
- package/src/sw/routes.js +71 -0
- package/src/sw/strategies.js +247 -0
- package/src/sw/sync.js +80 -0
- package/src/tokens/index.css +26 -0
- package/src/tokens/primitives/colors.css +54 -0
- package/src/tokens/primitives/motion.css +34 -0
- package/src/tokens/primitives/radius.css +16 -0
- package/src/tokens/primitives/shadow.css +34 -0
- package/src/tokens/primitives/spacing.css +27 -0
- package/src/tokens/primitives/typography.css +46 -0
- package/src/tokens/primitives/zindex.css +18 -0
- package/src/tokens/registered/colors.css +133 -0
- package/src/tokens/registered/dimensions.css +31 -0
- package/src/tokens/semantic/components.css +125 -0
- package/src/tokens/semantic/contrast.css +33 -0
- package/src/tokens/semantic/dark.css +61 -0
- package/src/tokens/semantic/light.css +64 -0
- package/types/core/animations/index.d.ts +52 -0
- package/types/core/api/index.d.ts +68 -0
- package/types/core/events/index.d.ts +50 -0
- package/types/core/offline/index.d.ts +68 -0
- package/types/core/platform/index.d.ts +60 -0
- package/types/core/router/index.d.ts +203 -0
- package/types/core/security/index.d.ts +33 -0
- package/types/core/state/index.d.ts +68 -0
- package/types/core/storage/index.d.ts +40 -0
- package/types/core/ui/index.d.ts +446 -0
- package/types/core/workers/index.d.ts +221 -0
- package/types/elements/index.d.ts +150 -0
- package/types/index.d.ts +18 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# Native State Usage Guide
|
|
2
|
+
|
|
3
|
+
The Native State layer provides a lightweight, reactive, and persistent state management engine. It encapsulates reactive stores, opt-in deep tracking, derived computations, cross-tab synchronization, transaction-safe IndexedDB persistence, automated TTL/LRU cache eviction, and build-time store immutability validation.
|
|
4
|
+
|
|
5
|
+
Status: this guide documents the implemented public state contract: `state.create`, `ReactiveStore`, `derived`, `sync`, `storage`, `PlatformStorage`, deep reactivity, custom snapshots/cloners, transaction queues, and the build-time immutability linter.
|
|
6
|
+
|
|
7
|
+
Import from the state entry point:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import { state } from '@adukiorg/anza/state';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or import individual components directly:
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import { ReactiveStore, derived, sync, storage } from '@adukiorg/anza/state';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Choosing an API
|
|
22
|
+
|
|
23
|
+
| Need | Use |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| Initialize a reactive store | `state.create` or `new ReactiveStore` |
|
|
26
|
+
| Get a store property | `store.get` |
|
|
27
|
+
| Update a store property | `store.set` |
|
|
28
|
+
| Subscribe to property changes | `store.subscribe` |
|
|
29
|
+
| Clear or initialize state | `store.reset` |
|
|
30
|
+
| Restore state from snapshot | `store.hydrate` |
|
|
31
|
+
| Capture a cloned snapshot | `store.snapshot` |
|
|
32
|
+
| Opt-in nested object tracking | `options.deep` |
|
|
33
|
+
| Sync properties across tabs | `store.sync` or `state.sync` |
|
|
34
|
+
| Sync property with channel | `store.broadcast` or `state.broadcast` |
|
|
35
|
+
| Computed read-only state | `store.derived` or `state.derived` |
|
|
36
|
+
| Connect to IndexedDB storage | `storage` or `new PlatformStorage` |
|
|
37
|
+
| Store value in disk cache | `storage.set` |
|
|
38
|
+
| Retrieve value from disk cache | `storage.get` |
|
|
39
|
+
| Delete disk cache entry | `storage.delete` |
|
|
40
|
+
| Query disk cache entries | `storage.query` |
|
|
41
|
+
| Schema migrations | `storage.registerMigrations` |
|
|
42
|
+
| Disk space estimate | `storage.estimate` |
|
|
43
|
+
| Request persistent storage | `storage.persist` |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 2. Store Creation
|
|
48
|
+
|
|
49
|
+
Use `state.create(initial, options)` or `new ReactiveStore(initial, options)` to construct a reactive store.
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
const store = state.create({
|
|
53
|
+
active: false,
|
|
54
|
+
items: []
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Available options:
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
const store = new ReactiveStore({
|
|
62
|
+
user: { name: 'Alice', address: { city: 'Paris' } }
|
|
63
|
+
}, {
|
|
64
|
+
deep: true, // Enable deep reactivity proxying (default: false)
|
|
65
|
+
clone: (val) => myClone(val) // Custom cloner for snapshots (default: optimized fastClone)
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Rules:
|
|
70
|
+
|
|
71
|
+
- Pass plain objects or values that are structured-clone-safe.
|
|
72
|
+
- Custom cloners override the default snapshot copy behavior (useful for non-standard classes or complex structures).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 3. Reading and Writing
|
|
77
|
+
|
|
78
|
+
Read values using `store.get(key)` and update values using `store.set(key, value)`.
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
// Reading properties
|
|
82
|
+
const active = store.get('active');
|
|
83
|
+
|
|
84
|
+
// Writing properties
|
|
85
|
+
store.set('active', true);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Writes are batched and scheduled to notify subscribers inside a microtask. If you need to perform multiple updates sequentially without triggering multiple subscriber notifications, group them using `store.batch()`.
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
store.batch(() => {
|
|
92
|
+
store.set('active', true);
|
|
93
|
+
store.set('items', [1, 2, 3]);
|
|
94
|
+
});
|
|
95
|
+
// Subscribers are notified exactly once after the batch completes.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 4. Subscription Management
|
|
101
|
+
|
|
102
|
+
Use `store.subscribe(key, callback, signal?)` to listen to property changes.
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
const dispose = store.subscribe('active', (next, key, prev) => {
|
|
106
|
+
console.log(`${key} changed from ${prev} to ${next}`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Teardown the subscription
|
|
110
|
+
dispose();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
When integrating subscriptions inside component lifecycles, pass an `AbortSignal` to ensure automatic cleanup on unmount:
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
ui.element('ui-panel', {
|
|
117
|
+
mount({ el, ctrl }) {
|
|
118
|
+
store.subscribe('active', (active) => {
|
|
119
|
+
el.toggleAttribute('active', active);
|
|
120
|
+
}, ctrl.signal);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Rules:
|
|
126
|
+
|
|
127
|
+
- Subscriber callbacks receive `(next, key, prev)` — the current value, the key that changed, and the previous value.
|
|
128
|
+
- A callback subscribed to several keys is invoked once per flush, with one of the changed keys.
|
|
129
|
+
- Callbacks run asynchronously on microtask ticks unless batching is active.
|
|
130
|
+
- Aborted signals automatically clean up and remove the subscriber from the registry.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 5. Deep Reactivity
|
|
135
|
+
|
|
136
|
+
By default, stores only track top-level property assignments. Set `deep: true` to enable tracking of nested mutations:
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
const store = new ReactiveStore({
|
|
140
|
+
profile: { name: 'Bob', settings: { theme: 'dark' } }
|
|
141
|
+
}, { deep: true });
|
|
142
|
+
|
|
143
|
+
store.subscribe('profile', (next) => {
|
|
144
|
+
console.log('Profile changed:', next.settings.theme);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Mutating a nested key triggers the parent "profile" subscription
|
|
148
|
+
store.get('profile').settings.theme = 'light';
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Under the hood:
|
|
152
|
+
|
|
153
|
+
- The store wraps objects recursively in reactive `Proxy` traps.
|
|
154
|
+
- Mutations propagate up to mark the top-level keys as dirty.
|
|
155
|
+
- Mutated objects are cached in a `WeakMap` to avoid redundant proxy allocations.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 6. Snapshots and Cloners
|
|
160
|
+
|
|
161
|
+
Use `store.snapshot()` to extract a copy of the entire current state. You can restore this state later using `store.hydrate(state)` or clear it using `store.reset(state)`.
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
const backup = store.snapshot();
|
|
165
|
+
|
|
166
|
+
// Clear and override the current state
|
|
167
|
+
store.reset({ active: false });
|
|
168
|
+
|
|
169
|
+
// Restore the backup
|
|
170
|
+
store.hydrate(backup);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Cloning strategies:
|
|
174
|
+
|
|
175
|
+
- The default cloner uses an optimized `fastClone` path that avoids the overhead of JSON parsing or standard `structuredClone` for plain objects, arrays, and primitives. It falls back to constructor-based cloning for Date and RegExp.
|
|
176
|
+
- Provide a custom `clone` function in store options to handle domain-specific object structures.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 7. Derived Computations
|
|
181
|
+
|
|
182
|
+
Use `derived` to calculate read-only computed values that automatically react to store changes.
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
const store = state.create({ count: 2, price: 10 });
|
|
186
|
+
|
|
187
|
+
// Define derived state using state.derived (standalone)
|
|
188
|
+
const total = state.derived(() => {
|
|
189
|
+
return store.get('count') * store.get('price');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Or using store.derived (instance method)
|
|
193
|
+
const total = store.derived(() => {
|
|
194
|
+
return store.get('count') * store.get('price');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
console.log(total.value); // 20
|
|
198
|
+
|
|
199
|
+
// Derived state is an observable exposing .value
|
|
200
|
+
const stop = total.subscribe((next) => {
|
|
201
|
+
console.log(`New total: ${next}`);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
store.set('count', 3); // Triggers derived recalculation. total.value becomes 30.
|
|
205
|
+
|
|
206
|
+
stop();
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Rules:
|
|
210
|
+
|
|
211
|
+
- Derived functions capture store getters called during execution to automatically track dependencies.
|
|
212
|
+
- Subscriptions are dynamically added and removed as dependencies execute.
|
|
213
|
+
- Computation updates are batched and scheduled in microtasks.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 8. Cross-Tab Sync
|
|
218
|
+
|
|
219
|
+
Use the `sync` or `broadcast` delegates to coordinate state changes between multiple open browser tabs.
|
|
220
|
+
|
|
221
|
+
### State Synchronization
|
|
222
|
+
|
|
223
|
+
Sync coordinates state changes across same-origin tabs for specific keys using a BroadcastChannel:
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
// Using state.sync (standalone)
|
|
227
|
+
const stop = state.sync(store, ['theme', 'token'], {
|
|
228
|
+
channel: 'app-state-sync' // Optional custom channel name
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Using store.sync (instance method)
|
|
232
|
+
const stop = store.sync(['theme', 'token'], 'app-state-sync');
|
|
233
|
+
|
|
234
|
+
// To stop synchronization
|
|
235
|
+
stop();
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Broadcast Messaging
|
|
239
|
+
|
|
240
|
+
Use `broadcast` to emit specific key modifications or messages down a BroadcastChannel:
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
// Using state.broadcast (standalone)
|
|
244
|
+
const stop = state.broadcast(store, 'chat-events', ['message']);
|
|
245
|
+
|
|
246
|
+
// Using store.broadcast (instance method)
|
|
247
|
+
const stop = store.broadcast('chat-events', ['message']);
|
|
248
|
+
|
|
249
|
+
store.subscribe('message', (msg) => {
|
|
250
|
+
console.log('Sending message down channel:', msg);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
stop();
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## 9. Disk Persistence Layer
|
|
259
|
+
|
|
260
|
+
Use `storage` (an instance of `PlatformStorage`) to store data persistently in IndexedDB.
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
// Register schema migrations
|
|
264
|
+
storage.registerMigrations([
|
|
265
|
+
(db) => {
|
|
266
|
+
db.createObjectStore('settings');
|
|
267
|
+
}
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
await storage.open();
|
|
271
|
+
|
|
272
|
+
// Write to store
|
|
273
|
+
await storage.set('settings', 'theme', 'dark');
|
|
274
|
+
|
|
275
|
+
// Read from store
|
|
276
|
+
const theme = await storage.get('settings', 'theme');
|
|
277
|
+
|
|
278
|
+
// Delete from store
|
|
279
|
+
await storage.delete('settings', 'theme');
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
For custom database instances, construct your own `PlatformStorage`:
|
|
283
|
+
|
|
284
|
+
```javascript
|
|
285
|
+
const myDB = new PlatformStorage();
|
|
286
|
+
myDB.setDatabaseName('custom-platform-db');
|
|
287
|
+
myDB.registerMigrations([
|
|
288
|
+
(db) => {
|
|
289
|
+
db.createObjectStore('cache');
|
|
290
|
+
}
|
|
291
|
+
]);
|
|
292
|
+
await myDB.open();
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## 10. Transaction Queuing & Retries
|
|
298
|
+
|
|
299
|
+
All database writes (`set`, `delete`) are routed through a sequence-preserving `WriteQueue` to prevent transaction conflicts.
|
|
300
|
+
|
|
301
|
+
```javascript
|
|
302
|
+
// Concurrent writes are queued sequentially
|
|
303
|
+
await Promise.all([
|
|
304
|
+
storage.set('settings', 'key1', 'val1'),
|
|
305
|
+
storage.set('settings', 'key2', 'val2')
|
|
306
|
+
]);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Features:
|
|
310
|
+
|
|
311
|
+
- **Write Ordering**: Guarantees that writes resolve in the exact order they were requested.
|
|
312
|
+
- **Auto-Retries**: If a write fails due to transient locks or database errors, the queue retries the operation up to 3 times with exponential backoff delays.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## 11. Cache Eviction & Space Recovery
|
|
317
|
+
|
|
318
|
+
`PlatformStorage` automatically wraps stored items inside metadata envelopes:
|
|
319
|
+
|
|
320
|
+
```javascript
|
|
321
|
+
{
|
|
322
|
+
value: any,
|
|
323
|
+
lastAccessed: number,
|
|
324
|
+
expires: number | null
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
This supports advanced storage pruning strategies:
|
|
329
|
+
|
|
330
|
+
### Time-to-Live (TTL) Eviction
|
|
331
|
+
|
|
332
|
+
Specify a TTL (in milliseconds) during `set` to automatically expire records.
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
// Expire after 10 seconds
|
|
336
|
+
await storage.set('settings', 'token', 'abc', { ttl: 10000 });
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Expired records are automatically pruned from the database when new keys are written or checked.
|
|
340
|
+
|
|
341
|
+
### Least Recently Used (LRU) Eviction
|
|
342
|
+
|
|
343
|
+
When origin storage usage exceeds **80%** of the browser allocation:
|
|
344
|
+
|
|
345
|
+
1. A global `'quota'` event is dispatched on the `window` object containing the current `usage` and `quota`.
|
|
346
|
+
2. The storage layer automatically deletes expired records first.
|
|
347
|
+
3. If the ratio remains above 80%, it sorts active records by `lastAccessed` and evicts the oldest entries until usage falls below the threshold.
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
window.addEventListener('quota', (event) => {
|
|
351
|
+
const { usage, quota } = event.detail;
|
|
352
|
+
console.warn(`Storage space warning: ${usage} bytes used out of ${quota}`);
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## 12. Build-Time Immutability Warnings
|
|
359
|
+
|
|
360
|
+
Directly mutating properties retrieved from a store causes side-effects and breaks clean subscription flows. The compiler tools include an SWC-powered `ImmutabilityVisitor` to catch these issues at build-time.
|
|
361
|
+
|
|
362
|
+
### Bad Practice (Triggers Warning)
|
|
363
|
+
|
|
364
|
+
```javascript
|
|
365
|
+
const user = store.get('user');
|
|
366
|
+
user.name = 'Dave'; // Static compiler warning! Mutating store variable directly.
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Good Practice
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
const user = store.snapshot().user;
|
|
373
|
+
user.name = 'Dave';
|
|
374
|
+
store.set('user', user); // Correct: update via set()
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
When building or running in dev mode, the compiler logs warnings:
|
|
378
|
+
|
|
379
|
+
```text
|
|
380
|
+
[WARN] Immutability Violation in src/elements/profile.js: mutating property 'name' of store-retrieved variable 'user' directly. Use store.set() instead.
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 13. Testing State Code
|
|
386
|
+
|
|
387
|
+
### Stubbing Storage Quota
|
|
388
|
+
|
|
389
|
+
To test LRU eviction behavior under storage pressure, stub `navigator.storage.estimate` and dispatcher handlers:
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
let quotaFired = false;
|
|
393
|
+
const onQuota = () => { quotaFired = true; };
|
|
394
|
+
window.addEventListener('quota', onQuota);
|
|
395
|
+
|
|
396
|
+
const originalEstimate = navigator.storage?.estimate;
|
|
397
|
+
if (navigator.storage) {
|
|
398
|
+
navigator.storage.estimate = async () => ({
|
|
399
|
+
quota: 1000,
|
|
400
|
+
usage: 850 // >80% threshold
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Perform writes and verify eviction behavior...
|
|
405
|
+
|
|
406
|
+
window.removeEventListener('quota', onQuota);
|
|
407
|
+
navigator.storage.estimate = originalEstimate;
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Resetting Stores Between Tests
|
|
411
|
+
|
|
412
|
+
Always reset or clear stores and databases in test setup/teardown blocks:
|
|
413
|
+
|
|
414
|
+
```javascript
|
|
415
|
+
beforeEach(() => {
|
|
416
|
+
store.reset({ active: false });
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
afterEach(async () => {
|
|
420
|
+
// Delete the test IndexedDB database
|
|
421
|
+
await new Promise((resolve) => {
|
|
422
|
+
const req = indexedDB.deleteDatabase('test-platform-db');
|
|
423
|
+
req.onsuccess = () => resolve();
|
|
424
|
+
req.onerror = () => resolve();
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## 14. Checklist
|
|
432
|
+
|
|
433
|
+
- Use `state.create()` to instantiate reactive stores.
|
|
434
|
+
- Use `store.batch()` when writing multiple keys sequentially to avoid redundant notifications.
|
|
435
|
+
- Use `store.subscribe()` to listen to updates, and pass `AbortSignal` for automated component lifecycle cleanup.
|
|
436
|
+
- Enable `deep: true` in options only if you need to track nested object mutations directly.
|
|
437
|
+
- Use `state.derived()` to represent computed, dependency-tracked read-only state.
|
|
438
|
+
- Keep store mutations out of direct assignments on values returned from `store.get()`. Use `store.snapshot()` or update via `store.set()`.
|
|
439
|
+
- Register migrations via `storage.registerMigrations` sequentially in order to set up your IndexedDB stores correctly.
|
|
440
|
+
- Clean up test databases and reset store states inside `afterEach` hooks to prevent cross-test data pollution.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/storage/cache.js
|
|
3
|
+
*
|
|
4
|
+
* Cache API Wrapper.
|
|
5
|
+
* Provides a Promise-based cache client with seamless TTL (Time-To-Live)
|
|
6
|
+
* support by leveraging cloned Response header extensions.
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 11 — Networking §6, doc 22 — Storage Architecture §5
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class CacheStorage {
|
|
12
|
+
constructor(name) {
|
|
13
|
+
this.name = name;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Opens the underlying browser cache.
|
|
18
|
+
*/
|
|
19
|
+
open() {
|
|
20
|
+
return caches.open(this.name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Retrieves a cached response, automatically evicting if expired.
|
|
25
|
+
*/
|
|
26
|
+
async get(request) {
|
|
27
|
+
const cache = await this.open();
|
|
28
|
+
const req = typeof request === 'string' ? new Request(request) : request;
|
|
29
|
+
const res = await cache.match(req);
|
|
30
|
+
|
|
31
|
+
if (!res) return null;
|
|
32
|
+
|
|
33
|
+
// Evaluate custom TTL header
|
|
34
|
+
const expires = res.headers.get('x-expires-at');
|
|
35
|
+
if (expires && Date.now() > parseInt(expires, 10)) {
|
|
36
|
+
await cache.delete(req);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return res;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Caches a Response with an optional TTL (Time-to-Live).
|
|
45
|
+
*/
|
|
46
|
+
async set(request, response, ttlMs) {
|
|
47
|
+
const cache = await this.open();
|
|
48
|
+
const req = typeof request === 'string' ? new Request(request) : request;
|
|
49
|
+
|
|
50
|
+
let finalResponse = response.clone();
|
|
51
|
+
|
|
52
|
+
// Attach custom expiry header if a TTL is provided and response allows header mutation
|
|
53
|
+
if (ttlMs && response.type !== 'opaque') {
|
|
54
|
+
const headers = new Headers(response.headers);
|
|
55
|
+
headers.set('x-expires-at', String(Date.now() + ttlMs));
|
|
56
|
+
|
|
57
|
+
// Construct a new response copying the original body stream and properties
|
|
58
|
+
finalResponse = new Response(response.body ? response.clone().body : null, {
|
|
59
|
+
status: response.status,
|
|
60
|
+
statusText: response.statusText,
|
|
61
|
+
headers
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await cache.put(req, finalResponse);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Removes an entry from the cache.
|
|
70
|
+
*/
|
|
71
|
+
async delete(request) {
|
|
72
|
+
const cache = await this.open();
|
|
73
|
+
const req = typeof request === 'string' ? new Request(request) : request;
|
|
74
|
+
return cache.delete(req);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Deletes the entire cache storage pool.
|
|
79
|
+
*/
|
|
80
|
+
async clear() {
|
|
81
|
+
return caches.delete(this.name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/storage/idb.js
|
|
3
|
+
*
|
|
4
|
+
* Promise-wrapped IndexedDB Adapter.
|
|
5
|
+
* Encapsulates transactional storage operations, sequential migrations,
|
|
6
|
+
* and cursor/index queries.
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 22 — Storage Architecture §1, §3
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class Database {
|
|
12
|
+
#db = null;
|
|
13
|
+
|
|
14
|
+
constructor(name, version, migrations = []) {
|
|
15
|
+
this.name = name;
|
|
16
|
+
this.version = version;
|
|
17
|
+
this.migrations = migrations; // array of migration functions
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
open() {
|
|
21
|
+
if (this.#db) return Promise.resolve(this.#db);
|
|
22
|
+
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const request = indexedDB.open(this.name, this.version);
|
|
25
|
+
|
|
26
|
+
request.onblocked = () => {
|
|
27
|
+
console.warn(`IndexedDB database ${this.name} upgrade is blocked by another tab.`);
|
|
28
|
+
if (typeof window !== 'undefined') {
|
|
29
|
+
window.dispatchEvent(new CustomEvent('storage:blocked', { detail: { name: this.name } }));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
request.onupgradeneeded = (event) => {
|
|
34
|
+
const db = request.result;
|
|
35
|
+
const oldVersion = event.oldVersion;
|
|
36
|
+
const newVersion = event.newVersion;
|
|
37
|
+
|
|
38
|
+
// Perform sequential version migrations (non-skipping)
|
|
39
|
+
for (let v = oldVersion; v < newVersion; v++) {
|
|
40
|
+
const migrate = this.migrations[v];
|
|
41
|
+
if (typeof migrate === 'function') {
|
|
42
|
+
try {
|
|
43
|
+
migrate(db);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(`Migration to version ${v + 1} failed:`, err);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
request.onsuccess = () => {
|
|
52
|
+
this.#db = request.result;
|
|
53
|
+
this.#db.onversionchange = () => {
|
|
54
|
+
console.warn(`IndexedDB database ${this.name} version change requested. Closing connection.`);
|
|
55
|
+
this.close();
|
|
56
|
+
if (typeof window !== 'undefined') {
|
|
57
|
+
window.dispatchEvent(new CustomEvent('storage:versionchange', { detail: { name: this.name } }));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
resolve(this.#db);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
request.onerror = () => {
|
|
64
|
+
reject(request.error);
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Closes the active IndexedDB connection.
|
|
71
|
+
*/
|
|
72
|
+
close() {
|
|
73
|
+
if (this.#db) {
|
|
74
|
+
this.#db.close();
|
|
75
|
+
this.#db = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Run a callback inside a multi-store transaction context.
|
|
81
|
+
*/
|
|
82
|
+
transaction(storeNames, mode, callback) {
|
|
83
|
+
return this.open().then((db) => {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const tx = db.transaction(storeNames, mode);
|
|
86
|
+
const storeAccessor = (name) => tx.objectStore(name);
|
|
87
|
+
|
|
88
|
+
let result;
|
|
89
|
+
tx.oncomplete = () => resolve(result);
|
|
90
|
+
tx.onerror = () => reject(tx.error);
|
|
91
|
+
tx.onabort = () => reject(new Error('Transaction aborted'));
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const p = callback(storeAccessor, tx);
|
|
95
|
+
if (p && typeof p.then === 'function') {
|
|
96
|
+
p.then(
|
|
97
|
+
(val) => { result = val; },
|
|
98
|
+
(err) => {
|
|
99
|
+
try { tx.abort(); } catch {}
|
|
100
|
+
reject(err);
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
result = p;
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
try { tx.abort(); } catch {}
|
|
108
|
+
reject(err);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Helper to execute a single-store transaction.
|
|
117
|
+
*/
|
|
118
|
+
#run(storeName, mode, callback) {
|
|
119
|
+
return this.open().then((db) => {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const tx = db.transaction(storeName, mode);
|
|
122
|
+
const store = tx.objectStore(storeName);
|
|
123
|
+
|
|
124
|
+
let result;
|
|
125
|
+
tx.oncomplete = () => resolve(result);
|
|
126
|
+
tx.onerror = () => reject(tx.error);
|
|
127
|
+
tx.onabort = () => reject(new Error('Transaction aborted'));
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
result = callback(store, tx);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
tx.abort();
|
|
133
|
+
reject(err);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get(storeName, key) {
|
|
140
|
+
let req;
|
|
141
|
+
return this.#run(storeName, 'readonly', (store) => {
|
|
142
|
+
req = store.get(key);
|
|
143
|
+
}).then(() => req.result ?? null);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
set(storeName, key, value) {
|
|
147
|
+
return this.#run(storeName, 'readwrite', (store) => {
|
|
148
|
+
store.put(value, key);
|
|
149
|
+
}).then(() => {});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
delete(storeName, key) {
|
|
153
|
+
return this.#run(storeName, 'readwrite', (store) => {
|
|
154
|
+
store.delete(key);
|
|
155
|
+
}).then(() => {});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
clear(storeName) {
|
|
159
|
+
return this.#run(storeName, 'readwrite', (store) => {
|
|
160
|
+
store.clear();
|
|
161
|
+
}).then(() => {});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
getAll(storeName) {
|
|
165
|
+
let req;
|
|
166
|
+
return this.#run(storeName, 'readonly', (store) => {
|
|
167
|
+
req = store.getAll();
|
|
168
|
+
}).then(() => req.result || []);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
keys(storeName) {
|
|
172
|
+
let req;
|
|
173
|
+
return this.#run(storeName, 'readonly', (store) => {
|
|
174
|
+
req = store.getAllKeys();
|
|
175
|
+
}).then(() => req.result || []);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Advanced query matching using indices, ranges, directions, and limits.
|
|
180
|
+
*/
|
|
181
|
+
query(storeName, { index, range, direction = 'next', limit = Infinity } = {}) {
|
|
182
|
+
const results = [];
|
|
183
|
+
return this.#run(storeName, 'readonly', (store) => {
|
|
184
|
+
const source = index ? store.index(index) : store;
|
|
185
|
+
const req = source.openCursor(range, direction);
|
|
186
|
+
|
|
187
|
+
req.onsuccess = (e) => {
|
|
188
|
+
const cursor = e.target.result;
|
|
189
|
+
if (cursor && results.length < limit) {
|
|
190
|
+
results.push(cursor.value);
|
|
191
|
+
cursor.continue();
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}).then(() => results);
|
|
195
|
+
}
|
|
196
|
+
}
|