@blac/react 2.0.17 → 2.0.19
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 +79 -21
- package/dist/index.cjs +135 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +131 -43
- 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 +4 -4
- package/dist/utils/instance-keys.d.ts +0 -12
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
|
|
@@ -38,6 +44,53 @@ function Counter() {
|
|
|
38
44
|
}
|
|
39
45
|
```
|
|
40
46
|
|
|
47
|
+
## Reactivity model
|
|
48
|
+
|
|
49
|
+
BlaC follows one rule:
|
|
50
|
+
|
|
51
|
+
> **A component becomes reactive only to the paths it reads during its own render.**
|
|
52
|
+
|
|
53
|
+
`useBloc` returns a proxy that records which paths you read; the component
|
|
54
|
+
re-renders only when one of *those* paths changes. Reads outside render — effects,
|
|
55
|
+
event handlers, `setTimeout`, `await` — record nothing.
|
|
56
|
+
|
|
57
|
+
### Props are snapshots — `untracked()`
|
|
58
|
+
|
|
59
|
+
A tracking proxy stays bound to the component that *created* it. If a parent passes
|
|
60
|
+
a proxied value to a child and the child reads from it, that read is attributed to
|
|
61
|
+
the **parent** — so the parent over-subscribes and re-renders for paths only the
|
|
62
|
+
child uses.
|
|
63
|
+
|
|
64
|
+
Pass the value through `untracked()` to hand the child a detached snapshot instead:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { useBloc, untracked } from '@blac/react';
|
|
68
|
+
|
|
69
|
+
function List() {
|
|
70
|
+
const [state] = useBloc(TodoBloc);
|
|
71
|
+
// The parent reads each id (for keys). Rows read `.label` etc. off the
|
|
72
|
+
// snapshot, which is detached — those reads are NOT attributed to List.
|
|
73
|
+
return (
|
|
74
|
+
<ul>
|
|
75
|
+
{state.items.map((item) => (
|
|
76
|
+
<Row key={item.id} item={untracked(item)} />
|
|
77
|
+
))}
|
|
78
|
+
</ul>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The trade-off is intentional and React-like: a component that receives an
|
|
84
|
+
`untracked()` value is **not** reactive to it — a change triggers nothing, memo or
|
|
85
|
+
not. If no component reads a path during its *own* render, a change to that path
|
|
86
|
+
re-renders nothing. To react to data, a component must be a consumer itself (call
|
|
87
|
+
its own `useBloc`). Nobody up the tree re-renders for a path unless they read it
|
|
88
|
+
themselves.
|
|
89
|
+
|
|
90
|
+
> Making this automatic — per-component read scoping so `untracked()` isn't needed
|
|
91
|
+
> by hand — is a planned compiler/Babel plugin. See
|
|
92
|
+
> [`plans/blac-ambient-tracking/design.md`](../../plans/blac-ambient-tracking/design.md).
|
|
93
|
+
|
|
41
94
|
## useBloc
|
|
42
95
|
|
|
43
96
|
```tsx
|
|
@@ -88,11 +141,11 @@ function UserCard({ userId }: { userId: string }) {
|
|
|
88
141
|
- **Required when declared** — omitting `args` or passing the wrong shape is a type error.
|
|
89
142
|
- **Drives identity** — different `args` ⇒ different instance. Same `args` ⇒ same instance (dev-warn if args mismatch on same-keyed second call).
|
|
90
143
|
- **Serializable only** — non-serializable values (refs, callbacks) belong in the `deps` lane (below).
|
|
91
|
-
- **Per-component private instances** —
|
|
144
|
+
- **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
145
|
|
|
93
146
|
```tsx
|
|
94
|
-
const
|
|
95
|
-
const [state, cubit] = useBloc(FormCubit, { args: options,
|
|
147
|
+
const id = useId();
|
|
148
|
+
const [state, cubit] = useBloc(FormCubit, { args: { ...options, _id: id } });
|
|
96
149
|
```
|
|
97
150
|
|
|
98
151
|
### `deps`: Non-Serializable Refs and Callbacks
|
|
@@ -204,24 +257,29 @@ const [state] = useBloc(CounterCubit, {
|
|
|
204
257
|
|
|
205
258
|
### Options
|
|
206
259
|
|
|
207
|
-
| Option
|
|
208
|
-
|
|
|
209
|
-
| `args`
|
|
210
|
-
| `select`
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `onUnmount` | `(bloc) => void` | Called when component unmounts |
|
|
260
|
+
| Option | Type | Description |
|
|
261
|
+
| ----------- | ---------------------------- | -------------------------------------------------------------------------------- |
|
|
262
|
+
| `args` | `Args` type | Required when bloc declares `Args != void`; forbidden when `void` |
|
|
263
|
+
| `select` | `(state, bloc) => unknown[]` | Manual dependency selector (renamed from `dependencies`); disables auto-tracking |
|
|
264
|
+
| `onMount` | `(bloc) => void` | Called when component mounts |
|
|
265
|
+
| `onUnmount` | `(bloc) => void` | Called when component unmounts |
|
|
214
266
|
|
|
215
|
-
> Auto-tracking is always on when `select` is omitted — it is **not** a configurable option. `deps` and `
|
|
267
|
+
> 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
268
|
|
|
217
269
|
### Identity and Keying
|
|
218
270
|
|
|
219
271
|
Instance identity is resolved in precedence order:
|
|
220
272
|
|
|
221
|
-
1. **
|
|
222
|
-
2.
|
|
223
|
-
3. **`
|
|
224
|
-
|
|
273
|
+
1. **`<BlocProvider>` context id** — inherited from an ancestor provider
|
|
274
|
+
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`)
|
|
275
|
+
3. **`'default'`** — singleton fallback
|
|
276
|
+
|
|
277
|
+
For a per-mount private instance, embed a stable unique ID inside `args` so each mount hashes to a distinct key:
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
const id = useId();
|
|
281
|
+
useBloc(FormCubit, { args: { ...options, _id: id } });
|
|
282
|
+
```
|
|
225
283
|
|
|
226
284
|
Blocs declare explicit identity via a static class property:
|
|
227
285
|
|
|
@@ -237,24 +295,24 @@ class DocumentCubit extends Cubit<
|
|
|
237
295
|
|
|
238
296
|
### Instance Sharing and Lifecycle
|
|
239
297
|
|
|
240
|
-
By default, all components using `useBloc(MyBloc)` with the same identity share one instance.
|
|
298
|
+
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
299
|
|
|
242
300
|
```tsx
|
|
243
301
|
// All users with userId=123 share one instance
|
|
244
302
|
useBloc(UserCardCubit, { args: { userId: 123 } });
|
|
245
303
|
|
|
246
304
|
// Each component mount gets its own instance, disposed on unmount
|
|
247
|
-
const
|
|
248
|
-
useBloc(FormCubit, { args: options,
|
|
305
|
+
const id = useId();
|
|
306
|
+
useBloc(FormCubit, { args: { ...options, _id: id } });
|
|
249
307
|
|
|
250
|
-
// Explicit
|
|
251
|
-
useBloc(EditorCubit, {
|
|
308
|
+
// Explicit stable key (escape hatch for non-derivable identity)
|
|
309
|
+
useBloc(EditorCubit, { args: { _id: 'editor-1' } });
|
|
252
310
|
```
|
|
253
311
|
|
|
254
312
|
### Breaking Changes (v2)
|
|
255
313
|
|
|
256
314
|
- **`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
|
|
315
|
+
- **`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
316
|
- **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
317
|
- **`args` is required when declared, forbidden when void** — enforced by the type system; no runtime guard needed.
|
|
260
318
|
|
package/dist/index.cjs
CHANGED
|
@@ -112,6 +112,7 @@ function buildTrackedProxy(instance, trackedStateRef, onDepHandle) {
|
|
|
112
112
|
//#endregion
|
|
113
113
|
//#region src/useBloc.ts
|
|
114
114
|
let nextConsumerId = 0;
|
|
115
|
+
const ARGS_UNSET = Symbol("blac.argsKeyUnset");
|
|
115
116
|
const primaryRefId = (consumerId) => `useBloc@${consumerId}`;
|
|
116
117
|
const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
117
118
|
/**
|
|
@@ -125,7 +126,7 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
|
125
126
|
* {@link trackRender} + the container's path-scoped `DirtyChannel`.
|
|
126
127
|
* - **Manual select**: pass `options.select` to opt out of auto-tracking.
|
|
127
128
|
* The hook re-renders only when the returned array's elements change
|
|
128
|
-
* (per-index `Object.is`).
|
|
129
|
+
* (per-index `Object.is`).
|
|
129
130
|
*
|
|
130
131
|
* Lifecycle:
|
|
131
132
|
* - The bloc is acquired from the registry on mount and released on
|
|
@@ -152,7 +153,7 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
|
|
|
152
153
|
* const [state, bloc] = useBloc(MyBloc);
|
|
153
154
|
* ```
|
|
154
155
|
*
|
|
155
|
-
* @example Manual select
|
|
156
|
+
* @example Manual select
|
|
156
157
|
* ```ts
|
|
157
158
|
* const [state, bloc] = useBloc(MyBloc, {
|
|
158
159
|
* select: (state) => [state.count],
|
|
@@ -179,30 +180,48 @@ function useBloc(BlocClass, options) {
|
|
|
179
180
|
const ownArgs = options?.args;
|
|
180
181
|
const ownArgsRef = (0, react.useRef)(ownArgs);
|
|
181
182
|
ownArgsRef.current = ownArgs;
|
|
182
|
-
const
|
|
183
|
+
const ownArgsKeyRef = (0, react.useRef)({
|
|
184
|
+
ref: ARGS_UNSET,
|
|
185
|
+
key: void 0
|
|
186
|
+
});
|
|
187
|
+
if (!Object.is(ownArgsKeyRef.current.ref, ownArgs)) ownArgsKeyRef.current = {
|
|
188
|
+
ref: ownArgs,
|
|
189
|
+
key: ownArgs === void 0 ? void 0 : JSON.stringify(ownArgs)
|
|
190
|
+
};
|
|
191
|
+
const ownArgsKey = ownArgsKeyRef.current.key;
|
|
183
192
|
const providerArgs = useProvidedArgs(BlocClass);
|
|
184
193
|
const providerArgsRef = (0, react.useRef)(providerArgs);
|
|
185
194
|
providerArgsRef.current = providerArgs;
|
|
186
|
-
const
|
|
195
|
+
const providerArgsKeyRef = (0, react.useRef)({
|
|
196
|
+
ref: ARGS_UNSET,
|
|
197
|
+
key: void 0
|
|
198
|
+
});
|
|
199
|
+
if (!Object.is(providerArgsKeyRef.current.ref, providerArgs)) providerArgsKeyRef.current = {
|
|
200
|
+
ref: providerArgs,
|
|
201
|
+
key: providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs)
|
|
202
|
+
};
|
|
203
|
+
const providerArgsKey = providerArgsKeyRef.current.key;
|
|
187
204
|
const trackedStateRef = (0, react.useRef)(null);
|
|
188
205
|
const sessionRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
189
206
|
const depSubsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
190
207
|
const depWrapperCacheRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
208
|
+
const proxyCacheRef = (0, react.useRef)(new _dirtytalk_structural.ProxyCache());
|
|
209
|
+
const lastReconcileRef = (0, react.useRef)(null);
|
|
210
|
+
const [rebindNonce, bumpRebind] = (0, react.useReducer)((x) => x + 1, 0);
|
|
211
|
+
const ownedBlocRef = (0, react.useRef)(null);
|
|
191
212
|
const { bloc, instanceKey, trackedBloc } = (0, react.useMemo)(() => {
|
|
192
213
|
const effectiveArgs = ownArgsRef.current !== void 0 ? ownArgsRef.current : providerArgsRef.current;
|
|
193
214
|
const resolvedKey = (0, _blac_core.resolveInstanceKey)(BlocClass, effectiveArgs);
|
|
194
|
-
const refId = primaryRefId(consumerId);
|
|
195
215
|
const instance = (0, _blac_core.getRegistry)().acquire(BlocClass, resolvedKey, {
|
|
196
216
|
canCreate: true,
|
|
197
|
-
countRef:
|
|
198
|
-
refId,
|
|
217
|
+
countRef: false,
|
|
199
218
|
args: effectiveArgs
|
|
200
219
|
});
|
|
201
220
|
const onDepHandle = (handle) => {
|
|
202
221
|
const cache = depWrapperCacheRef.current;
|
|
203
222
|
const cached = cache.get(handle);
|
|
204
223
|
if (cached !== void 0) return cached;
|
|
205
|
-
const wrapper = makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef,
|
|
224
|
+
const wrapper = makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, onDepHandle);
|
|
206
225
|
cache.set(handle, wrapper);
|
|
207
226
|
return wrapper;
|
|
208
227
|
};
|
|
@@ -215,47 +234,80 @@ function useBloc(BlocClass, options) {
|
|
|
215
234
|
}, [
|
|
216
235
|
BlocClass,
|
|
217
236
|
ownArgsKey,
|
|
218
|
-
providerArgsKey
|
|
237
|
+
providerArgsKey,
|
|
238
|
+
rebindNonce
|
|
219
239
|
]);
|
|
220
240
|
const [, force] = (0, react.useReducer)((x) => x + 1, 0);
|
|
221
241
|
const pathRef = (0, react.useRef)((0, _dirtytalk_structural.emptyPathSet)());
|
|
222
242
|
const expandedInterestRef = (0, react.useRef)((0, _dirtytalk_structural.emptyPathSet)());
|
|
223
243
|
const lastSelectionRef = (0, react.useRef)(null);
|
|
244
|
+
const renderStateRef = (0, react.useRef)(void 0);
|
|
245
|
+
const prevBlocRef = (0, react.useRef)(null);
|
|
224
246
|
(0, react.useEffect)(() => {
|
|
225
247
|
const channel = bloc.channel;
|
|
226
|
-
if (selectRef.current !== void 0)
|
|
227
|
-
const
|
|
228
|
-
|
|
248
|
+
if (selectRef.current !== void 0) {
|
|
249
|
+
const unsub = channel.subscribe(() => _dirtytalk_structural.ALL_PATHS, () => {
|
|
250
|
+
const select = selectRef.current;
|
|
251
|
+
if (!select) {
|
|
252
|
+
force();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const next = select(bloc.state, bloc);
|
|
256
|
+
const prev = lastSelectionRef.current;
|
|
257
|
+
if (prev !== null && shallowArrayEqual(prev, next)) return;
|
|
258
|
+
lastSelectionRef.current = next;
|
|
229
259
|
force();
|
|
230
|
-
|
|
260
|
+
});
|
|
261
|
+
const select = selectRef.current;
|
|
262
|
+
if (select) {
|
|
263
|
+
const next = select(bloc.state, bloc);
|
|
264
|
+
const prev = lastSelectionRef.current;
|
|
265
|
+
if (prev === null || !shallowArrayEqual(prev, next)) {
|
|
266
|
+
lastSelectionRef.current = next;
|
|
267
|
+
force();
|
|
268
|
+
}
|
|
231
269
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (prev !== null && shallowArrayEqual(prev, next)) return;
|
|
235
|
-
lastSelectionRef.current = next;
|
|
236
|
-
force();
|
|
237
|
-
});
|
|
270
|
+
return unsub;
|
|
271
|
+
}
|
|
238
272
|
const unsub = channel.subscribe(() => expandedInterestRef.current, () => force());
|
|
239
273
|
bloc.registerConsumerPaths(consumerId, pathRef.current);
|
|
274
|
+
if (bloc.state !== renderStateRef.current) force();
|
|
240
275
|
return () => {
|
|
241
276
|
unsub();
|
|
242
277
|
bloc.unregisterConsumer(consumerId);
|
|
243
278
|
};
|
|
244
279
|
}, [bloc, consumerId]);
|
|
245
|
-
(0, react.
|
|
246
|
-
|
|
280
|
+
(0, react.useLayoutEffect)(() => {
|
|
281
|
+
const registry = (0, _blac_core.getRegistry)();
|
|
282
|
+
const live = registry.acquire(BlocClass, instanceKey, {
|
|
283
|
+
canCreate: true,
|
|
284
|
+
countRef: true,
|
|
285
|
+
refId: primaryRefId(consumerId)
|
|
286
|
+
});
|
|
287
|
+
ownedBlocRef.current = live;
|
|
288
|
+
onMountRef.current?.(live);
|
|
289
|
+
if (live !== bloc) bumpRebind();
|
|
247
290
|
return () => {
|
|
248
|
-
onUnmountRef.current?.(bloc);
|
|
249
|
-
|
|
291
|
+
onUnmountRef.current?.(ownedBlocRef.current ?? bloc);
|
|
292
|
+
registry.release(BlocClass, instanceKey, false, primaryRefId(consumerId));
|
|
250
293
|
};
|
|
251
|
-
}, [
|
|
294
|
+
}, [
|
|
295
|
+
BlocClass,
|
|
296
|
+
instanceKey,
|
|
297
|
+
consumerId
|
|
298
|
+
]);
|
|
252
299
|
const rawState = bloc.state;
|
|
300
|
+
renderStateRef.current = rawState;
|
|
301
|
+
if (prevBlocRef.current !== bloc) {
|
|
302
|
+
prevBlocRef.current = bloc;
|
|
303
|
+
lastSelectionRef.current = null;
|
|
304
|
+
}
|
|
253
305
|
let state;
|
|
254
306
|
if (selectRef.current !== void 0) {
|
|
255
307
|
state = rawState;
|
|
256
308
|
if (lastSelectionRef.current === null) lastSelectionRef.current = selectRef.current(rawState, bloc);
|
|
257
309
|
} else {
|
|
258
|
-
const tracked = (0, _dirtytalk_structural.trackRender)(rawState, bloc.interner);
|
|
310
|
+
const tracked = (0, _dirtytalk_structural.trackRender)(rawState, bloc.interner, proxyCacheRef.current);
|
|
259
311
|
state = tracked.value;
|
|
260
312
|
trackedStateRef.current = tracked.value;
|
|
261
313
|
pathRef.current = tracked.paths;
|
|
@@ -265,16 +317,33 @@ function useBloc(BlocClass, options) {
|
|
|
265
317
|
kind: "primary",
|
|
266
318
|
paths: tracked.paths
|
|
267
319
|
});
|
|
320
|
+
queueMicrotask(tracked.disarm);
|
|
268
321
|
}
|
|
269
322
|
(0, react.useLayoutEffect)(() => {
|
|
270
323
|
trackedStateRef.current = null;
|
|
271
|
-
if (selectRef.current !== void 0)
|
|
324
|
+
if (selectRef.current !== void 0) {
|
|
325
|
+
lastReconcileRef.current = null;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
272
328
|
const container = bloc;
|
|
273
329
|
const paths = pathRef.current;
|
|
330
|
+
const session = sessionRef.current;
|
|
331
|
+
const last = lastReconcileRef.current;
|
|
332
|
+
if (last !== null && last.primaryContainer === container && (0, _dirtytalk_structural.pathSetEquals)(last.primaryPaths, paths)) {
|
|
333
|
+
let unchanged = last.deps.size === session.size - 1;
|
|
334
|
+
if (unchanged) for (const [depContainer, entry] of session) {
|
|
335
|
+
if (entry.kind === "primary") continue;
|
|
336
|
+
const prevEntry = last.deps.get(depContainer);
|
|
337
|
+
if (prevEntry === void 0 || prevEntry.key !== entry.key || prevEntry.refId !== entry.refId || !Object.is(prevEntry.args, entry.args) || !(0, _dirtytalk_structural.pathSetEquals)(prevEntry.paths, entry.paths)) {
|
|
338
|
+
unchanged = false;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (unchanged) return;
|
|
343
|
+
}
|
|
274
344
|
container.registerConsumerPaths(consumerId, paths);
|
|
275
345
|
expandedInterestRef.current = expandWithAncestors(paths, container.interner);
|
|
276
346
|
const subs = depSubsRef.current;
|
|
277
|
-
const session = sessionRef.current;
|
|
278
347
|
for (const [depContainer, sub] of subs) if (!session.has(depContainer)) {
|
|
279
348
|
sub.unsubscribe();
|
|
280
349
|
depContainer.unregisterConsumer(consumerId);
|
|
@@ -290,6 +359,12 @@ function useBloc(BlocClass, options) {
|
|
|
290
359
|
existing.interestRef.current = interest;
|
|
291
360
|
continue;
|
|
292
361
|
}
|
|
362
|
+
(0, _blac_core.getRegistry)().acquire(entry.Type, entry.key, {
|
|
363
|
+
canCreate: true,
|
|
364
|
+
countRef: true,
|
|
365
|
+
refId: entry.refId,
|
|
366
|
+
args: entry.args
|
|
367
|
+
});
|
|
293
368
|
const interestRef = { current: interest };
|
|
294
369
|
const unsubscribe = depContainer.channel.subscribe(() => interestRef.current, () => force());
|
|
295
370
|
subs.set(depContainer, {
|
|
@@ -297,9 +372,25 @@ function useBloc(BlocClass, options) {
|
|
|
297
372
|
interestRef,
|
|
298
373
|
Type: entry.Type,
|
|
299
374
|
key: entry.key,
|
|
300
|
-
refId: entry.refId
|
|
375
|
+
refId: entry.refId,
|
|
376
|
+
args: entry.args
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const depsSignature = /* @__PURE__ */ new Map();
|
|
380
|
+
for (const [depContainer, entry] of session) {
|
|
381
|
+
if (entry.kind === "primary") continue;
|
|
382
|
+
depsSignature.set(depContainer, {
|
|
383
|
+
paths: entry.paths,
|
|
384
|
+
key: entry.key,
|
|
385
|
+
refId: entry.refId,
|
|
386
|
+
args: entry.args
|
|
301
387
|
});
|
|
302
388
|
}
|
|
389
|
+
lastReconcileRef.current = {
|
|
390
|
+
primaryContainer: container,
|
|
391
|
+
primaryPaths: paths,
|
|
392
|
+
deps: depsSignature
|
|
393
|
+
};
|
|
303
394
|
});
|
|
304
395
|
(0, react.useEffect)(() => {
|
|
305
396
|
const subs = depSubsRef.current;
|
|
@@ -323,10 +414,11 @@ function useBloc(BlocClass, options) {
|
|
|
323
414
|
* tracked getter's `this`. The wrapper exposes the same accessors as the core
|
|
324
415
|
* handle and overrides `.track()`:
|
|
325
416
|
*
|
|
326
|
-
* - **Inside a render** (`trackedStateRef.current != null`): resolve
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
417
|
+
* - **Inside a render** (`trackedStateRef.current != null`): resolve (ENSURE,
|
|
418
|
+
* no ref) the dep, `trackRender` its state, merge the recorded paths into the
|
|
419
|
+
* session entry, build/reuse a tracked proxy for the dep so its OWN getters
|
|
420
|
+
* track too, and return `[trackedValue, depProxy]`. The ownership ref is taken
|
|
421
|
+
* by the layout-effect reconcile pass, not here.
|
|
330
422
|
* - **Outside a render**: degrade to live `[dep.state, dep]` — matches the core
|
|
331
423
|
* base impl, safe in event handlers/effects/methods.
|
|
332
424
|
*
|
|
@@ -339,11 +431,12 @@ function useBloc(BlocClass, options) {
|
|
|
339
431
|
* the dep already has a non-primary session entry this render, reuse its proxy
|
|
340
432
|
* + union its paths instead of re-acquiring.
|
|
341
433
|
*/
|
|
342
|
-
function makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef,
|
|
434
|
+
function makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, onDepHandle) {
|
|
343
435
|
const brand = handle[_blac_core.DEP_BRAND];
|
|
344
436
|
const refId = depRefId(consumerId);
|
|
345
437
|
const registry = (0, _blac_core.getRegistry)();
|
|
346
438
|
const perDep = /* @__PURE__ */ new Map();
|
|
439
|
+
const proxyCache = new _dirtytalk_structural.ProxyCache();
|
|
347
440
|
const resolve = (options) => {
|
|
348
441
|
const args = options?.args ?? brand.defaultArgs;
|
|
349
442
|
const key = registry.resolveKey(brand.Type, void 0, args);
|
|
@@ -360,13 +453,7 @@ function makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, depSubs
|
|
|
360
453
|
if (trackedStateRef.current == null) return [dep.state, dep];
|
|
361
454
|
const session = sessionRef.current;
|
|
362
455
|
const existing = session.get(dep);
|
|
363
|
-
|
|
364
|
-
canCreate: true,
|
|
365
|
-
countRef: true,
|
|
366
|
-
refId,
|
|
367
|
-
args
|
|
368
|
-
});
|
|
369
|
-
const tracked = (0, _dirtytalk_structural.trackRender)(dep.state, dep.interner);
|
|
456
|
+
const tracked = (0, _dirtytalk_structural.trackRender)(dep.state, dep.interner, proxyCache);
|
|
370
457
|
let cache = perDep.get(dep);
|
|
371
458
|
if (cache === void 0) {
|
|
372
459
|
const ref = { current: tracked.value };
|
|
@@ -382,7 +469,8 @@ function makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, depSubs
|
|
|
382
469
|
paths: tracked.paths,
|
|
383
470
|
Type: brand.Type,
|
|
384
471
|
key,
|
|
385
|
-
refId
|
|
472
|
+
refId,
|
|
473
|
+
args
|
|
386
474
|
});
|
|
387
475
|
return [tracked.value, cache.proxy];
|
|
388
476
|
}
|
|
@@ -461,6 +549,12 @@ function configureBlacReact(config) {
|
|
|
461
549
|
//#endregion
|
|
462
550
|
exports.BlocProvider = BlocProvider;
|
|
463
551
|
exports.configureBlacReact = configureBlacReact;
|
|
552
|
+
Object.defineProperty(exports, "untracked", {
|
|
553
|
+
enumerable: true,
|
|
554
|
+
get: function() {
|
|
555
|
+
return _dirtytalk_structural.untracked;
|
|
556
|
+
}
|
|
557
|
+
});
|
|
464
558
|
exports.useBloc = useBloc;
|
|
465
559
|
exports.useProvidedArgs = useProvidedArgs;
|
|
466
560
|
|