@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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/api/retry.js
|
|
3
|
+
*
|
|
4
|
+
* Implements exponential backoff with jitter for transient errors.
|
|
5
|
+
* Ensures the network layer is self-healing for temporary disconnects/server errors,
|
|
6
|
+
* while instantly failing client-side (4xx) errors.
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 11 — Networking §8
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { PlatformError } from './fetch.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Retries an asynchronous operation with exponential backoff and full jitter.
|
|
15
|
+
*/
|
|
16
|
+
export async function retry(operation, options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
attempts = 3,
|
|
19
|
+
base = 100, // starting delay in ms
|
|
20
|
+
maxDelay = 3000, // maximum wait time in ms
|
|
21
|
+
signal
|
|
22
|
+
} = options;
|
|
23
|
+
|
|
24
|
+
let attempt = 0;
|
|
25
|
+
|
|
26
|
+
while (true) {
|
|
27
|
+
if (signal?.aborted) {
|
|
28
|
+
throw new PlatformError({
|
|
29
|
+
code: 'NETWORK_ERROR',
|
|
30
|
+
message: 'Network request aborted before retry attempt',
|
|
31
|
+
recoverable: false
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
return await operation();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
attempt++;
|
|
39
|
+
|
|
40
|
+
// Qualify transient failure: Network dropouts, timeouts, or server (5xx) issues
|
|
41
|
+
const isTransient = err.code === 'NETWORK_TIMEOUT' ||
|
|
42
|
+
err.code === 'NETWORK_ERROR' ||
|
|
43
|
+
(err.code === 'HTTP_ERROR' && err.context?.status >= 500);
|
|
44
|
+
|
|
45
|
+
if (attempt >= attempts || !isTransient) {
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Exponential backoff with full jitter calculation
|
|
50
|
+
const tempDelay = base * Math.pow(2, attempt);
|
|
51
|
+
const delay = Math.random() * Math.min(maxDelay, tempDelay);
|
|
52
|
+
|
|
53
|
+
await new Promise((resolve, reject) => {
|
|
54
|
+
const timeoutId = setTimeout(resolve, delay);
|
|
55
|
+
|
|
56
|
+
if (signal) {
|
|
57
|
+
signal.addEventListener('abort', () => {
|
|
58
|
+
clearTimeout(timeoutId);
|
|
59
|
+
reject(new PlatformError({
|
|
60
|
+
code: 'NETWORK_ERROR',
|
|
61
|
+
message: 'Network request aborted during retry backoff',
|
|
62
|
+
recoverable: false
|
|
63
|
+
}));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/api/stream.js
|
|
3
|
+
*
|
|
4
|
+
* Implements Streams API and progressive response handling.
|
|
5
|
+
* Parses NDJSON streams using native TransformStream pipelines, preserving backpressure.
|
|
6
|
+
* Useful for large data lists, logs, or live AI streaming responses.
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 11 — Networking §11
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a reusable TransformStream to parse Newline-Delimited JSON (NDJSON).
|
|
13
|
+
*/
|
|
14
|
+
export function createNDJSONTransform() {
|
|
15
|
+
let buffer = '';
|
|
16
|
+
return new TransformStream({
|
|
17
|
+
transform(chunk, controller) {
|
|
18
|
+
buffer += chunk;
|
|
19
|
+
const lines = buffer.split('\n');
|
|
20
|
+
buffer = lines.pop() || ''; // Keep trailing fragment in buffer
|
|
21
|
+
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (trimmed) {
|
|
25
|
+
try {
|
|
26
|
+
controller.enqueue(JSON.parse(trimmed));
|
|
27
|
+
} catch (err) {
|
|
28
|
+
controller.error(new Error(`Failed to parse NDJSON line: ${err.message}`));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
flush(controller) {
|
|
34
|
+
const trimmed = buffer.trim();
|
|
35
|
+
if (trimmed) {
|
|
36
|
+
try {
|
|
37
|
+
controller.enqueue(JSON.parse(trimmed));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
controller.error(new Error(`Failed to parse NDJSON line on flush: ${err.message}`));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
import { events } from './events/index.js';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Initiates a streaming request and yields parsed JSON chunks as an AsyncIterable.
|
|
50
|
+
*/
|
|
51
|
+
export async function* stream(url, opts = {}) {
|
|
52
|
+
const { signal, ...fetchOpts } = opts;
|
|
53
|
+
|
|
54
|
+
// Generate unique request ID to scope streaming telemetry events
|
|
55
|
+
const requestId = opts.requestId || (typeof crypto !== 'undefined' && crypto.randomUUID
|
|
56
|
+
? crypto.randomUUID()
|
|
57
|
+
: Math.random().toString(36).slice(2) + Date.now().toString(36));
|
|
58
|
+
|
|
59
|
+
// Register request-specific temporary listeners
|
|
60
|
+
const disposes = [];
|
|
61
|
+
if (opts.on && typeof opts.on === 'object') {
|
|
62
|
+
for (const [event, handler] of Object.entries(opts.on)) {
|
|
63
|
+
if (typeof handler === 'function') {
|
|
64
|
+
const dispose = events.on(event, (e) => {
|
|
65
|
+
if (e.detail?.requestId === requestId) {
|
|
66
|
+
handler(e);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
disposes.push(dispose);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await fetch(url, { ...fetchOpts, signal });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
events.emit('error', { error: err, requestId });
|
|
79
|
+
events.emit('failed', { error: err, requestId });
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const err = new Error(`Streaming failed: HTTP ${response.status} ${response.statusText}`);
|
|
85
|
+
events.emit('status:' + response.status, { response, requestId });
|
|
86
|
+
events.emit('failed', { response, requestId });
|
|
87
|
+
events.emit('error', { error: err, requestId });
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!response.body) {
|
|
92
|
+
const err = new Error('Streaming failed: Response body is not readable');
|
|
93
|
+
events.emit('error', { error: err, requestId });
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const reader = response.body
|
|
98
|
+
.pipeThrough(new TextDecoderStream())
|
|
99
|
+
.pipeThrough(createNDJSONTransform())
|
|
100
|
+
.getReader();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
while (true) {
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
result = await reader.read();
|
|
107
|
+
} catch (err) {
|
|
108
|
+
events.emit('error', { error: err, requestId });
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { value, done } = result;
|
|
113
|
+
if (done) break;
|
|
114
|
+
|
|
115
|
+
// Emit chunk telemetry event locally and globally
|
|
116
|
+
events.emit('chunk', { chunk: value, requestId });
|
|
117
|
+
|
|
118
|
+
yield value;
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
reader.releaseLock();
|
|
122
|
+
// Guarantee auto-cleanup of streaming listeners
|
|
123
|
+
for (const dispose of disposes) {
|
|
124
|
+
dispose();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/api/upload.js
|
|
3
|
+
*
|
|
4
|
+
* Handles file uploads with real-time progress events.
|
|
5
|
+
* Uses standard XMLHttpRequest to provide precise progress callbacks (opts.onProgress),
|
|
6
|
+
* bridging the standard fetch upload progress gap in Baseline browsers.
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 11 — Networking §14
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { PlatformError } from './fetch.js';
|
|
12
|
+
import { events } from './events/index.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Uploads a file or binary payload with progress tracking and AbortSignal support.
|
|
16
|
+
*/
|
|
17
|
+
export function upload(url, fileOrData, opts = {}) {
|
|
18
|
+
const {
|
|
19
|
+
method = 'POST',
|
|
20
|
+
headers = {},
|
|
21
|
+
onProgress,
|
|
22
|
+
signal
|
|
23
|
+
} = opts;
|
|
24
|
+
|
|
25
|
+
// Generate unique request ID to scope uploading telemetry events
|
|
26
|
+
const requestId = opts.requestId || (typeof crypto !== 'undefined' && crypto.randomUUID
|
|
27
|
+
? crypto.randomUUID()
|
|
28
|
+
: Math.random().toString(36).slice(2) + Date.now().toString(36));
|
|
29
|
+
|
|
30
|
+
// Register request-specific temporary listeners
|
|
31
|
+
const disposes = [];
|
|
32
|
+
if (opts.on && typeof opts.on === 'object') {
|
|
33
|
+
for (const [event, handler] of Object.entries(opts.on)) {
|
|
34
|
+
if (typeof handler === 'function') {
|
|
35
|
+
const dispose = events.on(event, (e) => {
|
|
36
|
+
if (e.detail?.requestId === requestId) {
|
|
37
|
+
handler(e);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
disposes.push(dispose);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const cleanup = () => {
|
|
46
|
+
for (const dispose of disposes) {
|
|
47
|
+
dispose();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const xhr = new XMLHttpRequest();
|
|
53
|
+
xhr.open(method, url, true);
|
|
54
|
+
|
|
55
|
+
// Apply headers
|
|
56
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
57
|
+
xhr.setRequestHeader(key, value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Attach upload progress listeners
|
|
61
|
+
if (xhr.upload) {
|
|
62
|
+
xhr.upload.onprogress = (event) => {
|
|
63
|
+
if (event.lengthComputable) {
|
|
64
|
+
const progressPayload = {
|
|
65
|
+
loaded: event.loaded,
|
|
66
|
+
total: event.total,
|
|
67
|
+
percentage: Math.round((event.loaded / event.total) * 100)
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Trigger legacy onProgress callback if defined
|
|
71
|
+
if (onProgress) {
|
|
72
|
+
onProgress(progressPayload);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Emit progress telemetry event
|
|
76
|
+
events.emit('progress', { ...progressPayload, requestId });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Handle abort triggers
|
|
82
|
+
if (signal) {
|
|
83
|
+
if (signal.aborted) {
|
|
84
|
+
xhr.abort();
|
|
85
|
+
cleanup();
|
|
86
|
+
return reject(
|
|
87
|
+
new PlatformError({
|
|
88
|
+
code: 'NETWORK_ERROR',
|
|
89
|
+
message: 'Upload aborted',
|
|
90
|
+
recoverable: false
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
signal.addEventListener('abort', () => {
|
|
95
|
+
xhr.abort();
|
|
96
|
+
cleanup();
|
|
97
|
+
reject(
|
|
98
|
+
new PlatformError({
|
|
99
|
+
code: 'NETWORK_ERROR',
|
|
100
|
+
message: 'Upload aborted',
|
|
101
|
+
recoverable: false
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
xhr.onload = () => {
|
|
108
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
109
|
+
let responseData = xhr.responseText;
|
|
110
|
+
try {
|
|
111
|
+
responseData = JSON.parse(responseData);
|
|
112
|
+
} catch {
|
|
113
|
+
// Keep raw text if not JSON format
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Mock a simple Response object for matching response event payloads
|
|
117
|
+
const mockResponse = {
|
|
118
|
+
status: xhr.status,
|
|
119
|
+
ok: true,
|
|
120
|
+
headers: new Headers({ 'Content-Type': xhr.getResponseHeader('Content-Type') || 'text/plain' })
|
|
121
|
+
};
|
|
122
|
+
events.emit(`status:${xhr.status}`, { response: mockResponse, requestId });
|
|
123
|
+
|
|
124
|
+
cleanup();
|
|
125
|
+
resolve(responseData);
|
|
126
|
+
} else {
|
|
127
|
+
const err = new PlatformError({
|
|
128
|
+
code: 'HTTP_ERROR',
|
|
129
|
+
message: `Upload failed with status code ${xhr.status}`,
|
|
130
|
+
context: { url, status: xhr.status },
|
|
131
|
+
recoverable: xhr.status >= 500
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const mockResponse = {
|
|
135
|
+
status: xhr.status,
|
|
136
|
+
ok: false,
|
|
137
|
+
headers: new Headers({ 'Content-Type': xhr.getResponseHeader('Content-Type') || 'text/plain' })
|
|
138
|
+
};
|
|
139
|
+
events.emit(`status:${xhr.status}`, { response: mockResponse, requestId });
|
|
140
|
+
events.emit('failed', { response: mockResponse, requestId });
|
|
141
|
+
events.emit('error', { error: err, requestId });
|
|
142
|
+
|
|
143
|
+
cleanup();
|
|
144
|
+
reject(err);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
xhr.onerror = (err) => {
|
|
149
|
+
const errorObj = new PlatformError({
|
|
150
|
+
code: 'NETWORK_ERROR',
|
|
151
|
+
message: 'Upload network connection failed',
|
|
152
|
+
cause: err,
|
|
153
|
+
context: { url },
|
|
154
|
+
recoverable: true
|
|
155
|
+
});
|
|
156
|
+
events.emit('failed', { error: errorObj, requestId });
|
|
157
|
+
events.emit('error', { error: errorObj, requestId });
|
|
158
|
+
|
|
159
|
+
cleanup();
|
|
160
|
+
reject(errorObj);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
xhr.ontimeout = () => {
|
|
164
|
+
const errorObj = new PlatformError({
|
|
165
|
+
code: 'NETWORK_TIMEOUT',
|
|
166
|
+
message: 'Upload request timed out',
|
|
167
|
+
context: { url },
|
|
168
|
+
recoverable: true
|
|
169
|
+
});
|
|
170
|
+
events.emit('timeout', { error: errorObj, requestId });
|
|
171
|
+
events.emit('failed', { error: errorObj, requestId });
|
|
172
|
+
events.emit('error', { error: errorObj, requestId });
|
|
173
|
+
|
|
174
|
+
cleanup();
|
|
175
|
+
reject(errorObj);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
xhr.send(fileOrData);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Networking & Caching Layer Usage Guide (`core.api`)
|
|
2
|
+
|
|
3
|
+
The `core.api` networking layer is a baseline-native, local-first HTTP client designed for high performance, self-healing retries, dynamic prefix routing, telemetry-driven event hooks, and fine-grained TTL caching.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Quick Start
|
|
8
|
+
|
|
9
|
+
Import the client directly from the ESM entry point:
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
import { api } from '@adukiorg/anza/api';
|
|
13
|
+
|
|
14
|
+
// Simple GET request (automatically parses JSON)
|
|
15
|
+
const profile = await api.get('/user/profile');
|
|
16
|
+
console.log(profile.name);
|
|
17
|
+
|
|
18
|
+
// Simple POST request with body serialization
|
|
19
|
+
const result = await api.post('/posts', { title: 'Native ESM rules' });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. Dynamic Outbound Prefix Resolution
|
|
25
|
+
|
|
26
|
+
Prefixes let you map short names or sub-paths to base URLs exactly once at application bootstrap. This keeps your request endpoints clean and isolates environment configuration.
|
|
27
|
+
|
|
28
|
+
### Registering Prefixes
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
import { api } from '@adukiorg/anza/api';
|
|
32
|
+
|
|
33
|
+
// Add configuration at startup
|
|
34
|
+
api.prefix.add('auth', 'https://auth.example.com');
|
|
35
|
+
api.prefix.add('default', 'https://api.example.com'); // acts as root fallback
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Automatic Path Resolution
|
|
39
|
+
|
|
40
|
+
The client automatically detects prefixes and safely concatenates paths without duplicating slashes:
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// Resolves to: https://auth.example.com/login
|
|
44
|
+
const token = await api.post('auth/login', { username, password });
|
|
45
|
+
|
|
46
|
+
// Resolves to root fallback: https://api.example.com/user/profile
|
|
47
|
+
const profile = await api.get('/user/profile');
|
|
48
|
+
|
|
49
|
+
// Fully qualified URLs are left unchanged
|
|
50
|
+
const data = await api.get('https://other-domain.org/data');
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 3. Fine-Grained TTL Caching
|
|
56
|
+
|
|
57
|
+
Caching in `core.api` defaults to a **zero-cache policy** (network-only) unless an explicit expiry/TTL is provided. This prevents dynamic state mutations from serving stale data by default.
|
|
58
|
+
|
|
59
|
+
### Caching with TTL
|
|
60
|
+
|
|
61
|
+
To cache successful GET responses, pass `expiry` or `ttl` (in milliseconds) inside request options:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
// Cache is checked first. On miss, network is called and cache is populated for 60 seconds
|
|
65
|
+
const products = await api.get('/products', { expiry: 60000 });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Cache Purging & Glob Invalidation
|
|
69
|
+
|
|
70
|
+
The cache manager supports granular deletions, whole store purges, or namespace-level invalidation utilizing standard glob patterns:
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
// Purge the entire API cache store
|
|
74
|
+
await api.cache.clear();
|
|
75
|
+
|
|
76
|
+
// Evict a single exact URL
|
|
77
|
+
await api.cache.delete('https://api.example.com/products');
|
|
78
|
+
|
|
79
|
+
// Glob Purging: Evict all endpoints starting with or containing /user/
|
|
80
|
+
await api.cache.delete('*/user/*');
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 4. Telemetry Events & Network Monitoring
|
|
86
|
+
|
|
87
|
+
The networking layer streams real-time connection events, allowing you to attach global hooks, security checks, and toast alerts based on HTTP statuses, content types, or failures.
|
|
88
|
+
|
|
89
|
+
### Standard Telemetry Events
|
|
90
|
+
|
|
91
|
+
| Event Name | Trigger |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `'error'` | Any network connection failure, request timeout, or server error |
|
|
94
|
+
| `'failed'` | Any non-ok response status (>= 400) or aborted connections |
|
|
95
|
+
| `'timeout'` | Requests exceeding their defined timeout limit |
|
|
96
|
+
| `'status:401'` | Specific HTTP 401 Unauthorized responses |
|
|
97
|
+
| `'status:500'` | Specific HTTP 500 Internal Server Error responses |
|
|
98
|
+
| `'status:xxx'` | Any specific HTTP status code (replace `xxx` with the desired code) |
|
|
99
|
+
| `'type:text'` | Successful responses carrying `text/plain` Content-Type |
|
|
100
|
+
| `'type:json'` | Successful responses carrying `application/json` Content-Type |
|
|
101
|
+
| `'type:stream'` | Successful responses carrying `text/event-stream` Content-Type |
|
|
102
|
+
|
|
103
|
+
### Request-Scoped Event Listeners (Automatic Cleanup)
|
|
104
|
+
|
|
105
|
+
If you only care about events triggered by a **specific network request**, you can define listeners directly in the request options using the `on` object. The API client scopes these listeners to this single request and **automatically cleans them up** as soon as the request terminates (succeeds or fails). No manual disposer calls are needed:
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
await api.get('/user/profile', {
|
|
109
|
+
on: {
|
|
110
|
+
// Fired only if this specific request returns a 401
|
|
111
|
+
'status:401': () => {
|
|
112
|
+
redirectToLogin();
|
|
113
|
+
},
|
|
114
|
+
// Fired only if this specific request fails or times out
|
|
115
|
+
'failed': (event) => {
|
|
116
|
+
console.error('Request failed:', event.detail.error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Global Listeners & Manual Disposers
|
|
123
|
+
|
|
124
|
+
For broad application state hooks (such as global loading spinners or error overlays), subscribe to events globally. The `api.on` method returns a synchronous disposer function to stop listening:
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
// Register a global listener and receive a clean unsubscription function
|
|
128
|
+
const offUnauthorized = api.on('status:401', (event) => {
|
|
129
|
+
redirectToLogin();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// To stop listening manually, simply invoke the returned function:
|
|
133
|
+
offUnauthorized();
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Advanced Lifecycle-Gated Cleaning (with AbortSignal)
|
|
137
|
+
|
|
138
|
+
If you are developing custom elements or reactive components, you can pass an `AbortSignal` as the third parameter to `api.on`. The networking engine will listen to the signal and **automatically clean up the listener** when the component unmounts or aborts, eliminating boilerplate disposer calls:
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
// Example inside a reactive Custom Element mount hook:
|
|
142
|
+
mount({ el, ctrl }) {
|
|
143
|
+
// Pass the element's mount AbortController signal
|
|
144
|
+
api.on('status:401', () => {
|
|
145
|
+
redirectToLogin();
|
|
146
|
+
}, ctrl.signal);
|
|
147
|
+
|
|
148
|
+
// Fully automated: detaches completely when the component unmounts!
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 5. Streaming & Progress Uploads
|
|
155
|
+
|
|
156
|
+
### Response Streams (NDJSON)
|
|
157
|
+
|
|
158
|
+
For live logs or AI streaming responses, the client parses newline-delimited JSON streams natively preserving backpressure. It fully supports request-scoped `on` listeners (e.g. `chunk`, `error`, `failed`) and broadcasts them to the global telemetry bus with automatic unsubscriptions:
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
// Stream lines as an AsyncIterable and hook event listeners directly
|
|
162
|
+
const eventStream = api.stream('https://api.example.com/logs/stream', {
|
|
163
|
+
on: {
|
|
164
|
+
// Triggers progressively for each enqueued JSON chunk
|
|
165
|
+
chunk: (event) => {
|
|
166
|
+
console.log('Progressive chunk:', event.detail.chunk);
|
|
167
|
+
},
|
|
168
|
+
// Triggers if the connection drops mid-stream
|
|
169
|
+
error: (event) => {
|
|
170
|
+
console.error('Stream dropped:', event.detail.error);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
for await (const chunk of eventStream) {
|
|
176
|
+
// Yields parsed JSON chunks symmetrically
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Tracking Upload Progress
|
|
181
|
+
|
|
182
|
+
Standard `fetch` uploads do not support progress callbacks in Baseline browsers. The client bridges this gap utilizing a highly precise native XMLHttpRequest gateway. It fully supports request-scoped `on` listeners—enabling clean tracking of upload `progress`, HTTP `status`, success, or `error` events:
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
const fileData = new FormData();
|
|
186
|
+
fileData.append('file', rawFileBlob);
|
|
187
|
+
|
|
188
|
+
const response = await api.upload('https://api.example.com/upload', fileData, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
on: {
|
|
191
|
+
// Fired progressively during transmission
|
|
192
|
+
progress: (event) => {
|
|
193
|
+
const { loaded, total, percentage } = event.detail;
|
|
194
|
+
console.log(`Uploaded: ${loaded} / ${total} (${percentage}%)`);
|
|
195
|
+
},
|
|
196
|
+
// Fired on successful completion
|
|
197
|
+
'status:200': (event) => {
|
|
198
|
+
console.log('Upload completed successfully!');
|
|
199
|
+
},
|
|
200
|
+
// Fired on connection errors
|
|
201
|
+
error: (event) => {
|
|
202
|
+
console.error('Upload failed:', event.detail.error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/events/bus.js
|
|
3
|
+
*
|
|
4
|
+
* Global Event Bus.
|
|
5
|
+
* Extends the native EventTarget for engine-optimized listener dispatch.
|
|
6
|
+
* Preserves backward-compatible .on() and .emit() wrapper methods while
|
|
7
|
+
* exposing the full native addEventListener / dispatchEvent interface.
|
|
8
|
+
*
|
|
9
|
+
* Source: doc 10 — Event Architecture §9
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class EventBus extends EventTarget {
|
|
13
|
+
/**
|
|
14
|
+
* Subscribes to a global event.
|
|
15
|
+
* If a signal is provided, the subscription is automatically cleaned up when the signal aborts.
|
|
16
|
+
* Returns a disposer function for manual cleanup.
|
|
17
|
+
*/
|
|
18
|
+
on(type, fn, signal) {
|
|
19
|
+
if (signal?.aborted) return () => {};
|
|
20
|
+
|
|
21
|
+
this.addEventListener(type, fn, { signal });
|
|
22
|
+
|
|
23
|
+
const dispose = () => {
|
|
24
|
+
this.removeEventListener(type, fn);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return dispose;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Dispatches a global custom event with a detail payload.
|
|
32
|
+
*/
|
|
33
|
+
emit(type, detail) {
|
|
34
|
+
this.dispatchEvent(new CustomEvent(type, { detail }));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const bus = new EventBus();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/events/delegate.js
|
|
3
|
+
*
|
|
4
|
+
* High-performance event delegation with fast-path selector matching.
|
|
5
|
+
* Handles dynamic delegation of events traversing Shadow DOM boundaries
|
|
6
|
+
* by evaluating event.composedPath(), caching matcher evaluations in a WeakMap.
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 10 — Event Architecture §6
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Dual-layered WeakMap cache to prevent redundant CSS selector evaluations
|
|
12
|
+
const matchesCache = new WeakMap();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Checks if an element matches a selector using cached query evaluations.
|
|
16
|
+
*/
|
|
17
|
+
function matchesSelector(element, selector) {
|
|
18
|
+
if (!element || typeof element.matches !== 'function') {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let selectorMap = matchesCache.get(element);
|
|
23
|
+
if (!selectorMap) {
|
|
24
|
+
selectorMap = new Map();
|
|
25
|
+
matchesCache.set(element, selectorMap);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (selectorMap.has(selector)) {
|
|
29
|
+
return selectorMap.get(selector);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const result = element.matches(selector);
|
|
33
|
+
selectorMap.set(selector, result);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Attaches a delegated event listener to an ancestor root element.
|
|
39
|
+
*
|
|
40
|
+
* @param {EventTarget} root - The root container element.
|
|
41
|
+
* @param {string} selector - The query selector to match dynamic descendants.
|
|
42
|
+
* @param {string} type - The event name to intercept.
|
|
43
|
+
* @param {Function} handler - The listener callback, with `this` bound to the matched element.
|
|
44
|
+
* @param {Object} [options={}] - Standard addEventListener options.
|
|
45
|
+
* @returns {Function} A disposer function that unregisters the delegation hook.
|
|
46
|
+
*/
|
|
47
|
+
export function delegate(root, selector, type, handler, options = {}) {
|
|
48
|
+
const signal = options.signal;
|
|
49
|
+
if (signal?.aborted) return () => {};
|
|
50
|
+
|
|
51
|
+
const listener = (event) => {
|
|
52
|
+
// composedPath() contains the full bubble chain, traversing through nested shadow trees
|
|
53
|
+
const path = event.composedPath();
|
|
54
|
+
|
|
55
|
+
for (const target of path) {
|
|
56
|
+
if (target === root) break;
|
|
57
|
+
if (matchesSelector(target, selector)) {
|
|
58
|
+
// Invoke handler with matched element bound to `this`
|
|
59
|
+
handler.call(target, event, target);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
root.addEventListener(type, listener, options);
|
|
66
|
+
|
|
67
|
+
const dispose = () => {
|
|
68
|
+
root.removeEventListener(type, listener, options);
|
|
69
|
+
if (signal) {
|
|
70
|
+
signal.removeEventListener('abort', dispose);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (signal) {
|
|
75
|
+
signal.addEventListener('abort', dispose, { once: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return dispose;
|
|
79
|
+
}
|