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