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