@violetflux/kerros 0.2.3 → 0.3.0

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
@@ -81,13 +81,17 @@ Ohne verstecktes Modul-Singleton kann derselbe Provider mehrfach eingebunden, ü
81
81
  ```ts
82
82
  function createStore<TStore, TProps = Record<never, never>>(
83
83
  useModel: (props: TProps) => TStore,
84
- ): readonly [StoreHook<TStore>, StoreProvider<TProps>]
84
+ ): readonly [StoreHook<TStore>, StoreProvider<TProps>, StoreGetter<TStore>]
85
85
 
86
86
  const [useStream, StreamProvider] = bindStore<Stream>('Stream')
87
87
  ```
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
+ Der dritte Getter liest außerhalb von React den zuletzt committeten Provider. Mit `scope?: string | number | symbol` am Provider wählt er die neueste exakt passende Instanz. Er abonniert keine Updates und wirft ohne verfügbaren Provider einen Fehler.
92
+
93
+ 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.
94
+
91
95
  `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
96
 
93
97
  ## Dokumentation
package/README.es.md CHANGED
@@ -81,13 +81,17 @@ Sin un singleton de módulo oculto, el mismo Provider puede montarse varias vece
81
81
  ```ts
82
82
  function createStore<TStore, TProps = Record<never, never>>(
83
83
  useModel: (props: TProps) => TStore,
84
- ): readonly [StoreHook<TStore>, StoreProvider<TProps>]
84
+ ): readonly [StoreHook<TStore>, StoreProvider<TProps>, StoreGetter<TStore>]
85
85
 
86
86
  const [useStream, StreamProvider] = bindStore<Stream>('Stream')
87
87
  ```
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
+ El tercer getter lee fuera de React el último Provider confirmado. Con `scope?: string | number | symbol` en el Provider selecciona la instancia más reciente que coincida exactamente. No se suscribe y lanza un error si no hay un Provider disponible.
92
+
93
+ 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.
94
+
91
95
  `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
96
 
93
97
  ## Documentación
package/README.fr.md CHANGED
@@ -81,13 +81,17 @@ Sans singleton de module caché, un même Provider peut être monté plusieurs f
81
81
  ```ts
82
82
  function createStore<TStore, TProps = Record<never, never>>(
83
83
  useModel: (props: TProps) => TStore,
84
- ): readonly [StoreHook<TStore>, StoreProvider<TProps>]
84
+ ): readonly [StoreHook<TStore>, StoreProvider<TProps>, StoreGetter<TStore>]
85
85
 
86
86
  const [useStream, StreamProvider] = bindStore<Stream>('Stream')
87
87
  ```
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
+ Le troisième getter lit hors de React le dernier Provider validé par un commit. Avec `scope?: string | number | symbol` sur le Provider, il choisit la dernière instance qui correspond exactement. Il ne s'abonne pas et lève une erreur sans Provider disponible.
92
+
93
+ 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.
94
+
91
95
  `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
96
 
93
97
  ## Documentation
package/README.ja.md CHANGED
@@ -82,13 +82,17 @@ Provider は Store Hook を実行し、Context には安定した購読コンテ
82
82
  ```ts
83
83
  function createStore<TStore, TProps = Record<never, never>>(
84
84
  useModel: (props: TProps) => TStore,
85
- ): readonly [StoreHook<TStore>, StoreProvider<TProps>]
85
+ ): readonly [StoreHook<TStore>, StoreProvider<TProps>, StoreGetter<TStore>]
86
86
 
87
87
  const [useStream, StreamProvider] = bindStore<Stream>('Stream')
88
88
  ```
89
89
 
90
90
  返される Store Hook は引数なしで自動追跡を使います。明示的なオブジェクト selector は派生値や計測済みホットスポット向けです。対応する Provider の外では明確なエラーを送出します。
91
91
 
92
+ 3 番目の getter は React 外から最後にコミットされた Provider を読み取ります。Provider の `scope?: string | number | symbol` を渡すと完全一致する最新インスタンスを選べます。getter は購読せず、利用可能な Provider がなければエラーを投げます。
93
+
94
+ React Element と Portal は自動的に原子的な値になります。`useRef()` と `createRef()` はそのまま返せます。`ref(value)` は Proxy 非互換の値や厳密な同一性が必要な場合だけ使います。
95
+
92
96
  高度な連携として、既存の Headless External Store にだけ `bindStore` を使います。通常の Hook 状態には `createStore` を使ってください。Context は元の Store インスタンスだけを保持し、コンシューマーは `getSnapshot` と `subscribe` を直接利用します。
93
97
 
94
98
  ## ドキュメント
package/README.ko.md CHANGED
@@ -82,13 +82,17 @@ Provider는 Store Hook을 실행하고 Context에는 안정적인 구독 컨테
82
82
  ```ts
83
83
  function createStore<TStore, TProps = Record<never, never>>(
84
84
  useModel: (props: TProps) => TStore,
85
- ): readonly [StoreHook<TStore>, StoreProvider<TProps>]
85
+ ): readonly [StoreHook<TStore>, StoreProvider<TProps>, StoreGetter<TStore>]
86
86
 
87
87
  const [useStream, StreamProvider] = bindStore<Stream>('Stream')
88
88
  ```
89
89
 
90
90
  반환된 Store Hook은 인자 없이 자동 추적을 사용합니다. 명시적 객체 selector는 파생 값과 측정된 핫스팟을 위한 고급 경로입니다. Provider 밖에서 호출하면 명확한 오류가 발생합니다.
91
91
 
92
+ 세 번째 getter는 React 밖에서 마지막으로 커밋된 Provider를 읽습니다. Provider의 `scope?: string | number | symbol`을 전달하면 정확히 일치하는 최신 인스턴스를 선택합니다. getter는 구독하지 않으며 사용 가능한 Provider가 없으면 오류를 던집니다.
93
+
94
+ React Element와 Portal은 자동으로 원자 값이 됩니다. `useRef()`와 `createRef()`는 그대로 반환할 수 있으며 `ref(value)`는 Proxy 비호환 값이나 엄격한 동일성이 필요할 때만 사용합니다.
95
+
92
96
  고급 통합이 필요한 기존 Headless External Store에만 `bindStore`를 사용하세요. 일반 Hook 상태에는 `createStore`를 사용합니다. Context는 원래 Store 인스턴스만 보관하고 소비자는 `getSnapshot`과 `subscribe`를 직접 사용합니다.
93
97
 
94
98
  ## 문서
package/README.md CHANGED
@@ -62,10 +62,10 @@ function useTaskModel() {
62
62
  return { tasks, addTask, finishTask }
63
63
  }
64
64
 
65
- export const [useTask, TaskProvider] = createStore(useTaskModel)
65
+ export const [useTask, TaskProvider, getTask] = createStore(useTaskModel)
66
66
  ```
67
67
 
68
- `createStore` returns two values: the Hook used by components and its matching Provider.
68
+ `createStore` returns the component Hook, its matching Provider, and an imperative getter for committed instances outside React.
69
69
 
70
70
  The Store is still a normal React Hook. It may use `useState`, `useReducer`, Context, SDK Hooks, or your own custom Hooks.
71
71
 
@@ -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
@@ -241,15 +243,25 @@ const [useCounter, CounterProvider] = createStore(useCounterModel)
241
243
  function createStore<TStore, TProps = Record<never, never>>(
242
244
  useModel: (props: TProps) => TStore,
243
245
  options?: { tracking?: boolean },
244
- ): readonly [StoreHook<TStore>, StoreProvider<TProps>]
246
+ ): readonly [StoreHook<TStore>, StoreProvider<TProps>, StoreGetter<TStore>]
245
247
  ```
246
248
 
247
249
  - `useModel` follows the Rules of Hooks
248
250
  - Provider props, excluding `children`, are passed to `useModel`
251
+ - Providers accept an optional `scope?: string | number | symbol` for imperative lookup
249
252
  - the returned Store Hook accepts either no argument for automatic tracking or an object-returning selector
253
+ - the returned getter reads the latest mounted committed Provider, or the latest exact scope match; it does not subscribe
250
254
  - using the Store Hook outside its matching Provider throws a clear error
251
255
  - Provider instances work with Strict Mode and server rendering
252
256
 
257
+ ### `ref` (identity escape hatch)
258
+
259
+ ```ts
260
+ function ref<T extends object>(value: T): T
261
+ ```
262
+
263
+ Marks an object as atomic and returns the exact same identity. Standard React refs do not need this helper.
264
+
253
265
  ### Advanced: bind an existing external Store
254
266
 
255
267
  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
@@ -54,10 +54,10 @@ function useTaskModel() {
54
54
  return { tasks, addTask, finishTask }
55
55
  }
56
56
 
57
- export const [useTask, TaskProvider] = createStore(useTaskModel)
57
+ export const [useTask, TaskProvider, getTask] = createStore(useTaskModel)
58
58
  ```
59
59
 
60
- `createStore` 返回两个值:组件调用的 Hook 和对应的 Provider
60
+ `createStore` 返回组件 Hook、对应的 Provider,以及在 React 外命令式读取已提交实例的 getter
61
61
 
62
62
  Store 仍然是普通 React Hook,可以继续使用 `useState`、`useReducer`、Context、SDK Hook 或其他 custom Hook。
63
63
 
@@ -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
  ## 多个实例
@@ -233,15 +235,25 @@ const [useCounter, CounterProvider] = createStore(useCounterModel)
233
235
  function createStore<TStore, TProps = Record<never, never>>(
234
236
  useModel: (props: TProps) => TStore,
235
237
  options?: { tracking?: boolean },
236
- ): readonly [StoreHook<TStore>, StoreProvider<TProps>]
238
+ ): readonly [StoreHook<TStore>, StoreProvider<TProps>, StoreGetter<TStore>]
237
239
  ```
238
240
 
239
241
  - `useModel` 必须遵守 Hooks 规则
240
242
  - 除 `children` 外的 Provider props 会传给 `useModel`
243
+ - Provider 接受可选的 `scope?: string | number | symbol`,用于命令式查找
241
244
  - Store Hook 可不传参数使用自动追踪,也可传入返回对象的 selector
245
+ - getter 读取最后挂载且已提交的 Provider,或 scope 精确匹配的最后一个实例;它不会订阅更新
242
246
  - 在对应 Provider 外调用会抛出明确错误
243
247
  - 支持 Strict Mode、服务端渲染和 Provider 多实例
244
248
 
249
+ ### `ref`(身份逃生口)
250
+
251
+ ```ts
252
+ function ref<T extends object>(value: T): T
253
+ ```
254
+
255
+ 把对象标记为原子值并返回完全相同的身份。标准 React ref 不需要这个辅助函数。
256
+
245
257
  ### 高级用法:绑定已有 External Store
246
258
 
247
259
  绝大多数应用只需要 `createStore`。只有当某个库或 SDK 已经在 React 外持有权威状态,并提供稳定的 `getSnapshot` 和 `subscribe` 函数时,才使用 `bindStore`。
@@ -0,0 +1,25 @@
1
+ # Third-party notices
2
+
3
+ Kerros includes an adapted subset of `proxy-compare@3.0.1` in its access-tracking implementation.
4
+
5
+ The Kerros website theme adapts the BeUI Motion segmented-control pattern in `theme/components/segmented.tsx` and its class-name utility in `theme/lib/utils.ts` from https://beui.dev (https://github.com/starc007/ui-components). This code is website-only and is not distributed with the Kerros npm package.
6
+
7
+ ## proxy-compare
8
+
9
+ Copyright (c) 2020 Daishi Kato
10
+
11
+ 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:
12
+
13
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
14
+
15
+ 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.
16
+
17
+ ## BeUI Motion
18
+
19
+ Copyright (c) 2026 Saurabh Chauhan
20
+
21
+ 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:
22
+
23
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
24
+
25
+ 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
@@ -75,12 +237,33 @@ function createStore(useModel, options) {
75
237
  const StoreContext = (0, react.createContext)(void 0);
76
238
  const storeName = useModel.name || "KerrosStore";
77
239
  const tracking = options?.tracking ?? true;
240
+ const registrations = [];
78
241
  /** Run the model Hook and publish its committed snapshot */
79
242
  const StoreProvider = (props) => {
243
+ const parent = (0, react.useContext)(StoreContext);
80
244
  const { children, ...storeProps } = props;
81
245
  const model = useModel(storeProps);
82
246
  const [container] = (0, react.useState)(() => createStoreContainer(model));
247
+ const scope = props.scope;
83
248
  useStoreLayoutEffect(() => container.publish(model), [container, model]);
249
+ useStoreLayoutEffect(() => {
250
+ const registration = {
251
+ container,
252
+ parent,
253
+ scope
254
+ };
255
+ const descendantIndex = registrations.findIndex((candidate) => isStoreDescendant(candidate, container, registrations));
256
+ if (descendantIndex < 0) registrations.push(registration);
257
+ else registrations.splice(descendantIndex, 0, registration);
258
+ return () => {
259
+ const index = registrations.lastIndexOf(registration);
260
+ if (index >= 0) registrations.splice(index, 1);
261
+ };
262
+ }, [
263
+ container,
264
+ parent,
265
+ scope
266
+ ]);
84
267
  return (0, react.createElement)(StoreContext.Provider, { value: container }, children);
85
268
  };
86
269
  StoreProvider.displayName = `${storeName}Provider`;
@@ -89,7 +272,28 @@ function createStore(useModel, options) {
89
272
  const useStore = ((selector) => {
90
273
  return useStoreValue(useStoreContext(StoreContext), selector, tracking);
91
274
  });
92
- return [useStore, StoreProvider];
275
+ /** Read the latest committed Store matching the optional scope */
276
+ const getStore = ((scope) => {
277
+ for (let index = registrations.length - 1; index >= 0; index--) {
278
+ const registration = registrations[index];
279
+ if (scope === void 0 || Object.is(registration.scope, scope)) return registration.container.getSnapshot();
280
+ }
281
+ throw new Error(scope === void 0 ? "Kerros store getter requires a mounted Provider" : "Kerros store getter could not find a mounted Provider for the requested scope");
282
+ });
283
+ return [
284
+ useStore,
285
+ StoreProvider,
286
+ getStore
287
+ ];
288
+ }
289
+ /** Test whether one committed Provider is nested below another container */
290
+ function isStoreDescendant(registration, ancestor, registrations) {
291
+ let parent = registration.parent;
292
+ while (parent) {
293
+ if (parent === ancestor) return true;
294
+ parent = registrations.find((candidate) => candidate.container === parent)?.parent;
295
+ }
296
+ return false;
93
297
  }
94
298
  function bindStore(nameOrOptions = "KerrosExternalStore", inputOptions) {
95
299
  const name = typeof nameOrOptions === "string" ? nameOrOptions : "KerrosExternalStore";
@@ -144,3 +348,4 @@ function useStoreContext(context) {
144
348
  //#endregion
145
349
  exports.bindStore = bindStore;
146
350
  exports.createStore = createStore;
351
+ exports.ref = ref;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { FC, PropsWithChildren } from "react";
2
2
  //#region src/index.d.ts
3
3
  declare const storeHookMarker: unique symbol;
4
+ declare const storeGetterMarker: unique symbol;
4
5
  declare const storeInstanceHookMarker: unique symbol;
5
6
  declare const externalStoreProviderMarker: unique symbol;
6
7
  /** Store behavior options */
@@ -17,8 +18,22 @@ interface StoreHook<TStore> {
17
18
  (): TStore;
18
19
  <TSelection extends object>(selector: StoreSelector<TStore, TSelection>): TSelection;
19
20
  }
21
+ /** Supported identity for imperative Provider lookup */
22
+ type StoreScope = string | number | symbol;
23
+ /** Imperative reader for the latest committed Store snapshot */
24
+ interface StoreGetter<TStore> {
25
+ /** Type-only Store getter identity */
26
+ readonly [storeGetterMarker]: TStore;
27
+ /** Read the most recently mounted Store */
28
+ (): TStore;
29
+ /** Read the most recently mounted Store matching one scope */
30
+ (scope: StoreScope): TStore;
31
+ }
20
32
  /** Provider created for a Store hook */
21
- type StoreProvider<TProps> = FC<PropsWithChildren<TProps>>;
33
+ type StoreProvider<TProps> = FC<PropsWithChildren<TProps & {
34
+ /** Optional identity used by the imperative Store getter */
35
+ scope?: StoreScope;
36
+ }>>;
22
37
  /** Hook returning the exact external Store instance */
23
38
  interface StoreInstanceHook<TStore> {
24
39
  /** Type-only Store instance hook identity */
@@ -26,9 +41,9 @@ interface StoreInstanceHook<TStore> {
26
41
  (): TStore;
27
42
  }
28
43
  /** Provider carrying an existing external Store instance */
29
- type ExternalStoreProvider<TStore> = StoreProvider<{
44
+ type ExternalStoreProvider<TStore> = FC<PropsWithChildren<{
30
45
  store: TStore;
31
- }> & {
46
+ }>> & {
32
47
  /** Type-only external Store Provider identity */
33
48
  readonly [externalStoreProviderMarker]: TStore;
34
49
  };
@@ -43,14 +58,16 @@ interface ExternalStore<TSnapshot> {
43
58
  type ExternalStoreSnapshot<TStore> = TStore extends ExternalStore<infer TSnapshot> ? TSnapshot : never;
44
59
  /** React bindings created for an existing external Store type */
45
60
  type StoreBinding<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>> = readonly [StoreHook<TSnapshot>, ExternalStoreProvider<TStore>, StoreInstanceHook<TStore>];
61
+ /** Preserve an exact object identity and compare it as one atomic Store value */
62
+ declare function ref<T extends object>(value: T): T;
46
63
  /**
47
64
  * Create a React Store with automatic tracking and explicit selector support
48
65
  */
49
- declare function createStore<TStore, TProps = Record<never, never>>(useModel: (props: TProps) => TStore, options?: StoreOptions): readonly [StoreHook<TStore>, StoreProvider<TProps>];
66
+ declare function createStore<TStore, TProps = Record<never, never>>(useModel: (props: TProps) => TStore, options?: StoreOptions): readonly [StoreHook<TStore>, StoreProvider<TProps>, StoreGetter<TStore>];
50
67
  /**
51
68
  * Bind existing external Store instances to scoped React consumers
52
69
  */
53
70
  declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
54
71
  declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(name?: string, options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
55
72
  //#endregion
56
- export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreOptions, StoreProvider, StoreSelector, bindStore, createStore };
73
+ export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreGetter, StoreHook, StoreOptions, StoreProvider, StoreScope, StoreSelector, bindStore, createStore, ref };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { FC, PropsWithChildren } from "react";
2
2
  //#region src/index.d.ts
3
3
  declare const storeHookMarker: unique symbol;
4
+ declare const storeGetterMarker: unique symbol;
4
5
  declare const storeInstanceHookMarker: unique symbol;
5
6
  declare const externalStoreProviderMarker: unique symbol;
6
7
  /** Store behavior options */
@@ -17,8 +18,22 @@ interface StoreHook<TStore> {
17
18
  (): TStore;
18
19
  <TSelection extends object>(selector: StoreSelector<TStore, TSelection>): TSelection;
19
20
  }
21
+ /** Supported identity for imperative Provider lookup */
22
+ type StoreScope = string | number | symbol;
23
+ /** Imperative reader for the latest committed Store snapshot */
24
+ interface StoreGetter<TStore> {
25
+ /** Type-only Store getter identity */
26
+ readonly [storeGetterMarker]: TStore;
27
+ /** Read the most recently mounted Store */
28
+ (): TStore;
29
+ /** Read the most recently mounted Store matching one scope */
30
+ (scope: StoreScope): TStore;
31
+ }
20
32
  /** Provider created for a Store hook */
21
- type StoreProvider<TProps> = FC<PropsWithChildren<TProps>>;
33
+ type StoreProvider<TProps> = FC<PropsWithChildren<TProps & {
34
+ /** Optional identity used by the imperative Store getter */
35
+ scope?: StoreScope;
36
+ }>>;
22
37
  /** Hook returning the exact external Store instance */
23
38
  interface StoreInstanceHook<TStore> {
24
39
  /** Type-only Store instance hook identity */
@@ -26,9 +41,9 @@ interface StoreInstanceHook<TStore> {
26
41
  (): TStore;
27
42
  }
28
43
  /** Provider carrying an existing external Store instance */
29
- type ExternalStoreProvider<TStore> = StoreProvider<{
44
+ type ExternalStoreProvider<TStore> = FC<PropsWithChildren<{
30
45
  store: TStore;
31
- }> & {
46
+ }>> & {
32
47
  /** Type-only external Store Provider identity */
33
48
  readonly [externalStoreProviderMarker]: TStore;
34
49
  };
@@ -43,14 +58,16 @@ interface ExternalStore<TSnapshot> {
43
58
  type ExternalStoreSnapshot<TStore> = TStore extends ExternalStore<infer TSnapshot> ? TSnapshot : never;
44
59
  /** React bindings created for an existing external Store type */
45
60
  type StoreBinding<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>> = readonly [StoreHook<TSnapshot>, ExternalStoreProvider<TStore>, StoreInstanceHook<TStore>];
61
+ /** Preserve an exact object identity and compare it as one atomic Store value */
62
+ declare function ref<T extends object>(value: T): T;
46
63
  /**
47
64
  * Create a React Store with automatic tracking and explicit selector support
48
65
  */
49
- declare function createStore<TStore, TProps = Record<never, never>>(useModel: (props: TProps) => TStore, options?: StoreOptions): readonly [StoreHook<TStore>, StoreProvider<TProps>];
66
+ declare function createStore<TStore, TProps = Record<never, never>>(useModel: (props: TProps) => TStore, options?: StoreOptions): readonly [StoreHook<TStore>, StoreProvider<TProps>, StoreGetter<TStore>];
50
67
  /**
51
68
  * Bind existing external Store instances to scoped React consumers
52
69
  */
53
70
  declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
54
71
  declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(name?: string, options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
55
72
  //#endregion
56
- export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreOptions, StoreProvider, StoreSelector, bindStore, createStore };
73
+ export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreGetter, StoreHook, StoreOptions, StoreProvider, StoreScope, 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
@@ -74,12 +236,33 @@ function createStore(useModel, options) {
74
236
  const StoreContext = createContext(void 0);
75
237
  const storeName = useModel.name || "KerrosStore";
76
238
  const tracking = options?.tracking ?? true;
239
+ const registrations = [];
77
240
  /** Run the model Hook and publish its committed snapshot */
78
241
  const StoreProvider = (props) => {
242
+ const parent = useContext(StoreContext);
79
243
  const { children, ...storeProps } = props;
80
244
  const model = useModel(storeProps);
81
245
  const [container] = useState(() => createStoreContainer(model));
246
+ const scope = props.scope;
82
247
  useStoreLayoutEffect(() => container.publish(model), [container, model]);
248
+ useStoreLayoutEffect(() => {
249
+ const registration = {
250
+ container,
251
+ parent,
252
+ scope
253
+ };
254
+ const descendantIndex = registrations.findIndex((candidate) => isStoreDescendant(candidate, container, registrations));
255
+ if (descendantIndex < 0) registrations.push(registration);
256
+ else registrations.splice(descendantIndex, 0, registration);
257
+ return () => {
258
+ const index = registrations.lastIndexOf(registration);
259
+ if (index >= 0) registrations.splice(index, 1);
260
+ };
261
+ }, [
262
+ container,
263
+ parent,
264
+ scope
265
+ ]);
83
266
  return createElement(StoreContext.Provider, { value: container }, children);
84
267
  };
85
268
  StoreProvider.displayName = `${storeName}Provider`;
@@ -88,7 +271,28 @@ function createStore(useModel, options) {
88
271
  const useStore = ((selector) => {
89
272
  return useStoreValue(useStoreContext(StoreContext), selector, tracking);
90
273
  });
91
- return [useStore, StoreProvider];
274
+ /** Read the latest committed Store matching the optional scope */
275
+ const getStore = ((scope) => {
276
+ for (let index = registrations.length - 1; index >= 0; index--) {
277
+ const registration = registrations[index];
278
+ if (scope === void 0 || Object.is(registration.scope, scope)) return registration.container.getSnapshot();
279
+ }
280
+ throw new Error(scope === void 0 ? "Kerros store getter requires a mounted Provider" : "Kerros store getter could not find a mounted Provider for the requested scope");
281
+ });
282
+ return [
283
+ useStore,
284
+ StoreProvider,
285
+ getStore
286
+ ];
287
+ }
288
+ /** Test whether one committed Provider is nested below another container */
289
+ function isStoreDescendant(registration, ancestor, registrations) {
290
+ let parent = registration.parent;
291
+ while (parent) {
292
+ if (parent === ancestor) return true;
293
+ parent = registrations.find((candidate) => candidate.container === parent)?.parent;
294
+ }
295
+ return false;
92
296
  }
93
297
  function bindStore(nameOrOptions = "KerrosExternalStore", inputOptions) {
94
298
  const name = typeof nameOrOptions === "string" ? nameOrOptions : "KerrosExternalStore";
@@ -141,4 +345,4 @@ function useStoreContext(context) {
141
345
  return store;
142
346
  }
143
347
  //#endregion
144
- export { bindStore, createStore };
348
+ 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.3.0",
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,23 +80,28 @@
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": {
86
86
  "@eslint/js": "10.0.1",
87
+ "@rsbuild/plugin-tailwindcss": "^2.0.3",
87
88
  "@rspress/core": "2.0.17",
89
+ "@tabler/icons-react": "^3.46.0",
88
90
  "@types/jsdom": "28.0.3",
89
91
  "@types/node": "26.1.1",
90
92
  "@types/react": "19.2.17",
91
93
  "@types/react-dom": "19.2.3",
92
94
  "@types/use-sync-external-store": "1.5.0",
95
+ "clsx": "^2.1.1",
93
96
  "eslint": "10.7.0",
94
97
  "eslint-plugin-react-hooks": "7.1.1",
95
98
  "globals": "17.7.0",
96
99
  "jsdom": "29.1.1",
100
+ "motion": "^13.1.0",
97
101
  "react": "19.2.7",
98
102
  "react-dom": "19.2.7",
103
+ "tailwind-merge": "^3.6.0",
104
+ "tailwindcss": "^4.3.3",
99
105
  "tinybench": "6.1.2",
100
106
  "tsdown": "0.22.9",
101
107
  "typescript": "5.9.3",
@@ -45,7 +45,7 @@ function useCounterModel() {
45
45
  return { count, increment }
46
46
  }
47
47
 
48
- export const [useCounter, CounterProvider] = createStore(useCounterModel)
48
+ export const [useCounter, CounterProvider, getCounter] = createStore(useCounterModel)
49
49
  ```
50
50
 
51
51
  Mount the Provider at the narrowest ancestor shared by all consumers:
@@ -72,6 +72,18 @@ function Counter() {
72
72
 
73
73
  Kerros automatically tracks object, array, and nested property reads made during render. An update to an unread field must not rerender this component; Kerros does not deep-compare the complete Store.
74
74
 
75
+ Use the third return value only for imperative work outside React. `getCounter()` reads the most recently mounted committed Provider; pass a `string`, `number`, or `symbol` scope to select the latest matching Provider:
76
+
77
+ ```tsx
78
+ <CounterProvider scope="main">
79
+ <App />
80
+ </CounterProvider>
81
+
82
+ getCounter('main').increment()
83
+ ```
84
+
85
+ The getter does not subscribe and is unavailable before commit, after unmount, or during SSR. Call it at execution time; do not cache its result as a live state source. Duplicate scopes shadow and fall back in mount order. Keep using the Store Hook for rendered state.
86
+
75
87
  ## Subscription modes
76
88
 
77
89
  - `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.
@@ -79,6 +91,13 @@ Kerros automatically tracks object, array, and nested property reads made during
79
91
  - `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
92
  - Primitive Store snapshots use `Object.is`. `Map`, `Set`, class instances, and other atomic objects are tracked by reference as a whole.
81
93
 
94
+ ## React and identity-sensitive values
95
+
96
+ - 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.
97
+ - React elements and portals are detected lazily and returned as atomic values without a Proxy.
98
+ - 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.
99
+ - `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.
100
+
82
101
  ## Advanced external Store binding
83
102
 
84
103
  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,13 +182,14 @@ function Providers({ children }: PropsWithChildren) {
163
182
  ## Guardrails
164
183
 
165
184
  - 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.
185
+ - 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
186
  - 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
187
  - Do not wrap inline selectors with `useCallback`; Kerros handles selector identity.
169
188
  - 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.
170
189
  - Do not mirror the same mutable state across Stores. Read it from its authoritative Store or move ownership.
171
190
  - Do not create circular Store dependencies. Split ownership or invert the Provider order.
172
191
  - Do not call a Store Hook outside its matching Provider; Kerros intentionally throws a clear error.
192
+ - Do not use a Store getter for reactive rendering. It exposes only committed mounted instances, throws when none match, and selects by mount priority rather than Context ancestry.
173
193
  - Do not replace scoped Providers with a hidden module singleton. Put an application-wide Provider at the root only when the state is truly application-wide.
174
194
  - Preserve SDK caches, subscriptions, and streams under a single owner. Bind an existing headless Store directly; call a connection-owning SDK Hook inside one `createStore` initializer only when no external Store instance exists.
175
195
  - Respect the project's React version. Avoid React 19-only APIs when the consuming project still supports React 17 or 18.