@blac/react 2.0.22 → 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,37 +57,9 @@ 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";
93
63
  }
94
64
  const gettersByProto = /* @__PURE__ */ new WeakMap();
95
65
  function collectGetters(proto) {
@@ -110,50 +80,251 @@ function collectGetters(proto) {
110
80
  /**
111
81
  * Build a per-consumer proxy for a bloc instance.
112
82
  *
113
- * 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
114
84
  * `#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.
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.
118
88
  *
119
89
  * One allocation per bloc acquisition (inside `useMemo`), so the proxy is
120
90
  * stable across renders.
121
91
  *
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.
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.
126
95
  */
127
96
  function buildTrackedProxy(instance, trackedStateRef, onDepHandle) {
128
97
  const getters = collectGetters(Object.getPrototypeOf(instance));
129
98
  const tracksState = supportsTrackedState(instance);
130
99
  const boundMethods = /* @__PURE__ */ new Map();
131
100
  const wrapDepHandle = (value) => {
132
- 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);
133
102
  return value;
134
103
  };
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));
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);
140
213
  }
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);
214
+ for (const [dep, entry] of entries) {
215
+ if (entry.kind === "primary") continue;
216
+ if (dep.$blac.disposed) {
217
+ this.onChange();
218
+ continue;
219
+ }
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;
147
236
  }
148
- return wrapDepHandle(bound);
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
+ });
149
246
  }
150
- return wrapDepHandle(value);
151
- } }) };
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;
152
324
  }
153
325
  //#endregion
154
326
  //#region src/useBloc.ts
155
327
  let nextConsumerId = 0;
156
- const ARGS_UNSET = Symbol("blac.argsKeyUnset");
157
328
  /**
158
329
  * React hook that connects a component to a state container with automatic
159
330
  * re-render on state changes.
@@ -161,8 +332,7 @@ const ARGS_UNSET = Symbol("blac.argsKeyUnset");
161
332
  * Two tracking modes:
162
333
  * - **Auto-tracking** (default): the returned state value is a proxy that
163
334
  * records read paths during render. The component re-renders when any
164
- * recorded path changes. Backed by `@dirtytalk/structural`'s
165
- * `trackRender` + the container's path-scoped `DirtyChannel`.
335
+ * recorded path changes.
166
336
  * - **Manual select**: pass `options.select` to opt out of auto-tracking.
167
337
  * The hook re-renders only when the returned array's elements change
168
338
  * (per-index `Object.is`).
@@ -185,7 +355,7 @@ const ARGS_UNSET = Symbol("blac.argsKeyUnset");
185
355
  * @typeParam T - The state container constructor type (inferred from BlocClass)
186
356
  * @param BlocClass - The state container class to connect to
187
357
  * @param options - Configuration options
188
- * @returns Tuple of `[state, bloc, ref]`
358
+ * @returns Tuple of `[state, bloc]`
189
359
  *
190
360
  * @example Basic usage
191
361
  * ```ts
@@ -206,55 +376,41 @@ const ARGS_UNSET = Symbol("blac.argsKeyUnset");
206
376
  * @public
207
377
  */
208
378
  function useBloc(BlocClass, options) {
209
- const registry = (0, react.useContext)(RegistryContext) ?? (0, _blac_core.getRegistry)();
379
+ const registry = require_RegistryProvider.useRegistry();
210
380
  const consumerRef = (0, react.useRef)(null);
211
381
  const consumer = consumerRef.current ??= createConsumer();
212
382
  const consumerId = consumer.id;
213
383
  consumer.select = options?.select;
214
384
  consumer.onMount = options?.onMount;
215
385
  consumer.onUnmount = options?.onUnmount;
216
- const ownArgs = options?.args;
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;
223
- const providerArgs = useProvidedArgs(BlocClass);
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;
386
+ consumer.ownArgs = options?.args;
387
+ consumer.providerArgs = useProvidedArgs(BlocClass);
388
+ const effectiveArgs = resolveEffectiveArgs(consumer);
389
+ const instanceKey = registry.resolveKey(BlocClass, void 0, effectiveArgs);
230
390
  const rebindNonce = consumer.rebindNonce;
231
- const { bloc, instanceKey, trackedBloc } = (0, react.useMemo)(() => {
232
- const effectiveArgs = resolveEffectiveArgs(consumer);
233
- const resolvedKey = (0, _blac_core.resolveInstanceKey)(BlocClass, effectiveArgs);
234
- const instance = registry.acquire(BlocClass, resolvedKey, {
391
+ const { bloc, trackedBloc } = (0, react.useMemo)(() => {
392
+ const instance = registry.acquire(BlocClass, instanceKey, {
235
393
  canCreate: true,
236
394
  countRef: false,
237
395
  args: effectiveArgs,
238
396
  sweepIfUnowned: true
239
397
  });
398
+ const depWrappers = /* @__PURE__ */ new Map();
240
399
  const onDepHandle = (handle) => {
241
- const cache = consumer.depWrappers ??= /* @__PURE__ */ new Map();
242
- const cached = cache.get(handle);
400
+ const cached = depWrappers.get(handle);
243
401
  if (cached !== void 0) return cached;
244
- const wrapper = makeDepWrapper(handle, registry, consumer, onDepHandle);
245
- cache.set(handle, wrapper);
402
+ const wrapper = makeDepWrapper(handle, registry, consumer.deps, consumer.tracked, onDepHandle);
403
+ depWrappers.set(handle, wrapper);
246
404
  return wrapper;
247
405
  };
248
406
  const { proxy } = buildTrackedProxy(instance, consumer.tracked, onDepHandle);
249
407
  return {
250
408
  bloc: instance,
251
- instanceKey: resolvedKey,
252
409
  trackedBloc: proxy
253
410
  };
254
411
  }, [
255
412
  BlocClass,
256
- ownArgsKey,
257
- providerArgsKey,
413
+ instanceKey,
258
414
  rebindNonce,
259
415
  registry
260
416
  ]);
@@ -284,7 +440,8 @@ function useBloc(BlocClass, options) {
284
440
  BlocClass,
285
441
  instanceKey,
286
442
  consumerId,
287
- rebindNonce
443
+ rebindNonce,
444
+ registry
288
445
  ]), consumer.getSnapshot, consumer.getServerSnapshot);
289
446
  (0, react.useLayoutEffect)(() => {
290
447
  const live = registry.acquire(BlocClass, instanceKey, {
@@ -316,7 +473,7 @@ function useBloc(BlocClass, options) {
316
473
  let state;
317
474
  if (select !== void 0) {
318
475
  state = rawState;
319
- if (consumer.selection === null) consumer.selection = select(rawState, bloc);
476
+ consumer.selection = select(rawState, bloc);
320
477
  consumer.interest = _dirtytalk_structural.ALL_PATHS;
321
478
  } else {
322
479
  const tracked = (0, _dirtytalk_structural.trackRender)(rawState, container.interner, consumer.proxyCache ??= new _dirtytalk_structural.ProxyCache());
@@ -324,12 +481,7 @@ function useBloc(BlocClass, options) {
324
481
  consumer.tracked.current = tracked.value;
325
482
  consumer.paths = tracked.paths;
326
483
  consumer.disarm = tracked.disarm;
327
- const session = consumer.session;
328
- session.clear();
329
- session.set(container, {
330
- kind: "primary",
331
- paths: tracked.paths
332
- });
484
+ consumer.deps.begin(container, tracked.paths);
333
485
  }
334
486
  (0, react.useLayoutEffect)(() => {
335
487
  consumer.tracked.current = null;
@@ -338,88 +490,18 @@ function useBloc(BlocClass, options) {
338
490
  consumer.disarm = null;
339
491
  disarm();
340
492
  }
493
+ const deps = consumer.deps;
341
494
  if (consumer.isSelectMode) {
342
- consumer.lastReconcile = null;
495
+ deps.invalidate();
343
496
  return;
344
497
  }
498
+ if (deps.isUnchanged()) return;
345
499
  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)) {
350
- let unchanged = last.deps.size === session.size - 1;
351
- if (unchanged) for (const [depContainer, entry] of session) {
352
- if (entry.kind === "primary") continue;
353
- const prevEntry = last.deps.get(depContainer);
354
- 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)) {
355
- unchanged = false;
356
- break;
357
- }
358
- }
359
- if (unchanged) return;
360
- }
361
- primary.registerConsumerPaths(consumerId, paths);
362
- consumer.interest = expandWithAncestors(paths, primary.interner);
363
- const subs = consumer.depSubs;
364
- for (const [depContainer, sub] of subs) if (!session.has(depContainer)) {
365
- sub.unsubscribe();
366
- depContainer.unregisterConsumer(consumerId);
367
- registry.release(sub.Type, sub.key, false, sub.refId);
368
- subs.delete(depContainer);
369
- }
370
- for (const [depContainer, entry] of session) {
371
- if (entry.kind === "primary") continue;
372
- const interest = expandWithAncestors(entry.paths, depContainer.interner);
373
- depContainer.registerConsumerPaths(consumerId, entry.paths);
374
- const existing = subs.get(depContainer);
375
- if (existing) {
376
- existing.interestRef.current = interest;
377
- continue;
378
- }
379
- registry.acquire(entry.Type, entry.key, {
380
- canCreate: true,
381
- countRef: true,
382
- refId: entry.refId,
383
- args: entry.args
384
- });
385
- const interestRef = { current: interest };
386
- const unsubscribe = depContainer.channel.subscribe(() => interestRef.current, () => consumer.bump());
387
- subs.set(depContainer, {
388
- unsubscribe,
389
- interestRef,
390
- Type: entry.Type,
391
- key: entry.key,
392
- refId: entry.refId,
393
- args: entry.args
394
- });
395
- }
396
- const depsSignature = /* @__PURE__ */ new Map();
397
- for (const [depContainer, entry] of session) {
398
- if (entry.kind === "primary") continue;
399
- depsSignature.set(depContainer, {
400
- paths: entry.paths,
401
- key: entry.key,
402
- refId: entry.refId,
403
- args: entry.args
404
- });
405
- }
406
- consumer.lastReconcile = {
407
- primaryContainer: primary,
408
- primaryPaths: paths,
409
- deps: depsSignature
410
- };
500
+ primary.registerConsumerPaths(consumerId, consumer.paths);
501
+ consumer.interest = expandWithAncestors(consumer.paths, primary.interner);
502
+ deps.reconcile(registry);
411
503
  });
412
- (0, react.useEffect)(() => {
413
- const subs = consumer.depSubs;
414
- return () => {
415
- for (const [depContainer, sub] of subs) {
416
- sub.unsubscribe();
417
- depContainer.unregisterConsumer(consumerId);
418
- registry.release(sub.Type, sub.key, false, sub.refId);
419
- }
420
- subs.clear();
421
- };
422
- }, [consumerId, registry]);
504
+ (0, react.useEffect)(() => () => consumer.deps.dispose(), [consumerId]);
423
505
  return [state, trackedBloc];
424
506
  }
425
507
  const noop = () => {};
@@ -428,7 +510,6 @@ function createConsumer() {
428
510
  const consumer = {
429
511
  id,
430
512
  primaryRefId: `useBloc@${id}`,
431
- depRefId: `useBloc@${id}:dep`,
432
513
  container: null,
433
514
  version: 0,
434
515
  notify: noop,
@@ -442,159 +523,35 @@ function createConsumer() {
442
523
  paths: (0, _dirtytalk_structural.emptyPathSet)(),
443
524
  isSelectMode: false,
444
525
  selection: null,
445
- session: /* @__PURE__ */ new Map(),
446
- depSubs: /* @__PURE__ */ new Map(),
447
- lastReconcile: null,
526
+ deps: new DepSession(id, () => consumer.bump()),
448
527
  select: void 0,
449
528
  onMount: void 0,
450
529
  onUnmount: void 0,
451
530
  ownArgs: void 0,
452
- ownArgsKeyFor: ARGS_UNSET,
453
- ownArgsKey: void 0,
454
531
  providerArgs: void 0,
455
- providerArgsKeyFor: ARGS_UNSET,
456
- providerArgsKey: void 0,
457
532
  tracked: { current: null },
458
533
  disarm: null,
459
534
  proxyCache: null,
460
- depWrappers: null,
461
535
  rebindNonce: 0,
462
536
  ownedBloc: null
463
537
  };
464
538
  return consumer;
465
539
  }
466
540
  const resolveEffectiveArgs = (consumer) => consumer.ownArgs !== void 0 ? consumer.ownArgs : consumer.providerArgs;
467
- /**
468
- * Build the per-consumer wrapper that replaces a branded dep handle inside a
469
- * tracked getter's `this`. The wrapper exposes the same accessors as the core
470
- * handle and overrides `.track()`:
471
- *
472
- * - **Inside a render** (`consumer.tracked.current != null`): resolve (ENSURE,
473
- * no ref) the dep, `trackRender` its state, merge the recorded paths into the
474
- * session entry, build/reuse a tracked proxy for the dep so its OWN getters
475
- * track too, and return `[trackedValue, depProxy]`. The ownership ref is taken
476
- * by the layout-effect reconcile pass, not here.
477
- * - **Outside a render**: degrade to live `[dep.state, dep]` — matches the core
478
- * base impl, safe in event handlers/effects/methods.
479
- *
480
- * `.untracked()` always returns the live instance with no subscription.
481
- *
482
- * Args resolve at call time (`options.args ?? defaultArgs`), so a single handle
483
- * can resolve different dep instances across calls; tracked-proxy state is
484
- * therefore cached per resolved instance, not per handle. Guards against a
485
- * container re-entering tracking within the same render (mutual A↔B deps): if
486
- * the dep already has a non-primary session entry this render, reuse its proxy
487
- * + union its paths instead of re-acquiring.
488
- */
489
- function makeDepWrapper(handle, registry, consumer, onDepHandle) {
490
- const brand = handle[_blac_core.DEP_BRAND];
491
- const refId = consumer.depRefId;
492
- const perDep = /* @__PURE__ */ new Map();
493
- const proxyCache = new _dirtytalk_structural.ProxyCache();
494
- const resolve = (options) => {
495
- const args = options?.args ?? brand.defaultArgs;
496
- const key = registry.resolveKey(brand.Type, void 0, args);
497
- return {
498
- dep: registry.ensure(brand.Type, key, args),
499
- key,
500
- args
501
- };
502
- };
503
- const wrapper = {
504
- untracked: (options) => resolve(options).dep,
505
- track: (options) => {
506
- const { dep, key, args } = resolve(options);
507
- if (consumer.tracked.current == null) return [dep.state, dep];
508
- const session = consumer.session;
509
- const existing = session.get(dep);
510
- const tracked = (0, _dirtytalk_structural.trackRender)(dep.state, dep.interner, proxyCache);
511
- let cache = perDep.get(dep);
512
- if (cache === void 0) {
513
- const ref = { current: tracked.value };
514
- cache = {
515
- ref,
516
- proxy: buildTrackedProxy(dep, ref, onDepHandle).proxy
517
- };
518
- perDep.set(dep, cache);
519
- } else cache.ref.current = tracked.value;
520
- if (existing !== void 0) existing.paths = unionPaths(existing.paths, tracked.paths);
521
- else session.set(dep, {
522
- kind: "dep",
523
- paths: tracked.paths,
524
- Type: brand.Type,
525
- key,
526
- refId,
527
- args
528
- });
529
- return [tracked.value, cache.proxy];
530
- }
531
- };
532
- Object.defineProperty(wrapper, _blac_core.DEP_BRAND, {
533
- value: brand,
534
- enumerable: false,
535
- writable: false,
536
- configurable: false
537
- });
538
- return wrapper;
539
- }
540
- /** Union two PathSets (ALL_PATHS dominates). */
541
- function unionPaths(a, b) {
542
- if (a === _dirtytalk_structural.ALL_PATHS || b === _dirtytalk_structural.ALL_PATHS) return _dirtytalk_structural.ALL_PATHS;
543
- const out = new Set(a);
544
- for (const id of b) out.add(id);
545
- return out;
546
- }
547
541
  const shallowArrayEqual = (a, b) => {
548
542
  if (a === b) return true;
549
543
  if (a.length !== b.length) return false;
550
544
  for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
551
545
  return true;
552
546
  };
553
- /**
554
- * Expand a PathSet to include an *ancestor-watch* id for every ancestor of
555
- * every tracked leaf.
556
- *
557
- * The auto-tracker records leaf paths (e.g. `'items.length'`), but
558
- * `StructuralContainer.patch` can only mark the parent (`'items'`) when it
559
- * replaces a value atomically (arrays, `null`, primitives — it can't see
560
- * inside). Without expansion, a subscriber with interest `{'items.length'}`
561
- * would miss a `patch`-triggered atomic-replacement of `items`.
562
- *
563
- * Ancestors are added under the interner's *ancestor-watch* lane
564
- * (`ancestorWatchIds`), NOT as normal ids. The source emits a matching
565
- * ancestor-watch mark only for paths it replaces atomically — never for a
566
- * plain-object structural pulse-up. So `{'items.length'}` wakes when the array
567
- * `items` is replaced, but `{'user.email'}` does NOT wake when a sibling
568
- * `user.name` changes and pulses `user` up: pulse-up `user` is a normal id and
569
- * the ancestor-watch `user` only intersects another ancestor-watch `user`.
570
- *
571
- * Example: leaf `'a.b.c'` adds ancestor-watch ids for `'a.b'` and `'a'` (but
572
- * NOT the `''` root — a root change is covered by `ALL_PATHS` from the source,
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.
577
- */
578
- function expandWithAncestors(paths, interner) {
579
- if (paths === _dirtytalk_structural.ALL_PATHS) return _dirtytalk_structural.ALL_PATHS;
580
- const leafPaths = paths;
581
- let expanded;
582
- for (const id of leafPaths) {
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]);
587
- }
588
- return expanded ?? paths;
589
- }
590
547
  //#endregion
591
548
  //#region src/useBlocDeps.ts
592
549
  /**
593
550
  * Feed live values from the component into a bloc's `deps` lane.
594
551
  *
595
552
  * 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`.
553
+ * bloc's merged `deps` view and withdrawn on unmount. The bloc observes
554
+ * changes through `onDepsChanged`.
598
555
  *
599
556
  * ```tsx
600
557
  * const [state, bloc] = useBloc(CanvasCubit);
@@ -611,17 +568,17 @@ function useBlocDeps(bloc, slice) {
611
568
  const latest = (0, react.useRef)(slice);
612
569
  latest.current = slice;
613
570
  (0, react.useEffect)(() => {
614
- bloc[_blac_core.APPLY_DEPS](ownerId, latest.current);
571
+ bloc[_blac_core_internal.APPLY_DEPS](ownerId, latest.current);
615
572
  });
616
573
  (0, react.useEffect)(() => {
617
574
  return () => {
618
- bloc[_blac_core.REMOVE_DEPS_OWNER](ownerId);
575
+ bloc[_blac_core_internal.REMOVE_DEPS_OWNER](ownerId);
619
576
  };
620
577
  }, [bloc, ownerId]);
621
578
  }
622
579
  //#endregion
623
580
  exports.BlocProvider = BlocProvider;
624
- exports.RegistryProvider = RegistryProvider;
581
+ exports.RegistryProvider = require_RegistryProvider.RegistryProvider;
625
582
  Object.defineProperty(exports, "untracked", {
626
583
  enumerable: true,
627
584
  get: function() {
@@ -631,5 +588,6 @@ Object.defineProperty(exports, "untracked", {
631
588
  exports.useBloc = useBloc;
632
589
  exports.useBlocDeps = useBlocDeps;
633
590
  exports.useProvidedArgs = useProvidedArgs;
591
+ exports.useRegistry = require_RegistryProvider.useRegistry;
634
592
 
635
593
  //# sourceMappingURL=index.cjs.map