@celestia-island/hikari 0.46.0 → 0.47.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -1,6 +1,7 @@
1
1
  import { onMounted, onUnmounted, ref, type Ref } from "vue";
2
2
 
3
3
  import { scheduleInterval, scheduleIntervalAfter, type IntervalHandle } from "../runtime/intervalBus";
4
+ import { reportHkRuntime, type HkRuntimeHandle } from "../runtime/registry";
4
5
 
5
6
  /** Fetch-based connectivity probe — the hikari-local successor of the
6
7
  * plana-ui probe. Polls `/api/health` (or a custom endpoint set via
@@ -28,6 +29,20 @@ const DEFAULT_RETRY_TOTAL = 3;
28
29
 
29
30
  let probeEndpoint: string | (() => string) = "";
30
31
 
32
+ // Runtime-registry reporting (the "context of contexts"): the probe is a
33
+ // per-component composable, so the MODULE is the hook — one registry
34
+ // entry counting live probe instances, with the configured endpoint in
35
+ // the read facet. Registration API unchanged.
36
+ let liveProbes = 0;
37
+ let probeRuntime: HkRuntimeHandle | null = null;
38
+ function ensureProbeRuntime(): HkRuntimeHandle {
39
+ return (probeRuntime ??= reportHkRuntime("connectionProbe", {
40
+ kind: "hook",
41
+ description: "The fetch-based connectivity probe: per-component /api/health polling with retry and countdown.",
42
+ read: () => ({ liveProbes, endpoint: endpointUrl() }),
43
+ }));
44
+ }
45
+
31
46
  /** Override the probed endpoint (defaults to same-origin `/api/health`). */
32
47
  export function setProbeEndpoint(endpoint: string | (() => string)): void {
33
48
  probeEndpoint = endpoint;
@@ -127,11 +142,15 @@ export function useConnectionProbe(): {
127
142
  }
128
143
 
129
144
  onMounted(() => {
145
+ liveProbes += 1;
146
+ ensureProbeRuntime().pulse({ liveProbes });
130
147
  void probeOnce();
131
148
  tickHandle = scheduleInterval(applyCountdown, 1000);
132
149
  });
133
150
 
134
151
  onUnmounted(() => {
152
+ liveProbes = Math.max(0, liveProbes - 1);
153
+ probeRuntime?.pulse({ liveProbes });
135
154
  pollHandle?.disconnect();
136
155
  pollHandle = null;
137
156
  tickHandle?.disconnect();
@@ -1,5 +1,7 @@
1
1
  import { reactive } from "vue";
2
2
 
3
+ import { reportHkRuntime, type HkRuntimeHandle } from "../runtime/registry";
4
+
3
5
  export interface ProgressDialogState {
4
6
  open: boolean;
5
7
  title: string;
@@ -24,7 +26,27 @@ const state = reactive<ProgressDialogState>({
24
26
  max: 100,
25
27
  });
26
28
 
29
+ // Runtime-registry reporting (the "context of contexts"). Lazy: the
30
+ // progress-dialog context reports itself on first use and pulses on
31
+ // open/close, so the live dialog tree is answerable from
32
+ // readHkRuntime("progressDialog") without importing this module.
33
+ let runtimeReport: HkRuntimeHandle | null = null;
34
+ function ensureRuntimeReport(): HkRuntimeHandle {
35
+ return (runtimeReport ??= reportHkRuntime("progressDialog", {
36
+ kind: "context",
37
+ description: "The progress dialog context: the single global task-progress surface (title, log tail, value/max).",
38
+ read: () => ({
39
+ open: state.open,
40
+ title: state.title,
41
+ logLines: state.logs.length,
42
+ value: state.value,
43
+ max: state.max,
44
+ }),
45
+ }));
46
+ }
47
+
27
48
  export function showProgressDialog(opts: { title: string }): ProgressDialogHandle {
49
+ ensureRuntimeReport().pulse({ open: true, title: opts.title });
28
50
  state.title = opts.title;
29
51
  state.logs = [];
30
52
  state.value = null;
@@ -43,10 +65,12 @@ export function showProgressDialog(opts: { title: string }): ProgressDialogHandl
43
65
  },
44
66
  close() {
45
67
  state.open = false;
68
+ runtimeReport?.pulse({ open: false });
46
69
  },
47
70
  };
48
71
  }
49
72
 
50
73
  export function useProgressDialog(): ProgressDialogState {
74
+ ensureRuntimeReport();
51
75
  return state;
52
76
  }
@@ -1,5 +1,7 @@
1
1
  import { ref, type Ref } from "vue";
2
2
 
3
+ import { reportHkRuntime, type HkRuntimeHandle } from "../runtime/registry";
4
+
3
5
  /** Where a reported error came from. */
4
6
  export type HkErrorSource = "vue" | "window" | "rejection" | "manual";
5
7
 
@@ -66,6 +68,10 @@ export function getErrorReportingOptions(): HkErrorReportingOptions {
66
68
 
67
69
  export function setErrorReportingOptions(options: HkErrorReportingOptions): void {
68
70
  activeOptions = options;
71
+ runtimeReport?.setMeta({
72
+ captureWindow: options.captureWindow !== false,
73
+ captureRejection: options.captureRejection !== false,
74
+ });
69
75
  }
70
76
 
71
77
  export function useErrorReportingState(): Ref<HkReportedError | null> {
@@ -86,6 +92,11 @@ export function reportError(err: unknown, source: HkErrorSource, info?: string):
86
92
 
87
93
  if (currentError.value !== null) return null;
88
94
 
95
+ // Accepted onto the landing (first-error-wins): later errors while the
96
+ // landing is up still log/fire onError above but do not count.
97
+ acceptedReports += 1;
98
+ runtimeReport?.pulse({ reports: acceptedReports });
99
+
89
100
  const normalized = normalizeError(err);
90
101
  const record: HkReportedError = {
91
102
  source,
@@ -102,3 +113,22 @@ export function reportError(err: unknown, source: HkErrorSource, info?: string):
102
113
  export function clearErrorReportingState(): void {
103
114
  currentError.value = null;
104
115
  }
116
+
117
+ // Runtime-registry reporting (the "context of contexts"): the error
118
+ // reporting module is a global singleton, so it reports itself at module
119
+ // init and pulses on every accepted error. The read facet keeps the
120
+ // current landing card observable (name/message/source only — never the
121
+ // whole error object) from readHkRuntime("errorReporting").
122
+ let acceptedReports = 0;
123
+ const runtimeReport: HkRuntimeHandle = reportHkRuntime("errorReporting", {
124
+ kind: "plugin",
125
+ description: "The global error reporting landing: app.config.errorHandler + window/rejection hooks with the full-screen error card.",
126
+ meta: { captureWindow: activeOptions.captureWindow !== false, captureRejection: activeOptions.captureRejection !== false },
127
+ read: () => ({
128
+ reports: acceptedReports,
129
+ landingUp: currentError.value !== null,
130
+ current: currentError.value
131
+ ? { name: currentError.value.name, message: currentError.value.message, source: currentError.value.source }
132
+ : null,
133
+ }),
134
+ });
@@ -1,5 +1,7 @@
1
1
  import { reactive } from "vue";
2
2
 
3
+ import { reportHkRuntime } from "../runtime/registry";
4
+
3
5
  type Messages = Record<string, string>;
4
6
 
5
7
  interface LocaleModule {
@@ -63,6 +65,7 @@ export async function setLocale(locale: string): Promise<void> {
63
65
  ...(mergedMessages["en"] ?? {}),
64
66
  ...(mergedMessages[locale] ?? {}),
65
67
  };
68
+ runtime.pulse({ locale });
66
69
  }
67
70
 
68
71
  export function mergeMessages(userMessages: Messages, locale?: string): void {
@@ -82,6 +85,7 @@ export function mergeMessages(userMessages: Messages, locale?: string): void {
82
85
  ...(mergedMessages["en"] ?? {}),
83
86
  ...(mergedMessages[state.locale] ?? {}),
84
87
  };
88
+ runtime.pulse({ locale: state.locale });
85
89
  }
86
90
 
87
91
  export function useI18n() {
@@ -96,3 +100,31 @@ export function useI18n() {
96
100
 
97
101
  return { t, locale: state.locale };
98
102
  }
103
+
104
+ // Runtime-registry reporting (the "context of contexts"): the i18n
105
+ // context is a module singleton, so it reports itself at module init and
106
+ // pulses on every locale switch / message merge. The facets make the
107
+ // locale tree reachable and steerable from anywhere via
108
+ // readHkRuntime("i18n") / writeHkRuntime("i18n", …) without importing
109
+ // this module.
110
+ const runtime = reportHkRuntime("i18n", {
111
+ kind: "context",
112
+ description: "The i18n context: active locale, message tables and per-locale merged overrides.",
113
+ meta: { locale: state.locale },
114
+ read: () => ({
115
+ locale: state.locale,
116
+ keys: Object.keys(state.messages).length,
117
+ mergedLocales: Object.keys(mergedMessages),
118
+ }),
119
+ write: (op) => {
120
+ if (op.type === "setLocale" && typeof op.locale === "string") {
121
+ void setLocale(op.locale);
122
+ return;
123
+ }
124
+ if (op.type === "mergeMessages" && typeof op.messages === "object" && op.messages !== null) {
125
+ mergeMessages(op.messages as Messages, typeof op.locale === "string" ? op.locale : undefined);
126
+ return;
127
+ }
128
+ throw new Error(`unknown i18n write op "${op.type}"`);
129
+ },
130
+ });
package/src/index.ts CHANGED
@@ -244,6 +244,22 @@ export {
244
244
  type HkTooltipBridgeOptions,
245
245
  installHkImageFallback,
246
246
  type HkImageFallbackOptions,
247
+ reportHkRuntime,
248
+ getHkRuntimeEntry,
249
+ listHkRuntime,
250
+ readHkRuntime,
251
+ writeHkRuntime,
252
+ hkRuntimeSnapshot,
253
+ useHkRegistry,
254
+ type HkRuntimeKind,
255
+ type HkRuntimeStatus,
256
+ type HkRuntimeMeta,
257
+ type HkRuntimeEntryState,
258
+ type HkRuntimeReport,
259
+ type HkRuntimeHandle,
260
+ type HkRuntimeWriteOp,
261
+ type HkRuntimeSnapshot,
262
+ type HkRuntimeSnapshotEntry,
247
263
  } from "./runtime";
248
264
 
249
265
  // i18n
@@ -1,5 +1,7 @@
1
1
  import { setCssAnimationsEnabled } from "../animation/registerAnimations";
2
2
 
3
+ import { reportHkRuntime, type HkRuntimeHandle } from "./registry";
4
+
3
5
  export interface FrameContext {
4
6
  delta: number;
5
7
  elapsed: number;
@@ -81,6 +83,35 @@ function busyCount(): number {
81
83
  return syncEntries.size + normalEntries.size + idleEntries.size + intervalEntries.size + activeTransitions.size;
82
84
  }
83
85
 
86
+ // Runtime-registry reporting (the "context of contexts"). Lazy: the bus
87
+ // reports itself the first time anything schedules onto it and pulses on
88
+ // every scheduling call / reduced-motion flip. The read facet exposes
89
+ // live load (entries per tier, paused) and the write facet lets another
90
+ // context or a host plugin flip reduced motion plugin-style via
91
+ // writeHkRuntime("animationBus", { type: "setReducedMotion", value }).
92
+ let runtimeReport: HkRuntimeHandle | null = null;
93
+ function ensureRuntimeReport(): HkRuntimeHandle {
94
+ return (runtimeReport ??= reportHkRuntime("animationBus", {
95
+ kind: "bus",
96
+ description: "The shared rAF frame bus: sync/normal/idle tiers, one-shots, transitions and bus-ridden intervals.",
97
+ read: () => ({
98
+ sync: syncEntries.size,
99
+ normal: normalEntries.size,
100
+ idle: idleEntries.size,
101
+ intervals: intervalEntries.size,
102
+ transitions: activeTransitions.size,
103
+ paused,
104
+ }),
105
+ write: (op) => {
106
+ if (op.type === "setReducedMotion" && typeof op.value === "boolean") {
107
+ setReducedMotion(op.value);
108
+ return;
109
+ }
110
+ throw new Error(`unknown animationBus write op "${op.type}"`);
111
+ },
112
+ }));
113
+ }
114
+
84
115
  function pickRegistry(priority: Priority): Map<string, Entry> {
85
116
  switch (priority) {
86
117
  case "sync": return syncEntries;
@@ -195,7 +226,10 @@ export function setReducedMotion(flag: boolean) {
195
226
  // the switch on every call so a direct setCssAnimationsEnabled(false)
196
227
  // from a performance-context caller is undone when motion resumes.
197
228
  setCssAnimationsEnabled(!flag);
198
- if (paused === flag) return;
229
+ if (paused === flag) {
230
+ ensureRuntimeReport().pulse({ paused });
231
+ return;
232
+ }
199
233
  paused = flag;
200
234
  if (flag) {
201
235
  if (raf !== 0) {
@@ -209,6 +243,7 @@ export function setReducedMotion(flag: boolean) {
209
243
  } else {
210
244
  ensure();
211
245
  }
246
+ ensureRuntimeReport().pulse({ paused });
212
247
  }
213
248
 
214
249
  export function notifyScrollStart() {
@@ -225,6 +260,7 @@ export function onFrame(cb: Callback, priority: Priority = "sync"): AnimationHan
225
260
  const registry = pickRegistry(priority);
226
261
  registry.set(id, { cb, priority, lastRun: 0 });
227
262
  ensure();
263
+ ensureRuntimeReport().pulse();
228
264
  return {
229
265
  disconnect() {
230
266
  registry.delete(id);
@@ -252,6 +288,7 @@ function pumpOneShots(now: number): void {
252
288
  export function onceFrame(cb: Callback): void {
253
289
  oneShotEntries.set(uid(), { cb, cancelled: false });
254
290
  ensureOneShotDrain();
291
+ ensureRuntimeReport().pulse();
255
292
  }
256
293
 
257
294
  export function scheduleFrame(cb: Callback): AnimationHandle {
@@ -259,6 +296,7 @@ export function scheduleFrame(cb: Callback): AnimationHandle {
259
296
  const id = uid();
260
297
  oneShotEntries.set(id, entry);
261
298
  ensureOneShotDrain();
299
+ ensureRuntimeReport().pulse();
262
300
  return {
263
301
  disconnect() {
264
302
  entry.cancelled = true;
@@ -271,6 +309,7 @@ export function reportTransition(durationMs: number): AnimationHandle {
271
309
  const id = uid();
272
310
  activeTransitions.add(id);
273
311
  ensure();
312
+ ensureRuntimeReport().pulse();
274
313
  const timer = setTimeout(() => {
275
314
  activeTransitions.delete(id);
276
315
  halt();
@@ -288,6 +327,7 @@ export function scheduleEvery(cb: () => void, intervalMs: number): AnimationHand
288
327
  const id = uid();
289
328
  intervalEntries.set(id, { cb, interval: intervalMs, lastRun: 0, once: false });
290
329
  ensure();
330
+ ensureRuntimeReport().pulse();
291
331
  return {
292
332
  disconnect() {
293
333
  intervalEntries.delete(id);
@@ -300,6 +340,7 @@ export function scheduleAfter(cb: () => void, delayMs: number): AnimationHandle
300
340
  const id = uid();
301
341
  intervalEntries.set(id, { cb, interval: delayMs, lastRun: performance.now(), once: true });
302
342
  ensure();
343
+ ensureRuntimeReport().pulse();
303
344
  return {
304
345
  disconnect() {
305
346
  intervalEntries.delete(id);
@@ -1,12 +1,28 @@
1
+ import { reportHkRuntime, type HkRuntimeHandle } from "./registry";
2
+
1
3
  export interface CronHandle {
2
4
  disconnect(): void;
3
5
  }
4
6
 
7
+ // Runtime-registry reporting (the "context of contexts"). Lazy: the bus
8
+ // reports itself on first schedule and pulses on every schedule/
9
+ // disconnect. Bare-timer cron has no inspectable state beyond its
10
+ // liveness, which is exactly what the registry is for.
11
+ let runtimeReport: HkRuntimeHandle | null = null;
12
+ function ensureRuntimeReport(): HkRuntimeHandle {
13
+ return (runtimeReport ??= reportHkRuntime("cronBus", {
14
+ kind: "bus",
15
+ description: "The bare timer bus (setInterval/setTimeout passthrough) used by cron-grade callers like toast auto-dismiss.",
16
+ }));
17
+ }
18
+
5
19
  export function scheduleCron(cb: () => void, intervalMs: number): CronHandle {
6
20
  const id = setInterval(cb, intervalMs);
21
+ ensureRuntimeReport().pulse();
7
22
  return {
8
23
  disconnect() {
9
24
  clearInterval(id);
25
+ runtimeReport?.pulse();
10
26
  },
11
27
  };
12
28
  }
@@ -17,9 +33,11 @@ export function scheduleCronAfter(cb: () => void, delayMs: number): CronHandle {
17
33
  fired = true;
18
34
  cb();
19
35
  }, delayMs);
36
+ ensureRuntimeReport().pulse();
20
37
  return {
21
38
  disconnect() {
22
39
  if (!fired) clearTimeout(id);
40
+ runtimeReport?.pulse();
23
41
  },
24
42
  };
25
43
  }
@@ -22,6 +22,8 @@
22
22
  * next field occurrence can be attributed.
23
23
  */
24
24
 
25
+ import { reportHkRuntime } from "./registry";
26
+
25
27
  /** Bare literals that read as a producer bug, never a real target. */
26
28
  const POISONED_LITERALS = new Set(["undefined", "null", "nan"]);
27
29
 
@@ -80,12 +82,47 @@ export function sanitizeHistoryUrl(
80
82
  * harness) is detected and re-patched. */
81
83
  const PATCH_MARKER = "__hikariHistoryNet";
82
84
 
85
+ // Runtime-registry reporting (the "context of contexts"): the net is
86
+ // install-and-forget, so it reports itself on INSTALL (not at module
87
+ // init — importing the module must not claim an installed hook) and
88
+ // pulses on every (re)install and every coercion — the install
89
+ // signature is unchanged. Coercions bump a running counter in the read
90
+ // facet so field occurrences are countable from
91
+ // readHkRuntime("historySafetyNet").
92
+ let coercions = 0;
93
+ let historyNetRuntime: ReturnType<typeof reportHkRuntime> | null = null;
94
+ /** Options captured by the CURRENTLY-PATCHED closure. A re-install while
95
+ * our patch is still on the prototype skips re-patching (the `continue`
96
+ * below), so the first install's fallback/evidence stay in effect — the
97
+ * registry meta must report THOSE, not the newest call's options, or it
98
+ * would misreport the active fold target. */
99
+ let activeFallback = "/";
100
+ let activeEvidenceKey: string | false = DEFAULT_EVIDENCE_KEY;
101
+ function ensureHistoryNetRuntime() {
102
+ return (historyNetRuntime ??= reportHkRuntime("historySafetyNet", {
103
+ kind: "hook",
104
+ description: "History.prototype pushState/replaceState net: off-origin or poisoned targets fold onto the app landing before the native call.",
105
+ read: () => ({ coercions }),
106
+ }));
107
+ }
108
+
83
109
  /** Patch History.prototype. Safe to call multiple times: already-
84
110
  * patched methods are left alone, but a method overwritten back to a
85
111
  * foreign implementation is patched again (self-healing). */
86
112
  export function installHistorySafetyNet(options: HistorySafetyNetOptions = {}): void {
87
113
  const fallback = options.fallback ?? "/";
88
114
  const evidenceKey = options.evidenceKey === undefined ? DEFAULT_EVIDENCE_KEY : options.evidenceKey;
115
+ const alreadyPatched =
116
+ typeof (History.prototype as unknown as Record<string, unknown>).pushState === "function" &&
117
+ ((History.prototype as unknown as Record<string, unknown>).pushState as { [PATCH_MARKER]?: boolean })[PATCH_MARKER] === true;
118
+ if (!alreadyPatched) {
119
+ // Only an install that actually patches may claim its options as
120
+ // active; a no-op re-install keeps the first install's capture.
121
+ activeFallback = fallback;
122
+ activeEvidenceKey = evidenceKey;
123
+ }
124
+ ensureHistoryNetRuntime().setMeta({ fallback: activeFallback, evidenceKey: activeEvidenceKey === false ? "off" : activeEvidenceKey });
125
+ ensureHistoryNetRuntime().pulse();
89
126
  for (const method of ["pushState", "replaceState"] as const) {
90
127
  const current: unknown = (History.prototype as unknown as Record<string, unknown>)[method];
91
128
  if (
@@ -100,6 +137,8 @@ export function installHistorySafetyNet(options: HistorySafetyNetOptions = {}):
100
137
  const safe = sanitizeHistoryUrl(url, fallback);
101
138
  if (safe !== url) {
102
139
  const info: HistoryCoercion = { method, raw: String(url ?? ""), foldedTo: fallback };
140
+ coercions += 1;
141
+ historyNetRuntime?.pulse({ coercions });
103
142
  reportCoercion(info, evidenceKey, options);
104
143
  }
105
144
  return native.call(this, state as never, title, safe as never);
@@ -27,6 +27,8 @@
27
27
  // no src swap — for hosts that style the class themselves)
28
28
  import "./imageFallback.scss";
29
29
 
30
+ import { reportHkRuntime } from "./registry";
31
+
30
32
  export interface HkImageFallbackOptions {
31
33
  /** Which images are covered. Default: `img`. */
32
34
  selector?: string;
@@ -57,6 +59,10 @@ interface FallbackState {
57
59
  selector: string;
58
60
  exclude: string;
59
61
  fallbackSrc: string;
62
+ /** How many images this install has re-rendered (registry meta). */
63
+ marked: number;
64
+ /** Runtime-registry handle for this install (the "context of contexts"). */
65
+ runtime?: ReturnType<typeof reportHkRuntime>;
60
66
  }
61
67
 
62
68
  const INSTALLS = new WeakMap<Document, FallbackState>();
@@ -72,6 +78,8 @@ function onError(state: FallbackState, e: Event) {
72
78
  if (state.fallbackSrc && target.getAttribute("src") !== state.fallbackSrc) {
73
79
  target.src = state.fallbackSrc;
74
80
  }
81
+ state.marked += 1;
82
+ state.runtime?.pulse({ marked: state.marked });
75
83
  }
76
84
 
77
85
  function onLoad(state: FallbackState, e: Event) {
@@ -91,12 +99,16 @@ function uninstall(state: FallbackState) {
91
99
  for (const [type, fn] of state.listeners.splice(0)) {
92
100
  state.root.removeEventListener(type, fn, true);
93
101
  }
102
+ state.runtime?.dispose();
94
103
  }
95
104
 
96
105
  /**
97
106
  * Install the document-level broken-image re-render. Returns the
98
107
  * uninstaller (removes listeners; already-marked images keep their
99
- * placeholder). Idempotent per document.
108
+ * placeholder). Idempotent per document. The install reports itself to
109
+ * the runtime registry (`imageFallback`, kind "hook") and pulses with a
110
+ * running `marked` count as images are re-rendered — the signature and
111
+ * options are unchanged.
100
112
  */
101
113
  export function installHkImageFallback(options: HkImageFallbackOptions = {}): () => void {
102
114
  if (typeof document === "undefined") return () => undefined;
@@ -109,7 +121,19 @@ export function installHkImageFallback(options: HkImageFallbackOptions = {}): ()
109
121
  selector: options.selector ?? "img",
110
122
  exclude: options.exclude ?? "[data-hk-img-native]",
111
123
  fallbackSrc: options.fallbackSrc !== undefined ? options.fallbackSrc : DEFAULT_FALLBACK_SRC,
124
+ marked: 0,
112
125
  };
126
+ state.runtime = reportHkRuntime("imageFallback", {
127
+ kind: "hook",
128
+ description: "Document-level broken-image re-render: swaps failed <img> loads for the muted placeholder tile.",
129
+ meta: {
130
+ selector: state.selector,
131
+ exclude: state.exclude,
132
+ fallback: state.fallbackSrc === "" ? "class-only" : state.fallbackSrc === DEFAULT_FALLBACK_SRC ? "bundled" : "custom",
133
+ marked: 0,
134
+ },
135
+ read: () => ({ installed: true, marked: state.marked }),
136
+ });
113
137
 
114
138
  // Resource errors never bubble — capture phase is the only delegation
115
139
  // point that sees them from the document.
@@ -49,3 +49,21 @@ export { provideScrollWindow, useScrollWindow, type ScrollWindowContext } from "
49
49
  export { useReportedTransition, type ReportedTransition, type ReportedTransitionTrack } from "../composables/useReportedTransition";
50
50
  export { showProgressDialog, useProgressDialog, type ProgressDialogHandle, type ProgressDialogState } from "../composables/useProgressDialog";
51
51
  export { useSafeArea, type SafeAreaInsets } from "../composables/useSafeArea";
52
+ export {
53
+ reportHkRuntime,
54
+ getHkRuntimeEntry,
55
+ listHkRuntime,
56
+ readHkRuntime,
57
+ writeHkRuntime,
58
+ hkRuntimeSnapshot,
59
+ useHkRegistry,
60
+ type HkRuntimeKind,
61
+ type HkRuntimeStatus,
62
+ type HkRuntimeMeta,
63
+ type HkRuntimeEntryState,
64
+ type HkRuntimeReport,
65
+ type HkRuntimeHandle,
66
+ type HkRuntimeWriteOp,
67
+ type HkRuntimeSnapshot,
68
+ type HkRuntimeSnapshotEntry,
69
+ } from "./registry";
@@ -29,6 +29,7 @@
29
29
  * burning down on a throttled timer.
30
30
  */
31
31
  import { onPageLifecycle } from "./pageLifecycle";
32
+ import { reportHkRuntime, type HkRuntimeHandle } from "./registry";
32
33
 
33
34
  export interface IntervalHandle {
34
35
  disconnect(): void;
@@ -46,6 +47,19 @@ interface Slot {
46
47
  const slots = new Set<Slot>();
47
48
  let lifecycleUnsub: (() => void) | null = null;
48
49
 
50
+ // Runtime-registry reporting (the "context of contexts"). Lazy: the bus
51
+ // reports itself on first schedule and pulses on every schedule/
52
+ // disconnect, so polling liveness is answerable from
53
+ // readHkRuntime("intervalBus") without importing this module.
54
+ let runtimeReport: HkRuntimeHandle | null = null;
55
+ function ensureRuntimeReport(): HkRuntimeHandle {
56
+ return (runtimeReport ??= reportHkRuntime("intervalBus", {
57
+ kind: "bus",
58
+ description: "The visibility-aware interval bus for data polling (parks while hidden, catches up on return).",
59
+ read: () => ({ slots: slots.size, once: Array.from(slots).filter((s) => s.once).length }),
60
+ }));
61
+ }
62
+
49
63
  /** Run a slot callback with bus isolation: a throwing consumer is
50
64
  * reported but never takes the shared timer/lifecycle machinery down
51
65
  * (mirrors pageLifecycle's per-listener isolation). A repeat slot whose
@@ -149,12 +163,14 @@ export function scheduleInterval(cb: () => void, intervalMs: number): IntervalHa
149
163
  if (typeof document === "undefined" || !document.hidden) {
150
164
  arm(slot);
151
165
  }
166
+ ensureRuntimeReport().pulse();
152
167
  return {
153
168
  disconnect() {
154
169
  slot.disconnected = true;
155
170
  park(slot);
156
171
  slots.delete(slot);
157
172
  maybePark();
173
+ runtimeReport?.pulse();
158
174
  },
159
175
  };
160
176
  }
@@ -178,12 +194,14 @@ export function scheduleIntervalAfter(cb: () => void, delayMs: number): Interval
178
194
  if (typeof document === "undefined" || !document.hidden) {
179
195
  arm(slot);
180
196
  }
197
+ ensureRuntimeReport().pulse();
181
198
  return {
182
199
  disconnect() {
183
200
  slot.disconnected = true;
184
201
  park(slot);
185
202
  slots.delete(slot);
186
203
  maybePark();
204
+ runtimeReport?.pulse();
187
205
  },
188
206
  };
189
207
  }
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { installHistorySafetyNet, type HistorySafetyNetOptions } from "./historySafetyNet";
16
+ import { reportHkRuntime } from "./registry";
16
17
 
17
18
  /** Bare literals that read as a producer bug, never a real target. */
18
19
  const POISONED_LITERALS = new Set(["undefined", "null", "nan"]);
@@ -41,17 +42,34 @@ export interface NavigationSafetyNetOptions extends HistorySafetyNetOptions {
41
42
  router?: GuardRouter;
42
43
  }
43
44
 
45
+ // Runtime-registry reporting (the "context of contexts"): the guard
46
+ // reports itself on INSTALL (not at module init — importing the module
47
+ // must not claim an installed guard) and counts blocked navigations —
48
+ // the install signature is unchanged.
49
+ let blocked = 0;
50
+ let navGuardRuntime: ReturnType<typeof reportHkRuntime> | null = null;
51
+ function ensureNavGuardRuntime() {
52
+ return (navGuardRuntime ??= reportHkRuntime("navigationGuard", {
53
+ kind: "hook",
54
+ description: "Router-level poisoned-target guard: bad navigation targets die as in-app redirects to the landing route.",
55
+ read: () => ({ blocked }),
56
+ }));
57
+ }
58
+
44
59
  /** Register the beforeEach poisoned-target fold on a router. */
45
60
  export function createPoisonedLocationGuard(
46
61
  router: GuardRouter,
47
62
  fallback = "/",
48
63
  ): void {
64
+ ensureNavGuardRuntime();
49
65
  router.beforeEach((to: GuardRoute) => {
50
66
  const path = to.path ?? "";
51
67
  if (!path.startsWith("/") || POISONED_LITERALS.has(path)) {
52
68
  console.warn(
53
69
  `[Router] Blocked poisoned navigation target "${path}" (fullPath "${to.fullPath}") — redirecting to "${fallback}"`,
54
70
  );
71
+ blocked += 1;
72
+ navGuardRuntime?.pulse({ blocked });
55
73
  return fallback;
56
74
  }
57
75
  return true;
@@ -63,5 +81,7 @@ export function createPoisonedLocationGuard(
63
81
  * navigation (module scope of the app's router setup is ideal). */
64
82
  export function installNavigationSafetyNet(options: NavigationSafetyNetOptions = {}): void {
65
83
  installHistorySafetyNet(options);
84
+ ensureNavGuardRuntime().setMeta({ routerGuard: Boolean(options.router), fallback: options.fallback ?? "/" });
85
+ ensureNavGuardRuntime().pulse();
66
86
  if (options.router) createPoisonedLocationGuard(options.router, options.fallback ?? "/");
67
87
  }