@violetflux/kerros 0.2.2 → 0.2.4

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.de.md CHANGED
@@ -88,6 +88,8 @@ const [useStream, StreamProvider] = bindStore<Stream>('Stream')
88
88
 
89
89
  Der zurückgegebene Store Hook verwendet ohne Argument automatisches Tracking. Explizite Objekt-Selektoren sind für abgeleitete Werte und gemessene Hotspots verfügbar. Außerhalb des passenden Providers wird ein verständlicher Fehler ausgelöst.
90
90
 
91
+ React-Elemente und Portale sind automatisch atomar. `useRef()` und `createRef()` können direkt zurückgegeben werden; `ref(value)` ist nur für Proxy-inkompatible Werte oder strikte Identität gedacht.
92
+
91
93
  `bindStore` ist eine fortgeschrittene Integration ausschließlich für einen bestehenden Headless External Store. Für normalen Hook-Zustand bleibt `createStore` die richtige Wahl. Der Context enthält nur die ursprüngliche Store-Instanz; Verbraucher verwenden `getSnapshot` und `subscribe` direkt.
92
94
 
93
95
  ## Dokumentation
package/README.es.md CHANGED
@@ -88,6 +88,8 @@ const [useStream, StreamProvider] = bindStore<Stream>('Stream')
88
88
 
89
89
  Sin argumentos, el Hook devuelto usa seguimiento automático. Los selectores de objeto explícitos quedan para valores derivados y puntos críticos medidos. Usarlo fuera de su Provider produce un error claro.
90
90
 
91
+ Los elementos y portales de React son atómicos automáticamente. `useRef()` y `createRef()` se pueden devolver directamente; `ref(value)` se reserva para valores incompatibles con Proxy o identidad estricta.
92
+
91
93
  `bindStore` es una integración avanzada solo para un Headless External Store existente. Para el estado normal de Hooks, usa `createStore`. El Context solo contiene la instancia original; los consumidores utilizan directamente `getSnapshot` y `subscribe`.
92
94
 
93
95
  ## Documentación
package/README.fr.md CHANGED
@@ -88,6 +88,8 @@ const [useStream, StreamProvider] = bindStore<Stream>('Stream')
88
88
 
89
89
  Sans argument, le Hook retourné active le suivi automatique. Les sélecteurs d'objet explicites servent aux valeurs dérivées et aux points chauds mesurés. Son utilisation hors du Provider lève une erreur claire.
90
90
 
91
+ Les éléments et portails React sont automatiquement atomiques. `useRef()` et `createRef()` peuvent être retournés directement ; `ref(value)` est réservé aux valeurs incompatibles avec Proxy ou à l'identité stricte.
92
+
91
93
  `bindStore` est une intégration avancée réservée à un Headless External Store existant. Pour un état Hook ordinaire, utilisez `createStore`. Le Context ne contient que l'instance d'origine ; les consommateurs utilisent directement `getSnapshot` et `subscribe`.
92
94
 
93
95
  ## Documentation
package/README.ja.md CHANGED
@@ -89,6 +89,8 @@ const [useStream, StreamProvider] = bindStore<Stream>('Stream')
89
89
 
90
90
  返される Store Hook は引数なしで自動追跡を使います。明示的なオブジェクト selector は派生値や計測済みホットスポット向けです。対応する Provider の外では明確なエラーを送出します。
91
91
 
92
+ React Element と Portal は自動的に原子的な値になります。`useRef()` と `createRef()` はそのまま返せます。`ref(value)` は Proxy 非互換の値や厳密な同一性が必要な場合だけ使います。
93
+
92
94
  高度な連携として、既存の Headless External Store にだけ `bindStore` を使います。通常の Hook 状態には `createStore` を使ってください。Context は元の Store インスタンスだけを保持し、コンシューマーは `getSnapshot` と `subscribe` を直接利用します。
93
95
 
94
96
  ## ドキュメント
package/README.ko.md CHANGED
@@ -89,6 +89,8 @@ const [useStream, StreamProvider] = bindStore<Stream>('Stream')
89
89
 
90
90
  반환된 Store Hook은 인자 없이 자동 추적을 사용합니다. 명시적 객체 selector는 파생 값과 측정된 핫스팟을 위한 고급 경로입니다. Provider 밖에서 호출하면 명확한 오류가 발생합니다.
91
91
 
92
+ React Element와 Portal은 자동으로 원자 값이 됩니다. `useRef()`와 `createRef()`는 그대로 반환할 수 있으며 `ref(value)`는 Proxy 비호환 값이나 엄격한 동일성이 필요할 때만 사용합니다.
93
+
92
94
  고급 통합이 필요한 기존 Headless External Store에만 `bindStore`를 사용하세요. 일반 Hook 상태에는 `createStore`를 사용합니다. Context는 원래 Store 인스턴스만 보관하고 소비자는 `getSnapshot`과 `subscribe`를 직접 사용합니다.
93
95
 
94
96
  ## 문서
package/README.md CHANGED
@@ -154,7 +154,9 @@ 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
+ React elements and portals are atomic automatically. Standard `useRef()` and `createRef()` containers can be returned directly in React 17, 18, and 19. Use `ref(value)` only for Proxy-intolerant third-party objects or strict identity; internal mutation of an atomic value is not reactive.
158
+
159
+ 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
160
 
159
161
  ## Multiple instances
160
162
 
@@ -250,6 +252,14 @@ function createStore<TStore, TProps = Record<never, never>>(
250
252
  - using the Store Hook outside its matching Provider throws a clear error
251
253
  - Provider instances work with Strict Mode and server rendering
252
254
 
255
+ ### `ref` (identity escape hatch)
256
+
257
+ ```ts
258
+ function ref<T extends object>(value: T): T
259
+ ```
260
+
261
+ Marks an object as atomic and returns the exact same identity. Standard React refs do not need this helper.
262
+
253
263
  ### Advanced: bind an existing external Store
254
264
 
255
265
  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.
@@ -286,7 +296,7 @@ import kerros from '@violetflux/eslint-plugin-kerros'
286
296
  export default [kerros.configs.recommendedTypeChecked]
287
297
  ```
288
298
 
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.
299
+ `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
300
 
291
301
  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
302
 
package/README.zh-CN.md CHANGED
@@ -146,7 +146,9 @@ 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
+ React Element Portal 会自动作为原子值处理。React 17、18、19 的标准 `useRef()`、`createRef()` 容器可以直接返回。只有第三方对象不能接受 Proxy,或者必须保留严格身份时才使用 `ref(value)`;原子值的内部原地修改不是响应式更新。
150
+
151
+ 无 selector 的结果是当前组件的只读追踪快照,可以直接解构、保存在渲染局部变量中,或传给同步渲染的子组件继续读取。不要修改快照,也不要把它保存到 state、ref、模块变量或长期缓存后当作实时状态源;展开、rest 解构、枚举和序列化会形成宽泛订阅。响应式 Effect 应在渲染期间读取值并声明正确依赖;只有不参与渲染、需要执行时读取最新状态的命令式逻辑才使用 `useInstance()`。也不要把 Effect Event 暴露成公共 Store action。
150
152
 
151
153
  ## 多个实例
152
154
 
@@ -242,6 +244,14 @@ function createStore<TStore, TProps = Record<never, never>>(
242
244
  - 在对应 Provider 外调用会抛出明确错误
243
245
  - 支持 Strict Mode、服务端渲染和 Provider 多实例
244
246
 
247
+ ### `ref`(身份逃生口)
248
+
249
+ ```ts
250
+ function ref<T extends object>(value: T): T
251
+ ```
252
+
253
+ 把对象标记为原子值并返回完全相同的身份。标准 React ref 不需要这个辅助函数。
254
+
245
255
  ### 高级用法:绑定已有 External Store
246
256
 
247
257
  绝大多数应用只需要 `createStore`。只有当某个库或 SDK 已经在 React 外持有权威状态,并提供稳定的 `getSnapshot` 和 `subscribe` 函数时,才使用 `bindStore`。
@@ -278,7 +288,7 @@ import kerros from '@violetflux/eslint-plugin-kerros'
278
288
  export default [kerros.configs.recommendedTypeChecked]
279
289
  ```
280
290
 
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 代码块。
291
+ `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
292
 
283
293
  维护者还需要分别为 `@violetflux/kerros` 和 `@violetflux/eslint-plugin-kerros` 配置 npm Trusted Publisher。这是唯一的仓库外发布步骤;仓库内工作流会先检查并发布运行库,再发布插件。
284
294
 
@@ -0,0 +1,13 @@
1
+ # Third-party notices
2
+
3
+ Kerros includes an adapted subset of `proxy-compare@3.0.1` in its access-tracking implementation.
4
+
5
+ ## proxy-compare
6
+
7
+ Copyright (c) 2020 Daishi Kato
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/dist/index.cjs CHANGED
@@ -1,7 +1,164 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let react = require("react");
3
- let proxy_compare = require("proxy-compare");
4
3
  let use_sync_external_store_shim_with_selector = require("use-sync-external-store/shim/with-selector");
4
+ //#region src/access-tracking.ts
5
+ /*!
6
+ * Adapted from proxy-compare 3.0.1.
7
+ * Copyright (c) 2020 Daishi Kato. Licensed under the MIT License.
8
+ */
9
+ const trackMemoSymbol = Symbol();
10
+ const getOriginalSymbol = Symbol();
11
+ const reactElementType = Symbol.for("react.element");
12
+ const reactTransitionalElementType = Symbol.for("react.transitional.element");
13
+ const reactPortalType = Symbol.for("react.portal");
14
+ const objectPrototype = Object.prototype;
15
+ const arrayPrototype = Array.prototype;
16
+ const trackingOverrides = /* @__PURE__ */ new WeakMap();
17
+ /** Create an access-tracking Proxy and lazily preserve React atomic values */
18
+ function createProxy(value, affected, proxyCache, targetCache) {
19
+ if (!isObjectToTrack(value)) return value;
20
+ const typedTargetCache = targetCache;
21
+ let targetAndCopied = typedTargetCache?.get(value);
22
+ if (!targetAndCopied) {
23
+ const target = getOriginalObject(value);
24
+ targetAndCopied = needsToCopyTargetObject(target) ? [target, copyTargetObject(target)] : [target];
25
+ typedTargetCache?.set(value, targetAndCopied);
26
+ }
27
+ const [target, copiedTarget] = targetAndCopied;
28
+ const typedProxyCache = proxyCache;
29
+ let handlerAndState = typedProxyCache?.get(target);
30
+ if (!handlerAndState || handlerAndState[1].copied !== Boolean(copiedTarget)) {
31
+ handlerAndState = createProxyHandler(target, Boolean(copiedTarget));
32
+ handlerAndState[1].proxy = new Proxy(copiedTarget ?? target, handlerAndState[0]);
33
+ typedProxyCache?.set(target, handlerAndState);
34
+ }
35
+ handlerAndState[1].affected = affected;
36
+ handlerAndState[1].proxyCache = proxyCache;
37
+ handlerAndState[1].targetCache = targetCache;
38
+ return handlerAndState[1].proxy;
39
+ }
40
+ /** Compare only paths read through a previous tracking Proxy */
41
+ function isChanged(previous, next, affected, cache, isEqual = Object.is) {
42
+ if (isEqual(previous, next)) return false;
43
+ if (!isObject(previous) || !isObject(next)) return true;
44
+ const used = affected.get(getOriginalObject(previous));
45
+ if (!used) return true;
46
+ if (cache) {
47
+ if (cache.get(previous) === next) return false;
48
+ cache.set(previous, next);
49
+ }
50
+ let changed = null;
51
+ for (const key of used.has ?? []) {
52
+ changed = Reflect.has(previous, key) !== Reflect.has(next, key);
53
+ if (changed) return true;
54
+ }
55
+ if (used.all) {
56
+ changed = areOwnKeysChanged(previous, next);
57
+ if (changed) return true;
58
+ } else for (const key of used.own ?? []) {
59
+ changed = Boolean(Reflect.getOwnPropertyDescriptor(previous, key)) !== Boolean(Reflect.getOwnPropertyDescriptor(next, key));
60
+ if (changed) return true;
61
+ }
62
+ for (const key of used.keys ?? []) {
63
+ changed = isChanged(Reflect.get(previous, key), Reflect.get(next, key), affected, cache, isEqual);
64
+ if (changed) return true;
65
+ }
66
+ if (changed === null) throw new Error("Invalid Kerros access tracking state");
67
+ return changed;
68
+ }
69
+ /** Mark an exact object identity as tracked or atomic */
70
+ function markToTrack(value, track = true) {
71
+ trackingOverrides.set(value, track);
72
+ }
73
+ /** Build the handler state shared by a single cached Proxy */
74
+ function createProxyHandler(original, copied) {
75
+ const state = { copied };
76
+ let trackWholeObject = false;
77
+ /** Record one access operation against the original snapshot object */
78
+ const record = (operation, key) => {
79
+ if (trackWholeObject) return;
80
+ let used = state.affected?.get(original);
81
+ if (!used) {
82
+ used = {};
83
+ state.affected?.set(original, used);
84
+ }
85
+ if (operation === "all") {
86
+ used.all = true;
87
+ return;
88
+ }
89
+ let keys = used[operation];
90
+ if (!keys) {
91
+ keys = /* @__PURE__ */ new Set();
92
+ used[operation] = keys;
93
+ }
94
+ keys.add(key);
95
+ };
96
+ const handler = {
97
+ get: (target, key) => {
98
+ if (key === getOriginalSymbol) return original;
99
+ record("keys", key);
100
+ return createProxy(Reflect.get(target, key), state.affected, state.proxyCache, state.targetCache);
101
+ },
102
+ getOwnPropertyDescriptor: (target, key) => {
103
+ record("own", key);
104
+ return Reflect.getOwnPropertyDescriptor(target, key);
105
+ },
106
+ has: (target, key) => {
107
+ if (key === trackMemoSymbol) {
108
+ trackWholeObject = true;
109
+ state.affected?.delete(original);
110
+ return true;
111
+ }
112
+ record("has", key);
113
+ return Reflect.has(target, key);
114
+ },
115
+ ownKeys: (target) => {
116
+ record("all");
117
+ return Reflect.ownKeys(target);
118
+ }
119
+ };
120
+ if (copied) {
121
+ handler.deleteProperty = () => false;
122
+ handler.set = () => false;
123
+ }
124
+ return [handler, state];
125
+ }
126
+ /** Decide lazily whether one reached value supports recursive tracking */
127
+ function isObjectToTrack(value) {
128
+ if (!isObject(value)) return false;
129
+ if (trackingOverrides.has(value)) return trackingOverrides.get(value);
130
+ const prototype = Object.getPrototypeOf(value);
131
+ if (prototype === arrayPrototype) return true;
132
+ if (prototype !== objectPrototype) return false;
133
+ const marker = value.$$typeof;
134
+ return marker !== reactElementType && marker !== reactTransitionalElementType && marker !== reactPortalType;
135
+ }
136
+ /** Narrow mutable object operations used by the compare algorithm */
137
+ function isObject(value) {
138
+ return typeof value === "object" && value !== null;
139
+ }
140
+ /** Unwrap a cached tracking Proxy when comparison receives one */
141
+ function getOriginalObject(value) {
142
+ return value[getOriginalSymbol] ?? value;
143
+ }
144
+ /** Detect invariant-sensitive frozen properties before Proxy creation */
145
+ function needsToCopyTargetObject(value) {
146
+ return Object.values(Object.getOwnPropertyDescriptors(value)).some((descriptor) => !descriptor.configurable && !descriptor.writable);
147
+ }
148
+ /** Copy an invariant-sensitive object with configurable descriptors */
149
+ function copyTargetObject(value) {
150
+ if (Array.isArray(value)) return Array.from(value);
151
+ const descriptors = Object.getOwnPropertyDescriptors(value);
152
+ for (const descriptor of Object.values(descriptors)) descriptor.configurable = true;
153
+ return Object.create(Object.getPrototypeOf(value), descriptors);
154
+ }
155
+ /** Compare complete own-key enumeration in insertion order */
156
+ function areOwnKeysChanged(previous, next) {
157
+ const previousKeys = Reflect.ownKeys(previous);
158
+ const nextKeys = Reflect.ownKeys(next);
159
+ return previousKeys.length !== nextKeys.length || previousKeys.some((key, index) => key !== nextKeys[index]);
160
+ }
161
+ //#endregion
5
162
  //#region src/tracking.ts
6
163
  const useStoreLayoutEffect$1 = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
7
164
  const proxyTargetCache = /* @__PURE__ */ new WeakMap();
@@ -23,12 +180,12 @@ function useStoreValue(store, selector, tracking) {
23
180
  if (selector || !tracking) return shallowEqual(previous, next);
24
181
  const committed = committedTracking.current;
25
182
  if (!committed) return Object.is(previous, next);
26
- return !(0, proxy_compare.isChanged)(committed.snapshot, next, committed.affected, /* @__PURE__ */ new WeakMap());
183
+ return !isChanged(committed.snapshot, next, committed.affected, /* @__PURE__ */ new WeakMap());
27
184
  }, [selector, tracking]);
28
185
  const snapshot = (0, use_sync_external_store_shim_with_selector.useSyncExternalStoreWithSelector)(store.subscribe, store.getSnapshot, store.getSnapshot, selectSnapshot, compareSelections);
29
186
  const affected = /* @__PURE__ */ new WeakMap();
30
187
  const shouldTrack = !selector && tracking;
31
- const value = shouldTrack ? (0, proxy_compare.createProxy)(snapshot, affected, proxyCache, proxyTargetCache) : snapshot;
188
+ const value = shouldTrack ? createProxy(snapshot, affected, proxyCache, proxyTargetCache) : snapshot;
32
189
  useStoreLayoutEffect$1(() => {
33
190
  if (shouldTrack) {
34
191
  const renderedSnapshot = snapshot;
@@ -37,7 +194,7 @@ function useStoreValue(store, selector, tracking) {
37
194
  snapshot: renderedSnapshot
38
195
  };
39
196
  const currentSnapshot = store.getSnapshot();
40
- if (!Object.is(renderedSnapshot, currentSnapshot) && (0, proxy_compare.isChanged)(renderedSnapshot, currentSnapshot, affected, /* @__PURE__ */ new WeakMap())) calibrate(currentSnapshot);
197
+ if (!Object.is(renderedSnapshot, currentSnapshot) && isChanged(renderedSnapshot, currentSnapshot, affected, /* @__PURE__ */ new WeakMap())) calibrate(currentSnapshot);
41
198
  }
42
199
  }, [
43
200
  affected,
@@ -67,6 +224,11 @@ function isShallowComparable(value) {
67
224
  }
68
225
  //#endregion
69
226
  //#region src/index.tsx
227
+ /** Preserve an exact object identity and compare it as one atomic Store value */
228
+ function ref(value) {
229
+ markToTrack(value, false);
230
+ return value;
231
+ }
70
232
  const useStoreLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
71
233
  /**
72
234
  * Create a React Store with automatic tracking and explicit selector support
@@ -144,3 +306,4 @@ function useStoreContext(context) {
144
306
  //#endregion
145
307
  exports.bindStore = bindStore;
146
308
  exports.createStore = createStore;
309
+ exports.ref = ref;
package/dist/index.d.cts CHANGED
@@ -43,6 +43,8 @@ interface ExternalStore<TSnapshot> {
43
43
  type ExternalStoreSnapshot<TStore> = TStore extends ExternalStore<infer TSnapshot> ? TSnapshot : never;
44
44
  /** React bindings created for an existing external Store type */
45
45
  type StoreBinding<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>> = readonly [StoreHook<TSnapshot>, ExternalStoreProvider<TStore>, StoreInstanceHook<TStore>];
46
+ /** Preserve an exact object identity and compare it as one atomic Store value */
47
+ declare function ref<T extends object>(value: T): T;
46
48
  /**
47
49
  * Create a React Store with automatic tracking and explicit selector support
48
50
  */
@@ -53,4 +55,4 @@ declare function createStore<TStore, TProps = Record<never, never>>(useModel: (p
53
55
  declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
54
56
  declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(name?: string, options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
55
57
  //#endregion
56
- export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreOptions, StoreProvider, StoreSelector, bindStore, createStore };
58
+ export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreOptions, StoreProvider, StoreSelector, bindStore, createStore, ref };
package/dist/index.d.mts CHANGED
@@ -43,6 +43,8 @@ interface ExternalStore<TSnapshot> {
43
43
  type ExternalStoreSnapshot<TStore> = TStore extends ExternalStore<infer TSnapshot> ? TSnapshot : never;
44
44
  /** React bindings created for an existing external Store type */
45
45
  type StoreBinding<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>> = readonly [StoreHook<TSnapshot>, ExternalStoreProvider<TStore>, StoreInstanceHook<TStore>];
46
+ /** Preserve an exact object identity and compare it as one atomic Store value */
47
+ declare function ref<T extends object>(value: T): T;
46
48
  /**
47
49
  * Create a React Store with automatic tracking and explicit selector support
48
50
  */
@@ -53,4 +55,4 @@ declare function createStore<TStore, TProps = Record<never, never>>(useModel: (p
53
55
  declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
54
56
  declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(name?: string, options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
55
57
  //#endregion
56
- export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreOptions, StoreProvider, StoreSelector, bindStore, createStore };
58
+ export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreOptions, StoreProvider, StoreSelector, bindStore, createStore, ref };
package/dist/index.mjs CHANGED
@@ -1,6 +1,163 @@
1
1
  import { createContext, createElement, useCallback, useContext, useEffect, useLayoutEffect, useReducer, useRef, useState } from "react";
2
- import { createProxy, isChanged } from "proxy-compare";
3
2
  import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
3
+ //#region src/access-tracking.ts
4
+ /*!
5
+ * Adapted from proxy-compare 3.0.1.
6
+ * Copyright (c) 2020 Daishi Kato. Licensed under the MIT License.
7
+ */
8
+ const trackMemoSymbol = Symbol();
9
+ const getOriginalSymbol = Symbol();
10
+ const reactElementType = Symbol.for("react.element");
11
+ const reactTransitionalElementType = Symbol.for("react.transitional.element");
12
+ const reactPortalType = Symbol.for("react.portal");
13
+ const objectPrototype = Object.prototype;
14
+ const arrayPrototype = Array.prototype;
15
+ const trackingOverrides = /* @__PURE__ */ new WeakMap();
16
+ /** Create an access-tracking Proxy and lazily preserve React atomic values */
17
+ function createProxy(value, affected, proxyCache, targetCache) {
18
+ if (!isObjectToTrack(value)) return value;
19
+ const typedTargetCache = targetCache;
20
+ let targetAndCopied = typedTargetCache?.get(value);
21
+ if (!targetAndCopied) {
22
+ const target = getOriginalObject(value);
23
+ targetAndCopied = needsToCopyTargetObject(target) ? [target, copyTargetObject(target)] : [target];
24
+ typedTargetCache?.set(value, targetAndCopied);
25
+ }
26
+ const [target, copiedTarget] = targetAndCopied;
27
+ const typedProxyCache = proxyCache;
28
+ let handlerAndState = typedProxyCache?.get(target);
29
+ if (!handlerAndState || handlerAndState[1].copied !== Boolean(copiedTarget)) {
30
+ handlerAndState = createProxyHandler(target, Boolean(copiedTarget));
31
+ handlerAndState[1].proxy = new Proxy(copiedTarget ?? target, handlerAndState[0]);
32
+ typedProxyCache?.set(target, handlerAndState);
33
+ }
34
+ handlerAndState[1].affected = affected;
35
+ handlerAndState[1].proxyCache = proxyCache;
36
+ handlerAndState[1].targetCache = targetCache;
37
+ return handlerAndState[1].proxy;
38
+ }
39
+ /** Compare only paths read through a previous tracking Proxy */
40
+ function isChanged(previous, next, affected, cache, isEqual = Object.is) {
41
+ if (isEqual(previous, next)) return false;
42
+ if (!isObject(previous) || !isObject(next)) return true;
43
+ const used = affected.get(getOriginalObject(previous));
44
+ if (!used) return true;
45
+ if (cache) {
46
+ if (cache.get(previous) === next) return false;
47
+ cache.set(previous, next);
48
+ }
49
+ let changed = null;
50
+ for (const key of used.has ?? []) {
51
+ changed = Reflect.has(previous, key) !== Reflect.has(next, key);
52
+ if (changed) return true;
53
+ }
54
+ if (used.all) {
55
+ changed = areOwnKeysChanged(previous, next);
56
+ if (changed) return true;
57
+ } else for (const key of used.own ?? []) {
58
+ changed = Boolean(Reflect.getOwnPropertyDescriptor(previous, key)) !== Boolean(Reflect.getOwnPropertyDescriptor(next, key));
59
+ if (changed) return true;
60
+ }
61
+ for (const key of used.keys ?? []) {
62
+ changed = isChanged(Reflect.get(previous, key), Reflect.get(next, key), affected, cache, isEqual);
63
+ if (changed) return true;
64
+ }
65
+ if (changed === null) throw new Error("Invalid Kerros access tracking state");
66
+ return changed;
67
+ }
68
+ /** Mark an exact object identity as tracked or atomic */
69
+ function markToTrack(value, track = true) {
70
+ trackingOverrides.set(value, track);
71
+ }
72
+ /** Build the handler state shared by a single cached Proxy */
73
+ function createProxyHandler(original, copied) {
74
+ const state = { copied };
75
+ let trackWholeObject = false;
76
+ /** Record one access operation against the original snapshot object */
77
+ const record = (operation, key) => {
78
+ if (trackWholeObject) return;
79
+ let used = state.affected?.get(original);
80
+ if (!used) {
81
+ used = {};
82
+ state.affected?.set(original, used);
83
+ }
84
+ if (operation === "all") {
85
+ used.all = true;
86
+ return;
87
+ }
88
+ let keys = used[operation];
89
+ if (!keys) {
90
+ keys = /* @__PURE__ */ new Set();
91
+ used[operation] = keys;
92
+ }
93
+ keys.add(key);
94
+ };
95
+ const handler = {
96
+ get: (target, key) => {
97
+ if (key === getOriginalSymbol) return original;
98
+ record("keys", key);
99
+ return createProxy(Reflect.get(target, key), state.affected, state.proxyCache, state.targetCache);
100
+ },
101
+ getOwnPropertyDescriptor: (target, key) => {
102
+ record("own", key);
103
+ return Reflect.getOwnPropertyDescriptor(target, key);
104
+ },
105
+ has: (target, key) => {
106
+ if (key === trackMemoSymbol) {
107
+ trackWholeObject = true;
108
+ state.affected?.delete(original);
109
+ return true;
110
+ }
111
+ record("has", key);
112
+ return Reflect.has(target, key);
113
+ },
114
+ ownKeys: (target) => {
115
+ record("all");
116
+ return Reflect.ownKeys(target);
117
+ }
118
+ };
119
+ if (copied) {
120
+ handler.deleteProperty = () => false;
121
+ handler.set = () => false;
122
+ }
123
+ return [handler, state];
124
+ }
125
+ /** Decide lazily whether one reached value supports recursive tracking */
126
+ function isObjectToTrack(value) {
127
+ if (!isObject(value)) return false;
128
+ if (trackingOverrides.has(value)) return trackingOverrides.get(value);
129
+ const prototype = Object.getPrototypeOf(value);
130
+ if (prototype === arrayPrototype) return true;
131
+ if (prototype !== objectPrototype) return false;
132
+ const marker = value.$$typeof;
133
+ return marker !== reactElementType && marker !== reactTransitionalElementType && marker !== reactPortalType;
134
+ }
135
+ /** Narrow mutable object operations used by the compare algorithm */
136
+ function isObject(value) {
137
+ return typeof value === "object" && value !== null;
138
+ }
139
+ /** Unwrap a cached tracking Proxy when comparison receives one */
140
+ function getOriginalObject(value) {
141
+ return value[getOriginalSymbol] ?? value;
142
+ }
143
+ /** Detect invariant-sensitive frozen properties before Proxy creation */
144
+ function needsToCopyTargetObject(value) {
145
+ return Object.values(Object.getOwnPropertyDescriptors(value)).some((descriptor) => !descriptor.configurable && !descriptor.writable);
146
+ }
147
+ /** Copy an invariant-sensitive object with configurable descriptors */
148
+ function copyTargetObject(value) {
149
+ if (Array.isArray(value)) return Array.from(value);
150
+ const descriptors = Object.getOwnPropertyDescriptors(value);
151
+ for (const descriptor of Object.values(descriptors)) descriptor.configurable = true;
152
+ return Object.create(Object.getPrototypeOf(value), descriptors);
153
+ }
154
+ /** Compare complete own-key enumeration in insertion order */
155
+ function areOwnKeysChanged(previous, next) {
156
+ const previousKeys = Reflect.ownKeys(previous);
157
+ const nextKeys = Reflect.ownKeys(next);
158
+ return previousKeys.length !== nextKeys.length || previousKeys.some((key, index) => key !== nextKeys[index]);
159
+ }
160
+ //#endregion
4
161
  //#region src/tracking.ts
5
162
  const useStoreLayoutEffect$1 = typeof window === "undefined" ? useEffect : useLayoutEffect;
6
163
  const proxyTargetCache = /* @__PURE__ */ new WeakMap();
@@ -66,6 +223,11 @@ function isShallowComparable(value) {
66
223
  }
67
224
  //#endregion
68
225
  //#region src/index.tsx
226
+ /** Preserve an exact object identity and compare it as one atomic Store value */
227
+ function ref(value) {
228
+ markToTrack(value, false);
229
+ return value;
230
+ }
69
231
  const useStoreLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
70
232
  /**
71
233
  * Create a React Store with automatic tracking and explicit selector support
@@ -141,4 +303,4 @@ function useStoreContext(context) {
141
303
  return store;
142
304
  }
143
305
  //#endregion
144
- export { bindStore, createStore };
306
+ export { bindStore, createStore, ref };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@violetflux/kerros",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Hook-native state sharing for React with automatic access tracking and focused selectors.",
5
5
  "keywords": [
6
6
  "react",
@@ -32,7 +32,8 @@
32
32
  "sideEffects": false,
33
33
  "files": [
34
34
  "dist",
35
- "skills"
35
+ "skills",
36
+ "THIRD_PARTY_NOTICES.md"
36
37
  ],
37
38
  "main": "./dist/index.cjs",
38
39
  "module": "./dist/index.mjs",
@@ -79,7 +80,6 @@
79
80
  "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
80
81
  },
81
82
  "dependencies": {
82
- "proxy-compare": "3.0.1",
83
83
  "use-sync-external-store": "1.6.0"
84
84
  },
85
85
  "devDependencies": {
@@ -74,11 +74,18 @@ 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.
81
81
 
82
+ ## React and identity-sensitive values
83
+
84
+ - Return standard `useRef()` and `createRef()` containers directly. They work with DOM refs, `forwardRef`, and `useImperativeHandle` in React 17, 18, and 19; do not wrap them by default.
85
+ - React elements and portals are detected lazily and returned as atomic values without a Proxy.
86
+ - Import `ref` from `@violetflux/kerros` only for a third-party object that cannot tolerate a Proxy or when strict object identity must survive the tracked snapshot.
87
+ - `ref(value)`, `Map`, `Set`, and class instances are non-reactive internally. Publish a new containing-field reference for observable changes; changing a React ref's `.current` also does not rerender.
88
+
82
89
  ## Advanced external Store binding
83
90
 
84
91
  Most applications should stop at `createStore`. Use `bindStore` only when a headless Store already owns authoritative state outside React and exposes stable `getSnapshot` and `subscribe` functions.
@@ -101,7 +108,7 @@ Mount the original instance without mirroring its snapshot:
101
108
  </StreamBindingProvider>
102
109
  ```
103
110
 
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.
111
+ 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
112
 
106
113
  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
114
 
@@ -163,7 +170,7 @@ function Providers({ children }: PropsWithChildren) {
163
170
  ## Guardrails
164
171
 
165
172
  - Use `createStore` for state owned by a React Hook. Treat `bindStore` as an advanced adapter for an already-authoritative headless Store; do not mirror that snapshot through another Hook Store.
166
- - Prefer selector-free reads with immediate destructuring. Do not let the tracked result escape render through saving, returning, spreading, serializing, or passing it as an argument.
173
+ - Prefer selector-free reads with immediate destructuring. A tracked value may continue through a custom Hook or synchronously rendered child, but must not escape the render chain through state, refs, module variables, long-lived caches, Effects, or deferred callbacks. Spreading, rest destructuring, enumeration, and serialization create broad subscriptions.
167
174
  - When an explicit selector is justified, return an object of concrete fields and actions. Do not use array selectors or select the complete Store.
168
175
  - Do not wrap inline selectors with `useCallback`; Kerros handles selector identity.
169
176
  - Keep public actions as ordinary functions unless their reference stability is an explicit producer-side requirement. In React 19, use `useEffectEvent` only for events called from Effects, never as a public Store action.