@violetflux/kerros 0.2.3 → 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,6 +154,8 @@ 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
+ 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
+
157
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
@@ -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.
package/README.zh-CN.md CHANGED
@@ -146,6 +146,8 @@ const { count, setCount } = useCounter()
146
146
 
147
147
  基础类型快照使用 `Object.is`。`Map`、`Set`、类实例及其他非普通对象按整体引用处理。Store 和 External Store 快照必须保持不可变:每次可观察变化都发布新引用。
148
148
 
149
+ React Element 和 Portal 会自动作为原子值处理。React 17、18、19 的标准 `useRef()`、`createRef()` 容器可以直接返回。只有第三方对象不能接受 Proxy,或者必须保留严格身份时才使用 `ref(value)`;原子值的内部原地修改不是响应式更新。
150
+
149
151
  无 selector 的结果是当前组件的只读追踪快照,可以直接解构、保存在渲染局部变量中,或传给同步渲染的子组件继续读取。不要修改快照,也不要把它保存到 state、ref、模块变量或长期缓存后当作实时状态源;展开、rest 解构、枚举和序列化会形成宽泛订阅。响应式 Effect 应在渲染期间读取值并声明正确依赖;只有不参与渲染、需要执行时读取最新状态的命令式逻辑才使用 `useInstance()`。也不要把 Effect Event 暴露成公共 Store action。
150
152
 
151
153
  ## 多个实例
@@ -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`。
@@ -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.3",
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": {
@@ -79,6 +79,13 @@ Kerros automatically tracks object, array, and nested property reads made during
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.
@@ -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.