@blac/react 2.0.16 → 2.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -21
- package/dist/buildTrackedProxy.d.ts +31 -0
- package/dist/index.cjs +233 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +234 -27
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +4 -5
- package/dist/useBloc.d.ts +2 -2
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -4,6 +4,12 @@ React bindings for BlaC — `useBloc` hook with proxy-based automatic re-render
|
|
|
4
4
|
|
|
5
5
|
**[Documentation](https://blac-docs.pages.dev/react/getting-started)** · **[npm](https://www.npmjs.com/package/@blac/react)**
|
|
6
6
|
|
|
7
|
+
> [!WARNING]
|
|
8
|
+
> **BlaC v2 is in pre-release (beta).** While in beta, **breaking API changes may
|
|
9
|
+
> ship in patch releases** without a major version bump. Pin an exact version and
|
|
10
|
+
> check the changelog before upgrading. Strict semver resumes once v2 is officially
|
|
11
|
+
> out of beta.
|
|
12
|
+
|
|
7
13
|
## Installation
|
|
8
14
|
|
|
9
15
|
```bash
|
|
@@ -88,11 +94,11 @@ function UserCard({ userId }: { userId: string }) {
|
|
|
88
94
|
- **Required when declared** — omitting `args` or passing the wrong shape is a type error.
|
|
89
95
|
- **Drives identity** — different `args` ⇒ different instance. Same `args` ⇒ same instance (dev-warn if args mismatch on same-keyed second call).
|
|
90
96
|
- **Serializable only** — non-serializable values (refs, callbacks) belong in the `deps` lane (below).
|
|
91
|
-
- **Per-component private instances** —
|
|
97
|
+
- **Per-component private instances** — embed a per-mount unique ID inside `args` (using React's `useId()`) to give each mount its own instance, disposed on unmount:
|
|
92
98
|
|
|
93
99
|
```tsx
|
|
94
|
-
const
|
|
95
|
-
const [state, cubit] = useBloc(FormCubit, { args: options,
|
|
100
|
+
const id = useId();
|
|
101
|
+
const [state, cubit] = useBloc(FormCubit, { args: { ...options, _id: id } });
|
|
96
102
|
```
|
|
97
103
|
|
|
98
104
|
### `deps`: Non-Serializable Refs and Callbacks
|
|
@@ -204,24 +210,29 @@ const [state] = useBloc(CounterCubit, {
|
|
|
204
210
|
|
|
205
211
|
### Options
|
|
206
212
|
|
|
207
|
-
| Option
|
|
208
|
-
|
|
|
209
|
-
| `args`
|
|
210
|
-
| `select`
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `onUnmount` | `(bloc) => void` | Called when component unmounts |
|
|
213
|
+
| Option | Type | Description |
|
|
214
|
+
| ----------- | ---------------------------- | -------------------------------------------------------------------------------- |
|
|
215
|
+
| `args` | `Args` type | Required when bloc declares `Args != void`; forbidden when `void` |
|
|
216
|
+
| `select` | `(state, bloc) => unknown[]` | Manual dependency selector (renamed from `dependencies`); disables auto-tracking |
|
|
217
|
+
| `onMount` | `(bloc) => void` | Called when component mounts |
|
|
218
|
+
| `onUnmount` | `(bloc) => void` | Called when component unmounts |
|
|
214
219
|
|
|
215
|
-
> Auto-tracking is always on when `select` is omitted — it is **not** a configurable option. `deps` and `
|
|
220
|
+
> Auto-tracking is always on when `select` is omitted — it is **not** a configurable option. `deps`, `autoInstance`, and `instanceId` are not `useBloc` options: wire deps from a mount effect (`APPLY_DEPS` / `REMOVE_DEPS_OWNER`); for per-mount private instances, embed a stable unique ID in `args` (e.g. `{ args: { _id: useId() } }`).
|
|
216
221
|
|
|
217
222
|
### Identity and Keying
|
|
218
223
|
|
|
219
224
|
Instance identity is resolved in precedence order:
|
|
220
225
|
|
|
221
|
-
1. **
|
|
222
|
-
2.
|
|
223
|
-
3. **`
|
|
224
|
-
|
|
226
|
+
1. **`<BlocProvider>` context id** — inherited from an ancestor provider
|
|
227
|
+
2. **`static key(args)` → structural hash of `args`** — default when the bloc declares `Args` (`static key` wins if defined; otherwise a stable hash of all `args`)
|
|
228
|
+
3. **`'default'`** — singleton fallback
|
|
229
|
+
|
|
230
|
+
For a per-mount private instance, embed a stable unique ID inside `args` so each mount hashes to a distinct key:
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
const id = useId();
|
|
234
|
+
useBloc(FormCubit, { args: { ...options, _id: id } });
|
|
235
|
+
```
|
|
225
236
|
|
|
226
237
|
Blocs declare explicit identity via a static class property:
|
|
227
238
|
|
|
@@ -237,24 +248,24 @@ class DocumentCubit extends Cubit<
|
|
|
237
248
|
|
|
238
249
|
### Instance Sharing and Lifecycle
|
|
239
250
|
|
|
240
|
-
By default, all components using `useBloc(MyBloc)` with the same identity share one instance.
|
|
251
|
+
By default, all components using `useBloc(MyBloc)` with the same identity share one instance. For per-component private instances, embed a unique ID in `args` so each mount derives a distinct key:
|
|
241
252
|
|
|
242
253
|
```tsx
|
|
243
254
|
// All users with userId=123 share one instance
|
|
244
255
|
useBloc(UserCardCubit, { args: { userId: 123 } });
|
|
245
256
|
|
|
246
257
|
// Each component mount gets its own instance, disposed on unmount
|
|
247
|
-
const
|
|
248
|
-
useBloc(FormCubit, { args: options,
|
|
258
|
+
const id = useId();
|
|
259
|
+
useBloc(FormCubit, { args: { ...options, _id: id } });
|
|
249
260
|
|
|
250
|
-
// Explicit
|
|
251
|
-
useBloc(EditorCubit, {
|
|
261
|
+
// Explicit stable key (escape hatch for non-derivable identity)
|
|
262
|
+
useBloc(EditorCubit, { args: { _id: 'editor-1' } });
|
|
252
263
|
```
|
|
253
264
|
|
|
254
265
|
### Breaking Changes (v2)
|
|
255
266
|
|
|
256
267
|
- **`dependencies` option renamed to `select`** — avoids confusion with the `deps` (non-serializable handles) lane.
|
|
257
|
-
- **`autoTrack`, `autoInstance`, and `deps` are no longer `useBloc` options** — auto-tracking is always on (opt out per-consumer with `select`); per-mount instances
|
|
268
|
+
- **`autoTrack`, `autoInstance`, `instanceId`, and `deps` are no longer `useBloc` options** — auto-tracking is always on (opt out per-consumer with `select`); per-mount private instances embed a unique ID in `args`; deps are wired from a mount effect via `APPLY_DEPS` / `REMOVE_DEPS_OWNER`.
|
|
258
269
|
- **Zero-arg constructor + `init(args)` lifecycle** — all blocs now use `new Type()` with no constructor args. Blocs that declare `Args` receive them via `init(args)` called by the framework before the first state snapshot.
|
|
259
270
|
- **`args` is required when declared, forbidden when void** — enforced by the type system; no runtime guard needed.
|
|
260
271
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a per-consumer proxy pair for a bloc instance.
|
|
3
|
+
*
|
|
4
|
+
* Returns two proxies:
|
|
5
|
+
* - `proxy` — the stable outer proxy returned to the consumer. Getter
|
|
6
|
+
* properties are invoked with `thisProxy` as `this` so that `this.state`
|
|
7
|
+
* reads inside getters are redirected to the current render's tracking
|
|
8
|
+
* proxy. Non-getter properties fall through to the live instance.
|
|
9
|
+
* - `thisProxy` — the inner `this`-proxy used as the receiver for getter
|
|
10
|
+
* calls. Intercepts `state` to return `trackedStateRef.current` when a
|
|
11
|
+
* tracking context is active; otherwise falls through to the live value.
|
|
12
|
+
* When `onDepHandle` is supplied, a read off `this` whose value carries the
|
|
13
|
+
* `DEP_BRAND` symbol (i.e. a `depend()` handle) is routed through
|
|
14
|
+
* `onDepHandle`, which returns a per-consumer wrapper whose `.track()` opts
|
|
15
|
+
* the consumer into cross-bloc reactivity. Core stays decoupled — detection
|
|
16
|
+
* is purely by the `DEP_BRAND` symbol.
|
|
17
|
+
*
|
|
18
|
+
* Both allocations happen exactly once per bloc acquisition (inside `useMemo`)
|
|
19
|
+
* so the proxies are stable across renders.
|
|
20
|
+
*
|
|
21
|
+
* @param onDepHandle - Optional callback invoked when a getter reads a branded
|
|
22
|
+
* dep handle off `this`. Receives the original handle and returns the value
|
|
23
|
+
* to expose in its place (the session-bound wrapper). The callback is
|
|
24
|
+
* responsible for caching wrappers per handle to avoid re-allocation.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildTrackedProxy<T extends object>(instance: T, trackedStateRef: {
|
|
27
|
+
current: unknown;
|
|
28
|
+
}, onDepHandle?: (handle: object) => unknown): {
|
|
29
|
+
proxy: T;
|
|
30
|
+
thisProxy: T;
|
|
31
|
+
};
|
package/dist/index.cjs
CHANGED
|
@@ -57,8 +57,63 @@ function useProvidedArgs(BlocClass) {
|
|
|
57
57
|
return (0, react.useContext)(ProvidedArgsContext).get(BlocClass);
|
|
58
58
|
}
|
|
59
59
|
//#endregion
|
|
60
|
+
//#region src/buildTrackedProxy.ts
|
|
61
|
+
/**
|
|
62
|
+
* Build a per-consumer proxy pair for a bloc instance.
|
|
63
|
+
*
|
|
64
|
+
* Returns two proxies:
|
|
65
|
+
* - `proxy` — the stable outer proxy returned to the consumer. Getter
|
|
66
|
+
* properties are invoked with `thisProxy` as `this` so that `this.state`
|
|
67
|
+
* reads inside getters are redirected to the current render's tracking
|
|
68
|
+
* proxy. Non-getter properties fall through to the live instance.
|
|
69
|
+
* - `thisProxy` — the inner `this`-proxy used as the receiver for getter
|
|
70
|
+
* calls. Intercepts `state` to return `trackedStateRef.current` when a
|
|
71
|
+
* tracking context is active; otherwise falls through to the live value.
|
|
72
|
+
* When `onDepHandle` is supplied, a read off `this` whose value carries the
|
|
73
|
+
* `DEP_BRAND` symbol (i.e. a `depend()` handle) is routed through
|
|
74
|
+
* `onDepHandle`, which returns a per-consumer wrapper whose `.track()` opts
|
|
75
|
+
* the consumer into cross-bloc reactivity. Core stays decoupled — detection
|
|
76
|
+
* is purely by the `DEP_BRAND` symbol.
|
|
77
|
+
*
|
|
78
|
+
* Both allocations happen exactly once per bloc acquisition (inside `useMemo`)
|
|
79
|
+
* so the proxies are stable across renders.
|
|
80
|
+
*
|
|
81
|
+
* @param onDepHandle - Optional callback invoked when a getter reads a branded
|
|
82
|
+
* dep handle off `this`. Receives the original handle and returns the value
|
|
83
|
+
* to expose in its place (the session-bound wrapper). The callback is
|
|
84
|
+
* responsible for caching wrappers per handle to avoid re-allocation.
|
|
85
|
+
*/
|
|
86
|
+
function buildTrackedProxy(instance, trackedStateRef, onDepHandle) {
|
|
87
|
+
const getterDescs = /* @__PURE__ */ new Map();
|
|
88
|
+
let proto = Object.getPrototypeOf(instance);
|
|
89
|
+
while (proto && proto !== Object.prototype) {
|
|
90
|
+
const keys = [...Object.getOwnPropertyNames(proto), ...Object.getOwnPropertySymbols(proto)];
|
|
91
|
+
for (const key of keys) {
|
|
92
|
+
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
93
|
+
if (desc?.get && !getterDescs.has(key)) getterDescs.set(key, desc);
|
|
94
|
+
}
|
|
95
|
+
proto = Object.getPrototypeOf(proto);
|
|
96
|
+
}
|
|
97
|
+
const thisProxy = new Proxy(instance, { get(t, k, r) {
|
|
98
|
+
if (k === "state") return trackedStateRef.current ?? Reflect.get(t, k, r);
|
|
99
|
+
const value = Reflect.get(t, k, r);
|
|
100
|
+
if (onDepHandle !== void 0 && (typeof value === "function" || typeof value === "object") && value !== null && value[_blac_core.DEP_BRAND] !== void 0) return onDepHandle(value);
|
|
101
|
+
return value;
|
|
102
|
+
} });
|
|
103
|
+
return {
|
|
104
|
+
proxy: new Proxy(instance, { get(target, key, receiver) {
|
|
105
|
+
const desc = getterDescs.get(key);
|
|
106
|
+
if (desc?.get) return desc.get.call(thisProxy);
|
|
107
|
+
return Reflect.get(target, key, receiver);
|
|
108
|
+
} }),
|
|
109
|
+
thisProxy
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
60
113
|
//#region src/useBloc.ts
|
|
61
114
|
let nextConsumerId = 0;
|
|
115
|
+
const primaryRefId = (consumerId) => `useBloc@${consumerId}`;
|
|
116
|
+
const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
62
117
|
/**
|
|
63
118
|
* React hook that connects a component to a state container with automatic
|
|
64
119
|
* re-render on state changes.
|
|
@@ -70,7 +125,7 @@ let nextConsumerId = 0;
|
|
|
70
125
|
* {@link trackRender} + the container's path-scoped `DirtyChannel`.
|
|
71
126
|
* - **Manual select**: pass `options.select` to opt out of auto-tracking.
|
|
72
127
|
* The hook re-renders only when the returned array's elements change
|
|
73
|
-
* (per-index `Object.is`).
|
|
128
|
+
* (per-index `Object.is`).
|
|
74
129
|
*
|
|
75
130
|
* Lifecycle:
|
|
76
131
|
* - The bloc is acquired from the registry on mount and released on
|
|
@@ -97,7 +152,7 @@ let nextConsumerId = 0;
|
|
|
97
152
|
* const [state, bloc] = useBloc(MyBloc);
|
|
98
153
|
* ```
|
|
99
154
|
*
|
|
100
|
-
* @example Manual select
|
|
155
|
+
* @example Manual select
|
|
101
156
|
* ```ts
|
|
102
157
|
* const [state, bloc] = useBloc(MyBloc, {
|
|
103
158
|
* select: (state) => [state.count],
|
|
@@ -128,23 +183,39 @@ function useBloc(BlocClass, options) {
|
|
|
128
183
|
const providerArgs = useProvidedArgs(BlocClass);
|
|
129
184
|
const providerArgsRef = (0, react.useRef)(providerArgs);
|
|
130
185
|
providerArgsRef.current = providerArgs;
|
|
131
|
-
const
|
|
186
|
+
const providerArgsKey = providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs);
|
|
187
|
+
const trackedStateRef = (0, react.useRef)(null);
|
|
188
|
+
const sessionRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
189
|
+
const depSubsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
190
|
+
const depWrapperCacheRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
191
|
+
const { bloc, instanceKey, trackedBloc } = (0, react.useMemo)(() => {
|
|
132
192
|
const effectiveArgs = ownArgsRef.current !== void 0 ? ownArgsRef.current : providerArgsRef.current;
|
|
133
193
|
const resolvedKey = (0, _blac_core.resolveInstanceKey)(BlocClass, effectiveArgs);
|
|
134
|
-
const refId =
|
|
194
|
+
const refId = primaryRefId(consumerId);
|
|
195
|
+
const instance = (0, _blac_core.getRegistry)().acquire(BlocClass, resolvedKey, {
|
|
196
|
+
canCreate: true,
|
|
197
|
+
countRef: true,
|
|
198
|
+
refId,
|
|
199
|
+
args: effectiveArgs
|
|
200
|
+
});
|
|
201
|
+
const onDepHandle = (handle) => {
|
|
202
|
+
const cache = depWrapperCacheRef.current;
|
|
203
|
+
const cached = cache.get(handle);
|
|
204
|
+
if (cached !== void 0) return cached;
|
|
205
|
+
const wrapper = makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, depSubsRef, onDepHandle);
|
|
206
|
+
cache.set(handle, wrapper);
|
|
207
|
+
return wrapper;
|
|
208
|
+
};
|
|
209
|
+
const { proxy } = buildTrackedProxy(instance, trackedStateRef, onDepHandle);
|
|
135
210
|
return {
|
|
136
|
-
bloc:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
refId,
|
|
140
|
-
args: effectiveArgs
|
|
141
|
-
}),
|
|
142
|
-
instanceKey: resolvedKey
|
|
211
|
+
bloc: instance,
|
|
212
|
+
instanceKey: resolvedKey,
|
|
213
|
+
trackedBloc: proxy
|
|
143
214
|
};
|
|
144
215
|
}, [
|
|
145
216
|
BlocClass,
|
|
146
217
|
ownArgsKey,
|
|
147
|
-
|
|
218
|
+
providerArgsKey
|
|
148
219
|
]);
|
|
149
220
|
const [, force] = (0, react.useReducer)((x) => x + 1, 0);
|
|
150
221
|
const pathRef = (0, react.useRef)((0, _dirtytalk_structural.emptyPathSet)());
|
|
@@ -175,8 +246,7 @@ function useBloc(BlocClass, options) {
|
|
|
175
246
|
onMountRef.current?.(bloc);
|
|
176
247
|
return () => {
|
|
177
248
|
onUnmountRef.current?.(bloc);
|
|
178
|
-
|
|
179
|
-
(0, _blac_core.getRegistry)().release(BlocClass, instanceKey, false, refId);
|
|
249
|
+
(0, _blac_core.getRegistry)().release(BlocClass, instanceKey, false, primaryRefId(consumerId));
|
|
180
250
|
};
|
|
181
251
|
}, [bloc, instanceKey]);
|
|
182
252
|
const rawState = bloc.state;
|
|
@@ -187,21 +257,151 @@ function useBloc(BlocClass, options) {
|
|
|
187
257
|
} else {
|
|
188
258
|
const tracked = (0, _dirtytalk_structural.trackRender)(rawState, bloc.interner);
|
|
189
259
|
state = tracked.value;
|
|
260
|
+
trackedStateRef.current = tracked.value;
|
|
190
261
|
pathRef.current = tracked.paths;
|
|
262
|
+
const session = sessionRef.current;
|
|
263
|
+
session.clear();
|
|
264
|
+
session.set(bloc, {
|
|
265
|
+
kind: "primary",
|
|
266
|
+
paths: tracked.paths
|
|
267
|
+
});
|
|
191
268
|
}
|
|
192
269
|
(0, react.useLayoutEffect)(() => {
|
|
270
|
+
trackedStateRef.current = null;
|
|
193
271
|
if (selectRef.current !== void 0) return;
|
|
194
272
|
const container = bloc;
|
|
195
273
|
const paths = pathRef.current;
|
|
196
274
|
container.registerConsumerPaths(consumerId, paths);
|
|
197
275
|
expandedInterestRef.current = expandWithAncestors(paths, container.interner);
|
|
276
|
+
const subs = depSubsRef.current;
|
|
277
|
+
const session = sessionRef.current;
|
|
278
|
+
for (const [depContainer, sub] of subs) if (!session.has(depContainer)) {
|
|
279
|
+
sub.unsubscribe();
|
|
280
|
+
depContainer.unregisterConsumer(consumerId);
|
|
281
|
+
(0, _blac_core.getRegistry)().release(sub.Type, sub.key, false, sub.refId);
|
|
282
|
+
subs.delete(depContainer);
|
|
283
|
+
}
|
|
284
|
+
for (const [depContainer, entry] of session) {
|
|
285
|
+
if (entry.kind === "primary") continue;
|
|
286
|
+
const interest = expandWithAncestors(entry.paths, depContainer.interner);
|
|
287
|
+
depContainer.registerConsumerPaths(consumerId, entry.paths);
|
|
288
|
+
const existing = subs.get(depContainer);
|
|
289
|
+
if (existing) {
|
|
290
|
+
existing.interestRef.current = interest;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const interestRef = { current: interest };
|
|
294
|
+
const unsubscribe = depContainer.channel.subscribe(() => interestRef.current, () => force());
|
|
295
|
+
subs.set(depContainer, {
|
|
296
|
+
unsubscribe,
|
|
297
|
+
interestRef,
|
|
298
|
+
Type: entry.Type,
|
|
299
|
+
key: entry.key,
|
|
300
|
+
refId: entry.refId
|
|
301
|
+
});
|
|
302
|
+
}
|
|
198
303
|
});
|
|
304
|
+
(0, react.useEffect)(() => {
|
|
305
|
+
const subs = depSubsRef.current;
|
|
306
|
+
return () => {
|
|
307
|
+
for (const [depContainer, sub] of subs) {
|
|
308
|
+
sub.unsubscribe();
|
|
309
|
+
depContainer.unregisterConsumer(consumerId);
|
|
310
|
+
(0, _blac_core.getRegistry)().release(sub.Type, sub.key, false, sub.refId);
|
|
311
|
+
}
|
|
312
|
+
subs.clear();
|
|
313
|
+
};
|
|
314
|
+
}, [consumerId]);
|
|
199
315
|
return [
|
|
200
316
|
state,
|
|
201
|
-
|
|
317
|
+
trackedBloc,
|
|
202
318
|
componentRef
|
|
203
319
|
];
|
|
204
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Build the per-consumer wrapper that replaces a branded dep handle inside a
|
|
323
|
+
* tracked getter's `this`. The wrapper exposes the same accessors as the core
|
|
324
|
+
* handle and overrides `.track()`:
|
|
325
|
+
*
|
|
326
|
+
* - **Inside a render** (`trackedStateRef.current != null`): resolve the dep,
|
|
327
|
+
* take a refcount (once per consumer), `trackRender` its state, merge the
|
|
328
|
+
* recorded paths into the session entry, build/reuse a tracked proxy for the
|
|
329
|
+
* dep so its OWN getters track too, and return `[trackedValue, depProxy]`.
|
|
330
|
+
* - **Outside a render**: degrade to live `[dep.state, dep]` — matches the core
|
|
331
|
+
* base impl, safe in event handlers/effects/methods.
|
|
332
|
+
*
|
|
333
|
+
* `.untracked()` always returns the live instance with no subscription.
|
|
334
|
+
*
|
|
335
|
+
* Args resolve at call time (`options.args ?? defaultArgs`), so a single handle
|
|
336
|
+
* can resolve different dep instances across calls; tracked-proxy state is
|
|
337
|
+
* therefore cached per resolved instance, not per handle. Guards against a
|
|
338
|
+
* container re-entering tracking within the same render (mutual A↔B deps): if
|
|
339
|
+
* the dep already has a non-primary session entry this render, reuse its proxy
|
|
340
|
+
* + union its paths instead of re-acquiring.
|
|
341
|
+
*/
|
|
342
|
+
function makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, depSubsRef, onDepHandle) {
|
|
343
|
+
const brand = handle[_blac_core.DEP_BRAND];
|
|
344
|
+
const refId = depRefId(consumerId);
|
|
345
|
+
const registry = (0, _blac_core.getRegistry)();
|
|
346
|
+
const perDep = /* @__PURE__ */ new Map();
|
|
347
|
+
const resolve = (options) => {
|
|
348
|
+
const args = options?.args ?? brand.defaultArgs;
|
|
349
|
+
const key = registry.resolveKey(brand.Type, void 0, args);
|
|
350
|
+
return {
|
|
351
|
+
dep: registry.ensure(brand.Type, key, args),
|
|
352
|
+
key,
|
|
353
|
+
args
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
const wrapper = {
|
|
357
|
+
untracked: (options) => resolve(options).dep,
|
|
358
|
+
track: (options) => {
|
|
359
|
+
const { dep, key, args } = resolve(options);
|
|
360
|
+
if (trackedStateRef.current == null) return [dep.state, dep];
|
|
361
|
+
const session = sessionRef.current;
|
|
362
|
+
const existing = session.get(dep);
|
|
363
|
+
if (existing === void 0 && !depSubsRef.current.has(dep)) registry.acquire(brand.Type, key, {
|
|
364
|
+
canCreate: true,
|
|
365
|
+
countRef: true,
|
|
366
|
+
refId,
|
|
367
|
+
args
|
|
368
|
+
});
|
|
369
|
+
const tracked = (0, _dirtytalk_structural.trackRender)(dep.state, dep.interner);
|
|
370
|
+
let cache = perDep.get(dep);
|
|
371
|
+
if (cache === void 0) {
|
|
372
|
+
const ref = { current: tracked.value };
|
|
373
|
+
cache = {
|
|
374
|
+
ref,
|
|
375
|
+
proxy: buildTrackedProxy(dep, ref, onDepHandle).proxy
|
|
376
|
+
};
|
|
377
|
+
perDep.set(dep, cache);
|
|
378
|
+
} else cache.ref.current = tracked.value;
|
|
379
|
+
if (existing !== void 0) existing.paths = unionPaths(existing.paths, tracked.paths);
|
|
380
|
+
else session.set(dep, {
|
|
381
|
+
kind: "dep",
|
|
382
|
+
paths: tracked.paths,
|
|
383
|
+
Type: brand.Type,
|
|
384
|
+
key,
|
|
385
|
+
refId
|
|
386
|
+
});
|
|
387
|
+
return [tracked.value, cache.proxy];
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
Object.defineProperty(wrapper, _blac_core.DEP_BRAND, {
|
|
391
|
+
value: brand,
|
|
392
|
+
enumerable: false,
|
|
393
|
+
writable: false,
|
|
394
|
+
configurable: false
|
|
395
|
+
});
|
|
396
|
+
return wrapper;
|
|
397
|
+
}
|
|
398
|
+
/** Union two PathSets (ALL_PATHS dominates). */
|
|
399
|
+
function unionPaths(a, b) {
|
|
400
|
+
if (a === _dirtytalk_structural.ALL_PATHS || b === _dirtytalk_structural.ALL_PATHS) return _dirtytalk_structural.ALL_PATHS;
|
|
401
|
+
const out = new Set(a);
|
|
402
|
+
for (const id of b) out.add(id);
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
205
405
|
const shallowArrayEqual = (a, b) => {
|
|
206
406
|
if (a === b) return true;
|
|
207
407
|
if (a.length !== b.length) return false;
|
|
@@ -209,19 +409,26 @@ const shallowArrayEqual = (a, b) => {
|
|
|
209
409
|
return true;
|
|
210
410
|
};
|
|
211
411
|
/**
|
|
212
|
-
* Expand a PathSet to include
|
|
412
|
+
* Expand a PathSet to include an *ancestor-watch* id for every ancestor of
|
|
413
|
+
* every tracked leaf.
|
|
213
414
|
*
|
|
214
415
|
* The auto-tracker records leaf paths (e.g. `'items.length'`), but
|
|
215
|
-
* `StructuralContainer.patch`
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
416
|
+
* `StructuralContainer.patch` can only mark the parent (`'items'`) when it
|
|
417
|
+
* replaces a value atomically (arrays, `null`, primitives — it can't see
|
|
418
|
+
* inside). Without expansion, a subscriber with interest `{'items.length'}`
|
|
419
|
+
* would miss a `patch`-triggered atomic-replacement of `items`.
|
|
420
|
+
*
|
|
421
|
+
* Ancestors are added under the interner's *ancestor-watch* lane
|
|
422
|
+
* (`internAncestor`), NOT as normal ids. The source emits a matching
|
|
423
|
+
* ancestor-watch mark only for paths it replaces atomically — never for a
|
|
424
|
+
* plain-object structural pulse-up. So `{'items.length'}` wakes when the array
|
|
425
|
+
* `items` is replaced, but `{'user.email'}` does NOT wake when a sibling
|
|
426
|
+
* `user.name` changes and pulses `user` up: pulse-up `user` is a normal id and
|
|
427
|
+
* the ancestor-watch `user` only intersects another ancestor-watch `user`.
|
|
220
428
|
*
|
|
221
|
-
* Example:
|
|
222
|
-
* root
|
|
223
|
-
*
|
|
224
|
-
* wake this consumer regardless of what it actually reads).
|
|
429
|
+
* Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but
|
|
430
|
+
* NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,
|
|
431
|
+
* and `''` would wake this consumer on every field change).
|
|
225
432
|
*/
|
|
226
433
|
function expandWithAncestors(paths, interner) {
|
|
227
434
|
if (paths === _dirtytalk_structural.ALL_PATHS) return _dirtytalk_structural.ALL_PATHS;
|
|
@@ -233,7 +440,7 @@ function expandWithAncestors(paths, interner) {
|
|
|
233
440
|
let idx = str.lastIndexOf(".");
|
|
234
441
|
while (idx > 0) {
|
|
235
442
|
const ancestor = str.slice(0, idx);
|
|
236
|
-
expanded.add(interner.
|
|
443
|
+
expanded.add(interner.internAncestor(ancestor));
|
|
237
444
|
idx = ancestor.lastIndexOf(".");
|
|
238
445
|
}
|
|
239
446
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["ALL_PATHS"],"sources":["../src/BlocProvider.tsx","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\n\n/**\n * Internal context value: a Map from bloc constructor to the args object\n * provided by the nearest BlocProvider for that bloc class.\n *\n * Stored as a WeakMap-keyed record so nested providers for different blocs\n * compose without clobbering each other.\n */\ntype ProvidedArgsMap = Map<StateContainerConstructor, unknown>;\n\nconst ProvidedArgsContext = createContext<ProvidedArgsMap>(new Map());\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps<T extends StateContainerConstructor> {\n /**\n * The bloc class whose args are being provided to descendants.\n */\n bloc: T;\n /**\n * Args that descendant `useBloc(bloc)` calls will resolve to when no\n * own `args` are given.\n */\n args: ExtractArgs<T>;\n children: ReactNode;\n}\n\n/**\n * Provides args to descendant `useBloc` calls for a specific bloc class via\n * React context.\n *\n * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the\n * args supplied here. Own `args` on the `useBloc` call always win.\n *\n * Multiple `BlocProvider` wrappers for different bloc classes compose: each\n * provider merges its entry into the inherited map, so nested providers for\n * different blocs do not interfere.\n *\n * @example\n * ```tsx\n * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>\n * <UserProfile />\n * </BlocProvider>\n * ```\n *\n * @example Per-mount private instance\n * ```tsx\n * const id = useId();\n * <BlocProvider bloc={CartBloc} args={{ _id: id }}>\n * <CartWidget />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider<T extends StateContainerConstructor>({\n bloc,\n args,\n children,\n}: BlocProviderProps<T>): ReactElement {\n const parentMap = useContext(ProvidedArgsContext);\n\n // Merge our entry into a new Map so sibling/parent providers for other blocs\n // are preserved. Memoised on (parentMap, bloc, args) identity.\n const mergedMap = useMemo(() => {\n const next = new Map(parentMap);\n next.set(bloc, args);\n return next;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parentMap, bloc, args]);\n\n return (\n <ProvidedArgsContext.Provider value={mergedMap}>\n {children}\n </ProvidedArgsContext.Provider>\n );\n}\n\n/**\n * Returns the args provided by the nearest {@link BlocProvider} for the given\n * bloc class, or `undefined` when called outside a matching provider.\n *\n * Used by `useBloc` to inherit provider args when no own `args` are given.\n */\nexport function useProvidedArgs<T extends StateContainerConstructor>(\n BlocClass: T,\n): ExtractArgs<T> | undefined {\n const map = useContext(ProvidedArgsContext);\n return map.get(BlocClass) as ExtractArgs<T> | undefined;\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n getRegistry,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n trackRender,\n PathInterner,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useProvidedArgs } from './BlocProvider';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`). This replaces the v1 `dependencies` option.\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is derived from `options.args` (own args),\n * then the surrounding {@link BlocProvider} context args for this bloc,\n * then the default key (no args).\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * Per-mount private instance:\n * ```ts\n * const id = useId();\n * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });\n * ```\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select (replaces v1 `dependencies`)\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Args-based shared instance\n * ```ts\n * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Priority: own args > provider args (for this bloc class) > none.\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const ownArgs = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const ownArgsRef = useRef(ownArgs);\n ownArgsRef.current = ownArgs;\n const ownArgsKey =\n ownArgs === undefined ? undefined : JSON.stringify(ownArgs);\n\n // Read provided args from the nearest BlocProvider for this bloc class.\n const providerArgs = useProvidedArgs(BlocClass);\n const providerArgsRef = useRef(providerArgs);\n providerArgsRef.current = providerArgs;\n const providerArgsKey =\n providerArgs === undefined ? undefined : JSON.stringify(providerArgs);\n\n const { bloc, instanceKey } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n }>(() => {\n // Own args win over provider args; provider args win over no args.\n const effectiveArgs =\n ownArgsRef.current !== undefined\n ? ownArgsRef.current\n : providerArgsRef.current;\n\n const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);\n const refId = `useBloc@${consumerId}`;\n const registry = getRegistry();\n const instance = registry.acquire(BlocClass, resolvedKey, {\n canCreate: true,\n countRef: true,\n refId,\n args: effectiveArgs,\n }) as TBloc;\n return { bloc: instance, instanceKey: resolvedKey };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, ownArgsKey, providerArgsKey]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We bypass `bloc.subscribe(listener)` (the legacy listener surface, which\n // StateContainer overrides) and talk directly to `bloc.channel`. The channel\n // is the StructuralContainer's path-scoped DirtyChannel; subscribing with a\n // dynamic interest function lets us narrow wakeups per consumer.\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // Expanded interest: leaf paths PLUS their ancestor paths, so that when\n // `patch` marks a parent (e.g. 'items') and the consumer tracked a child\n // (e.g. 'items.length'), the intersection still fires. Updated in\n // useLayoutEffect after each render once pathRef.current is populated.\n const expandedInterestRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n return unsub;\n }\n\n // Auto-track mode: subscribe with the expanded interest (leaf paths + their\n // ancestors). This ensures that when `patch` marks a parent path (e.g.\n // 'items') and the consumer tracked a child (e.g. 'items.length'), the\n // channel intersection still fires a re-render.\n //\n // `expandedInterestRef.current` is updated by the useLayoutEffect below\n // after each render, so the interest is always fresh at flush time.\n const unsub = channel.subscribe(\n () => expandedInterestRef.current,\n () => force(),\n );\n // Register the consumer's leaf paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Mount / unmount lifecycle.\n //\n // Order on unmount: onUnmount(bloc) -> release(...). The bloc must still be\n // alive when onUnmount runs (release may dispose it).\n // ---------------------------------------------------------------------------\n useEffect(() => {\n onMountRef.current?.(bloc as InstanceType<T>);\n return () => {\n onUnmountRef.current?.(bloc as InstanceType<T>);\n const refId = `useBloc@${consumerId}`;\n getRegistry().release(BlocClass, instanceKey, false, refId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n );\n state = tracked.value as ExtractState<T>;\n pathRef.current = tracked.paths;\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits, pathRef.current is the consumer's actual leaf\n // interest (populated by the proxy during JSX evaluation). Re-register with\n // the container so the skeleton reflects the latest paths, and expand the\n // interest to include ancestor paths for the channel subscription.\n //\n // useLayoutEffect runs before the browser paints (and before any emit\n // triggered by another effect), so the skeleton is fresh by the time the\n // next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n if (selectRef.current !== undefined) return;\n const container = bloc as unknown as StateContainer;\n const paths = pathRef.current;\n container.registerConsumerPaths(consumerId, paths);\n // Expand interest to include ancestor paths so that `patch`-marked parent\n // paths (e.g. 'items') wake a consumer that tracked a child (e.g.\n // 'items.length').\n expandedInterestRef.current = expandWithAncestors(\n paths,\n container.interner,\n );\n });\n\n return [\n state,\n bloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n\n/**\n * Expand a PathSet to include all ancestor paths of every tracked path.\n *\n * The auto-tracker records leaf paths (e.g. `'items.length'`), but\n * `StructuralContainer.patch` marks parent paths (e.g. `'items'`) since it\n * only walks the patch shape — not individual array elements or nested\n * descendants. Without ancestor expansion, a channel subscriber with interest\n * `{'items.length'}` would miss a `patch`-triggered `dirty={'items'}` because\n * `intersects({'items.length'}, {'items'}) = false`.\n *\n * Example: for leaf path `'a.b.c'`, adds `'a.b'` and `'a'` (but NOT `''`\n * root, since a root change would be captured by ALL_PATHS from the source\n * when there are ≤1 consumers, and adding `''` would make every field change\n * wake this consumer regardless of what it actually reads).\n */\nfunction expandWithAncestors(paths: PathSet, interner: PathInterner): PathSet {\n if (paths === ALL_PATHS) return ALL_PATHS;\n const leafPaths = paths as Set<number>;\n if (leafPaths.size === 0) return paths;\n\n const expanded = new Set<number>(leafPaths);\n for (const id of leafPaths) {\n const str = interner.lookup(id);\n // Add all non-root ancestor segments: 'a.b.c' → add 'a.b', 'a'\n let idx = str.lastIndexOf('.');\n while (idx > 0) {\n const ancestor = str.slice(0, idx);\n expanded.add(interner.intern(ancestor));\n idx = ancestor.lastIndexOf('.');\n }\n }\n return expanded;\n}\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["DEP_BRAND","ALL_PATHS","DEP_BRAND"],"sources":["../src/BlocProvider.tsx","../src/buildTrackedProxy.ts","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\n\n/**\n * Internal context value: a Map from bloc constructor to the args object\n * provided by the nearest BlocProvider for that bloc class.\n *\n * Stored as a WeakMap-keyed record so nested providers for different blocs\n * compose without clobbering each other.\n */\ntype ProvidedArgsMap = Map<StateContainerConstructor, unknown>;\n\nconst ProvidedArgsContext = createContext<ProvidedArgsMap>(new Map());\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps<T extends StateContainerConstructor> {\n /**\n * The bloc class whose args are being provided to descendants.\n */\n bloc: T;\n /**\n * Args that descendant `useBloc(bloc)` calls will resolve to when no\n * own `args` are given.\n */\n args: ExtractArgs<T>;\n children: ReactNode;\n}\n\n/**\n * Provides args to descendant `useBloc` calls for a specific bloc class via\n * React context.\n *\n * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the\n * args supplied here. Own `args` on the `useBloc` call always win.\n *\n * Multiple `BlocProvider` wrappers for different bloc classes compose: each\n * provider merges its entry into the inherited map, so nested providers for\n * different blocs do not interfere.\n *\n * @example\n * ```tsx\n * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>\n * <UserProfile />\n * </BlocProvider>\n * ```\n *\n * @example Per-mount private instance\n * ```tsx\n * const id = useId();\n * <BlocProvider bloc={CartBloc} args={{ _id: id }}>\n * <CartWidget />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider<T extends StateContainerConstructor>({\n bloc,\n args,\n children,\n}: BlocProviderProps<T>): ReactElement {\n const parentMap = useContext(ProvidedArgsContext);\n\n // Merge our entry into a new Map so sibling/parent providers for other blocs\n // are preserved. Memoised on (parentMap, bloc, args) identity.\n const mergedMap = useMemo(() => {\n const next = new Map(parentMap);\n next.set(bloc, args);\n return next;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parentMap, bloc, args]);\n\n return (\n <ProvidedArgsContext.Provider value={mergedMap}>\n {children}\n </ProvidedArgsContext.Provider>\n );\n}\n\n/**\n * Returns the args provided by the nearest {@link BlocProvider} for the given\n * bloc class, or `undefined` when called outside a matching provider.\n *\n * Used by `useBloc` to inherit provider args when no own `args` are given.\n */\nexport function useProvidedArgs<T extends StateContainerConstructor>(\n BlocClass: T,\n): ExtractArgs<T> | undefined {\n const map = useContext(ProvidedArgsContext);\n return map.get(BlocClass) as ExtractArgs<T> | undefined;\n}\n","import { DEP_BRAND } from '@blac/core';\n\n/**\n * Build a per-consumer proxy pair for a bloc instance.\n *\n * Returns two proxies:\n * - `proxy` — the stable outer proxy returned to the consumer. Getter\n * properties are invoked with `thisProxy` as `this` so that `this.state`\n * reads inside getters are redirected to the current render's tracking\n * proxy. Non-getter properties fall through to the live instance.\n * - `thisProxy` — the inner `this`-proxy used as the receiver for getter\n * calls. Intercepts `state` to return `trackedStateRef.current` when a\n * tracking context is active; otherwise falls through to the live value.\n * When `onDepHandle` is supplied, a read off `this` whose value carries the\n * `DEP_BRAND` symbol (i.e. a `depend()` handle) is routed through\n * `onDepHandle`, which returns a per-consumer wrapper whose `.track()` opts\n * the consumer into cross-bloc reactivity. Core stays decoupled — detection\n * is purely by the `DEP_BRAND` symbol.\n *\n * Both allocations happen exactly once per bloc acquisition (inside `useMemo`)\n * so the proxies are stable across renders.\n *\n * @param onDepHandle - Optional callback invoked when a getter reads a branded\n * dep handle off `this`. Receives the original handle and returns the value\n * to expose in its place (the session-bound wrapper). The callback is\n * responsible for caching wrappers per handle to avoid re-allocation.\n */\nexport function buildTrackedProxy<T extends object>(\n instance: T,\n trackedStateRef: { current: unknown },\n onDepHandle?: (handle: object) => unknown,\n): { proxy: T; thisProxy: T } {\n // Build a map of getter descriptors from the prototype chain (excluding\n // Object.prototype). This is computed once per bloc acquisition so that\n // the proxy's get trap is O(1) per property access. Both string- and\n // symbol-keyed getters are collected. Arrow-function class properties\n // (own, bound in the constructor) are not getters and pass through\n // unmodified.\n const getterDescs = new Map<string | symbol, PropertyDescriptor>();\n let proto = Object.getPrototypeOf(instance);\n while (proto && proto !== Object.prototype) {\n const keys: (string | symbol)[] = [\n ...Object.getOwnPropertyNames(proto),\n ...Object.getOwnPropertySymbols(proto),\n ];\n for (const key of keys) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc?.get && !getterDescs.has(key)) getterDescs.set(key, desc);\n }\n proto = Object.getPrototypeOf(proto);\n }\n\n // `this`-proxy for getter invocations, allocated ONCE per acquisition (the\n // trap closes over the stable `trackedStateRef`, so it never needs to be\n // rebuilt per access). Redirects `this.state` to the current render's\n // tracking proxy so getter reads during JSX record paths; outside render\n // `trackedStateRef.current` is null and it falls through to live state.\n // The receiver `r` (this proxy) is threaded through Reflect.get so chained\n // getter calls (getters reading other getters) stay in tracked context.\n const thisProxy = new Proxy(instance as object, {\n get(t, k, r) {\n if (k === 'state') return trackedStateRef.current ?? Reflect.get(t, k, r);\n const value = Reflect.get(t, k, r);\n // A branded dep handle read off `this` (e.g. `this.price`) is routed\n // through onDepHandle so the consumer's session can wrap `.track()`.\n if (\n onDepHandle !== undefined &&\n (typeof value === 'function' || typeof value === 'object') &&\n value !== null &&\n (value as Record<symbol, unknown>)[DEP_BRAND] !== undefined\n ) {\n return onDepHandle(value as object);\n }\n return value;\n },\n });\n\n // Stable proxy: one allocation per bloc acquisition. Non-getter access is\n // a single Map lookup + Reflect.get — no prototype walk on the hot path.\n const proxy = new Proxy(instance as object, {\n get(target, key, receiver) {\n const desc = getterDescs.get(key);\n if (desc?.get) return desc.get.call(thisProxy);\n return Reflect.get(target, key, receiver);\n },\n }) as T;\n\n return { proxy, thisProxy: thisProxy as T };\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n DEP_BRAND,\n getRegistry,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n trackRender,\n PathInterner,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useProvidedArgs } from './BlocProvider';\nimport { buildTrackedProxy } from './buildTrackedProxy';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n// Registry refId formats for a consumer's primary bloc and its tracked deps.\n// Centralised so the `acquire` and `release` sites can never drift apart — a\n// mismatch would leak the ref and keep the bloc alive past unmount.\nconst primaryRefId = (consumerId: string): string => `useBloc@${consumerId}`;\nconst depRefId = (consumerId: string): string => `useBloc@${consumerId}:dep`;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`).\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is derived from `options.args` (own args),\n * then the surrounding {@link BlocProvider} context args for this bloc,\n * then the default key (no args).\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * Per-mount private instance:\n * ```ts\n * const id = useId();\n * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });\n * ```\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Args-based shared instance\n * ```ts\n * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Priority: own args > provider args (for this bloc class) > none.\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const ownArgs = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const ownArgsRef = useRef(ownArgs);\n ownArgsRef.current = ownArgs;\n const ownArgsKey =\n ownArgs === undefined ? undefined : JSON.stringify(ownArgs);\n\n // Read provided args from the nearest BlocProvider for this bloc class.\n const providerArgs = useProvidedArgs(BlocClass);\n const providerArgsRef = useRef(providerArgs);\n providerArgsRef.current = providerArgs;\n const providerArgsKey =\n providerArgs === undefined ? undefined : JSON.stringify(providerArgs);\n\n // Current render's tracking proxy. Declared before the memo so the stable\n // ref object can be passed to buildTrackedProxy at acquisition time. The\n // proxy trap only reads `.current` at invocation time (not during creation),\n // so it is safe to pass on first mount even though the value is null.\n // Populated during each auto-track render snapshot (below); cleared to null\n // by useLayoutEffect after commit.\n const trackedStateRef = useRef<unknown>(null);\n\n // ---------------------------------------------------------------------------\n // Per-consumer cross-bloc session.\n //\n // Each render rebuilds a map of every container this consumer is currently\n // interested in. The PRIMARY bloc is the first uniform entry; every dep\n // reached through `this.<handle>.track()` inside a tracked getter adds an\n // entry. The layout-effect reconcile (below) diffs this map vs the previous\n // render to subscribe new deps and release dropped ones. The session lives in\n // this hook's refs only — there is no global ambient state, so sibling\n // renders never cross-contaminate.\n // ---------------------------------------------------------------------------\n const sessionRef = useRef<Map<StateContainer, SessionEntry>>(new Map());\n // Channel subscriptions for DEP containers (the primary keeps its own\n // dedicated effect). container -> { unsubscribe, interestRef, refId held }.\n const depSubsRef = useRef<Map<StateContainer, DepSub>>(new Map());\n // Per-handle wrapper cache, allocated once per bloc acquisition (in the memo)\n // so wrappers are stable across renders. handle -> session-bound wrapper.\n const depWrapperCacheRef = useRef<Map<object, unknown>>(new Map());\n\n const { bloc, instanceKey, trackedBloc } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n trackedBloc: TBloc;\n }>(() => {\n // Own args win over provider args; provider args win over no args.\n const effectiveArgs =\n ownArgsRef.current !== undefined\n ? ownArgsRef.current\n : providerArgsRef.current;\n\n const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);\n const refId = primaryRefId(consumerId);\n const registry = getRegistry();\n const instance = registry.acquire(BlocClass, resolvedKey, {\n canCreate: true,\n countRef: true,\n refId,\n args: effectiveArgs,\n }) as TBloc;\n\n // Build a session-bound wrapper for a dep handle the first time a getter\n // reads it off `this`; cache per handle so the wrapper identity is stable.\n // `onDepHandle` is threaded into each dep's tracked proxy too, so a nested\n // `this.<otherHandle>.track()` inside a dep's getter records into the SAME\n // consumer session — that is what makes deep chains (A→B→C) reactive.\n const onDepHandle = (handle: object): unknown => {\n const cache = depWrapperCacheRef.current;\n const cached = cache.get(handle);\n if (cached !== undefined) return cached;\n const wrapper = makeDepWrapper(\n handle as DepHandleLike,\n consumerId,\n trackedStateRef,\n sessionRef,\n depSubsRef,\n onDepHandle,\n );\n cache.set(handle, wrapper);\n return wrapper;\n };\n\n const { proxy } = buildTrackedProxy(\n instance as object,\n trackedStateRef,\n onDepHandle,\n );\n\n return {\n bloc: instance,\n instanceKey: resolvedKey,\n trackedBloc: proxy as TBloc,\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, ownArgsKey, providerArgsKey]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We talk directly to `bloc.channel` — the StructuralContainer's path-scoped\n // DirtyChannel. Subscribing with a dynamic interest function lets us narrow\n // wakeups per consumer, so a component only re-renders when a path it\n // actually read changes (rather than on every state change).\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // Expanded interest: leaf paths PLUS *ancestor-watch* ids for their parents,\n // so that when `patch` atomically replaces a parent (e.g. the array 'items')\n // and the consumer tracked a child (e.g. 'items.length'), the intersection\n // still fires — while a structural pulse-up of a plain-object parent (e.g.\n // 'user' when a sibling 'user.name' changed) does NOT, because pulse-up marks\n // are normal ids and ancestor-watch ids only intersect their own lane.\n // Updated in useLayoutEffect after each render once pathRef.current is\n // populated.\n const expandedInterestRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n return unsub;\n }\n\n // Auto-track mode: subscribe with the expanded interest (leaf paths + their\n // ancestors). This ensures that when `patch` marks a parent path (e.g.\n // 'items') and the consumer tracked a child (e.g. 'items.length'), the\n // channel intersection still fires a re-render.\n //\n // `expandedInterestRef.current` is updated by the useLayoutEffect below\n // after each render, so the interest is always fresh at flush time.\n const unsub = channel.subscribe(\n () => expandedInterestRef.current,\n () => force(),\n );\n // Register the consumer's leaf paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Mount / unmount lifecycle.\n //\n // Order on unmount: onUnmount(bloc) -> release(...). The bloc must still be\n // alive when onUnmount runs (release may dispose it).\n // ---------------------------------------------------------------------------\n useEffect(() => {\n onMountRef.current?.(bloc as InstanceType<T>);\n return () => {\n onUnmountRef.current?.(bloc as InstanceType<T>);\n getRegistry().release(\n BlocClass,\n instanceKey,\n false,\n primaryRefId(consumerId),\n );\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n );\n state = tracked.value as ExtractState<T>;\n trackedStateRef.current = tracked.value;\n pathRef.current = tracked.paths;\n // Rebuild the per-consumer session for this render. The primary bloc is the\n // first uniform entry; its `paths` are the SAME PathSet object the proxy\n // mutates during JSX (so it stays live as getters record leaves). Dep\n // entries are appended during JSX as `this.<handle>.track()` runs. Cleared\n // here (not in the layout effect) so a render that no longer tracks a dep\n // produces a session without it, and the reconcile drops it.\n const session = sessionRef.current;\n session.clear();\n session.set(bloc as unknown as StateContainer, {\n kind: 'primary',\n paths: tracked.paths,\n });\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits, pathRef.current is the consumer's actual leaf\n // interest (populated by the proxy during JSX evaluation). Re-register with\n // the container so the skeleton reflects the latest paths, and expand the\n // interest to include ancestor paths for the channel subscription.\n //\n // useLayoutEffect runs before the browser paints (and before any emit\n // triggered by another effect), so the skeleton is fresh by the time the\n // next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n // Clear the render-time tracking proxy now that JSX has been evaluated and\n // committed. Getters invoked after this point (event handlers, effects,\n // method→getter chains) fall through to live state instead of reading this\n // render's frozen snapshot. The render body re-seeds it next render.\n trackedStateRef.current = null;\n if (selectRef.current !== undefined) return;\n const container = bloc as unknown as StateContainer;\n const paths = pathRef.current;\n container.registerConsumerPaths(consumerId, paths);\n // Register the *normal* leaf paths above for the source-side skeleton, then\n // build the channel interest as leaves + ancestor-watch ids so that an\n // atomic `patch` replacement of a parent (e.g. the array 'items') wakes a\n // consumer that tracked a child (e.g. 'items.length').\n expandedInterestRef.current = expandWithAncestors(\n paths,\n container.interner,\n );\n\n // -----------------------------------------------------------------------\n // Reconcile DEP containers (cross-bloc `.track()` interest).\n //\n // The primary bloc keeps its own dedicated subscription effect above; this\n // block manages only the *dep* containers recorded in the session this\n // render. We diff the new dep set vs the previously-subscribed set:\n // - new dep -> acquire was already done in `.track()`; subscribe its\n // channel + registerConsumerPaths + seed interest.\n // - surviving -> refresh its interest ref (subscribe closure reads it).\n // - dropped -> unsubscribe, unregisterConsumer, and release its ref.\n // -----------------------------------------------------------------------\n const subs = depSubsRef.current;\n const session = sessionRef.current;\n\n // Pass 1: drop containers no longer in the session.\n for (const [depContainer, sub] of subs) {\n if (!session.has(depContainer)) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n subs.delete(depContainer);\n }\n }\n\n // Pass 2: add/refresh containers in the session (skip the primary).\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n const interest = expandWithAncestors(entry.paths, depContainer.interner);\n depContainer.registerConsumerPaths(consumerId, entry.paths);\n const existing = subs.get(depContainer);\n if (existing) {\n existing.interestRef.current = interest;\n continue;\n }\n const interestRef: { current: PathSet } = { current: interest };\n const unsubscribe = depContainer.channel.subscribe(\n () => interestRef.current,\n () => force(),\n );\n subs.set(depContainer, {\n unsubscribe,\n interestRef,\n Type: entry.Type,\n key: entry.key,\n refId: entry.refId,\n });\n }\n });\n\n // Unmount: tear down every dep subscription + ref exactly once. Kept in its\n // own effect (consumerId is stable for the component's lifetime, so this only\n // runs on final unmount, not on every reconcile). depSubsRef is mutated in\n // place by the reconcile, so the captured Map reference still holds the live\n // set at unmount.\n useEffect(() => {\n // Capture the ref's Map (mutated in place across renders) so the cleanup\n // reads the captured reference rather than depSubsRef.current directly.\n const subs = depSubsRef.current;\n return () => {\n for (const [depContainer, sub] of subs) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n }\n subs.clear();\n };\n }, [consumerId]);\n\n return [\n state,\n trackedBloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\n// ---------------------------------------------------------------------------\n// Cross-bloc session types + dep-handle wrapper.\n// ---------------------------------------------------------------------------\n\n/**\n * One entry in a consumer's per-render session map. Discriminated on `kind`:\n * the primary bloc is managed by its own dedicated effect, while dep entries\n * carry the registry coordinates the reconcile needs to release their ref.\n */\ntype SessionEntry =\n | {\n kind: 'primary';\n /** Tracked leaf paths recorded against the primary this render. */\n paths: PathSet;\n }\n | {\n kind: 'dep';\n /** Tracked leaf paths recorded against this dep this render. */\n paths: PathSet;\n /** Constructor for registry release. */\n Type: StateContainerConstructor;\n /** Resolved instance key for registry release. */\n key: string;\n /** refId held for this dep (released on drop/unmount). */\n refId: string;\n };\n\n/** A live dep-channel subscription tracked between renders for reconciliation. */\ninterface DepSub {\n unsubscribe: () => void;\n interestRef: { current: PathSet };\n Type: StateContainerConstructor;\n key: string;\n refId: string;\n}\n\n/** Per-access options shared by both dep accessors. */\ninterface DepAccessOptionsLike {\n args?: unknown;\n}\n\n/** Structural shape of a branded `depend()` handle as seen from React. */\ninterface DepHandleLike {\n track(options?: DepAccessOptionsLike): [unknown, StateContainer];\n untracked(options?: DepAccessOptionsLike): StateContainer;\n readonly [DEP_BRAND]: {\n Type: StateContainerConstructor;\n defaultArgs?: unknown;\n };\n}\n\n/**\n * Build the per-consumer wrapper that replaces a branded dep handle inside a\n * tracked getter's `this`. The wrapper exposes the same accessors as the core\n * handle and overrides `.track()`:\n *\n * - **Inside a render** (`trackedStateRef.current != null`): resolve the dep,\n * take a refcount (once per consumer), `trackRender` its state, merge the\n * recorded paths into the session entry, build/reuse a tracked proxy for the\n * dep so its OWN getters track too, and return `[trackedValue, depProxy]`.\n * - **Outside a render**: degrade to live `[dep.state, dep]` — matches the core\n * base impl, safe in event handlers/effects/methods.\n *\n * `.untracked()` always returns the live instance with no subscription.\n *\n * Args resolve at call time (`options.args ?? defaultArgs`), so a single handle\n * can resolve different dep instances across calls; tracked-proxy state is\n * therefore cached per resolved instance, not per handle. Guards against a\n * container re-entering tracking within the same render (mutual A↔B deps): if\n * the dep already has a non-primary session entry this render, reuse its proxy\n * + union its paths instead of re-acquiring.\n */\nfunction makeDepWrapper(\n handle: DepHandleLike,\n consumerId: string,\n trackedStateRef: { current: unknown },\n sessionRef: { current: Map<StateContainer, SessionEntry> },\n depSubsRef: { current: Map<StateContainer, DepSub> },\n onDepHandle: (handle: object) => unknown,\n): DepHandleLike {\n const brand = handle[DEP_BRAND];\n const refId = depRefId(consumerId);\n const registry = getRegistry();\n // Per-resolved-instance tracked-state ref + proxy. Call-time args mean one\n // handle can resolve several instances, so cache is keyed by the instance.\n const perDep = new Map<\n StateContainer,\n { ref: { current: unknown }; proxy: StateContainer }\n >();\n\n const resolve = (options?: DepAccessOptionsLike) => {\n const args = options?.args ?? brand.defaultArgs;\n const key = registry.resolveKey(brand.Type, undefined, args);\n const dep = registry.ensure(\n brand.Type,\n key,\n args,\n ) as unknown as StateContainer;\n return { dep, key, args };\n };\n\n const wrapper = {\n untracked: (options?: DepAccessOptionsLike) => resolve(options).dep,\n track: (options?: DepAccessOptionsLike) => {\n const { dep, key, args } = resolve(options);\n\n // Outside a render: live values, no subscription (core base behavior).\n if (trackedStateRef.current == null) {\n return [dep.state, dep];\n }\n\n const session = sessionRef.current;\n const existing = session.get(dep);\n\n // Take a refcount the FIRST time this dep is tracked, held across renders\n // and released by the reconcile (on drop) or unmount. The session map is\n // rebuilt every render, so it can't tell us whether we already hold a ref;\n // `depSubsRef` does — it persists across renders and is populated by the\n // reconcile after the first tracking render. If neither the (cleared) per-\n // render session nor the persistent sub set knows this dep, acquire once.\n // React runs the layout-effect reconcile before the next render, so the\n // sub is registered before a subsequent track sees it → no double-acquire.\n if (existing === undefined && !depSubsRef.current.has(dep)) {\n registry.acquire(brand.Type, key, {\n canCreate: true,\n countRef: true,\n refId,\n args,\n });\n }\n\n const tracked = trackRender(dep.state, dep.interner);\n let cache = perDep.get(dep);\n if (cache === undefined) {\n const ref = { current: tracked.value as unknown };\n cache = { ref, proxy: buildTrackedProxy(dep, ref, onDepHandle).proxy };\n perDep.set(dep, cache);\n } else {\n cache.ref.current = tracked.value;\n }\n\n if (existing !== undefined) {\n // Re-entry this render (`.track()` twice, or a mutual cycle): union the\n // new paths into the existing entry rather than re-acquiring.\n existing.paths = unionPaths(existing.paths, tracked.paths);\n } else {\n session.set(dep, {\n kind: 'dep',\n paths: tracked.paths,\n Type: brand.Type,\n key,\n refId,\n });\n }\n\n return [tracked.value, cache.proxy];\n },\n } as DepHandleLike;\n\n Object.defineProperty(wrapper, DEP_BRAND, {\n value: brand,\n enumerable: false,\n writable: false,\n configurable: false,\n });\n\n return wrapper;\n}\n\n/** Union two PathSets (ALL_PATHS dominates). */\nfunction unionPaths(a: PathSet, b: PathSet): PathSet {\n if (a === ALL_PATHS || b === ALL_PATHS) return ALL_PATHS;\n const out = new Set<number>(a as Set<number>);\n for (const id of b as Set<number>) out.add(id);\n return out;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n\n/**\n * Expand a PathSet to include an *ancestor-watch* id for every ancestor of\n * every tracked leaf.\n *\n * The auto-tracker records leaf paths (e.g. `'items.length'`), but\n * `StructuralContainer.patch` can only mark the parent (`'items'`) when it\n * replaces a value atomically (arrays, `null`, primitives — it can't see\n * inside). Without expansion, a subscriber with interest `{'items.length'}`\n * would miss a `patch`-triggered atomic-replacement of `items`.\n *\n * Ancestors are added under the interner's *ancestor-watch* lane\n * (`internAncestor`), NOT as normal ids. The source emits a matching\n * ancestor-watch mark only for paths it replaces atomically — never for a\n * plain-object structural pulse-up. So `{'items.length'}` wakes when the array\n * `items` is replaced, but `{'user.email'}` does NOT wake when a sibling\n * `user.name` changes and pulses `user` up: pulse-up `user` is a normal id and\n * the ancestor-watch `user` only intersects another ancestor-watch `user`.\n *\n * Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but\n * NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,\n * and `''` would wake this consumer on every field change).\n */\nfunction expandWithAncestors(paths: PathSet, interner: PathInterner): PathSet {\n if (paths === ALL_PATHS) return ALL_PATHS;\n const leafPaths = paths as Set<number>;\n if (leafPaths.size === 0) return paths;\n\n const expanded = new Set<number>(leafPaths);\n for (const id of leafPaths) {\n const str = interner.lookup(id);\n // Add all non-root ancestor segments as *ancestor-watch* ids: 'a.b.c' →\n // watch 'a.b' and 'a'. These live in the interner's ancestor lane so they\n // only intersect the source's atomic-replacement marks (`internAncestor`),\n // never a structural pulse-up mark of the same path. That is what lets a\n // descendant-reader (e.g. `items.length`) wake on an array/null replacement\n // without a sibling-leaf reader (`user.email`) waking when a sibling\n // (`user.name`) changes and pulses up through `user`.\n let idx = str.lastIndexOf('.');\n while (idx > 0) {\n const ancestor = str.slice(0, idx);\n expanded.add(interner.internAncestor(ancestor));\n idx = ancestor.lastIndexOf('.');\n }\n }\n return expanded;\n}\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef } from "react";
|
|
2
|
-
import { getRegistry, resolveInstanceKey } from "@blac/core";
|
|
2
|
+
import { DEP_BRAND, getRegistry, resolveInstanceKey } from "@blac/core";
|
|
3
3
|
import { ALL_PATHS, emptyPathSet, trackRender } from "@dirtytalk/structural";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/BlocProvider.tsx
|
|
@@ -56,8 +56,63 @@ function useProvidedArgs(BlocClass) {
|
|
|
56
56
|
return useContext(ProvidedArgsContext).get(BlocClass);
|
|
57
57
|
}
|
|
58
58
|
//#endregion
|
|
59
|
+
//#region src/buildTrackedProxy.ts
|
|
60
|
+
/**
|
|
61
|
+
* Build a per-consumer proxy pair for a bloc instance.
|
|
62
|
+
*
|
|
63
|
+
* Returns two proxies:
|
|
64
|
+
* - `proxy` — the stable outer proxy returned to the consumer. Getter
|
|
65
|
+
* properties are invoked with `thisProxy` as `this` so that `this.state`
|
|
66
|
+
* reads inside getters are redirected to the current render's tracking
|
|
67
|
+
* proxy. Non-getter properties fall through to the live instance.
|
|
68
|
+
* - `thisProxy` — the inner `this`-proxy used as the receiver for getter
|
|
69
|
+
* calls. Intercepts `state` to return `trackedStateRef.current` when a
|
|
70
|
+
* tracking context is active; otherwise falls through to the live value.
|
|
71
|
+
* When `onDepHandle` is supplied, a read off `this` whose value carries the
|
|
72
|
+
* `DEP_BRAND` symbol (i.e. a `depend()` handle) is routed through
|
|
73
|
+
* `onDepHandle`, which returns a per-consumer wrapper whose `.track()` opts
|
|
74
|
+
* the consumer into cross-bloc reactivity. Core stays decoupled — detection
|
|
75
|
+
* is purely by the `DEP_BRAND` symbol.
|
|
76
|
+
*
|
|
77
|
+
* Both allocations happen exactly once per bloc acquisition (inside `useMemo`)
|
|
78
|
+
* so the proxies are stable across renders.
|
|
79
|
+
*
|
|
80
|
+
* @param onDepHandle - Optional callback invoked when a getter reads a branded
|
|
81
|
+
* dep handle off `this`. Receives the original handle and returns the value
|
|
82
|
+
* to expose in its place (the session-bound wrapper). The callback is
|
|
83
|
+
* responsible for caching wrappers per handle to avoid re-allocation.
|
|
84
|
+
*/
|
|
85
|
+
function buildTrackedProxy(instance, trackedStateRef, onDepHandle) {
|
|
86
|
+
const getterDescs = /* @__PURE__ */ new Map();
|
|
87
|
+
let proto = Object.getPrototypeOf(instance);
|
|
88
|
+
while (proto && proto !== Object.prototype) {
|
|
89
|
+
const keys = [...Object.getOwnPropertyNames(proto), ...Object.getOwnPropertySymbols(proto)];
|
|
90
|
+
for (const key of keys) {
|
|
91
|
+
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
92
|
+
if (desc?.get && !getterDescs.has(key)) getterDescs.set(key, desc);
|
|
93
|
+
}
|
|
94
|
+
proto = Object.getPrototypeOf(proto);
|
|
95
|
+
}
|
|
96
|
+
const thisProxy = new Proxy(instance, { get(t, k, r) {
|
|
97
|
+
if (k === "state") return trackedStateRef.current ?? Reflect.get(t, k, r);
|
|
98
|
+
const value = Reflect.get(t, k, r);
|
|
99
|
+
if (onDepHandle !== void 0 && (typeof value === "function" || typeof value === "object") && value !== null && value[DEP_BRAND] !== void 0) return onDepHandle(value);
|
|
100
|
+
return value;
|
|
101
|
+
} });
|
|
102
|
+
return {
|
|
103
|
+
proxy: new Proxy(instance, { get(target, key, receiver) {
|
|
104
|
+
const desc = getterDescs.get(key);
|
|
105
|
+
if (desc?.get) return desc.get.call(thisProxy);
|
|
106
|
+
return Reflect.get(target, key, receiver);
|
|
107
|
+
} }),
|
|
108
|
+
thisProxy
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
59
112
|
//#region src/useBloc.ts
|
|
60
113
|
let nextConsumerId = 0;
|
|
114
|
+
const primaryRefId = (consumerId) => `useBloc@${consumerId}`;
|
|
115
|
+
const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
61
116
|
/**
|
|
62
117
|
* React hook that connects a component to a state container with automatic
|
|
63
118
|
* re-render on state changes.
|
|
@@ -69,7 +124,7 @@ let nextConsumerId = 0;
|
|
|
69
124
|
* {@link trackRender} + the container's path-scoped `DirtyChannel`.
|
|
70
125
|
* - **Manual select**: pass `options.select` to opt out of auto-tracking.
|
|
71
126
|
* The hook re-renders only when the returned array's elements change
|
|
72
|
-
* (per-index `Object.is`).
|
|
127
|
+
* (per-index `Object.is`).
|
|
73
128
|
*
|
|
74
129
|
* Lifecycle:
|
|
75
130
|
* - The bloc is acquired from the registry on mount and released on
|
|
@@ -96,7 +151,7 @@ let nextConsumerId = 0;
|
|
|
96
151
|
* const [state, bloc] = useBloc(MyBloc);
|
|
97
152
|
* ```
|
|
98
153
|
*
|
|
99
|
-
* @example Manual select
|
|
154
|
+
* @example Manual select
|
|
100
155
|
* ```ts
|
|
101
156
|
* const [state, bloc] = useBloc(MyBloc, {
|
|
102
157
|
* select: (state) => [state.count],
|
|
@@ -127,23 +182,39 @@ function useBloc(BlocClass, options) {
|
|
|
127
182
|
const providerArgs = useProvidedArgs(BlocClass);
|
|
128
183
|
const providerArgsRef = useRef(providerArgs);
|
|
129
184
|
providerArgsRef.current = providerArgs;
|
|
130
|
-
const
|
|
185
|
+
const providerArgsKey = providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs);
|
|
186
|
+
const trackedStateRef = useRef(null);
|
|
187
|
+
const sessionRef = useRef(/* @__PURE__ */ new Map());
|
|
188
|
+
const depSubsRef = useRef(/* @__PURE__ */ new Map());
|
|
189
|
+
const depWrapperCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
190
|
+
const { bloc, instanceKey, trackedBloc } = useMemo(() => {
|
|
131
191
|
const effectiveArgs = ownArgsRef.current !== void 0 ? ownArgsRef.current : providerArgsRef.current;
|
|
132
192
|
const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);
|
|
133
|
-
const refId =
|
|
193
|
+
const refId = primaryRefId(consumerId);
|
|
194
|
+
const instance = getRegistry().acquire(BlocClass, resolvedKey, {
|
|
195
|
+
canCreate: true,
|
|
196
|
+
countRef: true,
|
|
197
|
+
refId,
|
|
198
|
+
args: effectiveArgs
|
|
199
|
+
});
|
|
200
|
+
const onDepHandle = (handle) => {
|
|
201
|
+
const cache = depWrapperCacheRef.current;
|
|
202
|
+
const cached = cache.get(handle);
|
|
203
|
+
if (cached !== void 0) return cached;
|
|
204
|
+
const wrapper = makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, depSubsRef, onDepHandle);
|
|
205
|
+
cache.set(handle, wrapper);
|
|
206
|
+
return wrapper;
|
|
207
|
+
};
|
|
208
|
+
const { proxy } = buildTrackedProxy(instance, trackedStateRef, onDepHandle);
|
|
134
209
|
return {
|
|
135
|
-
bloc:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
refId,
|
|
139
|
-
args: effectiveArgs
|
|
140
|
-
}),
|
|
141
|
-
instanceKey: resolvedKey
|
|
210
|
+
bloc: instance,
|
|
211
|
+
instanceKey: resolvedKey,
|
|
212
|
+
trackedBloc: proxy
|
|
142
213
|
};
|
|
143
214
|
}, [
|
|
144
215
|
BlocClass,
|
|
145
216
|
ownArgsKey,
|
|
146
|
-
|
|
217
|
+
providerArgsKey
|
|
147
218
|
]);
|
|
148
219
|
const [, force] = useReducer((x) => x + 1, 0);
|
|
149
220
|
const pathRef = useRef(emptyPathSet());
|
|
@@ -174,8 +245,7 @@ function useBloc(BlocClass, options) {
|
|
|
174
245
|
onMountRef.current?.(bloc);
|
|
175
246
|
return () => {
|
|
176
247
|
onUnmountRef.current?.(bloc);
|
|
177
|
-
|
|
178
|
-
getRegistry().release(BlocClass, instanceKey, false, refId);
|
|
248
|
+
getRegistry().release(BlocClass, instanceKey, false, primaryRefId(consumerId));
|
|
179
249
|
};
|
|
180
250
|
}, [bloc, instanceKey]);
|
|
181
251
|
const rawState = bloc.state;
|
|
@@ -186,21 +256,151 @@ function useBloc(BlocClass, options) {
|
|
|
186
256
|
} else {
|
|
187
257
|
const tracked = trackRender(rawState, bloc.interner);
|
|
188
258
|
state = tracked.value;
|
|
259
|
+
trackedStateRef.current = tracked.value;
|
|
189
260
|
pathRef.current = tracked.paths;
|
|
261
|
+
const session = sessionRef.current;
|
|
262
|
+
session.clear();
|
|
263
|
+
session.set(bloc, {
|
|
264
|
+
kind: "primary",
|
|
265
|
+
paths: tracked.paths
|
|
266
|
+
});
|
|
190
267
|
}
|
|
191
268
|
useLayoutEffect(() => {
|
|
269
|
+
trackedStateRef.current = null;
|
|
192
270
|
if (selectRef.current !== void 0) return;
|
|
193
271
|
const container = bloc;
|
|
194
272
|
const paths = pathRef.current;
|
|
195
273
|
container.registerConsumerPaths(consumerId, paths);
|
|
196
274
|
expandedInterestRef.current = expandWithAncestors(paths, container.interner);
|
|
275
|
+
const subs = depSubsRef.current;
|
|
276
|
+
const session = sessionRef.current;
|
|
277
|
+
for (const [depContainer, sub] of subs) if (!session.has(depContainer)) {
|
|
278
|
+
sub.unsubscribe();
|
|
279
|
+
depContainer.unregisterConsumer(consumerId);
|
|
280
|
+
getRegistry().release(sub.Type, sub.key, false, sub.refId);
|
|
281
|
+
subs.delete(depContainer);
|
|
282
|
+
}
|
|
283
|
+
for (const [depContainer, entry] of session) {
|
|
284
|
+
if (entry.kind === "primary") continue;
|
|
285
|
+
const interest = expandWithAncestors(entry.paths, depContainer.interner);
|
|
286
|
+
depContainer.registerConsumerPaths(consumerId, entry.paths);
|
|
287
|
+
const existing = subs.get(depContainer);
|
|
288
|
+
if (existing) {
|
|
289
|
+
existing.interestRef.current = interest;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const interestRef = { current: interest };
|
|
293
|
+
const unsubscribe = depContainer.channel.subscribe(() => interestRef.current, () => force());
|
|
294
|
+
subs.set(depContainer, {
|
|
295
|
+
unsubscribe,
|
|
296
|
+
interestRef,
|
|
297
|
+
Type: entry.Type,
|
|
298
|
+
key: entry.key,
|
|
299
|
+
refId: entry.refId
|
|
300
|
+
});
|
|
301
|
+
}
|
|
197
302
|
});
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
const subs = depSubsRef.current;
|
|
305
|
+
return () => {
|
|
306
|
+
for (const [depContainer, sub] of subs) {
|
|
307
|
+
sub.unsubscribe();
|
|
308
|
+
depContainer.unregisterConsumer(consumerId);
|
|
309
|
+
getRegistry().release(sub.Type, sub.key, false, sub.refId);
|
|
310
|
+
}
|
|
311
|
+
subs.clear();
|
|
312
|
+
};
|
|
313
|
+
}, [consumerId]);
|
|
198
314
|
return [
|
|
199
315
|
state,
|
|
200
|
-
|
|
316
|
+
trackedBloc,
|
|
201
317
|
componentRef
|
|
202
318
|
];
|
|
203
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Build the per-consumer wrapper that replaces a branded dep handle inside a
|
|
322
|
+
* tracked getter's `this`. The wrapper exposes the same accessors as the core
|
|
323
|
+
* handle and overrides `.track()`:
|
|
324
|
+
*
|
|
325
|
+
* - **Inside a render** (`trackedStateRef.current != null`): resolve the dep,
|
|
326
|
+
* take a refcount (once per consumer), `trackRender` its state, merge the
|
|
327
|
+
* recorded paths into the session entry, build/reuse a tracked proxy for the
|
|
328
|
+
* dep so its OWN getters track too, and return `[trackedValue, depProxy]`.
|
|
329
|
+
* - **Outside a render**: degrade to live `[dep.state, dep]` — matches the core
|
|
330
|
+
* base impl, safe in event handlers/effects/methods.
|
|
331
|
+
*
|
|
332
|
+
* `.untracked()` always returns the live instance with no subscription.
|
|
333
|
+
*
|
|
334
|
+
* Args resolve at call time (`options.args ?? defaultArgs`), so a single handle
|
|
335
|
+
* can resolve different dep instances across calls; tracked-proxy state is
|
|
336
|
+
* therefore cached per resolved instance, not per handle. Guards against a
|
|
337
|
+
* container re-entering tracking within the same render (mutual A↔B deps): if
|
|
338
|
+
* the dep already has a non-primary session entry this render, reuse its proxy
|
|
339
|
+
* + union its paths instead of re-acquiring.
|
|
340
|
+
*/
|
|
341
|
+
function makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, depSubsRef, onDepHandle) {
|
|
342
|
+
const brand = handle[DEP_BRAND];
|
|
343
|
+
const refId = depRefId(consumerId);
|
|
344
|
+
const registry = getRegistry();
|
|
345
|
+
const perDep = /* @__PURE__ */ new Map();
|
|
346
|
+
const resolve = (options) => {
|
|
347
|
+
const args = options?.args ?? brand.defaultArgs;
|
|
348
|
+
const key = registry.resolveKey(brand.Type, void 0, args);
|
|
349
|
+
return {
|
|
350
|
+
dep: registry.ensure(brand.Type, key, args),
|
|
351
|
+
key,
|
|
352
|
+
args
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
const wrapper = {
|
|
356
|
+
untracked: (options) => resolve(options).dep,
|
|
357
|
+
track: (options) => {
|
|
358
|
+
const { dep, key, args } = resolve(options);
|
|
359
|
+
if (trackedStateRef.current == null) return [dep.state, dep];
|
|
360
|
+
const session = sessionRef.current;
|
|
361
|
+
const existing = session.get(dep);
|
|
362
|
+
if (existing === void 0 && !depSubsRef.current.has(dep)) registry.acquire(brand.Type, key, {
|
|
363
|
+
canCreate: true,
|
|
364
|
+
countRef: true,
|
|
365
|
+
refId,
|
|
366
|
+
args
|
|
367
|
+
});
|
|
368
|
+
const tracked = trackRender(dep.state, dep.interner);
|
|
369
|
+
let cache = perDep.get(dep);
|
|
370
|
+
if (cache === void 0) {
|
|
371
|
+
const ref = { current: tracked.value };
|
|
372
|
+
cache = {
|
|
373
|
+
ref,
|
|
374
|
+
proxy: buildTrackedProxy(dep, ref, onDepHandle).proxy
|
|
375
|
+
};
|
|
376
|
+
perDep.set(dep, cache);
|
|
377
|
+
} else cache.ref.current = tracked.value;
|
|
378
|
+
if (existing !== void 0) existing.paths = unionPaths(existing.paths, tracked.paths);
|
|
379
|
+
else session.set(dep, {
|
|
380
|
+
kind: "dep",
|
|
381
|
+
paths: tracked.paths,
|
|
382
|
+
Type: brand.Type,
|
|
383
|
+
key,
|
|
384
|
+
refId
|
|
385
|
+
});
|
|
386
|
+
return [tracked.value, cache.proxy];
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
Object.defineProperty(wrapper, DEP_BRAND, {
|
|
390
|
+
value: brand,
|
|
391
|
+
enumerable: false,
|
|
392
|
+
writable: false,
|
|
393
|
+
configurable: false
|
|
394
|
+
});
|
|
395
|
+
return wrapper;
|
|
396
|
+
}
|
|
397
|
+
/** Union two PathSets (ALL_PATHS dominates). */
|
|
398
|
+
function unionPaths(a, b) {
|
|
399
|
+
if (a === ALL_PATHS || b === ALL_PATHS) return ALL_PATHS;
|
|
400
|
+
const out = new Set(a);
|
|
401
|
+
for (const id of b) out.add(id);
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
204
404
|
const shallowArrayEqual = (a, b) => {
|
|
205
405
|
if (a === b) return true;
|
|
206
406
|
if (a.length !== b.length) return false;
|
|
@@ -208,19 +408,26 @@ const shallowArrayEqual = (a, b) => {
|
|
|
208
408
|
return true;
|
|
209
409
|
};
|
|
210
410
|
/**
|
|
211
|
-
* Expand a PathSet to include
|
|
411
|
+
* Expand a PathSet to include an *ancestor-watch* id for every ancestor of
|
|
412
|
+
* every tracked leaf.
|
|
212
413
|
*
|
|
213
414
|
* The auto-tracker records leaf paths (e.g. `'items.length'`), but
|
|
214
|
-
* `StructuralContainer.patch`
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
415
|
+
* `StructuralContainer.patch` can only mark the parent (`'items'`) when it
|
|
416
|
+
* replaces a value atomically (arrays, `null`, primitives — it can't see
|
|
417
|
+
* inside). Without expansion, a subscriber with interest `{'items.length'}`
|
|
418
|
+
* would miss a `patch`-triggered atomic-replacement of `items`.
|
|
419
|
+
*
|
|
420
|
+
* Ancestors are added under the interner's *ancestor-watch* lane
|
|
421
|
+
* (`internAncestor`), NOT as normal ids. The source emits a matching
|
|
422
|
+
* ancestor-watch mark only for paths it replaces atomically — never for a
|
|
423
|
+
* plain-object structural pulse-up. So `{'items.length'}` wakes when the array
|
|
424
|
+
* `items` is replaced, but `{'user.email'}` does NOT wake when a sibling
|
|
425
|
+
* `user.name` changes and pulses `user` up: pulse-up `user` is a normal id and
|
|
426
|
+
* the ancestor-watch `user` only intersects another ancestor-watch `user`.
|
|
219
427
|
*
|
|
220
|
-
* Example:
|
|
221
|
-
* root
|
|
222
|
-
*
|
|
223
|
-
* wake this consumer regardless of what it actually reads).
|
|
428
|
+
* Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but
|
|
429
|
+
* NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,
|
|
430
|
+
* and `''` would wake this consumer on every field change).
|
|
224
431
|
*/
|
|
225
432
|
function expandWithAncestors(paths, interner) {
|
|
226
433
|
if (paths === ALL_PATHS) return ALL_PATHS;
|
|
@@ -232,7 +439,7 @@ function expandWithAncestors(paths, interner) {
|
|
|
232
439
|
let idx = str.lastIndexOf(".");
|
|
233
440
|
while (idx > 0) {
|
|
234
441
|
const ancestor = str.slice(0, idx);
|
|
235
|
-
expanded.add(interner.
|
|
442
|
+
expanded.add(interner.internAncestor(ancestor));
|
|
236
443
|
idx = ancestor.lastIndexOf(".");
|
|
237
444
|
}
|
|
238
445
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/BlocProvider.tsx","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\n\n/**\n * Internal context value: a Map from bloc constructor to the args object\n * provided by the nearest BlocProvider for that bloc class.\n *\n * Stored as a WeakMap-keyed record so nested providers for different blocs\n * compose without clobbering each other.\n */\ntype ProvidedArgsMap = Map<StateContainerConstructor, unknown>;\n\nconst ProvidedArgsContext = createContext<ProvidedArgsMap>(new Map());\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps<T extends StateContainerConstructor> {\n /**\n * The bloc class whose args are being provided to descendants.\n */\n bloc: T;\n /**\n * Args that descendant `useBloc(bloc)` calls will resolve to when no\n * own `args` are given.\n */\n args: ExtractArgs<T>;\n children: ReactNode;\n}\n\n/**\n * Provides args to descendant `useBloc` calls for a specific bloc class via\n * React context.\n *\n * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the\n * args supplied here. Own `args` on the `useBloc` call always win.\n *\n * Multiple `BlocProvider` wrappers for different bloc classes compose: each\n * provider merges its entry into the inherited map, so nested providers for\n * different blocs do not interfere.\n *\n * @example\n * ```tsx\n * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>\n * <UserProfile />\n * </BlocProvider>\n * ```\n *\n * @example Per-mount private instance\n * ```tsx\n * const id = useId();\n * <BlocProvider bloc={CartBloc} args={{ _id: id }}>\n * <CartWidget />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider<T extends StateContainerConstructor>({\n bloc,\n args,\n children,\n}: BlocProviderProps<T>): ReactElement {\n const parentMap = useContext(ProvidedArgsContext);\n\n // Merge our entry into a new Map so sibling/parent providers for other blocs\n // are preserved. Memoised on (parentMap, bloc, args) identity.\n const mergedMap = useMemo(() => {\n const next = new Map(parentMap);\n next.set(bloc, args);\n return next;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parentMap, bloc, args]);\n\n return (\n <ProvidedArgsContext.Provider value={mergedMap}>\n {children}\n </ProvidedArgsContext.Provider>\n );\n}\n\n/**\n * Returns the args provided by the nearest {@link BlocProvider} for the given\n * bloc class, or `undefined` when called outside a matching provider.\n *\n * Used by `useBloc` to inherit provider args when no own `args` are given.\n */\nexport function useProvidedArgs<T extends StateContainerConstructor>(\n BlocClass: T,\n): ExtractArgs<T> | undefined {\n const map = useContext(ProvidedArgsContext);\n return map.get(BlocClass) as ExtractArgs<T> | undefined;\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n getRegistry,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n trackRender,\n PathInterner,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useProvidedArgs } from './BlocProvider';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`). This replaces the v1 `dependencies` option.\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is derived from `options.args` (own args),\n * then the surrounding {@link BlocProvider} context args for this bloc,\n * then the default key (no args).\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * Per-mount private instance:\n * ```ts\n * const id = useId();\n * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });\n * ```\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select (replaces v1 `dependencies`)\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Args-based shared instance\n * ```ts\n * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Priority: own args > provider args (for this bloc class) > none.\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const ownArgs = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const ownArgsRef = useRef(ownArgs);\n ownArgsRef.current = ownArgs;\n const ownArgsKey =\n ownArgs === undefined ? undefined : JSON.stringify(ownArgs);\n\n // Read provided args from the nearest BlocProvider for this bloc class.\n const providerArgs = useProvidedArgs(BlocClass);\n const providerArgsRef = useRef(providerArgs);\n providerArgsRef.current = providerArgs;\n const providerArgsKey =\n providerArgs === undefined ? undefined : JSON.stringify(providerArgs);\n\n const { bloc, instanceKey } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n }>(() => {\n // Own args win over provider args; provider args win over no args.\n const effectiveArgs =\n ownArgsRef.current !== undefined\n ? ownArgsRef.current\n : providerArgsRef.current;\n\n const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);\n const refId = `useBloc@${consumerId}`;\n const registry = getRegistry();\n const instance = registry.acquire(BlocClass, resolvedKey, {\n canCreate: true,\n countRef: true,\n refId,\n args: effectiveArgs,\n }) as TBloc;\n return { bloc: instance, instanceKey: resolvedKey };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, ownArgsKey, providerArgsKey]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We bypass `bloc.subscribe(listener)` (the legacy listener surface, which\n // StateContainer overrides) and talk directly to `bloc.channel`. The channel\n // is the StructuralContainer's path-scoped DirtyChannel; subscribing with a\n // dynamic interest function lets us narrow wakeups per consumer.\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // Expanded interest: leaf paths PLUS their ancestor paths, so that when\n // `patch` marks a parent (e.g. 'items') and the consumer tracked a child\n // (e.g. 'items.length'), the intersection still fires. Updated in\n // useLayoutEffect after each render once pathRef.current is populated.\n const expandedInterestRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n return unsub;\n }\n\n // Auto-track mode: subscribe with the expanded interest (leaf paths + their\n // ancestors). This ensures that when `patch` marks a parent path (e.g.\n // 'items') and the consumer tracked a child (e.g. 'items.length'), the\n // channel intersection still fires a re-render.\n //\n // `expandedInterestRef.current` is updated by the useLayoutEffect below\n // after each render, so the interest is always fresh at flush time.\n const unsub = channel.subscribe(\n () => expandedInterestRef.current,\n () => force(),\n );\n // Register the consumer's leaf paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Mount / unmount lifecycle.\n //\n // Order on unmount: onUnmount(bloc) -> release(...). The bloc must still be\n // alive when onUnmount runs (release may dispose it).\n // ---------------------------------------------------------------------------\n useEffect(() => {\n onMountRef.current?.(bloc as InstanceType<T>);\n return () => {\n onUnmountRef.current?.(bloc as InstanceType<T>);\n const refId = `useBloc@${consumerId}`;\n getRegistry().release(BlocClass, instanceKey, false, refId);\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n );\n state = tracked.value as ExtractState<T>;\n pathRef.current = tracked.paths;\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits, pathRef.current is the consumer's actual leaf\n // interest (populated by the proxy during JSX evaluation). Re-register with\n // the container so the skeleton reflects the latest paths, and expand the\n // interest to include ancestor paths for the channel subscription.\n //\n // useLayoutEffect runs before the browser paints (and before any emit\n // triggered by another effect), so the skeleton is fresh by the time the\n // next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n if (selectRef.current !== undefined) return;\n const container = bloc as unknown as StateContainer;\n const paths = pathRef.current;\n container.registerConsumerPaths(consumerId, paths);\n // Expand interest to include ancestor paths so that `patch`-marked parent\n // paths (e.g. 'items') wake a consumer that tracked a child (e.g.\n // 'items.length').\n expandedInterestRef.current = expandWithAncestors(\n paths,\n container.interner,\n );\n });\n\n return [\n state,\n bloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n\n/**\n * Expand a PathSet to include all ancestor paths of every tracked path.\n *\n * The auto-tracker records leaf paths (e.g. `'items.length'`), but\n * `StructuralContainer.patch` marks parent paths (e.g. `'items'`) since it\n * only walks the patch shape — not individual array elements or nested\n * descendants. Without ancestor expansion, a channel subscriber with interest\n * `{'items.length'}` would miss a `patch`-triggered `dirty={'items'}` because\n * `intersects({'items.length'}, {'items'}) = false`.\n *\n * Example: for leaf path `'a.b.c'`, adds `'a.b'` and `'a'` (but NOT `''`\n * root, since a root change would be captured by ALL_PATHS from the source\n * when there are ≤1 consumers, and adding `''` would make every field change\n * wake this consumer regardless of what it actually reads).\n */\nfunction expandWithAncestors(paths: PathSet, interner: PathInterner): PathSet {\n if (paths === ALL_PATHS) return ALL_PATHS;\n const leafPaths = paths as Set<number>;\n if (leafPaths.size === 0) return paths;\n\n const expanded = new Set<number>(leafPaths);\n for (const id of leafPaths) {\n const str = interner.lookup(id);\n // Add all non-root ancestor segments: 'a.b.c' → add 'a.b', 'a'\n let idx = str.lastIndexOf('.');\n while (idx > 0) {\n const ancestor = str.slice(0, idx);\n expanded.add(interner.intern(ancestor));\n idx = ancestor.lastIndexOf('.');\n }\n }\n return expanded;\n}\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":";;;;;AAkBA,MAAM,sBAAsB,8BAA+B,IAAI,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CpE,SAAgB,aAAkD,EAChE,MACA,MACA,YACqC;CACrC,MAAM,YAAY,WAAW,mBAAmB;CAIhD,MAAM,YAAY,cAAc;EAC9B,MAAM,OAAO,IAAI,IAAI,SAAS;EAC9B,KAAK,IAAI,MAAM,IAAI;EACnB,OAAO;CAET,GAAG;EAAC;EAAW;EAAM;CAAI,CAAC;CAE1B,OACE,oBAAC,oBAAoB,UAArB;EAA8B,OAAO;EAClC;CAC2B,CAAA;AAElC;;;;;;;AAQA,SAAgB,gBACd,WAC4B;CAE5B,OADY,WAAW,mBACd,EAAE,IAAI,SAAS;AAC1B;;;ACpEA,IAAI,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDrB,SAAgB,QAGd,WACA,SACmC;CAGnC,MAAM,eAAe,OAAqB,CAAC,CAAC;CAK5C,MAAM,gBAAgB,OAAsB,IAAI;CAChD,IAAI,cAAc,YAAY,MAC5B,cAAc,UAAU,WAAW;CAErC,MAAM,aAAa,cAAc;CAGjC,MAAM;CAIN,MAAM,YAAY,OAAO,SAAS,MAAM;CACxC,UAAU,UAAU,SAAS;CAC7B,MAAM,aAAa,OAAO,SAAS,OAAO;CAC1C,WAAW,UAAU,SAAS;CAC9B,MAAM,eAAe,OAAO,SAAS,SAAS;CAC9C,aAAa,UAAU,SAAS;CAYhC,MAAM,UAAW,SAAmD;CACpE,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,aACJ,YAAY,KAAA,IAAY,KAAA,IAAY,KAAK,UAAU,OAAO;CAG5D,MAAM,eAAe,gBAAgB,SAAS;CAC9C,MAAM,kBAAkB,OAAO,YAAY;CAC3C,gBAAgB,UAAU;CAI1B,MAAM,EAAE,MAAM,gBAAgB,cAGrB;EAEP,MAAM,gBACJ,WAAW,YAAY,KAAA,IACnB,WAAW,UACX,gBAAgB;EAEtB,MAAM,cAAc,mBAAmB,WAAW,aAAa;EAC/D,MAAM,QAAQ,WAAW;EAQzB,OAAO;GAAE,MAPQ,YACO,EAAE,QAAQ,WAAW,aAAa;IACxD,WAAW;IACX,UAAU;IACV;IACA,MAAM;GACR,CACsB;GAAG,aAAa;EAAY;CAEpD,GAAG;EAAC;EAAW;EAvBb,iBAAiB,KAAA,IAAY,KAAA,IAAY,KAAK,UAAU,YAAY;CAuB5B,CAAC;CAU3C,MAAM,GAAG,SAAS,YAAY,MAAc,IAAI,GAAG,CAAC;CACpD,MAAM,UAAU,OAAgB,aAAa,CAAC;CAK9C,MAAM,sBAAsB,OAAgB,aAAa,CAAC;CAG1D,MAAM,mBAAmB,OAAyB,IAAI;CAEtD,gBAAgB;EAId,MAAM,UAAW,KAAmC;EAGpD,IAFqB,UAAU,YAAY,KAAA,GAqBzC,OAlBc,QAAQ,gBACd,iBACA;GACJ,MAAM,SAAS,UAAU;GACzB,IAAI,CAAC,QAAQ;IACX,MAAM;IACN;GACF;GACA,MAAM,OAAO,OACV,KAAmC,OACpC,IACF;GACA,MAAM,OAAO,iBAAiB;GAC9B,IAAI,SAAS,QAAQ,kBAAkB,MAAM,IAAI,GAAG;GACpD,iBAAiB,UAAU;GAC3B,MAAM;EACR,CAES;EAUb,MAAM,QAAQ,QAAQ,gBACd,oBAAoB,eACpB,MAAM,CACd;EAIA,KAAoC,sBAClC,YACA,QAAQ,OACV;EACA,aAAa;GACX,MAAM;GACN,KAAoC,mBAAmB,UAAU;EACnE;CACF,GAAG,CAAC,MAAM,UAAU,CAAC;CAQrB,gBAAgB;EACd,WAAW,UAAU,IAAuB;EAC5C,aAAa;GACX,aAAa,UAAU,IAAuB;GAC9C,MAAM,QAAQ,WAAW;GACzB,YAAY,EAAE,QAAQ,WAAW,aAAa,OAAO,KAAK;EAC5D;CAEF,GAAG,CAAC,MAAM,WAAW,CAAC;CAUtB,MAAM,WAAY,KAAmC;CACrD,IAAI;CACJ,IAAI,UAAU,YAAY,KAAA,GAAW;EACnC,QAAQ;EAGR,IAAI,iBAAiB,YAAY,MAC/B,iBAAiB,UAAU,UAAU,QACnC,UACA,IACF;CAEJ,OAAO;EACL,MAAM,UAAU,YACd,UACC,KAAmC,QACtC;EACA,QAAQ,QAAQ;EAChB,QAAQ,UAAU,QAAQ;CAQ5B;CAWA,sBAAsB;EACpB,IAAI,UAAU,YAAY,KAAA,GAAW;EACrC,MAAM,YAAY;EAClB,MAAM,QAAQ,QAAQ;EACtB,UAAU,sBAAsB,YAAY,KAAK;EAIjD,oBAAoB,UAAU,oBAC5B,OACA,UAAU,QACZ;CACF,CAAC;CAED,OAAO;EACL;EACA;EACA;CACF;AACF;AAEA,MAAM,qBAAqB,GAAc,MAA0B;CACjE,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAClC,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAC5B,IAAI,CAAC,OAAO,GAAG,EAAE,IAAI,EAAE,EAAE,GAAG,OAAO;CAErC,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,SAAS,oBAAoB,OAAgB,UAAiC;CAC5E,IAAI,UAAU,WAAW,OAAO;CAChC,MAAM,YAAY;CAClB,IAAI,UAAU,SAAS,GAAG,OAAO;CAEjC,MAAM,WAAW,IAAI,IAAY,SAAS;CAC1C,KAAK,MAAM,MAAM,WAAW;EAC1B,MAAM,MAAM,SAAS,OAAO,EAAE;EAE9B,IAAI,MAAM,IAAI,YAAY,GAAG;EAC7B,OAAO,MAAM,GAAG;GACd,MAAM,WAAW,IAAI,MAAM,GAAG,GAAG;GACjC,SAAS,IAAI,SAAS,OAAO,QAAQ,CAAC;GACtC,MAAM,SAAS,YAAY,GAAG;EAChC;CACF;CACA,OAAO;AACT;ACjVA,IAAI,eAAgC,CAAmB;;;;;;AAOvD,SAAgB,mBAAmB,QAAwC;CACzE,eAAe;EAAE,GAAG;EAAc,GAAG;CAAO;AAC9C"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/BlocProvider.tsx","../src/buildTrackedProxy.ts","../src/useBloc.ts","../src/config.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\n\n/**\n * Internal context value: a Map from bloc constructor to the args object\n * provided by the nearest BlocProvider for that bloc class.\n *\n * Stored as a WeakMap-keyed record so nested providers for different blocs\n * compose without clobbering each other.\n */\ntype ProvidedArgsMap = Map<StateContainerConstructor, unknown>;\n\nconst ProvidedArgsContext = createContext<ProvidedArgsMap>(new Map());\n\n/**\n * Props for {@link BlocProvider}.\n */\nexport interface BlocProviderProps<T extends StateContainerConstructor> {\n /**\n * The bloc class whose args are being provided to descendants.\n */\n bloc: T;\n /**\n * Args that descendant `useBloc(bloc)` calls will resolve to when no\n * own `args` are given.\n */\n args: ExtractArgs<T>;\n children: ReactNode;\n}\n\n/**\n * Provides args to descendant `useBloc` calls for a specific bloc class via\n * React context.\n *\n * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the\n * args supplied here. Own `args` on the `useBloc` call always win.\n *\n * Multiple `BlocProvider` wrappers for different bloc classes compose: each\n * provider merges its entry into the inherited map, so nested providers for\n * different blocs do not interfere.\n *\n * @example\n * ```tsx\n * <BlocProvider bloc={UserBloc} args={{ userId: 'alice' }}>\n * <UserProfile />\n * </BlocProvider>\n * ```\n *\n * @example Per-mount private instance\n * ```tsx\n * const id = useId();\n * <BlocProvider bloc={CartBloc} args={{ _id: id }}>\n * <CartWidget />\n * </BlocProvider>\n * ```\n */\nexport function BlocProvider<T extends StateContainerConstructor>({\n bloc,\n args,\n children,\n}: BlocProviderProps<T>): ReactElement {\n const parentMap = useContext(ProvidedArgsContext);\n\n // Merge our entry into a new Map so sibling/parent providers for other blocs\n // are preserved. Memoised on (parentMap, bloc, args) identity.\n const mergedMap = useMemo(() => {\n const next = new Map(parentMap);\n next.set(bloc, args);\n return next;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parentMap, bloc, args]);\n\n return (\n <ProvidedArgsContext.Provider value={mergedMap}>\n {children}\n </ProvidedArgsContext.Provider>\n );\n}\n\n/**\n * Returns the args provided by the nearest {@link BlocProvider} for the given\n * bloc class, or `undefined` when called outside a matching provider.\n *\n * Used by `useBloc` to inherit provider args when no own `args` are given.\n */\nexport function useProvidedArgs<T extends StateContainerConstructor>(\n BlocClass: T,\n): ExtractArgs<T> | undefined {\n const map = useContext(ProvidedArgsContext);\n return map.get(BlocClass) as ExtractArgs<T> | undefined;\n}\n","import { DEP_BRAND } from '@blac/core';\n\n/**\n * Build a per-consumer proxy pair for a bloc instance.\n *\n * Returns two proxies:\n * - `proxy` — the stable outer proxy returned to the consumer. Getter\n * properties are invoked with `thisProxy` as `this` so that `this.state`\n * reads inside getters are redirected to the current render's tracking\n * proxy. Non-getter properties fall through to the live instance.\n * - `thisProxy` — the inner `this`-proxy used as the receiver for getter\n * calls. Intercepts `state` to return `trackedStateRef.current` when a\n * tracking context is active; otherwise falls through to the live value.\n * When `onDepHandle` is supplied, a read off `this` whose value carries the\n * `DEP_BRAND` symbol (i.e. a `depend()` handle) is routed through\n * `onDepHandle`, which returns a per-consumer wrapper whose `.track()` opts\n * the consumer into cross-bloc reactivity. Core stays decoupled — detection\n * is purely by the `DEP_BRAND` symbol.\n *\n * Both allocations happen exactly once per bloc acquisition (inside `useMemo`)\n * so the proxies are stable across renders.\n *\n * @param onDepHandle - Optional callback invoked when a getter reads a branded\n * dep handle off `this`. Receives the original handle and returns the value\n * to expose in its place (the session-bound wrapper). The callback is\n * responsible for caching wrappers per handle to avoid re-allocation.\n */\nexport function buildTrackedProxy<T extends object>(\n instance: T,\n trackedStateRef: { current: unknown },\n onDepHandle?: (handle: object) => unknown,\n): { proxy: T; thisProxy: T } {\n // Build a map of getter descriptors from the prototype chain (excluding\n // Object.prototype). This is computed once per bloc acquisition so that\n // the proxy's get trap is O(1) per property access. Both string- and\n // symbol-keyed getters are collected. Arrow-function class properties\n // (own, bound in the constructor) are not getters and pass through\n // unmodified.\n const getterDescs = new Map<string | symbol, PropertyDescriptor>();\n let proto = Object.getPrototypeOf(instance);\n while (proto && proto !== Object.prototype) {\n const keys: (string | symbol)[] = [\n ...Object.getOwnPropertyNames(proto),\n ...Object.getOwnPropertySymbols(proto),\n ];\n for (const key of keys) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc?.get && !getterDescs.has(key)) getterDescs.set(key, desc);\n }\n proto = Object.getPrototypeOf(proto);\n }\n\n // `this`-proxy for getter invocations, allocated ONCE per acquisition (the\n // trap closes over the stable `trackedStateRef`, so it never needs to be\n // rebuilt per access). Redirects `this.state` to the current render's\n // tracking proxy so getter reads during JSX record paths; outside render\n // `trackedStateRef.current` is null and it falls through to live state.\n // The receiver `r` (this proxy) is threaded through Reflect.get so chained\n // getter calls (getters reading other getters) stay in tracked context.\n const thisProxy = new Proxy(instance as object, {\n get(t, k, r) {\n if (k === 'state') return trackedStateRef.current ?? Reflect.get(t, k, r);\n const value = Reflect.get(t, k, r);\n // A branded dep handle read off `this` (e.g. `this.price`) is routed\n // through onDepHandle so the consumer's session can wrap `.track()`.\n if (\n onDepHandle !== undefined &&\n (typeof value === 'function' || typeof value === 'object') &&\n value !== null &&\n (value as Record<symbol, unknown>)[DEP_BRAND] !== undefined\n ) {\n return onDepHandle(value as object);\n }\n return value;\n },\n });\n\n // Stable proxy: one allocation per bloc acquisition. Non-getter access is\n // a single Map lookup + Reflect.get — no prototype walk on the hot path.\n const proxy = new Proxy(instance as object, {\n get(target, key, receiver) {\n const desc = getterDescs.get(key);\n if (desc?.get) return desc.get.call(thisProxy);\n return Reflect.get(target, key, receiver);\n },\n }) as T;\n\n return { proxy, thisProxy: thisProxy as T };\n}\n","import {\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n type RefObject,\n} from 'react';\nimport {\n DEP_BRAND,\n getRegistry,\n resolveInstanceKey,\n type ExtractArgs,\n type ExtractState,\n type InstanceState,\n type StateContainer,\n type StateContainerConstructor,\n} from '@blac/core';\nimport {\n ALL_PATHS,\n emptyPathSet,\n trackRender,\n PathInterner,\n type PathSet,\n} from '@dirtytalk/structural';\nimport { useProvidedArgs } from './BlocProvider';\nimport { buildTrackedProxy } from './buildTrackedProxy';\nimport type { ComponentRef, UseBlocOptions, UseBlocReturn } from './types';\n\nlet nextConsumerId = 0;\n\n// Registry refId formats for a consumer's primary bloc and its tracked deps.\n// Centralised so the `acquire` and `release` sites can never drift apart — a\n// mismatch would leak the ref and keep the bloc alive past unmount.\nconst primaryRefId = (consumerId: string): string => `useBloc@${consumerId}`;\nconst depRefId = (consumerId: string): string => `useBloc@${consumerId}:dep`;\n\n/**\n * React hook that connects a component to a state container with automatic\n * re-render on state changes.\n *\n * Two tracking modes:\n * - **Auto-tracking** (default): the returned state value is a proxy that\n * records read paths during render. The component re-renders when any\n * recorded path changes. Backed by `@dirtytalk/structural`'s\n * {@link trackRender} + the container's path-scoped `DirtyChannel`.\n * - **Manual select**: pass `options.select` to opt out of auto-tracking.\n * The hook re-renders only when the returned array's elements change\n * (per-index `Object.is`).\n *\n * Lifecycle:\n * - The bloc is acquired from the registry on mount and released on\n * unmount. The instance key is derived from `options.args` (own args),\n * then the surrounding {@link BlocProvider} context args for this bloc,\n * then the default key (no args).\n * - `options.onMount` fires after the bloc is acquired; `options.onUnmount`\n * fires *before* the registry releases its ref, so the bloc is still\n * alive when the callback runs.\n *\n * Per-mount private instance:\n * ```ts\n * const id = useId();\n * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });\n * ```\n *\n * @template T - The state container constructor type (inferred from BlocClass)\n * @param BlocClass - The state container class to connect to\n * @param options - Configuration options\n * @returns Tuple of `[state, bloc, ref]`\n *\n * @example Basic usage\n * ```ts\n * const [state, bloc] = useBloc(MyBloc);\n * ```\n *\n * @example Manual select\n * ```ts\n * const [state, bloc] = useBloc(MyBloc, {\n * select: (state) => [state.count],\n * });\n * ```\n *\n * @example Args-based shared instance\n * ```ts\n * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });\n * ```\n */\nexport function useBloc<\n T extends StateContainerConstructor = StateContainerConstructor,\n>(\n BlocClass: T,\n options?: UseBlocOptions<T>,\n): UseBlocReturn<T, ExtractState<T>> {\n type TBloc = InstanceState<T>;\n\n const componentRef = useRef<ComponentRef>({});\n\n // Stable per-consumer id (for the structural container's consumer registry).\n // Plain counter rather than `useId()` so we don't compete with internal hooks\n // for SSR id slots.\n const consumerIdRef = useRef<string | null>(null);\n if (consumerIdRef.current === null) {\n consumerIdRef.current = `useBloc-${nextConsumerId++}`;\n }\n const consumerId = consumerIdRef.current;\n // Reserve a useId slot — kept for forwards compatibility and to match\n // BlocProvider-driven SSR hydration alignment (no-op call).\n useId();\n\n // Refs that always carry the latest option callbacks, so the commit effect\n // can read them without re-keying.\n const selectRef = useRef(options?.select);\n selectRef.current = options?.select;\n const onMountRef = useRef(options?.onMount);\n onMountRef.current = options?.onMount;\n const onUnmountRef = useRef(options?.onUnmount);\n onUnmountRef.current = options?.onUnmount;\n\n // ---------------------------------------------------------------------------\n // Identity resolution\n //\n // Priority: own args > provider args (for this bloc class) > none.\n //\n // Args are user-supplied; callers commonly pass a fresh object literal each\n // render. Memoising on `args` directly would bust every render. We compute a\n // structural key (JSON.stringify) for the useMemo dep instead — undefined\n // args (void-args blocs) collapse to an undefined key.\n // ---------------------------------------------------------------------------\n const ownArgs = (options as { args?: ExtractArgs<T> } | undefined)?.args;\n const ownArgsRef = useRef(ownArgs);\n ownArgsRef.current = ownArgs;\n const ownArgsKey =\n ownArgs === undefined ? undefined : JSON.stringify(ownArgs);\n\n // Read provided args from the nearest BlocProvider for this bloc class.\n const providerArgs = useProvidedArgs(BlocClass);\n const providerArgsRef = useRef(providerArgs);\n providerArgsRef.current = providerArgs;\n const providerArgsKey =\n providerArgs === undefined ? undefined : JSON.stringify(providerArgs);\n\n // Current render's tracking proxy. Declared before the memo so the stable\n // ref object can be passed to buildTrackedProxy at acquisition time. The\n // proxy trap only reads `.current` at invocation time (not during creation),\n // so it is safe to pass on first mount even though the value is null.\n // Populated during each auto-track render snapshot (below); cleared to null\n // by useLayoutEffect after commit.\n const trackedStateRef = useRef<unknown>(null);\n\n // ---------------------------------------------------------------------------\n // Per-consumer cross-bloc session.\n //\n // Each render rebuilds a map of every container this consumer is currently\n // interested in. The PRIMARY bloc is the first uniform entry; every dep\n // reached through `this.<handle>.track()` inside a tracked getter adds an\n // entry. The layout-effect reconcile (below) diffs this map vs the previous\n // render to subscribe new deps and release dropped ones. The session lives in\n // this hook's refs only — there is no global ambient state, so sibling\n // renders never cross-contaminate.\n // ---------------------------------------------------------------------------\n const sessionRef = useRef<Map<StateContainer, SessionEntry>>(new Map());\n // Channel subscriptions for DEP containers (the primary keeps its own\n // dedicated effect). container -> { unsubscribe, interestRef, refId held }.\n const depSubsRef = useRef<Map<StateContainer, DepSub>>(new Map());\n // Per-handle wrapper cache, allocated once per bloc acquisition (in the memo)\n // so wrappers are stable across renders. handle -> session-bound wrapper.\n const depWrapperCacheRef = useRef<Map<object, unknown>>(new Map());\n\n const { bloc, instanceKey, trackedBloc } = useMemo<{\n bloc: TBloc;\n instanceKey: string;\n trackedBloc: TBloc;\n }>(() => {\n // Own args win over provider args; provider args win over no args.\n const effectiveArgs =\n ownArgsRef.current !== undefined\n ? ownArgsRef.current\n : providerArgsRef.current;\n\n const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);\n const refId = primaryRefId(consumerId);\n const registry = getRegistry();\n const instance = registry.acquire(BlocClass, resolvedKey, {\n canCreate: true,\n countRef: true,\n refId,\n args: effectiveArgs,\n }) as TBloc;\n\n // Build a session-bound wrapper for a dep handle the first time a getter\n // reads it off `this`; cache per handle so the wrapper identity is stable.\n // `onDepHandle` is threaded into each dep's tracked proxy too, so a nested\n // `this.<otherHandle>.track()` inside a dep's getter records into the SAME\n // consumer session — that is what makes deep chains (A→B→C) reactive.\n const onDepHandle = (handle: object): unknown => {\n const cache = depWrapperCacheRef.current;\n const cached = cache.get(handle);\n if (cached !== undefined) return cached;\n const wrapper = makeDepWrapper(\n handle as DepHandleLike,\n consumerId,\n trackedStateRef,\n sessionRef,\n depSubsRef,\n onDepHandle,\n );\n cache.set(handle, wrapper);\n return wrapper;\n };\n\n const { proxy } = buildTrackedProxy(\n instance as object,\n trackedStateRef,\n onDepHandle,\n );\n\n return {\n bloc: instance,\n instanceKey: resolvedKey,\n trackedBloc: proxy as TBloc,\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [BlocClass, ownArgsKey, providerArgsKey]);\n\n // ---------------------------------------------------------------------------\n // Channel subscription\n //\n // We talk directly to `bloc.channel` — the StructuralContainer's path-scoped\n // DirtyChannel. Subscribing with a dynamic interest function lets us narrow\n // wakeups per consumer, so a component only re-renders when a path it\n // actually read changes (rather than on every state change).\n // ---------------------------------------------------------------------------\n const [, force] = useReducer((x: number) => x + 1, 0);\n const pathRef = useRef<PathSet>(emptyPathSet());\n // Expanded interest: leaf paths PLUS *ancestor-watch* ids for their parents,\n // so that when `patch` atomically replaces a parent (e.g. the array 'items')\n // and the consumer tracked a child (e.g. 'items.length'), the intersection\n // still fires — while a structural pulse-up of a plain-object parent (e.g.\n // 'user' when a sibling 'user.name' changed) does NOT, because pulse-up marks\n // are normal ids and ancestor-watch ids only intersect their own lane.\n // Updated in useLayoutEffect after each render once pathRef.current is\n // populated.\n const expandedInterestRef = useRef<PathSet>(emptyPathSet());\n // For select-mode: cache the last selected array so we can compare against\n // the next one before forcing a re-render.\n const lastSelectionRef = useRef<unknown[] | null>(null);\n\n useEffect(() => {\n // Subscribe via the channel directly. For auto-track we re-register the\n // current path interest on each commit (below); for select-mode we use\n // ALL_PATHS and compare selections in the callback.\n const channel = (bloc as unknown as StateContainer).channel;\n const isSelectMode = selectRef.current !== undefined;\n\n if (isSelectMode) {\n const unsub = channel.subscribe(\n () => ALL_PATHS,\n () => {\n const select = selectRef.current;\n if (!select) {\n force();\n return;\n }\n const next = select(\n (bloc as unknown as StateContainer).state as ExtractState<T>,\n bloc as InstanceState<T>,\n );\n const prev = lastSelectionRef.current;\n if (prev !== null && shallowArrayEqual(prev, next)) return;\n lastSelectionRef.current = next;\n force();\n },\n );\n return unsub;\n }\n\n // Auto-track mode: subscribe with the expanded interest (leaf paths + their\n // ancestors). This ensures that when `patch` marks a parent path (e.g.\n // 'items') and the consumer tracked a child (e.g. 'items.length'), the\n // channel intersection still fires a re-render.\n //\n // `expandedInterestRef.current` is updated by the useLayoutEffect below\n // after each render, so the interest is always fresh at flush time.\n const unsub = channel.subscribe(\n () => expandedInterestRef.current,\n () => force(),\n );\n // Register the consumer's leaf paths with the container for skeleton\n // recomputation, so the source-side diff can skip us when our paths don't\n // intersect the change.\n (bloc as unknown as StateContainer).registerConsumerPaths(\n consumerId,\n pathRef.current,\n );\n return () => {\n unsub();\n (bloc as unknown as StateContainer).unregisterConsumer(consumerId);\n };\n }, [bloc, consumerId]);\n\n // ---------------------------------------------------------------------------\n // Mount / unmount lifecycle.\n //\n // Order on unmount: onUnmount(bloc) -> release(...). The bloc must still be\n // alive when onUnmount runs (release may dispose it).\n // ---------------------------------------------------------------------------\n useEffect(() => {\n onMountRef.current?.(bloc as InstanceType<T>);\n return () => {\n onUnmountRef.current?.(bloc as InstanceType<T>);\n getRegistry().release(\n BlocClass,\n instanceKey,\n false,\n primaryRefId(consumerId),\n );\n };\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [bloc, instanceKey]);\n\n // ---------------------------------------------------------------------------\n // Snapshot\n //\n // - Auto-track: wrap state in trackRender, record paths into pathRef, and\n // re-register with the container so the skeleton picks up new interest.\n // - Select-mode: return state directly; the subscription callback compares\n // selections to decide whether to re-render.\n // ---------------------------------------------------------------------------\n const rawState = (bloc as unknown as StateContainer).state as ExtractState<T>;\n let state: ExtractState<T>;\n if (selectRef.current !== undefined) {\n state = rawState;\n // Seed the last selection on the first render so we don't fire an\n // immediate \"different from null\" wakeup on the first emit.\n if (lastSelectionRef.current === null) {\n lastSelectionRef.current = selectRef.current(\n rawState,\n bloc as InstanceState<T>,\n );\n }\n } else {\n const tracked = trackRender(\n rawState,\n (bloc as unknown as StateContainer).interner,\n );\n state = tracked.value as ExtractState<T>;\n trackedStateRef.current = tracked.value;\n pathRef.current = tracked.paths;\n // Rebuild the per-consumer session for this render. The primary bloc is the\n // first uniform entry; its `paths` are the SAME PathSet object the proxy\n // mutates during JSX (so it stays live as getters record leaves). Dep\n // entries are appended during JSX as `this.<handle>.track()` runs. Cleared\n // here (not in the layout effect) so a render that no longer tracks a dep\n // produces a session without it, and the reconcile drops it.\n const session = sessionRef.current;\n session.clear();\n session.set(bloc as unknown as StateContainer, {\n kind: 'primary',\n paths: tracked.paths,\n });\n // NOTE: registerConsumerPaths is intentionally NOT called here. The\n // proxy hasn't been accessed yet, so `tracked.paths` is an empty Set\n // that the proxy will mutate during JSX evaluation. Registering at\n // this point would store an empty interest with the container and\n // freeze the skeleton at that snapshot — subsequent emits would\n // diff against an empty skeleton and silently drop wakeups. The\n // useLayoutEffect below registers the populated set after render.\n }\n\n // After the render commits, pathRef.current is the consumer's actual leaf\n // interest (populated by the proxy during JSX evaluation). Re-register with\n // the container so the skeleton reflects the latest paths, and expand the\n // interest to include ancestor paths for the channel subscription.\n //\n // useLayoutEffect runs before the browser paints (and before any emit\n // triggered by another effect), so the skeleton is fresh by the time the\n // next emit fires.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n // Clear the render-time tracking proxy now that JSX has been evaluated and\n // committed. Getters invoked after this point (event handlers, effects,\n // method→getter chains) fall through to live state instead of reading this\n // render's frozen snapshot. The render body re-seeds it next render.\n trackedStateRef.current = null;\n if (selectRef.current !== undefined) return;\n const container = bloc as unknown as StateContainer;\n const paths = pathRef.current;\n container.registerConsumerPaths(consumerId, paths);\n // Register the *normal* leaf paths above for the source-side skeleton, then\n // build the channel interest as leaves + ancestor-watch ids so that an\n // atomic `patch` replacement of a parent (e.g. the array 'items') wakes a\n // consumer that tracked a child (e.g. 'items.length').\n expandedInterestRef.current = expandWithAncestors(\n paths,\n container.interner,\n );\n\n // -----------------------------------------------------------------------\n // Reconcile DEP containers (cross-bloc `.track()` interest).\n //\n // The primary bloc keeps its own dedicated subscription effect above; this\n // block manages only the *dep* containers recorded in the session this\n // render. We diff the new dep set vs the previously-subscribed set:\n // - new dep -> acquire was already done in `.track()`; subscribe its\n // channel + registerConsumerPaths + seed interest.\n // - surviving -> refresh its interest ref (subscribe closure reads it).\n // - dropped -> unsubscribe, unregisterConsumer, and release its ref.\n // -----------------------------------------------------------------------\n const subs = depSubsRef.current;\n const session = sessionRef.current;\n\n // Pass 1: drop containers no longer in the session.\n for (const [depContainer, sub] of subs) {\n if (!session.has(depContainer)) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n subs.delete(depContainer);\n }\n }\n\n // Pass 2: add/refresh containers in the session (skip the primary).\n for (const [depContainer, entry] of session) {\n if (entry.kind === 'primary') continue;\n const interest = expandWithAncestors(entry.paths, depContainer.interner);\n depContainer.registerConsumerPaths(consumerId, entry.paths);\n const existing = subs.get(depContainer);\n if (existing) {\n existing.interestRef.current = interest;\n continue;\n }\n const interestRef: { current: PathSet } = { current: interest };\n const unsubscribe = depContainer.channel.subscribe(\n () => interestRef.current,\n () => force(),\n );\n subs.set(depContainer, {\n unsubscribe,\n interestRef,\n Type: entry.Type,\n key: entry.key,\n refId: entry.refId,\n });\n }\n });\n\n // Unmount: tear down every dep subscription + ref exactly once. Kept in its\n // own effect (consumerId is stable for the component's lifetime, so this only\n // runs on final unmount, not on every reconcile). depSubsRef is mutated in\n // place by the reconcile, so the captured Map reference still holds the live\n // set at unmount.\n useEffect(() => {\n // Capture the ref's Map (mutated in place across renders) so the cleanup\n // reads the captured reference rather than depSubsRef.current directly.\n const subs = depSubsRef.current;\n return () => {\n for (const [depContainer, sub] of subs) {\n sub.unsubscribe();\n depContainer.unregisterConsumer(consumerId);\n getRegistry().release(sub.Type, sub.key, false, sub.refId);\n }\n subs.clear();\n };\n }, [consumerId]);\n\n return [\n state,\n trackedBloc,\n componentRef as RefObject<ComponentRef>,\n ] as UseBlocReturn<T, ExtractState<T>>;\n}\n\n// ---------------------------------------------------------------------------\n// Cross-bloc session types + dep-handle wrapper.\n// ---------------------------------------------------------------------------\n\n/**\n * One entry in a consumer's per-render session map. Discriminated on `kind`:\n * the primary bloc is managed by its own dedicated effect, while dep entries\n * carry the registry coordinates the reconcile needs to release their ref.\n */\ntype SessionEntry =\n | {\n kind: 'primary';\n /** Tracked leaf paths recorded against the primary this render. */\n paths: PathSet;\n }\n | {\n kind: 'dep';\n /** Tracked leaf paths recorded against this dep this render. */\n paths: PathSet;\n /** Constructor for registry release. */\n Type: StateContainerConstructor;\n /** Resolved instance key for registry release. */\n key: string;\n /** refId held for this dep (released on drop/unmount). */\n refId: string;\n };\n\n/** A live dep-channel subscription tracked between renders for reconciliation. */\ninterface DepSub {\n unsubscribe: () => void;\n interestRef: { current: PathSet };\n Type: StateContainerConstructor;\n key: string;\n refId: string;\n}\n\n/** Per-access options shared by both dep accessors. */\ninterface DepAccessOptionsLike {\n args?: unknown;\n}\n\n/** Structural shape of a branded `depend()` handle as seen from React. */\ninterface DepHandleLike {\n track(options?: DepAccessOptionsLike): [unknown, StateContainer];\n untracked(options?: DepAccessOptionsLike): StateContainer;\n readonly [DEP_BRAND]: {\n Type: StateContainerConstructor;\n defaultArgs?: unknown;\n };\n}\n\n/**\n * Build the per-consumer wrapper that replaces a branded dep handle inside a\n * tracked getter's `this`. The wrapper exposes the same accessors as the core\n * handle and overrides `.track()`:\n *\n * - **Inside a render** (`trackedStateRef.current != null`): resolve the dep,\n * take a refcount (once per consumer), `trackRender` its state, merge the\n * recorded paths into the session entry, build/reuse a tracked proxy for the\n * dep so its OWN getters track too, and return `[trackedValue, depProxy]`.\n * - **Outside a render**: degrade to live `[dep.state, dep]` — matches the core\n * base impl, safe in event handlers/effects/methods.\n *\n * `.untracked()` always returns the live instance with no subscription.\n *\n * Args resolve at call time (`options.args ?? defaultArgs`), so a single handle\n * can resolve different dep instances across calls; tracked-proxy state is\n * therefore cached per resolved instance, not per handle. Guards against a\n * container re-entering tracking within the same render (mutual A↔B deps): if\n * the dep already has a non-primary session entry this render, reuse its proxy\n * + union its paths instead of re-acquiring.\n */\nfunction makeDepWrapper(\n handle: DepHandleLike,\n consumerId: string,\n trackedStateRef: { current: unknown },\n sessionRef: { current: Map<StateContainer, SessionEntry> },\n depSubsRef: { current: Map<StateContainer, DepSub> },\n onDepHandle: (handle: object) => unknown,\n): DepHandleLike {\n const brand = handle[DEP_BRAND];\n const refId = depRefId(consumerId);\n const registry = getRegistry();\n // Per-resolved-instance tracked-state ref + proxy. Call-time args mean one\n // handle can resolve several instances, so cache is keyed by the instance.\n const perDep = new Map<\n StateContainer,\n { ref: { current: unknown }; proxy: StateContainer }\n >();\n\n const resolve = (options?: DepAccessOptionsLike) => {\n const args = options?.args ?? brand.defaultArgs;\n const key = registry.resolveKey(brand.Type, undefined, args);\n const dep = registry.ensure(\n brand.Type,\n key,\n args,\n ) as unknown as StateContainer;\n return { dep, key, args };\n };\n\n const wrapper = {\n untracked: (options?: DepAccessOptionsLike) => resolve(options).dep,\n track: (options?: DepAccessOptionsLike) => {\n const { dep, key, args } = resolve(options);\n\n // Outside a render: live values, no subscription (core base behavior).\n if (trackedStateRef.current == null) {\n return [dep.state, dep];\n }\n\n const session = sessionRef.current;\n const existing = session.get(dep);\n\n // Take a refcount the FIRST time this dep is tracked, held across renders\n // and released by the reconcile (on drop) or unmount. The session map is\n // rebuilt every render, so it can't tell us whether we already hold a ref;\n // `depSubsRef` does — it persists across renders and is populated by the\n // reconcile after the first tracking render. If neither the (cleared) per-\n // render session nor the persistent sub set knows this dep, acquire once.\n // React runs the layout-effect reconcile before the next render, so the\n // sub is registered before a subsequent track sees it → no double-acquire.\n if (existing === undefined && !depSubsRef.current.has(dep)) {\n registry.acquire(brand.Type, key, {\n canCreate: true,\n countRef: true,\n refId,\n args,\n });\n }\n\n const tracked = trackRender(dep.state, dep.interner);\n let cache = perDep.get(dep);\n if (cache === undefined) {\n const ref = { current: tracked.value as unknown };\n cache = { ref, proxy: buildTrackedProxy(dep, ref, onDepHandle).proxy };\n perDep.set(dep, cache);\n } else {\n cache.ref.current = tracked.value;\n }\n\n if (existing !== undefined) {\n // Re-entry this render (`.track()` twice, or a mutual cycle): union the\n // new paths into the existing entry rather than re-acquiring.\n existing.paths = unionPaths(existing.paths, tracked.paths);\n } else {\n session.set(dep, {\n kind: 'dep',\n paths: tracked.paths,\n Type: brand.Type,\n key,\n refId,\n });\n }\n\n return [tracked.value, cache.proxy];\n },\n } as DepHandleLike;\n\n Object.defineProperty(wrapper, DEP_BRAND, {\n value: brand,\n enumerable: false,\n writable: false,\n configurable: false,\n });\n\n return wrapper;\n}\n\n/** Union two PathSets (ALL_PATHS dominates). */\nfunction unionPaths(a: PathSet, b: PathSet): PathSet {\n if (a === ALL_PATHS || b === ALL_PATHS) return ALL_PATHS;\n const out = new Set<number>(a as Set<number>);\n for (const id of b as Set<number>) out.add(id);\n return out;\n}\n\nconst shallowArrayEqual = (a: unknown[], b: unknown[]): boolean => {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return false;\n }\n return true;\n};\n\n/**\n * Expand a PathSet to include an *ancestor-watch* id for every ancestor of\n * every tracked leaf.\n *\n * The auto-tracker records leaf paths (e.g. `'items.length'`), but\n * `StructuralContainer.patch` can only mark the parent (`'items'`) when it\n * replaces a value atomically (arrays, `null`, primitives — it can't see\n * inside). Without expansion, a subscriber with interest `{'items.length'}`\n * would miss a `patch`-triggered atomic-replacement of `items`.\n *\n * Ancestors are added under the interner's *ancestor-watch* lane\n * (`internAncestor`), NOT as normal ids. The source emits a matching\n * ancestor-watch mark only for paths it replaces atomically — never for a\n * plain-object structural pulse-up. So `{'items.length'}` wakes when the array\n * `items` is replaced, but `{'user.email'}` does NOT wake when a sibling\n * `user.name` changes and pulses `user` up: pulse-up `user` is a normal id and\n * the ancestor-watch `user` only intersects another ancestor-watch `user`.\n *\n * Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but\n * NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,\n * and `''` would wake this consumer on every field change).\n */\nfunction expandWithAncestors(paths: PathSet, interner: PathInterner): PathSet {\n if (paths === ALL_PATHS) return ALL_PATHS;\n const leafPaths = paths as Set<number>;\n if (leafPaths.size === 0) return paths;\n\n const expanded = new Set<number>(leafPaths);\n for (const id of leafPaths) {\n const str = interner.lookup(id);\n // Add all non-root ancestor segments as *ancestor-watch* ids: 'a.b.c' →\n // watch 'a.b' and 'a'. These live in the interner's ancestor lane so they\n // only intersect the source's atomic-replacement marks (`internAncestor`),\n // never a structural pulse-up mark of the same path. That is what lets a\n // descendant-reader (e.g. `items.length`) wake on an array/null replacement\n // without a sibling-leaf reader (`user.email`) waking when a sibling\n // (`user.name`) changes and pulses up through `user`.\n let idx = str.lastIndexOf('.');\n while (idx > 0) {\n const ancestor = str.slice(0, idx);\n expanded.add(interner.internAncestor(ancestor));\n idx = ancestor.lastIndexOf('.');\n }\n }\n return expanded;\n}\n","/**\n * Global configuration for `@blac/react`.\n *\n * The hook's tracking model is fixed: when `useBloc` is called without a\n * `select`, render-time auto-tracking is used (via\n * `@dirtytalk/structural`'s `trackRender`). When `select` is provided,\n * re-renders are driven by per-index `Object.is` over the returned array.\n *\n * Reserved for forwards-compatible knobs; currently empty.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BlacReactConfig {}\n\nconst defaultConfig: BlacReactConfig = {};\n\nlet globalConfig: BlacReactConfig = { ...defaultConfig };\n\n/**\n * Configure global defaults for `@blac/react` hooks.\n *\n * @param config - Partial configuration to merge with current globals\n */\nexport function configureBlacReact(config: Partial<BlacReactConfig>): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get the current global configuration.\n * @internal\n */\nexport function getBlacReactConfig(): BlacReactConfig {\n return globalConfig;\n}\n\n/**\n * Reset configuration to defaults (useful for testing).\n * @internal\n */\nexport function resetBlacReactConfig(): void {\n globalConfig = { ...defaultConfig };\n}\n"],"mappings":"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"}
|
package/dist/testing.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.cjs","names":["StateContainerRegistry"],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n /** Args used to resolve which instance the stub is registered under. */\n args?: ExtractArgs<T>;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, args, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n // Pass args explicitly so createCubitStub calls
|
|
1
|
+
{"version":3,"file":"testing.cjs","names":["StateContainerRegistry"],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n /** Args used to resolve which instance the stub is registered under. */\n args?: ExtractArgs<T>;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, args, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n // Pass args explicitly so createCubitStub calls [INIT_CONFIG] → init().\n const instance = createCubitStub(BlocClass, { ...stubOptions, args } as any);\n registerOverride(BlocClass, instance, args);\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return {\n ...renderResult,\n bloc: instance,\n };\n}\n\nexport function renderWithRegistry(\n ui: ReactElement,\n setup: (registry: StateContainerRegistry) => void,\n): RenderResult {\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n try {\n setup(testRegistry);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return renderResult;\n}\n"],"mappings":";;;;;AAmBA,SAAgB,eACd,IACA,SAC0C;CAC1C,MAAM,EAAE,MAAM,WAAW,MAAM,GAAG,gBAAgB;CAElD,MAAM,YAAA,GAAA,WAAA,aAAuB;CAE7B,CAAA,GAAA,WAAA,aAAY,IADaA,WAAAA,uBACF,CAAC;CAGxB,MAAM,YAAA,GAAA,mBAAA,iBAA2B,WAAW;EAAE,GAAG;EAAa;CAAK,CAAQ;CAC3E,CAAA,GAAA,mBAAA,kBAAiB,WAAW,UAAU,IAAI;CAE1C,IAAI;CACJ,IAAI;EACF,gBAAA,GAAA,uBAAA,QAAsB,EAAE;CAC1B,SAAS,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,QAAQ;EACpB,MAAM;CACR;CAEA,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,gBAAgB;EAChB,CAAA,GAAA,WAAA,aAAY,QAAQ;CACtB;CAEA,OAAO;EACL,GAAG;EACH,MAAM;CACR;AACF;AAEA,SAAgB,mBACd,IACA,OACc;CACd,MAAM,YAAA,GAAA,WAAA,aAAuB;CAC7B,MAAM,eAAe,IAAIA,WAAAA,uBAAuB;CAChD,CAAA,GAAA,WAAA,aAAY,YAAY;CAExB,IAAI;EACF,MAAM,YAAY;CACpB,SAAS,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,QAAQ;EACpB,MAAM;CACR;CAEA,IAAI;CACJ,IAAI;EACF,gBAAA,GAAA,uBAAA,QAAsB,EAAE;CAC1B,SAAS,GAAG;EACV,CAAA,GAAA,WAAA,aAAY,QAAQ;EACpB,MAAM;CACR;CAEA,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,gBAAgB;EAChB,CAAA,GAAA,WAAA,aAAY,QAAQ;CACtB;CAEA,OAAO;AACT"}
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","names":[],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n /** Args used to resolve which instance the stub is registered under. */\n args?: ExtractArgs<T>;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, args, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n // Pass args explicitly so createCubitStub calls
|
|
1
|
+
{"version":3,"file":"testing.js","names":[],"sources":["../src/testing.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport type { RenderResult } from '@testing-library/react';\nimport type { ExtractArgs, StateContainerConstructor } from '@blac/core';\nimport { StateContainerRegistry, getRegistry, setRegistry } from '@blac/core';\nimport {\n registerOverride,\n createCubitStub,\n type CubitStubOptions,\n} from '@blac/core/testing';\nimport { render } from '@testing-library/react';\n\ninterface RenderWithBlocOptions<\n T extends StateContainerConstructor,\n> extends CubitStubOptions<T> {\n bloc: T;\n /** Args used to resolve which instance the stub is registered under. */\n args?: ExtractArgs<T>;\n}\n\nexport function renderWithBloc<T extends StateContainerConstructor>(\n ui: ReactElement,\n options: RenderWithBlocOptions<T>,\n): RenderResult & { bloc: InstanceType<T> } {\n const { bloc: BlocClass, args, ...stubOptions } = options;\n\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n // Pass args explicitly so createCubitStub calls [INIT_CONFIG] → init().\n const instance = createCubitStub(BlocClass, { ...stubOptions, args } as any);\n registerOverride(BlocClass, instance, args);\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return {\n ...renderResult,\n bloc: instance,\n };\n}\n\nexport function renderWithRegistry(\n ui: ReactElement,\n setup: (registry: StateContainerRegistry) => void,\n): RenderResult {\n const previous = getRegistry();\n const testRegistry = new StateContainerRegistry();\n setRegistry(testRegistry);\n\n try {\n setup(testRegistry);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n let renderResult: RenderResult;\n try {\n renderResult = render(ui);\n } catch (e) {\n setRegistry(previous);\n throw e;\n }\n\n const originalUnmount = renderResult.unmount;\n renderResult.unmount = () => {\n originalUnmount();\n setRegistry(previous);\n };\n\n return renderResult;\n}\n"],"mappings":";;;;AAmBA,SAAgB,eACd,IACA,SAC0C;CAC1C,MAAM,EAAE,MAAM,WAAW,MAAM,GAAG,gBAAgB;CAElD,MAAM,WAAW,YAAY;CAE7B,YAAY,IADa,uBACF,CAAC;CAGxB,MAAM,WAAW,gBAAgB,WAAW;EAAE,GAAG;EAAa;CAAK,CAAQ;CAC3E,iBAAiB,WAAW,UAAU,IAAI;CAE1C,IAAI;CACJ,IAAI;EACF,eAAe,OAAO,EAAE;CAC1B,SAAS,GAAG;EACV,YAAY,QAAQ;EACpB,MAAM;CACR;CAEA,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,gBAAgB;EAChB,YAAY,QAAQ;CACtB;CAEA,OAAO;EACL,GAAG;EACH,MAAM;CACR;AACF;AAEA,SAAgB,mBACd,IACA,OACc;CACd,MAAM,WAAW,YAAY;CAC7B,MAAM,eAAe,IAAI,uBAAuB;CAChD,YAAY,YAAY;CAExB,IAAI;EACF,MAAM,YAAY;CACpB,SAAS,GAAG;EACV,YAAY,QAAQ;EACpB,MAAM;CACR;CAEA,IAAI;CACJ,IAAI;EACF,eAAe,OAAO,EAAE;CAC1B,SAAS,GAAG;EACV,YAAY,QAAQ;EACpB,MAAM;CACR;CAEA,MAAM,kBAAkB,aAAa;CACrC,aAAa,gBAAgB;EAC3B,gBAAgB;EAChB,YAAY,QAAQ;CACtB;CAEA,OAAO;AACT"}
|
package/dist/types.d.ts
CHANGED
|
@@ -19,12 +19,11 @@ type ArgsOption<T extends StateContainerConstructor> = ExtractArgs<T> extends vo
|
|
|
19
19
|
*
|
|
20
20
|
* @remarks
|
|
21
21
|
* Instance identity is derived entirely from `args`. For a per-mount private
|
|
22
|
-
* instance
|
|
23
|
-
* object: `{ args: { _id: useId() } }`.
|
|
22
|
+
* instance, pass a stable unique object: `{ args: { _id: useId() } }`.
|
|
24
23
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* To re-render on a fixed set of values rather than on auto-tracked reads, pass
|
|
25
|
+
* `select` — it returns a tuple/array that is compared per-index via
|
|
26
|
+
* `Object.is`.
|
|
28
27
|
*/
|
|
29
28
|
export type UseBlocOptions<TBloc extends StateContainerConstructor> = ArgsOption<TBloc> & {
|
|
30
29
|
/**
|
package/dist/useBloc.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { UseBlocOptions, UseBlocReturn } from './types';
|
|
|
11
11
|
* {@link trackRender} + the container's path-scoped `DirtyChannel`.
|
|
12
12
|
* - **Manual select**: pass `options.select` to opt out of auto-tracking.
|
|
13
13
|
* The hook re-renders only when the returned array's elements change
|
|
14
|
-
* (per-index `Object.is`).
|
|
14
|
+
* (per-index `Object.is`).
|
|
15
15
|
*
|
|
16
16
|
* Lifecycle:
|
|
17
17
|
* - The bloc is acquired from the registry on mount and released on
|
|
@@ -38,7 +38,7 @@ import type { UseBlocOptions, UseBlocReturn } from './types';
|
|
|
38
38
|
* const [state, bloc] = useBloc(MyBloc);
|
|
39
39
|
* ```
|
|
40
40
|
*
|
|
41
|
-
* @example Manual select
|
|
41
|
+
* @example Manual select
|
|
42
42
|
* ```ts
|
|
43
43
|
* const [state, bloc] = useBloc(MyBloc, {
|
|
44
44
|
* select: (state) => [state.count],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blac/react",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Brendan Mullins <jsnanigans@gmail.com>",
|
|
6
6
|
"description": "React bindings for BlaC — useBloc hook with automatic re-render optimization",
|
|
@@ -78,13 +78,13 @@
|
|
|
78
78
|
}
|
|
79
79
|
],
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"@dirtytalk/structural": "^0.0.
|
|
81
|
+
"@dirtytalk/structural": "^0.0.6"
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
84
|
"@testing-library/react": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
85
85
|
"@types/react": "^18.0.0 || ^19.0.0",
|
|
86
86
|
"react": "^18.0.0 || ^19.0.0",
|
|
87
|
-
"@blac/core": "^2.0.
|
|
87
|
+
"@blac/core": "^2.0.18"
|
|
88
88
|
},
|
|
89
89
|
"peerDependenciesMeta": {
|
|
90
90
|
"@types/react": {
|
|
@@ -100,10 +100,13 @@
|
|
|
100
100
|
"devDependencies": {
|
|
101
101
|
"@size-limit/preset-small-lib": "^11.1.6",
|
|
102
102
|
"size-limit": "^11.1.6",
|
|
103
|
+
"@babel/core": "^7.28.0",
|
|
104
|
+
"@rolldown/plugin-babel": "^0.2.3",
|
|
103
105
|
"@testing-library/dom": "^10.4.1",
|
|
104
106
|
"@testing-library/jest-dom": "^6.9.1",
|
|
105
107
|
"@testing-library/react": "^16.3.2",
|
|
106
108
|
"@testing-library/user-event": "^14.6.1",
|
|
109
|
+
"@types/babel__core": "^7.20.5",
|
|
107
110
|
"@types/react": "^19.2.15",
|
|
108
111
|
"@types/react-dom": "^19.2.3",
|
|
109
112
|
"@vitejs/plugin-react": "^6.0.2",
|
|
@@ -117,7 +120,7 @@
|
|
|
117
120
|
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
118
121
|
"vite-plus": "^0.1.23",
|
|
119
122
|
"vitest": "npm:@voidzero-dev/vite-plus-test@latest",
|
|
120
|
-
"@blac/core": "2.0.
|
|
123
|
+
"@blac/core": "2.0.18"
|
|
121
124
|
},
|
|
122
125
|
"scripts": {
|
|
123
126
|
"dev": "vp pack --watch",
|