@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,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/storage/index.js
|
|
3
|
+
*
|
|
4
|
+
* Unified Storage Gateway.
|
|
5
|
+
* Integrates LRU caching, Cache API, OPFS, and IndexedDB under a unified,
|
|
6
|
+
* tiered public storage surface.
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 22 — Storage Architecture §1, §3
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Database } from './idb.js';
|
|
12
|
+
import { opfs } from './opfs.js';
|
|
13
|
+
import { CacheStorage } from './cache.js';
|
|
14
|
+
import { LRUCache, WeakLRUCache } from './lru.js';
|
|
15
|
+
import { quota } from './quota.js';
|
|
16
|
+
import { supports } from '../platform/index.js';
|
|
17
|
+
|
|
18
|
+
// Default migration: the core key-value store.
|
|
19
|
+
const defaultMigrations = [(db) => db.createObjectStore('keyval')];
|
|
20
|
+
|
|
21
|
+
// Initialize default storage pool instances (reassignable via storage.configure).
|
|
22
|
+
let idb = new Database('platform-db', 1, defaultMigrations);
|
|
23
|
+
let cache = new CacheStorage('platform-cache');
|
|
24
|
+
let lru = new LRUCache(200);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalizes the 3rd argument of get/set/delete: either a tier string or an
|
|
28
|
+
* options object `{ tier, ttl }`.
|
|
29
|
+
*/
|
|
30
|
+
function resolve(tierOrOptions, ttl = null) {
|
|
31
|
+
if (tierOrOptions && typeof tierOrOptions === 'object') {
|
|
32
|
+
return { tier: tierOrOptions.tier ?? 'idb', ttl: tierOrOptions.ttl ?? null };
|
|
33
|
+
}
|
|
34
|
+
return { tier: tierOrOptions ?? 'idb', ttl };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Expiry envelope so idb/opfs honor TTL like memory/cache do.
|
|
38
|
+
function wrapExpiry(value, ttl) {
|
|
39
|
+
if (!ttl) return value;
|
|
40
|
+
return { __exp: Date.now() + ttl, __v: value };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function unwrapExpiry(value) {
|
|
44
|
+
if (value && typeof value === 'object' && typeof value.__exp === 'number') {
|
|
45
|
+
if (Date.now() > value.__exp) return { expired: true, value: null };
|
|
46
|
+
return { expired: false, value: value.__v };
|
|
47
|
+
}
|
|
48
|
+
return { expired: false, value };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Helper functions for transparent compression
|
|
52
|
+
async function compress(value) {
|
|
53
|
+
const isObject = typeof value === 'object' && value !== null;
|
|
54
|
+
const stringVal = isObject ? JSON.stringify(value) : String(value);
|
|
55
|
+
const bytes = new TextEncoder().encode(stringVal);
|
|
56
|
+
const stream = new ReadableStream({
|
|
57
|
+
start(controller) {
|
|
58
|
+
controller.enqueue(bytes);
|
|
59
|
+
controller.close();
|
|
60
|
+
}
|
|
61
|
+
}).pipeThrough(new CompressionStream('gzip'));
|
|
62
|
+
const response = new Response(stream);
|
|
63
|
+
const buffer = await response.arrayBuffer();
|
|
64
|
+
return {
|
|
65
|
+
_compressed: true,
|
|
66
|
+
_type: isObject ? 'json' : 'string',
|
|
67
|
+
data: buffer
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function decompress(envelope) {
|
|
72
|
+
const stream = new ReadableStream({
|
|
73
|
+
start(controller) {
|
|
74
|
+
controller.enqueue(new Uint8Array(envelope.data));
|
|
75
|
+
controller.close();
|
|
76
|
+
}
|
|
77
|
+
}).pipeThrough(new DecompressionStream('gzip'));
|
|
78
|
+
const response = new Response(stream);
|
|
79
|
+
const text = await response.text();
|
|
80
|
+
return envelope._type === 'json' ? JSON.parse(text) : text;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Replay write journal automatically on startup
|
|
84
|
+
async function replayJournal() {
|
|
85
|
+
if (typeof localStorage === 'undefined') return;
|
|
86
|
+
const keysToRemove = [];
|
|
87
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
88
|
+
const k = localStorage.key(i);
|
|
89
|
+
if (k && k.startsWith('storage:journal:')) {
|
|
90
|
+
try {
|
|
91
|
+
const raw = localStorage.getItem(k);
|
|
92
|
+
if (raw) {
|
|
93
|
+
const { key, value } = JSON.parse(raw);
|
|
94
|
+
console.log(`Replaying journal write for key: ${key}`);
|
|
95
|
+
await storage.set(key, value, 'idb');
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(`Failed to replay journal entry ${k}:`, err);
|
|
99
|
+
}
|
|
100
|
+
keysToRemove.push(k);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const k of keysToRemove) {
|
|
104
|
+
localStorage.removeItem(k);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof window !== 'undefined') {
|
|
109
|
+
Promise.resolve().then(() => replayJournal().catch(console.error));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const storage = {
|
|
113
|
+
compressionThreshold: 64 * 1024,
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Reconfigures the storage pool. Call before first use.
|
|
117
|
+
*
|
|
118
|
+
* storage.configure({
|
|
119
|
+
* idb: { name, version, migrations },
|
|
120
|
+
* lru: { maxSize },
|
|
121
|
+
* cache: { name }
|
|
122
|
+
* });
|
|
123
|
+
*/
|
|
124
|
+
configure({ idb: idbOpts, lru: lruOpts, cache: cacheOpts } = {}) {
|
|
125
|
+
if (idbOpts) {
|
|
126
|
+
idb = new Database(
|
|
127
|
+
idbOpts.name ?? 'platform-db',
|
|
128
|
+
idbOpts.version ?? 1,
|
|
129
|
+
idbOpts.migrations ?? defaultMigrations
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (lruOpts) {
|
|
133
|
+
lru = new LRUCache(lruOpts.maxSize ?? 200);
|
|
134
|
+
}
|
|
135
|
+
if (cacheOpts) {
|
|
136
|
+
cache = new CacheStorage(cacheOpts.name ?? 'platform-cache');
|
|
137
|
+
}
|
|
138
|
+
return this;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Retrieves an item from the requested tier.
|
|
143
|
+
* The 2nd argument may be a tier string or `{ tier }`.
|
|
144
|
+
*/
|
|
145
|
+
async get(key, tierOrOptions = 'idb') {
|
|
146
|
+
const { tier } = resolve(tierOrOptions);
|
|
147
|
+
|
|
148
|
+
if (tier === 'memory') {
|
|
149
|
+
return lru.get(key);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (tier === 'opfs') {
|
|
153
|
+
const stored = await opfs.get(key);
|
|
154
|
+
const ex = unwrapExpiry(stored);
|
|
155
|
+
if (ex.expired) {
|
|
156
|
+
await opfs.delete(key);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return ex.value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (tier === 'cache') {
|
|
163
|
+
const res = await cache.get(key);
|
|
164
|
+
if (!res) return null;
|
|
165
|
+
try {
|
|
166
|
+
return await res.json();
|
|
167
|
+
} catch {
|
|
168
|
+
return await res.text();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Default 'idb' read with LRU memory caching
|
|
173
|
+
const cached = lru.get(key);
|
|
174
|
+
if (cached !== null) return cached;
|
|
175
|
+
|
|
176
|
+
let value = await idb.get('keyval', key);
|
|
177
|
+
if (value !== null) {
|
|
178
|
+
const ex = unwrapExpiry(value);
|
|
179
|
+
if (ex.expired) {
|
|
180
|
+
await idb.delete('keyval', key);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
value = ex.value;
|
|
184
|
+
|
|
185
|
+
if (value && value._compressed && value.data) {
|
|
186
|
+
try {
|
|
187
|
+
value = await decompress(value);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error('Decompression failed:', err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
lru.set(key, value);
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Saves an item to the requested tier.
|
|
199
|
+
* The 2nd argument may be a tier string or `{ tier, ttl }`. TTL is honored across all tiers.
|
|
200
|
+
*/
|
|
201
|
+
async set(key, value, tierOrOptions = 'idb') {
|
|
202
|
+
const { tier, ttl } = resolve(tierOrOptions);
|
|
203
|
+
|
|
204
|
+
// Quota proactive check before write
|
|
205
|
+
await quota.check();
|
|
206
|
+
|
|
207
|
+
if (tier === 'memory') {
|
|
208
|
+
lru.set(key, value, ttl);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (tier === 'opfs') {
|
|
213
|
+
await opfs.set(key, wrapExpiry(value, ttl));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (tier === 'cache') {
|
|
218
|
+
const response = new Response(
|
|
219
|
+
typeof value === 'object' ? JSON.stringify(value) : String(value),
|
|
220
|
+
{
|
|
221
|
+
headers: {
|
|
222
|
+
'content-type': typeof value === 'object' ? 'application/json' : 'text/plain'
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
await cache.set(key, response, ttl);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Default: Write to IDB and cache in LRU
|
|
231
|
+
const journalKey = `storage:journal:${key}`;
|
|
232
|
+
if (typeof localStorage !== 'undefined') {
|
|
233
|
+
try {
|
|
234
|
+
localStorage.setItem(journalKey, JSON.stringify({ key, value }));
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.warn('Failed to write journal:', err);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
let finalValue = value;
|
|
242
|
+
if (supports.compression) {
|
|
243
|
+
const serialized = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
244
|
+
if (serialized.length > this.compressionThreshold) {
|
|
245
|
+
try {
|
|
246
|
+
finalValue = await compress(value);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.error('Compression failed:', err);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await idb.set('keyval', key, wrapExpiry(finalValue, ttl));
|
|
254
|
+
lru.set(key, value, ttl);
|
|
255
|
+
} finally {
|
|
256
|
+
if (typeof localStorage !== 'undefined') {
|
|
257
|
+
localStorage.removeItem(journalKey);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Deletes an item from the storage tiers.
|
|
264
|
+
*/
|
|
265
|
+
async delete(key, tierOrOptions = 'idb') {
|
|
266
|
+
const { tier } = resolve(tierOrOptions);
|
|
267
|
+
lru.delete(key);
|
|
268
|
+
|
|
269
|
+
if (tier === 'opfs') {
|
|
270
|
+
await opfs.delete(key);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (tier === 'cache') {
|
|
275
|
+
await cache.delete(key);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await idb.delete('keyval', key);
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Performs advanced index/cursor queries on the IDB store.
|
|
284
|
+
*/
|
|
285
|
+
async query(storeName, queryOpts) {
|
|
286
|
+
return idb.query(storeName, queryOpts);
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Run a callback inside a multi-store transaction context.
|
|
291
|
+
*/
|
|
292
|
+
transaction(storeNames, mode, fn) {
|
|
293
|
+
return idb.transaction(storeNames, mode, fn);
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Lists keys available in the store.
|
|
298
|
+
*/
|
|
299
|
+
async list(tier = 'idb') {
|
|
300
|
+
if (tier === 'opfs') {
|
|
301
|
+
return opfs.list();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (tier === 'cache') {
|
|
305
|
+
const c = await cache.open();
|
|
306
|
+
const keys = await c.keys();
|
|
307
|
+
return keys.map((req) => req.url);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return idb.keys('keyval');
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Clears the storage pool for a specific tier or all.
|
|
315
|
+
*/
|
|
316
|
+
async clear(tier = 'all') {
|
|
317
|
+
lru.clear();
|
|
318
|
+
|
|
319
|
+
if (tier === 'all' || tier === 'opfs') {
|
|
320
|
+
await opfs.clear();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (tier === 'all' || tier === 'cache') {
|
|
324
|
+
await cache.clear();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (tier === 'all' || tier === 'idb') {
|
|
328
|
+
await idb.clear('keyval');
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Retrieves storage quotas.
|
|
334
|
+
*/
|
|
335
|
+
async estimate() {
|
|
336
|
+
const est = await quota.estimate();
|
|
337
|
+
const persisted = await this.persisted();
|
|
338
|
+
return {
|
|
339
|
+
quota: est.quota,
|
|
340
|
+
usage: est.usage,
|
|
341
|
+
persisted
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Requests storage persistence.
|
|
347
|
+
*/
|
|
348
|
+
persist() {
|
|
349
|
+
return quota.persist();
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Checks if storage is persisted.
|
|
354
|
+
*/
|
|
355
|
+
async persisted() {
|
|
356
|
+
if (typeof navigator !== 'undefined' && navigator.storage?.persisted) {
|
|
357
|
+
return navigator.storage.persisted();
|
|
358
|
+
}
|
|
359
|
+
return false;
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Registers a callback for storage quota warning events.
|
|
364
|
+
*/
|
|
365
|
+
onQuotaWarning(handler) {
|
|
366
|
+
return quota.onQuotaWarning(handler);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
// Named class exports — lets consumers instantiate adapters directly:
|
|
372
|
+
// import { Database, LRUCache } from '@adukiorg/anza/storage';
|
|
373
|
+
export { Database, LRUCache, WeakLRUCache };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/storage/lru.js
|
|
3
|
+
*
|
|
4
|
+
* Bounded Least-Recently-Used (LRU) Cache.
|
|
5
|
+
* Provides Map-based LRU and WeakRef-based GC-eligible bounded caches
|
|
6
|
+
* supporting custom TTL values.
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 22 — Storage Architecture §2
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class LRUCache {
|
|
12
|
+
constructor(maxSize = 100) {
|
|
13
|
+
this.maxSize = maxSize;
|
|
14
|
+
this.cache = new Map();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get(key) {
|
|
18
|
+
if (!this.cache.has(key)) return null;
|
|
19
|
+
|
|
20
|
+
const entry = this.cache.get(key);
|
|
21
|
+
if (entry.expires && Date.now() > entry.expires) {
|
|
22
|
+
this.cache.delete(key);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Refresh access position by moving key to the end
|
|
27
|
+
this.cache.delete(key);
|
|
28
|
+
this.cache.set(key, entry);
|
|
29
|
+
return entry.value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
set(key, value, ttlMs) {
|
|
33
|
+
if (this.cache.has(key)) {
|
|
34
|
+
this.cache.delete(key);
|
|
35
|
+
} else if (this.cache.size >= this.maxSize) {
|
|
36
|
+
// Map keys iterator guarantees insertion-order; first item is least recently used
|
|
37
|
+
const oldest = this.cache.keys().next().value;
|
|
38
|
+
this.cache.delete(oldest);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const expires = ttlMs ? Date.now() + ttlMs : null;
|
|
42
|
+
this.cache.set(key, { value, expires });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
delete(key) {
|
|
46
|
+
return this.cache.delete(key);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
clear() {
|
|
50
|
+
this.cache.clear();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* WeakRef-based bounded LRU cache.
|
|
56
|
+
* Yields elements to the Garbage Collector if memory pressure requires.
|
|
57
|
+
*/
|
|
58
|
+
export class WeakLRUCache {
|
|
59
|
+
constructor(maxSize = 100) {
|
|
60
|
+
this.maxSize = maxSize;
|
|
61
|
+
this.cache = new Map();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get(key) {
|
|
65
|
+
if (!this.cache.has(key)) return null;
|
|
66
|
+
|
|
67
|
+
const entry = this.cache.get(key);
|
|
68
|
+
const value = entry.ref.deref();
|
|
69
|
+
|
|
70
|
+
// Clean up if GC collected the reference or TTL expired
|
|
71
|
+
if (value === undefined || (entry.expires && Date.now() > entry.expires)) {
|
|
72
|
+
this.cache.delete(key);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.cache.delete(key);
|
|
77
|
+
this.cache.set(key, entry);
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
set(key, value, ttlMs) {
|
|
82
|
+
if (typeof value !== 'object' && typeof value !== 'function') {
|
|
83
|
+
throw new TypeError('WeakLRUCache values must be objects or functions');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (this.cache.has(key)) {
|
|
87
|
+
this.cache.delete(key);
|
|
88
|
+
} else if (this.cache.size >= this.maxSize) {
|
|
89
|
+
const oldest = this.cache.keys().next().value;
|
|
90
|
+
this.cache.delete(oldest);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const expires = ttlMs ? Date.now() + ttlMs : null;
|
|
94
|
+
this.cache.set(key, {
|
|
95
|
+
ref: new WeakRef(value),
|
|
96
|
+
expires
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
delete(key) {
|
|
101
|
+
return this.cache.delete(key);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
clear() {
|
|
105
|
+
this.cache.clear();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Storage Missing Support
|
|
2
|
+
|
|
3
|
+
This document tracks remaining support, runtime gaps, concurrency constraints, serialization enhancements, and performance optimization work for `src/core/storage`. Implemented behavior belongs in `usage.md` (or core documentation); unsupported, under-tested, or performance-gated behavior belongs here until it is built and verified.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 0. Toolchain and Naming Rules
|
|
8
|
+
|
|
9
|
+
Heavy compilation, static analysis, and code generation belong in `tools/`, not in browser runtime modules.
|
|
10
|
+
|
|
11
|
+
Current toolchain storage support requirements:
|
|
12
|
+
|
|
13
|
+
- Analyze storage tier utilization and statically compile Dedicated Web Worker scripts to avoid runtime ObjectURL blobs.
|
|
14
|
+
- Detect synchronous storage access patterns (like `localStorage` or `sessionStorage` usage) outside designated bootstrap paths.
|
|
15
|
+
- Statically validate migration schema progression profiles.
|
|
16
|
+
|
|
17
|
+
Naming rules for storage support:
|
|
18
|
+
|
|
19
|
+
- Prefer one-word files: `idb.js`, `lru.js`, `opfs.js`, `quota.js`, `cache.js`, `index.js`.
|
|
20
|
+
- Plural folders: `tests/`, `migrations/`.
|
|
21
|
+
- Single-word methods: `get`, `set`, `delete`, `query`, `list`, `clear`, `estimate`, `persist`, `persisted`, `transaction`.
|
|
22
|
+
- Single-word events: `warning`, `quota`, `blocked`.
|
|
23
|
+
- Avoid compound names where single words carry full meaning in context.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 1. Critical Runtime and Type Gaps
|
|
28
|
+
|
|
29
|
+
### 1.1. Missing Unified Gateway Methods
|
|
30
|
+
|
|
31
|
+
- **Status**: Runtime gap.
|
|
32
|
+
- **Files**:
|
|
33
|
+
- `src/core/storage/index.js`
|
|
34
|
+
- **Problem**:
|
|
35
|
+
The Storage Architecture specification (§15) defines `storage.transaction(stores, mode, fn)`, `storage.persisted()`, and `storage.onQuotaWarning(handler)` as part of the Unified Storage Gateway interface. Currently, these methods are not implemented on the exported `storage` facade.
|
|
36
|
+
- **Expected Support**:
|
|
37
|
+
- Implement `storage.transaction(stores, mode, fn)` to execute multi-store operations inside a single transactional block.
|
|
38
|
+
- Implement `storage.persisted()` wrapping `navigator.storage.persisted()`.
|
|
39
|
+
- Implement `storage.onQuotaWarning(handler)` to subscribe to quota limit checks.
|
|
40
|
+
|
|
41
|
+
### 1.2. Storage Type Mismatches
|
|
42
|
+
|
|
43
|
+
- **Status**: Type mismatch.
|
|
44
|
+
- **Files**:
|
|
45
|
+
- `types/core/storage/index.d.ts`
|
|
46
|
+
- **Problem**:
|
|
47
|
+
The type declaration file does not align with the public specifications. It is missing declarations for `persisted()`, `transaction()`, and the proper returned structure from `estimate()` (including the `persisted` flag).
|
|
48
|
+
- **Expected Support**:
|
|
49
|
+
- Align `types/core/storage/index.d.ts` to expose the true interface.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. Concurrency & Durability Gaps
|
|
54
|
+
|
|
55
|
+
### 2.1. Web Locks Coordination for OPFS
|
|
56
|
+
|
|
57
|
+
- **Status**: Critical concurrency gap.
|
|
58
|
+
- **Files**:
|
|
59
|
+
- `src/core/storage/opfs.js`
|
|
60
|
+
- **Problem**:
|
|
61
|
+
The OPFS manager does not coordinate file access across different tabs. Concurrent reads and writes to the same virtual file will throw errors or lead to raw binary data corruption.
|
|
62
|
+
- **Expected Support**:
|
|
63
|
+
- Wrap OPFS operations (`get`, `set`, `delete`) in an exclusive Web Lock (`workers.lock` or `navigator.locks.request`) scoped to the filename to serialize cross-tab operations.
|
|
64
|
+
|
|
65
|
+
### 2.2. IndexedDB Upgrade Blocking Coordination
|
|
66
|
+
|
|
67
|
+
- **Status**: Runtime gap.
|
|
68
|
+
- **Files**:
|
|
69
|
+
- `src/core/storage/idb.js`
|
|
70
|
+
- **Problem**:
|
|
71
|
+
If a user has multiple tabs open, an IndexedDB database schema upgrade (triggered by opening with a higher version) is blocked indefinitely because the old tabs hold active connections. Currently, the database class does not listen to `onblocked` or `onversionchange` events.
|
|
72
|
+
- **Expected Support**:
|
|
73
|
+
- Handle the `blocked` event on the database open request, dispatching an event to warn the system of connection locks.
|
|
74
|
+
- Listen to the `versionchange` event on the active database connection. If triggered, gracefully close the database immediately so that other tabs can proceed with schema upgrades.
|
|
75
|
+
|
|
76
|
+
### 2.3. Write Journal for Durability
|
|
77
|
+
|
|
78
|
+
- **Status**: Planned but missing.
|
|
79
|
+
- **Files**:
|
|
80
|
+
- `src/core/storage/index.js`
|
|
81
|
+
- **Problem**:
|
|
82
|
+
Transactions can fail to write to disk due to abrupt tab crashes or sudden power losses, leaving memory and disk states inconsistent.
|
|
83
|
+
- **Expected Support**:
|
|
84
|
+
- Introduce a write journal using crash-durable `localStorage` to buffer pending keys before committing them to IndexedDB.
|
|
85
|
+
- Verify and replay uncommitted journal operations automatically during client boot.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 3. Serialization & Performance Gaps
|
|
90
|
+
|
|
91
|
+
### 3.1. Transparent Compression for Large Records
|
|
92
|
+
|
|
93
|
+
- **Status**: Performance enhancement.
|
|
94
|
+
- **Files**:
|
|
95
|
+
- `src/core/storage/index.js`
|
|
96
|
+
- **Problem**:
|
|
97
|
+
Writing large objects (>64KB) directly to IndexedDB consumes excess quota, increases serialization/deserialization delay, and degrades storage bandwidth.
|
|
98
|
+
- **Expected Support**:
|
|
99
|
+
- Implement transparent compression using the browser's native Compression Streams API (`new CompressionStream('gzip')`) when writing values exceeding 64KB.
|
|
100
|
+
- Inject a metadata envelope (`{ value, compressed: true }`) to identify compressed payloads and decompress them automatically on read.
|
|
101
|
+
|
|
102
|
+
### 3.2. Worker Script Optimization
|
|
103
|
+
|
|
104
|
+
- **Status**: Optimization.
|
|
105
|
+
- **Files**:
|
|
106
|
+
- `src/core/storage/opfs.js`
|
|
107
|
+
- **Problem**:
|
|
108
|
+
The OPFS manager instantiates its Dedicated Worker using a Blob URL wrapper on an inline string (`URL.createObjectURL(new Blob(...))`). This slows down initial load, blocks minification, and triggers Content Security Policy (CSP) security warnings.
|
|
109
|
+
- **Expected Support**:
|
|
110
|
+
- Offload worker script extraction to the build-time compiler (`tools/`) to generate a static asset or optimize string injection.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 4. Test Coverage Gaps
|
|
115
|
+
|
|
116
|
+
### 4.1. OPFS Integration Tests
|
|
117
|
+
|
|
118
|
+
- **Status**: Missing coverage.
|
|
119
|
+
- **Files**:
|
|
120
|
+
- `tests/core/storage/opfs.test.js` [NEW]
|
|
121
|
+
- **Needed Coverage**:
|
|
122
|
+
- Dedicated Worker execution offloading.
|
|
123
|
+
- Parallel reads/writes and verification of data persistence.
|
|
124
|
+
- Cross-tab change invalidation alerts over BroadcastChannel.
|
|
125
|
+
|
|
126
|
+
### 4.2. Cache API and TTL Verification
|
|
127
|
+
|
|
128
|
+
- **Status**: Missing coverage.
|
|
129
|
+
- **Files**:
|
|
130
|
+
- `tests/core/storage/cache.test.js` [NEW]
|
|
131
|
+
- **Needed Coverage**:
|
|
132
|
+
- Cache matches and correct header insertion for `x-expires-at`.
|
|
133
|
+
- Automatic eviction of expired items.
|
|
134
|
+
|
|
135
|
+
### 4.3. Quota Warning and Eviction Tests
|
|
136
|
+
|
|
137
|
+
- **Status**: Missing coverage.
|
|
138
|
+
- **Files**:
|
|
139
|
+
- `tests/core/storage/quota.test.js` [NEW]
|
|
140
|
+
- **Needed Coverage**:
|
|
141
|
+
- Mocking quota estimate queries.
|
|
142
|
+
- Verification that warnings fire when threshold usage exceeds 80%.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 5. Suggested Implementation Order
|
|
147
|
+
|
|
148
|
+
1. Align `types/core/storage/index.d.ts` with the full unified storage API.
|
|
149
|
+
2. Add comprehensive tests for `opfs.js`, `cache.js`, and `quota.js`.
|
|
150
|
+
3. Implement Web Locks for OPFS file synchronization.
|
|
151
|
+
4. Implement database blocking and connection version upgrade handlers.
|
|
152
|
+
5. Implement transparent Compression Streams for large values (>64KB).
|
|
153
|
+
6. Implement write journaling for transactional durability.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 6. Done Criteria
|
|
158
|
+
|
|
159
|
+
This missing-support list is complete when:
|
|
160
|
+
|
|
161
|
+
- TypeScript declarations map the complete unified storage API.
|
|
162
|
+
- OPFS, Cache TTL, and Quota warning components have complete integration tests.
|
|
163
|
+
- Web Locks synchronize multi-tab file writes.
|
|
164
|
+
- DB upgrades are not blocked by connections in sibling tabs.
|
|
165
|
+
- Large records (>64KB) are compressed transparently using native streams.
|