@blac/react 2.0.20 → 2.0.22

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createContext, useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef } from "react";
2
- import { DEP_BRAND, getRegistry, resolveInstanceKey } from "@blac/core";
1
+ import { createContext, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from "react";
2
+ import { APPLY_DEPS, DEP_BRAND, REMOVE_DEPS_OWNER, WITH_TRACKED_STATE, getRegistry, resolveInstanceKey } from "@blac/core";
3
3
  import { ALL_PATHS, ProxyCache, emptyPathSet, pathSetEquals, trackRender, untracked } from "@dirtytalk/structural";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  //#region src/BlocProvider.tsx
@@ -29,6 +29,7 @@ const ProvidedArgsContext = createContext(/* @__PURE__ */ new Map());
29
29
  * <CartWidget />
30
30
  * </BlocProvider>
31
31
  * ```
32
+ * @public
32
33
  */
33
34
  function BlocProvider({ bloc, args, children }) {
34
35
  const parentMap = useContext(ProvidedArgsContext);
@@ -39,7 +40,7 @@ function BlocProvider({ bloc, args, children }) {
39
40
  }, [
40
41
  parentMap,
41
42
  bloc,
42
- args
43
+ resolveInstanceKey(bloc, args)
43
44
  ]);
44
45
  return /* @__PURE__ */ jsx(ProvidedArgsContext.Provider, {
45
46
  value: mergedMap,
@@ -51,69 +52,107 @@ function BlocProvider({ bloc, args, children }) {
51
52
  * bloc class, or `undefined` when called outside a matching provider.
52
53
  *
53
54
  * Used by `useBloc` to inherit provider args when no own `args` are given.
55
+ * @public
54
56
  */
55
57
  function useProvidedArgs(BlocClass) {
56
58
  return useContext(ProvidedArgsContext).get(BlocClass);
57
59
  }
58
60
  //#endregion
61
+ //#region src/RegistryProvider.tsx
62
+ /**
63
+ * Internal context value: a registry to use in place of the module-global
64
+ * default (`getRegistry()`), or `null` when no provider is present.
65
+ */
66
+ const RegistryContext = createContext(null);
67
+ /**
68
+ * Scopes descendant `useBloc` calls to a specific registry instead of the
69
+ * module-global default.
70
+ *
71
+ * Useful for test isolation, SSR isolation, and micro-frontends that must
72
+ * not share bloc instances with the host page.
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * <RegistryProvider registry={new StateContainerRegistry()}>
77
+ * <App />
78
+ * </RegistryProvider>
79
+ * ```
80
+ * @public
81
+ */
82
+ function RegistryProvider({ registry, children }) {
83
+ return /* @__PURE__ */ jsx(RegistryContext.Provider, {
84
+ value: registry,
85
+ children
86
+ });
87
+ }
88
+ //#endregion
59
89
  //#region src/buildTrackedProxy.ts
90
+ function supportsTrackedState(value) {
91
+ return typeof value[WITH_TRACKED_STATE] === "function";
92
+ }
93
+ const gettersByProto = /* @__PURE__ */ new WeakMap();
94
+ function collectGetters(proto) {
95
+ const cached = gettersByProto.get(proto);
96
+ if (cached !== void 0) return cached;
97
+ const getters = /* @__PURE__ */ new Map();
98
+ let p = proto;
99
+ while (p !== null && p !== Object.prototype) {
100
+ for (const key of Reflect.ownKeys(p)) {
101
+ const desc = Object.getOwnPropertyDescriptor(p, key);
102
+ if (desc?.get && !getters.has(key)) getters.set(key, desc.get);
103
+ }
104
+ p = Object.getPrototypeOf(p);
105
+ }
106
+ gettersByProto.set(proto, getters);
107
+ return getters;
108
+ }
60
109
  /**
61
- * Build a per-consumer proxy pair for a bloc instance.
110
+ * Build a per-consumer proxy for a bloc instance.
62
111
  *
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.
112
+ * Getters run with `this` bound to the **real instance**, not a proxy, so ES
113
+ * `#private` fields and methods work in user blocs. Path recording is done by
114
+ * `[WITH_TRACKED_STATE]`, which makes the instance's own `state` getter report
115
+ * the render's tracking proxy for the duration of the call; nested getter
116
+ * chains stay tracked because the override is still set.
76
117
  *
77
- * Both allocations happen exactly once per bloc acquisition (inside `useMemo`)
78
- * so the proxies are stable across renders.
118
+ * One allocation per bloc acquisition (inside `useMemo`), so the proxy is
119
+ * stable across renders.
79
120
  *
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.
121
+ * @param onDepHandle - Optional callback invoked when a read returns a branded
122
+ * dep handle. Receives the original handle and returns the value to expose in
123
+ * its place (the session-bound wrapper). The callback is responsible for
124
+ * caching wrappers per handle to avoid re-allocation.
84
125
  */
85
126
  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);
127
+ const getters = collectGetters(Object.getPrototypeOf(instance));
128
+ const tracksState = supportsTrackedState(instance);
129
+ const boundMethods = /* @__PURE__ */ new Map();
130
+ const wrapDepHandle = (value) => {
99
131
  if (onDepHandle !== void 0 && (typeof value === "function" || typeof value === "object") && value !== null && value[DEP_BRAND] !== void 0) return onDepHandle(value);
100
132
  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
133
  };
134
+ return { proxy: new Proxy(instance, { get(target, key) {
135
+ const getter = getters.get(key);
136
+ if (getter !== void 0) {
137
+ const tracked = trackedStateRef.current;
138
+ return wrapDepHandle(tracked == null || !tracksState ? getter.call(target) : target[WITH_TRACKED_STATE](tracked, getter, onDepHandle));
139
+ }
140
+ const value = Reflect.get(target, key, target);
141
+ if (typeof value === "function") {
142
+ let bound = boundMethods.get(key);
143
+ if (bound === void 0) {
144
+ bound = value.bind(target);
145
+ boundMethods.set(key, bound);
146
+ }
147
+ return wrapDepHandle(bound);
148
+ }
149
+ return wrapDepHandle(value);
150
+ } }) };
110
151
  }
111
152
  //#endregion
112
153
  //#region src/useBloc.ts
113
154
  let nextConsumerId = 0;
114
155
  const ARGS_UNSET = Symbol("blac.argsKeyUnset");
115
- const primaryRefId = (consumerId) => `useBloc@${consumerId}`;
116
- const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
117
156
  /**
118
157
  * React hook that connects a component to a state container with automatic
119
158
  * re-render on state changes.
@@ -122,7 +161,7 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
122
161
  * - **Auto-tracking** (default): the returned state value is a proxy that
123
162
  * records read paths during render. The component re-renders when any
124
163
  * recorded path changes. Backed by `@dirtytalk/structural`'s
125
- * {@link trackRender} + the container's path-scoped `DirtyChannel`.
164
+ * `trackRender` + the container's path-scoped `DirtyChannel`.
126
165
  * - **Manual select**: pass `options.select` to opt out of auto-tracking.
127
166
  * The hook re-renders only when the returned array's elements change
128
167
  * (per-index `Object.is`).
@@ -142,7 +181,7 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
142
181
  * const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });
143
182
  * ```
144
183
  *
145
- * @template T - The state container constructor type (inferred from BlocClass)
184
+ * @typeParam T - The state container constructor type (inferred from BlocClass)
146
185
  * @param BlocClass - The state container class to connect to
147
186
  * @param options - Configuration options
148
187
  * @returns Tuple of `[state, bloc, ref]`
@@ -163,68 +202,49 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
163
202
  * ```ts
164
203
  * const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });
165
204
  * ```
205
+ * @public
166
206
  */
167
207
  function useBloc(BlocClass, options) {
168
- const componentRef = useRef({});
169
- const consumerIdRef = useRef(null);
170
- if (consumerIdRef.current === null) consumerIdRef.current = `useBloc-${nextConsumerId++}`;
171
- const consumerId = consumerIdRef.current;
172
- useId();
173
- const selectRef = useRef(options?.select);
174
- selectRef.current = options?.select;
175
- const onMountRef = useRef(options?.onMount);
176
- onMountRef.current = options?.onMount;
177
- const onUnmountRef = useRef(options?.onUnmount);
178
- onUnmountRef.current = options?.onUnmount;
208
+ const registry = useContext(RegistryContext) ?? getRegistry();
209
+ const consumerRef = useRef(null);
210
+ const consumer = consumerRef.current ??= createConsumer();
211
+ const consumerId = consumer.id;
212
+ consumer.select = options?.select;
213
+ consumer.onMount = options?.onMount;
214
+ consumer.onUnmount = options?.onUnmount;
179
215
  const ownArgs = options?.args;
180
- const ownArgsRef = useRef(ownArgs);
181
- ownArgsRef.current = ownArgs;
182
- const ownArgsKeyRef = useRef({
183
- ref: ARGS_UNSET,
184
- key: void 0
185
- });
186
- if (!Object.is(ownArgsKeyRef.current.ref, ownArgs)) ownArgsKeyRef.current = {
187
- ref: ownArgs,
188
- key: ownArgs === void 0 ? void 0 : JSON.stringify(ownArgs)
189
- };
190
- const ownArgsKey = ownArgsKeyRef.current.key;
216
+ consumer.ownArgs = ownArgs;
217
+ if (!Object.is(consumer.ownArgsKeyFor, ownArgs)) {
218
+ consumer.ownArgsKeyFor = ownArgs;
219
+ consumer.ownArgsKey = ownArgs === void 0 ? void 0 : JSON.stringify(ownArgs);
220
+ }
221
+ const ownArgsKey = consumer.ownArgsKey;
191
222
  const providerArgs = useProvidedArgs(BlocClass);
192
- const providerArgsRef = useRef(providerArgs);
193
- providerArgsRef.current = providerArgs;
194
- const providerArgsKeyRef = useRef({
195
- ref: ARGS_UNSET,
196
- key: void 0
197
- });
198
- if (!Object.is(providerArgsKeyRef.current.ref, providerArgs)) providerArgsKeyRef.current = {
199
- ref: providerArgs,
200
- key: providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs)
201
- };
202
- const providerArgsKey = providerArgsKeyRef.current.key;
203
- const trackedStateRef = useRef(null);
204
- const sessionRef = useRef(/* @__PURE__ */ new Map());
205
- const depSubsRef = useRef(/* @__PURE__ */ new Map());
206
- const depWrapperCacheRef = useRef(/* @__PURE__ */ new Map());
207
- const proxyCacheRef = useRef(new ProxyCache());
208
- const lastReconcileRef = useRef(null);
209
- const [rebindNonce, bumpRebind] = useReducer((x) => x + 1, 0);
210
- const ownedBlocRef = useRef(null);
223
+ consumer.providerArgs = providerArgs;
224
+ if (!Object.is(consumer.providerArgsKeyFor, providerArgs)) {
225
+ consumer.providerArgsKeyFor = providerArgs;
226
+ consumer.providerArgsKey = providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs);
227
+ }
228
+ const providerArgsKey = consumer.providerArgsKey;
229
+ const rebindNonce = consumer.rebindNonce;
211
230
  const { bloc, instanceKey, trackedBloc } = useMemo(() => {
212
- const effectiveArgs = ownArgsRef.current !== void 0 ? ownArgsRef.current : providerArgsRef.current;
231
+ const effectiveArgs = resolveEffectiveArgs(consumer);
213
232
  const resolvedKey = resolveInstanceKey(BlocClass, effectiveArgs);
214
- const instance = getRegistry().acquire(BlocClass, resolvedKey, {
233
+ const instance = registry.acquire(BlocClass, resolvedKey, {
215
234
  canCreate: true,
216
235
  countRef: false,
217
- args: effectiveArgs
236
+ args: effectiveArgs,
237
+ sweepIfUnowned: true
218
238
  });
219
239
  const onDepHandle = (handle) => {
220
- const cache = depWrapperCacheRef.current;
240
+ const cache = consumer.depWrappers ??= /* @__PURE__ */ new Map();
221
241
  const cached = cache.get(handle);
222
242
  if (cached !== void 0) return cached;
223
- const wrapper = makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, onDepHandle);
243
+ const wrapper = makeDepWrapper(handle, registry, consumer, onDepHandle);
224
244
  cache.set(handle, wrapper);
225
245
  return wrapper;
226
246
  };
227
- const { proxy } = buildTrackedProxy(instance, trackedStateRef, onDepHandle);
247
+ const { proxy } = buildTrackedProxy(instance, consumer.tracked, onDepHandle);
228
248
  return {
229
249
  bloc: instance,
230
250
  instanceKey: resolvedKey,
@@ -234,101 +254,98 @@ function useBloc(BlocClass, options) {
234
254
  BlocClass,
235
255
  ownArgsKey,
236
256
  providerArgsKey,
237
- rebindNonce
257
+ rebindNonce,
258
+ registry
238
259
  ]);
239
- const [, force] = useReducer((x) => x + 1, 0);
240
- const pathRef = useRef(emptyPathSet());
241
- const expandedInterestRef = useRef(emptyPathSet());
242
- const lastSelectionRef = useRef(null);
243
- const renderStateRef = useRef(void 0);
244
- const prevBlocRef = useRef(null);
245
- useEffect(() => {
246
- const channel = bloc.channel;
247
- if (selectRef.current !== void 0) {
248
- const unsub = channel.subscribe(() => ALL_PATHS, () => {
249
- const select = selectRef.current;
250
- if (!select) {
251
- force();
252
- return;
253
- }
254
- const next = select(bloc.state, bloc);
255
- const prev = lastSelectionRef.current;
256
- if (prev !== null && shallowArrayEqual(prev, next)) return;
257
- lastSelectionRef.current = next;
258
- force();
259
- });
260
- const select = selectRef.current;
261
- if (select) {
262
- const next = select(bloc.state, bloc);
263
- const prev = lastSelectionRef.current;
264
- if (prev === null || !shallowArrayEqual(prev, next)) {
265
- lastSelectionRef.current = next;
266
- force();
260
+ consumer.container = bloc;
261
+ useSyncExternalStore(useMemo(() => (onStoreChange) => {
262
+ consumer.notify = onStoreChange;
263
+ const container = consumer.container;
264
+ const unsubscribe = container.channel.subscribe(() => consumer.interest, () => {
265
+ if (consumer.isSelectMode) {
266
+ const select = consumer.select;
267
+ if (select) {
268
+ const next = select(container.state, container);
269
+ const prev = consumer.selection;
270
+ if (prev !== null && shallowArrayEqual(prev, next)) return;
271
+ consumer.selection = next;
267
272
  }
268
273
  }
269
- return unsub;
270
- }
271
- const unsub = channel.subscribe(() => expandedInterestRef.current, () => force());
272
- bloc.registerConsumerPaths(consumerId, pathRef.current);
273
- if (bloc.state !== renderStateRef.current) force();
274
+ consumer.bump();
275
+ });
276
+ container.registerConsumerPaths(consumerId, consumer.paths);
274
277
  return () => {
275
- unsub();
276
- bloc.unregisterConsumer(consumerId);
278
+ consumer.notify = noop;
279
+ unsubscribe();
280
+ container.unregisterConsumer(consumerId);
277
281
  };
278
- }, [bloc, consumerId]);
282
+ }, [
283
+ BlocClass,
284
+ instanceKey,
285
+ consumerId,
286
+ rebindNonce
287
+ ]), consumer.getSnapshot, consumer.getServerSnapshot);
279
288
  useLayoutEffect(() => {
280
- const registry = getRegistry();
281
289
  const live = registry.acquire(BlocClass, instanceKey, {
282
290
  canCreate: true,
283
291
  countRef: true,
284
- refId: primaryRefId(consumerId)
292
+ refId: consumer.primaryRefId,
293
+ args: resolveEffectiveArgs(consumer)
285
294
  });
286
- ownedBlocRef.current = live;
287
- onMountRef.current?.(live);
288
- if (live !== bloc) bumpRebind();
295
+ consumer.ownedBloc = live;
296
+ consumer.onMount?.(live);
297
+ if (live !== bloc) {
298
+ consumer.rebindNonce++;
299
+ consumer.bump();
300
+ }
289
301
  return () => {
290
- onUnmountRef.current?.(ownedBlocRef.current ?? bloc);
291
- registry.release(BlocClass, instanceKey, false, primaryRefId(consumerId));
302
+ consumer.onUnmount?.(consumer.ownedBloc ?? bloc);
303
+ registry.release(BlocClass, instanceKey, false, consumer.primaryRefId);
292
304
  };
293
305
  }, [
294
306
  BlocClass,
295
307
  instanceKey,
296
- consumerId
308
+ consumerId,
309
+ registry
297
310
  ]);
298
- const rawState = bloc.state;
299
- renderStateRef.current = rawState;
300
- if (prevBlocRef.current !== bloc) {
301
- prevBlocRef.current = bloc;
302
- lastSelectionRef.current = null;
303
- }
311
+ const container = consumer.container;
312
+ const rawState = container.state;
313
+ const select = consumer.select;
314
+ consumer.isSelectMode = select !== void 0;
304
315
  let state;
305
- if (selectRef.current !== void 0) {
316
+ if (select !== void 0) {
306
317
  state = rawState;
307
- if (lastSelectionRef.current === null) lastSelectionRef.current = selectRef.current(rawState, bloc);
318
+ if (consumer.selection === null) consumer.selection = select(rawState, bloc);
319
+ consumer.interest = ALL_PATHS;
308
320
  } else {
309
- const tracked = trackRender(rawState, bloc.interner, proxyCacheRef.current);
321
+ const tracked = trackRender(rawState, container.interner, consumer.proxyCache ??= new ProxyCache());
310
322
  state = tracked.value;
311
- trackedStateRef.current = tracked.value;
312
- pathRef.current = tracked.paths;
313
- const session = sessionRef.current;
323
+ consumer.tracked.current = tracked.value;
324
+ consumer.paths = tracked.paths;
325
+ consumer.disarm = tracked.disarm;
326
+ const session = consumer.session;
314
327
  session.clear();
315
- session.set(bloc, {
328
+ session.set(container, {
316
329
  kind: "primary",
317
330
  paths: tracked.paths
318
331
  });
319
- queueMicrotask(tracked.disarm);
320
332
  }
321
333
  useLayoutEffect(() => {
322
- trackedStateRef.current = null;
323
- if (selectRef.current !== void 0) {
324
- lastReconcileRef.current = null;
334
+ consumer.tracked.current = null;
335
+ const disarm = consumer.disarm;
336
+ if (disarm !== null) {
337
+ consumer.disarm = null;
338
+ disarm();
339
+ }
340
+ if (consumer.isSelectMode) {
341
+ consumer.lastReconcile = null;
325
342
  return;
326
343
  }
327
- const container = bloc;
328
- const paths = pathRef.current;
329
- const session = sessionRef.current;
330
- const last = lastReconcileRef.current;
331
- if (last !== null && last.primaryContainer === container && pathSetEquals(last.primaryPaths, paths)) {
344
+ const primary = consumer.container;
345
+ const paths = consumer.paths;
346
+ const session = consumer.session;
347
+ const last = consumer.lastReconcile;
348
+ if (last !== null && last.primaryContainer === primary && pathSetEquals(last.primaryPaths, paths)) {
332
349
  let unchanged = last.deps.size === session.size - 1;
333
350
  if (unchanged) for (const [depContainer, entry] of session) {
334
351
  if (entry.kind === "primary") continue;
@@ -340,13 +357,13 @@ function useBloc(BlocClass, options) {
340
357
  }
341
358
  if (unchanged) return;
342
359
  }
343
- container.registerConsumerPaths(consumerId, paths);
344
- expandedInterestRef.current = expandWithAncestors(paths, container.interner);
345
- const subs = depSubsRef.current;
360
+ primary.registerConsumerPaths(consumerId, paths);
361
+ consumer.interest = expandWithAncestors(paths, primary.interner);
362
+ const subs = consumer.depSubs;
346
363
  for (const [depContainer, sub] of subs) if (!session.has(depContainer)) {
347
364
  sub.unsubscribe();
348
365
  depContainer.unregisterConsumer(consumerId);
349
- getRegistry().release(sub.Type, sub.key, false, sub.refId);
366
+ registry.release(sub.Type, sub.key, false, sub.refId);
350
367
  subs.delete(depContainer);
351
368
  }
352
369
  for (const [depContainer, entry] of session) {
@@ -358,14 +375,14 @@ function useBloc(BlocClass, options) {
358
375
  existing.interestRef.current = interest;
359
376
  continue;
360
377
  }
361
- getRegistry().acquire(entry.Type, entry.key, {
378
+ registry.acquire(entry.Type, entry.key, {
362
379
  canCreate: true,
363
380
  countRef: true,
364
381
  refId: entry.refId,
365
382
  args: entry.args
366
383
  });
367
384
  const interestRef = { current: interest };
368
- const unsubscribe = depContainer.channel.subscribe(() => interestRef.current, () => force());
385
+ const unsubscribe = depContainer.channel.subscribe(() => interestRef.current, () => consumer.bump());
369
386
  subs.set(depContainer, {
370
387
  unsubscribe,
371
388
  interestRef,
@@ -385,35 +402,73 @@ function useBloc(BlocClass, options) {
385
402
  args: entry.args
386
403
  });
387
404
  }
388
- lastReconcileRef.current = {
389
- primaryContainer: container,
405
+ consumer.lastReconcile = {
406
+ primaryContainer: primary,
390
407
  primaryPaths: paths,
391
408
  deps: depsSignature
392
409
  };
393
410
  });
394
411
  useEffect(() => {
395
- const subs = depSubsRef.current;
412
+ const subs = consumer.depSubs;
396
413
  return () => {
397
414
  for (const [depContainer, sub] of subs) {
398
415
  sub.unsubscribe();
399
416
  depContainer.unregisterConsumer(consumerId);
400
- getRegistry().release(sub.Type, sub.key, false, sub.refId);
417
+ registry.release(sub.Type, sub.key, false, sub.refId);
401
418
  }
402
419
  subs.clear();
403
420
  };
404
- }, [consumerId]);
405
- return [
406
- state,
407
- trackedBloc,
408
- componentRef
409
- ];
421
+ }, [consumerId, registry]);
422
+ return [state, trackedBloc];
423
+ }
424
+ const noop = () => {};
425
+ function createConsumer() {
426
+ const id = `useBloc-${nextConsumerId++}`;
427
+ const consumer = {
428
+ id,
429
+ primaryRefId: `useBloc@${id}`,
430
+ depRefId: `useBloc@${id}:dep`,
431
+ container: null,
432
+ version: 0,
433
+ notify: noop,
434
+ bump: () => {
435
+ consumer.version++;
436
+ consumer.notify();
437
+ },
438
+ getSnapshot: () => consumer.version,
439
+ getServerSnapshot: () => consumer.version,
440
+ interest: emptyPathSet(),
441
+ paths: emptyPathSet(),
442
+ isSelectMode: false,
443
+ selection: null,
444
+ session: /* @__PURE__ */ new Map(),
445
+ depSubs: /* @__PURE__ */ new Map(),
446
+ lastReconcile: null,
447
+ select: void 0,
448
+ onMount: void 0,
449
+ onUnmount: void 0,
450
+ ownArgs: void 0,
451
+ ownArgsKeyFor: ARGS_UNSET,
452
+ ownArgsKey: void 0,
453
+ providerArgs: void 0,
454
+ providerArgsKeyFor: ARGS_UNSET,
455
+ providerArgsKey: void 0,
456
+ tracked: { current: null },
457
+ disarm: null,
458
+ proxyCache: null,
459
+ depWrappers: null,
460
+ rebindNonce: 0,
461
+ ownedBloc: null
462
+ };
463
+ return consumer;
410
464
  }
465
+ const resolveEffectiveArgs = (consumer) => consumer.ownArgs !== void 0 ? consumer.ownArgs : consumer.providerArgs;
411
466
  /**
412
467
  * Build the per-consumer wrapper that replaces a branded dep handle inside a
413
468
  * tracked getter's `this`. The wrapper exposes the same accessors as the core
414
469
  * handle and overrides `.track()`:
415
470
  *
416
- * - **Inside a render** (`trackedStateRef.current != null`): resolve (ENSURE,
471
+ * - **Inside a render** (`consumer.tracked.current != null`): resolve (ENSURE,
417
472
  * no ref) the dep, `trackRender` its state, merge the recorded paths into the
418
473
  * session entry, build/reuse a tracked proxy for the dep so its OWN getters
419
474
  * track too, and return `[trackedValue, depProxy]`. The ownership ref is taken
@@ -430,10 +485,9 @@ function useBloc(BlocClass, options) {
430
485
  * the dep already has a non-primary session entry this render, reuse its proxy
431
486
  * + union its paths instead of re-acquiring.
432
487
  */
433
- function makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, onDepHandle) {
488
+ function makeDepWrapper(handle, registry, consumer, onDepHandle) {
434
489
  const brand = handle[DEP_BRAND];
435
- const refId = depRefId(consumerId);
436
- const registry = getRegistry();
490
+ const refId = consumer.depRefId;
437
491
  const perDep = /* @__PURE__ */ new Map();
438
492
  const proxyCache = new ProxyCache();
439
493
  const resolve = (options) => {
@@ -449,8 +503,8 @@ function makeDepWrapper(handle, consumerId, trackedStateRef, sessionRef, onDepHa
449
503
  untracked: (options) => resolve(options).dep,
450
504
  track: (options) => {
451
505
  const { dep, key, args } = resolve(options);
452
- if (trackedStateRef.current == null) return [dep.state, dep];
453
- const session = sessionRef.current;
506
+ if (consumer.tracked.current == null) return [dep.state, dep];
507
+ const session = consumer.session;
454
508
  const existing = session.get(dep);
455
509
  const tracked = trackRender(dep.state, dep.interner, proxyCache);
456
510
  let cache = perDep.get(dep);
@@ -506,7 +560,7 @@ const shallowArrayEqual = (a, b) => {
506
560
  * would miss a `patch`-triggered atomic-replacement of `items`.
507
561
  *
508
562
  * Ancestors are added under the interner's *ancestor-watch* lane
509
- * (`internAncestor`), NOT as normal ids. The source emits a matching
563
+ * (`ancestorWatchIds`), NOT as normal ids. The source emits a matching
510
564
  * ancestor-watch mark only for paths it replaces atomically — never for a
511
565
  * plain-object structural pulse-up. So `{'items.length'}` wakes when the array
512
566
  * `items` is replaced, but `{'user.email'}` does NOT wake when a sibling
@@ -516,36 +570,55 @@ const shallowArrayEqual = (a, b) => {
516
570
  * Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but
517
571
  * NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,
518
572
  * and `''` would wake this consumer on every field change).
573
+ *
574
+ * Returns `paths` itself when no leaf has an ancestor (top-level fields only);
575
+ * the set is frozen by then (see `Consumer.disarm`), so sharing it is safe.
519
576
  */
520
577
  function expandWithAncestors(paths, interner) {
521
578
  if (paths === ALL_PATHS) return ALL_PATHS;
522
579
  const leafPaths = paths;
523
- if (leafPaths.size === 0) return paths;
524
- const expanded = new Set(leafPaths);
580
+ let expanded;
525
581
  for (const id of leafPaths) {
526
- const str = interner.lookup(id);
527
- let idx = str.lastIndexOf(".");
528
- while (idx > 0) {
529
- const ancestor = str.slice(0, idx);
530
- expanded.add(interner.internAncestor(ancestor));
531
- idx = ancestor.lastIndexOf(".");
532
- }
582
+ const watch = interner.ancestorWatchIds(id);
583
+ if (watch.length === 0) continue;
584
+ expanded ??= new Set(leafPaths);
585
+ for (let i = 0; i < watch.length; i++) expanded.add(watch[i]);
533
586
  }
534
- return expanded;
587
+ return expanded ?? paths;
535
588
  }
536
- let globalConfig = {};
589
+ //#endregion
590
+ //#region src/useBlocDeps.ts
537
591
  /**
538
- * Configure global defaults for `@blac/react` hooks.
592
+ * Feed live values from the component into a bloc's `deps` lane.
593
+ *
594
+ * Each calling component is one owner: its slice is shallow-merged into the
595
+ * bloc's merged `deps` view, and withdrawn when the component unmounts. The
596
+ * bloc observes changes through `onDepsChanged`.
597
+ *
598
+ * ```tsx
599
+ * const [state, bloc] = useBloc(CanvasCubit);
600
+ * useBlocDeps(bloc, { canvas, onTick });
601
+ * ```
539
602
  *
540
- * @param config - Partial configuration to merge with current globals
603
+ * Use this for values a bloc must *read* but should not own — DOM handles,
604
+ * callbacks, values from other hooks. Pass identity-stable values (`useMemo` /
605
+ * `useCallback`) as you would to any hook; a new function identity each render
606
+ * counts as a change.
541
607
  */
542
- function configureBlacReact(config) {
543
- globalConfig = {
544
- ...globalConfig,
545
- ...config
546
- };
608
+ function useBlocDeps(bloc, slice) {
609
+ const ownerId = useId();
610
+ const latest = useRef(slice);
611
+ latest.current = slice;
612
+ useEffect(() => {
613
+ bloc[APPLY_DEPS](ownerId, latest.current);
614
+ });
615
+ useEffect(() => {
616
+ return () => {
617
+ bloc[REMOVE_DEPS_OWNER](ownerId);
618
+ };
619
+ }, [bloc, ownerId]);
547
620
  }
548
621
  //#endregion
549
- export { BlocProvider, configureBlacReact, untracked, useBloc, useProvidedArgs };
622
+ export { BlocProvider, RegistryProvider, untracked, useBloc, useBlocDeps, useProvidedArgs };
550
623
 
551
624
  //# sourceMappingURL=index.js.map