@violetflux/kerros 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  Kerros is a lightweight way to share state between React components.
26
26
 
27
- Write a Store the same way you write a custom Hook. When local state needs to be shared, pass it to `createStore`, mount its Provider, and let each component select what it needs.
27
+ Write a Store the same way you write a custom Hook. When local state needs to be shared, pass it to `createStore`, mount its Provider, and let automatic tracking focus each component on the properties it reads.
28
28
 
29
29
  > [!TIP]
30
30
  > **Install with your coding agent** — paste this sentence into your coding agent to install both the dependency and the project Skill:
@@ -154,7 +154,7 @@ const { count, setCount } = useCounter()
154
154
 
155
155
  Primitive snapshots use `Object.is`. `Map`, `Set`, class instances, and other non-plain objects are treated as atomic references. Store and external Store snapshots must be immutable: publish a new reference for every observable change.
156
156
 
157
- Do not save, return, spread, serialize, or pass the complete selector-free result around. Read properties immediately, normally by destructuring. Effects and `useEffectEvent` may perform imperative reads from `useInstance()`, but rendered state must use the subscribed Store Hook; never expose an Effect Event as a public Store action.
157
+ The selector-free result is the component's read-only tracked snapshot. You may destructure it, keep it in a render-local variable, or pass it to a synchronously rendered child. Do not mutate it or retain it in state, refs, module variables, or long-lived caches as a live state object; spread, rest destructuring, enumeration, and serialization create broad subscriptions. Reactive Effects should read values during render and declare correct dependencies. Use `useInstance()` only for imperative latest-state reads that do not drive rendering, and never expose an Effect Event as a public Store action.
158
158
 
159
159
  ## Multiple instances
160
160
 
@@ -254,7 +254,7 @@ function createStore<TStore, TProps = Record<never, never>>(
254
254
 
255
255
  Most applications only need `createStore`. Use `bindStore` when a library or SDK already owns authoritative state outside React and exposes stable `getSnapshot` and `subscribe` functions.
256
256
 
257
- Without this API, an integration must either repeat the Context and selector subscription code or copy the external snapshot through a second Store. `bindStore` provides Provider scoping and selectors while keeping the original Store as the only state owner.
257
+ Without this API, an integration must either repeat the Context and focused-subscription code or copy the external snapshot through a second Store. `bindStore` provides Provider scoping, selector-free automatic tracking, and explicit selectors while keeping the original Store as the only state owner.
258
258
 
259
259
  ```tsx
260
260
  const [useStream, StreamBindingProvider] = bindStore<Stream>('Stream')
@@ -286,7 +286,7 @@ import kerros from '@violetflux/eslint-plugin-kerros'
286
286
  export default [kerros.configs.recommendedTypeChecked]
287
287
  ```
288
288
 
289
- `recommendedTypeChecked` enables all 17 rules as errors and uses TypeScript `projectService`. Very large repositories may use `kerros.configs.fastTypeChecked`, which keeps type-aware Store recognition but disables the most expensive whole-program and deep analyses. See the [measured ESLint benchmark](https://github.com/violetflux/kerros/blob/main/benchmarks/eslint/RESULTS.md); the fast profile is a tradeoff, not an untyped fallback. The plugin analyzes complete TS/TSX files, not incomplete Markdown snippets.
289
+ `recommendedTypeChecked` enables all 16 rules as errors and uses TypeScript `projectService`. Very large repositories may use `kerros.configs.fastTypeChecked`, which keeps type-aware Store recognition but disables the most expensive whole-program and deep analyses. See the [measured ESLint benchmark](https://github.com/violetflux/kerros/blob/main/benchmarks/eslint/RESULTS.md); the fast profile is a tradeoff, not an untyped fallback. The plugin analyzes complete TS/TSX files, not incomplete Markdown snippets.
290
290
 
291
291
  For maintainers, npm Trusted Publisher entries must be configured for both `@violetflux/kerros` and `@violetflux/eslint-plugin-kerros`. That npm-side configuration is the only release step outside this repository; CI checks and publishes the runtime first, then the plugin.
292
292
 
package/README.zh-CN.md CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  Kerros 是一个在 React 组件间共享状态的轻量方案。
18
18
 
19
- 你怎么写 custom Hook,就可以怎么写 Store。只有当局部状态需要被多个组件使用时,再交给 `createStore`,用 Provider 决定共享范围,用 selector 选择组件真正需要的数据。
19
+ 你怎么写 custom Hook,就可以怎么写 Store。只有当局部状态需要被多个组件使用时,再交给 `createStore`,用 Provider 决定共享范围;组件直接解构需要的数据,Kerros 默认自动追踪实际访问的属性。
20
20
 
21
21
  > [!TIP]
22
22
  > **使用 Coding Agent 安装**:复制下面这句话并粘贴给你的 Coding Agent,它会同时安装依赖和当前项目的 Skill:
@@ -146,7 +146,7 @@ const { count, setCount } = useCounter()
146
146
 
147
147
  基础类型快照使用 `Object.is`。`Map`、`Set`、类实例及其他非普通对象按整体引用处理。Store 和 External Store 快照必须保持不可变:每次可观察变化都发布新引用。
148
148
 
149
- 不要保存、返回、展开、序列化或传递无 selector 的完整结果;应立即读取属性,通常直接解构。Effect `useEffectEvent` 可以通过 `useInstance()` 做命令式读取,但参与渲染的状态必须使用订阅 Hook;也不要把 Effect Event 暴露成公共 Store action。
149
+ selector 的结果是当前组件的只读追踪快照,可以直接解构、保存在渲染局部变量中,或传给同步渲染的子组件继续读取。不要修改快照,也不要把它保存到 state、ref、模块变量或长期缓存后当作实时状态源;展开、rest 解构、枚举和序列化会形成宽泛订阅。响应式 Effect 应在渲染期间读取值并声明正确依赖;只有不参与渲染、需要执行时读取最新状态的命令式逻辑才使用 `useInstance()`。也不要把 Effect Event 暴露成公共 Store action。
150
150
 
151
151
  ## 多个实例
152
152
 
@@ -246,7 +246,7 @@ function createStore<TStore, TProps = Record<never, never>>(
246
246
 
247
247
  绝大多数应用只需要 `createStore`。只有当某个库或 SDK 已经在 React 外持有权威状态,并提供稳定的 `getSnapshot` 和 `subscribe` 函数时,才使用 `bindStore`。
248
248
 
249
- 没有这个 API 时,集成层只能重复实现 Context selector 订阅,或者把 External Store 快照复制进第二个 Store。`bindStore` 只提供 Provider 作用域和 selector,原 Store 仍是唯一状态所有者。
249
+ 没有这个 API 时,集成层只能重复实现 Context 和细粒度订阅,或者把 External Store 快照复制进第二个 Store。`bindStore` 提供 Provider 作用域、无 selector 自动追踪和显式 selector,原 Store 仍是唯一状态所有者。
250
250
 
251
251
  ```tsx
252
252
  const [useStream, StreamBindingProvider] = bindStore<Stream>('Stream')
@@ -278,7 +278,7 @@ import kerros from '@violetflux/eslint-plugin-kerros'
278
278
  export default [kerros.configs.recommendedTypeChecked]
279
279
  ```
280
280
 
281
- `recommendedTypeChecked` 把全部 17 条规则设为 error,并启用 TypeScript `projectService`。超大型仓库可改用 `kerros.configs.fastTypeChecked`:它仍然通过类型识别真实 Kerros Hook,只关闭最昂贵的全程序与深层分析。请参考[真实 ESLint 压测](https://github.com/violetflux/kerros/blob/main/benchmarks/eslint/RESULTS.md);fast 是性能取舍,不是不可靠的命名降级。插件首版只分析完整 TS/TSX 文件,不分析不完整 Markdown 代码块。
281
+ `recommendedTypeChecked` 把全部 16 条规则设为 error,并启用 TypeScript `projectService`。超大型仓库可改用 `kerros.configs.fastTypeChecked`:它仍然通过类型识别真实 Kerros Hook,只关闭最昂贵的全程序与深层分析。请参考[真实 ESLint 压测](https://github.com/violetflux/kerros/blob/main/benchmarks/eslint/RESULTS.md);fast 是性能取舍,不是不可靠的命名降级。插件首版只分析完整 TS/TSX 文件,不分析不完整 Markdown 代码块。
282
282
 
283
283
  维护者还需要分别为 `@violetflux/kerros` 和 `@violetflux/eslint-plugin-kerros` 配置 npm Trusted Publisher。这是唯一的仓库外发布步骤;仓库内工作流会先检查并发布运行库,再发布插件。
284
284
 
package/dist/index.cjs CHANGED
@@ -4,6 +4,7 @@ let proxy_compare = require("proxy-compare");
4
4
  let use_sync_external_store_shim_with_selector = require("use-sync-external-store/shim/with-selector");
5
5
  //#region src/tracking.ts
6
6
  const useStoreLayoutEffect$1 = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
7
+ const proxyTargetCache = /* @__PURE__ */ new WeakMap();
7
8
  /**
8
9
  * Subscribe through either an explicit selector, shallow snapshots, or render access tracking
9
10
  */
@@ -27,7 +28,7 @@ function useStoreValue(store, selector, tracking) {
27
28
  const snapshot = (0, use_sync_external_store_shim_with_selector.useSyncExternalStoreWithSelector)(store.subscribe, store.getSnapshot, store.getSnapshot, selectSnapshot, compareSelections);
28
29
  const affected = /* @__PURE__ */ new WeakMap();
29
30
  const shouldTrack = !selector && tracking;
30
- const value = shouldTrack ? (0, proxy_compare.createProxy)(snapshot, affected, proxyCache) : snapshot;
31
+ const value = shouldTrack ? (0, proxy_compare.createProxy)(snapshot, affected, proxyCache, proxyTargetCache) : snapshot;
31
32
  useStoreLayoutEffect$1(() => {
32
33
  if (shouldTrack) {
33
34
  const renderedSnapshot = snapshot;
@@ -36,7 +37,7 @@ function useStoreValue(store, selector, tracking) {
36
37
  snapshot: renderedSnapshot
37
38
  };
38
39
  const currentSnapshot = store.getSnapshot();
39
- if ((0, proxy_compare.isChanged)(renderedSnapshot, currentSnapshot, affected, /* @__PURE__ */ new WeakMap())) calibrate(currentSnapshot);
40
+ if (!Object.is(renderedSnapshot, currentSnapshot) && (0, proxy_compare.isChanged)(renderedSnapshot, currentSnapshot, affected, /* @__PURE__ */ new WeakMap())) calibrate(currentSnapshot);
40
41
  }
41
42
  }, [
42
43
  affected,
package/dist/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { createProxy, isChanged } from "proxy-compare";
3
3
  import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
4
4
  //#region src/tracking.ts
5
5
  const useStoreLayoutEffect$1 = typeof window === "undefined" ? useEffect : useLayoutEffect;
6
+ const proxyTargetCache = /* @__PURE__ */ new WeakMap();
6
7
  /**
7
8
  * Subscribe through either an explicit selector, shallow snapshots, or render access tracking
8
9
  */
@@ -26,7 +27,7 @@ function useStoreValue(store, selector, tracking) {
26
27
  const snapshot = useSyncExternalStoreWithSelector(store.subscribe, store.getSnapshot, store.getSnapshot, selectSnapshot, compareSelections);
27
28
  const affected = /* @__PURE__ */ new WeakMap();
28
29
  const shouldTrack = !selector && tracking;
29
- const value = shouldTrack ? createProxy(snapshot, affected, proxyCache) : snapshot;
30
+ const value = shouldTrack ? createProxy(snapshot, affected, proxyCache, proxyTargetCache) : snapshot;
30
31
  useStoreLayoutEffect$1(() => {
31
32
  if (shouldTrack) {
32
33
  const renderedSnapshot = snapshot;
@@ -35,7 +36,7 @@ function useStoreValue(store, selector, tracking) {
35
36
  snapshot: renderedSnapshot
36
37
  };
37
38
  const currentSnapshot = store.getSnapshot();
38
- if (isChanged(renderedSnapshot, currentSnapshot, affected, /* @__PURE__ */ new WeakMap())) calibrate(currentSnapshot);
39
+ if (!Object.is(renderedSnapshot, currentSnapshot) && isChanged(renderedSnapshot, currentSnapshot, affected, /* @__PURE__ */ new WeakMap())) calibrate(currentSnapshot);
39
40
  }
40
41
  }, [
41
42
  affected,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@violetflux/kerros",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Hook-native state sharing for React with automatic access tracking and focused selectors.",
5
5
  "keywords": [
6
6
  "react",
@@ -74,7 +74,7 @@ Kerros automatically tracks object, array, and nested property reads made during
74
74
 
75
75
  ## Subscription modes
76
76
 
77
- - `useStore()` is the default. Read properties immediately, normally through destructuring. Do not save, return, spread, serialize, or pass the complete tracked result.
77
+ - `useStore()` is the default and returns a read-only tracked render snapshot. It may be destructured, kept in a render-local variable, returned from a custom Hook, or passed to a synchronously rendered child. Do not mutate it or retain it as a live state object; spread, rest destructuring, enumeration, and serialization create broad subscriptions.
78
78
  - `useStore(s => ({ ... }))` is the advanced path for derived values or measured hot spots. Keep the selector inline, name its parameter `s`, and return an object whose top-level fields are shallowly compared with `Object.is`.
79
79
  - `createStore(model, { tracking: false })` and `bindStore({ tracking: false })` disable automatic tracking for selector-free calls and compare the complete Store at the top level instead.
80
80
  - Primitive Store snapshots use `Object.is`. `Map`, `Set`, class instances, and other atomic objects are tracked by reference as a whole.
@@ -101,7 +101,7 @@ Mount the original instance without mirroring its snapshot:
101
101
  </StreamBindingProvider>
102
102
  ```
103
103
 
104
- Use `useStream()` with immediate property access for ordinary snapshot reads; use an explicit selector only for derived values or measured hot spots. Use `useStreamInstance()` only in Provider descendants that need imperative commands or must supply the current instance to another headless service. It reads Context without subscribing to snapshots, so never use `useStreamInstance().getSnapshot()` for rendered state.
104
+ Treat `useStream()` as a read-only tracked render snapshot: destructure it, keep it in a render-local variable, return it from a custom Hook, or pass it to a synchronously rendered child. Use an explicit selector only for derived values or measured hot spots. Do not mutate the snapshot or retain it as a live state object. Use `useStreamInstance()` only in Provider descendants that need imperative commands, imperative latest-state reads that do not drive rendering, or must supply the current instance to another headless service. It reads Context without subscribing to snapshots, so never use `useStreamInstance().getSnapshot()` for rendered state.
105
105
 
106
106
  Keep creation, start, stop, and disposal in the owner that creates the instance. If that owner already has the instance, use it directly instead of calling the instance Hook.
107
107