@blac/react 2.0.22 → 2.1.2

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";
@@ -106,53 +76,265 @@ function collectGetters(proto) {
106
76
  gettersByProto.set(proto, getters);
107
77
  return getters;
108
78
  }
79
+ var BlocHandler = class {
80
+ constructor(getters, tracksState, trackedStateRef, onDepHandle) {
81
+ this.getters = getters;
82
+ this.tracksState = tracksState;
83
+ this.trackedStateRef = trackedStateRef;
84
+ this.onDepHandle = onDepHandle;
85
+ this.boundMethods = void 0;
86
+ }
87
+ get(target, key) {
88
+ const getter = this.getters.get(key);
89
+ if (getter !== void 0) {
90
+ const tracked = this.trackedStateRef.current;
91
+ return this.wrapDepHandle(tracked == null || !this.tracksState ? getter.call(target) : target[WITH_TRACKED_STATE](tracked, getter, this.onDepHandle));
92
+ }
93
+ const value = Reflect.get(target, key, target);
94
+ if (typeof value === "function") {
95
+ const fn = value;
96
+ this.boundMethods ??= /* @__PURE__ */ new Map();
97
+ let cached = this.boundMethods.get(key);
98
+ if (cached === void 0 || cached.fn !== fn) {
99
+ cached = {
100
+ fn,
101
+ bound: fn.bind(target)
102
+ };
103
+ this.boundMethods.set(key, cached);
104
+ }
105
+ return this.wrapDepHandle(cached.bound);
106
+ }
107
+ return this.wrapDepHandle(value);
108
+ }
109
+ set(target, key, value) {
110
+ return Reflect.set(target, key, value, target);
111
+ }
112
+ wrapDepHandle(value) {
113
+ if (this.onDepHandle !== void 0 && (typeof value === "function" || typeof value === "object") && value !== null && value[DEP_BRAND] !== void 0) return this.onDepHandle(value);
114
+ return value;
115
+ }
116
+ };
109
117
  /**
110
118
  * Build a per-consumer proxy for a bloc instance.
111
119
  *
112
- * Getters run with `this` bound to the **real instance**, not a proxy, so ES
120
+ * Getters run with `this` bound to the real instance, not the proxy, so ES
113
121
  * `#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.
122
+ * `[WITH_TRACKED_STATE]`, which makes the instance's own `state` getter
123
+ * report the render's tracking proxy for the call's duration; nested getter
124
+ * chains stay tracked because the override remains set.
117
125
  *
118
126
  * One allocation per bloc acquisition (inside `useMemo`), so the proxy is
119
127
  * stable across renders.
120
128
  *
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.
129
+ * @param onDepHandle - Invoked when a read returns a branded dep handle;
130
+ * returns the value (the session-bound wrapper) to expose in its place.
131
+ * Must cache wrappers per handle to avoid re-allocation.
125
132
  */
126
133
  function buildTrackedProxy(instance, trackedStateRef, onDepHandle) {
127
- const getters = collectGetters(Object.getPrototypeOf(instance));
128
- const tracksState = supportsTrackedState(instance);
129
- const boundMethods = /* @__PURE__ */ new Map();
130
- const wrapDepHandle = (value) => {
131
- if (onDepHandle !== void 0 && (typeof value === "function" || typeof value === "object") && value !== null && value[DEP_BRAND] !== void 0) return onDepHandle(value);
132
- return value;
133
- };
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));
134
+ const handler = new BlocHandler(collectGetters(Object.getPrototypeOf(instance)), supportsTrackedState(instance), trackedStateRef, onDepHandle);
135
+ return new Proxy(instance, handler);
136
+ }
137
+ //#endregion
138
+ //#region src/depSession.ts
139
+ const NO_PATHS$1 = emptyPathSet();
140
+ /**
141
+ * The containers one `useBloc` consumer reads during a render: its primary
142
+ * bloc plus every dep reached through `this.<handle>.track()`.
143
+ *
144
+ * The render records entries; the commit calls `reconcile`, which subscribes
145
+ * and takes a ref on new deps and drops the ones no longer read. `dispose`
146
+ * releases everything. Refs are only taken at commit, so an uncommitted render
147
+ * never leaks one.
148
+ */
149
+ var DepSession = class {
150
+ constructor(consumerId, onChange) {
151
+ this.consumerId = consumerId;
152
+ this.onChange = onChange;
153
+ this.primary = null;
154
+ this.primaryPaths = NO_PATHS$1;
155
+ this.entries = null;
156
+ this.subs = null;
157
+ this.reconciled = false;
158
+ this.lastPrimary = null;
159
+ this.lastPrimaryPaths = NO_PATHS$1;
160
+ this.lastEntries = null;
161
+ this.disarms = null;
162
+ }
163
+ get refId() {
164
+ return `useBloc@${this.consumerId}:dep`;
165
+ }
166
+ begin(primary, paths) {
167
+ this.entries?.clear();
168
+ if (this.disarms !== null) this.disarms.length = 0;
169
+ this.primary = primary;
170
+ this.primaryPaths = paths;
171
+ }
172
+ record(dep, paths, Type, key, args, disarm) {
173
+ (this.disarms ??= []).push(disarm);
174
+ if (dep === this.primary) {
175
+ this.primaryPaths = unionPaths(this.primaryPaths, paths);
176
+ return;
139
177
  }
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);
178
+ const entries = this.entries ??= /* @__PURE__ */ new Map();
179
+ const existing = entries.get(dep);
180
+ if (existing !== void 0) existing.paths = unionPaths(existing.paths, paths);
181
+ else entries.set(dep, {
182
+ paths,
183
+ Type,
184
+ key,
185
+ args
186
+ });
187
+ }
188
+ /** Stops every dep tracked this render from recording further reads. */
189
+ disarm() {
190
+ const disarms = this.disarms;
191
+ if (disarms === null) return;
192
+ for (const disarm of disarms) disarm();
193
+ disarms.length = 0;
194
+ }
195
+ /** Forces the next `reconcile` to run in full. */
196
+ invalidate() {
197
+ this.reconciled = false;
198
+ }
199
+ isUnchanged() {
200
+ if (!this.reconciled || this.lastPrimary !== this.primary || !pathSetEquals(this.lastPrimaryPaths, this.primaryPaths)) return false;
201
+ const last = this.lastEntries;
202
+ const entries = this.entries;
203
+ if ((last?.size ?? 0) !== (entries?.size ?? 0)) return false;
204
+ if (last === null || entries === null) return true;
205
+ for (const [container, entry] of entries) {
206
+ const prev = last.get(container);
207
+ if (prev === void 0 || !sameEntry(prev, entry)) return false;
208
+ }
209
+ return true;
210
+ }
211
+ reconcile(registry) {
212
+ const { consumerId } = this;
213
+ const entries = this.entries;
214
+ this.reconciled = true;
215
+ this.lastPrimary = this.primary;
216
+ this.lastPrimaryPaths = this.primaryPaths;
217
+ this.lastEntries = entries === null || entries.size === 0 ? null : new Map(entries);
218
+ const subs = this.subs;
219
+ if (subs !== null) {
220
+ for (const [dep, sub] of subs) if (entries?.has(dep) !== true) {
221
+ this.drop(dep, sub);
222
+ subs.delete(dep);
146
223
  }
147
- return wrapDepHandle(bound);
148
224
  }
149
- return wrapDepHandle(value);
150
- } }) };
225
+ if (entries === null) return;
226
+ for (const [dep, entry] of entries) {
227
+ if (dep.$blac.disposed) {
228
+ this.onChange();
229
+ continue;
230
+ }
231
+ dep.registerConsumerPaths(consumerId, entry.paths);
232
+ const existing = subs?.get(dep);
233
+ if (existing) {
234
+ existing.subscription.setInterest(entry.paths);
235
+ continue;
236
+ }
237
+ const refId = this.refId;
238
+ if (registry.acquire(entry.Type, entry.key, {
239
+ canCreate: true,
240
+ countRef: true,
241
+ refId,
242
+ args: entry.args
243
+ }) !== dep) {
244
+ registry.release(entry.Type, entry.key, false, refId);
245
+ this.onChange();
246
+ continue;
247
+ }
248
+ const subscription = dep.subscribePaths(this.onChange);
249
+ subscription.setInterest(entry.paths);
250
+ (this.subs ??= /* @__PURE__ */ new Map()).set(dep, {
251
+ subscription,
252
+ registry,
253
+ Type: entry.Type,
254
+ key: entry.key
255
+ });
256
+ }
257
+ }
258
+ dispose() {
259
+ const subs = this.subs;
260
+ if (subs !== null) {
261
+ for (const [dep, sub] of subs) this.drop(dep, sub);
262
+ subs.clear();
263
+ }
264
+ this.reconciled = false;
265
+ }
266
+ drop(dep, sub) {
267
+ sub.subscription.unsubscribe();
268
+ dep.unregisterConsumer(this.consumerId);
269
+ sub.registry.release(sub.Type, sub.key, false, this.refId);
270
+ }
271
+ };
272
+ function sameEntry(a, b) {
273
+ return pathSetEquals(a.paths, b.paths) && a.key === b.key && Object.is(a.args, b.args);
274
+ }
275
+ function unionPaths(a, b) {
276
+ if (a === ALL_PATHS || b === ALL_PATHS) return ALL_PATHS;
277
+ const out = new Set(a);
278
+ for (const id of b) out.add(id);
279
+ return out;
280
+ }
281
+ /**
282
+ * Wrap a dep handle read inside a tracked getter. During a render (`tracked`
283
+ * is set) `.track()` records the dep's read paths into `session` and returns
284
+ * a tracked proxy, so the dep's own getters track too. Outside a render it
285
+ * returns the live `[state, dep]`, like the core handle.
286
+ *
287
+ * Args resolve per call, so one handle can reach several instances; proxies
288
+ * are cached per resolved instance.
289
+ */
290
+ function makeDepWrapper(handle, registry, session, tracked, onDepHandle) {
291
+ const brand = handle[DEP_BRAND];
292
+ const perDep = /* @__PURE__ */ new Map();
293
+ const proxyCache = new ProxyCache();
294
+ const resolve = (options) => {
295
+ const args = options?.args ?? brand.defaultArgs;
296
+ const key = registry.resolveKey(brand.Type, void 0, args);
297
+ return {
298
+ dep: registry.acquire(brand.Type, key, {
299
+ canCreate: true,
300
+ countRef: false,
301
+ args,
302
+ sweepIfUnowned: tracked.current != null
303
+ }),
304
+ key,
305
+ args
306
+ };
307
+ };
308
+ const wrapper = {
309
+ untracked: (options) => resolve(options).dep,
310
+ track: (options) => {
311
+ const { dep, key, args } = resolve(options);
312
+ if (tracked.current == null) return [dep.state, dep];
313
+ const result = trackRender(dep.state, dep.interner, proxyCache);
314
+ let cache = perDep.get(dep);
315
+ if (cache === void 0) {
316
+ const ref = { current: result.value };
317
+ cache = {
318
+ ref,
319
+ proxy: buildTrackedProxy(dep, ref, onDepHandle)
320
+ };
321
+ perDep.set(dep, cache);
322
+ } else cache.ref.current = result.value;
323
+ session.record(dep, result.paths, brand.Type, key, args, result.disarm);
324
+ return [result.value, cache.proxy];
325
+ }
326
+ };
327
+ Object.defineProperty(wrapper, DEP_BRAND, {
328
+ value: brand,
329
+ enumerable: false,
330
+ writable: false,
331
+ configurable: false
332
+ });
333
+ return wrapper;
151
334
  }
152
335
  //#endregion
153
336
  //#region src/useBloc.ts
154
337
  let nextConsumerId = 0;
155
- const ARGS_UNSET = Symbol("blac.argsKeyUnset");
156
338
  /**
157
339
  * React hook that connects a component to a state container with automatic
158
340
  * re-render on state changes.
@@ -160,8 +342,7 @@ const ARGS_UNSET = Symbol("blac.argsKeyUnset");
160
342
  * Two tracking modes:
161
343
  * - **Auto-tracking** (default): the returned state value is a proxy that
162
344
  * 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`.
345
+ * recorded path changes.
165
346
  * - **Manual select**: pass `options.select` to opt out of auto-tracking.
166
347
  * The hook re-renders only when the returned array's elements change
167
348
  * (per-index `Object.is`).
@@ -184,7 +365,7 @@ const ARGS_UNSET = Symbol("blac.argsKeyUnset");
184
365
  * @typeParam T - The state container constructor type (inferred from BlocClass)
185
366
  * @param BlocClass - The state container class to connect to
186
367
  * @param options - Configuration options
187
- * @returns Tuple of `[state, bloc, ref]`
368
+ * @returns Tuple of `[state, bloc]`
188
369
  *
189
370
  * @example Basic usage
190
371
  * ```ts
@@ -205,230 +386,57 @@ const ARGS_UNSET = Symbol("blac.argsKeyUnset");
205
386
  * @public
206
387
  */
207
388
  function useBloc(BlocClass, options) {
208
- const registry = useContext(RegistryContext) ?? getRegistry();
389
+ const registry = useRegistry();
209
390
  const consumerRef = useRef(null);
210
391
  const consumer = consumerRef.current ??= createConsumer();
211
- const consumerId = consumer.id;
212
392
  consumer.select = options?.select;
213
393
  consumer.onMount = options?.onMount;
214
394
  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;
229
- 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, {
234
- canCreate: true,
235
- countRef: false,
236
- args: effectiveArgs,
237
- sweepIfUnowned: true
238
- });
239
- const onDepHandle = (handle) => {
240
- const cache = consumer.depWrappers ??= /* @__PURE__ */ new Map();
241
- const cached = cache.get(handle);
242
- if (cached !== void 0) return cached;
243
- const wrapper = makeDepWrapper(handle, registry, consumer, onDepHandle);
244
- cache.set(handle, wrapper);
245
- return wrapper;
246
- };
247
- const { proxy } = buildTrackedProxy(instance, consumer.tracked, onDepHandle);
248
- return {
249
- bloc: instance,
250
- instanceKey: resolvedKey,
251
- trackedBloc: proxy
252
- };
253
- }, [
254
- BlocClass,
255
- ownArgsKey,
256
- providerArgsKey,
257
- rebindNonce,
258
- registry
259
- ]);
260
- consumer.container = bloc;
261
- useSyncExternalStore(useMemo(() => (onStoreChange) => {
262
- consumer.notify = onStoreChange;
263
- const container = consumer.container;
264
- const unsubscribe = container.channel.subscribe(() => consumer.interest, () => {
265
- if (consumer.isSelectMode) {
266
- const select = consumer.select;
267
- if (select) {
268
- const next = select(container.state, container);
269
- const prev = consumer.selection;
270
- if (prev !== null && shallowArrayEqual(prev, next)) return;
271
- consumer.selection = next;
272
- }
273
- }
274
- consumer.bump();
275
- });
276
- container.registerConsumerPaths(consumerId, consumer.paths);
277
- return () => {
278
- consumer.notify = noop;
279
- unsubscribe();
280
- container.unregisterConsumer(consumerId);
281
- };
282
- }, [
283
- BlocClass,
284
- instanceKey,
285
- consumerId,
286
- rebindNonce
287
- ]), consumer.getSnapshot, consumer.getServerSnapshot);
288
- useLayoutEffect(() => {
289
- const live = registry.acquire(BlocClass, instanceKey, {
290
- canCreate: true,
291
- countRef: true,
292
- refId: consumer.primaryRefId,
293
- args: resolveEffectiveArgs(consumer)
294
- });
295
- consumer.ownedBloc = live;
296
- consumer.onMount?.(live);
297
- if (live !== bloc) {
298
- consumer.rebindNonce++;
299
- consumer.bump();
300
- }
301
- return () => {
302
- consumer.onUnmount?.(consumer.ownedBloc ?? bloc);
303
- registry.release(BlocClass, instanceKey, false, consumer.primaryRefId);
304
- };
305
- }, [
306
- BlocClass,
307
- instanceKey,
308
- consumerId,
309
- registry
310
- ]);
395
+ consumer.ownArgs = options?.args;
396
+ consumer.providerArgs = useProvidedArgs(BlocClass);
397
+ const effectiveArgs = resolveEffectiveArgs(consumer);
398
+ const instanceKey = registry.resolveKey(BlocClass, void 0, effectiveArgs);
399
+ if (consumer.BlocClass !== BlocClass || consumer.instanceKey !== instanceKey || consumer.registry !== registry || consumer.boundNonce !== consumer.rebindNonce) bind(consumer, BlocClass, instanceKey, registry, effectiveArgs);
400
+ useSyncExternalStore(consumer.subscribe, consumer.getSnapshot, consumer.getSnapshot);
311
401
  const container = consumer.container;
312
402
  const rawState = container.state;
403
+ consumer.renderedState = rawState;
313
404
  const select = consumer.select;
314
405
  consumer.isSelectMode = select !== void 0;
315
406
  let state;
316
407
  if (select !== void 0) {
317
408
  state = rawState;
318
- if (consumer.selection === null) consumer.selection = select(rawState, bloc);
319
- consumer.interest = ALL_PATHS;
409
+ consumer.selection = select(rawState, container);
410
+ setInterest(consumer, ALL_PATHS);
320
411
  } else {
321
412
  const tracked = trackRender(rawState, container.interner, consumer.proxyCache ??= new ProxyCache());
322
413
  state = tracked.value;
323
414
  consumer.tracked.current = tracked.value;
324
415
  consumer.paths = tracked.paths;
325
416
  consumer.disarm = tracked.disarm;
326
- const session = consumer.session;
327
- session.clear();
328
- session.set(container, {
329
- kind: "primary",
330
- paths: tracked.paths
331
- });
417
+ consumer.deps.begin(container, tracked.paths);
332
418
  }
333
- useLayoutEffect(() => {
334
- consumer.tracked.current = null;
335
- const disarm = consumer.disarm;
336
- if (disarm !== null) {
337
- consumer.disarm = null;
338
- disarm();
339
- }
340
- if (consumer.isSelectMode) {
341
- consumer.lastReconcile = null;
342
- return;
343
- }
344
- 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
- };
410
- });
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]);
422
- return [state, trackedBloc];
419
+ useLayoutEffect(consumer.commit);
420
+ useLayoutEffect(consumer.mount, NO_DEPS);
421
+ return [state, consumer.trackedBloc];
423
422
  }
424
423
  const noop = () => {};
424
+ const NO_PATHS = emptyPathSet();
425
+ const NO_DEPS = [];
425
426
  function createConsumer() {
426
427
  const id = `useBloc-${nextConsumerId++}`;
427
428
  const consumer = {
428
429
  id,
429
430
  primaryRefId: `useBloc@${id}`,
430
- depRefId: `useBloc@${id}:dep`,
431
+ BlocClass: null,
432
+ instanceKey: "",
433
+ registry: null,
431
434
  container: null,
435
+ trackedBloc: null,
436
+ subscribe: null,
437
+ boundNonce: 0,
438
+ rebindNonce: 0,
439
+ renderedState: void 0,
432
440
  version: 0,
433
441
  notify: noop,
434
442
  bump: () => {
@@ -436,164 +444,159 @@ function createConsumer() {
436
444
  consumer.notify();
437
445
  },
438
446
  getSnapshot: () => consumer.version,
439
- getServerSnapshot: () => consumer.version,
440
- interest: emptyPathSet(),
441
- paths: emptyPathSet(),
447
+ onChange: () => onChange(consumer),
448
+ unsubscribe: () => unsubscribe(consumer),
449
+ commit: () => commit(consumer),
450
+ mount: () => teardown,
451
+ interest: NO_PATHS,
452
+ subscription: null,
453
+ subscribedTo: null,
454
+ paths: NO_PATHS,
442
455
  isSelectMode: false,
443
456
  selection: null,
444
- session: /* @__PURE__ */ new Map(),
445
- depSubs: /* @__PURE__ */ new Map(),
446
- lastReconcile: null,
457
+ deps: null,
447
458
  select: void 0,
448
459
  onMount: void 0,
449
460
  onUnmount: void 0,
450
461
  ownArgs: void 0,
451
- ownArgsKeyFor: ARGS_UNSET,
452
- ownArgsKey: void 0,
453
462
  providerArgs: void 0,
454
- providerArgsKeyFor: ARGS_UNSET,
455
- providerArgsKey: void 0,
456
463
  tracked: { current: null },
457
464
  disarm: null,
458
465
  proxyCache: null,
459
- depWrappers: null,
460
- rebindNonce: 0,
466
+ ownedClass: null,
467
+ ownedKey: "",
468
+ ownedRegistry: null,
461
469
  ownedBloc: null
462
470
  };
471
+ const teardown = () => {
472
+ release(consumer);
473
+ consumer.deps.dispose();
474
+ };
475
+ consumer.deps = new DepSession(id, consumer.bump);
463
476
  return consumer;
464
477
  }
465
- 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
- };
478
+ function bind(consumer, BlocClass, instanceKey, registry, args) {
479
+ const instance = registry.acquire(BlocClass, instanceKey, {
480
+ canCreate: true,
481
+ countRef: false,
482
+ args,
483
+ sweepIfUnowned: true
484
+ });
485
+ let depWrappers;
486
+ const onDepHandle = (handle) => {
487
+ depWrappers ??= /* @__PURE__ */ new Map();
488
+ const cached = depWrappers.get(handle);
489
+ if (cached !== void 0) return cached;
490
+ const wrapper = makeDepWrapper(handle, registry, consumer.deps, consumer.tracked, onDepHandle);
491
+ depWrappers.set(handle, wrapper);
492
+ return wrapper;
501
493
  };
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];
494
+ consumer.BlocClass = BlocClass;
495
+ consumer.instanceKey = instanceKey;
496
+ consumer.registry = registry;
497
+ consumer.boundNonce = consumer.rebindNonce;
498
+ consumer.container = instance;
499
+ consumer.trackedBloc = buildTrackedProxy(instance, consumer.tracked, onDepHandle);
500
+ consumer.subscribe = (onStoreChange) => subscribe(consumer, onStoreChange);
501
+ }
502
+ function subscribe(consumer, onStoreChange) {
503
+ consumer.notify = onStoreChange;
504
+ const container = consumer.container;
505
+ const subscription = container.subscribePaths(consumer.onChange);
506
+ subscription.setInterest(consumer.interest);
507
+ consumer.subscription = subscription;
508
+ consumer.subscribedTo = container;
509
+ container.registerConsumerPaths(consumer.id, consumer.paths);
510
+ return consumer.unsubscribe;
511
+ }
512
+ function unsubscribe(consumer) {
513
+ consumer.notify = noop;
514
+ consumer.subscription?.unsubscribe();
515
+ consumer.subscription = null;
516
+ consumer.subscribedTo?.unregisterConsumer(consumer.id);
517
+ consumer.subscribedTo = null;
518
+ }
519
+ function onChange(consumer) {
520
+ if (consumer.isSelectMode) {
521
+ const select = consumer.select;
522
+ const container = consumer.subscribedTo;
523
+ if (select && container !== null) {
524
+ const next = select(container.state, container);
525
+ const prev = consumer.selection;
526
+ if (prev !== null && shallowArrayEqual(prev, next)) return;
527
+ consumer.selection = next;
529
528
  }
530
- };
531
- Object.defineProperty(wrapper, DEP_BRAND, {
532
- value: brand,
533
- enumerable: false,
534
- writable: false,
535
- configurable: false
529
+ }
530
+ consumer.bump();
531
+ }
532
+ function claim(consumer) {
533
+ const { BlocClass, instanceKey, registry } = consumer;
534
+ if (consumer.ownedClass === BlocClass && consumer.ownedKey === instanceKey && consumer.ownedRegistry === registry) return;
535
+ release(consumer);
536
+ const live = registry.acquire(BlocClass, instanceKey, {
537
+ canCreate: true,
538
+ countRef: true,
539
+ refId: consumer.primaryRefId,
540
+ args: resolveEffectiveArgs(consumer)
536
541
  });
537
- return wrapper;
542
+ consumer.ownedClass = BlocClass;
543
+ consumer.ownedKey = instanceKey;
544
+ consumer.ownedRegistry = registry;
545
+ consumer.ownedBloc = live;
546
+ consumer.onMount?.(live);
547
+ if (live !== consumer.container) {
548
+ consumer.rebindNonce++;
549
+ consumer.bump();
550
+ }
538
551
  }
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;
552
+ function release(consumer) {
553
+ const { ownedClass, ownedRegistry } = consumer;
554
+ if (ownedClass === null || ownedRegistry === null) return;
555
+ consumer.onUnmount?.(consumer.ownedBloc);
556
+ consumer.ownedClass = null;
557
+ consumer.ownedRegistry = null;
558
+ ownedRegistry.release(ownedClass, consumer.ownedKey, false, consumer.primaryRefId);
545
559
  }
560
+ function commit(consumer) {
561
+ claim(consumer);
562
+ consumer.tracked.current = null;
563
+ const disarm = consumer.disarm;
564
+ if (disarm !== null) {
565
+ consumer.disarm = null;
566
+ disarm();
567
+ }
568
+ const deps = consumer.deps;
569
+ deps.disarm();
570
+ if (consumer.isSelectMode) {
571
+ deps.invalidate();
572
+ return;
573
+ }
574
+ const primary = consumer.container;
575
+ if (primary.state !== consumer.renderedState) consumer.bump();
576
+ if (deps.isUnchanged()) return;
577
+ primary.registerConsumerPaths(consumer.id, consumer.paths);
578
+ setInterest(consumer, consumer.paths);
579
+ deps.reconcile(consumer.registry);
580
+ }
581
+ const setInterest = (consumer, interest) => {
582
+ consumer.interest = interest;
583
+ consumer.subscription?.setInterest(interest);
584
+ };
585
+ const resolveEffectiveArgs = (consumer) => consumer.ownArgs !== void 0 ? consumer.ownArgs : consumer.providerArgs;
546
586
  const shallowArrayEqual = (a, b) => {
547
587
  if (a === b) return true;
548
588
  if (a.length !== b.length) return false;
549
589
  for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
550
590
  return true;
551
591
  };
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
592
  //#endregion
590
593
  //#region src/useBlocDeps.ts
591
594
  /**
592
595
  * Feed live values from the component into a bloc's `deps` lane.
593
596
  *
594
597
  * 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`.
598
+ * bloc's merged `deps` view and withdrawn on unmount. The bloc observes
599
+ * changes through `onDepsChanged`.
597
600
  *
598
601
  * ```tsx
599
602
  * const [state, bloc] = useBloc(CanvasCubit);
@@ -619,6 +622,6 @@ function useBlocDeps(bloc, slice) {
619
622
  }, [bloc, ownerId]);
620
623
  }
621
624
  //#endregion
622
- export { BlocProvider, RegistryProvider, untracked, useBloc, useBlocDeps, useProvidedArgs };
625
+ export { BlocProvider, RegistryProvider, untracked, useBloc, useBlocDeps, useProvidedArgs, useRegistry };
623
626
 
624
627
  //# sourceMappingURL=index.js.map