@blac/react 2.0.21 → 2.1.1

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
@@ -1,20 +1,18 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_RegistryProvider = require("./RegistryProvider-F2qb_prD.cjs");
2
3
  let react = require("react");
3
- let _blac_core = require("@blac/core");
4
4
  let _dirtytalk_structural = require("@dirtytalk/structural");
5
5
  let react_jsx_runtime = require("react/jsx-runtime");
6
+ let _blac_core_internal = require("@blac/core/internal");
6
7
  //#region src/BlocProvider.tsx
7
8
  const ProvidedArgsContext = (0, react.createContext)(/* @__PURE__ */ new Map());
8
9
  /**
9
10
  * Provides args to descendant `useBloc` calls for a specific bloc class via
10
- * React context.
11
- *
12
- * Descendants calling `useBloc(Bloc)` without their own `args` resolve to the
13
- * args supplied here. Own `args` on the `useBloc` call always win.
11
+ * React context. Own `args` on the `useBloc` call always win.
14
12
  *
15
13
  * Multiple `BlocProvider` wrappers for different bloc classes compose: each
16
- * provider merges its entry into the inherited map, so nested providers for
17
- * different blocs do not interfere.
14
+ * merges its entry into the inherited map, so nested providers for different
15
+ * blocs do not interfere.
18
16
  *
19
17
  * @example
20
18
  * ```tsx
@@ -41,7 +39,7 @@ function BlocProvider({ bloc, args, children }) {
41
39
  }, [
42
40
  parentMap,
43
41
  bloc,
44
- (0, _blac_core.resolveInstanceKey)(bloc, args)
42
+ JSON.stringify(args)
45
43
  ]);
46
44
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidedArgsContext.Provider, {
47
45
  value: mergedMap,
@@ -59,96 +57,274 @@ function useProvidedArgs(BlocClass) {
59
57
  return (0, react.useContext)(ProvidedArgsContext).get(BlocClass);
60
58
  }
61
59
  //#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
90
60
  //#region src/buildTrackedProxy.ts
91
61
  function supportsTrackedState(value) {
92
- return typeof value[_blac_core.WITH_TRACKED_STATE] === "function";
62
+ return typeof value[_blac_core_internal.WITH_TRACKED_STATE] === "function";
63
+ }
64
+ const gettersByProto = /* @__PURE__ */ new WeakMap();
65
+ function collectGetters(proto) {
66
+ const cached = gettersByProto.get(proto);
67
+ if (cached !== void 0) return cached;
68
+ const getters = /* @__PURE__ */ new Map();
69
+ let p = proto;
70
+ while (p !== null && p !== Object.prototype) {
71
+ for (const key of Reflect.ownKeys(p)) {
72
+ const desc = Object.getOwnPropertyDescriptor(p, key);
73
+ if (desc?.get && !getters.has(key)) getters.set(key, desc.get);
74
+ }
75
+ p = Object.getPrototypeOf(p);
76
+ }
77
+ gettersByProto.set(proto, getters);
78
+ return getters;
93
79
  }
94
80
  /**
95
81
  * Build a per-consumer proxy for a bloc instance.
96
82
  *
97
- * Getters run with `this` bound to the **real instance**, not a proxy, so ES
83
+ * Getters run with `this` bound to the real instance, not the proxy, so ES
98
84
  * `#private` fields and methods work in user blocs. Path recording is done by
99
- * `[WITH_TRACKED_STATE]`, which makes the instance's own `state` getter report
100
- * the render's tracking proxy for the duration of the call; nested getter
101
- * chains stay tracked because the override is still set.
85
+ * `[WITH_TRACKED_STATE]`, which makes the instance's own `state` getter
86
+ * report the render's tracking proxy for the call's duration; nested getter
87
+ * chains stay tracked because the override remains set.
102
88
  *
103
89
  * One allocation per bloc acquisition (inside `useMemo`), so the proxy is
104
90
  * stable across renders.
105
91
  *
106
- * @param onDepHandle - Optional callback invoked when a read returns a branded
107
- * dep handle. Receives the original handle and returns the value to expose in
108
- * its place (the session-bound wrapper). The callback is responsible for
109
- * caching wrappers per handle to avoid re-allocation.
92
+ * @param onDepHandle - Invoked when a read returns a branded dep handle;
93
+ * returns the value (the session-bound wrapper) to expose in its place.
94
+ * Must cache wrappers per handle to avoid re-allocation.
110
95
  */
111
96
  function buildTrackedProxy(instance, trackedStateRef, onDepHandle) {
112
- const getterDescs = /* @__PURE__ */ new Map();
113
- let proto = Object.getPrototypeOf(instance);
114
- while (proto && proto !== Object.prototype) {
115
- const keys = [...Object.getOwnPropertyNames(proto), ...Object.getOwnPropertySymbols(proto)];
116
- for (const key of keys) {
117
- const desc = Object.getOwnPropertyDescriptor(proto, key);
118
- if (desc?.get && !getterDescs.has(key)) getterDescs.set(key, desc);
119
- }
120
- proto = Object.getPrototypeOf(proto);
121
- }
97
+ const getters = collectGetters(Object.getPrototypeOf(instance));
98
+ const tracksState = supportsTrackedState(instance);
122
99
  const boundMethods = /* @__PURE__ */ new Map();
123
100
  const wrapDepHandle = (value) => {
124
- if (onDepHandle !== void 0 && (typeof value === "function" || typeof value === "object") && value !== null && value[_blac_core.DEP_BRAND] !== void 0) return onDepHandle(value);
101
+ if (onDepHandle !== void 0 && (typeof value === "function" || typeof value === "object") && value !== null && value[_blac_core_internal.DEP_BRAND] !== void 0) return onDepHandle(value);
125
102
  return value;
126
103
  };
127
- return { proxy: new Proxy(instance, { get(target, key) {
128
- const getter = getterDescs.get(key)?.get;
129
- if (getter) {
130
- const tracked = trackedStateRef.current;
131
- const read = () => getter.call(target);
132
- return wrapDepHandle(tracked == null || !supportsTrackedState(target) ? read() : target[_blac_core.WITH_TRACKED_STATE](tracked, read, onDepHandle));
104
+ return { proxy: new Proxy(instance, {
105
+ get(target, key) {
106
+ const getter = getters.get(key);
107
+ if (getter !== void 0) {
108
+ const tracked = trackedStateRef.current;
109
+ return wrapDepHandle(tracked == null || !tracksState ? getter.call(target) : target[_blac_core_internal.WITH_TRACKED_STATE](tracked, getter, onDepHandle));
110
+ }
111
+ const value = Reflect.get(target, key, target);
112
+ if (typeof value === "function") {
113
+ const fn = value;
114
+ let cached = boundMethods.get(key);
115
+ if (cached === void 0 || cached.fn !== fn) {
116
+ cached = {
117
+ fn,
118
+ bound: fn.bind(target)
119
+ };
120
+ boundMethods.set(key, cached);
121
+ }
122
+ return wrapDepHandle(cached.bound);
123
+ }
124
+ return wrapDepHandle(value);
125
+ },
126
+ set(target, key, value) {
127
+ return Reflect.set(target, key, value, target);
128
+ }
129
+ }) };
130
+ }
131
+ //#endregion
132
+ //#region src/expandWithAncestors.ts
133
+ /**
134
+ * Add an ancestor-watch id for every ancestor of every tracked leaf, so a
135
+ * `patch` that replaces a parent atomically (e.g. the array `items`) wakes a
136
+ * consumer that read a child (`items.length`).
137
+ *
138
+ * Ancestor-watch ids only match marks the source emits for atomic
139
+ * replacements, never a plain-object pulse-up: `{'user.email'}` does not wake
140
+ * when a sibling `user.name` changes. The `''` root is never added; a root
141
+ * change already marks `ALL_PATHS`.
142
+ *
143
+ * Returns `paths` itself when no leaf has an ancestor.
144
+ */
145
+ function expandWithAncestors(paths, interner) {
146
+ if (paths === _dirtytalk_structural.ALL_PATHS) return _dirtytalk_structural.ALL_PATHS;
147
+ const leafPaths = paths;
148
+ let expanded;
149
+ for (const id of leafPaths) {
150
+ const watch = interner.ancestorWatchIds(id);
151
+ if (watch.length === 0) continue;
152
+ expanded ??= new Set(leafPaths);
153
+ for (let i = 0; i < watch.length; i++) expanded.add(watch[i]);
154
+ }
155
+ return expanded ?? paths;
156
+ }
157
+ //#endregion
158
+ //#region src/depSession.ts
159
+ /**
160
+ * The containers one `useBloc` consumer reads during a render: its primary
161
+ * bloc plus every dep reached through `this.<handle>.track()`.
162
+ *
163
+ * The render records entries; the commit calls `reconcile`, which subscribes
164
+ * and takes a ref on new deps and drops the ones no longer read. `dispose`
165
+ * releases everything. Refs are only taken at commit, so an uncommitted render
166
+ * never leaks one.
167
+ */
168
+ var DepSession = class {
169
+ constructor(consumerId, onChange) {
170
+ this.consumerId = consumerId;
171
+ this.onChange = onChange;
172
+ this.entries = /* @__PURE__ */ new Map();
173
+ this.subs = /* @__PURE__ */ new Map();
174
+ this.lastReconciled = null;
175
+ this.refId = `useBloc@${consumerId}:dep`;
176
+ }
177
+ begin(primary, paths) {
178
+ this.entries.clear();
179
+ this.entries.set(primary, {
180
+ kind: "primary",
181
+ paths
182
+ });
183
+ }
184
+ record(dep, paths, Type, key, args) {
185
+ const existing = this.entries.get(dep);
186
+ if (existing !== void 0) existing.paths = unionPaths(existing.paths, paths);
187
+ else this.entries.set(dep, {
188
+ kind: "dep",
189
+ paths,
190
+ Type,
191
+ key,
192
+ args
193
+ });
194
+ }
195
+ /** Forces the next `reconcile` to run in full. */
196
+ invalidate() {
197
+ this.lastReconciled = null;
198
+ }
199
+ isUnchanged() {
200
+ const last = this.lastReconciled;
201
+ if (last === null || last.size !== this.entries.size) return false;
202
+ for (const [container, entry] of this.entries) {
203
+ const prev = last.get(container);
204
+ if (prev === void 0 || !sameEntry(prev, entry)) return false;
205
+ }
206
+ return true;
207
+ }
208
+ reconcile(registry) {
209
+ const { consumerId, refId, subs, entries } = this;
210
+ for (const [dep, sub] of subs) if (!entries.has(dep)) {
211
+ this.drop(dep, sub);
212
+ subs.delete(dep);
133
213
  }
134
- const value = Reflect.get(target, key, target);
135
- if (typeof value === "function") {
136
- let bound = boundMethods.get(key);
137
- if (bound === void 0) {
138
- bound = value.bind(target);
139
- boundMethods.set(key, bound);
214
+ for (const [dep, entry] of entries) {
215
+ if (entry.kind === "primary") continue;
216
+ if (dep.$blac.disposed) {
217
+ this.onChange();
218
+ continue;
140
219
  }
141
- return wrapDepHandle(bound);
220
+ const interest = expandWithAncestors(entry.paths, dep.interner);
221
+ dep.registerConsumerPaths(consumerId, entry.paths);
222
+ const existing = subs.get(dep);
223
+ if (existing) {
224
+ existing.interestRef.current = interest;
225
+ continue;
226
+ }
227
+ if (registry.acquire(entry.Type, entry.key, {
228
+ canCreate: true,
229
+ countRef: true,
230
+ refId,
231
+ args: entry.args
232
+ }) !== dep) {
233
+ registry.release(entry.Type, entry.key, false, refId);
234
+ this.onChange();
235
+ continue;
236
+ }
237
+ const interestRef = { current: interest };
238
+ const unsubscribe = dep.channel.subscribe(() => interestRef.current, this.onChange);
239
+ subs.set(dep, {
240
+ unsubscribe,
241
+ interestRef,
242
+ registry,
243
+ Type: entry.Type,
244
+ key: entry.key
245
+ });
142
246
  }
143
- return wrapDepHandle(value);
144
- } }) };
247
+ this.lastReconciled = new Map(entries);
248
+ }
249
+ dispose() {
250
+ for (const [dep, sub] of this.subs) this.drop(dep, sub);
251
+ this.subs.clear();
252
+ this.lastReconciled = null;
253
+ }
254
+ drop(dep, sub) {
255
+ sub.unsubscribe();
256
+ dep.unregisterConsumer(this.consumerId);
257
+ sub.registry.release(sub.Type, sub.key, false, this.refId);
258
+ }
259
+ };
260
+ function sameEntry(a, b) {
261
+ if (a.kind !== b.kind || !(0, _dirtytalk_structural.pathSetEquals)(a.paths, b.paths)) return false;
262
+ if (a.kind === "primary" || b.kind === "primary") return true;
263
+ return a.key === b.key && Object.is(a.args, b.args);
264
+ }
265
+ function unionPaths(a, b) {
266
+ if (a === _dirtytalk_structural.ALL_PATHS || b === _dirtytalk_structural.ALL_PATHS) return _dirtytalk_structural.ALL_PATHS;
267
+ const out = new Set(a);
268
+ for (const id of b) out.add(id);
269
+ return out;
270
+ }
271
+ /**
272
+ * Wrap a dep handle read inside a tracked getter. During a render (`tracked`
273
+ * is set) `.track()` records the dep's read paths into `session` and returns
274
+ * a tracked proxy, so the dep's own getters track too. Outside a render it
275
+ * returns the live `[state, dep]`, like the core handle.
276
+ *
277
+ * Args resolve per call, so one handle can reach several instances; proxies
278
+ * are cached per resolved instance.
279
+ */
280
+ function makeDepWrapper(handle, registry, session, tracked, onDepHandle) {
281
+ const brand = handle[_blac_core_internal.DEP_BRAND];
282
+ const perDep = /* @__PURE__ */ new Map();
283
+ const proxyCache = new _dirtytalk_structural.ProxyCache();
284
+ const resolve = (options) => {
285
+ const args = options?.args ?? brand.defaultArgs;
286
+ const key = registry.resolveKey(brand.Type, void 0, args);
287
+ return {
288
+ dep: registry.acquire(brand.Type, key, {
289
+ canCreate: true,
290
+ countRef: false,
291
+ args,
292
+ sweepIfUnowned: tracked.current != null
293
+ }),
294
+ key,
295
+ args
296
+ };
297
+ };
298
+ const wrapper = {
299
+ untracked: (options) => resolve(options).dep,
300
+ track: (options) => {
301
+ const { dep, key, args } = resolve(options);
302
+ if (tracked.current == null) return [dep.state, dep];
303
+ const result = (0, _dirtytalk_structural.trackRender)(dep.state, dep.interner, proxyCache);
304
+ let cache = perDep.get(dep);
305
+ if (cache === void 0) {
306
+ const ref = { current: result.value };
307
+ cache = {
308
+ ref,
309
+ proxy: buildTrackedProxy(dep, ref, onDepHandle).proxy
310
+ };
311
+ perDep.set(dep, cache);
312
+ } else cache.ref.current = result.value;
313
+ session.record(dep, result.paths, brand.Type, key, args);
314
+ return [result.value, cache.proxy];
315
+ }
316
+ };
317
+ Object.defineProperty(wrapper, _blac_core_internal.DEP_BRAND, {
318
+ value: brand,
319
+ enumerable: false,
320
+ writable: false,
321
+ configurable: false
322
+ });
323
+ return wrapper;
145
324
  }
146
325
  //#endregion
147
326
  //#region src/useBloc.ts
148
327
  let nextConsumerId = 0;
149
- const ARGS_UNSET = Symbol("blac.argsKeyUnset");
150
- const primaryRefId = (consumerId) => `useBloc@${consumerId}`;
151
- const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
152
328
  /**
153
329
  * React hook that connects a component to a state container with automatic
154
330
  * re-render on state changes.
@@ -156,8 +332,7 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
156
332
  * Two tracking modes:
157
333
  * - **Auto-tracking** (default): the returned state value is a proxy that
158
334
  * records read paths during render. The component re-renders when any
159
- * recorded path changes. Backed by `@dirtytalk/structural`'s
160
- * `trackRender` + the container's path-scoped `DirtyChannel`.
335
+ * recorded path changes.
161
336
  * - **Manual select**: pass `options.select` to opt out of auto-tracking.
162
337
  * The hook re-renders only when the returned array's elements change
163
338
  * (per-index `Object.is`).
@@ -180,7 +355,7 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
180
355
  * @typeParam T - The state container constructor type (inferred from BlocClass)
181
356
  * @param BlocClass - The state container class to connect to
182
357
  * @param options - Configuration options
183
- * @returns Tuple of `[state, bloc, ref]`
358
+ * @returns Tuple of `[state, bloc]`
184
359
  *
185
360
  * @example Basic usage
186
361
  * ```ts
@@ -201,77 +376,41 @@ const depRefId = (consumerId) => `useBloc@${consumerId}:dep`;
201
376
  * @public
202
377
  */
203
378
  function useBloc(BlocClass, options) {
204
- const registry = (0, react.useContext)(RegistryContext) ?? (0, _blac_core.getRegistry)();
205
- const consumerIdRef = (0, react.useRef)(null);
206
- if (consumerIdRef.current === null) consumerIdRef.current = `useBloc-${nextConsumerId++}`;
207
- const consumerId = consumerIdRef.current;
208
- const selectRef = (0, react.useRef)(options?.select);
209
- selectRef.current = options?.select;
210
- const onMountRef = (0, react.useRef)(options?.onMount);
211
- onMountRef.current = options?.onMount;
212
- const onUnmountRef = (0, react.useRef)(options?.onUnmount);
213
- onUnmountRef.current = options?.onUnmount;
214
- const ownArgs = options?.args;
215
- const ownArgsRef = (0, react.useRef)(ownArgs);
216
- ownArgsRef.current = ownArgs;
217
- const ownArgsKeyRef = (0, react.useRef)({
218
- ref: ARGS_UNSET,
219
- key: void 0
220
- });
221
- if (!Object.is(ownArgsKeyRef.current.ref, ownArgs)) ownArgsKeyRef.current = {
222
- ref: ownArgs,
223
- key: ownArgs === void 0 ? void 0 : JSON.stringify(ownArgs)
224
- };
225
- const ownArgsKey = ownArgsKeyRef.current.key;
226
- const providerArgs = useProvidedArgs(BlocClass);
227
- const providerArgsRef = (0, react.useRef)(providerArgs);
228
- providerArgsRef.current = providerArgs;
229
- const providerArgsKeyRef = (0, react.useRef)({
230
- ref: ARGS_UNSET,
231
- key: void 0
232
- });
233
- if (!Object.is(providerArgsKeyRef.current.ref, providerArgs)) providerArgsKeyRef.current = {
234
- ref: providerArgs,
235
- key: providerArgs === void 0 ? void 0 : JSON.stringify(providerArgs)
236
- };
237
- const providerArgsKey = providerArgsKeyRef.current.key;
238
- const resolveEffectiveArgs = () => ownArgsRef.current !== void 0 ? ownArgsRef.current : providerArgsRef.current;
239
- const trackedStateRef = (0, react.useRef)(null);
240
- const proxyCacheRef = (0, react.useRef)(new _dirtytalk_structural.ProxyCache());
241
- const depWrapperCacheRef = (0, react.useRef)(/* @__PURE__ */ new Map());
242
- const rebindNonceRef = (0, react.useRef)(0);
243
- const rebindNonce = rebindNonceRef.current;
379
+ const registry = require_RegistryProvider.useRegistry();
244
380
  const consumerRef = (0, react.useRef)(null);
245
- consumerRef.current ??= createConsumer();
246
- const consumer = consumerRef.current;
247
- const ownedBlocRef = (0, react.useRef)(null);
248
- const { bloc, instanceKey, trackedBloc } = (0, react.useMemo)(() => {
249
- const effectiveArgs = resolveEffectiveArgs();
250
- const resolvedKey = (0, _blac_core.resolveInstanceKey)(BlocClass, effectiveArgs);
251
- const instance = registry.acquire(BlocClass, resolvedKey, {
381
+ const consumer = consumerRef.current ??= createConsumer();
382
+ const consumerId = consumer.id;
383
+ consumer.select = options?.select;
384
+ consumer.onMount = options?.onMount;
385
+ consumer.onUnmount = options?.onUnmount;
386
+ consumer.ownArgs = options?.args;
387
+ consumer.providerArgs = useProvidedArgs(BlocClass);
388
+ const effectiveArgs = resolveEffectiveArgs(consumer);
389
+ const instanceKey = registry.resolveKey(BlocClass, void 0, effectiveArgs);
390
+ const rebindNonce = consumer.rebindNonce;
391
+ const { bloc, trackedBloc } = (0, react.useMemo)(() => {
392
+ const instance = registry.acquire(BlocClass, instanceKey, {
252
393
  canCreate: true,
253
394
  countRef: false,
254
395
  args: effectiveArgs,
255
396
  sweepIfUnowned: true
256
397
  });
398
+ const depWrappers = /* @__PURE__ */ new Map();
257
399
  const onDepHandle = (handle) => {
258
- const cache = depWrapperCacheRef.current;
259
- const cached = cache.get(handle);
400
+ const cached = depWrappers.get(handle);
260
401
  if (cached !== void 0) return cached;
261
- const wrapper = makeDepWrapper(handle, consumerId, registry, trackedStateRef, consumer, onDepHandle);
262
- cache.set(handle, wrapper);
402
+ const wrapper = makeDepWrapper(handle, registry, consumer.deps, consumer.tracked, onDepHandle);
403
+ depWrappers.set(handle, wrapper);
263
404
  return wrapper;
264
405
  };
265
- const { proxy } = buildTrackedProxy(instance, trackedStateRef, onDepHandle);
406
+ const { proxy } = buildTrackedProxy(instance, consumer.tracked, onDepHandle);
266
407
  return {
267
408
  bloc: instance,
268
- instanceKey: resolvedKey,
269
409
  trackedBloc: proxy
270
410
  };
271
411
  }, [
272
412
  BlocClass,
273
- ownArgsKey,
274
- providerArgsKey,
413
+ instanceKey,
275
414
  rebindNonce,
276
415
  registry
277
416
  ]);
@@ -281,7 +420,7 @@ function useBloc(BlocClass, options) {
281
420
  const container = consumer.container;
282
421
  const unsubscribe = container.channel.subscribe(() => consumer.interest, () => {
283
422
  if (consumer.isSelectMode) {
284
- const select = selectRef.current;
423
+ const select = consumer.select;
285
424
  if (select) {
286
425
  const next = select(container.state, container);
287
426
  const prev = consumer.selection;
@@ -301,24 +440,25 @@ function useBloc(BlocClass, options) {
301
440
  BlocClass,
302
441
  instanceKey,
303
442
  consumerId,
304
- rebindNonce
443
+ rebindNonce,
444
+ registry
305
445
  ]), consumer.getSnapshot, consumer.getServerSnapshot);
306
446
  (0, react.useLayoutEffect)(() => {
307
447
  const live = registry.acquire(BlocClass, instanceKey, {
308
448
  canCreate: true,
309
449
  countRef: true,
310
- refId: primaryRefId(consumerId),
311
- args: resolveEffectiveArgs()
450
+ refId: consumer.primaryRefId,
451
+ args: resolveEffectiveArgs(consumer)
312
452
  });
313
- ownedBlocRef.current = live;
314
- onMountRef.current?.(live);
453
+ consumer.ownedBloc = live;
454
+ consumer.onMount?.(live);
315
455
  if (live !== bloc) {
316
- rebindNonceRef.current++;
456
+ consumer.rebindNonce++;
317
457
  consumer.bump();
318
458
  }
319
459
  return () => {
320
- onUnmountRef.current?.(ownedBlocRef.current ?? bloc);
321
- registry.release(BlocClass, instanceKey, false, primaryRefId(consumerId));
460
+ consumer.onUnmount?.(consumer.ownedBloc ?? bloc);
461
+ registry.release(BlocClass, instanceKey, false, consumer.primaryRefId);
322
462
  };
323
463
  }, [
324
464
  BlocClass,
@@ -328,115 +468,48 @@ function useBloc(BlocClass, options) {
328
468
  ]);
329
469
  const container = consumer.container;
330
470
  const rawState = container.state;
331
- const select = selectRef.current;
471
+ const select = consumer.select;
332
472
  consumer.isSelectMode = select !== void 0;
333
473
  let state;
334
474
  if (select !== void 0) {
335
475
  state = rawState;
336
- if (consumer.selection === null) consumer.selection = select(rawState, bloc);
476
+ consumer.selection = select(rawState, bloc);
337
477
  consumer.interest = _dirtytalk_structural.ALL_PATHS;
338
478
  } else {
339
- const tracked = (0, _dirtytalk_structural.trackRender)(rawState, container.interner, proxyCacheRef.current);
479
+ const tracked = (0, _dirtytalk_structural.trackRender)(rawState, container.interner, consumer.proxyCache ??= new _dirtytalk_structural.ProxyCache());
340
480
  state = tracked.value;
341
- trackedStateRef.current = tracked.value;
481
+ consumer.tracked.current = tracked.value;
342
482
  consumer.paths = tracked.paths;
343
- const session = consumer.session;
344
- session.clear();
345
- session.set(container, {
346
- kind: "primary",
347
- paths: tracked.paths
348
- });
349
- queueMicrotask(tracked.disarm);
483
+ consumer.disarm = tracked.disarm;
484
+ consumer.deps.begin(container, tracked.paths);
350
485
  }
351
486
  (0, react.useLayoutEffect)(() => {
352
- trackedStateRef.current = null;
487
+ consumer.tracked.current = null;
488
+ const disarm = consumer.disarm;
489
+ if (disarm !== null) {
490
+ consumer.disarm = null;
491
+ disarm();
492
+ }
493
+ const deps = consumer.deps;
353
494
  if (consumer.isSelectMode) {
354
- consumer.lastReconcile = null;
495
+ deps.invalidate();
355
496
  return;
356
497
  }
498
+ if (deps.isUnchanged()) return;
357
499
  const primary = consumer.container;
358
- const paths = consumer.paths;
359
- const session = consumer.session;
360
- const last = consumer.lastReconcile;
361
- if (last !== null && last.primaryContainer === primary && (0, _dirtytalk_structural.pathSetEquals)(last.primaryPaths, paths)) {
362
- let unchanged = last.deps.size === session.size - 1;
363
- if (unchanged) for (const [depContainer, entry] of session) {
364
- if (entry.kind === "primary") continue;
365
- const prevEntry = last.deps.get(depContainer);
366
- 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)) {
367
- unchanged = false;
368
- break;
369
- }
370
- }
371
- if (unchanged) return;
372
- }
373
- primary.registerConsumerPaths(consumerId, paths);
374
- consumer.interest = expandWithAncestors(paths, primary.interner);
375
- const subs = consumer.depSubs;
376
- for (const [depContainer, sub] of subs) if (!session.has(depContainer)) {
377
- sub.unsubscribe();
378
- depContainer.unregisterConsumer(consumerId);
379
- registry.release(sub.Type, sub.key, false, sub.refId);
380
- subs.delete(depContainer);
381
- }
382
- for (const [depContainer, entry] of session) {
383
- if (entry.kind === "primary") continue;
384
- const interest = expandWithAncestors(entry.paths, depContainer.interner);
385
- depContainer.registerConsumerPaths(consumerId, entry.paths);
386
- const existing = subs.get(depContainer);
387
- if (existing) {
388
- existing.interestRef.current = interest;
389
- continue;
390
- }
391
- registry.acquire(entry.Type, entry.key, {
392
- canCreate: true,
393
- countRef: true,
394
- refId: entry.refId,
395
- args: entry.args
396
- });
397
- const interestRef = { current: interest };
398
- const unsubscribe = depContainer.channel.subscribe(() => interestRef.current, () => consumer.bump());
399
- subs.set(depContainer, {
400
- unsubscribe,
401
- interestRef,
402
- Type: entry.Type,
403
- key: entry.key,
404
- refId: entry.refId,
405
- args: entry.args
406
- });
407
- }
408
- const depsSignature = /* @__PURE__ */ new Map();
409
- for (const [depContainer, entry] of session) {
410
- if (entry.kind === "primary") continue;
411
- depsSignature.set(depContainer, {
412
- paths: entry.paths,
413
- key: entry.key,
414
- refId: entry.refId,
415
- args: entry.args
416
- });
417
- }
418
- consumer.lastReconcile = {
419
- primaryContainer: primary,
420
- primaryPaths: paths,
421
- deps: depsSignature
422
- };
500
+ primary.registerConsumerPaths(consumerId, consumer.paths);
501
+ consumer.interest = expandWithAncestors(consumer.paths, primary.interner);
502
+ deps.reconcile(registry);
423
503
  });
424
- (0, react.useEffect)(() => {
425
- const subs = consumer.depSubs;
426
- return () => {
427
- for (const [depContainer, sub] of subs) {
428
- sub.unsubscribe();
429
- depContainer.unregisterConsumer(consumerId);
430
- registry.release(sub.Type, sub.key, false, sub.refId);
431
- }
432
- subs.clear();
433
- };
434
- }, [consumerId, registry]);
504
+ (0, react.useEffect)(() => () => consumer.deps.dispose(), [consumerId]);
435
505
  return [state, trackedBloc];
436
506
  }
437
507
  const noop = () => {};
438
508
  function createConsumer() {
509
+ const id = `useBloc-${nextConsumerId++}`;
439
510
  const consumer = {
511
+ id,
512
+ primaryRefId: `useBloc@${id}`,
440
513
  container: null,
441
514
  version: 0,
442
515
  notify: noop,
@@ -450,144 +523,35 @@ function createConsumer() {
450
523
  paths: (0, _dirtytalk_structural.emptyPathSet)(),
451
524
  isSelectMode: false,
452
525
  selection: null,
453
- session: /* @__PURE__ */ new Map(),
454
- depSubs: /* @__PURE__ */ new Map(),
455
- lastReconcile: null
526
+ deps: new DepSession(id, () => consumer.bump()),
527
+ select: void 0,
528
+ onMount: void 0,
529
+ onUnmount: void 0,
530
+ ownArgs: void 0,
531
+ providerArgs: void 0,
532
+ tracked: { current: null },
533
+ disarm: null,
534
+ proxyCache: null,
535
+ rebindNonce: 0,
536
+ ownedBloc: null
456
537
  };
457
538
  return consumer;
458
539
  }
459
- /**
460
- * Build the per-consumer wrapper that replaces a branded dep handle inside a
461
- * tracked getter's `this`. The wrapper exposes the same accessors as the core
462
- * handle and overrides `.track()`:
463
- *
464
- * - **Inside a render** (`trackedStateRef.current != null`): resolve (ENSURE,
465
- * no ref) the dep, `trackRender` its state, merge the recorded paths into the
466
- * session entry, build/reuse a tracked proxy for the dep so its OWN getters
467
- * track too, and return `[trackedValue, depProxy]`. The ownership ref is taken
468
- * by the layout-effect reconcile pass, not here.
469
- * - **Outside a render**: degrade to live `[dep.state, dep]` — matches the core
470
- * base impl, safe in event handlers/effects/methods.
471
- *
472
- * `.untracked()` always returns the live instance with no subscription.
473
- *
474
- * Args resolve at call time (`options.args ?? defaultArgs`), so a single handle
475
- * can resolve different dep instances across calls; tracked-proxy state is
476
- * therefore cached per resolved instance, not per handle. Guards against a
477
- * container re-entering tracking within the same render (mutual A↔B deps): if
478
- * the dep already has a non-primary session entry this render, reuse its proxy
479
- * + union its paths instead of re-acquiring.
480
- */
481
- function makeDepWrapper(handle, consumerId, registry, trackedStateRef, consumer, onDepHandle) {
482
- const brand = handle[_blac_core.DEP_BRAND];
483
- const refId = depRefId(consumerId);
484
- const perDep = /* @__PURE__ */ new Map();
485
- const proxyCache = new _dirtytalk_structural.ProxyCache();
486
- const resolve = (options) => {
487
- const args = options?.args ?? brand.defaultArgs;
488
- const key = registry.resolveKey(brand.Type, void 0, args);
489
- return {
490
- dep: registry.ensure(brand.Type, key, args),
491
- key,
492
- args
493
- };
494
- };
495
- const wrapper = {
496
- untracked: (options) => resolve(options).dep,
497
- track: (options) => {
498
- const { dep, key, args } = resolve(options);
499
- if (trackedStateRef.current == null) return [dep.state, dep];
500
- const session = consumer.session;
501
- const existing = session.get(dep);
502
- const tracked = (0, _dirtytalk_structural.trackRender)(dep.state, dep.interner, proxyCache);
503
- let cache = perDep.get(dep);
504
- if (cache === void 0) {
505
- const ref = { current: tracked.value };
506
- cache = {
507
- ref,
508
- proxy: buildTrackedProxy(dep, ref, onDepHandle).proxy
509
- };
510
- perDep.set(dep, cache);
511
- } else cache.ref.current = tracked.value;
512
- if (existing !== void 0) existing.paths = unionPaths(existing.paths, tracked.paths);
513
- else session.set(dep, {
514
- kind: "dep",
515
- paths: tracked.paths,
516
- Type: brand.Type,
517
- key,
518
- refId,
519
- args
520
- });
521
- return [tracked.value, cache.proxy];
522
- }
523
- };
524
- Object.defineProperty(wrapper, _blac_core.DEP_BRAND, {
525
- value: brand,
526
- enumerable: false,
527
- writable: false,
528
- configurable: false
529
- });
530
- return wrapper;
531
- }
532
- /** Union two PathSets (ALL_PATHS dominates). */
533
- function unionPaths(a, b) {
534
- if (a === _dirtytalk_structural.ALL_PATHS || b === _dirtytalk_structural.ALL_PATHS) return _dirtytalk_structural.ALL_PATHS;
535
- const out = new Set(a);
536
- for (const id of b) out.add(id);
537
- return out;
538
- }
540
+ const resolveEffectiveArgs = (consumer) => consumer.ownArgs !== void 0 ? consumer.ownArgs : consumer.providerArgs;
539
541
  const shallowArrayEqual = (a, b) => {
540
542
  if (a === b) return true;
541
543
  if (a.length !== b.length) return false;
542
544
  for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
543
545
  return true;
544
546
  };
545
- /**
546
- * Expand a PathSet to include an *ancestor-watch* id for every ancestor of
547
- * every tracked leaf.
548
- *
549
- * The auto-tracker records leaf paths (e.g. `'items.length'`), but
550
- * `StructuralContainer.patch` can only mark the parent (`'items'`) when it
551
- * replaces a value atomically (arrays, `null`, primitives — it can't see
552
- * inside). Without expansion, a subscriber with interest `{'items.length'}`
553
- * would miss a `patch`-triggered atomic-replacement of `items`.
554
- *
555
- * Ancestors are added under the interner's *ancestor-watch* lane
556
- * (`internAncestor`), NOT as normal ids. The source emits a matching
557
- * ancestor-watch mark only for paths it replaces atomically — never for a
558
- * plain-object structural pulse-up. So `{'items.length'}` wakes when the array
559
- * `items` is replaced, but `{'user.email'}` does NOT wake when a sibling
560
- * `user.name` changes and pulses `user` up: pulse-up `user` is a normal id and
561
- * the ancestor-watch `user` only intersects another ancestor-watch `user`.
562
- *
563
- * Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but
564
- * NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,
565
- * and `''` would wake this consumer on every field change).
566
- */
567
- function expandWithAncestors(paths, interner) {
568
- if (paths === _dirtytalk_structural.ALL_PATHS) return _dirtytalk_structural.ALL_PATHS;
569
- const leafPaths = paths;
570
- if (leafPaths.size === 0) return paths;
571
- const expanded = new Set(leafPaths);
572
- for (const id of leafPaths) {
573
- const str = interner.lookup(id);
574
- let idx = str.lastIndexOf(".");
575
- while (idx > 0) {
576
- const ancestor = str.slice(0, idx);
577
- expanded.add(interner.internAncestor(ancestor));
578
- idx = ancestor.lastIndexOf(".");
579
- }
580
- }
581
- return expanded;
582
- }
583
547
  //#endregion
584
548
  //#region src/useBlocDeps.ts
585
549
  /**
586
550
  * Feed live values from the component into a bloc's `deps` lane.
587
551
  *
588
552
  * Each calling component is one owner: its slice is shallow-merged into the
589
- * bloc's merged `deps` view, and withdrawn when the component unmounts. The
590
- * bloc observes changes through `onDepsChanged`.
553
+ * bloc's merged `deps` view and withdrawn on unmount. The bloc observes
554
+ * changes through `onDepsChanged`.
591
555
  *
592
556
  * ```tsx
593
557
  * const [state, bloc] = useBloc(CanvasCubit);
@@ -604,17 +568,17 @@ function useBlocDeps(bloc, slice) {
604
568
  const latest = (0, react.useRef)(slice);
605
569
  latest.current = slice;
606
570
  (0, react.useEffect)(() => {
607
- bloc[_blac_core.APPLY_DEPS](ownerId, latest.current);
571
+ bloc[_blac_core_internal.APPLY_DEPS](ownerId, latest.current);
608
572
  });
609
573
  (0, react.useEffect)(() => {
610
574
  return () => {
611
- bloc[_blac_core.REMOVE_DEPS_OWNER](ownerId);
575
+ bloc[_blac_core_internal.REMOVE_DEPS_OWNER](ownerId);
612
576
  };
613
577
  }, [bloc, ownerId]);
614
578
  }
615
579
  //#endregion
616
580
  exports.BlocProvider = BlocProvider;
617
- exports.RegistryProvider = RegistryProvider;
581
+ exports.RegistryProvider = require_RegistryProvider.RegistryProvider;
618
582
  Object.defineProperty(exports, "untracked", {
619
583
  enumerable: true,
620
584
  get: function() {
@@ -624,5 +588,6 @@ Object.defineProperty(exports, "untracked", {
624
588
  exports.useBloc = useBloc;
625
589
  exports.useBlocDeps = useBlocDeps;
626
590
  exports.useProvidedArgs = useProvidedArgs;
591
+ exports.useRegistry = require_RegistryProvider.useRegistry;
627
592
 
628
593
  //# sourceMappingURL=index.cjs.map