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