@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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/api/events/index.js
|
|
3
|
+
*
|
|
4
|
+
* Performant telemetry event emitter for all API network calls.
|
|
5
|
+
* Triggers hooks on timeouts, failures, specific HTTP status codes, or response content types.
|
|
6
|
+
*
|
|
7
|
+
* Source: doc 11 — Networking §2, core/api/plan.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class ApiEventEmitter {
|
|
11
|
+
#listeners = new Map();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Subscribes to a specific network or status event.
|
|
15
|
+
* Supports lifecycle-gated cleaning using AbortSignal.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} event
|
|
18
|
+
* @param {Function} handler
|
|
19
|
+
* @param {AbortSignal} [signal]
|
|
20
|
+
* @returns {Function} Disposer
|
|
21
|
+
*/
|
|
22
|
+
on(event, handler, signal) {
|
|
23
|
+
if (signal?.aborted) return () => {};
|
|
24
|
+
|
|
25
|
+
if (!this.#listeners.has(event)) {
|
|
26
|
+
this.#listeners.set(event, new Set());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const listener = { handler };
|
|
30
|
+
this.#listeners.get(event).add(listener);
|
|
31
|
+
|
|
32
|
+
let disposed = false;
|
|
33
|
+
const dispose = () => {
|
|
34
|
+
if (disposed) return;
|
|
35
|
+
disposed = true;
|
|
36
|
+
|
|
37
|
+
const set = this.#listeners.get(event);
|
|
38
|
+
if (set) {
|
|
39
|
+
set.delete(listener);
|
|
40
|
+
if (set.size === 0) {
|
|
41
|
+
this.#listeners.delete(event);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (signal) {
|
|
47
|
+
signal.addEventListener('abort', dispose, { once: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return dispose;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Emits a telemetry event with a detail payload.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} event
|
|
57
|
+
* @param {any} detail
|
|
58
|
+
*/
|
|
59
|
+
emit(event, detail) {
|
|
60
|
+
const set = this.#listeners.get(event);
|
|
61
|
+
if (!set) return;
|
|
62
|
+
|
|
63
|
+
const apiEvent = { type: event, detail };
|
|
64
|
+
|
|
65
|
+
for (const listener of [...set]) {
|
|
66
|
+
try {
|
|
67
|
+
listener.handler(apiEvent);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(`Error in API event listener for "${event}":`, err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const events = new ApiEventEmitter();
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/api/fetch.js
|
|
3
|
+
*
|
|
4
|
+
* Core fetch wrapper with AbortSignal, timeouts, and browser task priorities.
|
|
5
|
+
* Maps network and HTTP responses to a standard library-wide error shape.
|
|
6
|
+
*
|
|
7
|
+
* Source: doc 11 — Networking §3, §4, §9
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class PlatformError extends Error {
|
|
11
|
+
constructor({ code, message, cause, context, recoverable = true }) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'PlatformError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.cause = cause;
|
|
16
|
+
this.context = context || {};
|
|
17
|
+
this.recoverable = recoverable;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Executes a network fetch request with standard platform enhancements.
|
|
23
|
+
*/
|
|
24
|
+
export async function execute(descriptor) {
|
|
25
|
+
const {
|
|
26
|
+
url,
|
|
27
|
+
timeout = 10000,
|
|
28
|
+
priority = 'user-visible',
|
|
29
|
+
signal,
|
|
30
|
+
...fetchOpts
|
|
31
|
+
} = descriptor;
|
|
32
|
+
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const timeoutId = setTimeout(() => controller.abort('timeout'), timeout);
|
|
35
|
+
|
|
36
|
+
// Compose signals cleanly (leveraging AbortSignal.any where supported)
|
|
37
|
+
let activeSignal = controller.signal;
|
|
38
|
+
if (signal) {
|
|
39
|
+
if (typeof AbortSignal.any === 'function') {
|
|
40
|
+
activeSignal = AbortSignal.any([controller.signal, signal]);
|
|
41
|
+
} else {
|
|
42
|
+
if (signal.aborted) {
|
|
43
|
+
controller.abort(signal.reason);
|
|
44
|
+
} else {
|
|
45
|
+
signal.addEventListener('abort', () => {
|
|
46
|
+
controller.abort(signal.reason || 'aborted');
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const runFetch = async () => {
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(url, { ...fetchOpts, signal: activeSignal });
|
|
55
|
+
clearTimeout(timeoutId);
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw new PlatformError({
|
|
59
|
+
code: 'HTTP_ERROR',
|
|
60
|
+
message: `HTTP error ${response.status}: ${response.statusText}`,
|
|
61
|
+
context: { url, status: response.status, method: fetchOpts.method || 'GET' },
|
|
62
|
+
recoverable: response.status >= 500
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return response;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
|
|
70
|
+
if (err instanceof PlatformError) throw err;
|
|
71
|
+
|
|
72
|
+
const isTimeout = activeSignal.aborted && controller.signal.aborted;
|
|
73
|
+
throw new PlatformError({
|
|
74
|
+
code: isTimeout ? 'NETWORK_TIMEOUT' : 'NETWORK_ERROR',
|
|
75
|
+
message: err.message || (isTimeout ? 'Network request timed out' : 'Network request failed'),
|
|
76
|
+
cause: err,
|
|
77
|
+
context: { url, method: fetchOpts.method || 'GET' },
|
|
78
|
+
recoverable: true
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Run in browser task scheduler if supported (improves Interaction to Next Paint)
|
|
84
|
+
if (typeof globalThis.scheduler !== 'undefined' && typeof scheduler.postTask === 'function') {
|
|
85
|
+
return scheduler.postTask(runFetch, { priority, signal: activeSignal }).catch((err) => {
|
|
86
|
+
if (err instanceof PlatformError) throw err;
|
|
87
|
+
const isTimeout = activeSignal.aborted && controller.signal.aborted;
|
|
88
|
+
throw new PlatformError({
|
|
89
|
+
code: isTimeout ? 'NETWORK_TIMEOUT' : 'NETWORK_ERROR',
|
|
90
|
+
message: err.message || (isTimeout ? 'Network request timed out' : 'Network request failed'),
|
|
91
|
+
cause: err,
|
|
92
|
+
context: { url, method: fetchOpts.method || 'GET' },
|
|
93
|
+
recoverable: true
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return runFetch();
|
|
99
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/api/index.js
|
|
3
|
+
*
|
|
4
|
+
* Public networking layer entry point.
|
|
5
|
+
* Composes request mutators through the outbound/inbound pipeline,
|
|
6
|
+
* applying caching strategies, transient error retries, and standard timeout controls.
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 11 — Networking §2, §4
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { pipeline } from './pipeline.js';
|
|
12
|
+
import { execute, PlatformError } from './fetch.js';
|
|
13
|
+
import { retry } from './retry.js';
|
|
14
|
+
import { handle as handleCache } from './cache.js';
|
|
15
|
+
import { stream, createNDJSONTransform } from './stream.js';
|
|
16
|
+
import { upload } from './upload.js';
|
|
17
|
+
import { prefixes } from './prefixes/index.js';
|
|
18
|
+
import { events } from './events/index.js';
|
|
19
|
+
import { cache as apiCache } from './caches/index.js';
|
|
20
|
+
|
|
21
|
+
// Register global telemetry inbound interceptor to emit requests status/errors events
|
|
22
|
+
pipeline.inbound((responseOrError) => {
|
|
23
|
+
const requestId = responseOrError?.requestId;
|
|
24
|
+
|
|
25
|
+
if (responseOrError instanceof Error) {
|
|
26
|
+
const err = responseOrError;
|
|
27
|
+
const isTimeout = err.code === 'NETWORK_TIMEOUT';
|
|
28
|
+
if (isTimeout) {
|
|
29
|
+
events.emit('timeout', { error: err, requestId });
|
|
30
|
+
}
|
|
31
|
+
events.emit('error', { error: err, requestId });
|
|
32
|
+
events.emit('failed', { error: err, requestId });
|
|
33
|
+
} else {
|
|
34
|
+
const response = responseOrError;
|
|
35
|
+
const status = response.status;
|
|
36
|
+
|
|
37
|
+
events.emit(`status:${status}`, { response, requestId });
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
events.emit('failed', { response, requestId });
|
|
41
|
+
events.emit('error', { response, requestId });
|
|
42
|
+
} else {
|
|
43
|
+
const contentType = response.headers.get('Content-Type') || '';
|
|
44
|
+
if (contentType.includes('application/json')) {
|
|
45
|
+
events.emit('type:json', { response, requestId });
|
|
46
|
+
} else if (contentType.includes('text/event-stream')) {
|
|
47
|
+
events.emit('type:stream', { response, requestId });
|
|
48
|
+
} else if (contentType.includes('text/')) {
|
|
49
|
+
events.emit('type:text', { response, requestId });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return responseOrError;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalizes options and routes the request descriptor through the core pipeline.
|
|
58
|
+
*/
|
|
59
|
+
async function request(url, method, body, opts = {}) {
|
|
60
|
+
const resolvedUrl = prefixes.resolve(url);
|
|
61
|
+
const headers = new Headers(opts.headers || {});
|
|
62
|
+
|
|
63
|
+
// Auto-serialize JSON bodies
|
|
64
|
+
let parsedBody = body;
|
|
65
|
+
if (body && typeof body === 'object' && !(body instanceof Blob) && !(body instanceof FormData)) {
|
|
66
|
+
parsedBody = JSON.stringify(body);
|
|
67
|
+
if (!headers.has('Content-Type')) {
|
|
68
|
+
headers.set('Content-Type', 'application/json');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Generate unique request ID to scope request-specific event listeners
|
|
73
|
+
const requestId = typeof crypto !== 'undefined' && crypto.randomUUID
|
|
74
|
+
? crypto.randomUUID()
|
|
75
|
+
: Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
76
|
+
|
|
77
|
+
const descriptor = {
|
|
78
|
+
requestId,
|
|
79
|
+
url: resolvedUrl,
|
|
80
|
+
method,
|
|
81
|
+
headers,
|
|
82
|
+
body: parsedBody,
|
|
83
|
+
signal: opts.signal,
|
|
84
|
+
priority: opts.priority || 'user-visible',
|
|
85
|
+
timeout: opts.timeout || 10000,
|
|
86
|
+
cache: opts.cache, // cache strategy name: 'cache-first' | 'network-first' | 'stale-while-revalidate'
|
|
87
|
+
retries: opts.retries ?? 3,
|
|
88
|
+
...opts
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Register request-specific temporary listeners with automatic scoped cleanup
|
|
92
|
+
const disposes = [];
|
|
93
|
+
if (opts.on && typeof opts.on === 'object') {
|
|
94
|
+
for (const [event, handler] of Object.entries(opts.on)) {
|
|
95
|
+
if (typeof handler === 'function') {
|
|
96
|
+
const dispose = events.on(event, (e) => {
|
|
97
|
+
if (e.detail?.requestId === requestId) {
|
|
98
|
+
handler(e);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
disposes.push(dispose);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Run through pipeline -> cache handler -> retry handler -> fetch executor
|
|
108
|
+
const response = await pipeline.run(descriptor, async (currentDesc) => {
|
|
109
|
+
return handleCache(currentDesc, async (cacheDesc) => {
|
|
110
|
+
return retry(
|
|
111
|
+
() => execute(cacheDesc),
|
|
112
|
+
{
|
|
113
|
+
attempts: cacheDesc.retries,
|
|
114
|
+
signal: cacheDesc.signal
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Automatically extract response payload if successful
|
|
121
|
+
const contentType = response.headers.get('Content-Type') || '';
|
|
122
|
+
if (contentType.includes('application/json')) {
|
|
123
|
+
return response.json();
|
|
124
|
+
}
|
|
125
|
+
return response.text();
|
|
126
|
+
} finally {
|
|
127
|
+
// Perform guaranteed automatic cleanup of temporary request-specific listeners
|
|
128
|
+
for (const dispose of disposes) {
|
|
129
|
+
dispose();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const api = {
|
|
135
|
+
get: (url, opts) => request(url, 'GET', null, opts),
|
|
136
|
+
post: (url, body, opts) => request(url, 'POST', body, opts),
|
|
137
|
+
put: (url, body, opts) => request(url, 'PUT', body, opts),
|
|
138
|
+
patch: (url, body, opts) => request(url, 'PATCH', body, opts),
|
|
139
|
+
delete: (url, opts) => request(url, 'DELETE', null, opts),
|
|
140
|
+
stream,
|
|
141
|
+
upload,
|
|
142
|
+
pipeline,
|
|
143
|
+
PlatformError,
|
|
144
|
+
|
|
145
|
+
// Prefix registry singleton APIs
|
|
146
|
+
prefix: prefixes,
|
|
147
|
+
|
|
148
|
+
// Cache manager APIs
|
|
149
|
+
cache: apiCache,
|
|
150
|
+
|
|
151
|
+
// Event emitter hooks
|
|
152
|
+
on: (event, handler, signal) => events.on(event, handler, signal),
|
|
153
|
+
emit: (event, detail) => events.emit(event, detail)
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export { pipeline, PlatformError, execute, retry, createNDJSONTransform, stream, upload, prefixes, events, apiCache as cache };
|
|
157
|
+
|
|
158
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/api/pipeline.js
|
|
3
|
+
*
|
|
4
|
+
* Composable interceptor chain.
|
|
5
|
+
* Manages request modification (outbound), short-circuiting (caching/mocking),
|
|
6
|
+
* and response normalization (inbound).
|
|
7
|
+
*
|
|
8
|
+
* Source: doc 11 — Networking §4, §5
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class Pipeline {
|
|
12
|
+
#outbound = [];
|
|
13
|
+
#inbound = [];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Registers an outbound (request) interceptor.
|
|
17
|
+
* Outbound interceptors receive a request descriptor and can:
|
|
18
|
+
* 1. Return a modified descriptor to pass to the next interceptor.
|
|
19
|
+
* 2. Return a Response instance to short-circuit the pipeline (e.g. cache hit).
|
|
20
|
+
*/
|
|
21
|
+
outbound(interceptor) {
|
|
22
|
+
if (typeof interceptor !== 'function') {
|
|
23
|
+
throw new Error('Pipeline interceptor must be a function');
|
|
24
|
+
}
|
|
25
|
+
this.#outbound.push(interceptor);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Registers an inbound (response/error) interceptor.
|
|
31
|
+
* Inbound interceptors receive the Response (or Error) and return a Response
|
|
32
|
+
* or throw a normalized Error.
|
|
33
|
+
*/
|
|
34
|
+
inbound(interceptor) {
|
|
35
|
+
if (typeof interceptor !== 'function') {
|
|
36
|
+
throw new Error('Pipeline interceptor must be a function');
|
|
37
|
+
}
|
|
38
|
+
this.#inbound.push(interceptor);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Executes the pipeline with a given request descriptor and a final fetch executor.
|
|
44
|
+
*/
|
|
45
|
+
async run(descriptor, executeFetch) {
|
|
46
|
+
let current = { ...descriptor };
|
|
47
|
+
|
|
48
|
+
// 1. Outbound Pipeline
|
|
49
|
+
for (const interceptor of this.#outbound) {
|
|
50
|
+
const result = await interceptor(current);
|
|
51
|
+
if (result instanceof Response) {
|
|
52
|
+
// Short circuit: Return mock or cached response
|
|
53
|
+
try {
|
|
54
|
+
result.requestId = current.requestId;
|
|
55
|
+
} catch (_) {}
|
|
56
|
+
return this.#runInbound(result);
|
|
57
|
+
}
|
|
58
|
+
if (result) {
|
|
59
|
+
current = result;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Fetch Execution
|
|
64
|
+
let response;
|
|
65
|
+
try {
|
|
66
|
+
response = await executeFetch(current);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// Pass network/timeout errors to the inbound pipeline for normalization
|
|
69
|
+
try {
|
|
70
|
+
err.requestId = current.requestId;
|
|
71
|
+
} catch (_) {}
|
|
72
|
+
return this.#runInbound(err);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 3. Inbound Pipeline
|
|
76
|
+
try {
|
|
77
|
+
response.requestId = current.requestId;
|
|
78
|
+
} catch (_) {}
|
|
79
|
+
return this.#runInbound(response);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async #runInbound(responseOrError) {
|
|
83
|
+
let current = responseOrError;
|
|
84
|
+
for (const interceptor of this.#inbound) {
|
|
85
|
+
try {
|
|
86
|
+
current = await interceptor(current);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
current = err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (current instanceof Error) {
|
|
92
|
+
throw current;
|
|
93
|
+
}
|
|
94
|
+
return current;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const pipeline = new Pipeline();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# API Client & Cache Enhancement Plan
|
|
2
|
+
|
|
3
|
+
This document outlines the blueprint for enhancing the `core.api` networking client with localized, fine-grained caching, prefix registration, namespace-level invalidation, and custom network/status-code events.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Architectural Architecture & Requirements
|
|
8
|
+
|
|
9
|
+
We will extend `core.api` to provide:
|
|
10
|
+
|
|
11
|
+
1. **Default Zero-Cache Policy:** All calls default to no cache, running directly against the network.
|
|
12
|
+
2. **TTL/Expiry-based API Caching:** Active caching when `expiry` or `ttl` (Time-To-Live) options are provided. Hits cache first, falls back to the network on a miss, and caches successful responses.
|
|
13
|
+
3. **Namespace-level Glob Invalidation:** Support for clearing the entire cache, a single URL, or a glob namespace pattern (e.g. `/user/*`).
|
|
14
|
+
4. **Outbound Prefix Resolution:** Initializing base prefixes once (optionally) to cleanly rewrite and resolve endpoint URLs.
|
|
15
|
+
5. **Network Event Hub:** An integrated event listener system firing on generic network failures (errors, timeouts) or specific responses (status codes like `401`, `500`, or content types like `json`, `text`).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2. Structural & Folder Layout
|
|
20
|
+
|
|
21
|
+
In accordance with our naming and folder conventions (`RULE[user_global]`), we will group caching and events into highly structured subfolders under `src/core/api/`:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
src/core/api/
|
|
25
|
+
├── caches/ # Subfolder for caching adapters & glob matching
|
|
26
|
+
│ ├── glob.js # Glob/Namespace pattern matching helper
|
|
27
|
+
│ └── index.js # Unified local API cache client
|
|
28
|
+
├── events/ # Subfolder for network telemetry events
|
|
29
|
+
│ └── index.js # API event emitter implementation
|
|
30
|
+
├── prefixes/ # Subfolder for prefix/base URL resolving
|
|
31
|
+
│ └── index.js # Prefix store and path normalization
|
|
32
|
+
├── index.js # Core API client entry point
|
|
33
|
+
├── fetch.js # Network request executor
|
|
34
|
+
├── pipeline.js # Inbound/Outbound pipeline coordinator
|
|
35
|
+
├── retry.js # Exponential backoff retry handler
|
|
36
|
+
├── stream.js # Streams and NDJSON transform pipeline
|
|
37
|
+
└── upload.js # XMLHttpRequest-based upload gateway
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 3. Technical Blueprint & Interface Design
|
|
43
|
+
|
|
44
|
+
### Caches & Glob Purging (`src/core/api/caches/`)
|
|
45
|
+
|
|
46
|
+
The API cache uses the native browser Cache API with custom header tags to track record creation times and TTL.
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
// src/core/api/caches/index.js
|
|
50
|
+
export class ApiCache {
|
|
51
|
+
constructor(name = 'platform-api-cache') {
|
|
52
|
+
this.name = name;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async get(url) {
|
|
56
|
+
if (typeof caches === 'undefined') return null;
|
|
57
|
+
const store = await caches.open(this.name);
|
|
58
|
+
const cached = await store.match(url);
|
|
59
|
+
if (!cached) return null;
|
|
60
|
+
|
|
61
|
+
const expiresAt = cached.headers.get('x-expires-at');
|
|
62
|
+
if (expiresAt && Date.now() > Number(expiresAt)) {
|
|
63
|
+
await store.delete(url);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return cached.clone();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async set(url, response, ttlMs) {
|
|
70
|
+
if (typeof caches === 'undefined') return;
|
|
71
|
+
const store = await caches.open(this.name);
|
|
72
|
+
const headers = new Headers(response.headers);
|
|
73
|
+
headers.set('x-expires-at', String(Date.now() + ttlMs));
|
|
74
|
+
|
|
75
|
+
const cloned = new Response(response.body ? response.clone().body : null, {
|
|
76
|
+
status: response.status,
|
|
77
|
+
statusText: response.statusText,
|
|
78
|
+
headers
|
|
79
|
+
});
|
|
80
|
+
await store.put(url, cloned);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async delete(pattern) {
|
|
84
|
+
if (typeof caches === 'undefined') return;
|
|
85
|
+
const store = await caches.open(this.name);
|
|
86
|
+
|
|
87
|
+
if (pattern.includes('*')) {
|
|
88
|
+
const regex = globToRegex(pattern);
|
|
89
|
+
const keys = await store.keys();
|
|
90
|
+
for (const req of keys) {
|
|
91
|
+
if (regex.test(req.url) || regex.test(new URL(req.url).pathname)) {
|
|
92
|
+
await store.delete(req);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
await store.delete(pattern);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async clear() {
|
|
101
|
+
if (typeof caches === 'undefined') return;
|
|
102
|
+
await caches.delete(this.name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Glob to Regex conversion helper:
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// src/core/api/caches/glob.js
|
|
111
|
+
export function globToRegex(pattern) {
|
|
112
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
113
|
+
const wildcarded = escaped.replace(/\*/g, '.*');
|
|
114
|
+
return new RegExp(`^${wildcarded}$`);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Prefix Resolver (`src/core/api/prefixes/`)
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
// src/core/api/prefixes/index.js
|
|
122
|
+
export class PrefixRegistry {
|
|
123
|
+
#prefixes = new Map();
|
|
124
|
+
|
|
125
|
+
add(name, value) {
|
|
126
|
+
this.#prefixes.set(name, value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
resolve(url) {
|
|
130
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
131
|
+
return url;
|
|
132
|
+
}
|
|
133
|
+
for (const [prefix, base] of this.#prefixes.entries()) {
|
|
134
|
+
if (url.startsWith(`/${prefix}/`)) {
|
|
135
|
+
return base + url.slice(prefix.length + 1);
|
|
136
|
+
}
|
|
137
|
+
if (url.startsWith(`${prefix}/`)) {
|
|
138
|
+
return base + '/' + url.slice(prefix.length);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Default fallback to registered root or baseline domain
|
|
142
|
+
const root = this.#prefixes.get('root') || this.#prefixes.get('default');
|
|
143
|
+
if (root) {
|
|
144
|
+
return root + (url.startsWith('/') ? url : '/' + url);
|
|
145
|
+
}
|
|
146
|
+
return url;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Telemetry Events (`src/core/api/events/`)
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
// src/core/api/events/index.js
|
|
155
|
+
export class ApiEventEmitter {
|
|
156
|
+
#listeners = new Map();
|
|
157
|
+
|
|
158
|
+
on(event, handler, signal) {
|
|
159
|
+
if (signal?.aborted) return () => {};
|
|
160
|
+
if (!this.#listeners.has(event)) {
|
|
161
|
+
this.#listeners.set(event, new Set());
|
|
162
|
+
}
|
|
163
|
+
const listener = { handler };
|
|
164
|
+
this.#listeners.get(event).add(listener);
|
|
165
|
+
|
|
166
|
+
const dispose = () => {
|
|
167
|
+
const set = this.#listeners.get(event);
|
|
168
|
+
if (set) {
|
|
169
|
+
set.delete(listener);
|
|
170
|
+
if (set.size === 0) this.#listeners.delete(event);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (signal) {
|
|
175
|
+
signal.addEventListener('abort', dispose);
|
|
176
|
+
}
|
|
177
|
+
return dispose;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
emit(event, detail) {
|
|
181
|
+
const set = this.#listeners.get(event);
|
|
182
|
+
if (!set) return;
|
|
183
|
+
const customEvent = { type: event, detail };
|
|
184
|
+
for (const listener of [...set]) {
|
|
185
|
+
try {
|
|
186
|
+
listener.handler(customEvent);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.error(`Error in API event listener for "${event}":`, err);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 4. Verification Plan
|
|
198
|
+
|
|
199
|
+
### Automated Verification
|
|
200
|
+
|
|
201
|
+
- Write comprehensive test coverage validating:
|
|
202
|
+
- Cache hits on active TTL/Expiry options.
|
|
203
|
+
- Glob namespace clearing matches (e.g. `/user/*` successfully purges `/user/profile` and `/user/settings`).
|
|
204
|
+
- Outbound path prefix expansion.
|
|
205
|
+
- Correct event dispatching for generic `'error'`, `'timeout'`, specific codes (`status:401`), and content types (`type:json`).
|
|
206
|
+
|
|
207
|
+
### Integration Check
|
|
208
|
+
|
|
209
|
+
- Assert network trace outputs and test page lifecycle triggers inside the browser environment.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/api/prefixes/index.js
|
|
3
|
+
*
|
|
4
|
+
* Prefix and Base URL registry for relative path expansion.
|
|
5
|
+
* Allows clean routing/prefix matching at connection startup.
|
|
6
|
+
*
|
|
7
|
+
* Source: doc 11 — Networking §2, core/api/plan.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class PrefixRegistry {
|
|
11
|
+
#prefixes = new Map();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Registers a new base URL prefix.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} name
|
|
17
|
+
* @param {string} value
|
|
18
|
+
*/
|
|
19
|
+
add(name, value) {
|
|
20
|
+
this.#prefixes.set(name, value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Clears all registered prefixes.
|
|
25
|
+
*/
|
|
26
|
+
clear() {
|
|
27
|
+
this.#prefixes.clear();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolves a URL prefix against registered base URLs.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} url
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
resolve(url) {
|
|
37
|
+
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) {
|
|
38
|
+
return url;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const [prefix, base] of this.#prefixes.entries()) {
|
|
42
|
+
if (url.startsWith(`/${prefix}/`)) {
|
|
43
|
+
const suffix = url.slice(prefix.length + 2);
|
|
44
|
+
const normalizedBase = base.endsWith('/') ? base : base + '/';
|
|
45
|
+
return normalizedBase + suffix;
|
|
46
|
+
}
|
|
47
|
+
if (url.startsWith(`${prefix}/`)) {
|
|
48
|
+
const suffix = url.slice(prefix.length + 1);
|
|
49
|
+
const normalizedBase = base.endsWith('/') ? base : base + '/';
|
|
50
|
+
return normalizedBase + suffix;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Default fallback base
|
|
55
|
+
const root = this.#prefixes.get('root') || this.#prefixes.get('default');
|
|
56
|
+
if (root) {
|
|
57
|
+
const normalizedRoot = root.endsWith('/') ? root.slice(0, -1) : root;
|
|
58
|
+
const normalizedUrl = url.startsWith('/') ? url : '/' + url;
|
|
59
|
+
return normalizedRoot + normalizedUrl;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return url;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const prefixes = new PrefixRegistry();
|