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