loader-status.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. /**
  2. * Fiber-state projection vocabulary and the kernel-owned status store for the
  3. * boot loading page. The status AppRoot renders is a projection of the real
  4. * cordis fiber states (display the truth, not a retelling) — the boot chain
  5. * subscribes `internal/status` and recomputes one row per loader entry.
  6. *
  7. * The store is hand-rolled here because of the shell self-sufficiency rule
  8. * (web2 §0): the snapshot-store machinery lives in the runtime PLUGIN
  9. * package, and the shell kernel must not value-import any plugin package —
  10. * the loading page has to work while (and especially when) plugins fail.
  11. * @module @deepseek-ai/dsh-client-web/src/loader-status
  12. */
  13. import type { FiberState } from 'cordis'
  14. /**
  15. * Value mirror of cordis's `FiberState` const enum: a const enum has no
  16. * runtime object to import (and esbuild-based pipelines cannot inline it
  17. * across modules), so these values mirror the pinned vendored definition
  18. * while retaining its type (same rationale as dsh-tool-cordis's mirror).
  19. */
  20. export const FIBER_STATE = {
  21. PENDING: 0 as FiberState.PENDING,
  22. LOADING: 1 as FiberState.LOADING,
  23. ACTIVE: 2 as FiberState.ACTIVE,
  24. FAILED: 3 as FiberState.FAILED,
  25. DISPOSED: 4 as FiberState.DISPOSED,
  26. UNLOADING: 5 as FiberState.UNLOADING,
  27. } as const
  28. /** One entry's projected state label (lower-case face of {@link FiberState}). */
  29. export type LoaderEntryState = 'pending' | 'loading' | 'active' | 'failed' | 'disposed' | 'unloading'
  30. /** Label for each fiber state, keyed by member (inlining-safe — no reverse mapping). */
  31. export const STATE_LABELS: Record<FiberState, LoaderEntryState> = {
  32. [FIBER_STATE.PENDING]: 'pending',
  33. [FIBER_STATE.LOADING]: 'loading',
  34. [FIBER_STATE.ACTIVE]: 'active',
  35. [FIBER_STATE.FAILED]: 'failed',
  36. [FIBER_STATE.DISPOSED]: 'disposed',
  37. [FIBER_STATE.UNLOADING]: 'unloading',
  38. }
  39. /** Per-entry state projection (AppRoot's status feed), keyed by entry name. */
  40. export type LoaderStatus = Record<string, LoaderEntryState>
  41. /** Minimal observable snapshot the kernel components consume (useSyncExternalStore shape). */
  42. export interface KernelSignal<T> {
  43. /** Current value (stable reference between changes). */
  44. getSnapshot(): T
  45. /**
  46. * Subscribe to changes.
  47. * @param fn - change listener.
  48. * @returns the unsubscribe disposer.
  49. */
  50. subscribe(fn: () => void): () => void
  51. }
  52. /** Writable one-value signal (settled flag, boot failure report). */
  53. export interface KernelValueSignal<T> extends KernelSignal<T> {
  54. /**
  55. * Publish a new value and notify subscribers.
  56. * @param next - the new value.
  57. */
  58. set(next: T): void
  59. }
  60. /**
  61. * Create a writable kernel signal.
  62. * @param init - initial value.
  63. * @returns the signal.
  64. */
  65. export function createSignal<T>(init: T): KernelValueSignal<T> {
  66. let value = init
  67. const listeners = new Set<() => void>()
  68. return {
  69. getSnapshot: () => value,
  70. subscribe: (fn) => { listeners.add(fn); return () => { listeners.delete(fn) } },
  71. set: (next) => {
  72. value = next
  73. for (const fn of [...listeners]) fn()
  74. },
  75. }
  76. }
  77. /** The boot status store: per-entry rows over a {@link KernelSignal} face. */
  78. export interface LoaderStatusStore extends KernelSignal<LoaderStatus> {
  79. /**
  80. * Project one entry's state (copy-on-write so getSnapshot references only
  81. * change on writes — useSyncExternalStore contract).
  82. * @param id - entry name.
  83. * @param state - projected fiber state.
  84. */
  85. set(id: string, state: LoaderEntryState): void
  86. }
  87. /**
  88. * Create the boot status store.
  89. * @returns the store (empty until the boot chain projects rows).
  90. */
  91. export function createLoaderStatusStore(): LoaderStatusStore {
  92. let value: LoaderStatus = {}
  93. const listeners = new Set<() => void>()
  94. return {
  95. getSnapshot: () => value,
  96. subscribe: (fn) => { listeners.add(fn); return () => { listeners.delete(fn) } },
  97. set: (id, state) => {
  98. value = { ...value, [id]: state }
  99. for (const fn of [...listeners]) fn()
  100. },
  101. }
  102. }