refactor(gui): move the snapshot-store engine into the client runtime

The data layer no longer depends on the React glue package, and business
plugins no longer depend on web-react at all:

- The store engine (zustand vanilla + immer + persist + dev freeze),
  defineStore, and shallowEqual move to @deepseek-ai/dsh-client-runtime,
  exported from the ./client main entry — no ./store subpath survives on
  either package (the web-react one is deleted, none is opened on runtime).
- Store products are bare snapshot sources: useSelector leaves
  SnapshotStore/StoreInstance and Session; every hook is composed at the
  binding site in web-react's renderer (per-source cached uSES binding).
  The SlotRendererHost sessions face carries bare observables only.
- SessionProvider becomes a standard-kit seat: an entry whose children
  declare a session-scope slot receives the framework component as a prop,
  retiring the last value import of web-react from plugin packages.
  UseSession and the session-area types now live in ui-slots.
- web-react shrinks to the shell-only React glue (renderer, providers,
  uSES bridge); zustand/immer belong to runtime alone; the module-table
  seed and tsdown externals drop the web-react/store seat.
- NODE_ENV replacement is defined once in the shared tsdown client preset
  (browser bundles inline the engine and lost vite's define); the 3-line
  process.env typecheck shim moves to runtime with the engine.
- Stray tsc artifacts (.js/.d.ts/.d.ts.map beside sources under src/)
  swept repo-wide; they shadow real sources under vitest resolution.

Verified: both aggregate typecheck programs at zero; 604 client tests
green; repo-wide grep for web-react/store at zero; real-host playwright
run 7/7 including persist round-trip.

ci: fix test/docs
This commit is contained in:
imccyu
2026-07-23 02:56:16 +08:00
parent 004a988168
commit 8b3d1ac943
77 changed files with 484 additions and 317 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-19-gui-web-client-architecture.md: 32ec0a371f78aff8841949b983ddcca475ed6831
2026-07-19-gui-web-client-architecture.zh.md: 8a929a3e253d26b7394ad374dc0844b03fcd8b5c
2026-07-19-gui-web-client-architecture.md: f704a00c3b38f5a3a62d66cb37b121f0f21770df
2026-07-19-gui-web-client-architecture.zh.md: 3aa8fdbe82102d8c920a9f77635320c432d8b95d

View File

@@ -86,7 +86,7 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
The glue package is the whole ctx↔React boundary; components stay framework-free.
- The snapshot store engine: zustand vanilla with draft-based updates, `flush: 'sync'` by default (controlled inputs need same-tick echo) with opt-in `'raf'` batching for frame-driven stores, opt-in whole-value localStorage persistence, dev-mode deep freeze. Plugins reach it only through `defineStore` declarations per the [slot system standard](2026-07-22-slot-type-chain-implementation.md); the framework (runtime data layer, renderer machinery) is the engine's only direct consumer. Both a Session object and a snapshot store satisfy the one data contract React consumes: `ObservableSnapshot<T>` (`getSnapshot`/`subscribe`).
- The snapshot store engine **lives in the runtime package** (zustand vanilla with draft-based updates, `flush: 'sync'` by default with opt-in `'raf'` batching, opt-in whole-value localStorage persistence, dev-mode deep freeze — all exported from `runtime`'s `./client` main entry, no subpath): store products are bare observable sources with no hook members. Plugins reach the engine only through `defineStore` declarations per the [slot system standard](2026-07-22-slot-type-chain-implementation.md). web-react composes every hook at the binding site (`bindSnapshotSelector`, per-source cached) from the one data contract React consumes: `ObservableSnapshot<T>` (`getSnapshot`/`subscribe`) — a Session object and a snapshot store both satisfy it. Business plugin packages depend on runtime and ui-slots only; web-react is shell-only glue.
- `bindSnapshotSelector(source)`: binds a source into a typed selector hook over uSES-with-selector. The four uSES contract clauses hold by construction: getSnapshot returns the cached reference; subscribe is a bind-time closure (reference-stable forever); pure CSR passes no server snapshot; equality defaults to `Object.is` with `shallowEqual` opt-in per call.
- `useInvoke(fn)`: wraps an async action into a stable trigger plus pending flag; pending rides a per-hook external store read through uSES (no setState on the render path), concurrent invocations are counted, and the invoke reference never changes.
- Equality protocol, whole chain: producers use structural sharing; consumers short-circuit with `Object.is` or `shallowEqual`; `React.memo` shallow. Deep comparison is banned everywhere.

View File

@@ -86,7 +86,7 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
胶水包就是整条 ctx↔React 边界;组件保持零框架依赖。
- 快照 store 引擎zustand vanilla + 草稿式更新,缺省 `flush: 'sync'`(受控输入要求同 tick 回响),帧驱动 store 可选 `'raf'` 合批,可选整值 localStorage 持久化dev 深冻结。插件只经 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md) 的 `defineStore` 声明触及引擎的直接消费方只有框架runtime 数据层、渲染器机械。Session 对象与快照 store 同构满足 React 消费的唯一数据契约:`ObservableSnapshot<T>``getSnapshot`/`subscribe`)。
- 快照 store 引擎**住 runtime 包**zustand vanilla + 草稿式更新,缺省 `flush: 'sync'`,帧驱动 store 可选 `'raf'` 合批,可选整值 localStorage 持久化dev 深冻结——全部从 `runtime``./client` 主出口导出无子路径store 产物是裸的可观察源,不带任何 hook 成员。插件只经 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md) 的 `defineStore` 声明触及引擎。web-react 在绑定处(`bindSnapshotSelector`,按源缓存)从 React 消费的唯一数据契约合成每个 hook`ObservableSnapshot<T>``getSnapshot`/`subscribe`——Session 对象与快照 store 同构满足它。业务插件包只依赖 runtime 与 ui-slotsweb-react 是仅壳可用的胶水
- `bindSnapshotSelector(source)`:把一个源绑定为经 uSES-with-selector 的带类型 selector hook。uSES 契约四条按构造成立getSnapshot 恒返缓存引用subscribe 是绑定期闭包(引用永稳);纯 CSR 不传 server snapshot相等性缺省 `Object.is`,按调用可选 `shallowEqual`
- `useInvoke(fn)`:把异步动作包成引用恒定的触发器加 pending 标志pending 走 per-hook 外部 store 经 uSES 读出(渲染路径零 setState并发调用计数invoke 引用永不变。
- 相等性协议,全链一致:生产端结构共享;消费端以 `Object.is``shallowEqual` 短路;`React.memo` 浅比较。深比较全链禁止。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-22-slot-type-chain-implementation.md: f6c5e09d53e442c4f10f30ad03d7d50729ebba0f
2026-07-22-slot-type-chain-implementation.zh.md: 61ad6e86042bb0899604461b976ab55da037a765
2026-07-22-slot-type-chain-implementation.md: 1e9bd711e8316e2556fe238eb0a20d76e1d0d5b1
2026-07-22-slot-type-chain-implementation.zh.md: 0eab839d033faac2f2c3356900c7ca1cd69d2dc9

View File

@@ -51,7 +51,7 @@ Parity rule: **the declaring entry holds the exclusive right to render its child
### The store seat: framework engine, registrant schema
The framework owns exactly one subscription machine (the uSES snapshot store engine zustand vanilla + immer + optional localStorage persistence). What a store *contains* is the registrant's declaration, written as a factory so no module-level handle exists (a module-scoped handle would be a de-facto singleton surviving plugin reloads):
The framework owns exactly one subscription machine: the snapshot store engine (zustand vanilla + immer + optional localStorage persistence) lives in the **runtime package** (`./client` main entry — no subpath), producing bare observable sources; web-react binds them into hooks at the outlet (per-source cached uSES binding). What a store *contains* is the registrant's declaration, written as a factory so no module-level handle exists (a module-scoped handle would be a de-facto singleton surviving plugin reloads):
```ts ignore-check
export function createChatStore() {
@@ -80,7 +80,7 @@ Hooks are framework-made only: `useSession`, `useSessions`, `useStore`, `renderS
### Tree context and the renderer seam
`SessionProvider` is a framework component, self-wired (it reads the runtime's current-session state internally; the assembler passes nothing), render-prop shaped — `children(sessionId)` with an `empty` branch, remounting under `key={sessionId}`. `BindingContext` is machinery-internal; business components see zero React contexts. Inject factories execute inside the outlet on purpose (per-entry error boundaries catch them; a crashing registrant blacks out only its own entry while assembly errors rethrow); the outlet reads tree context as a machinery-only implicit parameter — the "identity from the register closure, situation from the tree position" split.
`SessionProvider` is a framework component **delivered as a standard-kit seat**: an entry whose `children` declare a session-scope slot receives it as a prop (type in ui-slots, value injected by the renderer) — components never value-import it. It is self-wired (it reads the runtime's current-session state internally; the assembler passes nothing), render-prop shaped — `children(sessionId)` with an `empty` branch, remounting under `key={sessionId}`. `BindingContext` is machinery-internal; business components see zero React contexts. Inject factories execute inside the outlet on purpose (per-entry error boundaries catch them; a crashing registrant blacks out only its own entry while assembly errors rethrow); the outlet reads tree context as a machinery-only implicit parameter — the "identity from the register closure, situation from the tree position" split.
Rendering lives behind an install seam so the runtime stays React-free: `SlotRenderer` (interface in ui-slots, implementation `createSlotRenderer()` in web-react) is installed once at shell boot via `ctx.slots.install(...)`; double install and render-before-install throw. Ownership bookkeeping is a single `Map<key, entry>` in the service — ledger, slots, contributions, render bindings, and store instances all live and die on the one entry axis, which closes the stale-authority window across plugin reloads by construction (a disposed entry's captured `renderSlot` throws a stale-authorization error on entry).

View File

@@ -51,7 +51,7 @@ ctx.slots.register({
### store 席位引擎归框架schema 归注册方
框架拥有恰好一台订阅机械uSES 快照 store 引擎——zustand vanilla + immer + 可选 localStorage 持久化。store 里*装什么*是注册方的声明,且必须写成工厂函数,使模块级句柄根本无从存在(模块级句柄会成为跨插件重载存活的事实单例):
框架拥有恰好一台订阅机械快照 store 引擎zustand vanilla + immer + 可选 localStorage 持久化)住 **runtime 包**`./client` 主出口——无子路径产出裸的可观察源web-react 在 outlet 处把它们绑定成 hook按源缓存的 uSES 绑定)。store 里*装什么*是注册方的声明,且必须写成工厂函数,使模块级句柄根本无从存在(模块级句柄会成为跨插件重载存活的事实单例):
```ts ignore-check
export function createChatStore() {
@@ -80,7 +80,7 @@ hook 只许框架造:`useSession`、`useSessions`、`useStore`、`renderSlot`
### 树上语境与渲染器安装缝
`SessionProvider` 是框架组件,框架自接线(内部自读 runtime 的当前会话状态装配方零传参render-prop 形——`children(sessionId)` 外加 `empty` 分支,以 `key={sessionId}` 重挂。`BindingContext` 属机械内部;业务组件可见的 React Context 为零。inject 工厂有意在 outlet 内部执行per-entry 错误边界接得住它们崩溃的注册方只黑掉自己那一格装配错误则重抛outlet 把树上语境当作仅机械可用的暗参读取——即「身份出自 register 闭包、现场出自树位置」的分工。
`SessionProvider` 是框架组件,**以标配席形式送达**`children` 里声明了 session scope 坑的 entry 经 prop 收到它(类型住 ui-slots值由渲染器注入——组件永不对它做值 import。它框架自接线(内部自读 runtime 的当前会话状态装配方零传参render-prop 形——`children(sessionId)` 外加 `empty` 分支,以 `key={sessionId}` 重挂。`BindingContext` 属机械内部;业务组件可见的 React Context 为零。inject 工厂有意在 outlet 内部执行per-entry 错误边界接得住它们崩溃的注册方只黑掉自己那一格装配错误则重抛outlet 把树上语境当作仅机械可用的暗参读取——即「身份出自 register 闭包、现场出自树位置」的分工。
渲染住在一条安装缝之后runtime 因此保持 React-free`SlotRenderer`(接口住 ui-slots实现 `createSlotRenderer()` 住 web-react在壳 boot 时经 `ctx.slots.install(...)` 安装一次;双重安装与安装前渲染均 throw。归属记账是服务里的单一 `Map<key, entry>`——账本、坑、贡献、渲染绑定、store 实例全部沿同一条 entry 轴生灭,跨插件重载的陈旧权威窗口由此在构造上关闭(已 dispose 的 entry 所捕获的 `renderSlot`一进入口即抛陈旧授权stale-authorization错误